pi-subagents 0.58.0 → 0.60.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 (116) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +93 -5
  8. package/docs/workflows.md +94 -3
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +87 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  15. package/skills/pi-subagents/references/execution-controls.md +52 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +159 -42
  21. package/src/agents/agent-serializer.ts +4 -2
  22. package/src/agents/agents.ts +67 -26
  23. package/src/agents/runtime-agent-registry.ts +9 -16
  24. package/src/api/background-work.ts +5 -1
  25. package/src/api/delegation.ts +0 -7
  26. package/src/api/preflight.ts +6 -9
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +5 -3
  29. package/src/extension/index.ts +51 -23
  30. package/src/extension/public-execution.ts +16 -2
  31. package/src/extension/schemas.ts +36 -10
  32. package/src/extension/tool-description.ts +24 -5
  33. package/src/intercom/result-intercom.ts +2 -0
  34. package/src/profiles/profiles.ts +5 -6
  35. package/src/runs/background/active-async-capacity.ts +2 -2
  36. package/src/runs/background/async-execution.ts +52 -37
  37. package/src/runs/background/async-job-tracker.ts +17 -12
  38. package/src/runs/background/async-resume.ts +23 -25
  39. package/src/runs/background/async-status-snapshot.ts +23 -261
  40. package/src/runs/background/async-status.ts +110 -8
  41. package/src/runs/background/chain-append.ts +6 -3
  42. package/src/runs/background/chain-root-attachment.ts +60 -8
  43. package/src/runs/background/control-channel.ts +3 -2
  44. package/src/runs/background/fleet-view.ts +21 -11
  45. package/src/runs/background/notify.ts +158 -6
  46. package/src/runs/background/result-files.ts +2 -1
  47. package/src/runs/background/result-watcher.ts +2 -0
  48. package/src/runs/background/resume-guidance.ts +1 -1
  49. package/src/runs/background/retained-children.ts +1 -1
  50. package/src/runs/background/run-status.ts +30 -9
  51. package/src/runs/background/scheduled-runs.ts +86 -7
  52. package/src/runs/background/stale-run-reconciler.ts +10 -4
  53. package/src/runs/background/steering.ts +4 -14
  54. package/src/runs/background/subagent-runner.ts +351 -358
  55. package/src/runs/background/subagent-wait.ts +68 -12
  56. package/src/runs/background/terminal-run-index.ts +1 -1
  57. package/src/runs/background/wait-completions.ts +25 -1
  58. package/src/runs/background/wait-config.ts +23 -9
  59. package/src/runs/background/wait-tool.ts +9 -2
  60. package/src/runs/foreground/async-steering-action.ts +2 -2
  61. package/src/runs/foreground/execution.ts +125 -122
  62. package/src/runs/foreground/foreground-control.ts +3 -0
  63. package/src/runs/foreground/foreground-history.ts +1 -0
  64. package/src/runs/foreground/subagent-executor.ts +576 -240
  65. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  66. package/src/runs/shared/abort-recovery.ts +119 -0
  67. package/src/runs/shared/async-status-projection.ts +597 -0
  68. package/src/runs/shared/background-process-options.ts +9 -0
  69. package/src/runs/shared/child-identity.ts +19 -4
  70. package/src/runs/shared/child-launch-plan.ts +151 -0
  71. package/src/runs/shared/completion-evidence.ts +89 -0
  72. package/src/runs/shared/completion-guard.ts +1 -1
  73. package/src/runs/shared/dynamic-fanout.ts +2 -2
  74. package/src/runs/shared/host-step-status.ts +230 -0
  75. package/src/runs/shared/lane-metadata.ts +105 -0
  76. package/src/runs/shared/mcp-config-sources.ts +42 -6
  77. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  78. package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
  79. package/src/runs/shared/model-fallback.ts +9 -2
  80. package/src/runs/shared/mutation-evidence.ts +52 -3
  81. package/src/runs/shared/nested-events.ts +6 -2
  82. package/src/runs/shared/nested-render.ts +7 -3
  83. package/src/runs/shared/parallel-handoff.ts +419 -7
  84. package/src/runs/shared/parallel-utils.ts +7 -0
  85. package/src/runs/shared/pi-args.ts +67 -3
  86. package/src/runs/shared/single-output.ts +72 -22
  87. package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
  88. package/src/runs/shared/workflow-graph.ts +15 -0
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +5 -2
  94. package/src/shared/launch-contract.ts +1 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +232 -30
  97. package/src/shared/utils.ts +35 -55
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/tui/fleet-status.ts +134 -75
  104. package/src/tui/fleet.ts +11 -5
  105. package/src/tui/render-helpers.ts +31 -0
  106. package/src/tui/render.ts +895 -121
  107. package/src/watchdog/change-signature.ts +40 -1
  108. package/src/watchdog/turn-delta.ts +1 -1
  109. package/src/workflows/chat-progress.ts +6 -3
  110. package/src/workflows/host-command.ts +235 -0
  111. package/src/workflows/scripted-workflow.ts +503 -38
  112. package/src/workflows/workflow-child-summary.ts +9 -5
  113. package/src/workflows/workflow-preflight.ts +270 -0
  114. package/src/workflows/workflow-receipt.ts +43 -4
  115. package/src/workflows/workflow-settlement.ts +246 -0
  116. package/src/runs/shared/turn-budget.ts +0 -98
@@ -46,6 +46,7 @@ import {
46
46
  findLatestSessionFile,
47
47
  detectSubagentError,
48
48
  hasEmptyTerminalAssistantResponse,
49
+ formatEmptyTerminalAssistantResponseError,
49
50
  extractToolArgsPreview,
50
51
  extractTextFromContent,
51
52
  boundStreamedRecentTools,
@@ -55,7 +56,9 @@ import {
55
56
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
56
57
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
57
58
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, resolveToolTimeoutMs, toolTimeoutCallKey, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
58
- import { evaluateCompletionMutationGuard, validateImplementationToolContract } from "../shared/completion-guard.ts";
59
+ import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
60
+ import { planCompletionEvidence } from "../shared/completion-evidence.ts";
61
+ import { planAbortRecovery } from "../shared/abort-recovery.ts";
59
62
  import { arbitrateCompletionGuardRescue } from "../shared/llm-intent-arbiter.ts";
60
63
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
61
64
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
@@ -63,7 +66,8 @@ import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
63
66
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
64
67
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
65
68
  import { resolvePermissionRules } from "../shared/permissions.ts";
66
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
69
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
70
+ import { deriveChildSessionName } from "../../shared/child-session-name.ts";
67
71
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
68
72
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
69
73
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
@@ -72,7 +76,7 @@ import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructu
72
76
  import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
73
77
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
74
78
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
75
- import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
79
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, hasSingleOutputChangedSinceSnapshot, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
76
80
  import {
77
81
  buildModelCandidates,
78
82
  formatSubagentModelVerificationError,
@@ -103,7 +107,6 @@ import {
103
107
  import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport, validateAcceptanceInput } from "../shared/acceptance.ts";
104
108
  import { PROMPT_REDACTED } from "../../shared/utils.ts";
105
109
  import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
106
- import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
107
110
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
108
111
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
109
112
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
@@ -294,6 +297,9 @@ function snapshotStreamResult(result: SingleResult, progress: AgentProgress): Si
294
297
  return snapshot;
295
298
  }
296
299
 
300
+ const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
301
+ type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
302
+
297
303
  async function runSingleAttempt(
298
304
  runtimeCwd: string,
299
305
  agent: AgentConfig,
@@ -323,6 +329,10 @@ async function runSingleAttempt(
323
329
  assertThinkingWithinCeiling({ model: modelArg, configThinking: effectiveThinking, ceiling: options.thinkingCeiling, agent: agent.name, runId: options.runId });
324
330
  const expectedModelForVerification = shared.verifyModel ? modelArg : undefined;
325
331
  const resolvedThinking = resolveEffectiveThinking(modelArg, effectiveThinking);
332
+ // Display name for the child session: applied inside the child via
333
+ // PI_SUBAGENT_SESSION_NAME and echoed back on the result payload so hosts
334
+ // can label this run without reading the child's session file.
335
+ const childSessionName = deriveChildSessionName({ agent: agent.name, task: shared.originalTask ?? task });
326
336
  const watchdogConfig = resolveWatchdogConfig(options.cwd ?? runtimeCwd);
327
337
  const childWatchdog = watchdogConfig.ok
328
338
  ? resolveChildWatchdogConfig({
@@ -351,13 +361,15 @@ async function runSingleAttempt(
351
361
  inheritSkills: agent.inheritSkills,
352
362
  requireReadTool: Boolean(shared.resolvedSkillNames?.length),
353
363
  tools: agent.tools,
364
+ allowNestedSubagents: agent.allowNestedSubagents,
354
365
  extensions: agent.extensions,
355
366
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
356
- systemPrompt: appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget),
367
+ systemPrompt: shared.systemPrompt,
357
368
  mcpDirectTools: agent.mcpDirectTools,
358
369
  cwd: options.cwd ?? runtimeCwd,
359
370
  promptFileStem: agent.name,
360
371
  intercomSessionName: options.intercomSessionName,
372
+ sessionName: childSessionName,
361
373
  orchestratorIntercomTarget: options.orchestratorIntercomTarget,
362
374
  runId: options.runId,
363
375
  childAgentName: agent.name,
@@ -368,6 +380,7 @@ async function runSingleAttempt(
368
380
  parentCapabilityToken: options.nestedRoute?.capabilityToken,
369
381
  runFanoutBudget: options.runFanoutBudget,
370
382
  parentSessionId: options.parentSessionId,
383
+ forkCacheKey: options.context === "fork" ? deriveForkPromptCacheKey(options.parentSessionId) : undefined,
371
384
  steerInboxDir: options.steerInboxDir,
372
385
  steerCapabilityPath: options.steerCapabilityPath,
373
386
  steerAckDir: options.steerAckDir,
@@ -380,6 +393,7 @@ async function runSingleAttempt(
380
393
  permissionAuditPath,
381
394
  childWatchdog,
382
395
  waitToolEnabled: options.waitToolEnabled,
396
+ waitToolDefaultTimeoutMs: options.waitToolDefaultTimeoutMs,
383
397
  capabilityCeiling: options.capabilityCeiling,
384
398
  thinkingCeiling: options.thinkingCeiling,
385
399
  extensionBindings: options.extensionBindings,
@@ -390,9 +404,10 @@ async function runSingleAttempt(
390
404
  shared.launchWarnings.emitted = true;
391
405
  }
392
406
 
393
- const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
407
+ const effectiveSystemPrompt = shared.systemPrompt;
394
408
  const toolPlan = resolvePiLaunchToolPlan({
395
409
  tools: agent.tools,
410
+ allowNestedSubagents: agent.allowNestedSubagents,
396
411
  extensions: agent.extensions,
397
412
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
398
413
  mcpDirectTools: agent.mcpDirectTools,
@@ -425,6 +440,7 @@ async function runSingleAttempt(
425
440
  index: options.index ?? 0,
426
441
  agent: agent.name,
427
442
  task,
443
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
428
444
  messages: [],
429
445
  finalOutput: "",
430
446
  exitCode: 1,
@@ -465,6 +481,7 @@ async function runSingleAttempt(
465
481
  index: options.index ?? 0,
466
482
  agent: agent.name,
467
483
  task: shared.originalTask ?? task,
484
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
468
485
  ...(options.agentContract ? { agentContract: options.agentContract } : {}),
469
486
  launchContractDigest,
470
487
  launchResolvedExtensions,
@@ -478,7 +495,6 @@ async function runSingleAttempt(
478
495
  transcriptPath: shared.transcriptWriter ? shared.artifactPaths?.transcriptPath : undefined,
479
496
  skills: shared.resolvedSkillNames,
480
497
  skillsWarning: shared.skillsWarning,
481
- ...(options.turnBudget ? { turnBudget: initialTurnBudgetState(options.turnBudget) } : {}),
482
498
  ...(options.toolBudget ? { toolBudget: initialToolBudgetState(options.toolBudget) } : {}),
483
499
  ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
484
500
  ...(capabilityAudit ? { capabilityAudit } : {}),
@@ -507,6 +523,7 @@ async function runSingleAttempt(
507
523
  const progress: AgentProgress = {
508
524
  index: options.index ?? 0,
509
525
  agent: agent.name,
526
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
510
527
  status: "running",
511
528
  task,
512
529
  skills: shared.resolvedSkillNames,
@@ -566,6 +583,7 @@ async function runSingleAttempt(
566
583
  return result;
567
584
  }
568
585
  }
586
+ let afterCompactionSettlement = false;
569
587
  const exitCode = await new Promise<number>((resolve) => {
570
588
  const proc = spawn(spawnSpec.command, spawnSpec.args, {
571
589
  cwd: options.cwd ?? runtimeCwd,
@@ -585,20 +603,7 @@ async function runSingleAttempt(
585
603
  let timeoutTimer: NodeJS.Timeout | undefined;
586
604
  let timeoutTerminationTimer: NodeJS.Timeout | undefined;
587
605
  let timeoutHardKillTimer: NodeJS.Timeout | undefined;
588
- let turnBudgetSoftReached = false;
589
- let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
590
- let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
591
606
  let protocolHardKillTimer: NodeJS.Timeout | undefined;
592
- const clearTurnBudgetTimers = () => {
593
- if (turnBudgetTerminationTimer) {
594
- clearTimeout(turnBudgetTerminationTimer);
595
- turnBudgetTerminationTimer = undefined;
596
- }
597
- if (turnBudgetHardKillTimer) {
598
- clearTimeout(turnBudgetHardKillTimer);
599
- turnBudgetHardKillTimer = undefined;
600
- }
601
- };
602
607
  const clearTimeoutTimers = () => {
603
608
  if (timeoutTimer) {
604
609
  clearTimeout(timeoutTimer);
@@ -661,6 +666,7 @@ async function runSingleAttempt(
661
666
  let forcedTerminationSignal = false;
662
667
  let cleanTerminalAssistantStopReceived = false;
663
668
  let agentSettledReceived = false;
669
+ let compactionStartedReceived = false;
664
670
  let finalDrainTimer: NodeJS.Timeout | undefined;
665
671
  let finalHardKillTimer: NodeJS.Timeout | undefined;
666
672
  let watchdogTailTimer: NodeJS.Timeout | undefined;
@@ -759,7 +765,6 @@ async function runSingleAttempt(
759
765
  clearStdioGuard();
760
766
  clearTimeoutTimers();
761
767
  clearAllToolTimeouts();
762
- clearTurnBudgetTimers();
763
768
  if (protocolHardKillTimer) {
764
769
  clearTimeout(protocolHardKillTimer);
765
770
  protocolHardKillTimer = undefined;
@@ -897,57 +902,6 @@ async function runSingleAttempt(
897
902
  }));
898
903
  return true;
899
904
  };
900
- const requestTurnBudgetAbort = (turnCount: number) => {
901
- const budget = options.turnBudget;
902
- if (!budget || result.timedOut || result.turnBudgetExceeded || interruptedByControl || processClosed || lifecycleFinished) return;
903
- const message = turnBudgetExceededMessage(budget, turnCount);
904
- result.turnBudgetExceeded = true;
905
- result.wrapUpRequested = true;
906
- result.turnBudget = turnBudgetState(budget, turnCount, true);
907
- result.error = message;
908
- result.finalOutput = message;
909
- progress.status = "failed";
910
- progress.error = message;
911
- progress.durationMs = Date.now() - startTime;
912
- fireUpdate();
913
- trySignalChild(proc, "SIGINT");
914
- turnBudgetTerminationTimer = setTimeout(() => {
915
- if (processClosed || lifecycleFinished || result.timedOut) return;
916
- trySignalChild(proc, "SIGTERM");
917
- }, 1000);
918
- turnBudgetTerminationTimer.unref?.();
919
- turnBudgetHardKillTimer = setTimeout(() => {
920
- if (processClosed || lifecycleFinished || result.timedOut) return;
921
- trySignalChild(proc, "SIGKILL");
922
- }, 4000);
923
- turnBudgetHardKillTimer.unref?.();
924
- };
925
-
926
- const updateTurnBudget = (turnCount: number, terminalAssistantStop: boolean, toolWorkActiveOrStarting: boolean) => {
927
- const budget = options.turnBudget;
928
- if (!budget || result.timedOut || result.turnBudgetExceeded) return;
929
- if (turnCount < budget.maxTurns) {
930
- result.turnBudget = { ...budget, outcome: "within-budget", turnCount };
931
- return;
932
- }
933
- if (!turnBudgetSoftReached) {
934
- turnBudgetSoftReached = true;
935
- result.wrapUpRequested = true;
936
- appendRecentOutput(progress, [turnBudgetSoftNote(budget, turnCount)]);
937
- }
938
- const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting, options.enforceHardTurnLimit);
939
- if (decision === "defer") {
940
- result.turnBudget = turnBudgetDeferredState(
941
- budget,
942
- turnCount,
943
- result.turnBudget?.terminationDeferredAtTurn,
944
- );
945
- return;
946
- }
947
- result.turnBudget = turnBudgetState(budget, turnCount, false);
948
- if (decision === "abort") requestTurnBudgetAbort(turnCount);
949
- };
950
-
951
905
  const updateActivityState = (now: number): boolean => {
952
906
  if (!controlConfig.enabled) return false;
953
907
  const idleState = deriveActivityState({
@@ -1001,7 +955,7 @@ async function runSingleAttempt(
1001
955
  const fireUpdate = () => {
1002
956
  if (!options.onUpdate || processClosed) return;
1003
957
  progress.durationMs = Date.now() - startTime;
1004
- const output = (result.timedOut || result.turnBudgetExceeded) && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
958
+ const output = result.timedOut && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
1005
959
  emitUpdateSnapshot(output || "(running...)");
1006
960
  };
1007
961
 
@@ -1021,8 +975,20 @@ async function runSingleAttempt(
1021
975
  }
1022
976
  shared.transcriptWriter?.writeChildEvent(evt);
1023
977
  shared.orcaProgressTab?.event(evt);
978
+ if (evt.type === "compaction_start") compactionStartedReceived = true;
979
+ if (evt.type === "compaction_end" && evt.willRetry === true) {
980
+ compactionStartedReceived = false;
981
+ afterCompactionSettlement = false;
982
+ }
983
+ if (evt.type === "agent_start" || evt.type === "auto_retry_start") {
984
+ compactionStartedReceived = false;
985
+ afterCompactionSettlement = false;
986
+ }
1024
987
  const lifecycleAction = projectChildLifecycle(evt, false, childLifecycleState);
1025
- if (evt.type === "agent_settled" && lifecycleAction === "start-drain") agentSettledReceived = true;
988
+ if (evt.type === "agent_settled" && lifecycleAction === "start-drain") {
989
+ agentSettledReceived = true;
990
+ afterCompactionSettlement = compactionStartedReceived;
991
+ }
1026
992
  applyChildLifecycle(lifecycleAction);
1027
993
 
1028
994
  if (isChildWatchdogStatusEvent(evt)) {
@@ -1094,16 +1060,11 @@ async function runSingleAttempt(
1094
1060
  if (evt.message.role === "assistant") {
1095
1061
  result.usage.turns++;
1096
1062
  progress.turnCount = result.usage.turns;
1097
- const stopReason = (evt.message as { stopReason?: string }).stopReason;
1098
1063
  const toolCalls = Array.isArray(evt.message.content)
1099
1064
  ? evt.message.content.filter((part) => (part as { type?: string }).type === "toolCall")
1100
1065
  : [];
1101
1066
  const hasToolCall = toolCalls.length > 0;
1102
- const terminalAssistantStop = stopReason === "stop" && !hasToolCall;
1103
- const terminalStructuredOutputCall = Boolean(options.structuredOutput)
1104
- && toolCalls.length === 1
1105
- && (toolCalls[0] as { name?: string }).name === "structured_output";
1106
- updateTurnBudget(result.usage.turns, terminalAssistantStop || terminalStructuredOutputCall, hasToolCall || Boolean(progress.currentTool));
1067
+ const terminalAssistantStop = (evt.message as { stopReason?: string }).stopReason === "stop" && !hasToolCall;
1107
1068
  const u = evt.message.usage;
1108
1069
  if (u) {
1109
1070
  const window = (u.input || 0) + (u.cacheRead || 0);
@@ -1363,14 +1324,17 @@ async function runSingleAttempt(
1363
1324
  const rawStdout = rawStdoutTail.text();
1364
1325
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
1365
1326
  const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1327
+ const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(result.messages ?? []);
1366
1328
  if (signal) result.processSignal = signal;
1329
+ if (!closeError && forcedDrainAfterEmptyTerminal && stderr.trim()) {
1330
+ closeError = stderr.trim();
1331
+ }
1367
1332
  if (!closeError && isUnexplainedProcessSignal({
1368
1333
  processSignal: signal,
1369
1334
  interrupted: result.interrupted,
1370
1335
  timedOut: result.timedOut,
1371
1336
  stopped: result.stopped,
1372
- turnBudgetExceeded: result.turnBudgetExceeded,
1373
- forcedDrainAfterFinalSuccess,
1337
+ forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1374
1338
  })) {
1375
1339
  closeError = formatProcessSignalError(signal!);
1376
1340
  }
@@ -1380,7 +1344,7 @@ async function runSingleAttempt(
1380
1344
  if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
1381
1345
  closeError = stderr.trim();
1382
1346
  }
1383
- const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
1347
+ const finalCode = forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
1384
1348
  if (!result.error && closeError) result.error = closeError;
1385
1349
  finish(finalCode);
1386
1350
  });
@@ -1452,6 +1416,9 @@ async function runSingleAttempt(
1452
1416
  }
1453
1417
  });
1454
1418
  result.exitCode = exitCode;
1419
+ if (afterCompactionSettlement) {
1420
+ (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT] = true;
1421
+ }
1455
1422
  if (interruptedByControl) {
1456
1423
  result.exitCode = 0;
1457
1424
  result.interrupted = true;
@@ -1472,7 +1439,6 @@ async function runSingleAttempt(
1472
1439
  && !abortedBySignal
1473
1440
  && !result.timedOut
1474
1441
  && !result.stopped
1475
- && !result.turnBudgetExceeded
1476
1442
  && !result.protocolError
1477
1443
  && !toolAvailabilityError) {
1478
1444
  const processExitCode = result.exitCode;
@@ -1525,9 +1491,12 @@ async function runSingleAttempt(
1525
1491
  : messages;
1526
1492
  const errInfo = detectSubagentError(errorMessages);
1527
1493
  const missingOutput = !finalText?.trim() && !validatedStructuredOutput;
1528
- if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
1494
+ const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
1495
+ && hasEmptyTerminalAssistantResponse(messages)
1496
+ && (progress.toolCount > 0 || Boolean(finalText?.trim()));
1497
+ if ((missingOutput || terminalEmptyAfterUsefulWork) && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
1529
1498
  result.exitCode = 1;
1530
- result.error = "Subagent produced no output (possible model cold-start or empty response).";
1499
+ result.error = formatEmptyTerminalAssistantResponseError(messages);
1531
1500
  } else if (errInfo.hasError) {
1532
1501
  result.exitCode = errInfo.exitCode ?? 1;
1533
1502
  result.error = errInfo.details
@@ -1546,6 +1515,7 @@ async function runSingleAttempt(
1546
1515
  }
1547
1516
 
1548
1517
  result.progressSummary = {
1518
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
1549
1519
  toolCount: progress.toolCount,
1550
1520
  tokens: progress.tokens,
1551
1521
  durationMs: progress.durationMs,
@@ -1558,9 +1528,17 @@ async function runSingleAttempt(
1558
1528
  result.outputState = fullOutput.trim() || result.structuredOutput !== undefined ? "present" : "absent";
1559
1529
  if (result.timedOut) {
1560
1530
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
1531
+ let requiredOutputMissing: boolean | undefined;
1532
+ if (options.outputMode === "file-only" && options.outputPath) {
1533
+ const outputChanged = hasSingleOutputChangedSinceSnapshot(options.outputPath, shared.outputSnapshot);
1534
+ requiredOutputMissing = outputChanged === undefined ? undefined : !outputChanged;
1535
+ } else if (options.structuredOutput) {
1536
+ requiredOutputMissing = !existsSync(options.structuredOutput.outputPath);
1537
+ }
1561
1538
  result.timeoutRecovery = buildTimeoutRecoverySummary({
1562
1539
  termination: "timed-out",
1563
1540
  evidence: mutationEvidence,
1541
+ requiredOutputMissing,
1564
1542
  currentTool: progress.currentTool,
1565
1543
  currentToolArgs: progress.currentToolArgs,
1566
1544
  currentPath: progress.currentPath,
@@ -1571,14 +1549,6 @@ async function runSingleAttempt(
1571
1549
  fullOutput = fullOutput.trim()
1572
1550
  ? `${timeoutMessage}\n\n${result.timeoutRecovery.message}\n\nPartial output before timeout:\n${fullOutput}`
1573
1551
  : `${timeoutMessage}\n\n${result.timeoutRecovery.message}`;
1574
- } else if (result.turnBudgetExceeded && result.turnBudget) {
1575
- fullOutput = formatTurnBudgetOutput(turnBudgetExceededMessage(result.turnBudget, result.turnBudget.turnCount), fullOutput);
1576
- } else if (result.turnBudget?.outcome === "termination-deferred") {
1577
- const note = turnBudgetDeferredNote(result.turnBudget, result.turnBudget.terminationDeferredAtTurn ?? result.turnBudget.turnCount);
1578
- fullOutput = fullOutput.trim() ? `${note}\n\n${fullOutput}` : note;
1579
- } else if (result.wrapUpRequested && result.turnBudget?.outcome === "wrap-up-requested") {
1580
- const note = turnBudgetSoftNote(result.turnBudget, result.turnBudget.wrapUpRequestedAtTurn ?? result.turnBudget.turnCount);
1581
- fullOutput = fullOutput.trim() ? `${note}\n\n${fullOutput}` : note;
1582
1552
  }
1583
1553
  const completionGuardEnabled = isAgentContractV1(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
1584
1554
  const completionGuard = ((result.exitCode === 0 && !result.error) || toolAvailabilityError) && completionGuardEnabled
@@ -1595,7 +1565,6 @@ async function runSingleAttempt(
1595
1565
  : undefined;
1596
1566
  const mutationAttemptObserved = observedMutationAttempt || mutationEvidence.attemptedMutation;
1597
1567
  let completionGuardTriggered = completionGuard?.triggered === true && !mutationAttemptObserved;
1598
- const completionGuardBlocked = completionGuard?.blocked === true;
1599
1568
  // The classifier is deliberately narrow, so a read-only review task can
1600
1569
  // still be misread as implementation. Arbitrate BEFORE any failure side
1601
1570
  // effect is published (effects, exit code, progress, notifications,
@@ -1612,31 +1581,26 @@ async function runSingleAttempt(
1612
1581
  completionGuardTriggered = arbitration.triggered;
1613
1582
  arbiterRescued = arbitration.rescued;
1614
1583
  }
1615
- if (completionGuard) {
1584
+ const completionEvidence = planCompletionEvidence({
1585
+ guard: completionGuard,
1586
+ guardTriggered: completionGuardTriggered,
1587
+ completionGuardEnabled,
1588
+ mutationCapable: hasMutationToolCapability(contractTools, toolPlan.effectiveMcpTools),
1589
+ implementationMutationExpected: expectsImplementationMutation(agent.name, shared.originalTask ?? task),
1590
+ mutationAttemptObserved,
1591
+ mutationEvidence,
1592
+ arbiterRescued,
1593
+ agentContractV1: isAgentContractV1(options.agentContract),
1594
+ });
1595
+ if (completionEvidence.fileMutation) {
1616
1596
  result.effects = {
1617
1597
  ...(result.effects ?? {}),
1618
- fileMutation: {
1619
- status: completionGuardBlocked
1620
- ? "blocked"
1621
- : completionGuard.expectedMutation
1622
- ? completionGuardTriggered
1623
- ? "missing"
1624
- : arbiterRescued
1625
- ? "not-applicable"
1626
- : "observed"
1627
- : "not-applicable",
1628
- expected: completionGuard.expectedMutation,
1629
- attempted: completionGuardBlocked ? false : completionGuard.attemptedMutation || mutationAttemptObserved,
1630
- evidence: mutationEvidence,
1631
- ...(completionGuardBlocked && completionGuard.message ? { message: completionGuard.message } : {}),
1632
- ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1633
- ...(arbiterRescued ? { resolvedBy: "llm-intent-arbiter" } : {}),
1634
- },
1598
+ fileMutation: completionEvidence.fileMutation,
1635
1599
  };
1636
1600
  }
1637
- if (completionGuardTriggered && !isAgentContractV1(options.agentContract)) {
1601
+ if (completionEvidence.legacyFailureError) {
1638
1602
  result.exitCode = 1;
1639
- result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
1603
+ result.error = completionEvidence.legacyFailureError;
1640
1604
  progress.status = "failed";
1641
1605
  progress.error = result.error;
1642
1606
  emitControlEvent(buildControlEvent({
@@ -1651,10 +1615,14 @@ async function runSingleAttempt(
1651
1615
  }));
1652
1616
  }
1653
1617
  if (options.outputPath && result.exitCode === 0) {
1654
- const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
1618
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot, options.outputClaimPath);
1655
1619
  fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
1656
1620
  result.savedOutputPath = resolvedOutput.savedPath;
1657
1621
  result.outputSaveError = resolvedOutput.saveError;
1622
+ if (resolvedOutput.fatalError) {
1623
+ result.exitCode = 1;
1624
+ result.error = result.error ? `${result.error}\n${resolvedOutput.saveError}` : resolvedOutput.saveError;
1625
+ }
1658
1626
  if (resolvedOutput.savedPath) {
1659
1627
  result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
1660
1628
  if (result.outputState === "absent") result.outputState = "unknown";
@@ -1711,6 +1679,7 @@ async function runSyncCompletionInner(
1711
1679
  ...options,
1712
1680
  capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])),
1713
1681
  };
1682
+ const childSessionName = deriveChildSessionName({ agent: agentName, task });
1714
1683
  const agent = agents.find((a) => a.name === agentName);
1715
1684
  if (!agent) {
1716
1685
  const diagnosticContext = options.unknownAgentDiagnosticContext
@@ -1953,12 +1922,16 @@ async function runSyncCompletionInner(
1953
1922
  // Escalated to "file" after an unexplained zero-activity startup failure so
1954
1923
  // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1955
1924
  let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1925
+ let abortRecoveryAttempted = false;
1926
+ let nextAttemptTask = taskWithAcceptance;
1956
1927
  modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1957
1928
  const candidate = modelsToTry[modelIndex];
1958
1929
  for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
1930
+ const recoveringAbort = abortRecoveryAttempted;
1931
+ const attemptTask = nextAttemptTask;
1959
1932
  const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
1960
1933
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1961
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, attemptOptions, {
1934
+ const result = await runSingleAttempt(runtimeCwd, agent, attemptTask, candidate, attemptOptions, {
1962
1935
  sessionEnabled,
1963
1936
  systemPrompt,
1964
1937
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
@@ -1978,7 +1951,7 @@ async function runSyncCompletionInner(
1978
1951
  verifyModel,
1979
1952
  });
1980
1953
  lastResult = result;
1981
- if (startupAttemptIndex === 0) {
1954
+ if (!recoveringAbort && startupAttemptIndex === 0) {
1982
1955
  if (result.model) attemptedModels.push(result.model);
1983
1956
  else if (candidate) attemptedModels.push(candidate);
1984
1957
  }
@@ -1994,11 +1967,42 @@ async function runSyncCompletionInner(
1994
1967
  usage: { ...result.usage },
1995
1968
  };
1996
1969
  modelAttempts.push(attempt);
1970
+ if (!attemptSucceeded) {
1971
+ const afterCompactionSettlement = (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT];
1972
+ const abortRecovery = planAbortRecovery({
1973
+ messages: result.messages ?? [],
1974
+ error: result.error,
1975
+ processSignal: result.processSignal,
1976
+ sessionAvailable: Boolean(options.sessionFile && existsSync(options.sessionFile)),
1977
+ alreadyResumed: abortRecoveryAttempted,
1978
+ stopped: result.stopped || result.detached || options.signal?.aborted,
1979
+ interrupted: result.interrupted || intercomDetached || options.interruptSignal?.aborted,
1980
+ timedOut: result.timedOut,
1981
+ toolBudgetExhausted: result.toolBudgetBlocked,
1982
+ usageBudgetExhausted: false,
1983
+ structuredOutputFailed: result.structuredOutputFailed,
1984
+ acceptanceFailed: false,
1985
+ currentTool: result.progress?.currentTool,
1986
+ afterCompactionSettlement,
1987
+ });
1988
+ if (abortRecovery.action === "resume") {
1989
+ abortRecoveryAttempted = true;
1990
+ nextAttemptTask = abortRecovery.prompt;
1991
+ attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
1992
+ continue;
1993
+ }
1994
+ if (abortRecovery.diagnostic) {
1995
+ result.error = result.error ? `${result.error}\n${abortRecovery.diagnostic}` : abortRecovery.diagnostic;
1996
+ attempt.error = result.error;
1997
+ break modelAttemptsLoop;
1998
+ }
1999
+ }
2000
+ if (recoveringAbort && !attemptSucceeded) break modelAttemptsLoop;
1997
2001
  if (options.workflowChildPermitLaunch && !attemptSucceeded) break modelAttemptsLoop;
1998
2002
  // Preserve the legacy intercom handoff contract: once this logical run has
1999
2003
  // been handed to a supervisor, terminating that attempt must not launch a
2000
2004
  // startup retry or model fallback. Explicit user detach retains fallback.
2001
- if (intercomDetached || result.timedOut || result.turnBudgetExceeded) break modelAttemptsLoop;
2005
+ if (intercomDetached || result.timedOut) break modelAttemptsLoop;
2002
2006
  if (attemptSucceeded) break modelAttemptsLoop;
2003
2007
 
2004
2008
  const startupFailure = isRetryableSubagentStartupFailure({
@@ -2015,7 +2019,6 @@ async function runSyncCompletionInner(
2015
2019
  interrupted: result.interrupted,
2016
2020
  timedOut: result.timedOut,
2017
2021
  stopped: result.stopped,
2018
- turnBudgetExceeded: result.turnBudgetExceeded,
2019
2022
  });
2020
2023
  const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2021
2024
  if (startupFailure && retryDelayMs !== undefined) {
@@ -2089,11 +2092,13 @@ async function runSyncCompletionInner(
2089
2092
  usage: emptyUsage(),
2090
2093
  error: "Subagent did not produce a result.",
2091
2094
  } satisfies SingleResult, options.context);
2095
+ result.task = task;
2092
2096
 
2093
2097
  result.usage = aggregateUsage;
2094
2098
  result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
2095
2099
  result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
2096
2100
  result.progressSummary = {
2101
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
2097
2102
  toolCount: totalToolCount,
2098
2103
  tokens: aggregateUsage.input + aggregateUsage.output,
2099
2104
  durationMs: totalDurationMs,
@@ -2147,8 +2152,6 @@ async function runSyncCompletionInner(
2147
2152
  result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." });
2148
2153
  } else if (result.timedOut) {
2149
2154
  result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." });
2150
- } else if (result.turnBudgetExceeded) {
2151
- result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." });
2152
2155
  } else {
2153
2156
  result.acceptance = await evaluateAcceptance({
2154
2157
  acceptance: effectiveAcceptance,
@@ -18,6 +18,7 @@ interface BeginForegroundChildInput {
18
18
 
19
19
  function copyProgress(target: ForegroundChildControl, progress: AgentProgress | undefined): void {
20
20
  if (!progress) return;
21
+ target.sessionName = progress.sessionName;
21
22
  target.currentActivityState = progress.activityState;
22
23
  target.lastActivityAt = progress.lastActivityAt;
23
24
  target.currentTool = progress.currentTool;
@@ -36,6 +37,7 @@ function copyProgress(target: ForegroundChildControl, progress: AgentProgress |
36
37
 
37
38
  function syncCurrentChild(control: ForegroundRunControl, child: ForegroundChildControl): void {
38
39
  control.currentAgent = child.agent;
40
+ control.sessionName = child.sessionName;
39
41
  control.currentIndex = child.index;
40
42
  control.description = child.description;
41
43
  control.currentActivityState = child.currentActivityState;
@@ -59,6 +61,7 @@ function syncCurrentChild(control: ForegroundRunControl, child: ForegroundChildC
59
61
 
60
62
  function clearCurrentChild(control: ForegroundRunControl): void {
61
63
  control.currentAgent = undefined;
64
+ control.sessionName = undefined;
62
65
  control.currentIndex = undefined;
63
66
  control.currentActivityState = undefined;
64
67
  control.lastActivityAt = undefined;
@@ -29,6 +29,7 @@ function compactChild(child: ForegroundResumeChild): ForegroundResumeChild {
29
29
  return {
30
30
  agent: child.agent,
31
31
  index: child.index,
32
+ ...(child.sessionName ? { sessionName: child.sessionName } : {}),
32
33
  ...(child.context ? { context: child.context } : {}),
33
34
  ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
34
35
  ...(child.model ? { model: child.model } : {}),