pi-subagents 0.35.0 → 0.36.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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. package/src/slash/subagents-editor.ts +0 -86
@@ -6,6 +6,7 @@ import {
6
6
  SUBAGENT_ASYNC_COMPLETE_EVENT,
7
7
  type IntercomEventBus,
8
8
  type NestedRunSummary,
9
+ type ParallelHandoffReference,
9
10
  type SubagentResultIntercomChild,
10
11
  type SubagentState,
11
12
  } from "../../shared/types.ts";
@@ -18,9 +19,11 @@ import {
18
19
  } from "../../intercom/result-intercom.ts";
19
20
  import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
20
21
  import { resolveWatchPath } from "../../shared/utils.ts";
22
+ import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
21
23
 
22
24
  const WATCHER_RESTART_DELAY_MS = 3000;
23
25
  const POLL_INTERVAL_MS = 3000;
26
+ const RETRY_DELAY_MS = 100;
24
27
 
25
28
  type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "realpathSync" | "watch">;
26
29
 
@@ -34,6 +37,7 @@ type ResultWatcherTimers = {
34
37
  type ResultWatcherDeps = {
35
38
  fs?: ResultWatcherFs;
36
39
  timers?: ResultWatcherTimers;
40
+ notifier?: Pick<CompletionNotifier, "deliver">;
37
41
  };
38
42
 
39
43
  type ResultFileChild = {
@@ -49,21 +53,14 @@ type ResultFileChild = {
49
53
  children?: unknown;
50
54
  };
51
55
 
52
- type ResultFileData = {
53
- id?: string;
56
+ type ResultFileData = CompletionNotification & {
54
57
  runId?: string;
55
- agent?: string;
56
- success?: boolean;
57
- state?: string;
58
58
  mode?: string;
59
- summary?: string;
60
59
  results?: ResultFileChild[];
61
60
  nestedChildren?: unknown;
62
- sessionId?: string;
63
- cwd?: string;
64
- sessionFile?: string;
65
61
  asyncDir?: string;
66
62
  intercomTarget?: string;
63
+ parallelHandoff?: ParallelHandoffReference;
67
64
  };
68
65
 
69
66
  function sanitizeNestedResultChildren(value: unknown, resultPath: string, label: string): NestedRunSummary[] | undefined {
@@ -79,22 +76,24 @@ function sanitizeNestedResultChildren(value: unknown, resultPath: string, label:
79
76
  return children.length ? children : undefined;
80
77
  }
81
78
 
82
- function getErrorCode(error: unknown): string | undefined {
83
- return typeof error === "object" && error !== null && "code" in error
84
- ? (error as NodeJS.ErrnoException).code
85
- : undefined;
79
+ function errorCode(error: unknown): string | undefined {
80
+ return typeof error === "object" && error !== null && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
86
81
  }
87
82
 
88
- function isNotFoundError(error: unknown): boolean {
89
- return getErrorCode(error) === "ENOENT";
83
+ function isNotFound(error: unknown): boolean {
84
+ return errorCode(error) === "ENOENT";
90
85
  }
91
86
 
92
- function shouldFallBackToPolling(error: unknown): boolean {
93
- const code = getErrorCode(error);
87
+ function shouldPoll(error: unknown): boolean {
88
+ const code = errorCode(error);
94
89
  return code === "EMFILE" || code === "ENOSPC";
95
90
  }
96
91
 
97
-
92
+ /**
93
+ * Watches persisted async results for the session currently owned by this
94
+ * runtime. `stopResultWatcher()` revokes ownership before closing resources,
95
+ * so old callbacks can never emit or delete after reload/session replacement.
96
+ */
98
97
  export function createResultWatcher(
99
98
  pi: { events: IntercomEventBus },
100
99
  state: SubagentState,
@@ -103,18 +102,41 @@ export function createResultWatcher(
103
102
  deps: ResultWatcherDeps = {},
104
103
  ): {
105
104
  startResultWatcher: () => void;
106
- primeExistingResults: () => void;
105
+ primeExistingResults: (options?: { triggerTurn?: boolean }) => void;
107
106
  stopResultWatcher: () => void;
108
107
  } {
109
108
  const fsApi = deps.fs ?? fs;
110
109
  const timers = deps.timers ?? { setTimeout, clearTimeout, setInterval, clearInterval };
110
+ const notifier = deps.notifier ?? { deliver: async () => true };
111
+ const pendingTriggerTurn = new Map<string, boolean>();
112
+ const processing = new Set<string>();
113
+ let deliveryActive = true;
114
+ let deliveryEpoch = 0;
115
+ // The sole in-memory ownership lease. It is acquired for one active session
116
+ // and revoked before the watcher, queues, or callbacks are torn down.
117
+ let activeSessionId: string | null = null;
111
118
 
112
- const handleResult = async (file: string) => {
119
+ const ownsSession = (sessionId: string, epoch: number) => {
120
+ if (!deliveryActive || epoch !== deliveryEpoch) return false;
121
+ if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
122
+ return activeSessionId === sessionId && state.currentSessionId === sessionId;
123
+ };
124
+
125
+ const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
126
+ const pendingMode = pendingTriggerTurn.get(file);
127
+ pendingTriggerTurn.set(file, pendingMode === false || !triggerTurn ? false : true);
128
+ state.resultFileCoalescer.schedule(file, delayMs);
129
+ };
130
+
131
+ const handleResult = async (file: string, triggerTurn: boolean) => {
113
132
  const resultPath = path.join(resultsDir, file);
114
- if (!fsApi.existsSync(resultPath)) return;
133
+ if (processing.has(file) || !fsApi.existsSync(resultPath)) return;
134
+ processing.add(file);
115
135
  try {
116
136
  const data = JSON.parse(fsApi.readFileSync(resultPath, "utf-8")) as ResultFileData;
117
- if (typeof data.sessionId !== "string" || data.sessionId !== state.currentSessionId) return;
137
+ if (typeof data.sessionId !== "string" || !data.sessionId) return;
138
+ const epoch = deliveryEpoch;
139
+ if (!ownsSession(data.sessionId, epoch)) return;
118
140
 
119
141
  const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
120
142
  const hasExplicitNestedChildren = data.nestedChildren !== undefined;
@@ -124,24 +146,32 @@ export function createResultWatcher(
124
146
  nestedChildren = compactNestedResultChildren(projectNestedRegistryForRoot(runId)?.children);
125
147
  } catch (error) {
126
148
  console.error(`Failed to enrich subagent result file '${resultPath}' with nested registry children; will retry later:`, error);
149
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
127
150
  return;
128
151
  }
129
152
  }
130
- const now = Date.now();
153
+
131
154
  const completionKey = buildCompletionKey(data, `result:${file}`);
132
- if (markSeenWithTtl(state.completionSeen, completionKey, now, completionTtlMs)) {
133
- fsApi.unlinkSync(resultPath);
155
+ const lastSeenAt = state.completionSeen.get(completionKey);
156
+ if (lastSeenAt !== undefined && Date.now() - lastSeenAt > completionTtlMs) {
157
+ state.completionSeen.delete(completionKey);
158
+ } else if (lastSeenAt !== undefined) {
159
+ if (!ownsSession(data.sessionId, epoch) || !fsApi.existsSync(resultPath)) return;
160
+ try {
161
+ fsApi.unlinkSync(resultPath);
162
+ } catch (error) {
163
+ if (!isNotFound(error)) {
164
+ console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
165
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
166
+ }
167
+ }
134
168
  return;
135
169
  }
136
170
 
137
171
  const hasResultChildren = Array.isArray(data.results) && data.results.length > 0;
138
- const resultChildren = hasResultChildren
172
+ const resultChildren: ResultFileChild[] = hasResultChildren
139
173
  ? data.results!
140
- : [{
141
- agent: data.agent,
142
- output: data.summary,
143
- success: data.success,
144
- }];
174
+ : [{ agent: data.agent ?? undefined, output: data.summary, success: data.success }];
145
175
  const normalizedChildren = attachNestedChildrenToResultChildren(runId, resultChildren.map((result = {}, index): SubagentResultIntercomChild => {
146
176
  const baseOutput = result.output ?? data.summary;
147
177
  const hasRealOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
@@ -160,10 +190,7 @@ export function createResultWatcher(
160
190
  : undefined;
161
191
  return {
162
192
  agent: result.agent ?? data.agent ?? `step-${index + 1}`,
163
- status: resolveSubagentResultStatus({
164
- success: result.success,
165
- state: childState,
166
- }),
193
+ status: resolveSubagentResultStatus({ success: result.success, state: childState }),
167
194
  summary,
168
195
  index,
169
196
  artifactPath: result.artifactPaths?.outputPath,
@@ -174,11 +201,11 @@ export function createResultWatcher(
174
201
  }), nestedChildren);
175
202
 
176
203
  const intercomTarget = data.intercomTarget?.trim();
177
- if (intercomTarget) {
204
+ if (intercomTarget && triggerTurn) {
178
205
  const mode = data.mode === "single" || data.mode === "parallel" || data.mode === "chain"
179
206
  ? data.mode
180
207
  : resultChildren.length > 1 ? "chain" : "single";
181
- const payload = buildSubagentResultIntercomPayload({
208
+ const delivered = await deliverSubagentResultIntercomEvent(pi.events, buildSubagentResultIntercomPayload({
182
209
  to: intercomTarget,
183
210
  runId,
184
211
  mode,
@@ -186,20 +213,45 @@ export function createResultWatcher(
186
213
  children: normalizedChildren,
187
214
  asyncId: data.id,
188
215
  asyncDir: data.asyncDir,
189
- });
190
- const delivered = await deliverSubagentResultIntercomEvent(pi.events, payload);
191
- if (!delivered) {
192
- console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
193
- }
216
+ ...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
217
+ }));
218
+ if (!ownsSession(data.sessionId, epoch)) return;
219
+ if (!delivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
194
220
  }
195
221
 
196
- pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
222
+ const accepted = await notifier.deliver({
197
223
  ...data,
224
+ id: data.id ?? runId,
198
225
  runId,
226
+ triggerTurn,
199
227
  ...(nestedChildren?.length ? { nestedChildren } : {}),
200
228
  ...(Array.isArray(data.results) ? {
201
- results: hasResultChildren
202
- ? normalizedChildren.map((child, index) => ({
229
+ results: hasResultChildren ? normalizedChildren.map((child, index) => ({
230
+ ...data.results![index],
231
+ agent: child.agent,
232
+ status: child.status,
233
+ summary: child.summary,
234
+ index: child.index,
235
+ artifactPath: child.artifactPath,
236
+ sessionPath: child.sessionPath,
237
+ children: child.children,
238
+ })) : [],
239
+ } : {}),
240
+ });
241
+ if (!ownsSession(data.sessionId, epoch)) return;
242
+ if (!accepted) {
243
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
244
+ return;
245
+ }
246
+ markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
247
+ try {
248
+ pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
249
+ ...data,
250
+ runId,
251
+ triggerTurn,
252
+ ...(nestedChildren?.length ? { nestedChildren } : {}),
253
+ ...(Array.isArray(data.results) ? {
254
+ results: hasResultChildren ? normalizedChildren.map((child, index) => ({
203
255
  ...data.results![index],
204
256
  agent: child.agent,
205
257
  status: child.status,
@@ -208,40 +260,50 @@ export function createResultWatcher(
208
260
  artifactPath: child.artifactPath,
209
261
  sessionPath: child.sessionPath,
210
262
  children: child.children,
211
- }))
212
- : [],
213
- } : {}),
214
- });
215
- fsApi.unlinkSync(resultPath);
263
+ })) : [],
264
+ } : {}),
265
+ });
266
+ } catch (error) {
267
+ console.error(`Completion observer failed for '${resultPath}':`, error);
268
+ }
269
+ if (!ownsSession(data.sessionId, epoch) || !fsApi.existsSync(resultPath)) return;
270
+ try {
271
+ fsApi.unlinkSync(resultPath);
272
+ } catch (error) {
273
+ if (!isNotFound(error)) {
274
+ console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
275
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
276
+ }
277
+ }
216
278
  } catch (error) {
217
- if (isNotFoundError(error)) return;
218
- console.error(`Failed to process subagent result file '${resultPath}':`, error);
279
+ if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
280
+ } finally {
281
+ processing.delete(file);
219
282
  }
220
283
  };
221
284
 
222
285
  state.resultFileCoalescer = createFileCoalescer((file) => {
223
- void handleResult(file);
286
+ const triggerTurn = pendingTriggerTurn.get(file) !== false;
287
+ pendingTriggerTurn.delete(file);
288
+ void handleResult(file, triggerTurn);
224
289
  }, 50);
225
290
 
226
- const primeExistingResults = () => {
291
+ const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
227
292
  try {
293
+ const triggerTurn = options.triggerTurn !== false;
228
294
  fsApi.readdirSync(resultsDir)
229
295
  .filter((f) => f.endsWith(".json"))
230
- .forEach((file) => state.resultFileCoalescer.schedule(file, 0));
296
+ .forEach((file) => scheduleResult(file, triggerTurn));
231
297
  } catch (error) {
232
- if (isNotFoundError(error)) return;
233
- console.error(`Failed to scan subagent result directory '${resultsDir}':`, error);
298
+ if (!isNotFound(error)) console.error(`Failed to scan subagent result directory '${resultsDir}':`, error);
234
299
  }
235
300
  };
236
301
 
237
- const startPollingFallback = (reason: unknown) => {
302
+ const startPolling = (reason: unknown) => {
238
303
  state.watcher?.close();
239
304
  state.watcher = null;
240
305
  if (state.watcherRestartTimer) return;
241
-
242
- console.error(
243
- `Subagent result watcher for '${resultsDir}' fell back to polling because native fs.watch is unavailable (${getErrorCode(reason) ?? "unknown error"}).`,
244
- );
306
+ console.error(`Subagent result watcher for '${resultsDir}' fell back to polling because native fs.watch is unavailable (${errorCode(reason) ?? "unknown error"}).`);
245
307
  primeExistingResults();
246
308
  state.watcherRestartTimer = timers.setInterval(primeExistingResults, POLL_INTERVAL_MS);
247
309
  state.watcherRestartTimer.unref?.();
@@ -255,10 +317,7 @@ export function createResultWatcher(
255
317
  fsApi.mkdirSync(resultsDir, { recursive: true });
256
318
  startResultWatcher();
257
319
  } catch (error) {
258
- if (shouldFallBackToPolling(error)) {
259
- startPollingFallback(error);
260
- return;
261
- }
320
+ if (shouldPoll(error)) return startPolling(error);
262
321
  console.error(`Failed to restart subagent result watcher for '${resultsDir}':`, error);
263
322
  scheduleRestart();
264
323
  }
@@ -268,6 +327,9 @@ export function createResultWatcher(
268
327
 
269
328
  const startResultWatcher = () => {
270
329
  if (state.watcher) return;
330
+ activeSessionId = state.currentSessionId;
331
+ deliveryActive = true;
332
+ deliveryEpoch += 1;
271
333
  if (state.watcherRestartTimer) {
272
334
  timers.clearTimeout(state.watcherRestartTimer);
273
335
  timers.clearInterval(state.watcherRestartTimer);
@@ -275,17 +337,13 @@ export function createResultWatcher(
275
337
  }
276
338
  try {
277
339
  const watchDir = resolveWatchPath(resultsDir, fsApi.realpathSync.native);
278
- state.watcher = fsApi.watch(watchDir, (ev, file) => {
279
- if (ev !== "rename" || !file) return;
340
+ state.watcher = fsApi.watch(watchDir, (event, file) => {
341
+ if (event !== "rename" || !file) return;
280
342
  const fileName = file.toString();
281
- if (!fileName.endsWith(".json")) return;
282
- state.resultFileCoalescer.schedule(fileName);
343
+ if (fileName.endsWith(".json")) scheduleResult(fileName, true);
283
344
  });
284
345
  state.watcher.on("error", (error) => {
285
- if (shouldFallBackToPolling(error)) {
286
- startPollingFallback(error);
287
- return;
288
- }
346
+ if (shouldPoll(error)) return startPolling(error);
289
347
  console.error(`Subagent result watcher failed for '${resultsDir}':`, error);
290
348
  state.watcher?.close();
291
349
  state.watcher = null;
@@ -293,10 +351,7 @@ export function createResultWatcher(
293
351
  });
294
352
  state.watcher.unref?.();
295
353
  } catch (error) {
296
- if (shouldFallBackToPolling(error)) {
297
- startPollingFallback(error);
298
- return;
299
- }
354
+ if (shouldPoll(error)) return startPolling(error);
300
355
  console.error(`Failed to start subagent result watcher for '${resultsDir}':`, error);
301
356
  state.watcher = null;
302
357
  scheduleRestart();
@@ -304,6 +359,9 @@ export function createResultWatcher(
304
359
  };
305
360
 
306
361
  const stopResultWatcher = () => {
362
+ deliveryActive = false;
363
+ activeSessionId = null;
364
+ deliveryEpoch += 1;
307
365
  state.watcher?.close();
308
366
  state.watcher = null;
309
367
  if (state.watcherRestartTimer) {
@@ -312,6 +370,8 @@ export function createResultWatcher(
312
370
  }
313
371
  state.watcherRestartTimer = null;
314
372
  state.resultFileCoalescer.clear();
373
+ pendingTriggerTurn.clear();
374
+ processing.clear();
315
375
  };
316
376
 
317
377
  return { startResultWatcher, primeExistingResults, stopResultWatcher };
@@ -366,6 +366,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
366
366
  status.turnBudget ? `Turn budget: ${status.turnBudget.turnCount}/${status.turnBudget.maxTurns}+${status.turnBudget.graceTurns} (${status.turnBudget.outcome})` : undefined,
367
367
  `Dir: ${asyncDir}`,
368
368
  outputPath ? `Output: ${outputPath}` : undefined,
369
+ status.parallelHandoff ? `Parallel handoff: ${status.parallelHandoff.path}` : undefined,
369
370
  reconciliation.message ? `Diagnosis: ${reconciliation.message}` : undefined,
370
371
  reconciliation.resultPath && fs.existsSync(reconciliation.resultPath) ? `Result: ${reconciliation.resultPath}` : undefined,
371
372
  ].filter((line): line is string => Boolean(line));
@@ -408,7 +409,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
408
409
  if (resultPath) {
409
410
  try {
410
411
  const raw = fs.readFileSync(resultPath, "utf-8");
411
- const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; sessionFile?: string; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null }> };
412
+ const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null }> };
412
413
  if (params.view === "transcript") {
413
414
  try {
414
415
  return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
@@ -420,6 +421,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
420
421
  const status = data.state === "stopped" ? "stopped" : data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
421
422
  const runId = data.runId ?? data.id ?? resolvedId;
422
423
  const lines = [`Run: ${runId}`, `State: ${status}`, `Result: ${resultPath}`];
424
+ if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
423
425
  const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
424
426
  lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
425
427
  if (data.summary) lines.push("", data.summary);