@ferris1225/pi-subagents 4.2.5 → 4.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -346
- package/agents/executor.md +53 -53
- package/package.json +3 -5
- package/src/dispatch.ts +552 -541
- package/src/durable.ts +517 -510
- package/src/monitor.ts +1 -1
- package/src/prompt.ts +69 -69
- package/src/rpc-run.ts +987 -993
- package/src/runtime.ts +315 -312
- package/src/thread-lifecycle.ts +1341 -1324
- package/src/tools.ts +384 -384
package/src/runtime.ts
CHANGED
|
@@ -1,312 +1,315 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared per-session runtime state for pi-subagents.
|
|
3
|
-
*
|
|
4
|
-
* The extension registers several tools (subagent, subagent_control/stop)
|
|
5
|
-
* that share the background queue, completion batcher, abort controllers per
|
|
6
|
-
* run, and settled-results store.
|
|
7
|
-
* `createRuntime` builds those once per extension load and hands the same object
|
|
8
|
-
* to every registration site, so state stays in one place without globals.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { rmSync } from "node:fs";
|
|
13
|
-
import { resolveSubagentConcurrency, BackgroundTaskQueue } from "./background.ts";
|
|
14
|
-
import {
|
|
15
|
-
completionGroupTriggersTurn,
|
|
16
|
-
createCompletionBatcher,
|
|
17
|
-
formatActiveRunsFooter,
|
|
18
|
-
formatCompletionMessage,
|
|
19
|
-
type CompletionBatcher,
|
|
20
|
-
type CompletionMessageItem,
|
|
21
|
-
} from "./completion.ts";
|
|
22
|
-
import { type ThinkingLevel } from "./config.ts";
|
|
23
|
-
import { removeThreadRecord, threadRecordFromThread, upsertThreadRecord, type ThreadRecord } from "./durable.ts";
|
|
24
|
-
import { isRunActiveStatus, monitor } from "./monitor.ts";
|
|
25
|
-
import type { RpcRunControl } from "./rpc-run.ts";
|
|
26
|
-
import type { StartBackgroundInternal } from "./thread-lifecycle.ts";
|
|
27
|
-
import { isFailedResult, type SingleResult } from "./spawn.ts";
|
|
28
|
-
import type { IsolationMode, WorktreeFinalization, WorktreeIsolation } from "./worktree.ts";
|
|
29
|
-
|
|
30
|
-
export type ThreadState =
|
|
31
|
-
| "queued"
|
|
32
|
-
| "resuming"
|
|
33
|
-
| "running"
|
|
34
|
-
| "interrupting"
|
|
35
|
-
| "parked"
|
|
36
|
-
| "completed"
|
|
37
|
-
| "failed"
|
|
38
|
-
| "stopped";
|
|
39
|
-
|
|
40
|
-
export type ThreadLifecycleOperation = "park" | "resume" | "stop" | "settle";
|
|
41
|
-
|
|
42
|
-
export interface SubagentThread {
|
|
43
|
-
id: number;
|
|
44
|
-
generation: number;
|
|
45
|
-
agentName: string;
|
|
46
|
-
task: string;
|
|
47
|
-
/** Caller-facing cwd in the original worktree. */
|
|
48
|
-
cwd: string;
|
|
49
|
-
/** Actual child cwd (the equivalent path inside an isolated worktree). */
|
|
50
|
-
executionCwd: string;
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
thread.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
thread.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
runtime.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared per-session runtime state for pi-subagents.
|
|
3
|
+
*
|
|
4
|
+
* The extension registers several tools (subagent, subagent_control/stop)
|
|
5
|
+
* that share the background queue, completion batcher, abort controllers per
|
|
6
|
+
* run, and settled-results store.
|
|
7
|
+
* `createRuntime` builds those once per extension load and hands the same object
|
|
8
|
+
* to every registration site, so state stays in one place without globals.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { rmSync } from "node:fs";
|
|
13
|
+
import { resolveSubagentConcurrency, BackgroundTaskQueue } from "./background.ts";
|
|
14
|
+
import {
|
|
15
|
+
completionGroupTriggersTurn,
|
|
16
|
+
createCompletionBatcher,
|
|
17
|
+
formatActiveRunsFooter,
|
|
18
|
+
formatCompletionMessage,
|
|
19
|
+
type CompletionBatcher,
|
|
20
|
+
type CompletionMessageItem,
|
|
21
|
+
} from "./completion.ts";
|
|
22
|
+
import { type ThinkingLevel } from "./config.ts";
|
|
23
|
+
import { removeThreadRecord, threadRecordFromThread, upsertThreadRecord, type ThreadRecord } from "./durable.ts";
|
|
24
|
+
import { isRunActiveStatus, monitor } from "./monitor.ts";
|
|
25
|
+
import type { RpcRunControl } from "./rpc-run.ts";
|
|
26
|
+
import type { StartBackgroundInternal } from "./thread-lifecycle.ts";
|
|
27
|
+
import { isFailedResult, type SingleResult } from "./spawn.ts";
|
|
28
|
+
import type { IsolationMode, WorktreeFinalization, WorktreeIsolation } from "./worktree.ts";
|
|
29
|
+
|
|
30
|
+
export type ThreadState =
|
|
31
|
+
| "queued"
|
|
32
|
+
| "resuming"
|
|
33
|
+
| "running"
|
|
34
|
+
| "interrupting"
|
|
35
|
+
| "parked"
|
|
36
|
+
| "completed"
|
|
37
|
+
| "failed"
|
|
38
|
+
| "stopped";
|
|
39
|
+
|
|
40
|
+
export type ThreadLifecycleOperation = "park" | "resume" | "stop" | "settle";
|
|
41
|
+
|
|
42
|
+
export interface SubagentThread {
|
|
43
|
+
id: number;
|
|
44
|
+
generation: number;
|
|
45
|
+
agentName: string;
|
|
46
|
+
task: string;
|
|
47
|
+
/** Caller-facing cwd in the original worktree. */
|
|
48
|
+
cwd: string;
|
|
49
|
+
/** Actual child cwd (the equivalent path inside an isolated worktree). */
|
|
50
|
+
executionCwd: string;
|
|
51
|
+
/** Level actually used, after clamping to the effective model's capability. */
|
|
52
|
+
thinkingLevel?: ThinkingLevel;
|
|
53
|
+
/** Level the dispatch asked for, before clamping; replayed on every resume. */
|
|
54
|
+
requestedThinkingLevel?: ThinkingLevel;
|
|
55
|
+
isolation: IsolationMode;
|
|
56
|
+
worktree?: WorktreeIsolation;
|
|
57
|
+
state: ThreadState;
|
|
58
|
+
control: RpcRunControl;
|
|
59
|
+
queueController?: AbortController;
|
|
60
|
+
/** Resolves only after the current generation's child process, isolation
|
|
61
|
+
* finalization, and queue work have fully quiesced and released their
|
|
62
|
+
* concurrency slot. */
|
|
63
|
+
generationCompletion: Promise<void>;
|
|
64
|
+
/** Synchronous CAS used by lifecycle controls across their async preflight. */
|
|
65
|
+
lifecycleVersion: number;
|
|
66
|
+
lifecycleOperation?: ThreadLifecycleOperation;
|
|
67
|
+
sessionId?: string;
|
|
68
|
+
sessionDir?: string;
|
|
69
|
+
/** Active execution time accumulated across retained resume generations. */
|
|
70
|
+
elapsedMs: number;
|
|
71
|
+
/** Most recent generation result, retained for parked destructive-stop output. */
|
|
72
|
+
lastResult?: SingleResult;
|
|
73
|
+
/** A destructive stop retires context even if the active child settles later. */
|
|
74
|
+
retireOnSettle?: boolean;
|
|
75
|
+
retired?: boolean;
|
|
76
|
+
/** Installed by dispatch so the control tool can restart the same logical id. */
|
|
77
|
+
resume: (objective?: string, ctx?: ExtensionContext) => Promise<SingleResult>;
|
|
78
|
+
/** Dispatch-owned, generation-guarded worktree settlement hook. Its apply
|
|
79
|
+
* runs under the canonical original-repository lane. */
|
|
80
|
+
finalizeIsolation: (generation: number, result?: SingleResult) => Promise<WorktreeFinalization | undefined>;
|
|
81
|
+
/** Best-effort shutdown notification for retained integration artifacts. */
|
|
82
|
+
notifyIsolationFailure?: (finalization: WorktreeFinalization) => void;
|
|
83
|
+
isolationFailureNotified?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SubagentRuntime {
|
|
87
|
+
configPath: string;
|
|
88
|
+
backgroundQueue: BackgroundTaskQueue;
|
|
89
|
+
/** Live parent tool names from ExtensionAPI, read again for each child launch. */
|
|
90
|
+
getActiveTools: () => string[];
|
|
91
|
+
/** False after session_shutdown; guards delivery and queue work. */
|
|
92
|
+
sessionActive: boolean;
|
|
93
|
+
/** The process-wide background dispatcher. Set at tool registration so
|
|
94
|
+
* threads restored from the durable manifest can resume before any dispatch. */
|
|
95
|
+
dispatcher?: StartBackgroundInternal;
|
|
96
|
+
/** Resolves when the load-time durable restore pass has finished. Everything
|
|
97
|
+
* that answers "which threads exist" awaits it — the lookup tools, a fresh
|
|
98
|
+
* dispatch before it allocates a run id, and the restored-thread notice — so
|
|
99
|
+
* a reload can never report parked work as missing, or hand a new run an id a
|
|
100
|
+
* record still owns, while the manifest is being read. Resolved by default;
|
|
101
|
+
* `bootstrapDurableState` publishes the real pass. */
|
|
102
|
+
durableRestore: Promise<void>;
|
|
103
|
+
/** Run ids restored from the durable manifest at load; consumed by the
|
|
104
|
+
* one-time session-start notice. */
|
|
105
|
+
restoredRunIds: number[];
|
|
106
|
+
restoredNotified: boolean;
|
|
107
|
+
/** Deliver a batch of completion messages to the main window, waking it only
|
|
108
|
+
* when the batch needs a turn. */
|
|
109
|
+
sendCompletionGroup: (items: CompletionMessageItem[]) => void;
|
|
110
|
+
completionBatcher: CompletionBatcher<CompletionMessageItem>;
|
|
111
|
+
/** Abort controllers per active run, so subagent_stop can cancel a run in-turn. */
|
|
112
|
+
runControllers: Map<number, AbortController>;
|
|
113
|
+
/** Final results keyed by run id, so a dispatch with wait: true can hand the
|
|
114
|
+
* model the actual result in-turn instead of it sleeping/polling for a
|
|
115
|
+
* wake-up message. */
|
|
116
|
+
settledRuns: Map<number, SingleResult>;
|
|
117
|
+
settledListeners: Map<number, Set<(result: SingleResult) => void>>;
|
|
118
|
+
registerRunResult: (runId: number, result: SingleResult) => void;
|
|
119
|
+
/** Logical threads outlive process attempts and completed generations. */
|
|
120
|
+
threads: Map<number, SubagentThread>;
|
|
121
|
+
/** Resume setup that has claimed a thread but has not yet enqueued its
|
|
122
|
+
* next generation. Shutdown invalidates these claims and waits for cleanup. */
|
|
123
|
+
preflightOperations: Set<Promise<void>>;
|
|
124
|
+
/** Every session directory retained for this parent session. */
|
|
125
|
+
sessionDirs: Set<string>;
|
|
126
|
+
retainSession: (result: Pick<SingleResult, "sessionDir">) => void;
|
|
127
|
+
retireThreadSession: (thread: SubagentThread) => void;
|
|
128
|
+
/** Flip sessionActive off and release all session-scoped resources. */
|
|
129
|
+
shutdown: () => Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRuntime {
|
|
133
|
+
const backgroundQueue = new BackgroundTaskQueue(resolveSubagentConcurrency());
|
|
134
|
+
|
|
135
|
+
const runtime: SubagentRuntime = {
|
|
136
|
+
configPath,
|
|
137
|
+
backgroundQueue,
|
|
138
|
+
getActiveTools: () => pi.getActiveTools(),
|
|
139
|
+
sessionActive: true,
|
|
140
|
+
durableRestore: Promise.resolve(),
|
|
141
|
+
restoredRunIds: [],
|
|
142
|
+
restoredNotified: false,
|
|
143
|
+
sendCompletionGroup: (items) => {
|
|
144
|
+
if (!runtime.sessionActive || items.length === 0) return;
|
|
145
|
+
// A result arriving for one run does not mean sibling runs are done.
|
|
146
|
+
// Computing this at delivery (emit) time — not when the item was
|
|
147
|
+
// pushed — reflects the current monitor state, since finishing runs
|
|
148
|
+
// are removed from the monitor before their completion is pushed.
|
|
149
|
+
const active = monitor
|
|
150
|
+
.getRuns()
|
|
151
|
+
.filter((run) => isRunActiveStatus(run.status))
|
|
152
|
+
.map((run) => ({
|
|
153
|
+
id: run.id,
|
|
154
|
+
agent: run.agent,
|
|
155
|
+
label: run.label,
|
|
156
|
+
...(run.status === "queued" && run.waitReason ? { wait: run.waitReason } : {}),
|
|
157
|
+
}));
|
|
158
|
+
const message = {
|
|
159
|
+
customType: "subagent-result",
|
|
160
|
+
content: formatCompletionMessage(items) + formatActiveRunsFooter(active),
|
|
161
|
+
display: true,
|
|
162
|
+
};
|
|
163
|
+
if (completionGroupTriggersTurn(items)) {
|
|
164
|
+
// steer: the result is injected after the current tool call even mid-turn,
|
|
165
|
+
// or starts a new turn when idle. followUp would sit in the queue until the
|
|
166
|
+
// whole turn ends — a main agent waiting for the result (sleep/poll) would
|
|
167
|
+
// never see it delivered, which is exactly the "returned but never woken"
|
|
168
|
+
// failure mode.
|
|
169
|
+
pi.sendMessage(message, { deliverAs: "steer", triggerTurn: true });
|
|
170
|
+
} else {
|
|
171
|
+
// No-wake delivery: nextTurn rides along with the next user turn and can
|
|
172
|
+
// never start a continuation by itself. followUp would auto-continue
|
|
173
|
+
// whenever pi is already streaming, defeating the opt-out.
|
|
174
|
+
pi.sendMessage(message, { deliverAs: "nextTurn" });
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
completionBatcher: undefined as unknown as CompletionBatcher<CompletionMessageItem>,
|
|
178
|
+
runControllers: new Map<number, AbortController>(),
|
|
179
|
+
settledRuns: new Map<number, SingleResult>(),
|
|
180
|
+
settledListeners: new Map<number, Set<(result: SingleResult) => void>>(),
|
|
181
|
+
threads: new Map<number, SubagentThread>(),
|
|
182
|
+
preflightOperations: new Set<Promise<void>>(),
|
|
183
|
+
sessionDirs: new Set<string>(),
|
|
184
|
+
retainSession: (result) => {
|
|
185
|
+
if (result.sessionDir) runtime.sessionDirs.add(result.sessionDir);
|
|
186
|
+
},
|
|
187
|
+
retireThreadSession: (thread) => {
|
|
188
|
+
thread.retired = true;
|
|
189
|
+
if (!thread.sessionDir) return;
|
|
190
|
+
const sessionDir = thread.sessionDir;
|
|
191
|
+
try {
|
|
192
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
193
|
+
runtime.sessionDirs.delete(sessionDir);
|
|
194
|
+
thread.sessionDir = undefined;
|
|
195
|
+
thread.sessionId = undefined;
|
|
196
|
+
} catch {
|
|
197
|
+
/* best-effort; shutdown retries the still-retained directory */
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
registerRunResult: (runId, result) => {
|
|
201
|
+
runtime.settledRuns.set(runId, result);
|
|
202
|
+
const listeners = runtime.settledListeners.get(runId);
|
|
203
|
+
if (listeners) {
|
|
204
|
+
runtime.settledListeners.delete(runId);
|
|
205
|
+
for (const listener of listeners) {
|
|
206
|
+
try {
|
|
207
|
+
listener(result);
|
|
208
|
+
} catch {
|
|
209
|
+
/* listener errors must never break settling */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
shutdown: async () => {
|
|
215
|
+
if (!runtime.sessionActive) return;
|
|
216
|
+
runtime.sessionActive = false;
|
|
217
|
+
const shutdownThreads = [...runtime.threads.values()];
|
|
218
|
+
const liveStates = new Set(["queued", "resuming", "running", "interrupting"]);
|
|
219
|
+
const previousStates = new Map(shutdownThreads.map((thread) => [thread.id, thread.state] as const));
|
|
220
|
+
// Invalidate every lifecycle claim synchronously before the first await.
|
|
221
|
+
// Resume preflight checks both this version and sessionActive, then
|
|
222
|
+
// cleans any worktree/session it created before resolving its tracker.
|
|
223
|
+
// A generation already inside its settlement keeps its own claim: it
|
|
224
|
+
// finalizes its worktree and persists its terminal record itself.
|
|
225
|
+
const interrupting = shutdownThreads.filter((thread) =>
|
|
226
|
+
!thread.retired &&
|
|
227
|
+
thread.lifecycleOperation !== "settle" &&
|
|
228
|
+
liveStates.has(thread.state),
|
|
229
|
+
);
|
|
230
|
+
for (const thread of shutdownThreads) {
|
|
231
|
+
thread.lifecycleVersion++;
|
|
232
|
+
if (thread.retired) {
|
|
233
|
+
thread.lifecycleOperation = "stop";
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (thread.lifecycleOperation === "settle") continue;
|
|
237
|
+
thread.lifecycleOperation = "stop";
|
|
238
|
+
// Deliberately NOT retireOnSettle: shutdown interrupts to the last
|
|
239
|
+
// checkpoint but keeps the session/worktree resumable across reload.
|
|
240
|
+
thread.retireOnSettle = false;
|
|
241
|
+
if (liveStates.has(thread.state)) thread.state = "stopped";
|
|
242
|
+
}
|
|
243
|
+
const preflights = [...runtime.preflightOperations];
|
|
244
|
+
runtime.completionBatcher.dispose();
|
|
245
|
+
runtime.backgroundQueue.cancelAll();
|
|
246
|
+
// Await live RPC process-tree cleanup and continuation preflight rollback
|
|
247
|
+
// before persisting records or releasing ownership maps.
|
|
248
|
+
await Promise.all([
|
|
249
|
+
Promise.all(
|
|
250
|
+
interrupting.map((thread) =>
|
|
251
|
+
thread.control.stop("Parent session shut down").catch(() => undefined),
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
Promise.allSettled(preflights),
|
|
255
|
+
runtime.backgroundQueue.waitForIdle(),
|
|
256
|
+
]);
|
|
257
|
+
// Only interrupted (parked) threads stay resumable across reloads:
|
|
258
|
+
// each keeps its durable record and retained artifacts. Settled
|
|
259
|
+
// threads drop their record — the manifest exists only while
|
|
260
|
+
// unfinished work needs it — and their sessions are deleted now. A
|
|
261
|
+
// thread whose settlement finished during the wait above already
|
|
262
|
+
// wrote (or removed) its own record; the lastResult-derived state
|
|
263
|
+
// below matches it.
|
|
264
|
+
const settled: Array<{ runId: number; cwd: string }> = [];
|
|
265
|
+
const records: ThreadRecord[] = [];
|
|
266
|
+
for (const thread of runtime.threads.values()) {
|
|
267
|
+
if (thread.retired) continue;
|
|
268
|
+
const previous = previousStates.get(thread.id) ?? thread.state;
|
|
269
|
+
let state: "parked" | "completed" | "failed";
|
|
270
|
+
if (previous === "completed" || previous === "failed") {
|
|
271
|
+
state = previous;
|
|
272
|
+
} else if (thread.lifecycleOperation === "settle" && thread.lastResult) {
|
|
273
|
+
state = isFailedResult(thread.lastResult) ? "failed" : "completed";
|
|
274
|
+
} else {
|
|
275
|
+
state = "parked";
|
|
276
|
+
}
|
|
277
|
+
if (state === "parked") records.push(threadRecordFromThread(thread, state));
|
|
278
|
+
else settled.push({ runId: thread.id, cwd: thread.cwd });
|
|
279
|
+
}
|
|
280
|
+
await Promise.all([
|
|
281
|
+
...records.map((record) => upsertThreadRecord(runtime.configPath, record).catch(() => undefined)),
|
|
282
|
+
...settled.map(({ runId, cwd }) => removeThreadRecord(runtime.configPath, runId, cwd).catch(() => undefined)),
|
|
283
|
+
]);
|
|
284
|
+
// Retained-failure recovery records are persisted by the finalization
|
|
285
|
+
// itself; shutdown only drops sessions no record claims anymore.
|
|
286
|
+
const referenced = new Set(
|
|
287
|
+
records.flatMap((record) =>
|
|
288
|
+
[record.sessionDir, record.worktree?.tempDir].filter(Boolean) as string[],
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
for (const sessionDir of runtime.sessionDirs) {
|
|
292
|
+
if (referenced.has(sessionDir)) continue;
|
|
293
|
+
try {
|
|
294
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
295
|
+
} catch {
|
|
296
|
+
/* best-effort; the state-root sweep catches leftovers later */
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
runtime.settledRuns.clear();
|
|
300
|
+
runtime.settledListeners.clear();
|
|
301
|
+
runtime.runControllers.clear();
|
|
302
|
+
// sessionDirs entries still referenced by records stay owned by the
|
|
303
|
+
// manifest; the next process re-registers them at restore.
|
|
304
|
+
runtime.sessionDirs.clear();
|
|
305
|
+
runtime.preflightOperations.clear();
|
|
306
|
+
runtime.threads.clear();
|
|
307
|
+
monitor.clear();
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
runtime.completionBatcher = createCompletionBatcher<CompletionMessageItem>({
|
|
312
|
+
emit: runtime.sendCompletionGroup,
|
|
313
|
+
});
|
|
314
|
+
return runtime;
|
|
315
|
+
}
|