pi-subagents 0.34.0 → 0.35.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 (98) hide show
  1. package/CHANGELOG.md +78 -9
  2. package/README.md +213 -32
  3. package/index.ts +1 -0
  4. package/install.mjs +1 -1
  5. package/package.json +23 -8
  6. package/prompts/review-loop.md +3 -1
  7. package/skills/pi-subagents/SKILL.md +87 -25
  8. package/src/agents/agent-management.ts +82 -15
  9. package/src/agents/agent-serializer.ts +19 -0
  10. package/src/agents/agents.ts +91 -49
  11. package/src/agents/frontmatter.ts +67 -13
  12. package/src/agents/skills.ts +25 -12
  13. package/src/api/background-work.ts +197 -0
  14. package/src/api/delegation.ts +158 -0
  15. package/src/extension/chain-validation.ts +165 -0
  16. package/src/extension/doctor.ts +15 -0
  17. package/src/extension/fanout-child.ts +3 -1
  18. package/src/extension/index.ts +65 -124
  19. package/src/extension/rpc.ts +10 -2
  20. package/src/extension/schemas.ts +18 -14
  21. package/src/extension/steering-notices.ts +35 -0
  22. package/src/extension/tool-description.ts +20 -9
  23. package/src/intercom/intercom-bridge.ts +3 -2
  24. package/src/intercom/native-supervisor-channel.ts +9 -1
  25. package/src/intercom/result-intercom.ts +4 -0
  26. package/src/runs/background/async-execution.ts +293 -44
  27. package/src/runs/background/async-job-tracker.ts +56 -9
  28. package/src/runs/background/async-resume.ts +159 -52
  29. package/src/runs/background/async-status.ts +25 -18
  30. package/src/runs/background/auto-drain.ts +67 -0
  31. package/src/runs/background/chain-root-attachment.ts +16 -8
  32. package/src/runs/background/control-channel.ts +260 -13
  33. package/src/runs/background/fleet-view.ts +23 -2
  34. package/src/runs/background/notify.ts +79 -10
  35. package/src/runs/background/result-watcher.ts +12 -9
  36. package/src/runs/background/run-id-resolver.ts +14 -2
  37. package/src/runs/background/run-status.ts +23 -15
  38. package/src/runs/background/scheduled-runs.ts +3 -0
  39. package/src/runs/background/stale-run-reconciler.ts +32 -10
  40. package/src/runs/background/steering.ts +237 -0
  41. package/src/runs/background/subagent-runner.ts +898 -236
  42. package/src/runs/background/subagent-wait.ts +484 -0
  43. package/src/runs/background/top-level-async.ts +2 -1
  44. package/src/runs/background/wait-config.ts +36 -0
  45. package/src/runs/background/wait-tool.ts +26 -0
  46. package/src/runs/foreground/async-steering-action.ts +230 -0
  47. package/src/runs/foreground/chain-clarify.ts +22 -6
  48. package/src/runs/foreground/chain-execution.ts +50 -32
  49. package/src/runs/foreground/execution.ts +308 -94
  50. package/src/runs/foreground/subagent-executor.ts +592 -268
  51. package/src/runs/shared/acceptance.ts +355 -97
  52. package/src/runs/shared/child-protocol.ts +121 -0
  53. package/src/runs/shared/completion-guard.ts +8 -127
  54. package/src/runs/shared/dynamic-fanout.ts +6 -4
  55. package/src/runs/shared/model-fallback.ts +36 -0
  56. package/src/runs/shared/nested-events.ts +9 -4
  57. package/src/runs/shared/nested-render.ts +4 -1
  58. package/src/runs/shared/parallel-utils.ts +7 -0
  59. package/src/runs/shared/pi-args.ts +34 -7
  60. package/src/runs/shared/pi-spawn.ts +18 -12
  61. package/src/runs/shared/session-lease.ts +279 -0
  62. package/src/runs/shared/single-output.ts +61 -6
  63. package/src/runs/shared/spawn-budget.ts +128 -0
  64. package/src/runs/shared/subagent-control.ts +10 -6
  65. package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
  66. package/src/runs/shared/task-intent.ts +176 -0
  67. package/src/runs/shared/tool-availability.ts +65 -0
  68. package/src/runs/shared/turn-budget.ts +49 -4
  69. package/src/shared/atomic-json.ts +4 -1
  70. package/src/shared/fork-context.ts +28 -3
  71. package/src/shared/model-info.ts +7 -4
  72. package/src/shared/status-format.ts +7 -1
  73. package/src/shared/types.ts +203 -25
  74. package/src/shared/utils.ts +35 -7
  75. package/src/slash/delegation-adapters.ts +457 -0
  76. package/src/slash/delegation-request.ts +103 -0
  77. package/src/slash/prompt-template-bridge.ts +167 -344
  78. package/src/slash/slash-commands.ts +239 -6
  79. package/src/slash/subagents-admin.ts +428 -0
  80. package/src/slash/subagents-editor.ts +86 -0
  81. package/src/tui/fleet.ts +405 -0
  82. package/src/tui/render.ts +90 -16
  83. package/src/watchdog/change-signature.ts +127 -0
  84. package/src/watchdog/child-status.ts +205 -0
  85. package/src/watchdog/emission-guard.ts +123 -0
  86. package/src/watchdog/lsp-diagnostics.ts +532 -0
  87. package/src/watchdog/model-selection.ts +167 -0
  88. package/src/watchdog/register-child.ts +117 -0
  89. package/src/watchdog/register-main.ts +433 -0
  90. package/src/watchdog/render.ts +54 -0
  91. package/src/watchdog/review.ts +293 -0
  92. package/src/watchdog/runtime.ts +712 -0
  93. package/src/watchdog/settings.ts +528 -0
  94. package/src/watchdog/tool-actions.ts +155 -0
  95. package/src/watchdog/turn-delta.ts +161 -0
  96. package/src/watchdog/types.ts +188 -0
  97. package/src/watchdog/warning-format.ts +73 -0
  98. package/src/runs/background/wait.ts +0 -394
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
5
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
6
6
  import { readStatus } from "../../shared/utils.ts";
7
7
  import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
8
8
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
@@ -27,8 +27,7 @@ interface AsyncRunStepSummary {
27
27
  recentOutput?: string[];
28
28
  turnCount?: number;
29
29
  toolCount?: number;
30
- steerCount?: number;
31
- lastSteerAt?: number;
30
+ steering?: SteeringStatus;
32
31
  durationMs?: number;
33
32
  tokens?: TokenUsage;
34
33
  totalCost?: CostSummary;
@@ -38,6 +37,7 @@ interface AsyncRunStepSummary {
38
37
  attemptedModels?: string[];
39
38
  error?: string;
40
39
  timedOut?: boolean;
40
+ stopped?: boolean;
41
41
  turnBudget?: TurnBudgetState;
42
42
  turnBudgetExceeded?: boolean;
43
43
  wrapUpRequested?: boolean;
@@ -48,7 +48,7 @@ export interface AsyncRunSummary {
48
48
  id: string;
49
49
  asyncDir: string;
50
50
  sessionId?: string;
51
- state: "queued" | "running" | "complete" | "failed" | "paused";
51
+ state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped";
52
52
  error?: string;
53
53
  activityState?: ActivityState;
54
54
  lastActivityAt?: number;
@@ -57,8 +57,7 @@ export interface AsyncRunSummary {
57
57
  currentPath?: string;
58
58
  turnCount?: number;
59
59
  toolCount?: number;
60
- steerCount?: number;
61
- lastSteerAt?: number;
60
+ steering?: SteeringStatus;
62
61
  mode: SubagentRunMode;
63
62
  cwd?: string;
64
63
  startedAt: number;
@@ -67,6 +66,7 @@ export interface AsyncRunSummary {
67
66
  timeoutMs?: number;
68
67
  deadlineAt?: number;
69
68
  timedOut?: boolean;
69
+ stopped?: boolean;
70
70
  turnBudget?: TurnBudgetState;
71
71
  turnBudgetExceeded?: boolean;
72
72
  wrapUpRequested?: boolean;
@@ -178,8 +178,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
178
178
  ...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
179
179
  ...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
180
180
  ...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
181
- ...(step.steerCount !== undefined ? { steerCount: step.steerCount } : {}),
182
- ...(step.lastSteerAt !== undefined ? { lastSteerAt: step.lastSteerAt } : {}),
181
+ ...(step.steering ? { steering: step.steering } : {}),
183
182
  ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
184
183
  ...(step.tokens ? { tokens: step.tokens } : {}),
185
184
  ...(step.totalCost ? { totalCost: step.totalCost } : {}),
@@ -189,6 +188,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
189
188
  ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
190
189
  ...(step.error ? { error: step.error } : {}),
191
190
  ...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
191
+ ...(step.stopped !== undefined ? { stopped: step.stopped } : {}),
192
192
  ...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
193
193
  ...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
194
194
  ...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
@@ -209,8 +209,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
209
209
  currentPath: status.currentPath,
210
210
  turnCount: status.turnCount,
211
211
  toolCount: status.toolCount,
212
- steerCount: status.steerCount,
213
- lastSteerAt: status.lastSteerAt,
212
+ steering: status.steering,
214
213
  mode: status.mode,
215
214
  cwd: status.cwd,
216
215
  startedAt: status.startedAt,
@@ -219,6 +218,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
219
218
  ...(status.timeoutMs !== undefined ? { timeoutMs: status.timeoutMs } : {}),
220
219
  ...(status.deadlineAt !== undefined ? { deadlineAt: status.deadlineAt } : {}),
221
220
  ...(status.timedOut !== undefined ? { timedOut: status.timedOut } : {}),
221
+ ...(status.stopped !== undefined ? { stopped: status.stopped } : {}),
222
222
  ...(status.turnBudget ? { turnBudget: status.turnBudget } : {}),
223
223
  ...(status.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: status.turnBudgetExceeded } : {}),
224
224
  ...(status.wrapUpRequested !== undefined ? { wrapUpRequested: status.wrapUpRequested } : {}),
@@ -237,12 +237,17 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
237
237
  };
238
238
  }
239
239
 
240
+ export function summarizeAsyncStatus(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
241
+ return statusToSummary(asyncDir, status);
242
+ }
243
+
240
244
  function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
241
245
  const rank = (state: AsyncRunSummary["state"]): number => {
242
246
  switch (state) {
243
247
  case "running": return 0;
244
248
  case "queued": return 1;
245
249
  case "failed": return 2;
250
+ case "stopped": return 2;
246
251
  case "paused": return 2;
247
252
  case "complete": return 3;
248
253
  }
@@ -294,11 +299,13 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
294
299
  if (options.sessionId && status.sessionId !== options.sessionId) continue;
295
300
  const nestedWarnings: string[] = [];
296
301
  let nestedRoute: NestedRoute | undefined;
297
- try {
298
- nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
299
- if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
300
- } catch (error) {
301
- nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
302
+ if (options.reconcile !== false) {
303
+ try {
304
+ nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
305
+ if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
306
+ } catch (error) {
307
+ nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
308
+ }
302
309
  }
303
310
  const summary = statusToSummary(asyncDir, status, nestedWarnings, nestedRoute);
304
311
  runs.push(summary);
@@ -308,18 +315,18 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
308
315
  return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
309
316
  }
310
317
 
311
- function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number; steerCount?: number; lastSteerAt?: number; turnBudget?: TurnBudgetState; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean }): string | undefined {
318
+ function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number; steering?: SteeringStatus; turnBudget?: TurnBudgetState; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean }): string | undefined {
312
319
  const facts: string[] = [];
313
320
  if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
314
321
  else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
315
322
  if (input.currentPath) facts.push(shortenPath(input.currentPath));
316
323
  if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
317
324
  if (input.turnBudgetExceeded && input.turnBudget) facts.push(`turn budget exceeded ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
325
+ else if (input.turnBudget?.outcome === "termination-deferred") facts.push(`turn-budget termination deferred ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
318
326
  else if (input.wrapUpRequested && input.turnBudget) facts.push(`wrap-up requested ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}`);
319
327
  else if (input.turnBudget) facts.push(`turn budget ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
320
328
  if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
321
- if (input.steerCount !== undefined) facts.push(`${input.steerCount} steers`);
322
- if (typeof input.lastSteerAt === "number" && Number.isFinite(input.lastSteerAt)) facts.push(`last steer ${new Date(input.lastSteerAt).toISOString()}`);
329
+ if (input.steering) facts.push(`steering ${input.steering.scheduled} scheduled, ${input.steering.pending} pending, ${input.steering.delivered} delivered, ${input.steering.failed} failed, ${input.steering.recovered} recovered`);
323
330
  const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
324
331
  return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
325
332
  }
@@ -0,0 +1,67 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { snapshotBackgroundWork } from "../../api/background-work.ts";
3
+ import { ASYNC_DIR, RESULTS_DIR, type Details, type SubagentState } from "../../shared/types.ts";
4
+ import { listAsyncRuns } from "./async-status.ts";
5
+ import { waitForSubagents, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
6
+
7
+ export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
8
+
9
+ export interface AutoDrainDeps {
10
+ state: SubagentState;
11
+ events?: WaitEventBus;
12
+ timeoutMs?: number;
13
+ now?: () => number;
14
+ wait?: (
15
+ params: SubagentWaitParams,
16
+ signal: AbortSignal | undefined,
17
+ deps: SubagentWaitDeps,
18
+ ) => Promise<AgentToolResult<Details>>;
19
+ hasWork?: (sessionId: string, nowMs: number) => boolean;
20
+ }
21
+
22
+ function resultText(value: AgentToolResult<Details>): string {
23
+ return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
24
+ }
25
+
26
+ function hasOutstandingWork(sessionId: string, nowMs: number): boolean {
27
+ const asyncRuns = listAsyncRuns(ASYNC_DIR, {
28
+ states: ["queued", "running"],
29
+ sessionId,
30
+ resultsDir: RESULTS_DIR,
31
+ now: () => nowMs,
32
+ });
33
+ return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0;
34
+ }
35
+
36
+ /** Drain all work owned by the current headless session, including work added while draining. */
37
+ export async function drainOutstandingWork(deps: AutoDrainDeps): Promise<void> {
38
+ const sessionId = deps.state.currentSessionId;
39
+ if (!sessionId) throw new Error("Cannot auto-drain background work without an active session identity.");
40
+ const now = deps.now ?? Date.now;
41
+ const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
42
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
43
+ const deadlineAt = now() + timeoutMs;
44
+ const hasWork = deps.hasWork ?? hasOutstandingWork;
45
+ const wait = deps.wait ?? waitForSubagents;
46
+
47
+ while (hasWork(sessionId, now())) {
48
+ const remainingMs = deadlineAt - now();
49
+ if (remainingMs <= 0) {
50
+ throw new Error(`Auto-drain timed out after ${timeoutMs}ms with background work still active in session '${sessionId}'.`);
51
+ }
52
+ const waitResult = await wait(
53
+ { all: true, timeoutMs: remainingMs },
54
+ undefined,
55
+ {
56
+ state: deps.state,
57
+ events: deps.events,
58
+ now,
59
+ stopOnAttention: false,
60
+ failOnFailedRuns: true,
61
+ },
62
+ );
63
+ if (waitResult.isError) {
64
+ throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "subagent_wait returned an error without details"}.`);
65
+ }
66
+ }
67
+ }
@@ -27,6 +27,7 @@ export interface ImportedAsyncRootResult {
27
27
  structuredOutputSchemaPath?: string;
28
28
  acceptance?: AcceptanceLedger;
29
29
  timedOut?: boolean;
30
+ stopped?: boolean;
30
31
  }
31
32
 
32
33
  interface AsyncResultFile {
@@ -35,12 +36,14 @@ interface AsyncResultFile {
35
36
  summary?: string;
36
37
  error?: string;
37
38
  timedOut?: boolean;
39
+ stopped?: boolean;
38
40
  results?: Array<{
39
41
  agent?: string;
40
42
  output?: string;
41
43
  error?: string;
42
44
  success?: boolean;
43
45
  timedOut?: boolean;
46
+ stopped?: boolean;
44
47
  sessionFile?: string;
45
48
  intercomTarget?: string;
46
49
  model?: string;
@@ -54,8 +57,8 @@ interface AsyncResultFile {
54
57
  }>;
55
58
  }
56
59
 
57
- const TERMINAL_STATES = new Set(["complete", "failed", "paused"]);
58
- const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "paused"]);
60
+ const TERMINAL_STATES = new Set(["complete", "failed", "paused", "stopped"]);
61
+ const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "paused", "stopped"]);
59
62
 
60
63
  function readResultFile(resultPath: string): AsyncResultFile | undefined {
61
64
  try {
@@ -79,11 +82,12 @@ function isTerminalStatus(status: AsyncStatus | null, index: number): boolean {
79
82
  return TERMINAL_STATES.has(status.state);
80
83
  }
81
84
 
82
- function resultState(result: AsyncResultFile | undefined, child: NonNullable<AsyncResultFile["results"]>[number] | undefined): "complete" | "failed" | "paused" | undefined {
85
+ function resultState(result: AsyncResultFile | undefined, child: NonNullable<AsyncResultFile["results"]>[number] | undefined): "complete" | "failed" | "paused" | "stopped" | undefined {
83
86
  if (!result) return undefined;
87
+ if (child?.stopped === true) return "stopped";
84
88
  if (child?.success === true) return "complete";
85
- if (child?.success === false) return result.state === "paused" ? "paused" : "failed";
86
- if (result.state === "complete" || result.state === "failed" || result.state === "paused") return result.state;
89
+ if (child?.success === false) return result.state === "stopped" ? "stopped" : result.state === "paused" ? "paused" : "failed";
90
+ if (result.state === "complete" || result.state === "failed" || result.state === "paused" || result.state === "stopped") return result.state;
87
91
  if (result.success === true) return "complete";
88
92
  if (result.success === false) return "failed";
89
93
  return undefined;
@@ -92,7 +96,8 @@ function resultState(result: AsyncResultFile | undefined, child: NonNullable<Asy
92
96
  function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus, step: NonNullable<AsyncStatus["steps"]>[number] | undefined): ImportedAsyncRootResult {
93
97
  const agent = step?.agent ?? status.steps?.[root.index]?.agent ?? "subagent";
94
98
  const timedOut = step?.timedOut === true || status.timedOut === true;
95
- const message = step?.error ?? status.error ?? `Attached async root ${root.runId} ended without a result file at ${root.resultPath}.`;
99
+ const stopped = step?.stopped === true || status.stopped === true || status.state === "stopped";
100
+ const message = step?.error ?? status.error ?? (stopped ? "Subagent stopped by user." : `Attached async root ${root.runId} ended without a result file at ${root.resultPath}.`);
96
101
  return {
97
102
  agent,
98
103
  output: message,
@@ -100,6 +105,7 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
100
105
  exitCode: 1,
101
106
  error: message,
102
107
  ...(timedOut ? { timedOut: true } : {}),
108
+ ...(stopped ? { stopped: true } : {}),
103
109
  ...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
104
110
  ...(step?.model ? { model: step.model } : {}),
105
111
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
@@ -136,8 +142,9 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
136
142
  const agent = child?.agent ?? step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent";
137
143
  const output = child?.output ?? result.summary ?? "";
138
144
  const timedOut = child?.timedOut === true || step?.timedOut === true || result.timedOut === true || status?.timedOut === true;
139
- const success = state === "complete" && !timedOut;
140
- const error = child?.error ?? (success ? undefined : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
145
+ const stopped = child?.stopped === true || step?.stopped === true || result.stopped === true || status?.stopped === true || state === "stopped";
146
+ const success = state === "complete" && !timedOut && !stopped;
147
+ const error = child?.error ?? (success ? undefined : stopped ? "Subagent stopped by user." : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
141
148
  return {
142
149
  agent,
143
150
  output: success ? output : (output || error || ""),
@@ -145,6 +152,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
145
152
  exitCode: success ? 0 : 1,
146
153
  ...(error ? { error } : {}),
147
154
  ...(timedOut ? { timedOut: true } : {}),
155
+ ...(stopped ? { stopped: true } : {}),
148
156
  ...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
149
157
  ...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
150
158
  ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),