@hheei/pi-subagents 0.2.3 → 0.3.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 +32 -17
- package/dist/agent-resolver.d.ts +9 -0
- package/dist/agent-resolver.js +278 -51
- package/dist/bridge-client.d.ts +5 -0
- package/dist/bridge-client.js +6 -0
- 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 +5 -3
- package/dist/child-control.js +176 -35
- package/dist/commands.d.ts +1 -4
- package/dist/commands.js +1 -52
- package/dist/domain.d.ts +12 -12
- package/dist/domain.js +2 -1
- package/dist/extension.d.ts +5 -3
- package/dist/extension.js +42 -44
- package/dist/host-adapter.js +1 -1
- package/dist/info.d.ts +9 -0
- package/dist/info.js +54 -0
- package/dist/launch-spec.d.ts +0 -1
- package/dist/launch-spec.js +38 -18
- package/dist/manager.d.ts +4 -2
- package/dist/manager.js +299 -114
- package/dist/registry.js +19 -5
- package/dist/rpc-events.d.ts +1 -1
- package/dist/rpc-events.js +0 -1
- package/dist/runtime.js +8 -0
- package/dist/session-bootstrap.d.ts +2 -1
- package/dist/session-bootstrap.js +22 -14
- package/dist/state.d.ts +13 -0
- package/dist/state.js +59 -16
- 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 +17 -0
- package/dist/tool-presentation.js +160 -0
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +133 -180
- package/package.json +6 -6
- 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,12 +1,29 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { errorMessage, isRecord } from "@hheei/pi-ext-core";
|
|
3
|
+
import { isOperationError } from "./domain.js";
|
|
2
4
|
import { stripHindsightContent } from "./launch-spec.js";
|
|
3
5
|
import { BridgeError, isChildInputPayload, isChildLifecyclePayload, isContactReportPayload, } from "./protocol.js";
|
|
4
6
|
import { findSessionFile, planSessionPlacement } from "./session-bootstrap.js";
|
|
5
|
-
|
|
7
|
+
async function readSessionJsonlEntries(filePath) {
|
|
8
|
+
const content = await readFile(filePath, "utf8");
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const line of content.split("\n")) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
if (trimmed.length > 0) {
|
|
13
|
+
try {
|
|
14
|
+
entries.push(JSON.parse(trimmed));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Ignore corrupted line
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return entries;
|
|
22
|
+
}
|
|
6
23
|
import { createStateProjector } from "./state.js";
|
|
7
24
|
/**
|
|
8
25
|
* 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 `
|
|
26
|
+
* session lock, so an idle one is reclaimed after this window and a later `agent_send` starts it
|
|
10
27
|
* again from the same session.
|
|
11
28
|
*/
|
|
12
29
|
export const SUBAGENT_IDLE_TIMEOUT_MS = 15_000;
|
|
@@ -29,9 +46,19 @@ function failure(operation, reason, childId, state, sideEffects = [], safeToRetr
|
|
|
29
46
|
safeToRetry,
|
|
30
47
|
};
|
|
31
48
|
}
|
|
32
|
-
function
|
|
49
|
+
function acknowledgedPath(response) {
|
|
50
|
+
if (!isRecord(response) ||
|
|
51
|
+
response.accepted !== true ||
|
|
52
|
+
typeof response.path !== "string" ||
|
|
53
|
+
response.path === "") {
|
|
54
|
+
throw new BridgeError("invalid_state", "Child did not confirm its session path");
|
|
55
|
+
}
|
|
56
|
+
return response.path;
|
|
57
|
+
}
|
|
58
|
+
function publicChild(record, live, path = record.sessionPath) {
|
|
33
59
|
return {
|
|
34
60
|
id: record.subagentId,
|
|
61
|
+
...(path === undefined ? {} : { path }),
|
|
35
62
|
agent: record.launchConfig.agent.name,
|
|
36
63
|
...(record.launchConfig.agent.displayName === undefined
|
|
37
64
|
? {}
|
|
@@ -40,12 +67,12 @@ function publicChild(record, live) {
|
|
|
40
67
|
presentation: record.presentation,
|
|
41
68
|
cwd: record.cwd,
|
|
42
69
|
sessionId: record.sessionId,
|
|
70
|
+
...(record.initialTask === undefined ? {} : { task: record.initialTask }),
|
|
43
71
|
...(record.latestSummary === undefined ? {} : { summary: record.latestSummary }),
|
|
44
72
|
...(record.activeTool === undefined ? {} : { activeTool: record.activeTool }),
|
|
45
73
|
...(record.usage === undefined ? {} : { usage: record.usage }),
|
|
46
74
|
...(record.interrupted === undefined ? {} : { interrupted: record.interrupted }),
|
|
47
75
|
freshness: live ? "live" : "last_known",
|
|
48
|
-
interactive: record.launchConfig.interactive,
|
|
49
76
|
model: record.launchConfig.model,
|
|
50
77
|
thinking: record.launchConfig.thinking,
|
|
51
78
|
createdAt: record.createdAt,
|
|
@@ -131,12 +158,15 @@ export class SubagentManager {
|
|
|
131
158
|
/** Bumped by every inbound bridge signal, so a reclaim in flight can tell the child moved. */
|
|
132
159
|
#activity = new Map();
|
|
133
160
|
#projectors = new Map();
|
|
161
|
+
/** Display paths do not imply that a session has been flushed to disk. */
|
|
162
|
+
#paths = new Map();
|
|
134
163
|
/** Known child work in flight, separate from failures recorded before Pi finally settles. */
|
|
135
164
|
#working = new Set();
|
|
136
165
|
/** A settled event from an older run cannot clear a newer synchronous start signal. */
|
|
137
166
|
#workVersions = new Map();
|
|
138
167
|
#settledPanelCloseTimers = new Map();
|
|
139
|
-
#
|
|
168
|
+
#pendingAutoReports = new Map();
|
|
169
|
+
#autoReportBatchTimer;
|
|
140
170
|
#lastDeliveredTurn = new Map();
|
|
141
171
|
#stopping = new Set();
|
|
142
172
|
#listeners = new Set();
|
|
@@ -168,7 +198,7 @@ export class SubagentManager {
|
|
|
168
198
|
this.#childEventListeners.delete(listener);
|
|
169
199
|
};
|
|
170
200
|
}
|
|
171
|
-
/** Presentation-only:
|
|
201
|
+
/** Presentation-only: status and similar projections re-read list() after this. */
|
|
172
202
|
onChange(listener) {
|
|
173
203
|
this.#listeners.add(listener);
|
|
174
204
|
return () => {
|
|
@@ -176,6 +206,11 @@ export class SubagentManager {
|
|
|
176
206
|
};
|
|
177
207
|
}
|
|
178
208
|
dispose() {
|
|
209
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
210
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
211
|
+
this.#autoReportBatchTimer = undefined;
|
|
212
|
+
}
|
|
213
|
+
this.#pendingAutoReports.clear();
|
|
179
214
|
for (const timer of this.#settledPanelCloseTimers.values()) {
|
|
180
215
|
clearTimeout(timer);
|
|
181
216
|
}
|
|
@@ -190,12 +225,18 @@ export class SubagentManager {
|
|
|
190
225
|
*/
|
|
191
226
|
closeLocalConnections() {
|
|
192
227
|
this.dispose();
|
|
228
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
229
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
230
|
+
this.#autoReportBatchTimer = undefined;
|
|
231
|
+
}
|
|
232
|
+
this.#pendingAutoReports.clear();
|
|
193
233
|
this.#processes.clear();
|
|
194
234
|
this.#attachments.clear();
|
|
195
235
|
this.#leftSessions.clear();
|
|
196
236
|
this.#unresolvedRuntimes.clear();
|
|
197
237
|
this.#activity.clear();
|
|
198
238
|
this.#projectors.clear();
|
|
239
|
+
this.#paths.clear();
|
|
199
240
|
this.#working.clear();
|
|
200
241
|
this.#workVersions.clear();
|
|
201
242
|
this.#notify();
|
|
@@ -299,13 +340,15 @@ export class SubagentManager {
|
|
|
299
340
|
// A panel the host confirms is gone stops holding this child — the handle it left behind is
|
|
300
341
|
// also what a retried stop would act on, so it is released here before refusing.
|
|
301
342
|
await this.#releasePanel(id).catch(() => undefined);
|
|
302
|
-
if ((await this.#deps.registry.get(id))?.intent === "stopped") {
|
|
303
|
-
return STOPPED_SEND_REASON;
|
|
304
|
-
}
|
|
305
343
|
}
|
|
306
344
|
if (this.#runtimeStarted(id)) {
|
|
307
345
|
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
346
|
}
|
|
347
|
+
if (this.#attachments.has(id)) {
|
|
348
|
+
if ((await this.#deps.registry.get(id))?.intent === "stopped") {
|
|
349
|
+
return STOPPED_SEND_REASON;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
309
352
|
if (this.#unresolvedRuntimes.has(id)) {
|
|
310
353
|
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";
|
|
311
354
|
}
|
|
@@ -346,7 +389,7 @@ export class SubagentManager {
|
|
|
346
389
|
...withoutRuntime,
|
|
347
390
|
intent: "stopped",
|
|
348
391
|
state: "error",
|
|
349
|
-
interrupted: "The child
|
|
392
|
+
interrupted: "The host reports that the child process exited while running; the cause is unknown. Spawn a new child for more work",
|
|
350
393
|
};
|
|
351
394
|
});
|
|
352
395
|
if (retired !== undefined)
|
|
@@ -397,19 +440,29 @@ export class SubagentManager {
|
|
|
397
440
|
* only ever closed on a judgement, never on a guess.
|
|
398
441
|
*/
|
|
399
442
|
#scheduleAutoReport(id, record) {
|
|
400
|
-
this.#
|
|
401
|
-
|
|
402
|
-
this.#
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
this.#
|
|
443
|
+
this.#pendingAutoReports.set(id, record);
|
|
444
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
445
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
446
|
+
}
|
|
447
|
+
// Batch reports: if all children settled, flush in 500ms; otherwise wait up to 10000ms for others
|
|
448
|
+
const delayMs = this.#working.size > 0 ? 10_000 : 500;
|
|
449
|
+
this.#autoReportBatchTimer = setTimeout(async () => {
|
|
450
|
+
this.#autoReportBatchTimer = undefined;
|
|
451
|
+
const batch = [...this.#pendingAutoReports.entries()];
|
|
452
|
+
this.#pendingAutoReports.clear();
|
|
453
|
+
for (const [childId, childRecord] of batch) {
|
|
454
|
+
await this.#performAutoReport(childId, childRecord).catch((error) => {
|
|
455
|
+
diagnose(`failed to report child ${childId}: ${errorMessage(error)}`);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}, delayMs);
|
|
459
|
+
this.#autoReportBatchTimer.unref?.();
|
|
407
460
|
}
|
|
408
461
|
#clearAutoReport(id) {
|
|
409
|
-
|
|
410
|
-
if (
|
|
411
|
-
clearTimeout(
|
|
412
|
-
this.#
|
|
462
|
+
this.#pendingAutoReports.delete(id);
|
|
463
|
+
if (this.#pendingAutoReports.size === 0 && this.#autoReportBatchTimer !== undefined) {
|
|
464
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
465
|
+
this.#autoReportBatchTimer = undefined;
|
|
413
466
|
}
|
|
414
467
|
}
|
|
415
468
|
#scheduleSettledPanelClose(id) {
|
|
@@ -443,7 +496,10 @@ export class SubagentManager {
|
|
|
443
496
|
async #closeSettledPanel(id) {
|
|
444
497
|
await this.#mutate(id, async () => {
|
|
445
498
|
const record = await this.#deps.registry.get(id);
|
|
446
|
-
if (record === undefined ||
|
|
499
|
+
if (record === undefined ||
|
|
500
|
+
record.state === "running" ||
|
|
501
|
+
record.intent === "stopped" ||
|
|
502
|
+
this.#working.has(id)) {
|
|
447
503
|
return;
|
|
448
504
|
}
|
|
449
505
|
const attachment = this.#attachments.get(id);
|
|
@@ -516,83 +572,113 @@ export class SubagentManager {
|
|
|
516
572
|
const record = (await this.#deps.registry.get(id)) ?? _record;
|
|
517
573
|
if (record.intent === "stopped" || record.state === "running")
|
|
518
574
|
return;
|
|
575
|
+
const activity = this.#activity.get(id) ?? 0;
|
|
576
|
+
const workVersion = this.#workVersions.get(id) ?? 0;
|
|
519
577
|
const entries = await this.#entries(id);
|
|
520
578
|
const text = entries !== undefined ? extractLastAssistantText(entries) : undefined;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
579
|
+
await this.#mutate(id, async () => {
|
|
580
|
+
const current = await this.#deps.registry.get(id);
|
|
581
|
+
if (current === undefined ||
|
|
582
|
+
current.intent === "stopped" ||
|
|
583
|
+
current.state === "running" ||
|
|
584
|
+
this.#working.has(id) ||
|
|
585
|
+
(this.#activity.get(id) ?? 0) !== activity ||
|
|
586
|
+
(this.#workVersions.get(id) ?? 0) !== workVersion)
|
|
587
|
+
return;
|
|
588
|
+
const isError = current.state === "error" || record.state === "error";
|
|
589
|
+
const isBlocked = !isError &&
|
|
590
|
+
(current.state === "blocked" ||
|
|
591
|
+
record.state === "blocked" ||
|
|
592
|
+
(current.interrupted !== undefined && current.interrupted.trim() !== "") ||
|
|
593
|
+
(record.interrupted !== undefined && record.interrupted.trim() !== ""));
|
|
594
|
+
const failed = isError || isBlocked;
|
|
595
|
+
if (!failed && (text === undefined || text.trim() === "")) {
|
|
596
|
+
const projector = this.#projectors.get(id);
|
|
597
|
+
if (projector !== undefined) {
|
|
598
|
+
projector.syncState("done");
|
|
599
|
+
}
|
|
600
|
+
await this.#update(id, (current) => {
|
|
601
|
+
const { interrupted: _interrupted, ...clean } = current;
|
|
602
|
+
return {
|
|
603
|
+
...clean,
|
|
604
|
+
state: "done",
|
|
605
|
+
};
|
|
606
|
+
});
|
|
607
|
+
if (this.#taskRegistry !== undefined) {
|
|
608
|
+
this.#taskRegistry.settle(id, {
|
|
609
|
+
status: "completed",
|
|
610
|
+
output: "",
|
|
611
|
+
truncated: false,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
let message;
|
|
617
|
+
const interruptedText = current.interrupted?.trim() || record.interrupted?.trim();
|
|
618
|
+
if (isError) {
|
|
619
|
+
message = interruptedText || "Subagent failed without error details.";
|
|
620
|
+
}
|
|
621
|
+
else if (isBlocked) {
|
|
622
|
+
if (interruptedText !== undefined && interruptedText !== "") {
|
|
623
|
+
message =
|
|
624
|
+
text !== undefined && text.trim() !== ""
|
|
625
|
+
? `${interruptedText}\n\n${text}`
|
|
626
|
+
: interruptedText;
|
|
627
|
+
}
|
|
628
|
+
else if (text !== undefined && text.trim() !== "") {
|
|
629
|
+
message = text;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
message = "Subagent became idle without completing its task (interrupted or failed).";
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
message = text ?? "";
|
|
637
|
+
}
|
|
638
|
+
const lastDelivered = this.#lastDeliveredTurn.get(id);
|
|
639
|
+
if (lastDelivered !== undefined &&
|
|
640
|
+
lastDelivered.workVersion === workVersion &&
|
|
641
|
+
lastDelivered.message === message) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const reason = isError ? "error" : isBlocked ? "blocked" : "success";
|
|
645
|
+
const taskStatus = failed ? "failed" : "completed";
|
|
525
646
|
const projector = this.#projectors.get(id);
|
|
647
|
+
const updatedRecord = !failed
|
|
648
|
+
? await this.#update(id, (current) => {
|
|
649
|
+
const { interrupted: _interrupted, ...clean } = current;
|
|
650
|
+
return {
|
|
651
|
+
...clean,
|
|
652
|
+
state: "done",
|
|
653
|
+
};
|
|
654
|
+
})
|
|
655
|
+
: await this.#update(id, (current) => ({
|
|
656
|
+
...current,
|
|
657
|
+
state: isError ? "error" : "blocked",
|
|
658
|
+
interrupted: current.interrupted ?? message,
|
|
659
|
+
}));
|
|
526
660
|
if (projector !== undefined) {
|
|
527
|
-
projector.syncState("done");
|
|
661
|
+
projector.syncState(isError ? "error" : isBlocked ? "blocked" : "done");
|
|
528
662
|
}
|
|
529
|
-
|
|
530
|
-
const { interrupted: _interrupted, ...clean } = current;
|
|
531
|
-
return {
|
|
532
|
-
...clean,
|
|
533
|
-
state: "done",
|
|
534
|
-
};
|
|
535
|
-
});
|
|
663
|
+
this.#scheduleSettledPanelClose(id);
|
|
536
664
|
if (this.#taskRegistry !== undefined) {
|
|
537
665
|
this.#taskRegistry.settle(id, {
|
|
538
|
-
status:
|
|
539
|
-
output:
|
|
666
|
+
status: taskStatus,
|
|
667
|
+
output: message,
|
|
540
668
|
truncated: false,
|
|
541
669
|
});
|
|
542
670
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
text !== undefined && text.trim() !== ""
|
|
550
|
-
? `${record.interrupted}\n\n${text}`
|
|
551
|
-
: record.interrupted;
|
|
552
|
-
}
|
|
553
|
-
else if (text !== undefined && text.trim() !== "") {
|
|
554
|
-
message = text;
|
|
671
|
+
let deliveryOk = true;
|
|
672
|
+
if (this.#deps.channel !== undefined) {
|
|
673
|
+
const contactResult = await this.contactParent(id, reason, message, updatedRecord);
|
|
674
|
+
if (isOperationError(contactResult)) {
|
|
675
|
+
deliveryOk = false;
|
|
676
|
+
}
|
|
555
677
|
}
|
|
556
|
-
|
|
557
|
-
|
|
678
|
+
if (deliveryOk) {
|
|
679
|
+
this.#lastDeliveredTurn.set(id, { workVersion, message });
|
|
558
680
|
}
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
message = text ?? "";
|
|
562
|
-
}
|
|
563
|
-
if (this.#lastDeliveredTurn.get(id) === message)
|
|
564
|
-
return;
|
|
565
|
-
this.#lastDeliveredTurn.set(id, message);
|
|
566
|
-
const reason = isBlocked ? "blocked" : "success";
|
|
567
|
-
const taskStatus = isBlocked ? "failed" : "completed";
|
|
568
|
-
const projector = this.#projectors.get(id);
|
|
569
|
-
const updatedRecord = !isBlocked
|
|
570
|
-
? await this.#update(id, (current) => {
|
|
571
|
-
const { interrupted: _interrupted, ...clean } = current;
|
|
572
|
-
return {
|
|
573
|
-
...clean,
|
|
574
|
-
state: "done",
|
|
575
|
-
};
|
|
576
|
-
})
|
|
577
|
-
: await this.#update(id, (current) => ({
|
|
578
|
-
...current,
|
|
579
|
-
state: "blocked",
|
|
580
|
-
interrupted: current.interrupted ?? message,
|
|
581
|
-
}));
|
|
582
|
-
if (projector !== undefined) {
|
|
583
|
-
projector.syncState(isBlocked ? "blocked" : "done");
|
|
584
|
-
}
|
|
585
|
-
this.#scheduleSettledPanelClose(id);
|
|
586
|
-
if (this.#deps.channel !== undefined) {
|
|
587
|
-
await this.contactParent(id, reason, message, updatedRecord);
|
|
588
|
-
}
|
|
589
|
-
if (this.#taskRegistry !== undefined) {
|
|
590
|
-
this.#taskRegistry.settle(id, {
|
|
591
|
-
status: taskStatus,
|
|
592
|
-
output: message,
|
|
593
|
-
truncated: false,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
681
|
+
});
|
|
596
682
|
}
|
|
597
683
|
/** Reads a child's session entries; exposed for Task completion inspection. */
|
|
598
684
|
async getEntries(id) {
|
|
@@ -728,11 +814,37 @@ export class SubagentManager {
|
|
|
728
814
|
*/
|
|
729
815
|
async #settleUnrecovered(record) {
|
|
730
816
|
this.#working.delete(record.subagentId);
|
|
817
|
+
this.#projectors.delete(record.subagentId);
|
|
731
818
|
const retired = record.runtime;
|
|
732
|
-
if (retired === undefined)
|
|
733
|
-
return;
|
|
734
819
|
if (record.presentation === "panel") {
|
|
735
|
-
|
|
820
|
+
if (retired !== undefined) {
|
|
821
|
+
this.#unresolvedRuntimes.add(record.subagentId);
|
|
822
|
+
}
|
|
823
|
+
await this.#update(record.subagentId, (current) => {
|
|
824
|
+
if (current.intent === "stopped")
|
|
825
|
+
return current;
|
|
826
|
+
if (current.state !== "running")
|
|
827
|
+
return current;
|
|
828
|
+
return {
|
|
829
|
+
...current,
|
|
830
|
+
state: "blocked",
|
|
831
|
+
interrupted: "The child panel runtime did not reconnect; its process exit is not confirmed",
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (retired === undefined) {
|
|
837
|
+
await this.#update(record.subagentId, (current) => {
|
|
838
|
+
if (current.intent === "stopped")
|
|
839
|
+
return current;
|
|
840
|
+
if (current.state !== "running")
|
|
841
|
+
return current;
|
|
842
|
+
return {
|
|
843
|
+
...current,
|
|
844
|
+
state: "done",
|
|
845
|
+
interrupted: "The child runtime ended with the parent process; a later send starts it again from the same session",
|
|
846
|
+
};
|
|
847
|
+
});
|
|
736
848
|
return;
|
|
737
849
|
}
|
|
738
850
|
await this.#update(record.subagentId, (current) => {
|
|
@@ -762,6 +874,9 @@ export class SubagentManager {
|
|
|
762
874
|
throw new BridgeError("invalid_state", `Child ${id} did not confirm its current work state`);
|
|
763
875
|
}
|
|
764
876
|
const idle = state.idle && !state.pendingMessages;
|
|
877
|
+
if (typeof state.path === "string" && state.path !== "") {
|
|
878
|
+
this.#paths.set(id, state.path);
|
|
879
|
+
}
|
|
765
880
|
if ((this.#workVersions.get(id) ?? 0) === workVersion) {
|
|
766
881
|
if (idle)
|
|
767
882
|
this.#working.delete(id);
|
|
@@ -786,8 +901,9 @@ export class SubagentManager {
|
|
|
786
901
|
? current
|
|
787
902
|
: (({ unacknowledgedInput: _input, ...rest }) => rest)(current);
|
|
788
903
|
const hasError = snapshot.state === "error";
|
|
904
|
+
const { interrupted: _interrupted, ...withoutInterrupted } = base;
|
|
789
905
|
return {
|
|
790
|
-
...
|
|
906
|
+
...withoutInterrupted,
|
|
791
907
|
state: hasError ? "error" : idle ? "done" : "running",
|
|
792
908
|
...(snapshot.summary === undefined ? {} : { latestSummary: snapshot.summary }),
|
|
793
909
|
usage: snapshot.usage,
|
|
@@ -848,9 +964,11 @@ export class SubagentManager {
|
|
|
848
964
|
childId = record.subagentId;
|
|
849
965
|
this.#working.add(childId);
|
|
850
966
|
this.#notify();
|
|
851
|
-
await withDeadline(this.#start(record), this.#deps.deadlineMs ??
|
|
967
|
+
await withDeadline(this.#start(record), this.#deps.deadlineMs ?? 60_000);
|
|
968
|
+
let path;
|
|
852
969
|
try {
|
|
853
|
-
await this.#request(record.subagentId, "prompt", { message: cleanTask });
|
|
970
|
+
path = acknowledgedPath(await this.#request(record.subagentId, "prompt", { message: cleanTask }));
|
|
971
|
+
this.#paths.set(record.subagentId, path);
|
|
854
972
|
}
|
|
855
973
|
catch (error) {
|
|
856
974
|
let failureState = "error";
|
|
@@ -891,6 +1009,9 @@ export class SubagentManager {
|
|
|
891
1009
|
output: `Subagent ${record.subagentId} (${input.agent}) is running`,
|
|
892
1010
|
truncated: false,
|
|
893
1011
|
}),
|
|
1012
|
+
consume: () => {
|
|
1013
|
+
this.#deps.channel?.dismiss?.(record.subagentId);
|
|
1014
|
+
},
|
|
894
1015
|
}),
|
|
895
1016
|
});
|
|
896
1017
|
}
|
|
@@ -899,31 +1020,52 @@ export class SubagentManager {
|
|
|
899
1020
|
}
|
|
900
1021
|
}
|
|
901
1022
|
return {
|
|
902
|
-
child: publicChild((await this.#deps.registry.get(record.subagentId)) ?? record, true),
|
|
1023
|
+
child: publicChild((await this.#deps.registry.get(record.subagentId)) ?? record, true, path),
|
|
1024
|
+
path,
|
|
903
1025
|
...(placement.note === undefined ? {} : { note: placement.note }),
|
|
904
1026
|
};
|
|
905
1027
|
}
|
|
906
1028
|
catch (error) {
|
|
907
|
-
if (childId
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1029
|
+
if (childId === undefined)
|
|
1030
|
+
return failure("spawn", errorMessage(error));
|
|
1031
|
+
const failedId = childId;
|
|
1032
|
+
const reason = errorMessage(error);
|
|
1033
|
+
const failed = await this.#mutate(failedId, async () => {
|
|
1034
|
+
const record = await this.#deps.registry.get(failedId);
|
|
1035
|
+
const uncertain = this.#runtimeStarted(failedId) ||
|
|
1036
|
+
this.#unresolvedRuntimes.has(failedId) ||
|
|
1037
|
+
record?.runtime !== undefined;
|
|
1038
|
+
if (uncertain)
|
|
1039
|
+
this.#unresolvedRuntimes.add(failedId);
|
|
1040
|
+
if (!this.#isLive(failedId))
|
|
1041
|
+
this.#working.delete(failedId);
|
|
1042
|
+
await this.#update(failedId, (current) => ({
|
|
1043
|
+
...current,
|
|
1044
|
+
state: "error",
|
|
1045
|
+
interrupted: reason,
|
|
1046
|
+
}));
|
|
1047
|
+
this.#projectors.get(failedId)?.syncState("error");
|
|
1048
|
+
return failure("spawn", reason, failedId, "error", [
|
|
1049
|
+
"registry record persisted",
|
|
1050
|
+
...(uncertain ? ["runtime start or cleanup unconfirmed"] : ["no runtime remains"]),
|
|
1051
|
+
], !uncertain);
|
|
1052
|
+
});
|
|
1053
|
+
return failed;
|
|
914
1054
|
}
|
|
915
1055
|
}
|
|
916
1056
|
send(id, rawMessage, mode = "auto", signal) {
|
|
917
1057
|
const message = stripHindsightContent(rawMessage);
|
|
1058
|
+
this.#bumpActivity(id);
|
|
918
1059
|
this.#clearSettledPanelClose(id);
|
|
919
1060
|
return this.#mutate(id, async () => {
|
|
920
1061
|
const record = await this.#deps.registry.get(id);
|
|
921
1062
|
if (record === undefined)
|
|
922
1063
|
return failure("send", "Unknown child", id);
|
|
923
|
-
if (record.intent === "stopped"
|
|
1064
|
+
if (record.intent === "stopped") {
|
|
924
1065
|
const doubt = await this.#runtimeDoubt(id);
|
|
925
|
-
|
|
926
|
-
|
|
1066
|
+
if (doubt !== undefined) {
|
|
1067
|
+
return failure("send", doubt, id, record.state, ["runtime metadata retained"], false);
|
|
1068
|
+
}
|
|
927
1069
|
}
|
|
928
1070
|
let isResumed = false;
|
|
929
1071
|
// Deliverability is the bridge connection, minus a process that left the delegated session:
|
|
@@ -948,6 +1090,7 @@ export class SubagentManager {
|
|
|
948
1090
|
const resumedRecord = await this.#update(id, (current) => ({
|
|
949
1091
|
...current,
|
|
950
1092
|
state: "running",
|
|
1093
|
+
intent: "active",
|
|
951
1094
|
persistence,
|
|
952
1095
|
...(sessionPath === undefined ? {} : { sessionPath }),
|
|
953
1096
|
launchConfig: sessionPath === undefined
|
|
@@ -958,8 +1101,33 @@ export class SubagentManager {
|
|
|
958
1101
|
this.#projectors.set(id, createStateProjector(resumedRecord.state));
|
|
959
1102
|
}
|
|
960
1103
|
try {
|
|
961
|
-
await withDeadline(this.#start(resumedRecord), this.#deps.deadlineMs ??
|
|
1104
|
+
await withDeadline(this.#start(resumedRecord), this.#deps.deadlineMs ?? 60_000);
|
|
962
1105
|
isResumed = true;
|
|
1106
|
+
if (this.#taskRegistry !== undefined) {
|
|
1107
|
+
try {
|
|
1108
|
+
this.#taskRegistry.create({
|
|
1109
|
+
id: resumedRecord.subagentId,
|
|
1110
|
+
type: "agent",
|
|
1111
|
+
purpose: `${resumedRecord.launchConfig.agent}: resumed task`,
|
|
1112
|
+
inlineResult: true,
|
|
1113
|
+
begin: () => ({
|
|
1114
|
+
stop: () => {
|
|
1115
|
+
void this.stop(resumedRecord.subagentId);
|
|
1116
|
+
},
|
|
1117
|
+
describe: () => ({
|
|
1118
|
+
output: `Subagent ${resumedRecord.subagentId} (${resumedRecord.launchConfig.agent}) is running`,
|
|
1119
|
+
truncated: false,
|
|
1120
|
+
}),
|
|
1121
|
+
consume: () => {
|
|
1122
|
+
this.#deps.channel?.dismiss?.(resumedRecord.subagentId);
|
|
1123
|
+
},
|
|
1124
|
+
}),
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
catch (regError) {
|
|
1128
|
+
diagnose(`failed to renew background task for subagent ${resumedRecord.subagentId}: ${errorMessage(regError)}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
963
1131
|
}
|
|
964
1132
|
catch (error) {
|
|
965
1133
|
const reason = errorMessage(error);
|
|
@@ -1006,23 +1174,38 @@ export class SubagentManager {
|
|
|
1006
1174
|
return {
|
|
1007
1175
|
...clean,
|
|
1008
1176
|
state: "running",
|
|
1177
|
+
intent: "active",
|
|
1009
1178
|
unacknowledgedInput: message,
|
|
1010
1179
|
};
|
|
1011
1180
|
});
|
|
1012
1181
|
try {
|
|
1013
|
-
await this.#request(id, operation, { message });
|
|
1182
|
+
const path = acknowledgedPath(await this.#request(id, operation, { message }));
|
|
1183
|
+
this.#paths.set(id, path);
|
|
1014
1184
|
}
|
|
1015
1185
|
catch (error) {
|
|
1016
1186
|
return failure("send", errorMessage(error), id, pending.state, ["input persisted as unacknowledged before dispatch"], false);
|
|
1017
1187
|
}
|
|
1018
|
-
return publicChild((await this.#deps.registry.get(id)) ??
|
|
1188
|
+
return publicChild((await this.#deps.registry.get(id)) ?? pending, true, this.#paths.get(id));
|
|
1019
1189
|
});
|
|
1020
1190
|
}
|
|
1021
1191
|
async get(id) {
|
|
1022
1192
|
const record = await this.#deps.registry.get(id);
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1193
|
+
if (record === undefined)
|
|
1194
|
+
return failure("get", "Unknown child", id);
|
|
1195
|
+
const live = this.#isLive(id);
|
|
1196
|
+
const projector = this.#projectors.get(id);
|
|
1197
|
+
if (projector !== undefined) {
|
|
1198
|
+
const snap = projector.snapshot();
|
|
1199
|
+
const mergedRecord = {
|
|
1200
|
+
...record,
|
|
1201
|
+
...(snap.activeTool !== undefined ? { activeTool: snap.activeTool } : {}),
|
|
1202
|
+
...(snap.summary !== undefined ? { latestSummary: snap.summary } : {}),
|
|
1203
|
+
...(snap.interrupted !== undefined ? { interrupted: snap.interrupted } : {}),
|
|
1204
|
+
usage: snap.usage ?? record.usage,
|
|
1205
|
+
};
|
|
1206
|
+
return publicChild(mergedRecord, live, this.#paths.get(id));
|
|
1207
|
+
}
|
|
1208
|
+
return publicChild(record, live, this.#paths.get(id));
|
|
1026
1209
|
}
|
|
1027
1210
|
async list() {
|
|
1028
1211
|
const records = await this.#deps.registry.list();
|
|
@@ -1056,7 +1239,6 @@ export class SubagentManager {
|
|
|
1056
1239
|
stopped = await this.#update(id, (current) => ({
|
|
1057
1240
|
...current,
|
|
1058
1241
|
intent: "stopped",
|
|
1059
|
-
state: "error",
|
|
1060
1242
|
}));
|
|
1061
1243
|
}
|
|
1062
1244
|
catch (error) {
|
|
@@ -1086,6 +1268,7 @@ export class SubagentManager {
|
|
|
1086
1268
|
// bridge-only stop.
|
|
1087
1269
|
const cleanup = await attachment.cleanup();
|
|
1088
1270
|
if (!(await this.#panelGone(attachment))) {
|
|
1271
|
+
await this.#update(id, (current) => ({ ...current, state: "error" })).catch(() => undefined);
|
|
1089
1272
|
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);
|
|
1090
1273
|
}
|
|
1091
1274
|
this.#attachments.delete(id);
|
|
@@ -1112,7 +1295,10 @@ export class SubagentManager {
|
|
|
1112
1295
|
if (current !== undefined) {
|
|
1113
1296
|
stopped = await this.#deps.registry.update(id, current.revision, (value) => {
|
|
1114
1297
|
const { runtime: _runtime, ...withoutRuntime } = value;
|
|
1115
|
-
return
|
|
1298
|
+
return {
|
|
1299
|
+
...withoutRuntime,
|
|
1300
|
+
state: "done",
|
|
1301
|
+
};
|
|
1116
1302
|
});
|
|
1117
1303
|
}
|
|
1118
1304
|
if (this.#taskRegistry !== undefined) {
|
|
@@ -1176,7 +1362,6 @@ export class SubagentManager {
|
|
|
1176
1362
|
}
|
|
1177
1363
|
await this.#update(id, (current) => ({
|
|
1178
1364
|
...current,
|
|
1179
|
-
intent: "stopped",
|
|
1180
1365
|
state: "error",
|
|
1181
1366
|
interrupted: message,
|
|
1182
1367
|
}));
|
|
@@ -1374,7 +1559,7 @@ export class SubagentManager {
|
|
|
1374
1559
|
return;
|
|
1375
1560
|
projector.applyEvent(event);
|
|
1376
1561
|
let confirmedInput;
|
|
1377
|
-
if (value.type === "agent_end") {
|
|
1562
|
+
if (value.type === "turn_end" || value.type === "agent_end") {
|
|
1378
1563
|
// A finished turn is exactly when the session file has the message, so this is where
|
|
1379
1564
|
// unacknowledged input becomes part of the child's conversation.
|
|
1380
1565
|
const entries = await this.#entries(id);
|