pi-subagents 0.54.0 → 0.56.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 (71) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/agents/reviewer.md +12 -2
  3. package/docs/agents.md +4 -4
  4. package/docs/configuration.md +4 -4
  5. package/docs/extension-api.md +14 -4
  6. package/docs/models.md +30 -3
  7. package/docs/observability.md +14 -3
  8. package/docs/tool-reference.md +21 -6
  9. package/docs/workflows.md +7 -1
  10. package/package.json +1 -1
  11. package/prompts/council.md +9 -0
  12. package/prompts/parallel-review.md +5 -1
  13. package/prompts/review-loop.md +9 -5
  14. package/skills/council-mode/SKILL.md +33 -10
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  16. package/skills/pi-subagents/references/execution-controls.md +11 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  18. package/skills/pi-subagents/references/prompting-and-roles.md +8 -8
  19. package/src/agents/agent-management.ts +30 -9
  20. package/src/agents/agent-serializer.ts +4 -2
  21. package/src/agents/agents.ts +168 -41
  22. package/src/api/external-job-provider.ts +10 -1
  23. package/src/api/preflight.ts +45 -17
  24. package/src/api/project-panes.ts +2 -0
  25. package/src/extension/index.ts +20 -1
  26. package/src/extension/public-execution.ts +12 -9
  27. package/src/extension/rpc.ts +77 -3
  28. package/src/extension/schemas.ts +7 -1
  29. package/src/extension/tool-description.ts +9 -6
  30. package/src/inspectors/herdr/focus.ts +55 -0
  31. package/src/inspectors/herdr/project-panes.ts +228 -44
  32. package/src/integrations/herdr-status.ts +26 -4
  33. package/src/runs/background/async-execution.ts +137 -13
  34. package/src/runs/background/async-job-tracker.ts +33 -14
  35. package/src/runs/background/async-resume.ts +23 -2
  36. package/src/runs/background/async-status.ts +10 -0
  37. package/src/runs/background/control-channel.ts +98 -10
  38. package/src/runs/background/notify.ts +62 -1
  39. package/src/runs/background/run-status.ts +15 -1
  40. package/src/runs/background/subagent-runner.ts +308 -47
  41. package/src/runs/foreground/async-stop-action.ts +23 -2
  42. package/src/runs/foreground/execution.ts +115 -12
  43. package/src/runs/foreground/foreground-history.ts +3 -2
  44. package/src/runs/foreground/subagent-executor.ts +404 -45
  45. package/src/runs/foreground/workflow-detach-reconcile.ts +18 -6
  46. package/src/runs/shared/acceptance.ts +11 -5
  47. package/src/runs/shared/agent-contract.ts +1 -1
  48. package/src/runs/shared/child-identity.ts +36 -0
  49. package/src/runs/shared/completion-guard.ts +52 -2
  50. package/src/runs/shared/dynamic-fanout.ts +1 -1
  51. package/src/runs/shared/extension-bindings.ts +78 -0
  52. package/src/runs/shared/external-cli-runner.ts +2 -0
  53. package/src/runs/shared/external-job-bridge.ts +53 -37
  54. package/src/runs/shared/external-job-runner.ts +126 -23
  55. package/src/runs/shared/fast-mode-extension.ts +10 -0
  56. package/src/runs/shared/model-exclusions.ts +2 -1
  57. package/src/runs/shared/model-fallback.ts +12 -7
  58. package/src/runs/shared/mutation-evidence.ts +145 -0
  59. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  60. package/src/runs/shared/parallel-utils.ts +15 -0
  61. package/src/runs/shared/pi-args.ts +56 -12
  62. package/src/runs/shared/structured-output.ts +18 -4
  63. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  64. package/src/runs/shared/tool-availability.ts +1 -3
  65. package/src/shared/launch-contract.ts +12 -5
  66. package/src/shared/settings.ts +6 -1
  67. package/src/shared/thinking-ceiling.ts +52 -0
  68. package/src/shared/types.ts +107 -2
  69. package/src/tui/fleet-status.ts +66 -13
  70. package/src/tui/render.ts +5 -4
  71. package/src/workflows/scripted-workflow.ts +156 -12
@@ -11,7 +11,7 @@ import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilie
11
11
  import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
12
12
  import { updateActiveRunIndex } from "./active-run-index.ts";
13
13
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
14
- import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest } from "./control-channel.ts";
14
+ import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest, type StopRequest } from "./control-channel.ts";
15
15
  import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
16
16
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
17
17
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
@@ -48,6 +48,7 @@ import {
48
48
  type SteeringStatus,
49
49
  type SteeringTargetState,
50
50
  type SteeringTargetStatus,
51
+ type SubagentChildStatusEvent,
51
52
  DEFAULT_MAX_OUTPUT,
52
53
  type MaxOutputConfig,
53
54
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -79,13 +80,14 @@ import {
79
80
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
80
81
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
81
82
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
82
- import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
83
+ import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
83
84
  import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
84
85
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
86
+ import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
85
87
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
86
88
  import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
87
89
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
88
- import { formatModelAttemptNote, isContextOverflow, isRetryableModelFailure, recordRetryableModelFailure } from "../shared/model-fallback.ts";
90
+ import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailure, recordRetryableModelFailure } from "../shared/model-fallback.ts";
89
91
  import {
90
92
  SUBAGENT_STARTUP_RETRY_DELAYS_MS,
91
93
  formatSubagentExtensionConflictError,
@@ -99,7 +101,7 @@ import { createOwnedProcessTreeController, type OwnedProcessTreeController } fro
99
101
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
100
102
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
101
103
  import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
102
- import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
104
+ import { evaluateCompletionMutationGuard, validateImplementationToolContract } from "../shared/completion-guard.ts";
103
105
  import {
104
106
  createMutatingFailureState,
105
107
  didMutatingToolFail,
@@ -123,13 +125,16 @@ import {
123
125
  type WorktreeSetup,
124
126
  } from "../shared/worktree.ts";
125
127
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
128
+ import { assertThinkingWithinCeiling, decodeThinkingCeiling, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
126
129
  import { launchBindingDigest } from "../../shared/launch-contract.ts";
127
130
  import { writeInitialProgressFile } from "../../shared/settings.ts";
128
131
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
129
132
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
130
133
  import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
131
134
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
135
+ import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
132
136
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
137
+ import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
133
138
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
134
139
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
135
140
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
@@ -226,6 +231,7 @@ interface StepResult {
226
231
  timedOut?: boolean;
227
232
  stopped?: boolean;
228
233
  processSignal?: string | null;
234
+ timeoutRecovery?: import("../../shared/types.ts").TimeoutRecoverySummary;
229
235
  turnBudget?: TurnBudgetState;
230
236
  turnBudgetExceeded?: boolean;
231
237
  wrapUpRequested?: boolean;
@@ -537,6 +543,9 @@ interface RunPiStreamingResult {
537
543
  processCloseObservedAt?: number;
538
544
  processSignal?: string | null;
539
545
  processTree: ProcessTreeTerminalV1;
546
+ currentTool?: string;
547
+ currentToolArgs?: string;
548
+ currentPath?: string;
540
549
  }
541
550
 
542
551
  function runPiStreaming(
@@ -560,6 +569,8 @@ function runPiStreaming(
560
569
  toolTimeoutMs?: number,
561
570
  runDeadlineAt?: number,
562
571
  orcaProgressTab?: OrcaProgressTab,
572
+ expectedModelForVerification?: string,
573
+ modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>,
563
574
  ): Promise<RunPiStreamingResult> {
564
575
  return new Promise((resolve) => {
565
576
  const startedAt = Date.now();
@@ -605,6 +616,9 @@ function runPiStreaming(
605
616
  let observedMutationAttempt = false;
606
617
  let structuredOutputToolInvoked = false;
607
618
  let structuredOutputMessageStartIndex: number | undefined;
619
+ let currentTool: string | undefined;
620
+ let currentToolArgs: string | undefined;
621
+ let currentPath: string | undefined;
608
622
  let toolCount = 0;
609
623
  const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
610
624
  let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
@@ -696,12 +710,18 @@ function runPiStreaming(
696
710
 
697
711
  if (event.type === "tool_execution_end") {
698
712
  clearActiveToolTimeout(event);
713
+ currentTool = undefined;
714
+ currentToolArgs = undefined;
715
+ currentPath = undefined;
699
716
  return;
700
717
  }
701
718
 
702
719
  if (event.type === "tool_execution_start" && event.toolName) {
703
720
  toolCount += 1;
704
721
  armToolTimeout({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName });
722
+ currentTool = event.toolName;
723
+ currentToolArgs = extractToolArgsPreview(event.args ?? {});
724
+ currentPath = resolveCurrentPath(event.toolName, event.args);
705
725
  if (event.toolName === "structured_output") {
706
726
  structuredOutputToolInvoked = true;
707
727
  structuredOutputMessageStartIndex = messages.length;
@@ -718,7 +738,14 @@ function runPiStreaming(
718
738
  if (text) writeOutputText(text);
719
739
 
720
740
  if (event.type !== "message_end" || event.message.role !== "assistant") return;
721
- if (event.message.model) model = event.message.model;
741
+ const hasToolCall = assistantStartsToolCall(event.message);
742
+ if (event.message.model) {
743
+ model = event.message.model;
744
+ if (expectedModelForVerification && !hasToolCall) {
745
+ const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, event.message.model, modelVerificationRegistry);
746
+ if (modelVerificationError && !error) error = modelVerificationError;
747
+ }
748
+ }
722
749
  if (event.message.errorMessage) assistantError = event.message.errorMessage;
723
750
  const eventUsage = event.message.usage;
724
751
  if (eventUsage) {
@@ -1017,6 +1044,9 @@ function runPiStreaming(
1017
1044
  processCloseObservedAt,
1018
1045
  processSignal: signal,
1019
1046
  processTree,
1047
+ currentTool,
1048
+ currentToolArgs,
1049
+ currentPath,
1020
1050
  }));
1021
1051
  });
1022
1052
 
@@ -1195,6 +1225,8 @@ interface SingleStepContext {
1195
1225
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1196
1226
  onExternalJob?: (status: ExternalJobStatus) => void;
1197
1227
  skipAcceptance?: () => boolean;
1228
+ /** False when sibling work in the same Git worktree could have caused the tracked diff. */
1229
+ trackedMutationEvidenceForCompletionGuard?: boolean;
1198
1230
  orcaProgressTab?: OrcaProgressTab;
1199
1231
  }
1200
1232
 
@@ -1279,8 +1311,48 @@ async function runSingleStepInner(
1279
1311
  let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
1280
1312
  if (ctx.outputs) task = resolveOutputReferences(task, ctx.outputs);
1281
1313
  const taskForCompletionGuard = task;
1314
+ let resolvedTaskToolPlan: ReturnType<typeof resolvePiLaunchToolPlan> | undefined;
1315
+ if (!step.runner) {
1316
+ resolvedTaskToolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
1317
+ tools: step.tools,
1318
+ extensions: step.extensions,
1319
+ subagentOnlyExtensions: step.subagentOnlyExtensions,
1320
+ fast: step.fast,
1321
+ model: step.model,
1322
+ modelCandidates: step.modelCandidates,
1323
+ mcpDirectTools: step.mcpDirectTools,
1324
+ cwd: step.cwd ?? ctx.cwd,
1325
+ requireReadTool: Boolean(step.skills?.length),
1326
+ structuredOutput: Boolean(effectiveStructuredOutput),
1327
+ capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1328
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1329
+ permissionRules: step.permissionRules,
1330
+ }));
1331
+ const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
1332
+ const contractError = validateImplementationToolContract({
1333
+ agent: step.agent,
1334
+ task: taskForCompletionGuard,
1335
+ tools: contractTools,
1336
+ mcpDirectTools: resolvedTaskToolPlan.effectiveMcpTools,
1337
+ configuredExtensions: resolvedTaskToolPlan.configuredExtensions,
1338
+ requestedTools: resolvedTaskToolPlan.requestedBuiltinTools,
1339
+ acceptanceRole: step.acceptanceRole,
1340
+ completionGuard: step.completionGuard,
1341
+ });
1342
+ if (contractError) {
1343
+ return omitUndefinedProperties({
1344
+ agent: step.agent,
1345
+ context: step.context,
1346
+ output: contractError,
1347
+ error: contractError,
1348
+ exitCode: 1,
1349
+ capabilityCeiling: resolvedTaskToolPlan.capabilityCeiling,
1350
+ capabilityAudit: resolvedTaskToolPlan.capabilityAudit,
1351
+ });
1352
+ }
1353
+ }
1282
1354
  if (step.effectiveAcceptance) {
1283
- const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract) });
1355
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
1284
1356
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
1285
1357
  }
1286
1358
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -1395,6 +1467,7 @@ async function runSingleStepInner(
1395
1467
  timeoutMessage: ctx.timeoutMessage,
1396
1468
  stopMessage: ctx.stopMessage,
1397
1469
  onExternalJob: ctx.onExternalJob,
1470
+ followUp: step.externalJobFollowUp,
1398
1471
  }));
1399
1472
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1400
1473
  const resolvedOutput = step.outputPath && external.exitCode === 0
@@ -1450,11 +1523,15 @@ async function runSingleStepInner(
1450
1523
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1451
1524
  let finalResult: RunPiStreamingResult | undefined;
1452
1525
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
1526
+ let structuredAcceptanceReport: unknown;
1527
+ let structuredAcceptanceReportError: string | undefined;
1453
1528
  let completionGuardTriggeredFinal = false;
1454
1529
  let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
1455
1530
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
1456
1531
  let toolBudgetBlocked = false;
1457
1532
  let actualLaunchContractDigest = step.launchContractDigest;
1533
+ const mutationSnapshot = snapshotTrackedMutations(step.cwd ?? ctx.cwd);
1534
+ let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1458
1535
 
1459
1536
  let modelIndex = 0;
1460
1537
  let startupAttemptIndex = 0;
@@ -1466,6 +1543,13 @@ async function runSingleStepInner(
1466
1543
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1467
1544
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1468
1545
  const candidate = candidates[modelIndex];
1546
+ const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1547
+ try {
1548
+ assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling ?? decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]), agent: step.agent, runId: ctx.id });
1549
+ } catch (error) {
1550
+ const message = error instanceof Error ? error.message : String(error);
1551
+ return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1552
+ }
1469
1553
  ctx.onAttemptStart?.(omitUndefinedProperties({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) }));
1470
1554
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
1471
1555
  if (effectiveStructuredOutput) {
@@ -1476,6 +1560,7 @@ async function runSingleStepInner(
1476
1560
  }
1477
1561
  }
1478
1562
  const watchdogConfig = resolveWatchdogConfig(step.cwd ?? ctx.cwd);
1563
+ const extensionBindings = normalizeExtensionBindings(step.extensionBindings)?.value;
1479
1564
  const childWatchdog = watchdogConfig.ok
1480
1565
  ? resolveChildWatchdogConfig({
1481
1566
  config: watchdogConfig.config,
@@ -1499,6 +1584,8 @@ async function runSingleStepInner(
1499
1584
  tools: step.tools,
1500
1585
  extensions: step.extensions,
1501
1586
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1587
+ fast: step.fast,
1588
+ modelCandidates: step.modelCandidates,
1502
1589
  systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1503
1590
  systemPromptMode: step.systemPromptMode,
1504
1591
  mcpDirectTools: step.mcpDirectTools,
@@ -1529,16 +1616,21 @@ async function runSingleStepInner(
1529
1616
  : undefined,
1530
1617
  childWatchdog,
1531
1618
  waitToolEnabled: step.waitToolEnabled,
1619
+ thinkingCeiling: step.thinkingCeiling,
1620
+ extensionBindings,
1532
1621
  }));
1533
1622
  if (!launchWarningsEmitted && warnings.length > 0) {
1534
1623
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
1535
1624
  launchWarningsEmitted = true;
1536
1625
  }
1537
1626
  if (step.definitionDigest) {
1538
- const toolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
1627
+ const toolPlan = resolvedTaskToolPlan ?? resolvePiLaunchToolPlan(omitUndefinedProperties({
1539
1628
  tools: step.tools,
1540
1629
  extensions: step.extensions,
1541
1630
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1631
+ fast: step.fast,
1632
+ model: step.model,
1633
+ modelCandidates: step.modelCandidates,
1542
1634
  mcpDirectTools: step.mcpDirectTools,
1543
1635
  cwd: step.cwd ?? ctx.cwd,
1544
1636
  requireReadTool: Boolean(step.skills?.length),
@@ -1553,7 +1645,9 @@ async function runSingleStepInner(
1553
1645
  task: step.launchBindingTask ?? task,
1554
1646
  ...(candidate ? { model: candidate } : {}),
1555
1647
  modelCandidates: candidates as string[],
1648
+ ...(step.fast !== undefined ? { fast: step.fast } : {}),
1556
1649
  ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1650
+ ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
1557
1651
  systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1558
1652
  systemPromptMode: step.systemPromptMode,
1559
1653
  inheritProjectContext: step.inheritProjectContext,
@@ -1565,6 +1659,7 @@ async function runSingleStepInner(
1565
1659
  ...(step.outputPath ? { outputPath: step.outputPath } : {}),
1566
1660
  ...(step.outputMode ? { outputMode: step.outputMode } : {}),
1567
1661
  ...(step.structuredOutputSchema ? { structuredOutputSchema: step.structuredOutputSchema } : {}),
1662
+ ...(extensionBindings ? { extensionBindings } : {}),
1568
1663
  }));
1569
1664
  }
1570
1665
  capabilityAudit = attemptCapabilityAudit;
@@ -1590,6 +1685,8 @@ async function runSingleStepInner(
1590
1685
  ctx.toolTimeoutMs,
1591
1686
  ctx.deadlineAt,
1592
1687
  ctx.orcaProgressTab,
1688
+ expectedModelForVerification,
1689
+ step.modelVerificationRegistry,
1593
1690
  );
1594
1691
  if (run.processCloseObservedAt !== undefined) {
1595
1692
  writerProcesses.push({
@@ -1642,6 +1739,9 @@ async function runSingleStepInner(
1642
1739
  if (structured.error) structuredError = structured.error;
1643
1740
  else {
1644
1741
  structuredOutput = structured.value;
1742
+ const acceptanceReport = readStructuredOutputAcceptanceReport(effectiveStructuredOutput);
1743
+ structuredAcceptanceReport = acceptanceReport.value;
1744
+ structuredAcceptanceReportError = acceptanceReport.error;
1645
1745
  validatedStructuredOutput = true;
1646
1746
  }
1647
1747
  }
@@ -1662,21 +1762,31 @@ async function runSingleStepInner(
1662
1762
  ? "Subagent produced no output (possible model cold-start or empty response)."
1663
1763
  : undefined;
1664
1764
  const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1665
- const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
1765
+ const completionToolPlan = resolvedTaskToolPlan;
1766
+ const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1767
+ finalMutationEvidence = mutationEvidence;
1768
+ const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
1769
+ const completionGuard = run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
1666
1770
  ? evaluateCompletionMutationGuard(omitUndefinedProperties({
1667
1771
  agent: step.agent,
1668
1772
  task: taskForCompletionGuard,
1669
1773
  messages: run.messages,
1670
- tools: step.tools,
1671
- mcpDirectTools: step.mcpDirectTools,
1774
+ tools: completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools,
1775
+ mcpDirectTools: completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools,
1776
+ toolAvailabilityError,
1777
+ mutationEvidence: completionMutationEvidence,
1672
1778
  }))
1673
1779
  : undefined;
1674
- const completionGuardTriggered = completionGuard?.triggered === true && !run.observedMutationAttempt;
1780
+ const mutationAttemptObserved = run.observedMutationAttempt === true || completionMutationEvidence?.attemptedMutation === true;
1781
+ const completionGuardTriggered = completionGuard?.triggered === true && !mutationAttemptObserved;
1782
+ const completionGuardBlocked = completionGuard?.blocked === true;
1675
1783
  const fileMutationEffect = completionGuard
1676
1784
  ? {
1677
- status: completionGuard.expectedMutation ? completionGuardTriggered ? "missing" as const : "observed" as const : "not-applicable" as const,
1785
+ status: completionGuardBlocked ? "blocked" as const : completionGuard.expectedMutation ? completionGuardTriggered ? "missing" as const : "observed" as const : "not-applicable" as const,
1678
1786
  expected: completionGuard.expectedMutation,
1679
- attempted: completionGuard.attemptedMutation || run.observedMutationAttempt === true,
1787
+ attempted: completionGuardBlocked ? false : completionGuard.attemptedMutation || mutationAttemptObserved,
1788
+ ...(completionMutationEvidence ? { evidence: completionMutationEvidence } : {}),
1789
+ ...(completionGuardBlocked && completionGuard.message ? { message: completionGuard.message } : {}),
1680
1790
  ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1681
1791
  }
1682
1792
  : undefined;
@@ -1825,6 +1935,21 @@ async function runSingleStepInner(
1825
1935
  : resolvedOutput.savedPath
1826
1936
  ? "unknown"
1827
1937
  : finalResult?.outputState ?? "unknown";
1938
+ const timeoutRecovery = finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true
1939
+ ? buildTimeoutRecoverySummary({
1940
+ termination: "timed-out",
1941
+ evidence: finalMutationEvidence,
1942
+ currentTool: finalResult?.currentTool,
1943
+ currentToolArgs: finalResult?.currentToolArgs,
1944
+ currentPath: finalResult?.currentPath,
1945
+ sessionFile: step.sessionFile,
1946
+ transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
1947
+ artifactPaths,
1948
+ })
1949
+ : undefined;
1950
+ if (timeoutRecovery) outputForSummary = outputForSummary.trim()
1951
+ ? `${outputForSummary}\n\n${timeoutRecovery.message}`
1952
+ : timeoutRecovery.message;
1828
1953
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
1829
1954
  fullOutput: outputForSummary,
1830
1955
  outputPath: step.outputPath,
@@ -1839,6 +1964,8 @@ async function runSingleStepInner(
1839
1964
  ? await evaluateAcceptance(omitUndefinedProperties({
1840
1965
  acceptance: step.effectiveAcceptance,
1841
1966
  output: outputForAcceptance,
1967
+ report: structuredAcceptanceReport as import("../../shared/types.ts").AcceptanceReport | undefined,
1968
+ reportError: structuredAcceptanceReportError,
1842
1969
  fileOutput: childWrittenOutput !== undefined && step.outputPath
1843
1970
  ? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only" }
1844
1971
  : undefined,
@@ -1934,6 +2061,7 @@ async function runSingleStepInner(
1934
2061
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
1935
2062
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
1936
2063
  processSignal: finalResult?.processSignal,
2064
+ timeoutRecovery,
1937
2065
  turnBudget,
1938
2066
  turnBudgetExceeded: turnBudgetExceeded || undefined,
1939
2067
  wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
@@ -1959,21 +2087,8 @@ async function runSingleStep(
1959
2087
  step: SubagentStep,
1960
2088
  ctx: SingleStepContext,
1961
2089
  ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
1962
- if (step.importAsyncRoot) return runSingleStepInner(step, ctx);
1963
- const orcaProgressTab = createOrcaProgressTab({
1964
- cwd: step.cwd ?? ctx.cwd,
1965
- runId: ctx.id,
1966
- agent: step.agent,
1967
- index: ctx.flatIndex,
1968
- });
1969
- try {
1970
- const result = await runSingleStepInner(step, { ...ctx, orcaProgressTab });
1971
- orcaProgressTab?.finish(result.stopped ? "stopped" : result.exitCode === 0 && !result.error ? "completed" : "failed", result.sessionFile);
1972
- return result;
1973
- } catch (error) {
1974
- orcaProgressTab?.finish("failed");
1975
- throw error;
1976
- }
2090
+ if (!step.importAsyncRoot) ctx.orcaProgressTab?.section({ agent: step.agent, index: ctx.flatIndex, count: ctx.flatStepCount });
2091
+ return runSingleStepInner(step, ctx);
1977
2092
  }
1978
2093
 
1979
2094
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
@@ -2242,6 +2357,7 @@ async function runSubagent(
2242
2357
  const activeChildInterrupts = new Map<number, () => void>();
2243
2358
  const activeChildTimeouts = new Map<number, () => void>();
2244
2359
  const activeChildStops = new Map<number, () => void>();
2360
+ const childStopRequests = new Map<number, { childId: string; requestedAt: number }>();
2245
2361
  const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
2246
2362
  const pendingStepSteers: SteerRequest[] = [];
2247
2363
  const steeringCapabilities = new Map<number, SteerCapability>();
@@ -2285,6 +2401,7 @@ async function runSubagent(
2285
2401
  ...(task.launchContractDigest ? { launchContractDigest: task.launchContractDigest } : {}),
2286
2402
  ...(task.launchResolvedExtensions ? { launchResolvedExtensions: task.launchResolvedExtensions } : {}),
2287
2403
  ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
2404
+ ...(task.thinkingCeiling ? { thinkingCeiling: task.thinkingCeiling } : {}),
2288
2405
  status: "pending",
2289
2406
  ...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
2290
2407
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
@@ -2310,6 +2427,7 @@ async function runSubagent(
2310
2427
  structured: Boolean(step.collect.outputSchema),
2311
2428
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
2312
2429
  ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
2430
+ ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
2313
2431
  status: "pending",
2314
2432
  ...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
2315
2433
  recentTools: [],
@@ -2332,6 +2450,7 @@ async function runSubagent(
2332
2450
  ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
2333
2451
  ...(step.launchResolvedExtensions ? { launchResolvedExtensions: step.launchResolvedExtensions } : {}),
2334
2452
  ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
2453
+ ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
2335
2454
  status: "pending",
2336
2455
  ...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
2337
2456
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
@@ -2354,6 +2473,19 @@ async function runSubagent(
2354
2473
  step.processTerminal = { version: 1, state: "pending", runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId };
2355
2474
  }
2356
2475
  }
2476
+ const initialAgentLabel = initialStatusSteps.length === 1
2477
+ ? initialStatusSteps[0]!.agent
2478
+ : (config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single")) === "parallel"
2479
+ ? `parallel:${initialStatusSteps.map((step) => step.agent).join("+")}`
2480
+ : `chain:${initialStatusSteps.map((step) => step.agent).join("->")}`;
2481
+ const orcaProgressTab = flatSteps.every((step) => step.importAsyncRoot) ? undefined : createOrcaProgressTab({
2482
+ cwd,
2483
+ runId: id,
2484
+ agent: initialAgentLabel,
2485
+ index: 0,
2486
+ stepCount: Math.max(initialStatusSteps.length, 1),
2487
+ });
2488
+
2357
2489
  const statusPayload: RunnerStatusPayload = omitUndefinedProperties({
2358
2490
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2359
2491
  runId: id,
@@ -2572,6 +2704,83 @@ async function runSubagent(
2572
2704
  statusPayload.lastUpdate = Date.now();
2573
2705
  writeStatusPayload();
2574
2706
  };
2707
+ const childStopTargetId = (index: number): string => asyncStatusChildIdentity(requiredStatusStep(statusPayload, index), index);
2708
+ const appendChildStatusEvent = (index: number, childId: string, status: "stopping" | "stopped", now = Date.now()): void => {
2709
+ const step = requiredStatusStep(statusPayload, index);
2710
+ appendJsonl(eventsPath, JSON.stringify({
2711
+ type: "subagent.child-status",
2712
+ version: 1,
2713
+ ts: now,
2714
+ runId: id,
2715
+ childId,
2716
+ status,
2717
+ reason: "user",
2718
+ source: "async",
2719
+ stepIndex: index,
2720
+ agent: step.agent,
2721
+ ...(step.runId ? { childRunId: step.runId } : {}),
2722
+ ...(step.workflowKey ? { workflowKey: step.workflowKey } : {}),
2723
+ ...(step.phase ? { phase: step.phase } : {}),
2724
+ ...(step.label ? { label: step.label } : {}),
2725
+ } satisfies SubagentChildStatusEvent));
2726
+ };
2727
+ const appendTerminalChildStatusEvent = (index: number, now = Date.now()): void => {
2728
+ const request = childStopRequests.get(index);
2729
+ if (request) appendChildStatusEvent(index, request.childId, "stopped", now);
2730
+ };
2731
+ const markChildStopRequested = (index: number, childId: string, now = Date.now()): boolean => {
2732
+ const step = statusPayload.steps[index];
2733
+ if (!step || (step.status !== "pending" && step.status !== "running")) return false;
2734
+ childStopRequests.set(index, { childId, requestedAt: now });
2735
+ step.stopRequested = true;
2736
+ step.stopRequestedAt = now;
2737
+ delete step.activityState;
2738
+ statusPayload.lastUpdate = now;
2739
+ writeStatusPayload();
2740
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.stop_requested", ts: now, runId: id, stepIndex: index, childId, agent: step.agent }));
2741
+ appendChildStatusEvent(index, childId, "stopping", now);
2742
+ return true;
2743
+ };
2744
+ const markChildStopped = (index: number, now = Date.now()): void => {
2745
+ const step = requiredStatusStep(statusPayload, index);
2746
+ if (step.status === "stopped") return;
2747
+ step.status = "stopped";
2748
+ step.error = stopMessage;
2749
+ step.exitCode = 1;
2750
+ step.stopped = true;
2751
+ step.stopRequested = true;
2752
+ step.stopRequestedAt = childStopRequests.get(index)?.requestedAt ?? step.stopRequestedAt ?? now;
2753
+ delete step.activityState;
2754
+ step.endedAt = now;
2755
+ step.durationMs = step.startedAt ? now - step.startedAt : 0;
2756
+ step.lastActivityAt = now;
2757
+ statusPayload.lastUpdate = now;
2758
+ writeStatusPayload();
2759
+ const childId = childStopRequests.get(index)?.childId ?? childStopTargetId(index);
2760
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.stopped", ts: now, runId: id, stepIndex: index, childId, agent: step.agent, exitCode: 1, durationMs: step.durationMs }));
2761
+ appendChildStatusEvent(index, childId, "stopped", now);
2762
+ };
2763
+ const childStopResult = (index: number, agent: string, context?: "fresh" | "fork"): SingleStepResult => {
2764
+ markChildStopped(index);
2765
+ return stoppedStepResult(agent, context);
2766
+ };
2767
+ const stopChildStep = (request: StopRequest): void => {
2768
+ if (request.targetIndex === undefined) {
2769
+ stopRunner();
2770
+ return;
2771
+ }
2772
+ const childId = request.childId ?? childStopTargetId(request.targetIndex);
2773
+ const now = Date.now();
2774
+ if (!markChildStopRequested(request.targetIndex, childId, now)) {
2775
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.stop_failed", ts: now, runId: id, stepIndex: request.targetIndex, childId, message: "Child is not pending or running." }));
2776
+ return;
2777
+ }
2778
+ const stop = activeChildStops.get(request.targetIndex);
2779
+ if (stop) stop();
2780
+ else if (requiredStatusStep(statusPayload, request.targetIndex).status === "pending") {
2781
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.stop_queued", ts: now, runId: id, stepIndex: request.targetIndex, childId }));
2782
+ }
2783
+ };
2575
2784
  const registerStepInterrupt = (flatIndex: number, interrupt: (() => void) | undefined): void => {
2576
2785
  if (!interrupt) {
2577
2786
  activeChildInterrupts.delete(flatIndex);
@@ -2594,7 +2803,7 @@ async function runSubagent(
2594
2803
  return;
2595
2804
  }
2596
2805
  activeChildStops.set(flatIndex, stop);
2597
- if (stopped) stop();
2806
+ if (stopped || childStopRequests.has(flatIndex)) stop();
2598
2807
  };
2599
2808
  const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
2600
2809
  if (!abort) {
@@ -3499,7 +3708,7 @@ async function runSubagent(
3499
3708
  const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
3500
3709
  onInterrupt: interruptRunner,
3501
3710
  onTimeout: timeoutRunner,
3502
- onStop: stopRunner,
3711
+ onStop: stopChildStep,
3503
3712
  onSteer: (request) => {
3504
3713
  const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
3505
3714
  if (targetStep?.status === "pending") {
@@ -3575,6 +3784,19 @@ async function runSubagent(
3575
3784
  if (materialized.parallel.length > 1 && step.parallel.outputPath && !step.parallel.namespaceOutputPath) {
3576
3785
  throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} materialized ${materialized.parallel.length} items that resolve output to the same path: ${step.parallel.outputPath}. Remove the explicit output path or use an inherited relative agent output so each item can be isolated.`);
3577
3786
  }
3787
+ for (const [itemIndex] of materialized.parallel.entries()) {
3788
+ const thinkingOverride = step.thinkingOverrides?.[itemIndex];
3789
+ const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
3790
+ const configThinking = thinkingOverride ? thinkingOverride : step.parallel.thinking;
3791
+ const candidates = step.parallel.modelCandidates !== undefined
3792
+ ? step.parallel.modelCandidates.length > 0
3793
+ ? step.parallel.modelCandidates.map((candidate) => thinkingOverride ? applyThinkingSuffix(candidate, thinkingOverride, true) ?? candidate : candidate)
3794
+ : [undefined]
3795
+ : model ? [model] : [undefined];
3796
+ for (const candidate of candidates) {
3797
+ assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling ?? decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]), agent: step.parallel.agent, runId: id });
3798
+ }
3799
+ }
3578
3800
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
3579
3801
  if (!config.runFanoutBudget) throw new Error("Async runner is missing its run fan-out budget identity.");
3580
3802
  const runFanoutBudget = claimRunFanoutBatch(config.runFanoutBudget, materialized.parallel.map((_, itemIndex) => `chain[${stepIndex}].expand[${itemIndex}]`));
@@ -3731,6 +3953,7 @@ async function runSubagent(
3731
3953
  ...(task.skills ? { skills: task.skills } : {}),
3732
3954
  ...(task.model ? { model: task.model } : {}),
3733
3955
  ...(task.thinking ? { thinking: task.thinking } : {}),
3956
+ ...(task.thinkingCeiling ? { thinkingCeiling: task.thinkingCeiling } : {}),
3734
3957
  ...(task.modelCandidates && task.modelCandidates.length > 0 ? { attemptedModels: task.modelCandidates } : task.model ? { attemptedModels: [task.model] } : {}),
3735
3958
  recentTools: [],
3736
3959
  recentOutput: [],
@@ -3802,6 +4025,7 @@ async function runSubagent(
3802
4025
  }
3803
4026
  if (timedOut) return timedOutStepResult(task.agent, task.context);
3804
4027
  if (stopped) return stoppedStepResult(task.agent, task.context);
4028
+ if (childStopRequests.has(fi)) return childStopResult(fi, task.agent, task.context);
3805
4029
  if (interrupted) return pausedStepResult(task.agent, task.context);
3806
4030
  if (aborted && failFast) {
3807
4031
  const skippedAt = Date.now();
@@ -3852,6 +4076,7 @@ async function runSubagent(
3852
4076
  registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
3853
4077
  timeoutSignal: timeoutAbortController.signal,
3854
4078
  stopSignal: stopAbortController.signal,
4079
+ trackedMutationEvidenceForCompletionGuard: false,
3855
4080
  timeoutMessage,
3856
4081
  stopMessage,
3857
4082
  turnBudget: config.turnBudget,
@@ -3861,7 +4086,8 @@ async function runSubagent(
3861
4086
  onWriterProcess,
3862
4087
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3863
4088
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3864
- skipAcceptance: () => timedOut || stopped,
4089
+ skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4090
+ orcaProgressTab,
3865
4091
  }), config.deadlineAt);
3866
4092
  const taskEndTime = Date.now();
3867
4093
  const childInterrupted = singleResult.interrupted === true;
@@ -3910,6 +4136,7 @@ async function runSubagent(
3910
4136
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutputPath", singleResult.structuredOutputPath);
3911
4137
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutputSchemaPath", singleResult.structuredOutputSchemaPath);
3912
4138
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "acceptance", singleResult.acceptance);
4139
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "timeoutRecovery", singleResult.timeoutRecovery);
3913
4140
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "watchdog", singleResult.watchdog);
3914
4141
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "capabilityCeiling", singleResult.capabilityCeiling);
3915
4142
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "capabilityAudit", singleResult.capabilityAudit);
@@ -3918,13 +4145,14 @@ async function runSubagent(
3918
4145
  statusPayload.lastUpdate = taskEndTime;
3919
4146
  writeStatusPayload();
3920
4147
  appendCapabilityCeilingAppliedEvent(eventsPath, id, fi, task.agent, singleResult);
3921
- appendJsonl(eventsPath, JSON.stringify({
3922
- type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
3923
- ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
3924
- exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
3925
- }));
3926
- if (singleResult.exitCode !== 0 && failFast) aborted = true;
3927
- return stopped || childStopped ? { ...singleResult, output: stopMessage, error: stopMessage, exitCode: 1, interrupted: false, timedOut: false, stopped: true, skipped: false } : timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
4148
+ appendJsonl(eventsPath, JSON.stringify({
4149
+ type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
4150
+ ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
4151
+ exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
4152
+ }));
4153
+ if (stopped || childStopped) appendTerminalChildStatusEvent(fi, taskEndTime);
4154
+ if (singleResult.exitCode !== 0 && failFast && !childStopped) aborted = true;
4155
+ return stopped || childStopped ? { ...singleResult, output: stopMessage, error: stopMessage, exitCode: 1, interrupted: false, timedOut: false, stopped: true, skipped: false } : timedOut ? { ...singleResult, output: singleResult.output || (timeoutMessage ?? "Subagent timed out."), error: singleResult.error ?? timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
3928
4156
  }, globalSemaphore);
3929
4157
 
3930
4158
  flatIndex += dynamicSteps.length;
@@ -3964,6 +4192,7 @@ async function runSubagent(
3964
4192
  effects: pr.effects,
3965
4193
  execution: pr.execution,
3966
4194
  review: pr.review,
4195
+ timeoutRecovery: pr.timeoutRecovery,
3967
4196
  structuredOutput: pr.structuredOutput,
3968
4197
  structuredOutputPath: pr.structuredOutputPath,
3969
4198
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
@@ -4176,6 +4405,7 @@ async function runSubagent(
4176
4405
  }
4177
4406
  if (timedOut) return timedOutStepResult(task.agent, task.context);
4178
4407
  if (stopped) return stoppedStepResult(task.agent, task.context);
4408
+ if (childStopRequests.has(fi)) return childStopResult(fi, task.agent, task.context);
4179
4409
  if (interrupted) return pausedStepResult(task.agent, task.context);
4180
4410
  if (aborted && failFast) {
4181
4411
  const skippedAt = Date.now();
@@ -4242,6 +4472,7 @@ async function runSubagent(
4242
4472
  registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
4243
4473
  timeoutSignal: timeoutAbortController.signal,
4244
4474
  stopSignal: stopAbortController.signal,
4475
+ trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
4245
4476
  timeoutMessage,
4246
4477
  stopMessage,
4247
4478
  turnBudget: config.turnBudget,
@@ -4251,7 +4482,8 @@ async function runSubagent(
4251
4482
  onWriterProcess,
4252
4483
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4253
4484
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4254
- skipAcceptance: () => timedOut || stopped,
4485
+ skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4486
+ orcaProgressTab,
4255
4487
  }), config.deadlineAt);
4256
4488
  if (task.sessionFile) {
4257
4489
  latestSessionFile = task.sessionFile;
@@ -4305,6 +4537,7 @@ async function runSubagent(
4305
4537
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutputPath", singleResult.structuredOutputPath);
4306
4538
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutputSchemaPath", singleResult.structuredOutputSchemaPath);
4307
4539
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "acceptance", singleResult.acceptance);
4540
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "timeoutRecovery", singleResult.timeoutRecovery);
4308
4541
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "watchdog", singleResult.watchdog);
4309
4542
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "capabilityCeiling", singleResult.capabilityCeiling);
4310
4543
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "capabilityAudit", singleResult.capabilityAudit);
@@ -4319,6 +4552,7 @@ async function runSubagent(
4319
4552
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
4320
4553
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
4321
4554
  }));
4555
+ if (stopped || childStopped) appendTerminalChildStatusEvent(fi, taskEndTime);
4322
4556
  if (singleResult.completionGuardTriggered) {
4323
4557
  const event = buildControlEvent(omitUndefinedProperties({
4324
4558
  from: requiredStatusStep(statusPayload, fi).activityState,
@@ -4333,8 +4567,8 @@ async function runSubagent(
4333
4567
  appendControlEvent(event);
4334
4568
  }
4335
4569
 
4336
- if (singleResult.exitCode !== 0 && failFast) aborted = true;
4337
- return stopped || childStopped ? { ...singleResult, output: stopMessage, error: stopMessage, exitCode: 1, interrupted: false, timedOut: false, stopped: true, skipped: false } : timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
4570
+ if (singleResult.exitCode !== 0 && failFast && !childStopped) aborted = true;
4571
+ return stopped || childStopped ? { ...singleResult, output: stopMessage, error: stopMessage, exitCode: 1, interrupted: false, timedOut: false, stopped: true, skipped: false } : timedOut ? { ...singleResult, output: singleResult.output || (timeoutMessage ?? "Subagent timed out."), error: singleResult.error ?? timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
4338
4572
  },
4339
4573
  globalSemaphore,
4340
4574
  );
@@ -4391,10 +4625,11 @@ async function runSubagent(
4391
4625
  artifactPaths: pr.artifactPaths,
4392
4626
  transcriptPath: pr.transcriptPath,
4393
4627
  transcriptError: pr.transcriptError,
4394
- effects: pr.effects,
4395
- execution: pr.execution,
4396
- review: pr.review,
4397
- structuredOutput: pr.structuredOutput,
4628
+ effects: pr.effects,
4629
+ execution: pr.execution,
4630
+ review: pr.review,
4631
+ timeoutRecovery: pr.timeoutRecovery,
4632
+ structuredOutput: pr.structuredOutput,
4398
4633
  structuredOutputPath: pr.structuredOutputPath,
4399
4634
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
4400
4635
  acceptance: pr.acceptance,
@@ -4490,6 +4725,26 @@ async function runSubagent(
4490
4725
  }
4491
4726
  } else {
4492
4727
  const seqStep = step as SubagentStep;
4728
+ if (timedOut) {
4729
+ results.push(timedOutStepResult(seqStep.agent, seqStep.context));
4730
+ flatIndex++;
4731
+ continue;
4732
+ }
4733
+ if (stopped) {
4734
+ results.push(stoppedStepResult(seqStep.agent, seqStep.context));
4735
+ flatIndex++;
4736
+ continue;
4737
+ }
4738
+ if (childStopRequests.has(flatIndex)) {
4739
+ results.push(childStopResult(flatIndex, seqStep.agent, seqStep.context));
4740
+ flatIndex++;
4741
+ continue;
4742
+ }
4743
+ if (interrupted) {
4744
+ results.push(pausedStepResult(seqStep.agent, seqStep.context));
4745
+ flatIndex++;
4746
+ continue;
4747
+ }
4493
4748
  let singleWorktreeSetup: WorktreeSetup | undefined;
4494
4749
  if (seqStep.worktree) {
4495
4750
  try {
@@ -4574,7 +4829,8 @@ async function runSubagent(
4574
4829
  onWriterProcess,
4575
4830
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4576
4831
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4577
- skipAcceptance: () => timedOut || stopped,
4832
+ skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4833
+ orcaProgressTab,
4578
4834
  }), config.deadlineAt);
4579
4835
  } catch (error) {
4580
4836
  if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
@@ -4593,7 +4849,7 @@ async function runSubagent(
4593
4849
  launchContractDigest: singleResult.launchContractDigest,
4594
4850
  launchResolvedExtensions: singleResult.launchResolvedExtensions,
4595
4851
  runtimeAcknowledgedExtensions: singleResult.runtimeAcknowledgedExtensions,
4596
- output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
4852
+ output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
4597
4853
  outputState: singleResult.outputState,
4598
4854
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
4599
4855
  protocolError: singleResult.protocolError,
@@ -4612,6 +4868,7 @@ async function runSubagent(
4612
4868
  effects: singleResult.effects,
4613
4869
  execution: singleResult.execution,
4614
4870
  review: singleResult.review,
4871
+ timeoutRecovery: singleResult.timeoutRecovery,
4615
4872
  structuredOutput: singleResult.structuredOutput,
4616
4873
  structuredOutputPath: singleResult.structuredOutputPath,
4617
4874
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
@@ -4698,6 +4955,7 @@ async function runSubagent(
4698
4955
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "structuredOutputPath", singleResult.structuredOutputPath);
4699
4956
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "structuredOutputSchemaPath", singleResult.structuredOutputSchemaPath);
4700
4957
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "acceptance", singleResult.acceptance);
4958
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "timeoutRecovery", singleResult.timeoutRecovery);
4701
4959
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "watchdog", singleResult.watchdog);
4702
4960
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "capabilityCeiling", singleResult.capabilityCeiling);
4703
4961
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "capabilityAudit", singleResult.capabilityAudit);
@@ -4721,6 +4979,7 @@ async function runSubagent(
4721
4979
  durationMs: stepEndTime - stepStartTime,
4722
4980
  tokens: stepTokens,
4723
4981
  }));
4982
+ if (stopped || childStopped) appendTerminalChildStatusEvent(flatIndex, stepEndTime);
4724
4983
  if (singleWorktreeSetup && !singleResult.detached) {
4725
4984
  const diffs = diffWorktrees(singleWorktreeSetup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
4726
4985
  const diffSummary = formatWorktreeDiffSummary(diffs);
@@ -4981,6 +5240,7 @@ async function runSubagent(
4981
5240
  structuredOutputSchemaPath: r.structuredOutputSchemaPath,
4982
5241
  acceptance: r.acceptance,
4983
5242
  watchdog: r.watchdog,
5243
+ timeoutRecovery: r.timeoutRecovery,
4984
5244
  capabilityCeiling: r.capabilityCeiling,
4985
5245
  capabilityAudit: r.capabilityAudit,
4986
5246
  })),
@@ -5023,6 +5283,7 @@ async function runSubagent(
5023
5283
  statusPayload.lastUpdate = Date.now();
5024
5284
  }
5025
5285
  writeStatusPayload();
5286
+ orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
5026
5287
  appendJsonl(
5027
5288
  eventsPath,
5028
5289
  JSON.stringify({