pi-subagents 0.64.0 → 0.65.1

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 (106) hide show
  1. package/CHANGELOG.md +65 -0
  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 +19 -17
  6. package/docs/configuration.md +23 -15
  7. package/docs/extension-api.md +18 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +15 -8
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +13 -3
  13. package/package.json +5 -4
  14. package/runner-server-preload.mjs +13 -0
  15. package/skills/pi-subagents/SKILL.md +2 -1
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  17. package/skills/pi-subagents/references/execution-controls.md +7 -3
  18. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  19. package/src/agents/agent-management.ts +47 -15
  20. package/src/api/capability-ceiling.ts +0 -1
  21. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  22. package/src/api/preflight.ts +7 -4
  23. package/src/extension/config.ts +4 -2
  24. package/src/extension/doctor.ts +2 -10
  25. package/src/extension/fanout-child.ts +9 -11
  26. package/src/extension/index.ts +58 -7
  27. package/src/extension/public-execution.ts +14 -0
  28. package/src/extension/rpc.ts +3 -2
  29. package/src/extension/schemas.ts +3 -2
  30. package/src/extension/tool-description.ts +13 -8
  31. package/src/integrations/pi-web-session-liveness.ts +73 -0
  32. package/src/intercom/native-supervisor-channel.ts +159 -95
  33. package/src/intercom/supervisor-ui.ts +244 -0
  34. package/src/missions/workflow-state.ts +37 -16
  35. package/src/runs/background/async-execution.ts +51 -25
  36. package/src/runs/background/async-job-tracker.ts +11 -0
  37. package/src/runs/background/async-resume.ts +14 -3
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/control-channel.ts +2 -204
  40. package/src/runs/background/notify.ts +2 -0
  41. package/src/runs/background/process-terminal.ts +1 -1
  42. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  43. package/src/runs/background/run-child-session.ts +614 -0
  44. package/src/runs/background/run-status.ts +0 -1
  45. package/src/runs/background/runner-aliases.ts +152 -0
  46. package/src/runs/background/runner-child-sessions.ts +31 -0
  47. package/src/runs/background/scheduled-runs.ts +18 -4
  48. package/src/runs/background/subagent-runner.ts +203 -901
  49. package/src/runs/foreground/async-steering-action.ts +1 -17
  50. package/src/runs/foreground/execution.ts +204 -378
  51. package/src/runs/foreground/foreground-control.ts +4 -0
  52. package/src/runs/foreground/foreground-history.ts +3 -1
  53. package/src/runs/foreground/prompt-audit.ts +9 -5
  54. package/src/runs/foreground/subagent-executor.ts +161 -91
  55. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  56. package/src/runs/shared/abort-recovery.ts +3 -3
  57. package/src/runs/shared/acceptance.ts +14 -1
  58. package/src/runs/shared/capability-ceiling.ts +1 -2
  59. package/src/runs/shared/child-hooks.ts +25 -0
  60. package/src/runs/shared/child-identity.ts +13 -2
  61. package/src/runs/shared/child-launch.ts +314 -0
  62. package/src/runs/shared/child-lifecycle.ts +25 -0
  63. package/src/runs/shared/child-runtime-config.ts +126 -0
  64. package/src/runs/shared/child-session.ts +337 -0
  65. package/src/runs/shared/child-tool-plan.ts +530 -0
  66. package/src/runs/shared/claude-code-adapter.ts +5 -1
  67. package/src/runs/shared/completion-guard.ts +1 -1
  68. package/src/runs/shared/external-cli-preflight.ts +16 -0
  69. package/src/runs/shared/llm-intent-arbiter.ts +20 -11
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  71. package/src/runs/shared/model-exclusions.ts +84 -15
  72. package/src/runs/shared/model-fallback.ts +78 -6
  73. package/src/runs/shared/nested-events.ts +32 -48
  74. package/src/runs/shared/nested-path.ts +0 -14
  75. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  76. package/src/runs/shared/parallel-utils.ts +1 -2
  77. package/src/runs/shared/permissions.ts +0 -13
  78. package/src/runs/shared/process-signal.ts +4 -1
  79. package/src/runs/shared/run-fanout-budget.ts +0 -13
  80. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  81. package/src/runs/shared/structured-output.ts +17 -4
  82. package/src/runs/shared/subagent-control.ts +2 -0
  83. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  84. package/src/runs/shared/tool-availability.ts +18 -62
  85. package/src/runs/shared/tool-budget.ts +0 -14
  86. package/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  87. package/src/runs/shared/worktree.ts +192 -40
  88. package/src/shared/child-session-name.ts +1 -1
  89. package/src/shared/jsonl-writer.ts +11 -0
  90. package/src/shared/model-response-aliases.ts +13 -0
  91. package/src/shared/thinking-ceiling.ts +0 -6
  92. package/src/shared/types.ts +63 -28
  93. package/src/shared/utils.ts +6 -4
  94. package/src/shared/watch-strategy.ts +2 -0
  95. package/src/slash/slash-commands.ts +0 -6
  96. package/src/tui/fleet-status.ts +1 -1
  97. package/src/tui/fleet.ts +0 -1
  98. package/src/tui/render.ts +254 -41
  99. package/src/watchdog/child-status.ts +0 -1
  100. package/src/watchdog/register-child.ts +12 -12
  101. package/src/workflows/scripted-workflow.ts +80 -7
  102. package/src/workflows/workflow-checklist.ts +4 -3
  103. package/src/runs/shared/child-protocol.ts +0 -415
  104. package/src/runs/shared/pi-args.ts +0 -1059
  105. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  106. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -2,8 +2,7 @@
2
2
  * Core execution logic for running subagents
3
3
  */
4
4
 
5
- import { spawn } from "node:child_process";
6
- import { existsSync } from "node:fs";
5
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
7
6
  import * as path from "node:path";
8
7
  import type { Message } from "@earendil-works/pi-ai";
9
8
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig } from "../../agents/agents.ts";
@@ -31,7 +30,6 @@ import {
31
30
  type AcceptanceLedger,
32
31
  type ResolvedAcceptanceConfig,
33
32
  truncateOutput,
34
- getSubagentDepthEnv,
35
33
  } from "../../shared/types.ts";
36
34
  import {
37
35
  DEFAULT_CONTROL_CONFIG,
@@ -61,21 +59,18 @@ import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMuta
61
59
  import { planCompletionEvidence } from "../shared/completion-evidence.ts";
62
60
  import { planAbortRecovery } from "../shared/abort-recovery.ts";
63
61
  import { arbitrateCompletionGuardRescue } from "../shared/llm-intent-arbiter.ts";
64
- import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
65
62
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
66
63
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
67
64
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
68
- import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
69
65
  import { resolvePermissionRules } from "../shared/permissions.ts";
70
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
66
+ import { applyThinkingSuffix, deriveForkPromptCacheKey } from "../shared/child-tool-plan.ts";
71
67
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
72
- import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
73
- import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
68
+ import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
74
69
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
75
- import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
76
- import { clearStructuredOutputCaptures, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
77
- import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
78
- import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
70
+ import { assertThinkingWithinCeiling, intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
71
+ import { MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR } from "../shared/structured-output.ts";
72
+ import { formatMidToolExitError, isOrdinaryToolForMidToolExit } from "../shared/process-signal.ts";
73
+ import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
79
74
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
80
75
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, hasSingleOutputChangedSinceSnapshot, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
81
76
  import {
@@ -86,14 +81,6 @@ import {
86
81
  isRetryableModelFailureAttempt,
87
82
  recordRetryableModelFailure,
88
83
  } from "../shared/model-fallback.ts";
89
- import {
90
- SUBAGENT_STARTUP_RETRY_DELAYS_MS,
91
- formatSubagentExtensionConflictError,
92
- formatSubagentStartupRetryExhaustedError,
93
- formatSubagentStartupRetryNote,
94
- isRetryableSubagentStartupFailure,
95
- waitForSubagentStartupRetry,
96
- } from "../shared/subagent-startup-retry.ts";
97
84
  import {
98
85
  createMutatingFailureState,
99
86
  didMutatingToolFail,
@@ -112,7 +99,7 @@ import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.t
112
99
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
113
100
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
114
101
  import { consumeWorkflowChildPermit } from "../../shared/workflow-child-permit.ts";
115
- import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
102
+ import { projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState } from "../shared/child-lifecycle.ts";
116
103
  import {
117
104
  acceptChildWatchdogEvent,
118
105
  applyChildWatchdogMessage,
@@ -120,7 +107,10 @@ import {
120
107
  isChildWatchdogStatusEvent,
121
108
  resolveChildWatchdogConfig,
122
109
  type ChildWatchdogStateSnapshot,
110
+ type ChildWatchdogStatusEvent,
123
111
  } from "../../watchdog/child-status.ts";
112
+ import { buildInProcessChildLaunch } from "../shared/child-launch.ts";
113
+ import { childSessionFactory, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent } from "../shared/child-session.ts";
124
114
 
125
115
  const artifactOutputByResult = new WeakMap<SingleResult, string>();
126
116
  const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
@@ -288,8 +278,8 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
288
278
  /**
289
279
  * Streaming variant of snapshotResult for `onUpdate` progress events: it drops the
290
280
  * unbounded `messages` transcript in favour of compact tool-call summaries so a
291
- * single `tool_execution_update` line stays well under the child-stdout protocol
292
- * cap (`MAX_CHILD_PENDING_LINE_BYTES`). Non-streaming consumers such as
281
+ * single `tool_execution_update` event stays small; the parent records every
282
+ * one in its transcript and `events.jsonl`. Non-streaming consumers such as
293
283
  * foreground detach receipts and terminal consumers use snapshotResult directly.
294
284
  */
295
285
  function snapshotStreamResult(result: SingleResult, progress: AgentProgress): SingleResult {
@@ -351,6 +341,9 @@ function structuredDelegationProgressChanged(
351
341
  const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
352
342
  type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
353
343
 
344
+ const STOPPED_BEFORE_COMPLETION_ERROR = "Subagent stopped before completion.";
345
+
346
+
354
347
  async function runSingleAttempt(
355
348
  runtimeCwd: string,
356
349
  agent: AgentConfig,
@@ -369,7 +362,6 @@ async function runSingleAttempt(
369
362
  attemptNotes: string[];
370
363
  outputSnapshot?: SingleOutputSnapshot;
371
364
  originalTask?: string;
372
- taskDelivery?: SubagentTaskDelivery;
373
365
  orcaProgressTab?: OrcaProgressTab;
374
366
  launchWarnings: { emitted: boolean };
375
367
  verifyModel: boolean;
@@ -380,9 +372,9 @@ async function runSingleAttempt(
380
372
  assertThinkingWithinCeiling({ model: modelArg, configThinking: effectiveThinking, ceiling: options.thinkingCeiling, agent: agent.name, runId: options.runId });
381
373
  const expectedModelForVerification = shared.verifyModel ? modelArg : undefined;
382
374
  const resolvedThinking = resolveEffectiveThinking(modelArg, effectiveThinking);
383
- // Display name for the child session: applied inside the child via
384
- // PI_SUBAGENT_SESSION_NAME and echoed back on the result payload so hosts
385
- // can label this run without reading the child's session file.
375
+ // Display name for the child session: applied inside the child through its
376
+ // runtime config and echoed back on the result payload so hosts can label
377
+ // this run without reading the child's session file.
386
378
  const childSessionName = deriveChildSessionName({ agent: agent.name, task: shared.originalTask ?? task });
387
379
  const watchdogConfig = resolveWatchdogConfig(options.cwd ?? runtimeCwd);
388
380
  const childWatchdog = watchdogConfig.ok
@@ -397,15 +389,12 @@ async function runSingleAttempt(
397
389
  const permissionAuditPath = permissionRules && options.artifactsDir
398
390
  ? path.join(options.artifactsDir, "permission-audit", `${options.runId}-${options.index ?? 0}.jsonl`)
399
391
  : undefined;
400
- const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit, warnings } = buildPiArgs({
401
- baseArgs: ["--mode", "json", "-p"],
402
- task,
403
- taskDelivery: shared.taskDelivery,
392
+ let onWatchdogStatus: ((event: ChildWatchdogStatusEvent) => void) | undefined;
393
+ const launch = buildInProcessChildLaunch({
404
394
  sessionEnabled: shared.sessionEnabled,
405
395
  sessionDir: options.sessionDir,
406
396
  sessionFile: options.sessionFile,
407
397
  model: modelArg,
408
- thinking: effectiveThinking,
409
398
  systemPromptMode: agent.systemPromptMode,
410
399
  inheritProjectContext: agent.inheritProjectContext,
411
400
  inheritGlobalContext: agent.inheritGlobalContext,
@@ -419,63 +408,40 @@ async function runSingleAttempt(
419
408
  systemPrompt: shared.systemPrompt,
420
409
  mcpDirectTools: agent.mcpDirectTools,
421
410
  cwd: options.cwd ?? runtimeCwd,
422
- promptFileStem: agent.name,
423
411
  intercomSessionName: options.intercomSessionName,
424
412
  sessionName: childSessionName,
425
413
  orchestratorIntercomTarget: options.orchestratorIntercomTarget,
426
414
  runId: options.runId,
427
415
  childAgentName: agent.name,
428
416
  childIndex: options.index ?? 0,
429
- parentEventSink: options.nestedRoute?.eventSink,
430
- parentControlInbox: options.nestedRoute?.controlInbox,
431
- parentRootRunId: options.nestedRoute?.rootRunId,
432
- parentCapabilityToken: options.nestedRoute?.capabilityToken,
417
+ nestedRoute: options.nestedRoute,
433
418
  runFanoutBudget: options.runFanoutBudget,
434
419
  parentSessionId: options.parentSessionId,
435
420
  forkCacheKey: options.context === "fork" ? deriveForkPromptCacheKey(options.parentSessionId) : undefined,
436
- steerInboxDir: options.steerInboxDir,
437
- steerCapabilityPath: options.steerCapabilityPath,
438
- steerAckDir: options.steerAckDir,
439
421
  structuredOutput: options.structuredOutput,
440
422
  fast: options.fast ?? agent.fast,
441
423
  modelCandidates: shared.modelCandidates,
442
424
  toolBudget: options.toolBudget,
443
- allowZeroToolBudget: options.allowZeroToolBudget,
444
425
  permissionRules,
445
426
  permissionAuditPath,
446
427
  childWatchdog,
428
+ watchdogStatus: (event) => onWatchdogStatus?.(event),
447
429
  waitToolEnabled: options.waitToolEnabled,
448
430
  waitToolDefaultTimeoutMs: options.waitToolDefaultTimeoutMs,
449
431
  capabilityCeiling: options.capabilityCeiling,
450
432
  thinkingCeiling: options.thinkingCeiling,
451
- extensionBindings: options.extensionBindings,
433
+ maxSubagentDepth: options.maxSubagentDepth,
452
434
  runtimeSnapshotHost: options.runtimeSnapshotHost,
435
+ inherited: options.childRuntime,
436
+ host: "parent",
453
437
  });
438
+ const { toolPlan, capabilityAudit, warnings, launchResolvedExtensions, capture } = launch;
454
439
  if (!shared.launchWarnings.emitted && warnings.length > 0) {
455
440
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
456
441
  shared.launchWarnings.emitted = true;
457
442
  }
458
443
 
459
444
  const effectiveSystemPrompt = shared.systemPrompt;
460
- const toolPlan = resolvePiLaunchToolPlan({
461
- tools: agent.tools,
462
- allowNestedSubagents: agent.allowNestedSubagents,
463
- extensions: agent.extensions,
464
- subagentOnlyExtensions: agent.subagentOnlyExtensions,
465
- mcpDirectTools: agent.mcpDirectTools,
466
- cwd: options.cwd ?? runtimeCwd,
467
- requireReadTool: Boolean(shared.resolvedSkillNames?.length),
468
- structuredOutput: Boolean(options.structuredOutput),
469
- excludeTools: agent.excludeTools,
470
- fast: options.fast ?? agent.fast,
471
- model: modelArg,
472
- modelCandidates: shared.modelCandidates,
473
- capabilityCeiling: options.capabilityCeiling,
474
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
475
- agentName: agent.name,
476
- permissionRules,
477
- runtimeSnapshotHost: options.runtimeSnapshotHost,
478
- });
479
445
  const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
480
446
  const contractError = validateImplementationToolContract({
481
447
  agent: agent.name,
@@ -488,7 +454,6 @@ async function runSingleAttempt(
488
454
  completionGuard: agent.completionGuard,
489
455
  });
490
456
  if (contractError) {
491
- cleanupTempDir(tempDir);
492
457
  return {
493
458
  index: options.index ?? 0,
494
459
  agent: agent.name,
@@ -507,7 +472,6 @@ async function runSingleAttempt(
507
472
  ...(toolPlan.capabilityAudit ? { capabilityAudit: toolPlan.capabilityAudit } : {}),
508
473
  };
509
474
  }
510
- const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
511
475
  const launchContractDigest = launchBindingDigest({
512
476
  definitionDigest: agentDefinitionDigest(agent),
513
477
  task: shared.originalTask ?? task,
@@ -554,17 +518,6 @@ async function runSingleAttempt(
554
518
  ...(capabilityAudit ? { capabilityAudit } : {}),
555
519
  }, options.context);
556
520
  const startTime = Date.now();
557
- if (options.structuredOutput) {
558
- const cleanupError = clearStructuredOutputCaptures(options.structuredOutput);
559
- if (cleanupError) {
560
- cleanupTempDir(tempDir);
561
- result.exitCode = 1;
562
- result.error = cleanupError;
563
- result.finalOutput = cleanupError;
564
- result.progressSummary = { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime };
565
- return result;
566
- }
567
- }
568
521
  const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
569
522
  let interruptedByControl = false;
570
523
  const allControlEvents: ControlEvent[] = [];
@@ -599,7 +552,6 @@ async function runSingleAttempt(
599
552
  result.progress = progress;
600
553
  const attemptTimeout = resolveAttemptTimeout(options);
601
554
  if (attemptTimeout?.remainingMs === 0) {
602
- cleanupTempDir(tempDir);
603
555
  result.exitCode = 1;
604
556
  result.timedOut = true;
605
557
  result.error = attemptTimeout.message;
@@ -613,7 +565,6 @@ async function runSingleAttempt(
613
565
  };
614
566
  return result;
615
567
  }
616
- const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
617
568
  const mutationSnapshot = snapshotTrackedMutations(options.cwd ?? runtimeCwd);
618
569
  let observedMutationAttempt = false;
619
570
  let structuredOutputToolInvoked = false;
@@ -621,7 +572,6 @@ async function runSingleAttempt(
621
572
  let toolAvailabilityError: string | undefined;
622
573
  let abortedBySignal = options.signal?.aborted === true;
623
574
 
624
- const spawnSpec = getPiSpawnCommand(args);
625
575
  if (options.workflowChildPermitLaunch) {
626
576
  const permitError = consumeWorkflowChildPermit(options.workflowChildPermitLaunch.permit, {
627
577
  workflowRunId: options.workflowChildPermitLaunch.workflowRunId,
@@ -632,7 +582,6 @@ async function runSingleAttempt(
632
582
  runner: "pi",
633
583
  });
634
584
  if (permitError) {
635
- cleanupTempDir(tempDir);
636
585
  result.exitCode = 1;
637
586
  result.error = permitError;
638
587
  result.finalOutput = permitError;
@@ -641,16 +590,13 @@ async function runSingleAttempt(
641
590
  return result;
642
591
  }
643
592
  }
593
+ const childSessions = options.childSessionFactory ?? childSessionFactory();
644
594
  let afterCompactionSettlement = false;
645
595
  const exitCode = await new Promise<number>((resolve) => {
646
- const proc = spawn(spawnSpec.command, spawnSpec.args, {
647
- cwd: options.cwd ?? runtimeCwd,
648
- env: spawnEnv,
649
- stdio: ["ignore", "pipe", "pipe"],
650
- windowsHide: true,
651
- });
652
- const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
653
- let processClosed = false;
596
+ const jsonlWriter = createJsonlWriter(shared.jsonlPath, { pause() {}, resume() {} });
597
+ let session: ChildSession | undefined;
598
+ let unsubscribe: (() => void) | undefined;
599
+ let sessionSettled = false;
654
600
  let lifecycleFinished = false;
655
601
  let detached = false;
656
602
  let intercomStarted = false;
@@ -659,26 +605,26 @@ async function runSingleAttempt(
659
605
  let removeInterruptListener: (() => void) | undefined;
660
606
  let activityTimer: NodeJS.Timeout | undefined;
661
607
  let timeoutTimer: NodeJS.Timeout | undefined;
662
- let timeoutTerminationTimer: NodeJS.Timeout | undefined;
663
- let timeoutHardKillTimer: NodeJS.Timeout | undefined;
664
- let protocolHardKillTimer: NodeJS.Timeout | undefined;
608
+ let timeoutHardFinishTimer: NodeJS.Timeout | undefined;
665
609
  const clearTimeoutTimers = () => {
666
610
  if (timeoutTimer) {
667
611
  clearTimeout(timeoutTimer);
668
612
  timeoutTimer = undefined;
669
613
  }
670
- if (timeoutTerminationTimer) {
671
- clearTimeout(timeoutTerminationTimer);
672
- timeoutTerminationTimer = undefined;
673
- }
674
- if (timeoutHardKillTimer) {
675
- clearTimeout(timeoutHardKillTimer);
676
- timeoutHardKillTimer = undefined;
614
+ if (timeoutHardFinishTimer) {
615
+ clearTimeout(timeoutHardFinishTimer);
616
+ timeoutHardFinishTimer = undefined;
677
617
  }
678
618
  };
619
+ const abortChild = (): void => {
620
+ if (!session || sessionSettled || lifecycleFinished) return;
621
+ void session.abort().catch(() => {
622
+ // The session settles through its prompt promise; abort failures are not separately actionable.
623
+ });
624
+ };
679
625
 
680
626
  const detachForeground = (reason: string): boolean => {
681
- if (detached || processClosed || lifecycleFinished || options.signal?.aborted) return false;
627
+ if (detached || sessionSettled || lifecycleFinished || options.signal?.aborted) return false;
682
628
  const receiptProgress = snapshotProgress(progress);
683
629
  receiptProgress.status = "detached";
684
630
  receiptProgress.durationMs = Date.now() - startTime;
@@ -713,20 +659,21 @@ async function runSingleAttempt(
713
659
  }
714
660
  if (!accepted) return false;
715
661
  detached = true;
662
+ if (session) session.detached = true;
716
663
  return true;
717
664
  };
718
665
 
719
- // If the child emits a terminal assistant stop but never exits,
720
- // give it a short grace period to flush naturally, then clean it up.
666
+ // If the child emits a terminal assistant stop but its run never settles
667
+ // (a hook is stuck), abort it after a short grace period and then finish
668
+ // without it.
721
669
  const FINAL_STOP_GRACE_MS = 1000;
722
- const HARD_KILL_MS = 3000;
723
- let childExited = false;
724
- let forcedTerminationSignal = false;
670
+ const HARD_FINISH_MS = 3000;
671
+ let forcedTermination = false;
725
672
  let cleanTerminalAssistantStopReceived = false;
726
673
  let agentSettledReceived = false;
727
674
  let compactionStartedReceived = false;
728
675
  let finalDrainTimer: NodeJS.Timeout | undefined;
729
- let finalHardKillTimer: NodeJS.Timeout | undefined;
676
+ let finalHardFinishTimer: NodeJS.Timeout | undefined;
730
677
  let watchdogTailTimer: NodeJS.Timeout | undefined;
731
678
  let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
732
679
  const updateChildWatchdogState = (snapshot: ChildWatchdogStateSnapshot): void => {
@@ -745,9 +692,9 @@ async function runSingleAttempt(
745
692
  clearTimeout(finalDrainTimer);
746
693
  finalDrainTimer = undefined;
747
694
  }
748
- if (finalHardKillTimer) {
749
- clearTimeout(finalHardKillTimer);
750
- finalHardKillTimer = undefined;
695
+ if (finalHardFinishTimer) {
696
+ clearTimeout(finalHardFinishTimer);
697
+ finalHardFinishTimer = undefined;
751
698
  }
752
699
  };
753
700
  const startFinalDrain = () => {
@@ -755,25 +702,24 @@ async function runSingleAttempt(
755
702
  armWatchdogTail();
756
703
  return;
757
704
  }
758
- if (childExited || finalDrainTimer || lifecycleFinished || processClosed) return;
705
+ if (sessionSettled || finalDrainTimer || lifecycleFinished) return;
759
706
  finalDrainTimer = setTimeout(() => {
760
- if (lifecycleFinished || processClosed) return;
761
- const termSent = trySignalChild(proc, "SIGTERM");
762
- if (!termSent) return;
763
- forcedTerminationSignal = true;
707
+ if (lifecycleFinished || sessionSettled) return;
708
+ forcedTermination = true;
764
709
  if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !assistantError) {
765
- result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
710
+ result.error = result.error ?? `Subagent session did not settle within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Aborting it.`;
766
711
  }
767
- finalHardKillTimer = setTimeout(() => {
768
- if (lifecycleFinished || processClosed) return;
769
- forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
770
- }, HARD_KILL_MS);
771
- finalHardKillTimer.unref?.();
712
+ abortChild();
713
+ finalHardFinishTimer = setTimeout(() => {
714
+ if (lifecycleFinished || sessionSettled) return;
715
+ settle(undefined, true);
716
+ }, HARD_FINISH_MS);
717
+ finalHardFinishTimer.unref?.();
772
718
  }, FINAL_STOP_GRACE_MS);
773
719
  finalDrainTimer.unref?.();
774
720
  };
775
721
  function armWatchdogTail(): void {
776
- if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished || processClosed) return;
722
+ if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished || sessionSettled) return;
777
723
  watchdogTailTimer = setTimeout(() => {
778
724
  watchdogTailTimer = undefined;
779
725
  updateChildWatchdogState({
@@ -799,7 +745,7 @@ async function runSingleAttempt(
799
745
  const childLifecycleState: ChildLifecycleState = { compactionRetryActive: false };
800
746
 
801
747
  const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
802
- if (!options.allowIntercomDetach || processClosed) return;
748
+ if (!options.allowIntercomDetach || sessionSettled) return;
803
749
  if (!payload || typeof payload !== "object") return;
804
750
  const event = payload as { requestId?: unknown; runId?: unknown; agent?: unknown; childIndex?: unknown };
805
751
  const requestId = event.requestId;
@@ -819,13 +765,8 @@ async function runSingleAttempt(
819
765
  lifecycleFinished = true;
820
766
  clearFinalDrainTimers();
821
767
  clearWatchdogTailTimer();
822
- clearStdioGuard();
823
768
  clearTimeoutTimers();
824
769
  clearAllToolTimeouts();
825
- if (protocolHardKillTimer) {
826
- clearTimeout(protocolHardKillTimer);
827
- protocolHardKillTimer = undefined;
828
- }
829
770
  if (activityTimer) {
830
771
  clearInterval(activityTimer);
831
772
  activityTimer = undefined;
@@ -833,7 +774,13 @@ async function runSingleAttempt(
833
774
  unsubscribeIntercomDetach?.();
834
775
  removeAbortListener?.();
835
776
  removeInterruptListener?.();
836
- resolve(code);
777
+ unsubscribe?.();
778
+ onWatchdogStatus = undefined;
779
+ void jsonlWriter.close().catch(() => {
780
+ // JSONL artifact flush is best effort.
781
+ });
782
+ // Report the run only after the child's extensions have shut down.
783
+ void Promise.resolve().then(() => session?.dispose()).catch(() => undefined).then(() => resolve(code));
837
784
  };
838
785
 
839
786
  const drainPendingControlEvents = (): ControlEvent[] | undefined => {
@@ -996,7 +943,7 @@ async function runSingleAttempt(
996
943
 
997
944
  let lastStructuredDelegationProgressState: StructuredDelegationProgressState | undefined;
998
945
  const emitUpdateSnapshot = (text: string) => {
999
- if (!options.onUpdate || processClosed) return;
946
+ if (!options.onUpdate || sessionSettled) return;
1000
947
  const progressSnapshot = snapshotProgress(progress);
1001
948
  const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
1002
949
  const controlEvents = drainPendingControlEvents();
@@ -1012,7 +959,7 @@ async function runSingleAttempt(
1012
959
  };
1013
960
 
1014
961
  const fireUpdate = () => {
1015
- if (!options.onUpdate || processClosed) return;
962
+ if (!options.onUpdate || sessionSettled) return;
1016
963
  progress.durationMs = Date.now() - startTime;
1017
964
  if (options.suppressUnchangedDelegationUpdates) {
1018
965
  if (lastStructuredDelegationProgressState && !structuredDelegationProgressChanged(lastStructuredDelegationProgressState, progress, result)) return;
@@ -1022,20 +969,9 @@ async function runSingleAttempt(
1022
969
  emitUpdateSnapshot(output || "(running...)");
1023
970
  };
1024
971
 
1025
- const rawStdoutTail = createBoundedByteTail();
1026
- const processLine = (line: string) => {
1027
- if (!line.trim()) return;
1028
- jsonlWriter.writeLine(line);
1029
- let evt: { type?: string; message?: Message; toolName?: string; toolCallId?: string; args?: unknown; willRetry?: unknown };
1030
- try {
1031
- evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; toolCallId?: string; args?: unknown; willRetry?: unknown };
1032
- } catch {
1033
- rawStdoutTail.push(`${line}\n`);
1034
- shared.transcriptWriter?.writeStdoutLine(line);
1035
- shared.orcaProgressTab?.append(`${line}\n`);
1036
- // Non-JSON stdout lines are expected; only structured events are parsed.
1037
- return;
1038
- }
972
+ const processEvent = (evt: ChildSessionEvent & { message?: Message; toolName?: string; toolCallId?: string; args?: unknown; willRetry?: unknown }) => {
973
+ if (lifecycleFinished) return;
974
+ jsonlWriter.writeLine(JSON.stringify(projectChildSessionEventForJson(evt)));
1039
975
  shared.transcriptWriter?.writeChildEvent(evt);
1040
976
  shared.orcaProgressTab?.event(evt);
1041
977
  if (evt.type === "compaction_start") compactionStartedReceived = true;
@@ -1047,6 +983,20 @@ async function runSingleAttempt(
1047
983
  compactionStartedReceived = false;
1048
984
  afterCompactionSettlement = false;
1049
985
  }
986
+ if (evt.type === "agent_start") {
987
+ const diagnostic = capture.toolDiagnostic();
988
+ if (diagnostic) {
989
+ const message = formatChildToolDiagnostic(diagnostic, { host: "parent" });
990
+ toolAvailabilityError = message;
991
+ result.error = message;
992
+ result.finalOutput = message;
993
+ progress.status = "failed";
994
+ progress.error = message;
995
+ fireUpdate();
996
+ abortChild();
997
+ return;
998
+ }
999
+ }
1050
1000
  const lifecycleAction = projectChildLifecycle(evt, false, childLifecycleState);
1051
1001
  if (evt.type === "agent_settled" && lifecycleAction === "start-drain") {
1052
1002
  agentSettledReceived = true;
@@ -1150,14 +1100,14 @@ async function runSingleAttempt(
1150
1100
  progress.model = evt.message.model;
1151
1101
  if (!result.model) result.model = evt.message.model;
1152
1102
  if (expectedModelForVerification && !hasToolCall) {
1153
- const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, evt.message.model, options.availableModels);
1103
+ const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, evt.message.model, options.availableModels, options.modelResponseAliases);
1154
1104
  if (modelVerificationError && !result.error) result.error = modelVerificationError;
1155
1105
  }
1156
1106
  }
1157
1107
  if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
1158
1108
  const assistantText = extractTextFromContent(evt.message.content);
1159
1109
  appendRecentOutput(progress, assistantText.split("\n").slice(-10));
1160
- // Final assistant message: start the exit drain window.
1110
+ // Final assistant message: start the settle drain window.
1161
1111
  if (terminalAssistantStop) {
1162
1112
  if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
1163
1113
  cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
@@ -1219,11 +1169,12 @@ async function runSingleAttempt(
1219
1169
  fireUpdate();
1220
1170
  }
1221
1171
  };
1172
+ onWatchdogStatus = (event) => processEvent(event as unknown as Parameters<typeof processEvent>[0]);
1222
1173
 
1223
1174
  fireUpdate();
1224
1175
  if (controlConfig.enabled || options.onUpdate) {
1225
1176
  activityTimer = setInterval(() => {
1226
- if (processClosed || lifecycleFinished) {
1177
+ if (sessionSettled || lifecycleFinished) {
1227
1178
  return;
1228
1179
  }
1229
1180
  updateActivityState(Date.now());
@@ -1234,7 +1185,7 @@ async function runSingleAttempt(
1234
1185
 
1235
1186
  if (attemptTimeout) {
1236
1187
  timeoutTimer = setTimeout(() => {
1237
- if (processClosed || lifecycleFinished || interruptedByControl) return;
1188
+ if (sessionSettled || lifecycleFinished || interruptedByControl) return;
1238
1189
  result.timedOut = true;
1239
1190
  clearAllToolTimeouts();
1240
1191
  result.error = attemptTimeout.message;
@@ -1243,17 +1194,12 @@ async function runSingleAttempt(
1243
1194
  progress.error = attemptTimeout.message;
1244
1195
  progress.durationMs = Date.now() - startTime;
1245
1196
  fireUpdate();
1246
- trySignalChild(proc, "SIGINT");
1247
- timeoutTerminationTimer = setTimeout(() => {
1248
- if (processClosed || lifecycleFinished) return;
1249
- trySignalChild(proc, "SIGTERM");
1250
- }, 1000);
1251
- timeoutTerminationTimer.unref?.();
1252
- timeoutHardKillTimer = setTimeout(() => {
1253
- if (processClosed || lifecycleFinished) return;
1254
- trySignalChild(proc, "SIGKILL");
1197
+ abortChild();
1198
+ timeoutHardFinishTimer = setTimeout(() => {
1199
+ if (sessionSettled || lifecycleFinished) return;
1200
+ settle(undefined, true);
1255
1201
  }, 4000);
1256
- timeoutHardKillTimer.unref?.();
1202
+ timeoutHardFinishTimer.unref?.();
1257
1203
  }, attemptTimeout.remainingMs);
1258
1204
  timeoutTimer.unref?.();
1259
1205
  }
@@ -1261,8 +1207,7 @@ async function runSingleAttempt(
1261
1207
  let toolTimeoutSequence = 0;
1262
1208
  const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
1263
1209
  const activeToolTimeoutKeysByName = new Map<string, string[]>();
1264
- let toolTimeoutTerminationTimer: ReturnType<typeof setTimeout> | undefined;
1265
- let toolTimeoutHardKillTimer: ReturnType<typeof setTimeout> | undefined;
1210
+ let toolTimeoutHardFinishTimer: ReturnType<typeof setTimeout> | undefined;
1266
1211
  const removeToolTimeoutKey = (key: string): void => {
1267
1212
  const active = activeToolTimeouts.get(key);
1268
1213
  if (!active) return;
@@ -1284,17 +1229,13 @@ async function runSingleAttempt(
1284
1229
  };
1285
1230
  const clearAllToolTimeouts = (): void => {
1286
1231
  for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
1287
- if (toolTimeoutTerminationTimer) {
1288
- clearTimeout(toolTimeoutTerminationTimer);
1289
- toolTimeoutTerminationTimer = undefined;
1290
- }
1291
- if (toolTimeoutHardKillTimer) {
1292
- clearTimeout(toolTimeoutHardKillTimer);
1293
- toolTimeoutHardKillTimer = undefined;
1232
+ if (toolTimeoutHardFinishTimer) {
1233
+ clearTimeout(toolTimeoutHardFinishTimer);
1234
+ toolTimeoutHardFinishTimer = undefined;
1294
1235
  }
1295
1236
  };
1296
1237
  const terminateForToolTimeout = (message: string): void => {
1297
- if (processClosed || lifecycleFinished || interruptedByControl) return;
1238
+ if (sessionSettled || lifecycleFinished || interruptedByControl) return;
1298
1239
  result.timedOut = true;
1299
1240
  result.error = message;
1300
1241
  result.finalOutput = message;
@@ -1302,17 +1243,12 @@ async function runSingleAttempt(
1302
1243
  progress.error = message;
1303
1244
  progress.durationMs = Date.now() - startTime;
1304
1245
  fireUpdate();
1305
- trySignalChild(proc, "SIGINT");
1306
- toolTimeoutTerminationTimer = setTimeout(() => {
1307
- if (processClosed || lifecycleFinished) return;
1308
- trySignalChild(proc, "SIGTERM");
1309
- }, 1000);
1310
- toolTimeoutTerminationTimer.unref?.();
1311
- toolTimeoutHardKillTimer = setTimeout(() => {
1312
- if (processClosed || lifecycleFinished) return;
1313
- trySignalChild(proc, "SIGKILL");
1246
+ abortChild();
1247
+ toolTimeoutHardFinishTimer = setTimeout(() => {
1248
+ if (sessionSettled || lifecycleFinished) return;
1249
+ settle(undefined, true);
1314
1250
  }, 4000);
1315
- toolTimeoutHardKillTimer.unref?.();
1251
+ toolTimeoutHardFinishTimer.unref?.();
1316
1252
  };
1317
1253
  const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
1318
1254
  const timeoutForTool = effectiveToolTimeoutMs(event.toolName, options.toolTimeoutMs);
@@ -1333,111 +1269,42 @@ async function runSingleAttempt(
1333
1269
  activeToolTimeoutKeysByName.set(toolName, keys);
1334
1270
  };
1335
1271
 
1336
- const stderrTail = createBoundedByteTail();
1337
- const failProtocol = (limit: ProtocolOutputLimit): void => {
1338
- if (result.protocolError) return;
1339
- result.protocolError = limit;
1340
- result.error = formatProtocolOutputLimit(limit);
1341
- progress.status = "failed";
1342
- progress.error = result.error;
1343
- progress.durationMs = Date.now() - startTime;
1344
- fireUpdate();
1345
- if (!childExited) {
1346
- trySignalChild(proc, "SIGTERM");
1347
- protocolHardKillTimer = setTimeout(() => {
1348
- if (!childExited) trySignalChild(proc, "SIGKILL");
1349
- }, 3000);
1350
- protocolHardKillTimer.unref?.();
1351
- }
1352
- };
1353
- const stdoutReader = createBoundedLineReader({
1354
- oversizedLineProjector: PI_AGGREGATE_EVENT_PROJECTOR,
1355
- onLine: processLine,
1356
- onLimit: failProtocol,
1357
- });
1358
- const stderrReader = createBoundedLineReader({
1359
- stream: "stderr",
1360
- maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
1361
- onLine: (line) => shared.transcriptWriter?.writeStderrLine(line),
1362
- onLimit: (limit) => shared.transcriptWriter?.writeStderrLine(formatProtocolOutputLimit(limit)),
1363
- });
1364
-
1365
- const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
1366
- proc.stdout.on("data", (chunk: Buffer) => stdoutReader.push(chunk));
1367
- proc.stderr.on("data", (chunk: Buffer) => {
1368
- stderrTail.push(chunk);
1369
- stderrReader.push(chunk);
1370
- shared.orcaProgressTab?.append(chunk.toString("utf-8"));
1371
- });
1372
- proc.on("exit", () => {
1373
- childExited = true;
1374
- clearFinalDrainTimers();
1375
- });
1376
- proc.on("close", (code, signal) => {
1377
- if (lifecycleFinished) return;
1378
- processClosed = true;
1272
+ /** The child run ended (or was forced to end); fold in the captures and finish. */
1273
+ const settle = (promptError: unknown, forced = false): void => {
1274
+ if (lifecycleFinished || sessionSettled) return;
1275
+ sessionSettled = true;
1379
1276
  clearFinalDrainTimers();
1380
- clearStdioGuard();
1381
- void jsonlWriter.close().catch(() => {
1382
- // JSONL artifact flush is best effort.
1383
- });
1384
- const toolDiagnosticError = readChildToolDiagnosticError(toolDiagnosticPath);
1277
+ const diagnostic = capture.toolDiagnostic();
1278
+ const toolDiagnosticError = diagnostic ? formatChildToolDiagnostic(diagnostic, { host: "parent" }) : undefined;
1385
1279
  toolAvailabilityError = toolDiagnosticError;
1386
- result.runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
1387
- cleanupTempDir(tempDir);
1388
- stdoutReader.end();
1389
- stderrReader.end();
1390
- const stderr = stderrTail.text();
1391
- const rawStdout = rawStdoutTail.text();
1280
+ result.runtimeAcknowledgedExtensions = capture.runtimeAcknowledgedExtensions();
1392
1281
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
1393
- const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1394
- const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(result.messages ?? []);
1395
- if (signal) result.processSignal = signal;
1396
- if (!closeError && forcedDrainAfterEmptyTerminal && stderr.trim()) {
1397
- closeError = stderr.trim();
1398
- }
1399
- if (!closeError && isUnexplainedProcessSignal({
1400
- processSignal: signal,
1401
- interrupted: result.interrupted,
1402
- timedOut: result.timedOut,
1403
- stopped: result.stopped,
1404
- forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1405
- })) {
1406
- closeError = formatProcessSignalError(signal!);
1282
+ if (!closeError && promptError !== undefined) {
1283
+ closeError = promptError instanceof Error ? promptError.message : String(promptError);
1407
1284
  }
1408
- if (code !== 0 && rawStdout.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
1409
- closeError = rawStdout.trim();
1285
+ const forcedDrainAfterFinalSuccess = (forced || forcedTermination) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1286
+ const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(result.messages ?? []);
1287
+ if (!closeError && (abortedBySignal || session?.shutDown) && !result.interrupted && !result.timedOut) {
1288
+ closeError = session?.shutDown ? "Subagent stopped because the parent session shut down." : STOPPED_BEFORE_COMPLETION_ERROR;
1410
1289
  }
1411
- if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
1412
- closeError = stderr.trim();
1290
+ if (!closeError && forced && !forcedDrainAfterFinalSuccess) {
1291
+ closeError = "Subagent session did not settle after it was aborted.";
1413
1292
  }
1414
- const finalCode = forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
1293
+ const finalCode = forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal ? 0 : closeError || promptError !== undefined ? 1 : 0;
1415
1294
  if (!result.error && closeError) result.error = closeError;
1416
1295
  finish(finalCode);
1417
- });
1418
- proc.on("error", (error) => {
1419
- if (lifecycleFinished) return;
1420
- processClosed = true;
1421
- clearFinalDrainTimers();
1422
- clearStdioGuard();
1423
- void jsonlWriter.close().catch(() => {
1424
- // JSONL artifact flush is best effort.
1425
- });
1426
- cleanupTempDir(tempDir);
1427
- stdoutReader.end();
1428
- stderrReader.end();
1429
- if (!result.error) {
1430
- result.error = error instanceof Error ? error.message : String(error);
1431
- }
1432
- finish(1);
1433
- });
1296
+ };
1434
1297
 
1435
1298
  if (options.signal) {
1436
1299
  const kill = () => {
1437
- if (processClosed || lifecycleFinished) return;
1300
+ if (sessionSettled || lifecycleFinished) return;
1438
1301
  abortedBySignal = true;
1439
- proc.kill("SIGTERM");
1440
- setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
1302
+ abortChild();
1303
+ const hardTimer = setTimeout(() => {
1304
+ if (sessionSettled || lifecycleFinished) return;
1305
+ settle(undefined, true);
1306
+ }, 3000);
1307
+ hardTimer.unref?.();
1441
1308
  };
1442
1309
  if (options.signal.aborted) kill();
1443
1310
  else {
@@ -1448,7 +1315,7 @@ async function runSingleAttempt(
1448
1315
 
1449
1316
  if (options.interruptSignal) {
1450
1317
  const interrupt = () => {
1451
- if (processClosed || lifecycleFinished) return;
1318
+ if (sessionSettled || lifecycleFinished) return;
1452
1319
  if (result.timedOut) return;
1453
1320
  interruptedByControl = true;
1454
1321
  clearTimeoutTimers();
@@ -1459,11 +1326,12 @@ async function runSingleAttempt(
1459
1326
  result.finalOutput = "Interrupted. Waiting for explicit next action.";
1460
1327
  progress.activityState = undefined;
1461
1328
  fireUpdate();
1462
- trySignalChild(proc, "SIGINT");
1463
- setTimeout(() => {
1464
- if (lifecycleFinished || processClosed) return;
1465
- trySignalChild(proc, "SIGTERM");
1466
- }, 1000).unref?.();
1329
+ abortChild();
1330
+ const hardTimer = setTimeout(() => {
1331
+ if (sessionSettled || lifecycleFinished) return;
1332
+ settle(undefined, true);
1333
+ }, 3000);
1334
+ hardTimer.unref?.();
1467
1335
  };
1468
1336
  if (options.interruptSignal.aborted) interrupt();
1469
1337
  else {
@@ -1478,9 +1346,35 @@ async function runSingleAttempt(
1478
1346
  options.onDetachReady?.((reason = "user request") => detachForeground(reason));
1479
1347
  } catch (error) {
1480
1348
  // A consumer callback is advisory. Keep the child attached and observable
1481
- // rather than rejecting this attempt and orphaning its live process.
1349
+ // rather than rejecting this attempt and orphaning its live session.
1482
1350
  appendRecentOutput(progress, [`Foreground detach callback failed: ${error instanceof Error ? error.message : String(error)}`]);
1483
1351
  }
1352
+
1353
+ void (async () => {
1354
+ try {
1355
+ const created = await childSessions.create({
1356
+ ...launch.session,
1357
+ onExtensionError: (error) => {
1358
+ shared.transcriptWriter?.writeStderrLine(`Extension error (${error.extensionPath}, ${error.event}): ${error.error instanceof Error ? error.error.message : String(error.error)}`);
1359
+ },
1360
+ });
1361
+ if (lifecycleFinished) {
1362
+ void created.dispose();
1363
+ return;
1364
+ }
1365
+ session = created;
1366
+ created.detached = detached;
1367
+ unsubscribe = created.subscribe((event) => processEvent(event as Parameters<typeof processEvent>[0]));
1368
+ if (abortedBySignal || interruptedByControl || result.timedOut) {
1369
+ abortChild();
1370
+ }
1371
+ options.onChildSession?.({ steer: (text) => created.steer(text), followUp: (text) => created.followUp(text) });
1372
+ await created.prompt(`Task: ${task}`);
1373
+ settle(undefined);
1374
+ } catch (error) {
1375
+ settle(error ?? new Error("Child session failed."));
1376
+ }
1377
+ })();
1484
1378
  });
1485
1379
  result.exitCode = exitCode;
1486
1380
  if (afterCompactionSettlement) {
@@ -1506,20 +1400,10 @@ async function runSingleAttempt(
1506
1400
  && !abortedBySignal
1507
1401
  && !result.timedOut
1508
1402
  && !result.stopped
1509
- && !result.protocolError
1510
1403
  && !toolAvailabilityError) {
1511
- const processExitCode = result.exitCode;
1512
1404
  result.exitCode = 1;
1513
- result.error = formatMidToolExitError({
1514
- toolName: progress.currentTool,
1515
- exitCode: processExitCode,
1516
- processSignal: result.processSignal,
1517
- });
1405
+ result.error = formatMidToolExitError({ toolName: progress.currentTool });
1518
1406
  }
1519
- result.error = formatSubagentExtensionConflictError(result.error, {
1520
- agent: agent.name,
1521
- ambientExtensionsEnabled: !launchResolvedExtensions.disableAmbientExtensions,
1522
- });
1523
1407
  if (result.error && result.exitCode === 0) {
1524
1408
  result.exitCode = 1;
1525
1409
  }
@@ -1527,27 +1411,23 @@ async function runSingleAttempt(
1527
1411
  if (options.structuredOutput && result.exitCode === 0 && !result.error) {
1528
1412
  result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
1529
1413
  result.structuredOutputPath = options.structuredOutput.outputPath;
1530
- if (!structuredOutputToolInvoked) {
1414
+ const structured = capture.structuredOutput();
1415
+ if (!structuredOutputToolInvoked || !structured.called) {
1531
1416
  result.exitCode = 1;
1532
1417
  result.error = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
1533
1418
  result.structuredOutputFailed = true;
1534
1419
  } else {
1535
- const structured = await readStructuredOutput({
1536
- schema: options.structuredOutput.schema,
1537
- schemaPath: options.structuredOutput.schemaPath,
1538
- outputPath: options.structuredOutput.outputPath,
1539
- });
1540
- if (structured.error) {
1541
- result.exitCode = 1;
1542
- result.error = structured.error;
1543
- result.structuredOutputFailed = true;
1544
- } else {
1545
- result.structuredOutput = structured.value;
1546
- const acceptanceReport = readStructuredOutputAcceptanceReport(options.structuredOutput);
1547
- (result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReport = acceptanceReport.value;
1548
- (result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError = acceptanceReport.error;
1549
- validatedStructuredOutput = true;
1550
- }
1420
+ result.structuredOutput = structured.value;
1421
+ const acceptanceMode = options.structuredOutput.acceptanceReportPath
1422
+ ? options.structuredOutput.acceptanceReportRequired ? "required" : "optional"
1423
+ : undefined;
1424
+ const acceptanceReportError = acceptanceMode === "required" && !structured.acceptanceReportProvided
1425
+ ? MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR
1426
+ : undefined;
1427
+ (result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReport = acceptanceMode ? structured.acceptanceReport : undefined;
1428
+ (result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError = acceptanceReportError;
1429
+ writeStructuredOutputArtifacts(options.structuredOutput, structured.value, acceptanceMode ? structured.acceptanceReport : undefined);
1430
+ validatedStructuredOutput = true;
1551
1431
  }
1552
1432
  }
1553
1433
  if (result.exitCode === 0 && !result.error) {
@@ -1600,7 +1480,7 @@ async function runSingleAttempt(
1600
1480
  const outputChanged = hasSingleOutputChangedSinceSnapshot(options.outputPath, shared.outputSnapshot);
1601
1481
  requiredOutputMissing = outputChanged === undefined ? undefined : !outputChanged;
1602
1482
  } else if (options.structuredOutput) {
1603
- requiredOutputMissing = !existsSync(options.structuredOutput.outputPath);
1483
+ requiredOutputMissing = !capture.structuredOutput().called;
1604
1484
  }
1605
1485
  result.timeoutRecovery = buildTimeoutRecoverySummary({
1606
1486
  termination: "timed-out",
@@ -1719,6 +1599,20 @@ async function runSingleAttempt(
1719
1599
  return result;
1720
1600
  }
1721
1601
 
1602
+ /** Keep the structured output artifact files other consumers read by path. */
1603
+ function writeStructuredOutputArtifacts(runtime: NonNullable<RunSyncOptions["structuredOutput"]>, value: unknown, acceptanceReport: unknown): void {
1604
+ try {
1605
+ mkdirSync(path.dirname(runtime.outputPath), { recursive: true });
1606
+ writeFileSync(runtime.outputPath, JSON.stringify(value), { mode: 0o600 });
1607
+ if (runtime.acceptanceReportPath) {
1608
+ if (acceptanceReport !== undefined) writeFileSync(runtime.acceptanceReportPath, JSON.stringify(acceptanceReport), { mode: 0o600 });
1609
+ else rmSync(runtime.acceptanceReportPath, { force: true });
1610
+ }
1611
+ } catch {
1612
+ // The captured value is already on the result; the file is observability only.
1613
+ }
1614
+ }
1615
+
1722
1616
  /**
1723
1617
  * Run a subagent synchronously (blocking until complete)
1724
1618
  */
@@ -1744,7 +1638,7 @@ async function runSyncCompletionInner(
1744
1638
  }
1745
1639
  options = {
1746
1640
  ...options,
1747
- capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])),
1641
+ capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), options.childRuntime?.capabilityCeiling),
1748
1642
  };
1749
1643
  const childSessionName = deriveChildSessionName({ agent: agentName, task });
1750
1644
  const agent = agents.find((a) => a.name === agentName);
@@ -1766,7 +1660,7 @@ async function runSyncCompletionInner(
1766
1660
  thinkingCeiling: intersectThinkingCeilings(
1767
1661
  options.thinkingCeiling,
1768
1662
  agent.maxThinking,
1769
- decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
1663
+ options.childRuntime?.thinkingCeiling,
1770
1664
  ),
1771
1665
  };
1772
1666
  try {
@@ -1990,14 +1884,12 @@ async function runSyncCompletionInner(
1990
1884
  };
1991
1885
  let lastResult: SingleResult | undefined;
1992
1886
  const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1993
- // Escalated to "file" after an unexplained zero-activity startup failure so
1994
- // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1995
- let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1996
1887
  let abortRecoveryAttempted = false;
1997
1888
  let nextAttemptTask = taskWithAcceptance;
1998
1889
  modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1999
1890
  const candidate = modelsToTry[modelIndex];
2000
- for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
1891
+ // The inner loop re-runs the same candidate at most once, for abort recovery.
1892
+ for (;;) {
2001
1893
  const recoveringAbort = abortRecoveryAttempted;
2002
1894
  const attemptTask = nextAttemptTask;
2003
1895
  const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
@@ -2016,13 +1908,12 @@ async function runSyncCompletionInner(
2016
1908
  .filter((modelCandidate): modelCandidate is string => Boolean(modelCandidate)),
2017
1909
  outputSnapshot,
2018
1910
  originalTask: task,
2019
- taskDelivery: taskDeliveryOverride,
2020
1911
  orcaProgressTab,
2021
1912
  launchWarnings,
2022
1913
  verifyModel,
2023
1914
  });
2024
1915
  lastResult = result;
2025
- if (!recoveringAbort && startupAttemptIndex === 0) {
1916
+ if (!recoveringAbort) {
2026
1917
  if (result.model) attemptedModels.push(result.model);
2027
1918
  else if (candidate) attemptedModels.push(candidate);
2028
1919
  }
@@ -2072,75 +1963,10 @@ async function runSyncCompletionInner(
2072
1963
  if (options.workflowChildPermitLaunch && !attemptSucceeded) break modelAttemptsLoop;
2073
1964
  // Preserve the legacy intercom handoff contract: once this logical run has
2074
1965
  // been handed to a supervisor, terminating that attempt must not launch a
2075
- // startup retry or model fallback. Explicit user detach retains fallback.
1966
+ // model fallback. Explicit user detach retains fallback.
2076
1967
  if (intercomDetached || result.timedOut) break modelAttemptsLoop;
2077
1968
  if (attemptSucceeded) break modelAttemptsLoop;
2078
1969
 
2079
- const startupFailure = isRetryableSubagentStartupFailure({
2080
- exitCode: result.exitCode,
2081
- error: result.error,
2082
- finalOutput: result.finalOutput,
2083
- messageCount: result.messages?.length ?? 0,
2084
- toolCount: result.progressSummary?.toolCount ?? 0,
2085
- usage: result.usage,
2086
- durationMs: result.progressSummary?.durationMs ?? Number.POSITIVE_INFINITY,
2087
- protocolError: result.protocolError,
2088
- processSignal: result.processSignal,
2089
- detached: result.detached,
2090
- interrupted: result.interrupted,
2091
- timedOut: result.timedOut,
2092
- stopped: result.stopped,
2093
- });
2094
- const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2095
- if (startupFailure && retryDelayMs !== undefined) {
2096
- const retryNote = formatSubagentStartupRetryNote({
2097
- model: attempt.model,
2098
- attempt: startupAttemptIndex + 1,
2099
- maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
2100
- delayMs: retryDelayMs,
2101
- });
2102
- const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [options.signal, options.interruptSignal]);
2103
- if (!shouldRetry) {
2104
- if (options.interruptSignal?.aborted) {
2105
- result.exitCode = 0;
2106
- result.interrupted = true;
2107
- result.error = undefined;
2108
- result.finalOutput = "Interrupted. Waiting for explicit next action.";
2109
- if (result.progress) {
2110
- result.progress.status = "running";
2111
- result.progress.error = undefined;
2112
- }
2113
- } else {
2114
- const cancellationError = "Subagent startup retry cancelled before relaunch.";
2115
- result.error = cancellationError;
2116
- result.finalOutput = cancellationError;
2117
- attempt.error = cancellationError;
2118
- if (result.progress) result.progress.error = cancellationError;
2119
- }
2120
- break modelAttemptsLoop;
2121
- }
2122
- if (!taskDeliveryOverride && result.processSignal === "SIGKILL") {
2123
- taskDeliveryOverride = "file";
2124
- attemptNotes.push("[startup-retry] retrying with file task delivery to keep the task text out of the child process argv.");
2125
- }
2126
- attempt.error = retryNote;
2127
- attemptNotes.push(retryNote);
2128
- continue;
2129
- }
2130
- if (startupFailure) {
2131
- const startupError = formatSubagentStartupRetryExhaustedError({
2132
- model: attempt.model,
2133
- attempts: startupAttemptIndex + 1,
2134
- });
2135
- result.error = startupError;
2136
- result.finalOutput = startupError;
2137
- if (result.progress) {
2138
- result.progress.error = startupError;
2139
- result.progress.status = "failed";
2140
- }
2141
- attempt.error = startupError;
2142
- break modelAttemptsLoop;
2143
- }
2144
1970
  const retryableModelFailure = isRetryableModelFailureAttempt({ error: result.error, messages: result.messages, toolCount: result.progressSummary?.toolCount });
2145
1971
  if (retryableModelFailure) recordRetryableModelFailure(result.model ?? candidate, result.error);
2146
1972
  if (isContextOverflow(result.error)) {