-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstate.ts
More file actions
109 lines (97 loc) · 3.86 KB
/
state.ts
File metadata and controls
109 lines (97 loc) · 3.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
* Shared mutable state for the agenticoding extension.
*
* Single source of truth that all modules read/write through.
* Mutable by design — this is session-scoped imperative state.
*/
import type { AgentSession } from "@earendil-works/pi-coding-agent";
export interface AgenticodingState {
/** Compact ledger entries keyed by kebab-case name */
ledger: Map<string, string>;
/** Monotonically increasing epoch, set on first ledger_add */
epoch: number;
/** Last context usage percent from getContextUsage() */
lastContextPercent: number | null;
/** Handoff task queued by the tool until the compaction hook consumes it. */
pendingHandoff: { task: string; source: "tool" } | null;
/** User-requested handoff that must result in a real tool-driven compaction. */
pendingRequestedHandoff: {
direction: string;
enforcementAttempts: number;
toolCalled: boolean;
} | null;
/**
* Published child agent sessions keyed by toolCallId.
* Lifecycle: executeSpawn publishes → renderSpawnResult claims via get+delete.
* This is only the render handoff queue, not the full live-session registry.
*/
childSessions: Map<string, AgentSession>;
/**
* All live child agent sessions keyed by toolCallId, including claimed ones.
* Reset/teardown aborts this registry so claimed children cannot outlive /new or UI disposal.
* Completed children remove themselves from this registry before returning.
*
* INVARIANT: This Map is never replaced — only cleared via .clear().
* Spawn renderer ownership checks read this registry after attach, so its
* identity must stay stable across resets, completion cleanup, and disposal.
*/
liveChildSessions: Map<string, AgentSession>;
/**
* Generation counter for child-session ownership.
* Increment on /new so stale child updates/results cannot touch fresh state.
*/
childSessionEpoch: number;
}
/** Create a fresh state instance. Call reset() on /new. */
export function createState(): AgenticodingState {
const childSessions = new Map<string, AgentSession>();
const liveChildSessions = new Map<string, AgentSession>();
const state: AgenticodingState = {
ledger: new Map(),
epoch: 0,
lastContextPercent: null,
pendingHandoff: null,
pendingRequestedHandoff: null,
childSessions,
liveChildSessions,
childSessionEpoch: 0,
};
// Prevent replacement — spawn lifecycle code and renderer ownership checks
// depend on stable map identity. Only .clear() and .delete() are valid —
// assigning a new Map would silently break child-session invalidation.
Object.defineProperty(state, 'childSessions', {
get: () => childSessions,
set: () => { throw new Error('childSessions cannot be replaced — use .clear() instead'); },
enumerable: true,
configurable: false,
});
Object.defineProperty(state, 'liveChildSessions', {
get: () => liveChildSessions,
set: () => { throw new Error('liveChildSessions cannot be replaced — use .clear() instead'); },
enumerable: true,
configurable: false,
});
return state;
}
/** Reset all state. Used on /new or session reset. */
export function resetState(state: AgenticodingState): void {
state.childSessionEpoch++;
state.ledger.clear();
state.epoch = 0;
state.lastContextPercent = null;
state.pendingHandoff = null;
state.pendingRequestedHandoff = null;
abortAndClearChildSessions(state);
}
/** Abort all active child sessions and clear both registries. Called on /new (session reset). */
export function abortAndClearChildSessions(state: AgenticodingState): void {
const seen = new Map<any, string>(); // session → first id (for logging)
for (const [id, session] of [...state.childSessions.entries(), ...state.liveChildSessions.entries()]) {
if (!seen.has(session)) seen.set(session, id);
}
state.childSessions.clear();
state.liveChildSessions.clear();
for (const [session, id] of seen) {
session.abort().catch(e => console.warn("[spawn] abort failed:", id, e));
}
}