pi-subagents 0.57.0 → 0.59.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 (115) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/agents.md +18 -9
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +40 -1
  5. package/docs/models.md +27 -4
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +92 -6
  8. package/docs/workflows.md +115 -0
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +3 -1
  13. package/skills/pi-subagents/references/execution-controls.md +6 -2
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  16. package/src/agents/agent-management.ts +28 -17
  17. package/src/agents/agent-serializer.ts +9 -2
  18. package/src/agents/agents.ts +103 -27
  19. package/src/agents/runtime-agent-events.ts +70 -0
  20. package/src/agents/runtime-agent-registry.ts +19 -18
  21. package/src/api/agents.ts +10 -5
  22. package/src/api/background-work.ts +5 -1
  23. package/src/api/delegation.ts +0 -7
  24. package/src/api/preflight.ts +32 -10
  25. package/src/extension/fanout-child.ts +5 -3
  26. package/src/extension/index.ts +66 -27
  27. package/src/extension/public-execution.ts +15 -2
  28. package/src/extension/rpc.ts +35 -14
  29. package/src/extension/schemas.ts +37 -12
  30. package/src/extension/tool-description.ts +25 -6
  31. package/src/integrations/herdr-status.ts +5 -0
  32. package/src/intercom/result-intercom.ts +2 -0
  33. package/src/profiles/profiles.ts +5 -6
  34. package/src/runs/background/active-async-capacity.ts +2 -2
  35. package/src/runs/background/async-execution.ts +78 -46
  36. package/src/runs/background/async-job-tracker.ts +14 -12
  37. package/src/runs/background/async-resume.ts +29 -27
  38. package/src/runs/background/async-status-snapshot.ts +23 -261
  39. package/src/runs/background/async-status.ts +68 -7
  40. package/src/runs/background/chain-append.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +60 -8
  42. package/src/runs/background/fleet-view.ts +21 -11
  43. package/src/runs/background/notify.ts +184 -9
  44. package/src/runs/background/result-delivery-ownership.ts +45 -0
  45. package/src/runs/background/result-files.ts +2 -1
  46. package/src/runs/background/result-watcher.ts +29 -10
  47. package/src/runs/background/resume-guidance.ts +1 -1
  48. package/src/runs/background/retained-children.ts +1 -1
  49. package/src/runs/background/run-status.ts +18 -8
  50. package/src/runs/background/scheduled-runs.ts +86 -7
  51. package/src/runs/background/stale-run-reconciler.ts +10 -4
  52. package/src/runs/background/steering.ts +4 -14
  53. package/src/runs/background/subagent-runner.ts +389 -350
  54. package/src/runs/background/subagent-wait.ts +58 -10
  55. package/src/runs/background/terminal-run-index.ts +1 -1
  56. package/src/runs/background/wait-completions.ts +22 -1
  57. package/src/runs/background/wait-config.ts +23 -9
  58. package/src/runs/background/wait-tool.ts +9 -2
  59. package/src/runs/foreground/async-steering-action.ts +2 -2
  60. package/src/runs/foreground/execution.ts +134 -121
  61. package/src/runs/foreground/foreground-control.ts +3 -0
  62. package/src/runs/foreground/foreground-history.ts +1 -0
  63. package/src/runs/foreground/subagent-executor.ts +527 -257
  64. package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
  65. package/src/runs/shared/abort-recovery.ts +119 -0
  66. package/src/runs/shared/async-status-projection.ts +463 -0
  67. package/src/runs/shared/child-identity.ts +19 -4
  68. package/src/runs/shared/child-launch-plan.ts +151 -0
  69. package/src/runs/shared/completion-evidence.ts +89 -0
  70. package/src/runs/shared/completion-guard.ts +5 -4
  71. package/src/runs/shared/dynamic-fanout.ts +3 -3
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/host-step-status.ts +230 -0
  74. package/src/runs/shared/lane-metadata.ts +105 -0
  75. package/src/runs/shared/launch-cwd.ts +16 -0
  76. package/src/runs/shared/long-running-guard.ts +2 -1
  77. package/src/runs/shared/mcp-config-sources.ts +422 -0
  78. package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  79. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  80. package/src/runs/shared/model-exclusions.ts +12 -0
  81. package/src/runs/shared/model-fallback.ts +25 -6
  82. package/src/runs/shared/nested-events.ts +6 -2
  83. package/src/runs/shared/nested-render.ts +7 -3
  84. package/src/runs/shared/parallel-handoff.ts +419 -7
  85. package/src/runs/shared/parallel-utils.ts +15 -1
  86. package/src/runs/shared/pi-args.ts +81 -8
  87. package/src/runs/shared/single-output.ts +44 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +11 -2
  94. package/src/shared/launch-contract.ts +5 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +240 -30
  97. package/src/shared/utils.ts +41 -84
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/slash/subagents-admin.ts +3 -0
  104. package/src/tui/fleet-status.ts +125 -74
  105. package/src/tui/fleet.ts +11 -5
  106. package/src/tui/render.ts +330 -33
  107. package/src/watchdog/turn-delta.ts +1 -1
  108. package/src/workflows/chat-progress.ts +6 -3
  109. package/src/workflows/host-command.ts +230 -0
  110. package/src/workflows/scripted-workflow.ts +496 -35
  111. package/src/workflows/workflow-child-summary.ts +9 -5
  112. package/src/workflows/workflow-preflight.ts +270 -0
  113. package/src/workflows/workflow-receipt.ts +79 -5
  114. package/src/workflows/workflow-settlement.ts +246 -0
  115. package/src/runs/shared/turn-budget.ts +0 -98
@@ -15,7 +15,21 @@ import {
15
15
  resolveCompletionBatchConfig,
16
16
  } from "./completion-batcher.ts";
17
17
  import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type ScheduleOrigin, type SubagentState } from "../../shared/types.ts";
18
+ import { safeTerminalText } from "../../shared/display-text.ts";
19
+ import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
18
20
  import { isUnexplainedProcessSignal } from "../shared/process-signal.ts";
21
+ import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
22
+
23
+ export interface SubagentNotifyChildOutput {
24
+ workflowKey?: string;
25
+ runId?: string;
26
+ agent?: string;
27
+ status: string;
28
+ savedOutputPath?: string;
29
+ preview: string;
30
+ previewTruncated?: boolean;
31
+ previewUnavailableReason?: string;
32
+ }
19
33
 
20
34
  export interface SubagentNotifyDetails {
21
35
  agent: string;
@@ -26,6 +40,7 @@ export interface SubagentNotifyDetails {
26
40
  durationMs?: number;
27
41
  workflowRunId?: string;
28
42
  childRuns?: Array<{ runId: string; workflowKey?: string; agent?: string; status?: string }>;
43
+ childOutputs?: SubagentNotifyChildOutput[];
29
44
  reconciledFromDetachedChild?: string;
30
45
  sessionLabel?: string;
31
46
  sessionValue?: string;
@@ -56,7 +71,14 @@ export interface CompletionNotification {
56
71
  workflowKey?: string;
57
72
  agent?: string;
58
73
  status?: string;
74
+ state?: string;
59
75
  success?: boolean;
76
+ output?: string;
77
+ structuredOutput?: unknown;
78
+ outputState?: "present" | "absent" | "unknown";
79
+ outputReference?: string | { path?: string };
80
+ artifactPaths?: { outputPath?: string };
81
+ detached?: boolean;
60
82
  exitCode?: number | null;
61
83
  processSignal?: string | null;
62
84
  interrupted?: boolean;
@@ -91,6 +113,7 @@ export interface RegisterSubagentNotifyOptions {
91
113
  batchConfig?: CompletionBatchConfig;
92
114
  timers?: NotifyTimerApi;
93
115
  now?: () => number;
116
+ ownership?: Pick<ResultDeliveryOwnership, "owns">;
94
117
  }
95
118
 
96
119
  export interface CompletionNotifier {
@@ -98,6 +121,115 @@ export interface CompletionNotifier {
98
121
  dispose(): void;
99
122
  }
100
123
 
124
+ const CHILD_OUTPUT_PREVIEW_MAX_BYTES = 4 * 1024;
125
+ const CHILD_OUTPUT_PREVIEW_COUNT = 8;
126
+ const PREVIEW_TRUNCATION_MARKER = "...[preview truncated]";
127
+ type CompletionChild = NonNullable<CompletionNotification["results"]>[number];
128
+
129
+ function truncateUtf8Head(value: string, maxBytes: number): { text: string; truncated: boolean } {
130
+ const bytes = Buffer.from(value, "utf8");
131
+ if (bytes.length <= maxBytes) return { text: value, truncated: false };
132
+ const markerBytes = Buffer.byteLength(PREVIEW_TRUNCATION_MARKER, "utf8");
133
+ const contentBytes = Math.max(0, maxBytes - markerBytes);
134
+ let end = contentBytes;
135
+ while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
136
+ return {
137
+ text: `${bytes.subarray(0, end).toString("utf8")}${PREVIEW_TRUNCATION_MARKER}`,
138
+ truncated: true,
139
+ };
140
+ }
141
+
142
+ function boundedSafeText(value: string, maxBytes = 1_024): string {
143
+ return truncateUtf8Head(safeTerminalText(value).replace(/\n/g, "\\n"), maxBytes).text;
144
+ }
145
+
146
+ function outputPathFromReference(value: CompletionChild["outputReference"]): string | undefined {
147
+ if (typeof value === "string" && value.trim()) return value.trim();
148
+ if (value && typeof value === "object" && typeof value.path === "string" && value.path.trim()) return value.path.trim();
149
+ return undefined;
150
+ }
151
+
152
+ function childSavedOutputPath(child: CompletionChild): string | undefined {
153
+ return outputPathFromReference(child.outputReference);
154
+ }
155
+
156
+ function childStatus(child: CompletionChild, workflowState?: string): string {
157
+ const knownStatus = child.status === "complete"
158
+ ? "completed"
159
+ : child.status === "completed" || child.status === "failed" || child.status === "paused" || child.status === "stopped" || child.status === "detached"
160
+ ? child.status
161
+ : undefined;
162
+ if (knownStatus) return knownStatus;
163
+ return resolveSubagentResultStatus({
164
+ success: child.success,
165
+ state: child.state ?? (child.success === undefined ? workflowState : undefined),
166
+ interrupted: child.interrupted,
167
+ detached: child.detached,
168
+ processSignal: child.processSignal,
169
+ timedOut: child.timedOut,
170
+ stopped: child.stopped,
171
+ turnBudgetExceeded: child.turnBudgetExceeded,
172
+ exitCode: typeof child.exitCode === "number" ? child.exitCode : undefined,
173
+ });
174
+ }
175
+
176
+ function structuredOutputText(value: unknown): string | undefined {
177
+ if (value === undefined) return undefined;
178
+ try {
179
+ const serialized = JSON.stringify(value, null, 2);
180
+ return typeof serialized === "string" ? serialized : undefined;
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ }
185
+
186
+ function childInlinePreview(child: CompletionChild): { preview?: string; truncated?: boolean; unavailableReason?: string } {
187
+ const output = typeof child.output === "string" ? child.output : "";
188
+ const outputReference = childSavedOutputPath(child);
189
+ const referenceOnly = Boolean(outputReference && output.trim().startsWith("Output saved to:"));
190
+ const raw = child.outputState === "absent" || referenceOnly ? structuredOutputText(child.structuredOutput) : output || structuredOutputText(child.structuredOutput);
191
+ if (!raw?.trim()) {
192
+ return { unavailableReason: referenceOnly ? "saved output is file-only" : "no safe inline output" };
193
+ }
194
+ const safe = safeTerminalText(raw);
195
+ if (!safe.trim()) return { unavailableReason: "no safe inline output" };
196
+ const bounded = truncateUtf8Head(safe, CHILD_OUTPUT_PREVIEW_MAX_BYTES);
197
+ return { preview: bounded.text, ...(bounded.truncated ? { truncated: true } : {}) };
198
+ }
199
+
200
+ function formatChildOutputBlock(children: SubagentNotifyChildOutput[] | undefined): string | undefined {
201
+ if (!children?.length) return undefined;
202
+ const lines = ["Child outputs:"];
203
+ for (let index = 0; index < children.length; index++) {
204
+ const child = children[index]!;
205
+ const key = child.workflowKey ? boundedSafeText(child.workflowKey) : "unavailable";
206
+ const runId = child.runId ? boundedSafeText(child.runId) : "unavailable";
207
+ const status = boundedSafeText(child.status) || "unavailable";
208
+ lines.push(`- key=${key} run=${runId} status=${status}`);
209
+ lines.push(` Saved output: ${child.savedOutputPath ? boundedSafeText(child.savedOutputPath) : "unavailable"}`);
210
+ if (index >= CHILD_OUTPUT_PREVIEW_COUNT) {
211
+ lines.push(" Preview: unavailable (notice preview budget exceeded)");
212
+ continue;
213
+ }
214
+ if (!child.preview) {
215
+ lines.push(` Preview: unavailable (${child.previewUnavailableReason ?? "no safe inline output"})`);
216
+ continue;
217
+ }
218
+ lines.push(" Preview:");
219
+ for (const line of child.preview.split("\n")) lines.push(` | ${line}`);
220
+ }
221
+ if (children.length > CHILD_OUTPUT_PREVIEW_COUNT) {
222
+ lines.push(`- ${children.length - CHILD_OUTPUT_PREVIEW_COUNT} additional child preview(s) omitted by notice budget; child run metadata is retained below.`);
223
+ }
224
+ return lines.join("\n");
225
+ }
226
+
227
+ function formatResultPreview(details: SubagentNotifyDetails): string {
228
+ const summary = details.resultPreview.trim() ? details.resultPreview : "(no output)";
229
+ const childOutputs = formatChildOutputBlock(details.childOutputs);
230
+ return childOutputs ? `${summary}\n\n${childOutputs}` : summary;
231
+ }
232
+
101
233
  function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
102
234
  if (!details.sessionValue) return undefined;
103
235
  return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;
@@ -129,7 +261,7 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
129
261
  "",
130
262
  scheduleLine,
131
263
  scheduleLine ? "" : undefined,
132
- details.resultPreview.trim() ? details.resultPreview : "(no output)",
264
+ formatResultPreview(details),
133
265
  details.handoffPath ? "" : undefined,
134
266
  details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,
135
267
  correlationLines.length && !details.handoffPath ? "" : undefined,
@@ -216,7 +348,7 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
216
348
  if (!detail) continue;
217
349
  const sessionLine = formatSessionLine(detail);
218
350
  blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}${detail.scheduleOrigin ? ` — scheduled run from ${detail.scheduleOrigin.name ?? detail.scheduleOrigin.id} (schedule ${detail.scheduleOrigin.id})` : ""}`);
219
- blocks.push(detail.resultPreview.trim() ? detail.resultPreview : "(no output)");
351
+ blocks.push(formatResultPreview(detail));
220
352
  if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);
221
353
  blocks.push(...formatCorrelationLines(detail));
222
354
  if (sessionLine) blocks.push(sessionLine);
@@ -228,6 +360,8 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
228
360
  interface PendingCompletion {
229
361
  key: string;
230
362
  details: SubagentNotifyDetails;
363
+ sessionId: string;
364
+ completionOwnerId: unknown;
231
365
  triggerTurn: boolean;
232
366
  resolve(accepted: boolean): void;
233
367
  }
@@ -271,6 +405,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
271
405
  const paused = !stopped && !result.success && (
272
406
  result.exitCode === 0
273
407
  || result.state === "paused"
408
+ || result.interrupted === true
274
409
  || summary.startsWith("Paused after interrupt.")
275
410
  );
276
411
  const status = stopped ? "stopped" : paused ? "paused" : result.success ? "completed" : "failed";
@@ -286,14 +421,34 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
286
421
  const rawRunId = typeof result.runId === "string" ? result.runId : typeof result.id === "string" ? result.id : undefined;
287
422
  const workflowRunId = (result.mode === "workflow" || agent === "workflow") && rawRunId ? rawRunId : undefined;
288
423
  const childRuns = result.results?.flatMap((child) => {
289
- if (typeof child.runId !== "string" || !child.runId.trim()) return [];
424
+ const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
425
+ const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
426
+ if (!runId && (!workflowRunId || !workflowKey)) return [];
290
427
  return [{
291
- runId: child.runId,
292
- ...(typeof child.workflowKey === "string" ? { workflowKey: child.workflowKey } : {}),
428
+ runId: runId ?? "unavailable",
429
+ ...(workflowKey ? { workflowKey } : {}),
293
430
  ...(typeof child.agent === "string" ? { agent: child.agent } : {}),
294
- ...(typeof child.status === "string" ? { status: child.status } : {}),
431
+ status: childStatus(child, workflowRunId ? result.state : undefined),
295
432
  }];
296
433
  }) ?? [];
434
+ const childOutputs = workflowRunId && result.results?.length
435
+ ? result.results.map((child) => {
436
+ const inline = childInlinePreview(child);
437
+ const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
438
+ const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
439
+ const savedOutputPath = childSavedOutputPath(child);
440
+ return {
441
+ ...(workflowKey ? { workflowKey } : {}),
442
+ ...(runId ? { runId } : {}),
443
+ ...(typeof child.agent === "string" ? { agent: child.agent } : {}),
444
+ status: childStatus(child, result.state),
445
+ ...(savedOutputPath ? { savedOutputPath } : {}),
446
+ ...(inline.preview ? { preview: inline.preview } : { preview: "" }),
447
+ ...(inline.truncated ? { previewTruncated: true } : {}),
448
+ ...(inline.unavailableReason ? { previewUnavailableReason: inline.unavailableReason } : {}),
449
+ };
450
+ })
451
+ : undefined;
297
452
  const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
298
453
  const session =
299
454
  result.shareUrl
@@ -318,6 +473,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
318
473
  ...(handoffPath ? { handoffPath } : {}),
319
474
  ...(workflowRunId ? { workflowRunId } : {}),
320
475
  ...(childRuns.length ? { childRuns } : {}),
476
+ ...(childOutputs?.length ? { childOutputs } : {}),
321
477
  ...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
322
478
  ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
323
479
  };
@@ -335,6 +491,10 @@ export default function registerSubagentNotify(
335
491
  const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
336
492
  const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
337
493
  let disposed = false;
494
+ const ownsResult = options.ownership?.owns
495
+ ?? ((sessionId: string, completionOwnerId: unknown) => sessionId === state.currentSessionId
496
+ && typeof completionOwnerId === "string"
497
+ && completionOwnerId === state.completionOwnerId);
338
498
 
339
499
  const settle = (items: PendingCompletion[], accepted: boolean) => {
340
500
  for (const item of items) {
@@ -343,7 +503,18 @@ export default function registerSubagentNotify(
343
503
  item.resolve(accepted);
344
504
  }
345
505
  };
346
- const emit = (items: PendingCompletion[]) => settle(items, sendCompletion(pi, items));
506
+ const emit = (items: PendingCompletion[]) => {
507
+ const accepted: PendingCompletion[] = [];
508
+ const rejected: PendingCompletion[] = [];
509
+ for (const item of items) {
510
+ const owned = item.details.source === "foreground"
511
+ ? item.sessionId === state.currentSessionId
512
+ : ownsResult(item.sessionId, item.completionOwnerId);
513
+ (owned ? accepted : rejected).push(item);
514
+ }
515
+ settle(rejected, false);
516
+ settle(accepted, sendCompletion(pi, accepted));
517
+ };
347
518
  const getBatcher = (result: CompletionNotification) => {
348
519
  const key = completionBatchKey(result);
349
520
  let batcher = batchers.get(key);
@@ -360,8 +531,10 @@ export default function registerSubagentNotify(
360
531
  };
361
532
 
362
533
  const deliver = (result: CompletionNotification): Promise<boolean> => {
363
- if (disposed || typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return Promise.resolve(false);
364
- if (result.source !== "foreground" && (!state.completionOwnerId || result.completionOwnerId !== state.completionOwnerId)) return Promise.resolve(false);
534
+ if (disposed || typeof result.sessionId !== "string") return Promise.resolve(false);
535
+ if (result.source === "foreground") {
536
+ if (result.sessionId !== state.currentSessionId) return Promise.resolve(false);
537
+ } else if (!ownsResult(result.sessionId, result.completionOwnerId)) return Promise.resolve(false);
365
538
  if (result.intercomDelivered === true) return Promise.resolve(true);
366
539
  const key = buildCompletionKey(result, "notify");
367
540
  const seenAt = seen.get(key);
@@ -376,6 +549,8 @@ export default function registerSubagentNotify(
376
549
  const item: PendingCompletion = {
377
550
  key,
378
551
  details,
552
+ sessionId: result.sessionId,
553
+ completionOwnerId: result.completionOwnerId,
379
554
  triggerTurn: result.triggerTurn !== false,
380
555
  resolve,
381
556
  };
@@ -0,0 +1,45 @@
1
+ import type { SubagentState } from "../../shared/types.ts";
2
+
3
+ const MAX_CLAIMED_PREDECESSOR_SESSIONS = 8;
4
+
5
+ type ResultDeliveryState = Pick<SubagentState, "currentSessionId" | "completionOwnerId">;
6
+
7
+ export interface ResultDeliveryOwnership {
8
+ owns(sessionId: string, completionOwnerId: unknown): boolean;
9
+ claimedSessionIds(): readonly string[];
10
+ claimPredecessor(previousSessionFile: string | undefined, previousRuntimeSessionId: string | null): boolean;
11
+ clear(): void;
12
+ }
13
+
14
+ export function createResultDeliveryOwnership(state: ResultDeliveryState): ResultDeliveryOwnership {
15
+ const claimed = new Map<string, string>();
16
+
17
+ const currentOwner = (): string | undefined => typeof state.completionOwnerId === "string" && state.completionOwnerId
18
+ ? state.completionOwnerId
19
+ : undefined;
20
+
21
+ return {
22
+ owns(sessionId, completionOwnerId) {
23
+ const owner = currentOwner();
24
+ if (!owner || completionOwnerId !== owner) return false;
25
+ return sessionId === state.currentSessionId || claimed.get(sessionId) === owner;
26
+ },
27
+ claimedSessionIds() {
28
+ const owner = currentOwner();
29
+ if (!owner) return [];
30
+ return [...claimed].flatMap(([sessionId, claimedOwner]) => claimedOwner === owner ? [sessionId] : []);
31
+ },
32
+ claimPredecessor(previousSessionFile, previousRuntimeSessionId) {
33
+ const owner = currentOwner();
34
+ if (!owner || !previousSessionFile || !previousRuntimeSessionId) return false;
35
+ if (previousSessionFile !== previousRuntimeSessionId || state.currentSessionId !== previousRuntimeSessionId) return false;
36
+ claimed.delete(previousRuntimeSessionId);
37
+ claimed.set(previousRuntimeSessionId, owner);
38
+ while (claimed.size > MAX_CLAIMED_PREDECESSOR_SESSIONS) claimed.delete(claimed.keys().next().value!);
39
+ return true;
40
+ },
41
+ clear() {
42
+ claimed.clear();
43
+ },
44
+ };
45
+ }
@@ -343,7 +343,8 @@ export function resultPayloadPathForMissionObserverRun(resultsDir: string, runId
343
343
  if (!entry || entry.runId !== runId) return undefined;
344
344
  return resultPayloadLocationFromIndex(resultsDir, entry)?.path;
345
345
  } catch (error) {
346
- if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
346
+ const code = (error as NodeJS.ErrnoException).code;
347
+ if (code !== "ENOENT" && code !== "ENOTDIR" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
347
348
  return undefined;
348
349
  }
349
350
  }
@@ -25,6 +25,7 @@ import { recordWaitCompletion } from "./wait-completions.ts";
25
25
  import { MISSION_BINDING_FILE, syncMissionFromAsyncCompletion } from "../../missions/lifecycle.ts";
26
26
  import { missionObserverResultCandidateFiles, promotePendingResultFile, removeMissionObserverIndex, removeResultIndex, resultCandidateFilesForSession, resultPayloadPathForIndexedRun, resultPayloadPathForMissionObserverRun, resultPayloadPathForSessionRun, writeAsyncResultFile, writeResultIndexForData } from "./result-files.ts";
27
27
  import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
28
+ import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
28
29
 
29
30
  const WATCHER_RESTART_DELAY_MS = 3000;
30
31
  const POLL_INTERVAL_MS = 3000;
@@ -60,10 +61,13 @@ type ResultWatcherDeps = {
60
61
  /** Control how slow result-index scans are logged. Defaults to \"activity\". */
61
62
  resultScanLogging?: "all" | "activity" | "off";
62
63
  platform?: NodeJS.Platform;
64
+ /** Shared current/predecessor session ownership used by the notifier. */
65
+ ownership?: Pick<ResultDeliveryOwnership, "owns" | "claimedSessionIds">;
63
66
  };
64
67
 
65
68
  type ResultFileChild = {
66
69
  agent?: string;
70
+ sessionName?: string;
67
71
  output?: string;
68
72
  structuredOutput?: unknown;
69
73
  outputState?: SubagentOutputState;
@@ -200,6 +204,7 @@ export function createResultWatcher(
200
204
  deps: ResultWatcherDeps = {},
201
205
  ): {
202
206
  startResultWatcher: () => void;
207
+ transitionResultDelivery: () => void;
203
208
  primeExistingResults: (options?: { triggerTurn?: boolean }) => void;
204
209
  refreshResultDelivery: () => void;
205
210
  stopResultWatcher: () => void;
@@ -218,14 +223,16 @@ export function createResultWatcher(
218
223
  // The sole in-memory ownership lease. It is acquired for one active session
219
224
  // and revoked before the watcher, queues, or callbacks are torn down.
220
225
  let activeSessionId: string | null = null;
226
+ const ownsResult = deps.ownership?.owns
227
+ ?? ((sessionId: string, completionOwnerId: unknown) => sessionId === state.currentSessionId
228
+ && typeof completionOwnerId === "string"
229
+ && completionOwnerId === state.completionOwnerId);
230
+ const claimedSessionIds = () => deps.ownership?.claimedSessionIds() ?? [];
221
231
 
222
232
  const ownsCompletion = (sessionId: string, completionOwnerId: unknown, epoch: number) => {
223
233
  if (!deliveryActive || epoch !== deliveryEpoch) return false;
224
234
  if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
225
- return activeSessionId === sessionId
226
- && state.currentSessionId === sessionId
227
- && typeof completionOwnerId === "string"
228
- && completionOwnerId === state.completionOwnerId;
235
+ return activeSessionId === state.currentSessionId && ownsResult(sessionId, completionOwnerId);
229
236
  };
230
237
 
231
238
  const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
@@ -246,8 +253,11 @@ export function createResultWatcher(
246
253
  const resultPayloadPath = (file: string, observed?: ReadonlySet<string>): string | undefined => {
247
254
  if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
248
255
  const runId = file.replace(/\.json$/i, "");
249
- const sessionResult = state.currentSessionId ? resultPayloadPathForSessionRun(resultsDir, state.currentSessionId, runId) : undefined;
250
- if (sessionResult) return sessionResult;
256
+ for (const sessionId of [state.currentSessionId, ...claimedSessionIds()]) {
257
+ if (!sessionId) continue;
258
+ const sessionResult = resultPayloadPathForSessionRun(resultsDir, sessionId, runId);
259
+ if (sessionResult) return sessionResult;
260
+ }
251
261
  const observerResult = resultPayloadPathForMissionObserverRun(resultsDir, runId);
252
262
  if (observerResult) return observerResult;
253
263
  if (observed?.has(runId)) {
@@ -308,7 +318,7 @@ export function createResultWatcher(
308
318
  // Missing identity stays on the normal parser path so malformed or legacy
309
319
  // files keep their existing diagnostics and compatibility behavior.
310
320
  if (!identity.sessionId) return true;
311
- if (identity.sessionId === state.currentSessionId && identity.completionOwnerId === state.completionOwnerId) return true;
321
+ if (ownsResult(identity.sessionId, identity.completionOwnerId)) return true;
312
322
  if (identity.asyncDir && fsApi.existsSync(path.join(identity.asyncDir, MISSION_BINDING_FILE))) return true;
313
323
  if (identity.runId && (observed ?? observedRunIds()).has(identity.runId)) return true;
314
324
  return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
@@ -473,6 +483,7 @@ export function createResultWatcher(
473
483
  : undefined;
474
484
  return {
475
485
  agent: result.agent ?? data.agent ?? `step-${index + 1}`,
486
+ ...(result.sessionName ? { sessionName: result.sessionName } : {}),
476
487
  status: resolveSubagentResultStatus({
477
488
  success: result.success,
478
489
  state: childState,
@@ -621,8 +632,9 @@ export function createResultWatcher(
621
632
  };
622
633
  const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
623
634
  const files = new Set<string>();
624
- if (state.currentSessionId) {
625
- for (const file of resultCandidateFilesForSession(resultsDir, state.currentSessionId)) files.add(file);
635
+ for (const sessionId of [state.currentSessionId, ...claimedSessionIds()]) {
636
+ if (!sessionId) continue;
637
+ for (const file of resultCandidateFilesForSession(resultsDir, sessionId)) files.add(file);
626
638
  }
627
639
  for (const runId of state.asyncJobs.keys()) files.add(`${runId}.json`);
628
640
  for (const file of missionObserverResultCandidateFiles(resultsDir)) files.add(file);
@@ -755,6 +767,13 @@ export function createResultWatcher(
755
767
  }
756
768
  };
757
769
 
770
+ const transitionResultDelivery = () => {
771
+ deliveryActive = true;
772
+ activeSessionId = state.currentSessionId;
773
+ deliveryEpoch += 1;
774
+ identityCache.clear();
775
+ };
776
+
758
777
  const stopResultWatcher = () => {
759
778
  deliveryActive = false;
760
779
  activeSessionId = null;
@@ -769,5 +788,5 @@ export function createResultWatcher(
769
788
  identityCache.clear();
770
789
  };
771
790
 
772
- return { startResultWatcher, primeExistingResults, stopResultWatcher, refreshResultDelivery: () => { primeExistingResults(); startDemandPolling(); } };
791
+ return { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher, refreshResultDelivery: () => { primeExistingResults(); startDemandPolling(); } };
773
792
  }
@@ -39,7 +39,7 @@ export function formatResumeFirstFailedRunDetail(run: AsyncRunSummary): string |
39
39
  }
40
40
 
41
41
  export function formatResumeFirstFailedRunsNote(runs: AsyncRunSummary[]): string {
42
- const failedRuns = runs.filter((run) => run.state === "failed");
42
+ const failedRuns = runs.filter((run) => run.state === "failed" || run.state === "partial");
43
43
  const detachGuidance = failedRuns
44
44
  .map(formatIntercomDetachGuidance)
45
45
  .filter((guidance): guidance is string => Boolean(guidance));
@@ -27,7 +27,7 @@ export interface RetainedChild {
27
27
  }
28
28
 
29
29
  function isRetainedChildState(state: AsyncRunSummary["state"]): state is RetainedChildState {
30
- return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
30
+ return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped";
31
31
  }
32
32
 
33
33
  function isTerminalStepStatus(status: AsyncRunSummary["steps"][number]["status"]): boolean {
@@ -23,6 +23,7 @@ import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegis
23
23
  import { readMissionBinding } from "../../missions/lifecycle.ts";
24
24
  import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
25
25
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
26
+ import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
26
27
  import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
27
28
  import { getExternalJobProvider } from "../../api/external-job-provider.ts";
28
29
 
@@ -64,13 +65,14 @@ function formatCapacityOwner(inspect: ActiveAsyncCapacityInspection): string[] {
64
65
  }
65
66
 
66
67
  function formatWorkflowDebug(status: AsyncStatus): string[] {
67
- if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey) return [];
68
+ if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey && !status.lane) return [];
68
69
  const lines = [
69
70
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
70
71
  status.mode === "workflow" ? `Workflow children: ${(status.steps ?? []).length}` : undefined,
72
+ status.lane ? `Lane: ${status.lane.key}${status.lane.mode ? ` (${status.lane.mode})` : ""}` : undefined,
71
73
  ].filter((line): line is string => line !== undefined);
72
74
  for (const [index, step] of (status.steps ?? []).entries()) {
73
- lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${step.agent} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}`);
75
+ lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${runStatusStepDisplayName(step)} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}${step.lane ? ` · lane ${step.lane.key}` : ""}${step.worktreePath ? ` · worktree ${step.worktreePath} · branch ${step.branch ?? "unknown"}` : ""}`);
74
76
  }
75
77
  return lines;
76
78
  }
@@ -88,6 +90,7 @@ function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string;
88
90
  `Mode: ${status.mode}`,
89
91
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}` : undefined,
90
92
  status.workflowKey ? `Workflow key: ${status.workflowKey}` : undefined,
93
+ status.lane ? `Lane: ${status.lane.key}${status.lane.mode ? ` (${status.lane.mode})` : ""}` : undefined,
91
94
  `Status process terminal: ${formatProcessTerminal(overlayProcessTerminal)}`,
92
95
  `Sidecar process terminal: ${formatProcessTerminal(sidecarProcessTerminal)}`,
93
96
  ...formatCapacityOwner(capacity),
@@ -152,7 +155,12 @@ function stepLineLabel(status: AsyncStatus, index: number): string {
152
155
  return `Step ${index + 1}`;
153
156
  }
154
157
 
158
+ function runStatusStepDisplayName(step: { agent: string; sessionName?: string; label?: string }): string {
159
+ return step.sessionName?.trim() || (step.label ? `${step.label} (${step.agent})` : step.agent);
160
+ }
161
+
155
162
  function nestedRunDisplayName(run: NestedRunSummary): string {
163
+ if (run.sessionName?.trim()) return run.sessionName.trim();
156
164
  if (run.agent) return run.agent;
157
165
  if (run.agents?.length) return run.agents.join(", ");
158
166
  return run.id;
@@ -197,7 +205,7 @@ function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
197
205
  for (const child of run.children) {
198
206
  const output = rememberedForegroundChildOutput(child).trim().split(/\r?\n/).find((line) => line.trim());
199
207
  const parts = [
200
- `${child.index + 1}. ${child.agent} ${child.status}`,
208
+ `${child.index + 1}. ${child.sessionName?.trim() || child.agent} ${child.status}`,
201
209
  child.exitCode !== undefined ? `exit ${child.exitCode}` : undefined,
202
210
  child.detachedReason ? `detached: ${child.detachedReason}` : undefined,
203
211
  child.acceptance ? `acceptance: ${child.acceptance.status}` : undefined,
@@ -241,7 +249,7 @@ function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options:
241
249
  const lines = [
242
250
  `Run: ${run.runId}`,
243
251
  `State: ${child.status}`,
244
- `Child: ${index} (${child.agent})`,
252
+ `Child: ${index} (${child.sessionName?.trim() || child.agent})`,
245
253
  child.sessionFile ? `Session: ${child.sessionFile}` : undefined,
246
254
  child.transcriptPath ? `Transcript: ${child.transcriptPath}` : undefined,
247
255
  child.artifactPaths?.outputPath ? `Output: ${child.artifactPaths.outputPath}` : undefined,
@@ -277,7 +285,7 @@ function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): stri
277
285
  for (const [index, step] of run.steps.entries()) {
278
286
  const activity = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
279
287
  const budget = step.turnBudget ? `, turn budget: ${step.turnBudget.turnCount}/${step.turnBudget.maxTurns}+${step.turnBudget.graceTurns} (${step.turnBudget.outcome})` : "";
280
- lines.push(` ${index + 1}. ${step.agent} ${step.status}${activity ? `, ${activity}` : ""}${budget}${step.error ? `, error: ${step.error}` : ""}`);
288
+ lines.push(` ${index + 1}. ${step.sessionName?.trim() || step.agent} ${step.status}${activity ? `, ${activity}` : ""}${budget}${step.error ? `, error: ${step.error}` : ""}`);
281
289
  lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true }));
282
290
  }
283
291
  }
@@ -504,6 +512,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
504
512
  statusActivityText ? `Activity: ${statusActivityText}` : undefined,
505
513
  steeringText ? `Steering: ${steeringText}` : undefined,
506
514
  `Mode: ${status.mode}`,
515
+ ...(status.preflight ? [formatWorkflowPreflightPlanSummary(status.preflight)] : []),
516
+ ...(status.workflow?.preflightWarnings?.length ? [formatWorkflowPreflightWarningSummary(status.workflow.preflightWarnings)] : []),
507
517
  runFanoutBudget ? formatRunFanoutBudget(runFanoutBudget) : undefined,
508
518
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
509
519
  status.mode === "workflow" && workflowReturnPreview !== undefined ? `Return: ${workflowReturnPreview}` : undefined,
@@ -535,7 +545,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
535
545
  const errorText = step.error ? `, error: ${step.error}` : "";
536
546
  const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}` : "";
537
547
  const budgetText = step.turnBudget ? `, turn budget: ${step.turnBudget.turnCount}/${step.turnBudget.maxTurns}+${step.turnBudget.graceTurns} (${step.turnBudget.outcome})` : "";
538
- const display = step.label ? `${step.label} (${step.agent})` : step.agent;
548
+ const display = runStatusStepDisplayName(step);
539
549
  const phase = step.phase ? `[${step.phase}] ` : "";
540
550
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
541
551
  if (step.runner?.type === "external-cli") {
@@ -608,7 +618,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
608
618
 
609
619
  const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
610
620
  if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
611
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
621
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(status.preflight ? { preflight: status.preflight } : {}), ...(status.workflow?.preflightWarnings?.length ? { preflightWarnings: status.workflow.preflightWarnings } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
612
622
  }
613
623
  }
614
624
 
@@ -622,7 +632,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
622
632
  }
623
633
  try {
624
634
  const raw = fs.readFileSync(resultPath, "utf-8");
625
- const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
635
+ const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
626
636
  if (params.view === "transcript") {
627
637
  try {
628
638
  return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };