@hheei/pi-subagents 0.2.4 → 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.
Files changed (43) hide show
  1. package/README.md +32 -17
  2. package/dist/agent-resolver.d.ts +9 -0
  3. package/dist/agent-resolver.js +278 -51
  4. package/dist/bridge-client.d.ts +5 -0
  5. package/dist/bridge-client.js +6 -0
  6. package/dist/builtin-agents.js +50 -37
  7. package/dist/child-bridge.d.ts +0 -8
  8. package/dist/child-bridge.js +60 -43
  9. package/dist/child-control.d.ts +5 -3
  10. package/dist/child-control.js +176 -35
  11. package/dist/commands.d.ts +1 -4
  12. package/dist/commands.js +1 -52
  13. package/dist/domain.d.ts +12 -12
  14. package/dist/domain.js +2 -1
  15. package/dist/extension.d.ts +5 -3
  16. package/dist/extension.js +39 -42
  17. package/dist/host-adapter.js +1 -1
  18. package/dist/info.d.ts +9 -0
  19. package/dist/info.js +54 -0
  20. package/dist/launch-spec.d.ts +0 -1
  21. package/dist/launch-spec.js +38 -18
  22. package/dist/manager.d.ts +4 -2
  23. package/dist/manager.js +207 -58
  24. package/dist/registry.js +19 -5
  25. package/dist/rpc-events.d.ts +1 -1
  26. package/dist/rpc-events.js +0 -1
  27. package/dist/session-bootstrap.d.ts +2 -1
  28. package/dist/session-bootstrap.js +22 -14
  29. package/dist/state.d.ts +13 -0
  30. package/dist/state.js +59 -16
  31. package/dist/status.d.ts +14 -0
  32. package/dist/status.js +36 -0
  33. package/dist/tool-activation.d.ts +3 -4
  34. package/dist/tool-activation.js +13 -215
  35. package/dist/tool-presentation.d.ts +17 -0
  36. package/dist/tool-presentation.js +160 -0
  37. package/dist/tools.d.ts +6 -1
  38. package/dist/tools.js +133 -180
  39. package/package.json +6 -6
  40. package/dist/session-fork.d.ts +0 -16
  41. package/dist/session-fork.js +0 -67
  42. package/dist/widget.d.ts +0 -12
  43. 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
- import { readSessionJsonlEntries } from "./session-fork.js";
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 `send_agent` starts it
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 publicChild(record, live) {
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
- #autoReportTimers = new Map();
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: widget and similar projections re-read list() after this. */
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
  }
@@ -397,21 +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.#clearAutoReport(id);
401
- const timer = setTimeout(() => {
402
- this.#autoReportTimers.delete(id);
403
- void this.#performAutoReport(id, record).catch((error) => {
404
- diagnose(`failed to report child ${id}: ${errorMessage(error)}`);
405
- });
406
- }, 5_000);
407
- timer.unref?.();
408
- this.#autoReportTimers.set(id, timer);
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?.();
409
460
  }
410
461
  #clearAutoReport(id) {
411
- const timer = this.#autoReportTimers.get(id);
412
- if (timer !== undefined) {
413
- clearTimeout(timer);
414
- this.#autoReportTimers.delete(id);
462
+ this.#pendingAutoReports.delete(id);
463
+ if (this.#pendingAutoReports.size === 0 && this.#autoReportBatchTimer !== undefined) {
464
+ clearTimeout(this.#autoReportBatchTimer);
465
+ this.#autoReportBatchTimer = undefined;
415
466
  }
416
467
  }
417
468
  #scheduleSettledPanelClose(id) {
@@ -534,10 +585,14 @@ export class SubagentManager {
534
585
  (this.#activity.get(id) ?? 0) !== activity ||
535
586
  (this.#workVersions.get(id) ?? 0) !== workVersion)
536
587
  return;
537
- const hasError = record.state === "error" ||
538
- (record.interrupted !== undefined && record.interrupted.trim() !== "");
539
- const isBlocked = hasError;
540
- if (!isBlocked && (text === undefined || text.trim() === "")) {
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() === "")) {
541
596
  const projector = this.#projectors.get(id);
542
597
  if (projector !== undefined) {
543
598
  projector.syncState("done");
@@ -559,12 +614,16 @@ export class SubagentManager {
559
614
  return;
560
615
  }
561
616
  let message;
562
- if (isBlocked) {
563
- if (record.interrupted !== undefined && record.interrupted.trim() !== "") {
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 !== "") {
564
623
  message =
565
624
  text !== undefined && text.trim() !== ""
566
- ? `${record.interrupted}\n\n${text}`
567
- : record.interrupted;
625
+ ? `${interruptedText}\n\n${text}`
626
+ : interruptedText;
568
627
  }
569
628
  else if (text !== undefined && text.trim() !== "") {
570
629
  message = text;
@@ -576,13 +635,16 @@ export class SubagentManager {
576
635
  else {
577
636
  message = text ?? "";
578
637
  }
579
- if (this.#lastDeliveredTurn.get(id) === message)
638
+ const lastDelivered = this.#lastDeliveredTurn.get(id);
639
+ if (lastDelivered !== undefined &&
640
+ lastDelivered.workVersion === workVersion &&
641
+ lastDelivered.message === message) {
580
642
  return;
581
- this.#lastDeliveredTurn.set(id, message);
582
- const reason = isBlocked ? "blocked" : "success";
583
- const taskStatus = isBlocked ? "failed" : "completed";
643
+ }
644
+ const reason = isError ? "error" : isBlocked ? "blocked" : "success";
645
+ const taskStatus = failed ? "failed" : "completed";
584
646
  const projector = this.#projectors.get(id);
585
- const updatedRecord = !isBlocked
647
+ const updatedRecord = !failed
586
648
  ? await this.#update(id, (current) => {
587
649
  const { interrupted: _interrupted, ...clean } = current;
588
650
  return {
@@ -592,16 +654,13 @@ export class SubagentManager {
592
654
  })
593
655
  : await this.#update(id, (current) => ({
594
656
  ...current,
595
- state: "blocked",
657
+ state: isError ? "error" : "blocked",
596
658
  interrupted: current.interrupted ?? message,
597
659
  }));
598
660
  if (projector !== undefined) {
599
- projector.syncState(isBlocked ? "blocked" : "done");
661
+ projector.syncState(isError ? "error" : isBlocked ? "blocked" : "done");
600
662
  }
601
663
  this.#scheduleSettledPanelClose(id);
602
- if (this.#deps.channel !== undefined) {
603
- await this.contactParent(id, reason, message, updatedRecord);
604
- }
605
664
  if (this.#taskRegistry !== undefined) {
606
665
  this.#taskRegistry.settle(id, {
607
666
  status: taskStatus,
@@ -609,6 +668,16 @@ export class SubagentManager {
609
668
  truncated: false,
610
669
  });
611
670
  }
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
+ }
677
+ }
678
+ if (deliveryOk) {
679
+ this.#lastDeliveredTurn.set(id, { workVersion, message });
680
+ }
612
681
  });
613
682
  }
614
683
  /** Reads a child's session entries; exposed for Task completion inspection. */
@@ -745,11 +814,37 @@ export class SubagentManager {
745
814
  */
746
815
  async #settleUnrecovered(record) {
747
816
  this.#working.delete(record.subagentId);
817
+ this.#projectors.delete(record.subagentId);
748
818
  const retired = record.runtime;
749
- if (retired === undefined)
750
- return;
751
819
  if (record.presentation === "panel") {
752
- this.#unresolvedRuntimes.add(record.subagentId);
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
+ });
753
848
  return;
754
849
  }
755
850
  await this.#update(record.subagentId, (current) => {
@@ -779,6 +874,9 @@ export class SubagentManager {
779
874
  throw new BridgeError("invalid_state", `Child ${id} did not confirm its current work state`);
780
875
  }
781
876
  const idle = state.idle && !state.pendingMessages;
877
+ if (typeof state.path === "string" && state.path !== "") {
878
+ this.#paths.set(id, state.path);
879
+ }
782
880
  if ((this.#workVersions.get(id) ?? 0) === workVersion) {
783
881
  if (idle)
784
882
  this.#working.delete(id);
@@ -803,8 +901,9 @@ export class SubagentManager {
803
901
  ? current
804
902
  : (({ unacknowledgedInput: _input, ...rest }) => rest)(current);
805
903
  const hasError = snapshot.state === "error";
904
+ const { interrupted: _interrupted, ...withoutInterrupted } = base;
806
905
  return {
807
- ...base,
906
+ ...withoutInterrupted,
808
907
  state: hasError ? "error" : idle ? "done" : "running",
809
908
  ...(snapshot.summary === undefined ? {} : { latestSummary: snapshot.summary }),
810
909
  usage: snapshot.usage,
@@ -866,8 +965,10 @@ export class SubagentManager {
866
965
  this.#working.add(childId);
867
966
  this.#notify();
868
967
  await withDeadline(this.#start(record), this.#deps.deadlineMs ?? 60_000);
968
+ let path;
869
969
  try {
870
- 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);
871
972
  }
872
973
  catch (error) {
873
974
  let failureState = "error";
@@ -908,6 +1009,9 @@ export class SubagentManager {
908
1009
  output: `Subagent ${record.subagentId} (${input.agent}) is running`,
909
1010
  truncated: false,
910
1011
  }),
1012
+ consume: () => {
1013
+ this.#deps.channel?.dismiss?.(record.subagentId);
1014
+ },
911
1015
  }),
912
1016
  });
913
1017
  }
@@ -916,7 +1020,8 @@ export class SubagentManager {
916
1020
  }
917
1021
  }
918
1022
  return {
919
- 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,
920
1025
  ...(placement.note === undefined ? {} : { note: placement.note }),
921
1026
  };
922
1027
  }
@@ -956,10 +1061,11 @@ export class SubagentManager {
956
1061
  const record = await this.#deps.registry.get(id);
957
1062
  if (record === undefined)
958
1063
  return failure("send", "Unknown child", id);
959
- if (record.intent === "stopped" || record.state === "error") {
1064
+ if (record.intent === "stopped") {
960
1065
  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);
1066
+ if (doubt !== undefined) {
1067
+ return failure("send", doubt, id, record.state, ["runtime metadata retained"], false);
1068
+ }
963
1069
  }
964
1070
  let isResumed = false;
965
1071
  // Deliverability is the bridge connection, minus a process that left the delegated session:
@@ -984,6 +1090,7 @@ export class SubagentManager {
984
1090
  const resumedRecord = await this.#update(id, (current) => ({
985
1091
  ...current,
986
1092
  state: "running",
1093
+ intent: "active",
987
1094
  persistence,
988
1095
  ...(sessionPath === undefined ? {} : { sessionPath }),
989
1096
  launchConfig: sessionPath === undefined
@@ -996,6 +1103,31 @@ export class SubagentManager {
996
1103
  try {
997
1104
  await withDeadline(this.#start(resumedRecord), this.#deps.deadlineMs ?? 60_000);
998
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
+ }
999
1131
  }
1000
1132
  catch (error) {
1001
1133
  const reason = errorMessage(error);
@@ -1042,23 +1174,38 @@ export class SubagentManager {
1042
1174
  return {
1043
1175
  ...clean,
1044
1176
  state: "running",
1177
+ intent: "active",
1045
1178
  unacknowledgedInput: message,
1046
1179
  };
1047
1180
  });
1048
1181
  try {
1049
- await this.#request(id, operation, { message });
1182
+ const path = acknowledgedPath(await this.#request(id, operation, { message }));
1183
+ this.#paths.set(id, path);
1050
1184
  }
1051
1185
  catch (error) {
1052
1186
  return failure("send", errorMessage(error), id, pending.state, ["input persisted as unacknowledged before dispatch"], false);
1053
1187
  }
1054
- return publicChild((await this.#deps.registry.get(id)) ?? record, true);
1188
+ return publicChild((await this.#deps.registry.get(id)) ?? pending, true, this.#paths.get(id));
1055
1189
  });
1056
1190
  }
1057
1191
  async get(id) {
1058
1192
  const record = await this.#deps.registry.get(id);
1059
- return record === undefined
1060
- ? failure("get", "Unknown child", id)
1061
- : publicChild(record, this.#isLive(id));
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));
1062
1209
  }
1063
1210
  async list() {
1064
1211
  const records = await this.#deps.registry.list();
@@ -1092,7 +1239,6 @@ export class SubagentManager {
1092
1239
  stopped = await this.#update(id, (current) => ({
1093
1240
  ...current,
1094
1241
  intent: "stopped",
1095
- state: "error",
1096
1242
  }));
1097
1243
  }
1098
1244
  catch (error) {
@@ -1122,6 +1268,7 @@ export class SubagentManager {
1122
1268
  // bridge-only stop.
1123
1269
  const cleanup = await attachment.cleanup();
1124
1270
  if (!(await this.#panelGone(attachment))) {
1271
+ await this.#update(id, (current) => ({ ...current, state: "error" })).catch(() => undefined);
1125
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);
1126
1273
  }
1127
1274
  this.#attachments.delete(id);
@@ -1148,7 +1295,10 @@ export class SubagentManager {
1148
1295
  if (current !== undefined) {
1149
1296
  stopped = await this.#deps.registry.update(id, current.revision, (value) => {
1150
1297
  const { runtime: _runtime, ...withoutRuntime } = value;
1151
- return withoutRuntime;
1298
+ return {
1299
+ ...withoutRuntime,
1300
+ state: "done",
1301
+ };
1152
1302
  });
1153
1303
  }
1154
1304
  if (this.#taskRegistry !== undefined) {
@@ -1212,7 +1362,6 @@ export class SubagentManager {
1212
1362
  }
1213
1363
  await this.#update(id, (current) => ({
1214
1364
  ...current,
1215
- intent: "stopped",
1216
1365
  state: "error",
1217
1366
  interrupted: message,
1218
1367
  }));
@@ -1410,7 +1559,7 @@ export class SubagentManager {
1410
1559
  return;
1411
1560
  projector.applyEvent(event);
1412
1561
  let confirmedInput;
1413
- if (value.type === "agent_end") {
1562
+ if (value.type === "turn_end" || value.type === "agent_end") {
1414
1563
  // A finished turn is exactly when the session file has the message, so this is where
1415
1564
  // unacknowledged input becomes part of the child's conversation.
1416
1565
  const entries = await this.#entries(id);
package/dist/registry.js CHANGED
@@ -57,11 +57,10 @@ const LAUNCH_CONFIG_FIELDS = {
57
57
  skills: true,
58
58
  prompt: true,
59
59
  bridgeExtensionPath: true,
60
- interactive: true,
61
60
  title: true,
62
61
  task: true,
63
- referencedSessionPath: true,
64
62
  codemodeOnly: true,
63
+ mcp: true,
65
64
  };
66
65
  const AGENT_FIELDS = {
67
66
  name: true,
@@ -221,6 +220,23 @@ function parseRuntime(value, path) {
221
220
  const endpoint = expectString(raw.endpoint, "runtime.endpoint", path);
222
221
  return Object.freeze({ runtimeIdentity, endpoint });
223
222
  }
223
+ function parseMcpSelection(value, path) {
224
+ if (value === undefined)
225
+ return Object.freeze({ enabled: true });
226
+ const raw = expectObject(value, "launchConfig.mcp", path);
227
+ const enabled = expectOptionalBoolean(raw.enabled, "launchConfig.mcp.enabled", path, true);
228
+ const servers = raw.servers === undefined
229
+ ? undefined
230
+ : Object.freeze(expectStringArray(raw.servers, "launchConfig.mcp.servers", path));
231
+ const excluded = raw.excluded === undefined
232
+ ? undefined
233
+ : Object.freeze(expectStringArray(raw.excluded, "launchConfig.mcp.excluded", path));
234
+ return Object.freeze({
235
+ enabled,
236
+ ...(servers === undefined ? {} : { servers }),
237
+ ...(excluded === undefined ? {} : { excluded }),
238
+ });
239
+ }
224
240
  function parseLaunchConfig(value, path) {
225
241
  const raw = expectObject(value, "launchConfig", path);
226
242
  expectKeys(raw, LAUNCH_CONFIG_FIELDS, "launchConfig", path);
@@ -228,7 +244,6 @@ function parseLaunchConfig(value, path) {
228
244
  if (!isSessionId(sessionId))
229
245
  throw invalid(path, "launchConfig.sessionId is not a valid session id");
230
246
  const sessionPath = expectOptionalString(raw.sessionPath, "launchConfig.sessionPath", path);
231
- const referencedSessionPath = expectOptionalString(raw.referencedSessionPath, "launchConfig.referencedSessionPath", path);
232
247
  const extensions = parseSelection(raw.extensions, path, "extensions");
233
248
  const bridgeExtensionPath = expectString(raw.bridgeExtensionPath, "launchConfig.bridgeExtensionPath", path);
234
249
  if (!extensions.paths.includes(bridgeExtensionPath)) {
@@ -245,7 +260,6 @@ function parseLaunchConfig(value, path) {
245
260
  sessionId,
246
261
  sessionDir: expectString(raw.sessionDir, "launchConfig.sessionDir", path),
247
262
  ...(sessionPath === undefined ? {} : { sessionPath }),
248
- ...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
249
263
  agent: parseAgent(raw.agent, path),
250
264
  model: parseModel(raw.model, path),
251
265
  thinking: parseThinking(raw.thinking, path),
@@ -253,9 +267,9 @@ function parseLaunchConfig(value, path) {
253
267
  excludeTools: Object.freeze(expectStringArray(raw.excludeTools, "launchConfig.excludeTools", path)),
254
268
  extensions,
255
269
  skills: parseSelection(raw.skills, path, "skills"),
270
+ mcp: parseMcpSelection(raw.mcp, path),
256
271
  prompt: expectString(raw.prompt, "launchConfig.prompt", path),
257
272
  bridgeExtensionPath,
258
- interactive: expectOptionalBoolean(raw.interactive, "launchConfig.interactive", path, false),
259
273
  ...(expectOptionalBoolean(raw.codemodeOnly, "launchConfig.codemodeOnly", path, false)
260
274
  ? { codemodeOnly: true }
261
275
  : {}),
@@ -2,5 +2,5 @@
2
2
  * The Pi events a child forwards to its parent. Everything else — streaming token noise,
3
3
  * undocumented internals — is dropped, so the parent's projection only sees turn-level facts.
4
4
  */
5
- export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "auto_retry", "auto_retry_start", "auto_retry_end", "message_update", "agent_settled"];
5
+ export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_end", "auto_retry", "auto_retry_start", "auto_retry_end", "message_update", "agent_settled"];
6
6
  export declare function shouldForwardPiEvent(event: unknown): boolean;
@@ -9,7 +9,6 @@ export const FORWARDED_PI_EVENT_TYPES = [
9
9
  "turn_start",
10
10
  "turn_end",
11
11
  "tool_execution_start",
12
- "tool_execution_update",
13
12
  "tool_execution_end",
14
13
  "auto_retry",
15
14
  "auto_retry_start",
@@ -1,6 +1,8 @@
1
1
  import type { ModelRegistryLike, ParentAgentDefaults, SkillCatalogEntry } from "./agent-resolver.js";
2
2
  import type { EffectiveLaunchConfig, PersistenceState, PiInvocation, Presentation, SpawnSubagentInput, SubagentRecord } from "./domain.js";
3
3
  import type { SubagentRegistry } from "./registry.js";
4
+ /** Reads all JSON entries from a session file on disk. */
5
+ export declare function readSessionJsonlEntries(sessionPath: string): Promise<unknown[]>;
4
6
  export interface SessionPlacement {
5
7
  readonly sessionId: string;
6
8
  readonly sessionDir: string;
@@ -62,7 +64,6 @@ export interface ResolveSubagentLaunchOptions {
62
64
  readonly existingSubagentIds?: readonly string[];
63
65
  readonly enforceEnabled?: boolean;
64
66
  readonly sessionPath?: string;
65
- readonly referencedSessionPath?: string;
66
67
  }
67
68
  /**
68
69
  * One resolution chain for a spawn: agent definition + explicit parent defaults