@hheei/pi-subagents 0.2.4 → 0.4.0
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 +35 -20
- package/dist/agent-resolver.d.ts +9 -0
- package/dist/agent-resolver.js +278 -51
- package/dist/builtin-agents.js +50 -37
- package/dist/child-bridge.d.ts +0 -8
- package/dist/child-bridge.js +60 -43
- package/dist/child-control.d.ts +6 -4
- package/dist/child-control.js +251 -47
- package/dist/child-process.d.ts +2 -0
- package/dist/child-process.js +24 -12
- package/dist/commands.d.ts +1 -4
- package/dist/commands.js +1 -52
- package/dist/domain.d.ts +27 -12
- package/dist/domain.js +2 -1
- package/dist/extension.d.ts +7 -6
- package/dist/extension.js +96 -112
- package/dist/host-adapter.d.ts +19 -0
- package/dist/host-adapter.js +140 -1
- package/dist/info.d.ts +9 -0
- package/dist/info.js +80 -0
- package/dist/launch-spec.d.ts +0 -1
- package/dist/launch-spec.js +38 -18
- package/dist/lease.d.ts +83 -0
- package/dist/lease.js +239 -0
- package/dist/mailbox.d.ts +46 -0
- package/dist/mailbox.js +368 -0
- package/dist/manager.d.ts +20 -20
- package/dist/manager.js +421 -398
- package/dist/process-lock.d.ts +11 -0
- package/dist/process-lock.js +55 -0
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +1 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +99 -82
- package/dist/rpc-events.d.ts +1 -1
- package/dist/rpc-events.js +2 -1
- package/dist/runtime.d.ts +8 -21
- package/dist/runtime.js +61 -83
- package/dist/session-bootstrap.d.ts +2 -1
- package/dist/session-bootstrap.js +22 -14
- package/dist/state-db.d.ts +52 -0
- package/dist/state-db.js +209 -0
- package/dist/state.d.ts +25 -0
- package/dist/state.js +166 -37
- package/dist/status.d.ts +14 -0
- package/dist/status.js +36 -0
- package/dist/tool-activation.d.ts +3 -4
- package/dist/tool-activation.js +13 -215
- package/dist/tool-presentation.d.ts +14 -0
- package/dist/tool-presentation.js +193 -0
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +134 -180
- package/package.json +6 -6
- package/dist/bridge-client.d.ts +0 -55
- package/dist/bridge-client.js +0 -453
- package/dist/bridge-server.d.ts +0 -72
- package/dist/bridge-server.js +0 -490
- package/dist/session-fork.d.ts +0 -16
- package/dist/session-fork.js +0 -67
- package/dist/widget.d.ts +0 -12
- package/dist/widget.js +0 -149
package/dist/manager.js
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { errorMessage, isRecord } from "@hheei/pi-ext-core";
|
|
3
|
+
import { attachRetainedChild } from "./child-process.js";
|
|
4
|
+
import { isOperationError } from "./domain.js";
|
|
2
5
|
import { stripHindsightContent } from "./launch-spec.js";
|
|
6
|
+
import { commitLease, createConversationLease, RUNTIME_ENDED_RESUME, } from "./lease.js";
|
|
3
7
|
import { BridgeError, isChildInputPayload, isChildLifecyclePayload, isContactReportPayload, } from "./protocol.js";
|
|
4
8
|
import { findSessionFile, planSessionPlacement } from "./session-bootstrap.js";
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
async function readSessionJsonlEntries(filePath) {
|
|
10
|
+
const content = await readFile(filePath, "utf8");
|
|
11
|
+
const entries = [];
|
|
12
|
+
for (const line of content.split("\n")) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (trimmed.length > 0) {
|
|
15
|
+
try {
|
|
16
|
+
entries.push(JSON.parse(trimmed));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Ignore corrupted line
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return entries;
|
|
24
|
+
}
|
|
25
|
+
import { createStateProjector, parseTranscriptFacts, transcriptFacts, } from "./state.js";
|
|
7
26
|
/**
|
|
8
27
|
* How long an idle child keeps its runtime. A background child holds a whole Pi process and its
|
|
9
|
-
* session lock, so an idle one is reclaimed after this window and a later `
|
|
28
|
+
* session lock, so an idle one is reclaimed after this window and a later `agent_send` starts it
|
|
10
29
|
* again from the same session.
|
|
11
30
|
*/
|
|
12
31
|
export const SUBAGENT_IDLE_TIMEOUT_MS = 15_000;
|
|
13
32
|
export const SUBAGENT_SETTLED_CLOSE_TIMEOUT_MS = 15_000;
|
|
14
33
|
/** How long stop waits for an adopted child's bridge to go away before calling the exit unconfirmed. */
|
|
15
34
|
export const SUBAGENT_EXIT_WAIT_MS = 2_000;
|
|
16
|
-
const STOPPED_SEND_REASON = "Child is stopped; a stopped child never resumes — spawn a new child if this work is still needed";
|
|
17
35
|
function diagnose(message) {
|
|
18
36
|
if (process.env.DEBUG || process.env.PI_SUBAGENTS_DEBUG) {
|
|
19
37
|
console.error(`pi-subagents: ${message}`);
|
|
@@ -29,9 +47,19 @@ function failure(operation, reason, childId, state, sideEffects = [], safeToRetr
|
|
|
29
47
|
safeToRetry,
|
|
30
48
|
};
|
|
31
49
|
}
|
|
32
|
-
function
|
|
50
|
+
function acknowledgedPath(response) {
|
|
51
|
+
if (!isRecord(response) ||
|
|
52
|
+
response.accepted !== true ||
|
|
53
|
+
typeof response.path !== "string" ||
|
|
54
|
+
response.path === "") {
|
|
55
|
+
throw new BridgeError("invalid_state", "Child did not confirm its session path");
|
|
56
|
+
}
|
|
57
|
+
return response.path;
|
|
58
|
+
}
|
|
59
|
+
function publicChild(record, live, path = record.sessionPath) {
|
|
33
60
|
return {
|
|
34
61
|
id: record.subagentId,
|
|
62
|
+
...(path === undefined ? {} : { path }),
|
|
35
63
|
agent: record.launchConfig.agent.name,
|
|
36
64
|
...(record.launchConfig.agent.displayName === undefined
|
|
37
65
|
? {}
|
|
@@ -40,12 +68,12 @@ function publicChild(record, live) {
|
|
|
40
68
|
presentation: record.presentation,
|
|
41
69
|
cwd: record.cwd,
|
|
42
70
|
sessionId: record.sessionId,
|
|
71
|
+
...(record.initialTask === undefined ? {} : { task: record.initialTask }),
|
|
43
72
|
...(record.latestSummary === undefined ? {} : { summary: record.latestSummary }),
|
|
44
73
|
...(record.activeTool === undefined ? {} : { activeTool: record.activeTool }),
|
|
45
74
|
...(record.usage === undefined ? {} : { usage: record.usage }),
|
|
46
75
|
...(record.interrupted === undefined ? {} : { interrupted: record.interrupted }),
|
|
47
76
|
freshness: live ? "live" : "last_known",
|
|
48
|
-
interactive: record.launchConfig.interactive,
|
|
49
77
|
model: record.launchConfig.model,
|
|
50
78
|
thinking: record.launchConfig.thinking,
|
|
51
79
|
createdAt: record.createdAt,
|
|
@@ -64,25 +92,6 @@ function withDeadline(operation, timeoutMs) {
|
|
|
64
92
|
});
|
|
65
93
|
});
|
|
66
94
|
}
|
|
67
|
-
function sessionContainsUserInput(entries, expected) {
|
|
68
|
-
return entries.some((entry) => {
|
|
69
|
-
const value = isRecord(entry) ? entry : undefined;
|
|
70
|
-
const message = isRecord(value?.message) ? value.message : undefined;
|
|
71
|
-
if (message?.role !== "user")
|
|
72
|
-
return false;
|
|
73
|
-
if (typeof message.content === "string")
|
|
74
|
-
return message.content === expected;
|
|
75
|
-
if (!Array.isArray(message.content))
|
|
76
|
-
return false;
|
|
77
|
-
const text = message.content
|
|
78
|
-
.map((part) => {
|
|
79
|
-
const content = isRecord(part) ? part : undefined;
|
|
80
|
-
return content?.type === "text" && typeof content.text === "string" ? content.text : "";
|
|
81
|
-
})
|
|
82
|
-
.join("");
|
|
83
|
-
return text === expected;
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
95
|
/**
|
|
87
96
|
* Extracts the trailing text of the most recent assistant message in session entries.
|
|
88
97
|
* Used to automatically capture a subagent's final output without requiring a tool call.
|
|
@@ -114,6 +123,7 @@ export class SubagentManager {
|
|
|
114
123
|
#deps;
|
|
115
124
|
#chains = new Map();
|
|
116
125
|
#processes = new Map();
|
|
126
|
+
#ready;
|
|
117
127
|
/** Panels this process opened. A panel child's process is held by the host, not by us. */
|
|
118
128
|
#attachments = new Map();
|
|
119
129
|
/**
|
|
@@ -121,36 +131,22 @@ export class SubagentManager {
|
|
|
121
131
|
* process is not this child's runtime any more, so it is neither live nor ours to close.
|
|
122
132
|
*/
|
|
123
133
|
#leftSessions = new Set();
|
|
124
|
-
/**
|
|
125
|
-
* Children whose runtime this process never saw: a panel child outlives a parent restart, so a
|
|
126
|
-
* record that still names a runtime after recovery did not reconnect may be running right now.
|
|
127
|
-
* Nothing here can observe it and one session never gets two writers, so the next send refuses
|
|
128
|
-
* until an explicit stop clears the record.
|
|
129
|
-
*/
|
|
130
|
-
#unresolvedRuntimes = new Set();
|
|
131
134
|
/** Bumped by every inbound bridge signal, so a reclaim in flight can tell the child moved. */
|
|
132
135
|
#activity = new Map();
|
|
133
136
|
#projectors = new Map();
|
|
137
|
+
/** Display paths do not imply that a session has been flushed to disk. */
|
|
138
|
+
#paths = new Map();
|
|
134
139
|
/** Known child work in flight, separate from failures recorded before Pi finally settles. */
|
|
135
140
|
#working = new Set();
|
|
136
141
|
/** A settled event from an older run cannot clear a newer synchronous start signal. */
|
|
137
142
|
#workVersions = new Map();
|
|
138
143
|
#settledPanelCloseTimers = new Map();
|
|
139
|
-
#
|
|
144
|
+
#pendingAutoReports = new Map();
|
|
145
|
+
#autoReportBatchTimer;
|
|
140
146
|
#lastDeliveredTurn = new Map();
|
|
141
147
|
#stopping = new Set();
|
|
142
148
|
#listeners = new Set();
|
|
143
|
-
#childEventListeners = new Set();
|
|
144
149
|
#unsubscribe = [];
|
|
145
|
-
#taskRegistry;
|
|
146
|
-
bindTaskRegistry(registry) {
|
|
147
|
-
this.#taskRegistry = registry;
|
|
148
|
-
return () => {
|
|
149
|
-
if (this.#taskRegistry === registry) {
|
|
150
|
-
this.#taskRegistry = undefined;
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
150
|
constructor(deps) {
|
|
155
151
|
this.#deps = deps;
|
|
156
152
|
// The manager owns the transport's two inbound streams: child events and connection
|
|
@@ -158,17 +154,7 @@ export class SubagentManager {
|
|
|
158
154
|
this.#unsubscribe.push(deps.bridge.onEvent((childId, event) => this.handleEvent(childId, event)));
|
|
159
155
|
this.#unsubscribe.push(deps.bridge.onConnectionChange((childId, connected) => this.handleConnectionChange(childId, connected)));
|
|
160
156
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Observes the raw bridge events of every child. A Task producer needs the child's
|
|
163
|
-
* `task_result` and settle events without also owning the bridge connection.
|
|
164
|
-
*/
|
|
165
|
-
onChildEvent(listener) {
|
|
166
|
-
this.#childEventListeners.add(listener);
|
|
167
|
-
return () => {
|
|
168
|
-
this.#childEventListeners.delete(listener);
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
/** Presentation-only: widget and similar projections re-read list() after this. */
|
|
157
|
+
/** Presentation-only: status and similar projections re-read list() after this. */
|
|
172
158
|
onChange(listener) {
|
|
173
159
|
this.#listeners.add(listener);
|
|
174
160
|
return () => {
|
|
@@ -176,6 +162,11 @@ export class SubagentManager {
|
|
|
176
162
|
};
|
|
177
163
|
}
|
|
178
164
|
dispose() {
|
|
165
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
166
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
167
|
+
this.#autoReportBatchTimer = undefined;
|
|
168
|
+
}
|
|
169
|
+
this.#pendingAutoReports.clear();
|
|
179
170
|
for (const timer of this.#settledPanelCloseTimers.values()) {
|
|
180
171
|
clearTimeout(timer);
|
|
181
172
|
}
|
|
@@ -190,12 +181,17 @@ export class SubagentManager {
|
|
|
190
181
|
*/
|
|
191
182
|
closeLocalConnections() {
|
|
192
183
|
this.dispose();
|
|
184
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
185
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
186
|
+
this.#autoReportBatchTimer = undefined;
|
|
187
|
+
}
|
|
188
|
+
this.#pendingAutoReports.clear();
|
|
193
189
|
this.#processes.clear();
|
|
194
190
|
this.#attachments.clear();
|
|
195
191
|
this.#leftSessions.clear();
|
|
196
|
-
this.#unresolvedRuntimes.clear();
|
|
197
192
|
this.#activity.clear();
|
|
198
193
|
this.#projectors.clear();
|
|
194
|
+
this.#paths.clear();
|
|
199
195
|
this.#working.clear();
|
|
200
196
|
this.#workVersions.clear();
|
|
201
197
|
this.#notify();
|
|
@@ -215,10 +211,6 @@ export class SubagentManager {
|
|
|
215
211
|
const launch = await openPanel(record);
|
|
216
212
|
if (launch.handle !== undefined)
|
|
217
213
|
this.#attachments.set(record.subagentId, launch.handle);
|
|
218
|
-
// A panel that could not be opened may still exist and still own the child's session, and
|
|
219
|
-
// nothing here can observe it: the child stays unavailable until an explicit stop.
|
|
220
|
-
if (launch.unconfirmed === true)
|
|
221
|
-
this.#unresolvedRuntimes.add(record.subagentId);
|
|
222
214
|
if (launch.failure !== undefined)
|
|
223
215
|
throw new Error(launch.failure);
|
|
224
216
|
return;
|
|
@@ -249,117 +241,61 @@ export class SubagentManager {
|
|
|
249
241
|
*/
|
|
250
242
|
async #runtimeEnded(id) {
|
|
251
243
|
const runtime = this.#processes.get(id);
|
|
252
|
-
// A process
|
|
244
|
+
// A live process is not over. The lock is the connection: an exit this process already
|
|
245
|
+
// saw is recorded here even when the claim has not dropped yet. A later release is the
|
|
246
|
+
// same end, and only the transition that leaves `running` schedules a report.
|
|
253
247
|
if (runtime?.alive === true)
|
|
254
248
|
return;
|
|
255
|
-
|
|
249
|
+
const sawExit = runtime !== undefined;
|
|
250
|
+
if (sawExit) {
|
|
256
251
|
this.#processes.delete(id);
|
|
257
|
-
|
|
252
|
+
if (this.#deps.bridge.isConnected(id))
|
|
253
|
+
this.#deps.bridge.disconnect(id);
|
|
254
|
+
}
|
|
255
|
+
if (!sawExit && this.#deps.bridge.isConnected(id))
|
|
258
256
|
return;
|
|
259
257
|
this.#working.delete(id);
|
|
260
|
-
await this.#
|
|
258
|
+
await this.#dropDeadPanel(id);
|
|
261
259
|
this.#clearSettledPanelClose(id);
|
|
262
260
|
this.#projectors.delete(id);
|
|
263
|
-
|
|
264
|
-
if (current.intent === "stopped")
|
|
265
|
-
return current;
|
|
266
|
-
if (current.state !== "running")
|
|
267
|
-
return current;
|
|
268
|
-
return {
|
|
269
|
-
...current,
|
|
270
|
-
state: "blocked",
|
|
271
|
-
interrupted: current.presentation === "panel"
|
|
272
|
-
? "The child bridge disconnected; its panel process exit is not confirmed"
|
|
273
|
-
: "The child runtime ended while a turn was in flight; the session is unchanged and a send resumes it",
|
|
274
|
-
};
|
|
275
|
-
});
|
|
276
|
-
if (record.interrupted !== undefined && record.intent !== "stopped") {
|
|
277
|
-
this.#scheduleAutoReport(id, record);
|
|
278
|
-
}
|
|
279
|
-
if (record.presentation === "panel" &&
|
|
280
|
-
record.intent === "active" &&
|
|
281
|
-
record.state !== "done" &&
|
|
282
|
-
record.runtime !== undefined &&
|
|
283
|
-
!this.#attachments.has(id)) {
|
|
284
|
-
// An adopted child has no host handle: losing its bridge cannot prove its exit.
|
|
285
|
-
this.#unresolvedRuntimes.add(id);
|
|
286
|
-
}
|
|
261
|
+
await this.#recordWriterGone(id);
|
|
287
262
|
}
|
|
288
|
-
/** True
|
|
263
|
+
/** True while a writer this process can see is still there: a held lock, or a process handle. A panel tab is not a writer. */
|
|
289
264
|
#runtimeStarted(id) {
|
|
290
|
-
return this.#isLive(id) || this.#processes.has(id)
|
|
265
|
+
return this.#isLive(id) || this.#processes.has(id);
|
|
291
266
|
}
|
|
292
267
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* reconnected after a parent restart. Only an explicit stop clears that doubt.
|
|
268
|
+
* A new runtime may not start while a writer this process can see is still there. A connected
|
|
269
|
+
* bridge is that writer, so input goes to it instead of through here. A leftover tab is not a writer.
|
|
296
270
|
*/
|
|
297
|
-
|
|
298
|
-
if (this.#
|
|
299
|
-
|
|
300
|
-
// also what a retried stop would act on, so it is released here before refusing.
|
|
301
|
-
await this.#releasePanel(id).catch(() => undefined);
|
|
302
|
-
if ((await this.#deps.registry.get(id))?.intent === "stopped") {
|
|
303
|
-
return STOPPED_SEND_REASON;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (this.#runtimeStarted(id)) {
|
|
307
|
-
return "This child's previous runtime is not confirmed gone (its process or panel may still be running); stop the child to end it, then spawn a new one for this work";
|
|
308
|
-
}
|
|
309
|
-
if (this.#unresolvedRuntimes.has(id)) {
|
|
310
|
-
return "This child's panel runtime is disconnected and its exit cannot be confirmed; no replacement was started. Close any remaining panel, stop the child, then spawn a new child for this work";
|
|
271
|
+
#runtimeDoubt(id) {
|
|
272
|
+
if (this.#processes.get(id)?.alive === true) {
|
|
273
|
+
return "This child's process is still running; stop it, then send to this same child";
|
|
311
274
|
}
|
|
312
275
|
return undefined;
|
|
313
276
|
}
|
|
314
|
-
/**
|
|
315
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Counts an inbound signal before its handler is queued, so a decision already in flight can see
|
|
279
|
+
* it. invalidateReport is only for a new turn: compaction, a late update, or the bridge dropping
|
|
280
|
+
* are not the child starting over, and treating them as such dropped the reply.
|
|
281
|
+
*/
|
|
282
|
+
#bumpActivity(id, invalidateReport = true) {
|
|
316
283
|
this.#activity.set(id, (this.#activity.get(id) ?? 0) + 1);
|
|
317
|
-
|
|
284
|
+
if (invalidateReport)
|
|
285
|
+
this.#clearAutoReport(id);
|
|
318
286
|
this.#clearSettledPanelClose(id);
|
|
319
287
|
}
|
|
320
288
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* be serving the session the human switched to, and nobody asked for that panel to be ended. A
|
|
324
|
-
* panel that stays keeps its handle, so an explicit stop can still close it.
|
|
289
|
+
* Forgets a panel the host already says is gone, and closes that leftover tab. A tab the host
|
|
290
|
+
* still reports, or cannot see, stays in hand so stop can close it. It does not block a send.
|
|
325
291
|
*/
|
|
326
|
-
async #
|
|
292
|
+
async #dropDeadPanel(id) {
|
|
327
293
|
const attachment = this.#attachments.get(id);
|
|
328
294
|
if (attachment === undefined)
|
|
329
295
|
return;
|
|
330
296
|
const observed = await attachment.observe();
|
|
331
297
|
if (!(observed.known && !observed.alive))
|
|
332
298
|
return;
|
|
333
|
-
let retired;
|
|
334
|
-
const record = await this.#update(id, (current) => {
|
|
335
|
-
if (current.intent === "stopped")
|
|
336
|
-
return current;
|
|
337
|
-
retired = current.runtime?.runtimeIdentity;
|
|
338
|
-
const { runtime: _runtime, ...withoutRuntime } = current;
|
|
339
|
-
if (current.state === "done" || current.state === "blocked") {
|
|
340
|
-
return {
|
|
341
|
-
...withoutRuntime,
|
|
342
|
-
state: current.state,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
return {
|
|
346
|
-
...withoutRuntime,
|
|
347
|
-
intent: "stopped",
|
|
348
|
-
state: "error",
|
|
349
|
-
interrupted: "The host reports that the child process exited while running; the cause is unknown. Spawn a new child for more work",
|
|
350
|
-
};
|
|
351
|
-
});
|
|
352
|
-
if (retired !== undefined)
|
|
353
|
-
this.#deps.tokens?.forget(retired);
|
|
354
|
-
const projector = this.#projectors.get(id);
|
|
355
|
-
if (record.state === "done" && projector !== undefined) {
|
|
356
|
-
projector.syncState("done");
|
|
357
|
-
}
|
|
358
|
-
if (record.intent === "stopped") {
|
|
359
|
-
this.#clearSettledPanelClose(id);
|
|
360
|
-
this.#projectors.delete(id);
|
|
361
|
-
this.#unresolvedRuntimes.delete(id);
|
|
362
|
-
}
|
|
363
299
|
this.#attachments.delete(id);
|
|
364
300
|
try {
|
|
365
301
|
const cleanup = await attachment.cleanup();
|
|
@@ -371,6 +307,58 @@ export class SubagentManager {
|
|
|
371
307
|
diagnose(`could not close the leftover ${attachment.identity.host} panel of child ${id}: ${errorMessage(error)}`);
|
|
372
308
|
}
|
|
373
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* The ledger stops naming a writer that is gone. A child that was running becomes blocked, once,
|
|
312
|
+
* unless its session already ends in a completed reply: that reply is delivered instead. The next
|
|
313
|
+
* send starts the same session again. Callers that still have a tab close it themselves.
|
|
314
|
+
*/
|
|
315
|
+
async #recordWriterGone(id) {
|
|
316
|
+
const current = await this.#deps.registry.get(id);
|
|
317
|
+
if (current !== undefined &&
|
|
318
|
+
current.state === "running" &&
|
|
319
|
+
current.intent !== "stopped" &&
|
|
320
|
+
(await this.#finishedReply(id, current.unacknowledgedInput))) {
|
|
321
|
+
const released = await this.#releaseRuntime(id, "finished");
|
|
322
|
+
if (released === undefined)
|
|
323
|
+
return;
|
|
324
|
+
const next = await this.#update(id, (entry) => {
|
|
325
|
+
const { interrupted: _interrupted, ...rest } = entry;
|
|
326
|
+
return { ...rest, state: "done" };
|
|
327
|
+
});
|
|
328
|
+
this.#scheduleAutoReport(id, next);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
await this.#releaseRuntime(id, "drop");
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* True when the session file ends on a completed assistant reply and any send still being
|
|
335
|
+
* confirmed is already in that file. An unflushed prompt is not a finished turn.
|
|
336
|
+
*/
|
|
337
|
+
async #finishedReply(id, expectedInput) {
|
|
338
|
+
const entries = await this.#diskEntries(id);
|
|
339
|
+
if (entries === undefined || !persistedReplyFinished(entries))
|
|
340
|
+
return false;
|
|
341
|
+
if (expectedInput === undefined)
|
|
342
|
+
return true;
|
|
343
|
+
return transcriptFacts(entries, expectedInput).containsInput;
|
|
344
|
+
}
|
|
345
|
+
/** Runtime leaves the ledger only through a lease edge. */
|
|
346
|
+
async #releaseRuntime(id, outcome) {
|
|
347
|
+
const current = await this.#deps.registry.get(id);
|
|
348
|
+
if (current === undefined)
|
|
349
|
+
return undefined;
|
|
350
|
+
const identity = current.runtime?.runtimeIdentity;
|
|
351
|
+
const next = await commitLease(this.#deps.registry, id, { type: "runtime-gone", outcome });
|
|
352
|
+
if (identity !== undefined && next.runtime === undefined)
|
|
353
|
+
this.#deps.tokens?.forget(identity);
|
|
354
|
+
if (outcome === "drop" &&
|
|
355
|
+
current.state === "running" &&
|
|
356
|
+
current.intent !== "stopped" &&
|
|
357
|
+
next.state === "blocked") {
|
|
358
|
+
this.#scheduleAutoReport(id, next);
|
|
359
|
+
}
|
|
360
|
+
return next;
|
|
361
|
+
}
|
|
374
362
|
/** Waits for a child's bridge connection to end; false when it is still connected after the wait. */
|
|
375
363
|
#awaitDisconnect(id, timeoutMs) {
|
|
376
364
|
if (!this.#deps.bridge.isConnected(id))
|
|
@@ -397,21 +385,29 @@ export class SubagentManager {
|
|
|
397
385
|
* only ever closed on a judgement, never on a guess.
|
|
398
386
|
*/
|
|
399
387
|
#scheduleAutoReport(id, record) {
|
|
400
|
-
this.#
|
|
401
|
-
|
|
402
|
-
this.#
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
388
|
+
this.#pendingAutoReports.set(id, record);
|
|
389
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
390
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
391
|
+
}
|
|
392
|
+
// Batch reports: if all children settled, flush in 500ms; otherwise wait up to 10000ms for others
|
|
393
|
+
const delayMs = this.#working.size > 0 ? 10_000 : 500;
|
|
394
|
+
this.#autoReportBatchTimer = setTimeout(async () => {
|
|
395
|
+
this.#autoReportBatchTimer = undefined;
|
|
396
|
+
const batch = [...this.#pendingAutoReports.entries()];
|
|
397
|
+
this.#pendingAutoReports.clear();
|
|
398
|
+
for (const [childId, childRecord] of batch) {
|
|
399
|
+
await this.#performAutoReport(childId, childRecord).catch((error) => {
|
|
400
|
+
diagnose(`failed to report child ${childId}: ${errorMessage(error)}`);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}, delayMs);
|
|
404
|
+
this.#autoReportBatchTimer.unref?.();
|
|
409
405
|
}
|
|
410
406
|
#clearAutoReport(id) {
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
clearTimeout(
|
|
414
|
-
this.#
|
|
407
|
+
this.#pendingAutoReports.delete(id);
|
|
408
|
+
if (this.#pendingAutoReports.size === 0 && this.#autoReportBatchTimer !== undefined) {
|
|
409
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
410
|
+
this.#autoReportBatchTimer = undefined;
|
|
415
411
|
}
|
|
416
412
|
}
|
|
417
413
|
#scheduleSettledPanelClose(id) {
|
|
@@ -461,7 +457,8 @@ export class SubagentManager {
|
|
|
461
457
|
if (attachment !== undefined) {
|
|
462
458
|
const observed = await attachment.observe();
|
|
463
459
|
if (observed.known && !observed.alive) {
|
|
464
|
-
await this.#
|
|
460
|
+
await this.#dropDeadPanel(id);
|
|
461
|
+
await this.#recordWriterGone(id);
|
|
465
462
|
return;
|
|
466
463
|
}
|
|
467
464
|
if ((this.#activity.get(id) ?? 0) !== activity) {
|
|
@@ -494,25 +491,9 @@ export class SubagentManager {
|
|
|
494
491
|
this.#processes.delete(id);
|
|
495
492
|
if (attachment !== undefined)
|
|
496
493
|
this.#attachments.delete(id);
|
|
497
|
-
this.#
|
|
498
|
-
await this.#update(id, (current) => {
|
|
499
|
-
if (current.intent === "stopped")
|
|
500
|
-
return current;
|
|
501
|
-
const { runtime: _runtime, ...withoutRuntime } = current;
|
|
502
|
-
return withoutRuntime;
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
else {
|
|
506
|
-
await this.#update(id, (current) => {
|
|
507
|
-
if (current.intent === "stopped")
|
|
508
|
-
return current;
|
|
509
|
-
return {
|
|
510
|
-
...current,
|
|
511
|
-
state: "error",
|
|
512
|
-
interrupted: "The child panel is not confirmed gone: the child process may still be running",
|
|
513
|
-
};
|
|
514
|
-
});
|
|
494
|
+
await this.#releaseRuntime(id, "drop");
|
|
515
495
|
}
|
|
496
|
+
// The tab is still there, or the host could not say. Leave it. The caller closes it.
|
|
516
497
|
});
|
|
517
498
|
}
|
|
518
499
|
async #performAutoReport(id, _record) {
|
|
@@ -523,21 +504,37 @@ export class SubagentManager {
|
|
|
523
504
|
return;
|
|
524
505
|
const activity = this.#activity.get(id) ?? 0;
|
|
525
506
|
const workVersion = this.#workVersions.get(id) ?? 0;
|
|
526
|
-
const
|
|
527
|
-
const text =
|
|
507
|
+
const facts = await this.#facts(id);
|
|
508
|
+
const text = facts?.summary;
|
|
528
509
|
await this.#mutate(id, async () => {
|
|
529
510
|
const current = await this.#deps.registry.get(id);
|
|
511
|
+
const activityMoved = (this.#activity.get(id) ?? 0) !== activity;
|
|
512
|
+
const versionMoved = (this.#workVersions.get(id) ?? 0) !== workVersion;
|
|
530
513
|
if (current === undefined ||
|
|
531
514
|
current.intent === "stopped" ||
|
|
532
515
|
current.state === "running" ||
|
|
533
516
|
this.#working.has(id) ||
|
|
534
|
-
|
|
535
|
-
|
|
517
|
+
activityMoved ||
|
|
518
|
+
versionMoved) {
|
|
519
|
+
// Still settled: the signal was bookkeeping, not a new turn. Dropping the reply here left
|
|
520
|
+
// the parent silent until the process died and the ledger was still running.
|
|
521
|
+
if (current !== undefined &&
|
|
522
|
+
current.intent !== "stopped" &&
|
|
523
|
+
current.state !== "running" &&
|
|
524
|
+
!this.#working.has(id) &&
|
|
525
|
+
(activityMoved || versionMoved)) {
|
|
526
|
+
this.#scheduleAutoReport(id, current);
|
|
527
|
+
}
|
|
536
528
|
return;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const isBlocked =
|
|
540
|
-
|
|
529
|
+
}
|
|
530
|
+
const isError = current.state === "error" || record.state === "error";
|
|
531
|
+
const isBlocked = !isError &&
|
|
532
|
+
(current.state === "blocked" ||
|
|
533
|
+
record.state === "blocked" ||
|
|
534
|
+
(current.interrupted !== undefined && current.interrupted.trim() !== "") ||
|
|
535
|
+
(record.interrupted !== undefined && record.interrupted.trim() !== ""));
|
|
536
|
+
const failed = isError || isBlocked;
|
|
537
|
+
if (!failed && (text === undefined || text.trim() === "")) {
|
|
541
538
|
const projector = this.#projectors.get(id);
|
|
542
539
|
if (projector !== undefined) {
|
|
543
540
|
projector.syncState("done");
|
|
@@ -549,22 +546,19 @@ export class SubagentManager {
|
|
|
549
546
|
state: "done",
|
|
550
547
|
};
|
|
551
548
|
});
|
|
552
|
-
if (this.#taskRegistry !== undefined) {
|
|
553
|
-
this.#taskRegistry.settle(id, {
|
|
554
|
-
status: "completed",
|
|
555
|
-
output: "",
|
|
556
|
-
truncated: false,
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
549
|
return;
|
|
560
550
|
}
|
|
561
551
|
let message;
|
|
562
|
-
|
|
563
|
-
|
|
552
|
+
const interruptedText = current.interrupted?.trim() || record.interrupted?.trim();
|
|
553
|
+
if (isError) {
|
|
554
|
+
message = interruptedText || "Subagent failed without error details.";
|
|
555
|
+
}
|
|
556
|
+
else if (isBlocked) {
|
|
557
|
+
if (interruptedText !== undefined && interruptedText !== "") {
|
|
564
558
|
message =
|
|
565
559
|
text !== undefined && text.trim() !== ""
|
|
566
|
-
? `${
|
|
567
|
-
:
|
|
560
|
+
? `${interruptedText}\n\n${text}`
|
|
561
|
+
: interruptedText;
|
|
568
562
|
}
|
|
569
563
|
else if (text !== undefined && text.trim() !== "") {
|
|
570
564
|
message = text;
|
|
@@ -576,13 +570,15 @@ export class SubagentManager {
|
|
|
576
570
|
else {
|
|
577
571
|
message = text ?? "";
|
|
578
572
|
}
|
|
579
|
-
|
|
573
|
+
const lastDelivered = this.#lastDeliveredTurn.get(id);
|
|
574
|
+
if (lastDelivered !== undefined &&
|
|
575
|
+
lastDelivered.workVersion === workVersion &&
|
|
576
|
+
lastDelivered.message === message) {
|
|
580
577
|
return;
|
|
581
|
-
|
|
582
|
-
const reason = isBlocked ? "blocked" : "success";
|
|
583
|
-
const taskStatus = isBlocked ? "failed" : "completed";
|
|
578
|
+
}
|
|
579
|
+
const reason = isError ? "error" : isBlocked ? "blocked" : "success";
|
|
584
580
|
const projector = this.#projectors.get(id);
|
|
585
|
-
const updatedRecord = !
|
|
581
|
+
const updatedRecord = !failed
|
|
586
582
|
? await this.#update(id, (current) => {
|
|
587
583
|
const { interrupted: _interrupted, ...clean } = current;
|
|
588
584
|
return {
|
|
@@ -592,26 +588,26 @@ export class SubagentManager {
|
|
|
592
588
|
})
|
|
593
589
|
: await this.#update(id, (current) => ({
|
|
594
590
|
...current,
|
|
595
|
-
state: "blocked",
|
|
591
|
+
state: isError ? "error" : "blocked",
|
|
596
592
|
interrupted: current.interrupted ?? message,
|
|
597
593
|
}));
|
|
598
594
|
if (projector !== undefined) {
|
|
599
|
-
projector.syncState(isBlocked ? "blocked" : "done");
|
|
595
|
+
projector.syncState(isError ? "error" : isBlocked ? "blocked" : "done");
|
|
600
596
|
}
|
|
601
597
|
this.#scheduleSettledPanelClose(id);
|
|
598
|
+
let deliveryOk = true;
|
|
602
599
|
if (this.#deps.channel !== undefined) {
|
|
603
|
-
await this.contactParent(id, reason, message, updatedRecord);
|
|
600
|
+
const contactResult = await this.contactParent(id, reason, message, updatedRecord);
|
|
601
|
+
if (isOperationError(contactResult)) {
|
|
602
|
+
deliveryOk = false;
|
|
603
|
+
}
|
|
604
604
|
}
|
|
605
|
-
if (
|
|
606
|
-
this.#
|
|
607
|
-
status: taskStatus,
|
|
608
|
-
output: message,
|
|
609
|
-
truncated: false,
|
|
610
|
-
});
|
|
605
|
+
if (deliveryOk) {
|
|
606
|
+
this.#lastDeliveredTurn.set(id, { workVersion, message });
|
|
611
607
|
}
|
|
612
608
|
});
|
|
613
609
|
}
|
|
614
|
-
/** Reads a child's session entries;
|
|
610
|
+
/** Reads a child's session entries; used by transcript inspection fallbacks. */
|
|
615
611
|
async getEntries(id) {
|
|
616
612
|
return this.#entries(id);
|
|
617
613
|
}
|
|
@@ -664,6 +660,37 @@ export class SubagentManager {
|
|
|
664
660
|
}
|
|
665
661
|
return { sessionPath: record.sessionPath, persistence: record.persistence };
|
|
666
662
|
}
|
|
663
|
+
/** The session file, when the bridge cannot answer. A missing file is not an empty transcript. */
|
|
664
|
+
async #diskEntries(id) {
|
|
665
|
+
const record = await this.#deps.registry.get(id).catch(() => undefined);
|
|
666
|
+
if (record === undefined)
|
|
667
|
+
return undefined;
|
|
668
|
+
const sessionPath = record.sessionPath ??
|
|
669
|
+
(await findSessionFile(record.launchConfig.sessionDir, record.sessionId));
|
|
670
|
+
if (sessionPath === undefined)
|
|
671
|
+
return undefined;
|
|
672
|
+
try {
|
|
673
|
+
const diskEntries = await readSessionJsonlEntries(sessionPath);
|
|
674
|
+
return diskEntries.length > 0 ? diskEntries : undefined;
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Usage, the last assistant text, and whether one input landed. The child computes this
|
|
682
|
+
* from the session it already has; the transcript is not copied into the mailbox.
|
|
683
|
+
*/
|
|
684
|
+
async #facts(id, expectedInput) {
|
|
685
|
+
if (this.#deps.bridge.isConnected(id)) {
|
|
686
|
+
const response = await this.#request(id, "session_facts", expectedInput === undefined ? undefined : { expectedInput }).catch(() => undefined);
|
|
687
|
+
const facts = parseTranscriptFacts(response);
|
|
688
|
+
if (facts !== undefined)
|
|
689
|
+
return facts;
|
|
690
|
+
}
|
|
691
|
+
const entries = await this.#diskEntries(id);
|
|
692
|
+
return entries === undefined ? undefined : transcriptFacts(entries, expectedInput);
|
|
693
|
+
}
|
|
667
694
|
/** Reads a child's session entries over the bridge, falling back to disk when disconnected. */
|
|
668
695
|
async #entries(id) {
|
|
669
696
|
if (this.#deps.bridge.isConnected(id)) {
|
|
@@ -672,22 +699,7 @@ export class SubagentManager {
|
|
|
672
699
|
return response.entries;
|
|
673
700
|
}
|
|
674
701
|
}
|
|
675
|
-
|
|
676
|
-
if (record !== undefined) {
|
|
677
|
-
const sessionPath = record.sessionPath ??
|
|
678
|
-
(await findSessionFile(record.launchConfig.sessionDir, record.sessionId));
|
|
679
|
-
if (sessionPath !== undefined) {
|
|
680
|
-
try {
|
|
681
|
-
const diskEntries = await readSessionJsonlEntries(sessionPath);
|
|
682
|
-
if (diskEntries.length > 0)
|
|
683
|
-
return diskEntries;
|
|
684
|
-
}
|
|
685
|
-
catch {
|
|
686
|
-
// return undefined below
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
return undefined;
|
|
702
|
+
return this.#diskEntries(id);
|
|
691
703
|
}
|
|
692
704
|
#request(id, operation, payload) {
|
|
693
705
|
return withDeadline(this.#deps.bridge.request(id, operation, payload), this.#deps.deadlineMs ?? 30_000);
|
|
@@ -719,8 +731,6 @@ export class SubagentManager {
|
|
|
719
731
|
await this.#settleUnrecovered(record);
|
|
720
732
|
return undefined;
|
|
721
733
|
}
|
|
722
|
-
// The runtime this record names answered, so the doubt about it is gone.
|
|
723
|
-
this.#unresolvedRuntimes.delete(record.subagentId);
|
|
724
734
|
await this.#adopt(record.subagentId);
|
|
725
735
|
return { childId: record.subagentId };
|
|
726
736
|
}
|
|
@@ -738,29 +748,18 @@ export class SubagentManager {
|
|
|
738
748
|
};
|
|
739
749
|
}
|
|
740
750
|
/**
|
|
741
|
-
* A record that names a runtime which did not reconnect. A background child
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
* same session until the user stops it.
|
|
751
|
+
* A record that names a runtime which did not reconnect. A background child died with this
|
|
752
|
+
* process, because its stdin pipe did. A panel child is the same fact as any other missing
|
|
753
|
+
* writer: the ledger drops it, and a later send starts the session again.
|
|
745
754
|
*/
|
|
746
755
|
async #settleUnrecovered(record) {
|
|
747
756
|
this.#working.delete(record.subagentId);
|
|
748
|
-
|
|
749
|
-
if (retired === undefined)
|
|
750
|
-
return;
|
|
757
|
+
this.#projectors.delete(record.subagentId);
|
|
751
758
|
if (record.presentation === "panel") {
|
|
752
|
-
this.#
|
|
759
|
+
await this.#recordWriterGone(record.subagentId);
|
|
753
760
|
return;
|
|
754
761
|
}
|
|
755
|
-
await this.#
|
|
756
|
-
const { runtime: _runtime, ...rest } = current;
|
|
757
|
-
return {
|
|
758
|
-
...rest,
|
|
759
|
-
...(current.state === "running" ? { state: "done" } : {}),
|
|
760
|
-
interrupted: "The child runtime ended with the parent process; a later send starts it again from the same session",
|
|
761
|
-
};
|
|
762
|
-
});
|
|
763
|
-
this.#deps.tokens?.forget(retired.runtimeIdentity);
|
|
762
|
+
await this.#releaseRuntime(record.subagentId, "parent-exit");
|
|
764
763
|
}
|
|
765
764
|
/** Reads a connected child's real state and puts the record back in step with it. */
|
|
766
765
|
async #adopt(id) {
|
|
@@ -769,9 +768,9 @@ export class SubagentManager {
|
|
|
769
768
|
const record = await this.#deps.registry.get(id);
|
|
770
769
|
if (projector === undefined || record === undefined)
|
|
771
770
|
return;
|
|
772
|
-
const
|
|
773
|
-
if (
|
|
774
|
-
projector.
|
|
771
|
+
const facts = await this.#facts(id, record.unacknowledgedInput);
|
|
772
|
+
if (facts !== undefined)
|
|
773
|
+
projector.applyTranscriptFacts(facts);
|
|
775
774
|
const state = await this.#request(id, "get_state");
|
|
776
775
|
if (!isRecord(state) ||
|
|
777
776
|
typeof state.idle !== "boolean" ||
|
|
@@ -779,6 +778,9 @@ export class SubagentManager {
|
|
|
779
778
|
throw new BridgeError("invalid_state", `Child ${id} did not confirm its current work state`);
|
|
780
779
|
}
|
|
781
780
|
const idle = state.idle && !state.pendingMessages;
|
|
781
|
+
if (typeof state.path === "string" && state.path !== "") {
|
|
782
|
+
this.#paths.set(id, state.path);
|
|
783
|
+
}
|
|
782
784
|
if ((this.#workVersions.get(id) ?? 0) === workVersion) {
|
|
783
785
|
if (idle)
|
|
784
786
|
this.#working.delete(id);
|
|
@@ -791,9 +793,7 @@ export class SubagentManager {
|
|
|
791
793
|
if (projector.snapshot().state !== "error")
|
|
792
794
|
projector.syncState(idle ? "done" : "running");
|
|
793
795
|
const snapshot = projector.snapshot();
|
|
794
|
-
const confirmedInput =
|
|
795
|
-
record.unacknowledgedInput !== undefined &&
|
|
796
|
-
sessionContainsUserInput(entries, record.unacknowledgedInput)
|
|
796
|
+
const confirmedInput = facts?.containsInput && record.unacknowledgedInput !== undefined
|
|
797
797
|
? record.unacknowledgedInput
|
|
798
798
|
: undefined;
|
|
799
799
|
await this.#update(id, (current) => {
|
|
@@ -803,12 +803,11 @@ export class SubagentManager {
|
|
|
803
803
|
? current
|
|
804
804
|
: (({ unacknowledgedInput: _input, ...rest }) => rest)(current);
|
|
805
805
|
const hasError = snapshot.state === "error";
|
|
806
|
+
const { interrupted: _interrupted, ...withoutInterrupted } = base;
|
|
807
|
+
const { latestSummary: _summary, activeTool: _tool, usage: _usage, ...withoutVolatile } = withoutInterrupted;
|
|
806
808
|
return {
|
|
807
|
-
...
|
|
809
|
+
...withoutVolatile,
|
|
808
810
|
state: hasError ? "error" : idle ? "done" : "running",
|
|
809
|
-
...(snapshot.summary === undefined ? {} : { latestSummary: snapshot.summary }),
|
|
810
|
-
usage: snapshot.usage,
|
|
811
|
-
...(snapshot.interrupted === undefined ? {} : { interrupted: snapshot.interrupted }),
|
|
812
811
|
};
|
|
813
812
|
}, workVersion);
|
|
814
813
|
if (snapshot.state === "done" || snapshot.state === "blocked" || snapshot.state === "error") {
|
|
@@ -866,8 +865,10 @@ export class SubagentManager {
|
|
|
866
865
|
this.#working.add(childId);
|
|
867
866
|
this.#notify();
|
|
868
867
|
await withDeadline(this.#start(record), this.#deps.deadlineMs ?? 60_000);
|
|
868
|
+
let path;
|
|
869
869
|
try {
|
|
870
|
-
await this.#request(record.subagentId, "prompt", { message: cleanTask });
|
|
870
|
+
path = acknowledgedPath(await this.#request(record.subagentId, "prompt", { message: cleanTask }));
|
|
871
|
+
this.#paths.set(record.subagentId, path);
|
|
871
872
|
}
|
|
872
873
|
catch (error) {
|
|
873
874
|
let failureState = "error";
|
|
@@ -893,30 +894,9 @@ export class SubagentManager {
|
|
|
893
894
|
const { latestSummary: _latestSummary, ...rest } = current;
|
|
894
895
|
return { ...rest, state: "running" };
|
|
895
896
|
});
|
|
896
|
-
if (this.#taskRegistry !== undefined) {
|
|
897
|
-
try {
|
|
898
|
-
this.#taskRegistry.create({
|
|
899
|
-
id: record.subagentId,
|
|
900
|
-
type: "agent",
|
|
901
|
-
purpose: `${input.agent}: ${input.task}`,
|
|
902
|
-
inlineResult: true,
|
|
903
|
-
begin: () => ({
|
|
904
|
-
stop: () => {
|
|
905
|
-
void this.stop(record.subagentId);
|
|
906
|
-
},
|
|
907
|
-
describe: () => ({
|
|
908
|
-
output: `Subagent ${record.subagentId} (${input.agent}) is running`,
|
|
909
|
-
truncated: false,
|
|
910
|
-
}),
|
|
911
|
-
}),
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
catch (regError) {
|
|
915
|
-
diagnose(`failed to register background task for subagent ${record.subagentId}: ${errorMessage(regError)}`);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
897
|
return {
|
|
919
|
-
child: publicChild((await this.#deps.registry.get(record.subagentId)) ?? record, true),
|
|
898
|
+
child: publicChild((await this.#deps.registry.get(record.subagentId)) ?? record, true, path),
|
|
899
|
+
path,
|
|
920
900
|
...(placement.note === undefined ? {} : { note: placement.note }),
|
|
921
901
|
};
|
|
922
902
|
}
|
|
@@ -926,12 +906,7 @@ export class SubagentManager {
|
|
|
926
906
|
const failedId = childId;
|
|
927
907
|
const reason = errorMessage(error);
|
|
928
908
|
const failed = await this.#mutate(failedId, async () => {
|
|
929
|
-
const
|
|
930
|
-
const uncertain = this.#runtimeStarted(failedId) ||
|
|
931
|
-
this.#unresolvedRuntimes.has(failedId) ||
|
|
932
|
-
record?.runtime !== undefined;
|
|
933
|
-
if (uncertain)
|
|
934
|
-
this.#unresolvedRuntimes.add(failedId);
|
|
909
|
+
const uncertain = this.#runtimeStarted(failedId);
|
|
935
910
|
if (!this.#isLive(failedId))
|
|
936
911
|
this.#working.delete(failedId);
|
|
937
912
|
await this.#update(failedId, (current) => ({
|
|
@@ -956,20 +931,13 @@ export class SubagentManager {
|
|
|
956
931
|
const record = await this.#deps.registry.get(id);
|
|
957
932
|
if (record === undefined)
|
|
958
933
|
return failure("send", "Unknown child", id);
|
|
959
|
-
if (record.intent === "stopped" || record.state === "error") {
|
|
960
|
-
const doubt = await this.#runtimeDoubt(id);
|
|
961
|
-
return failure("send", doubt ??
|
|
962
|
-
`Subagent ${id} is in error state and cannot continue (${record.interrupted ?? "fatal error"}). You can reference its context via spawn_agent with forkFrom: "${id}".`, id, record.state, [], false);
|
|
963
|
-
}
|
|
964
934
|
let isResumed = false;
|
|
965
935
|
// Deliverability is the bridge connection, minus a process that left the delegated session:
|
|
966
936
|
// input goes to a session, and this process no longer serves this child's session. What is
|
|
967
937
|
// left to decide is whether a runtime of ours may still own it.
|
|
968
938
|
if (this.#leftSessions.has(id) || !this.#deps.bridge.isConnected(id)) {
|
|
969
|
-
const doubt =
|
|
939
|
+
const doubt = this.#runtimeDoubt(id);
|
|
970
940
|
if (doubt !== undefined) {
|
|
971
|
-
// A runtime is still unaccounted for: it may not answer, but it may also still own the
|
|
972
|
-
// delegated session, and one session never gets two writers.
|
|
973
941
|
return failure("send", doubt, id, record.state, ["runtime metadata retained"], false);
|
|
974
942
|
}
|
|
975
943
|
let placement;
|
|
@@ -984,6 +952,7 @@ export class SubagentManager {
|
|
|
984
952
|
const resumedRecord = await this.#update(id, (current) => ({
|
|
985
953
|
...current,
|
|
986
954
|
state: "running",
|
|
955
|
+
intent: "active",
|
|
987
956
|
persistence,
|
|
988
957
|
...(sessionPath === undefined ? {} : { sessionPath }),
|
|
989
958
|
launchConfig: sessionPath === undefined
|
|
@@ -1042,31 +1011,30 @@ export class SubagentManager {
|
|
|
1042
1011
|
return {
|
|
1043
1012
|
...clean,
|
|
1044
1013
|
state: "running",
|
|
1014
|
+
intent: "active",
|
|
1045
1015
|
unacknowledgedInput: message,
|
|
1046
1016
|
};
|
|
1047
1017
|
});
|
|
1048
1018
|
try {
|
|
1049
|
-
await this.#request(id, operation, { message });
|
|
1019
|
+
const path = acknowledgedPath(await this.#request(id, operation, { message }));
|
|
1020
|
+
this.#paths.set(id, path);
|
|
1050
1021
|
}
|
|
1051
1022
|
catch (error) {
|
|
1052
1023
|
return failure("send", errorMessage(error), id, pending.state, ["input persisted as unacknowledged before dispatch"], false);
|
|
1053
1024
|
}
|
|
1054
|
-
return publicChild((await this.#deps.registry.get(id)) ??
|
|
1025
|
+
return publicChild((await this.#deps.registry.get(id)) ?? pending, true, this.#paths.get(id));
|
|
1055
1026
|
});
|
|
1056
1027
|
}
|
|
1057
1028
|
async get(id) {
|
|
1058
1029
|
const record = await this.#deps.registry.get(id);
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1030
|
+
if (record === undefined)
|
|
1031
|
+
return failure("get", "Unknown child", id);
|
|
1032
|
+
const live = this.#isLive(id);
|
|
1033
|
+
return publicChild(this.#withSnapshot(record), live, this.#paths.get(id));
|
|
1062
1034
|
}
|
|
1063
1035
|
async list() {
|
|
1064
1036
|
const records = await this.#deps.registry.list();
|
|
1065
|
-
return records.map((record) => publicChild(record, this.#isLive(record.subagentId)));
|
|
1066
|
-
}
|
|
1067
|
-
/** Child startup and runs count until Pi settles, including post-agent_end continuation. */
|
|
1068
|
-
get activeCount() {
|
|
1069
|
-
return this.#working.size;
|
|
1037
|
+
return records.map((record) => publicChild(this.#withSnapshot(record), this.#isLive(record.subagentId), this.#paths.get(record.subagentId)));
|
|
1070
1038
|
}
|
|
1071
1039
|
stop(id) {
|
|
1072
1040
|
this.#clearSettledPanelClose(id);
|
|
@@ -1089,11 +1057,7 @@ export class SubagentManager {
|
|
|
1089
1057
|
let stopped;
|
|
1090
1058
|
try {
|
|
1091
1059
|
// The intent is written before anything is ended, so an interrupted stop is still a stop.
|
|
1092
|
-
stopped = await this.#
|
|
1093
|
-
...current,
|
|
1094
|
-
intent: "stopped",
|
|
1095
|
-
state: "error",
|
|
1096
|
-
}));
|
|
1060
|
+
stopped = await commitLease(this.#deps.registry, id, { type: "stop-intent" });
|
|
1097
1061
|
}
|
|
1098
1062
|
catch (error) {
|
|
1099
1063
|
this.#stopping.delete(id);
|
|
@@ -1122,6 +1086,7 @@ export class SubagentManager {
|
|
|
1122
1086
|
// bridge-only stop.
|
|
1123
1087
|
const cleanup = await attachment.cleanup();
|
|
1124
1088
|
if (!(await this.#panelGone(attachment))) {
|
|
1089
|
+
await this.#update(id, (current) => ({ ...current, state: "error" })).catch(() => undefined);
|
|
1125
1090
|
return failure("stop", `The child's ${attachment.identity.host} panel is not confirmed gone (${cleanup.stderr || "its state could not be observed"}): the child process may still be running`, id, "error", ["stopped intent persisted", "runtime metadata retained"], true);
|
|
1126
1091
|
}
|
|
1127
1092
|
this.#attachments.delete(id);
|
|
@@ -1139,27 +1104,10 @@ export class SubagentManager {
|
|
|
1139
1104
|
}
|
|
1140
1105
|
}
|
|
1141
1106
|
this.#projectors.delete(id);
|
|
1142
|
-
// An explicit stop is the one thing that clears the doubt about a runtime this process
|
|
1143
|
-
// never saw, so a later send may start a fresh one.
|
|
1144
|
-
this.#unresolvedRuntimes.delete(id);
|
|
1145
1107
|
this.#activity.delete(id);
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
stopped = await this.#deps.registry.update(id, current.revision, (value) => {
|
|
1150
|
-
const { runtime: _runtime, ...withoutRuntime } = value;
|
|
1151
|
-
return withoutRuntime;
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
if (this.#taskRegistry !== undefined) {
|
|
1155
|
-
this.#taskRegistry.settle(id, {
|
|
1156
|
-
status: "cancelled",
|
|
1157
|
-
output: `Subagent ${id} was stopped`,
|
|
1158
|
-
truncated: false,
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
if (runtimeIdentity !== undefined)
|
|
1162
|
-
this.#deps.tokens?.forget(runtimeIdentity);
|
|
1108
|
+
const finished = await this.#releaseRuntime(id, "finished");
|
|
1109
|
+
if (finished !== undefined)
|
|
1110
|
+
stopped = finished;
|
|
1163
1111
|
return publicChild(stopped, false);
|
|
1164
1112
|
}
|
|
1165
1113
|
catch (error) {
|
|
@@ -1196,13 +1144,6 @@ export class SubagentManager {
|
|
|
1196
1144
|
state: "blocked",
|
|
1197
1145
|
interrupted: message,
|
|
1198
1146
|
}));
|
|
1199
|
-
if (this.#taskRegistry !== undefined) {
|
|
1200
|
-
this.#taskRegistry.settle(id, {
|
|
1201
|
-
status: "failed",
|
|
1202
|
-
output: message,
|
|
1203
|
-
truncated: false,
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
1147
|
this.#scheduleSettledPanelClose(id);
|
|
1207
1148
|
}
|
|
1208
1149
|
else if (reason === "error") {
|
|
@@ -1212,17 +1153,9 @@ export class SubagentManager {
|
|
|
1212
1153
|
}
|
|
1213
1154
|
await this.#update(id, (current) => ({
|
|
1214
1155
|
...current,
|
|
1215
|
-
intent: "stopped",
|
|
1216
1156
|
state: "error",
|
|
1217
1157
|
interrupted: message,
|
|
1218
1158
|
}));
|
|
1219
|
-
if (this.#taskRegistry !== undefined) {
|
|
1220
|
-
this.#taskRegistry.settle(id, {
|
|
1221
|
-
status: "failed",
|
|
1222
|
-
output: message,
|
|
1223
|
-
truncated: false,
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
1159
|
this.#scheduleSettledPanelClose(id);
|
|
1227
1160
|
}
|
|
1228
1161
|
return { delivered: true };
|
|
@@ -1231,11 +1164,7 @@ export class SubagentManager {
|
|
|
1231
1164
|
return failure("contact_parent", errorMessage(error), id, record.state, [], true);
|
|
1232
1165
|
}
|
|
1233
1166
|
}
|
|
1234
|
-
/**
|
|
1235
|
-
* Handles a request a child sent to its parent. The two child-initiated operations are the
|
|
1236
|
-
* child's report channel and its Task result, and both are answered here so the child learns
|
|
1237
|
-
* whether the parent accepted them.
|
|
1238
|
-
*/
|
|
1167
|
+
/** Answers a request a child sent to its parent, so the child learns whether the parent accepted it. */
|
|
1239
1168
|
async handleChildRequest(childId, operation, payload) {
|
|
1240
1169
|
const record = await this.#deps.registry.get(childId);
|
|
1241
1170
|
if (record === undefined)
|
|
@@ -1296,15 +1225,16 @@ export class SubagentManager {
|
|
|
1296
1225
|
this.#notify();
|
|
1297
1226
|
}
|
|
1298
1227
|
// Counted before the per-child queue: an event that arrives while a reclaim is running is
|
|
1299
|
-
// queued behind it and could otherwise not cancel a decision already in flight.
|
|
1300
|
-
|
|
1228
|
+
// queued behind it and could otherwise not cancel a decision already in flight. Only a new
|
|
1229
|
+
// turn cancels a reply that is already waiting.
|
|
1230
|
+
this.#bumpActivity(childId, isRecord(event) && resumesChildWork(event.type));
|
|
1301
1231
|
const workVersion = this.#workVersions.get(childId) ?? 0;
|
|
1302
1232
|
void this.#mutate(childId, () => this.#observe(childId, event, workVersion)).catch((error) => {
|
|
1303
1233
|
diagnose(`failed to project event for ${childId}: ${errorMessage(error)}`);
|
|
1304
1234
|
});
|
|
1305
1235
|
}
|
|
1306
1236
|
handleConnectionChange(childId, connected) {
|
|
1307
|
-
this.#bumpActivity(childId);
|
|
1237
|
+
this.#bumpActivity(childId, false);
|
|
1308
1238
|
void this.#mutate(childId, async () => {
|
|
1309
1239
|
const record = await this.#deps.registry.get(childId);
|
|
1310
1240
|
if (record === undefined)
|
|
@@ -1316,8 +1246,6 @@ export class SubagentManager {
|
|
|
1316
1246
|
// the child's transcript from the session the human switched to.
|
|
1317
1247
|
if (this.#leftSessions.has(childId))
|
|
1318
1248
|
return;
|
|
1319
|
-
// The runtime this record names is answering, so the doubt about it is gone.
|
|
1320
|
-
this.#unresolvedRuntimes.delete(childId);
|
|
1321
1249
|
if (!this.#projectors.has(childId)) {
|
|
1322
1250
|
this.#projectors.set(childId, createStateProjector(record.state));
|
|
1323
1251
|
}
|
|
@@ -1329,18 +1257,7 @@ export class SubagentManager {
|
|
|
1329
1257
|
await this.#runtimeEnded(childId);
|
|
1330
1258
|
});
|
|
1331
1259
|
}
|
|
1332
|
-
#dispatch(childId, event) {
|
|
1333
|
-
for (const listener of this.#childEventListeners) {
|
|
1334
|
-
try {
|
|
1335
|
-
listener(childId, event);
|
|
1336
|
-
}
|
|
1337
|
-
catch (error) {
|
|
1338
|
-
diagnose(`child event listener failed for ${childId}: ${errorMessage(error)}`);
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
1260
|
async #observe(id, event, workVersion) {
|
|
1343
|
-
this.#dispatch(id, event);
|
|
1344
1261
|
const value = isRecord(event) ? event : undefined;
|
|
1345
1262
|
if (value === undefined)
|
|
1346
1263
|
return;
|
|
@@ -1356,11 +1273,7 @@ export class SubagentManager {
|
|
|
1356
1273
|
const message = value.message === undefined || value.message === ""
|
|
1357
1274
|
? "Task is unfinished and waiting for user intent."
|
|
1358
1275
|
: value.message;
|
|
1359
|
-
|
|
1360
|
-
...current,
|
|
1361
|
-
interrupted: message,
|
|
1362
|
-
}));
|
|
1363
|
-
await this.contactParent(id, "user_interrupt", message, updated);
|
|
1276
|
+
await this.contactParent(id, "user_interrupt", message, record);
|
|
1364
1277
|
return;
|
|
1365
1278
|
}
|
|
1366
1279
|
if (value.kind === "left_session") {
|
|
@@ -1374,18 +1287,10 @@ export class SubagentManager {
|
|
|
1374
1287
|
this.#projectors.delete(id);
|
|
1375
1288
|
this.#attachments.delete(id);
|
|
1376
1289
|
this.#leftSessions.add(id);
|
|
1377
|
-
this.#unresolvedRuntimes.delete(id);
|
|
1378
1290
|
// The socket is not this child's channel any more either: leaving it up would let a
|
|
1379
1291
|
// relaunch mistake the old connection for the new runtime.
|
|
1380
1292
|
this.#deps.bridge.disconnect(id);
|
|
1381
|
-
await this.#
|
|
1382
|
-
const { runtime: _runtime, ...withoutRuntime } = current;
|
|
1383
|
-
return {
|
|
1384
|
-
...withoutRuntime,
|
|
1385
|
-
state: "done",
|
|
1386
|
-
interrupted: "The child left its session; the surface is no longer controlled by the parent",
|
|
1387
|
-
};
|
|
1388
|
-
});
|
|
1293
|
+
await commitLease(this.#deps.registry, id, { type: "left-session" });
|
|
1389
1294
|
if (retired !== undefined)
|
|
1390
1295
|
this.#deps.tokens?.forget(retired);
|
|
1391
1296
|
return;
|
|
@@ -1410,14 +1315,13 @@ export class SubagentManager {
|
|
|
1410
1315
|
return;
|
|
1411
1316
|
projector.applyEvent(event);
|
|
1412
1317
|
let confirmedInput;
|
|
1413
|
-
if (value.type === "agent_end") {
|
|
1318
|
+
if (value.type === "turn_end" || value.type === "agent_end") {
|
|
1414
1319
|
// A finished turn is exactly when the session file has the message, so this is where
|
|
1415
1320
|
// unacknowledged input becomes part of the child's conversation.
|
|
1416
|
-
const
|
|
1417
|
-
if (
|
|
1418
|
-
projector.
|
|
1419
|
-
if (record.unacknowledgedInput !== undefined &&
|
|
1420
|
-
sessionContainsUserInput(entries, record.unacknowledgedInput)) {
|
|
1321
|
+
const facts = await this.#facts(id, record.unacknowledgedInput);
|
|
1322
|
+
if (facts !== undefined) {
|
|
1323
|
+
projector.applyTranscriptFacts(facts);
|
|
1324
|
+
if (record.unacknowledgedInput !== undefined && facts.containsInput) {
|
|
1421
1325
|
confirmedInput = record.unacknowledgedInput;
|
|
1422
1326
|
}
|
|
1423
1327
|
}
|
|
@@ -1426,20 +1330,23 @@ export class SubagentManager {
|
|
|
1426
1330
|
if (current === undefined || current.intent === "stopped")
|
|
1427
1331
|
return;
|
|
1428
1332
|
const snapshot = projector.snapshot();
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1333
|
+
const durable = durableProjection(current, snapshot, confirmedInput !== undefined);
|
|
1334
|
+
if (durable !== undefined) {
|
|
1335
|
+
await this.#update(id, (entry) => {
|
|
1336
|
+
const { interrupted: _prevInterrupted, activeTool: _prevActiveTool, latestSummary: _prevSummary, usage: _prevUsage, ...cleanEntry } = entry;
|
|
1337
|
+
const base = confirmedInput === undefined
|
|
1338
|
+
? cleanEntry
|
|
1339
|
+
: (({ unacknowledgedInput: _input, ...rest }) => rest)(cleanEntry);
|
|
1340
|
+
return {
|
|
1341
|
+
...base,
|
|
1342
|
+
state: snapshot.state,
|
|
1343
|
+
...(snapshot.interrupted === undefined ? {} : { interrupted: snapshot.interrupted }),
|
|
1344
|
+
};
|
|
1345
|
+
}, workVersion);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
this.#notify();
|
|
1349
|
+
}
|
|
1443
1350
|
if (snapshot.state === "done" || snapshot.state === "blocked" || snapshot.state === "error") {
|
|
1444
1351
|
this.#scheduleSettledPanelClose(id);
|
|
1445
1352
|
}
|
|
@@ -1453,6 +1360,82 @@ export class SubagentManager {
|
|
|
1453
1360
|
this.#scheduleAutoReport(id, latestRecord);
|
|
1454
1361
|
}
|
|
1455
1362
|
}
|
|
1363
|
+
holdAttachment(id, attachment) {
|
|
1364
|
+
this.#attachments.set(id, attachment);
|
|
1365
|
+
}
|
|
1366
|
+
/** Starts restoration. `before_agent_start` waits on `ready` so the turn sees the same ledger. */
|
|
1367
|
+
reconcile() {
|
|
1368
|
+
if (this.#ready !== undefined)
|
|
1369
|
+
return this.#ready;
|
|
1370
|
+
const ready = this.#reconcile();
|
|
1371
|
+
this.#ready = ready;
|
|
1372
|
+
return ready;
|
|
1373
|
+
}
|
|
1374
|
+
ready() {
|
|
1375
|
+
return this.reconcile();
|
|
1376
|
+
}
|
|
1377
|
+
async #reconcile() {
|
|
1378
|
+
const directory = this.#deps.directory;
|
|
1379
|
+
if (directory === undefined) {
|
|
1380
|
+
const recovery = await this.recover();
|
|
1381
|
+
return {
|
|
1382
|
+
ok: true,
|
|
1383
|
+
rebound: recovery.recovered,
|
|
1384
|
+
rehydrated: [],
|
|
1385
|
+
stopped: [],
|
|
1386
|
+
released: [],
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
const lease = createConversationLease({
|
|
1390
|
+
registry: this.#deps.registry,
|
|
1391
|
+
hosts: directory,
|
|
1392
|
+
processes: { alive: processAlive },
|
|
1393
|
+
forgetRuntime: (runtimeIdentity) => {
|
|
1394
|
+
this.#deps.tokens?.forget(runtimeIdentity);
|
|
1395
|
+
},
|
|
1396
|
+
});
|
|
1397
|
+
const result = await lease.ready();
|
|
1398
|
+
if (!result.ok)
|
|
1399
|
+
return result;
|
|
1400
|
+
for (const id of result.rebound) {
|
|
1401
|
+
const record = await this.#deps.registry.get(id);
|
|
1402
|
+
const attachment = record?.runtime?.attachment;
|
|
1403
|
+
if (attachment?.kind === "panel") {
|
|
1404
|
+
const handle = directory.rebind?.(attachment);
|
|
1405
|
+
if (handle !== undefined)
|
|
1406
|
+
this.#attachments.set(id, handle);
|
|
1407
|
+
}
|
|
1408
|
+
else if (attachment?.kind === "process") {
|
|
1409
|
+
const runtime = attachRetainedChild(attachment.pid);
|
|
1410
|
+
if (runtime !== undefined)
|
|
1411
|
+
this.#watchRuntime(id, runtime);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
for (const id of result.released) {
|
|
1415
|
+
await this.#noteReleasedRuntime(id);
|
|
1416
|
+
}
|
|
1417
|
+
return result;
|
|
1418
|
+
}
|
|
1419
|
+
/** A writer the lease already dropped. Same ledger line as a watched exit. */
|
|
1420
|
+
async #noteReleasedRuntime(id) {
|
|
1421
|
+
const record = await this.#deps.registry.get(id);
|
|
1422
|
+
if (record?.state !== "blocked" || record.interrupted !== RUNTIME_ENDED_RESUME)
|
|
1423
|
+
return;
|
|
1424
|
+
this.#scheduleAutoReport(id, record);
|
|
1425
|
+
}
|
|
1426
|
+
#withSnapshot(record) {
|
|
1427
|
+
const projector = this.#projectors.get(record.subagentId);
|
|
1428
|
+
if (projector === undefined)
|
|
1429
|
+
return record;
|
|
1430
|
+
const snap = projector.snapshot();
|
|
1431
|
+
return {
|
|
1432
|
+
...record,
|
|
1433
|
+
...(snap.activeTool !== undefined ? { activeTool: snap.activeTool } : {}),
|
|
1434
|
+
...(snap.summary !== undefined ? { latestSummary: snap.summary } : {}),
|
|
1435
|
+
...(snap.interrupted !== undefined ? { interrupted: snap.interrupted } : {}),
|
|
1436
|
+
usage: snap.usage ?? record.usage,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1456
1439
|
/** Runs an update inside the per-child chain used by the mutating operations. */
|
|
1457
1440
|
#mutateUpdate(id, updater) {
|
|
1458
1441
|
return this.#mutate(id, () => this.#update(id, updater));
|
|
@@ -1483,3 +1466,43 @@ export class SubagentManager {
|
|
|
1483
1466
|
return next;
|
|
1484
1467
|
}
|
|
1485
1468
|
}
|
|
1469
|
+
function durableProjection(record, snapshot, inputConfirmed) {
|
|
1470
|
+
if (inputConfirmed)
|
|
1471
|
+
return true;
|
|
1472
|
+
if (snapshot.state !== record.state)
|
|
1473
|
+
return true;
|
|
1474
|
+
return undefined;
|
|
1475
|
+
}
|
|
1476
|
+
function resumesChildWork(type) {
|
|
1477
|
+
return (type === "agent_start" ||
|
|
1478
|
+
type === "turn_start" ||
|
|
1479
|
+
type === "auto_retry_start" ||
|
|
1480
|
+
type === "auto_retry");
|
|
1481
|
+
}
|
|
1482
|
+
/** The last conversational message is an assistant reply that already finished. */
|
|
1483
|
+
function persistedReplyFinished(entries) {
|
|
1484
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
1485
|
+
const entry = entries[index];
|
|
1486
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message))
|
|
1487
|
+
continue;
|
|
1488
|
+
const message = entry.message;
|
|
1489
|
+
if (message.role === "user")
|
|
1490
|
+
return false;
|
|
1491
|
+
if (message.role !== "assistant")
|
|
1492
|
+
continue;
|
|
1493
|
+
return (message.stopReason !== "error" &&
|
|
1494
|
+
message.stopReason !== "aborted" &&
|
|
1495
|
+
message.stopReason !== "pending" &&
|
|
1496
|
+
message.stopReason !== "deferred");
|
|
1497
|
+
}
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
function processAlive(pid) {
|
|
1501
|
+
try {
|
|
1502
|
+
process.kill(pid, 0);
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
catch {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
}
|