pi-subagents 0.63.0 → 0.65.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 (107) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +92 -114
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +6 -5
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +46 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/notify.ts +41 -2
  34. package/src/runs/background/process-terminal.ts +1 -1
  35. package/src/runs/background/run-child-session.ts +613 -0
  36. package/src/runs/background/run-status.ts +0 -1
  37. package/src/runs/background/runner-aliases.ts +125 -0
  38. package/src/runs/background/runner-child-sessions.ts +31 -0
  39. package/src/runs/background/scheduled-runs.ts +18 -4
  40. package/src/runs/background/subagent-runner.ts +203 -898
  41. package/src/runs/foreground/async-steering-action.ts +1 -17
  42. package/src/runs/foreground/execution.ts +196 -378
  43. package/src/runs/foreground/foreground-control.ts +4 -0
  44. package/src/runs/foreground/subagent-executor.ts +121 -58
  45. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  46. package/src/runs/shared/abort-recovery.ts +3 -3
  47. package/src/runs/shared/acceptance.ts +10 -0
  48. package/src/runs/shared/async-status-projection.ts +11 -43
  49. package/src/runs/shared/capability-ceiling.ts +1 -2
  50. package/src/runs/shared/child-hooks.ts +25 -0
  51. package/src/runs/shared/child-identity.ts +13 -2
  52. package/src/runs/shared/child-launch.ts +314 -0
  53. package/src/runs/shared/child-lifecycle.ts +25 -0
  54. package/src/runs/shared/child-runtime-config.ts +126 -0
  55. package/src/runs/shared/child-session.ts +342 -0
  56. package/src/runs/shared/child-tool-plan.ts +530 -0
  57. package/src/runs/shared/claude-code-adapter.ts +5 -1
  58. package/src/runs/shared/completion-guard.ts +1 -1
  59. package/src/runs/shared/external-cli-preflight.ts +16 -0
  60. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  61. package/src/runs/shared/model-exclusions.ts +82 -14
  62. package/src/runs/shared/model-fallback.ts +47 -4
  63. package/src/runs/shared/nested-events.ts +29 -45
  64. package/src/runs/shared/nested-path.ts +0 -14
  65. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  66. package/src/runs/shared/parallel-utils.ts +0 -2
  67. package/src/runs/shared/permissions.ts +0 -13
  68. package/src/runs/shared/process-signal.ts +4 -1
  69. package/src/runs/shared/run-fanout-budget.ts +0 -13
  70. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  71. package/src/runs/shared/structured-output.ts +17 -4
  72. package/src/runs/shared/subagent-control.ts +6 -2
  73. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  74. package/src/runs/shared/tool-availability.ts +18 -62
  75. package/src/runs/shared/tool-budget.ts +0 -14
  76. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  77. package/src/runs/shared/worktree.ts +117 -30
  78. package/src/shared/child-session-name.ts +1 -1
  79. package/src/shared/jsonl-writer.ts +11 -0
  80. package/src/shared/thinking-ceiling.ts +0 -6
  81. package/src/shared/types.ts +64 -30
  82. package/src/shared/utils.ts +3 -4
  83. package/src/slash/slash-commands.ts +0 -6
  84. package/src/tui/fleet.ts +0 -1
  85. package/src/tui/render.ts +238 -31
  86. package/src/watchdog/child-status.ts +54 -34
  87. package/src/watchdog/diff-tool.ts +77 -0
  88. package/src/watchdog/emission-guard.ts +5 -3
  89. package/src/watchdog/guidance.ts +20 -0
  90. package/src/watchdog/register-child.ts +28 -25
  91. package/src/watchdog/register-main.ts +10 -9
  92. package/src/watchdog/render.ts +4 -5
  93. package/src/watchdog/review.ts +15 -4
  94. package/src/watchdog/rules.ts +70 -0
  95. package/src/watchdog/runtime.ts +75 -92
  96. package/src/watchdog/scope.ts +0 -11
  97. package/src/watchdog/settings.ts +48 -104
  98. package/src/watchdog/types.ts +18 -32
  99. package/src/watchdog/warning-format.ts +0 -1
  100. package/src/workflows/chat-progress.ts +3 -2
  101. package/src/workflows/scripted-workflow.ts +48 -1
  102. package/src/workflows/workflow-checklist.ts +10 -12
  103. package/src/workflows/workflow-preflight.ts +28 -1
  104. package/src/runs/shared/child-protocol.ts +0 -415
  105. package/src/runs/shared/pi-args.ts +0 -1062
  106. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  107. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -0,0 +1,613 @@
1
+ /**
2
+ * Drives one in-process child session for the detached async runner: creates
3
+ * the session through the runner's `ChildSessionFactory`, mirrors its events
4
+ * into `events.jsonl`, the transcript, and the step output log, applies
5
+ * interrupt, timeout, stop, and steer requests, and folds the run into the
6
+ * step result the runner finalizes.
7
+ */
8
+ import type { Message } from "@earendil-works/pi-ai";
9
+ import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
10
+ import { extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse } from "../../shared/utils.ts";
11
+ import type { EffectsProjection, RuntimeAcknowledgedChildExtensionsV1, SubagentOutputState, ToolBudgetState, Usage } from "../../shared/types.ts";
12
+ import {
13
+ acceptChildWatchdogEvent,
14
+ applyChildWatchdogMessage,
15
+ childWatchdogIsActive,
16
+ isChildWatchdogStatusEvent,
17
+ type ChildWatchdogConfig,
18
+ type ChildWatchdogStateSnapshot,
19
+ type ChildWatchdogStatusEvent,
20
+ } from "../../watchdog/child-status.ts";
21
+ import { projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState } from "../shared/child-lifecycle.ts";
22
+ import { formatSubagentModelVerificationError } from "../shared/model-fallback.ts";
23
+ import { isMutatingTool, resolveCurrentPath } from "../shared/long-running-guard.ts";
24
+ import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
25
+ import type { InProcessChildLaunch } from "../shared/child-launch.ts";
26
+ import { projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent, type ChildSessionFactory } from "../shared/child-session.ts";
27
+ import { formatSteerMessage } from "../shared/subagent-prompt-runtime.ts";
28
+ import type { SteerDeliveryStatus, SteerRequest } from "./control-channel.ts";
29
+
30
+ export interface ChildEventContext {
31
+ runId: string;
32
+ stepIndex: number;
33
+ agent: string;
34
+ }
35
+
36
+ export interface ChildUsage {
37
+ input?: number;
38
+ inputTokens?: number;
39
+ output?: number;
40
+ outputTokens?: number;
41
+ cacheRead?: number;
42
+ cacheReadTokens?: number;
43
+ cacheWrite?: number;
44
+ cost?: { total?: number };
45
+ }
46
+
47
+ export type ChildMessage = Message & {
48
+ model?: string;
49
+ errorMessage?: string;
50
+ usage?: ChildUsage;
51
+ };
52
+
53
+ export interface ChildEvent {
54
+ type?: string;
55
+ message?: ChildMessage;
56
+ toolName?: string;
57
+ toolCallId?: string;
58
+ args?: Record<string, unknown>;
59
+ willRetry?: unknown;
60
+ }
61
+
62
+ /** Outcome of handing a steer request to a live child session. */
63
+ export interface SteerDelivery {
64
+ state: "delivered" | "queued" | "failed";
65
+ deliveryStatus?: SteerDeliveryStatus;
66
+ message: string;
67
+ }
68
+
69
+ export type StepSteerHandler = (request: SteerRequest) => Promise<SteerDelivery>;
70
+
71
+ export interface RunChildSessionInput {
72
+ factory: ChildSessionFactory;
73
+ launch: InProcessChildLaunch;
74
+ /** Prompt text; the task with its `Task:` prefix. */
75
+ prompt: string;
76
+ childWatchdog?: ChildWatchdogConfig;
77
+ childEventContext?: ChildEventContext;
78
+ /** Persist one child event into `events.jsonl`; the runner owns the bounded log. */
79
+ appendChildEvent: (event: Record<string, unknown>) => void;
80
+ /** Append one line to the step output log and the progress tab. */
81
+ writeOutputLine: (line: string) => void;
82
+ registerInterrupt?: (interrupt: (() => void) | undefined) => void;
83
+ registerTimeout?: (interrupt: (() => void) | undefined) => void;
84
+ registerStop?: (stop: (() => void) | undefined) => void;
85
+ registerSteer?: (steer: StepSteerHandler | undefined) => void;
86
+ /** Receives the sink the child's watchdog hook reports status through; the launch's `watchdogStatus` must forward to it. */
87
+ registerWatchdogStatus?: (sink: ((event: ChildWatchdogStatusEvent) => void) | undefined) => void;
88
+ timeoutMessage?: string;
89
+ stopMessage?: string;
90
+ onChildEvent?: (event: ChildEvent) => void;
91
+ transcriptWriter?: ChildTranscriptWriter;
92
+ toolTimeoutMs?: number;
93
+ runDeadlineAt?: number;
94
+ expectedModelForVerification?: string;
95
+ modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>;
96
+ mutationTools?: readonly string[];
97
+ }
98
+
99
+ export interface RunChildSessionResult {
100
+ exitCode: number;
101
+ messages: Message[];
102
+ usage: Usage;
103
+ toolCount: number;
104
+ durationMs: number;
105
+ model?: string;
106
+ error?: string;
107
+ finalOutput: string;
108
+ outputState: SubagentOutputState;
109
+ interrupted?: boolean;
110
+ timedOut?: boolean;
111
+ stopped?: boolean;
112
+ observedMutationAttempt?: boolean;
113
+ structuredOutputToolInvoked?: boolean;
114
+ structuredOutputMessageStartIndex?: number;
115
+ watchdog?: ChildWatchdogStateSnapshot;
116
+ sessionFile?: string;
117
+ currentTool?: string;
118
+ currentToolArgs?: string;
119
+ currentPath?: string;
120
+ afterCompactionSettlement?: boolean;
121
+ /** Set by the runner while it finalizes the attempt. */
122
+ toolBudget?: ToolBudgetState;
123
+ toolBudgetBlocked?: boolean;
124
+ structuredOutput?: unknown;
125
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
126
+ abortRecoveryDiagnostic?: string;
127
+ effects?: EffectsProjection;
128
+ }
129
+
130
+ /** Events the child emits while the model streams; not persisted into the diagnostic log. */
131
+ function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
132
+ return event.type !== "message_update";
133
+ }
134
+
135
+ function assistantStartsToolCall(message: Message): boolean {
136
+ return Array.isArray(message.content)
137
+ && message.content.some((part) => (part as { type?: string }).type === "toolCall");
138
+ }
139
+
140
+ function isTerminalAssistantStop(message: Message): boolean {
141
+ return (message as { stopReason?: string }).stopReason === "stop" && !assistantStartsToolCall(message);
142
+ }
143
+
144
+ function emptyUsage(): Usage {
145
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
146
+ }
147
+
148
+ function omitUndefined<T extends object>(value: T): T {
149
+ for (const key of Object.keys(value) as Array<keyof T>) {
150
+ if (value[key] === undefined) delete value[key];
151
+ }
152
+ return value;
153
+ }
154
+
155
+ const FINAL_STOP_GRACE_MS = 1000;
156
+ const HARD_FINISH_MS = 3000;
157
+ const ABORT_SETTLE_MS = 3000;
158
+
159
+ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSessionResult> {
160
+ return new Promise((resolve) => {
161
+ const startedAt = Date.now();
162
+ const messages: Message[] = [];
163
+ const usage = emptyUsage();
164
+ let model: string | undefined;
165
+ let error: string | undefined;
166
+ let assistantError: string | undefined;
167
+ let interrupted = false;
168
+ let timedOut = false;
169
+ let stopped = false;
170
+ let observedMutationAttempt = false;
171
+ let structuredOutputToolInvoked = false;
172
+ let structuredOutputMessageStartIndex: number | undefined;
173
+ let currentTool: string | undefined;
174
+ let currentToolArgs: string | undefined;
175
+ let currentPath: string | undefined;
176
+ let toolCount = 0;
177
+ let session: ChildSession | undefined;
178
+ let unsubscribe: (() => void) | undefined;
179
+ let settled = false;
180
+ let promptSettled = false;
181
+ let forcedTermination = false;
182
+ let cleanTerminalAssistantStopReceived = false;
183
+ let agentSettledReceived = false;
184
+ let compactionStartedReceived = false;
185
+ let afterCompactionSettlement = false;
186
+ let finalDrainTimer: NodeJS.Timeout | undefined;
187
+ let finalHardFinishTimer: NodeJS.Timeout | undefined;
188
+ let watchdogTailTimer: NodeJS.Timeout | undefined;
189
+ let abortSettleTimer: NodeJS.Timeout | undefined;
190
+ let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
191
+ const childLifecycleState: ChildLifecycleState = { compactionRetryActive: false };
192
+ const timeoutMessage = () => input.timeoutMessage ?? "Subagent timed out.";
193
+ const stopMessage = () => input.stopMessage ?? "Subagent stopped by user.";
194
+
195
+ type ActiveToolCall = { key: string; tool: string; args?: string; path?: string };
196
+ let activeToolSequence = 0;
197
+ const activeToolCalls = new Map<string, ActiveToolCall>();
198
+ const activeToolKeysByName = new Map<string, string[]>();
199
+ const refreshCurrentTool = (): void => {
200
+ const active = [...activeToolCalls.values()].at(-1);
201
+ currentTool = active?.tool;
202
+ currentToolArgs = active?.args;
203
+ currentPath = active?.path;
204
+ };
205
+ const recordActiveToolCall = (event: { toolCallId?: unknown; toolName: string; args?: Record<string, unknown> }): void => {
206
+ const key = toolTimeoutCallKey(event, ++activeToolSequence);
207
+ const active = omitUndefined({
208
+ key,
209
+ tool: event.toolName,
210
+ args: extractToolArgsPreview(event.args ?? {}),
211
+ path: resolveCurrentPath(event.toolName, event.args),
212
+ });
213
+ activeToolCalls.set(key, active);
214
+ const keys = activeToolKeysByName.get(active.tool) ?? [];
215
+ keys.push(key);
216
+ activeToolKeysByName.set(active.tool, keys);
217
+ refreshCurrentTool();
218
+ };
219
+ const removeActiveToolCall = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
220
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
221
+ ? `id:${event.toolCallId}`
222
+ : typeof event.toolName === "string"
223
+ ? activeToolKeysByName.get(event.toolName)?.[0]
224
+ : activeToolCalls.size === 1
225
+ ? [...activeToolCalls.keys()][0]
226
+ : undefined;
227
+ if (!key) return;
228
+ const active = activeToolCalls.get(key);
229
+ if (!active) return;
230
+ activeToolCalls.delete(key);
231
+ const keys = activeToolKeysByName.get(active.tool)?.filter((candidate) => candidate !== key) ?? [];
232
+ if (keys.length > 0) activeToolKeysByName.set(active.tool, keys);
233
+ else activeToolKeysByName.delete(active.tool);
234
+ refreshCurrentTool();
235
+ };
236
+
237
+ const abortChild = (): void => {
238
+ if (!session || settled || promptSettled) return;
239
+ void session.abort().catch(() => {
240
+ // The run settles through its prompt promise; abort failures are not separately actionable.
241
+ });
242
+ if (!abortSettleTimer) {
243
+ abortSettleTimer = setTimeout(() => {
244
+ abortSettleTimer = undefined;
245
+ if (!settled && !promptSettled) settle(undefined, true);
246
+ }, ABORT_SETTLE_MS);
247
+ abortSettleTimer.unref?.();
248
+ }
249
+ };
250
+
251
+ const writeOutputText = (text: string) => {
252
+ for (const line of text.split("\n")) {
253
+ if (line.trim()) input.writeOutputLine(line);
254
+ }
255
+ };
256
+ const appendChildEvent = (event: Record<string, unknown>) => {
257
+ if (!input.childEventContext) return;
258
+ if (!shouldPersistChildEvent(event)) return;
259
+ input.appendChildEvent({
260
+ ...event,
261
+ subagentSource: "child",
262
+ subagentRunId: input.childEventContext.runId,
263
+ subagentStepIndex: input.childEventContext.stepIndex,
264
+ subagentAgent: input.childEventContext.agent,
265
+ observedAt: Date.now(),
266
+ });
267
+ };
268
+
269
+ const clearWatchdogTailTimer = (): void => {
270
+ if (watchdogTailTimer) {
271
+ clearTimeout(watchdogTailTimer);
272
+ watchdogTailTimer = undefined;
273
+ }
274
+ };
275
+ const clearFinalDrainTimers = (): void => {
276
+ if (finalDrainTimer) {
277
+ clearTimeout(finalDrainTimer);
278
+ finalDrainTimer = undefined;
279
+ }
280
+ if (finalHardFinishTimer) {
281
+ clearTimeout(finalHardFinishTimer);
282
+ finalHardFinishTimer = undefined;
283
+ }
284
+ };
285
+ function armWatchdogTail(): void {
286
+ if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled || promptSettled) return;
287
+ watchdogTailTimer = setTimeout(() => {
288
+ watchdogTailTimer = undefined;
289
+ childWatchdogState = {
290
+ phase: "stale",
291
+ seq: (childWatchdogState?.seq ?? 0) + 1,
292
+ lastUpdate: Date.now(),
293
+ reason: "child watchdog tail timeout",
294
+ timedOut: true,
295
+ };
296
+ startFinalDrain();
297
+ }, input.childWatchdog?.watchdogTailTimeoutMs ?? 120_000);
298
+ watchdogTailTimer.unref?.();
299
+ }
300
+ // If the child emits its terminal event but its run never settles (a hook
301
+ // is stuck), abort it after a short grace period and then finish without it.
302
+ function startFinalDrain(): void {
303
+ if (childWatchdogIsActive(childWatchdogState)) {
304
+ armWatchdogTail();
305
+ return;
306
+ }
307
+ if (promptSettled || finalDrainTimer || settled) return;
308
+ finalDrainTimer = setTimeout(() => {
309
+ if (settled || promptSettled) return;
310
+ forcedTermination = true;
311
+ if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
312
+ error = `Subagent session did not settle within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Aborting it.`;
313
+ }
314
+ abortChild();
315
+ finalHardFinishTimer = setTimeout(() => {
316
+ if (settled || promptSettled) return;
317
+ settle(undefined, true);
318
+ }, HARD_FINISH_MS);
319
+ finalHardFinishTimer.unref?.();
320
+ }, FINAL_STOP_GRACE_MS);
321
+ finalDrainTimer.unref?.();
322
+ }
323
+ const applyChildLifecycle = (action: ChildLifecycleAction): void => {
324
+ if (action === "cancel-drain") {
325
+ clearFinalDrainTimers();
326
+ clearWatchdogTailTimer();
327
+ return;
328
+ }
329
+ if (action === "start-drain") startFinalDrain();
330
+ };
331
+
332
+ let toolTimeoutSequence = 0;
333
+ const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
334
+ const activeToolTimeoutKeysByName = new Map<string, string[]>();
335
+ const removeToolTimeoutKey = (key: string): void => {
336
+ const active = activeToolTimeouts.get(key);
337
+ if (!active) return;
338
+ clearTimeout(active.timer);
339
+ activeToolTimeouts.delete(key);
340
+ const keys = activeToolTimeoutKeysByName.get(active.toolName)?.filter((candidate) => candidate !== key) ?? [];
341
+ if (keys.length > 0) activeToolTimeoutKeysByName.set(active.toolName, keys);
342
+ else activeToolTimeoutKeysByName.delete(active.toolName);
343
+ };
344
+ const clearActiveToolTimeout = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
345
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
346
+ ? `id:${event.toolCallId}`
347
+ : typeof event.toolName === "string"
348
+ ? activeToolTimeoutKeysByName.get(event.toolName)?.[0]
349
+ : activeToolTimeouts.size === 1
350
+ ? [...activeToolTimeouts.keys()][0]
351
+ : undefined;
352
+ if (key) removeToolTimeoutKey(key);
353
+ };
354
+ const clearAllToolTimeouts = (): void => {
355
+ for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
356
+ };
357
+ const terminateForTimeout = (message: string): void => {
358
+ if (settled || promptSettled || timedOut || stopped) return;
359
+ timedOut = true;
360
+ interrupted = false;
361
+ error = message;
362
+ abortChild();
363
+ };
364
+ const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
365
+ const timeoutForTool = effectiveToolTimeoutMs(event.toolName, input.toolTimeoutMs);
366
+ if (timeoutForTool === undefined) return;
367
+ const runRemaining = input.runDeadlineAt === undefined ? undefined : Math.max(0, input.runDeadlineAt - Date.now());
368
+ if (runRemaining !== undefined && timeoutForTool >= runRemaining) return;
369
+ const key = toolTimeoutCallKey(event, ++toolTimeoutSequence);
370
+ const toolName = event.toolName;
371
+ const timer = setTimeout(() => {
372
+ removeToolTimeoutKey(key);
373
+ terminateForTimeout(formatToolTimeoutMessage(toolName, timeoutForTool));
374
+ }, timeoutForTool);
375
+ timer.unref?.();
376
+ activeToolTimeouts.set(key, { toolName, timer });
377
+ const keys = activeToolTimeoutKeysByName.get(toolName) ?? [];
378
+ keys.push(key);
379
+ activeToolTimeoutKeysByName.set(toolName, keys);
380
+ };
381
+
382
+ const processEvent = (raw: ChildSessionEvent): void => {
383
+ if (settled) return;
384
+ const event = raw as ChildSessionEvent & ChildEvent;
385
+ appendChildEvent(projectChildSessionEventForJson(raw) as Record<string, unknown>);
386
+ input.transcriptWriter?.writeChildEvent(projectChildSessionEventForJson(raw) as ChildEvent);
387
+ if (event.type === "compaction_start") compactionStartedReceived = true;
388
+ if (event.type === "compaction_end" && event.willRetry === true) {
389
+ compactionStartedReceived = false;
390
+ afterCompactionSettlement = false;
391
+ }
392
+ if (event.type === "agent_start" || event.type === "auto_retry_start") {
393
+ compactionStartedReceived = false;
394
+ afterCompactionSettlement = false;
395
+ }
396
+ const lifecycleAction = projectChildLifecycle(event, false, childLifecycleState);
397
+ if (event.type === "agent_settled" && lifecycleAction === "start-drain") {
398
+ agentSettledReceived = true;
399
+ afterCompactionSettlement = compactionStartedReceived;
400
+ }
401
+ applyChildLifecycle(lifecycleAction);
402
+
403
+ if (isChildWatchdogStatusEvent(event)) {
404
+ if (!input.childWatchdog) return;
405
+ const next = acceptChildWatchdogEvent({
406
+ current: childWatchdogState,
407
+ event,
408
+ ...(input.childEventContext ? {
409
+ runId: input.childEventContext.runId,
410
+ agent: input.childEventContext.agent,
411
+ childIndex: input.childEventContext.stepIndex,
412
+ } : {}),
413
+ });
414
+ if (!next) return;
415
+ childWatchdogState = next;
416
+ input.onChildEvent?.(event);
417
+ if (childWatchdogIsActive(next)) {
418
+ clearFinalDrainTimers();
419
+ armWatchdogTail();
420
+ } else {
421
+ clearWatchdogTailTimer();
422
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) startFinalDrain();
423
+ }
424
+ return;
425
+ }
426
+
427
+ input.onChildEvent?.(event);
428
+
429
+ if (event.type === "tool_execution_end") {
430
+ clearActiveToolTimeout(event);
431
+ removeActiveToolCall(event);
432
+ return;
433
+ }
434
+
435
+ if (event.type === "tool_execution_start" && event.toolName) {
436
+ toolCount += 1;
437
+ const toolArgs = event.args && typeof event.args === "object" && !Array.isArray(event.args) ? event.args : {};
438
+ armToolTimeout({ toolCallId: event.toolCallId, toolName: event.toolName });
439
+ recordActiveToolCall({ toolCallId: event.toolCallId, toolName: event.toolName, args: toolArgs });
440
+ if (event.toolName === "structured_output") {
441
+ structuredOutputToolInvoked = true;
442
+ structuredOutputMessageStartIndex = messages.length;
443
+ }
444
+ observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, toolArgs, input.mutationTools);
445
+ const preview = extractToolArgsPreview(toolArgs);
446
+ input.writeOutputLine(preview ? `${event.toolName}: ${preview}` : event.toolName);
447
+ return;
448
+ }
449
+
450
+ if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
451
+ if (event.type === "tool_result_end") {
452
+ clearActiveToolTimeout(event);
453
+ removeActiveToolCall({
454
+ toolCallId: (event.message as { toolCallId?: unknown }).toolCallId ?? event.toolCallId,
455
+ toolName: (event.message as { toolName?: unknown }).toolName ?? event.toolName,
456
+ });
457
+ }
458
+ messages.push(event.message);
459
+ const text = extractTextFromContent(event.message.content);
460
+ if (text) writeOutputText(text);
461
+
462
+ if (input.childWatchdog && event.type === "message_end") {
463
+ const next = applyChildWatchdogMessage(childWatchdogState, event.message);
464
+ if (next) childWatchdogState = next;
465
+ }
466
+ if (event.type !== "message_end" || event.message.role !== "assistant") return;
467
+ const hasToolCall = assistantStartsToolCall(event.message);
468
+ if (event.message.model) {
469
+ model = event.message.model;
470
+ if (input.expectedModelForVerification && !hasToolCall) {
471
+ const modelVerificationError = formatSubagentModelVerificationError(input.expectedModelForVerification, event.message.model, input.modelVerificationRegistry);
472
+ if (modelVerificationError && !error) error = modelVerificationError;
473
+ }
474
+ }
475
+ if (event.message.errorMessage) assistantError = event.message.errorMessage;
476
+ const eventUsage = event.message.usage;
477
+ if (eventUsage) {
478
+ usage.turns++;
479
+ usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
480
+ usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
481
+ usage.cacheRead += eventUsage.cacheRead ?? 0;
482
+ usage.cacheWrite += eventUsage.cacheWrite ?? 0;
483
+ usage.cost += eventUsage.cost?.total ?? 0;
484
+ }
485
+ if (isTerminalAssistantStop(event.message)) {
486
+ if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
487
+ cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
488
+ clearAllToolTimeouts();
489
+ activeToolCalls.clear();
490
+ activeToolKeysByName.clear();
491
+ refreshCurrentTool();
492
+ applyChildLifecycle(projectChildLifecycle(event, true, childLifecycleState));
493
+ }
494
+ }
495
+ };
496
+
497
+ /** Stops observing the child and returns when its extensions have shut down. */
498
+ const finish = (): Promise<void> => {
499
+ clearFinalDrainTimers();
500
+ clearWatchdogTailTimer();
501
+ clearAllToolTimeouts();
502
+ if (abortSettleTimer) {
503
+ clearTimeout(abortSettleTimer);
504
+ abortSettleTimer = undefined;
505
+ }
506
+ input.registerInterrupt?.(undefined);
507
+ input.registerTimeout?.(undefined);
508
+ input.registerStop?.(undefined);
509
+ input.registerSteer?.(undefined);
510
+ input.registerWatchdogStatus?.(undefined);
511
+ unsubscribe?.();
512
+ return Promise.resolve().then(() => session?.dispose()).catch(() => undefined);
513
+ };
514
+
515
+ /** The child run ended (or was forced to end); fold in the outcome once the child's shutdown work is done. */
516
+ const settle = (promptError: unknown, forced = false): void => {
517
+ if (settled) return;
518
+ settled = true;
519
+ const closed = finish();
520
+ const finalOutput = getFinalOutput(messages);
521
+ let finalError = error ?? assistantError;
522
+ if (!finalError && promptError !== undefined) {
523
+ finalError = promptError instanceof Error ? promptError.message : String(promptError);
524
+ }
525
+ const forcedDrainAfterFinalSuccess = (forced || forcedTermination) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
526
+ const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
527
+ if (!finalError && forced && !forcedDrainAfterFinalSuccess && !interrupted && !timedOut && !stopped) {
528
+ finalError = "Subagent session did not settle after it was aborted.";
529
+ }
530
+ const exitCode = timedOut || stopped
531
+ ? 1
532
+ : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal)
533
+ ? 0
534
+ : finalError || promptError !== undefined ? 1 : 0;
535
+ void closed.then(() => resolve(omitUndefined({
536
+ exitCode,
537
+ messages,
538
+ usage,
539
+ toolCount,
540
+ durationMs: Date.now() - startedAt,
541
+ model,
542
+ error: stopped ? stopMessage() : timedOut ? (error ?? timeoutMessage()) : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError,
543
+ finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage() : error ?? timeoutMessage()) : finalOutput,
544
+ outputState: finalOutput.trim() ? "present" : "absent",
545
+ interrupted: interrupted || undefined,
546
+ timedOut: timedOut || undefined,
547
+ stopped: stopped || undefined,
548
+ observedMutationAttempt,
549
+ structuredOutputToolInvoked,
550
+ structuredOutputMessageStartIndex,
551
+ watchdog: childWatchdogState,
552
+ sessionFile: session?.sessionFile,
553
+ currentTool,
554
+ currentToolArgs,
555
+ currentPath,
556
+ afterCompactionSettlement: afterCompactionSettlement || undefined,
557
+ })));
558
+ };
559
+
560
+ input.registerInterrupt?.(() => {
561
+ if (settled || promptSettled || timedOut || stopped) return;
562
+ interrupted = true;
563
+ if (!error) error = "Interrupted. Waiting for explicit next action.";
564
+ abortChild();
565
+ });
566
+ input.registerTimeout?.(() => terminateForTimeout(timeoutMessage()));
567
+ input.registerStop?.(() => {
568
+ if (settled || promptSettled || timedOut || stopped) return;
569
+ stopped = true;
570
+ interrupted = false;
571
+ error = stopMessage();
572
+ abortChild();
573
+ });
574
+
575
+ void (async () => {
576
+ try {
577
+ const created = await input.factory.create({
578
+ ...input.launch.session,
579
+ onExtensionError: (extensionError) => {
580
+ input.transcriptWriter?.writeStderrLine(`Extension error (${extensionError.extensionPath}, ${extensionError.event}): ${extensionError.error instanceof Error ? extensionError.error.message : String(extensionError.error)}`);
581
+ },
582
+ });
583
+ if (settled) {
584
+ void created.dispose();
585
+ return;
586
+ }
587
+ session = created;
588
+ unsubscribe = created.subscribe(processEvent);
589
+ input.registerWatchdogStatus?.((event) => processEvent(event as unknown as ChildSessionEvent));
590
+ input.registerSteer?.(async (request) => {
591
+ const text = formatSteerMessage(request);
592
+ const followUp = request.mode === "follow_up";
593
+ try {
594
+ if (followUp) await created.followUp(text);
595
+ else await created.steer(text);
596
+ } catch (steerError) {
597
+ return { state: "failed", message: steerError instanceof Error ? steerError.message : String(steerError) };
598
+ }
599
+ return followUp
600
+ ? { state: "queued", deliveryStatus: "queued", message: "Pi queued the follow-up input." }
601
+ : { state: "delivered", deliveryStatus: "delivered", message: "Pi accepted the steering input." };
602
+ });
603
+ if (interrupted || timedOut || stopped) abortChild();
604
+ await created.prompt(input.prompt);
605
+ promptSettled = true;
606
+ settle(undefined);
607
+ } catch (promptError) {
608
+ promptSettled = true;
609
+ settle(promptError ?? new Error("Child session failed."));
610
+ }
611
+ })();
612
+ });
613
+ }
@@ -545,7 +545,6 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
545
545
  const liveWorkflowControls = status.mode === "workflow" && deps.state?.currentSessionId === status.sessionId && deps.state?.workflowControllers?.has(status.runId)
546
546
  ? [...(deps.state?.foregroundControls.values() ?? [])].filter((control) => control.parentWorkflowRunId === status.runId
547
547
  && control.sessionId === status.sessionId
548
- && Boolean(control.workflowSteeringDir && fs.existsSync(control.workflowSteeringDir))
549
548
  && (control.activeChildren?.size ?? 0) > 0)
550
549
  : [];
551
550
  let hasExternalJobFollowUpHint = false;