pi-subagents 0.58.0 → 0.59.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 (105) hide show
  1. package/CHANGELOG.md +65 -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 +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. 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";
@@ -64,6 +67,7 @@ import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-prog
64
67
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
65
68
  import { resolvePermissionRules } from "../shared/permissions.ts";
66
69
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, 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";
@@ -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,
@@ -380,6 +392,7 @@ async function runSingleAttempt(
380
392
  permissionAuditPath,
381
393
  childWatchdog,
382
394
  waitToolEnabled: options.waitToolEnabled,
395
+ waitToolDefaultTimeoutMs: options.waitToolDefaultTimeoutMs,
383
396
  capabilityCeiling: options.capabilityCeiling,
384
397
  thinkingCeiling: options.thinkingCeiling,
385
398
  extensionBindings: options.extensionBindings,
@@ -390,9 +403,10 @@ async function runSingleAttempt(
390
403
  shared.launchWarnings.emitted = true;
391
404
  }
392
405
 
393
- const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
406
+ const effectiveSystemPrompt = shared.systemPrompt;
394
407
  const toolPlan = resolvePiLaunchToolPlan({
395
408
  tools: agent.tools,
409
+ allowNestedSubagents: agent.allowNestedSubagents,
396
410
  extensions: agent.extensions,
397
411
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
398
412
  mcpDirectTools: agent.mcpDirectTools,
@@ -425,6 +439,7 @@ async function runSingleAttempt(
425
439
  index: options.index ?? 0,
426
440
  agent: agent.name,
427
441
  task,
442
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
428
443
  messages: [],
429
444
  finalOutput: "",
430
445
  exitCode: 1,
@@ -465,6 +480,7 @@ async function runSingleAttempt(
465
480
  index: options.index ?? 0,
466
481
  agent: agent.name,
467
482
  task: shared.originalTask ?? task,
483
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
468
484
  ...(options.agentContract ? { agentContract: options.agentContract } : {}),
469
485
  launchContractDigest,
470
486
  launchResolvedExtensions,
@@ -478,7 +494,6 @@ async function runSingleAttempt(
478
494
  transcriptPath: shared.transcriptWriter ? shared.artifactPaths?.transcriptPath : undefined,
479
495
  skills: shared.resolvedSkillNames,
480
496
  skillsWarning: shared.skillsWarning,
481
- ...(options.turnBudget ? { turnBudget: initialTurnBudgetState(options.turnBudget) } : {}),
482
497
  ...(options.toolBudget ? { toolBudget: initialToolBudgetState(options.toolBudget) } : {}),
483
498
  ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
484
499
  ...(capabilityAudit ? { capabilityAudit } : {}),
@@ -507,6 +522,7 @@ async function runSingleAttempt(
507
522
  const progress: AgentProgress = {
508
523
  index: options.index ?? 0,
509
524
  agent: agent.name,
525
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
510
526
  status: "running",
511
527
  task,
512
528
  skills: shared.resolvedSkillNames,
@@ -566,6 +582,7 @@ async function runSingleAttempt(
566
582
  return result;
567
583
  }
568
584
  }
585
+ let afterCompactionSettlement = false;
569
586
  const exitCode = await new Promise<number>((resolve) => {
570
587
  const proc = spawn(spawnSpec.command, spawnSpec.args, {
571
588
  cwd: options.cwd ?? runtimeCwd,
@@ -585,20 +602,7 @@ async function runSingleAttempt(
585
602
  let timeoutTimer: NodeJS.Timeout | undefined;
586
603
  let timeoutTerminationTimer: NodeJS.Timeout | undefined;
587
604
  let timeoutHardKillTimer: NodeJS.Timeout | undefined;
588
- let turnBudgetSoftReached = false;
589
- let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
590
- let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
591
605
  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
606
  const clearTimeoutTimers = () => {
603
607
  if (timeoutTimer) {
604
608
  clearTimeout(timeoutTimer);
@@ -661,6 +665,7 @@ async function runSingleAttempt(
661
665
  let forcedTerminationSignal = false;
662
666
  let cleanTerminalAssistantStopReceived = false;
663
667
  let agentSettledReceived = false;
668
+ let compactionStartedReceived = false;
664
669
  let finalDrainTimer: NodeJS.Timeout | undefined;
665
670
  let finalHardKillTimer: NodeJS.Timeout | undefined;
666
671
  let watchdogTailTimer: NodeJS.Timeout | undefined;
@@ -759,7 +764,6 @@ async function runSingleAttempt(
759
764
  clearStdioGuard();
760
765
  clearTimeoutTimers();
761
766
  clearAllToolTimeouts();
762
- clearTurnBudgetTimers();
763
767
  if (protocolHardKillTimer) {
764
768
  clearTimeout(protocolHardKillTimer);
765
769
  protocolHardKillTimer = undefined;
@@ -897,57 +901,6 @@ async function runSingleAttempt(
897
901
  }));
898
902
  return true;
899
903
  };
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
904
  const updateActivityState = (now: number): boolean => {
952
905
  if (!controlConfig.enabled) return false;
953
906
  const idleState = deriveActivityState({
@@ -1001,7 +954,7 @@ async function runSingleAttempt(
1001
954
  const fireUpdate = () => {
1002
955
  if (!options.onUpdate || processClosed) return;
1003
956
  progress.durationMs = Date.now() - startTime;
1004
- const output = (result.timedOut || result.turnBudgetExceeded) && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
957
+ const output = result.timedOut && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
1005
958
  emitUpdateSnapshot(output || "(running...)");
1006
959
  };
1007
960
 
@@ -1021,8 +974,20 @@ async function runSingleAttempt(
1021
974
  }
1022
975
  shared.transcriptWriter?.writeChildEvent(evt);
1023
976
  shared.orcaProgressTab?.event(evt);
977
+ if (evt.type === "compaction_start") compactionStartedReceived = true;
978
+ if (evt.type === "compaction_end" && evt.willRetry === true) {
979
+ compactionStartedReceived = false;
980
+ afterCompactionSettlement = false;
981
+ }
982
+ if (evt.type === "agent_start" || evt.type === "auto_retry_start") {
983
+ compactionStartedReceived = false;
984
+ afterCompactionSettlement = false;
985
+ }
1024
986
  const lifecycleAction = projectChildLifecycle(evt, false, childLifecycleState);
1025
- if (evt.type === "agent_settled" && lifecycleAction === "start-drain") agentSettledReceived = true;
987
+ if (evt.type === "agent_settled" && lifecycleAction === "start-drain") {
988
+ agentSettledReceived = true;
989
+ afterCompactionSettlement = compactionStartedReceived;
990
+ }
1026
991
  applyChildLifecycle(lifecycleAction);
1027
992
 
1028
993
  if (isChildWatchdogStatusEvent(evt)) {
@@ -1094,16 +1059,11 @@ async function runSingleAttempt(
1094
1059
  if (evt.message.role === "assistant") {
1095
1060
  result.usage.turns++;
1096
1061
  progress.turnCount = result.usage.turns;
1097
- const stopReason = (evt.message as { stopReason?: string }).stopReason;
1098
1062
  const toolCalls = Array.isArray(evt.message.content)
1099
1063
  ? evt.message.content.filter((part) => (part as { type?: string }).type === "toolCall")
1100
1064
  : [];
1101
1065
  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));
1066
+ const terminalAssistantStop = (evt.message as { stopReason?: string }).stopReason === "stop" && !hasToolCall;
1107
1067
  const u = evt.message.usage;
1108
1068
  if (u) {
1109
1069
  const window = (u.input || 0) + (u.cacheRead || 0);
@@ -1363,14 +1323,17 @@ async function runSingleAttempt(
1363
1323
  const rawStdout = rawStdoutTail.text();
1364
1324
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
1365
1325
  const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1326
+ const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(result.messages ?? []);
1366
1327
  if (signal) result.processSignal = signal;
1328
+ if (!closeError && forcedDrainAfterEmptyTerminal && stderr.trim()) {
1329
+ closeError = stderr.trim();
1330
+ }
1367
1331
  if (!closeError && isUnexplainedProcessSignal({
1368
1332
  processSignal: signal,
1369
1333
  interrupted: result.interrupted,
1370
1334
  timedOut: result.timedOut,
1371
1335
  stopped: result.stopped,
1372
- turnBudgetExceeded: result.turnBudgetExceeded,
1373
- forcedDrainAfterFinalSuccess,
1336
+ forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1374
1337
  })) {
1375
1338
  closeError = formatProcessSignalError(signal!);
1376
1339
  }
@@ -1380,7 +1343,7 @@ async function runSingleAttempt(
1380
1343
  if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
1381
1344
  closeError = stderr.trim();
1382
1345
  }
1383
- const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
1346
+ const finalCode = forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
1384
1347
  if (!result.error && closeError) result.error = closeError;
1385
1348
  finish(finalCode);
1386
1349
  });
@@ -1452,6 +1415,9 @@ async function runSingleAttempt(
1452
1415
  }
1453
1416
  });
1454
1417
  result.exitCode = exitCode;
1418
+ if (afterCompactionSettlement) {
1419
+ (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT] = true;
1420
+ }
1455
1421
  if (interruptedByControl) {
1456
1422
  result.exitCode = 0;
1457
1423
  result.interrupted = true;
@@ -1472,7 +1438,6 @@ async function runSingleAttempt(
1472
1438
  && !abortedBySignal
1473
1439
  && !result.timedOut
1474
1440
  && !result.stopped
1475
- && !result.turnBudgetExceeded
1476
1441
  && !result.protocolError
1477
1442
  && !toolAvailabilityError) {
1478
1443
  const processExitCode = result.exitCode;
@@ -1525,9 +1490,12 @@ async function runSingleAttempt(
1525
1490
  : messages;
1526
1491
  const errInfo = detectSubagentError(errorMessages);
1527
1492
  const missingOutput = !finalText?.trim() && !validatedStructuredOutput;
1528
- if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
1493
+ const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
1494
+ && hasEmptyTerminalAssistantResponse(messages)
1495
+ && (progress.toolCount > 0 || Boolean(finalText?.trim()));
1496
+ if ((missingOutput || terminalEmptyAfterUsefulWork) && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
1529
1497
  result.exitCode = 1;
1530
- result.error = "Subagent produced no output (possible model cold-start or empty response).";
1498
+ result.error = formatEmptyTerminalAssistantResponseError(messages);
1531
1499
  } else if (errInfo.hasError) {
1532
1500
  result.exitCode = errInfo.exitCode ?? 1;
1533
1501
  result.error = errInfo.details
@@ -1546,6 +1514,7 @@ async function runSingleAttempt(
1546
1514
  }
1547
1515
 
1548
1516
  result.progressSummary = {
1517
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
1549
1518
  toolCount: progress.toolCount,
1550
1519
  tokens: progress.tokens,
1551
1520
  durationMs: progress.durationMs,
@@ -1571,14 +1540,6 @@ async function runSingleAttempt(
1571
1540
  fullOutput = fullOutput.trim()
1572
1541
  ? `${timeoutMessage}\n\n${result.timeoutRecovery.message}\n\nPartial output before timeout:\n${fullOutput}`
1573
1542
  : `${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
1543
  }
1583
1544
  const completionGuardEnabled = isAgentContractV1(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
1584
1545
  const completionGuard = ((result.exitCode === 0 && !result.error) || toolAvailabilityError) && completionGuardEnabled
@@ -1595,7 +1556,6 @@ async function runSingleAttempt(
1595
1556
  : undefined;
1596
1557
  const mutationAttemptObserved = observedMutationAttempt || mutationEvidence.attemptedMutation;
1597
1558
  let completionGuardTriggered = completionGuard?.triggered === true && !mutationAttemptObserved;
1598
- const completionGuardBlocked = completionGuard?.blocked === true;
1599
1559
  // The classifier is deliberately narrow, so a read-only review task can
1600
1560
  // still be misread as implementation. Arbitrate BEFORE any failure side
1601
1561
  // effect is published (effects, exit code, progress, notifications,
@@ -1612,31 +1572,26 @@ async function runSingleAttempt(
1612
1572
  completionGuardTriggered = arbitration.triggered;
1613
1573
  arbiterRescued = arbitration.rescued;
1614
1574
  }
1615
- if (completionGuard) {
1575
+ const completionEvidence = planCompletionEvidence({
1576
+ guard: completionGuard,
1577
+ guardTriggered: completionGuardTriggered,
1578
+ completionGuardEnabled,
1579
+ mutationCapable: hasMutationToolCapability(contractTools, toolPlan.effectiveMcpTools),
1580
+ implementationMutationExpected: expectsImplementationMutation(agent.name, shared.originalTask ?? task),
1581
+ mutationAttemptObserved,
1582
+ mutationEvidence,
1583
+ arbiterRescued,
1584
+ agentContractV1: isAgentContractV1(options.agentContract),
1585
+ });
1586
+ if (completionEvidence.fileMutation) {
1616
1587
  result.effects = {
1617
1588
  ...(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
- },
1589
+ fileMutation: completionEvidence.fileMutation,
1635
1590
  };
1636
1591
  }
1637
- if (completionGuardTriggered && !isAgentContractV1(options.agentContract)) {
1592
+ if (completionEvidence.legacyFailureError) {
1638
1593
  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.";
1594
+ result.error = completionEvidence.legacyFailureError;
1640
1595
  progress.status = "failed";
1641
1596
  progress.error = result.error;
1642
1597
  emitControlEvent(buildControlEvent({
@@ -1651,10 +1606,14 @@ async function runSingleAttempt(
1651
1606
  }));
1652
1607
  }
1653
1608
  if (options.outputPath && result.exitCode === 0) {
1654
- const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
1609
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot, options.outputClaimPath);
1655
1610
  fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
1656
1611
  result.savedOutputPath = resolvedOutput.savedPath;
1657
1612
  result.outputSaveError = resolvedOutput.saveError;
1613
+ if (resolvedOutput.fatalError) {
1614
+ result.exitCode = 1;
1615
+ result.error = result.error ? `${result.error}\n${resolvedOutput.saveError}` : resolvedOutput.saveError;
1616
+ }
1658
1617
  if (resolvedOutput.savedPath) {
1659
1618
  result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
1660
1619
  if (result.outputState === "absent") result.outputState = "unknown";
@@ -1711,6 +1670,7 @@ async function runSyncCompletionInner(
1711
1670
  ...options,
1712
1671
  capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])),
1713
1672
  };
1673
+ const childSessionName = deriveChildSessionName({ agent: agentName, task });
1714
1674
  const agent = agents.find((a) => a.name === agentName);
1715
1675
  if (!agent) {
1716
1676
  const diagnosticContext = options.unknownAgentDiagnosticContext
@@ -1953,12 +1913,16 @@ async function runSyncCompletionInner(
1953
1913
  // Escalated to "file" after an unexplained zero-activity startup failure so
1954
1914
  // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1955
1915
  let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1916
+ let abortRecoveryAttempted = false;
1917
+ let nextAttemptTask = taskWithAcceptance;
1956
1918
  modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1957
1919
  const candidate = modelsToTry[modelIndex];
1958
1920
  for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
1921
+ const recoveringAbort = abortRecoveryAttempted;
1922
+ const attemptTask = nextAttemptTask;
1959
1923
  const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
1960
1924
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1961
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, attemptOptions, {
1925
+ const result = await runSingleAttempt(runtimeCwd, agent, attemptTask, candidate, attemptOptions, {
1962
1926
  sessionEnabled,
1963
1927
  systemPrompt,
1964
1928
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
@@ -1978,7 +1942,7 @@ async function runSyncCompletionInner(
1978
1942
  verifyModel,
1979
1943
  });
1980
1944
  lastResult = result;
1981
- if (startupAttemptIndex === 0) {
1945
+ if (!recoveringAbort && startupAttemptIndex === 0) {
1982
1946
  if (result.model) attemptedModels.push(result.model);
1983
1947
  else if (candidate) attemptedModels.push(candidate);
1984
1948
  }
@@ -1994,11 +1958,42 @@ async function runSyncCompletionInner(
1994
1958
  usage: { ...result.usage },
1995
1959
  };
1996
1960
  modelAttempts.push(attempt);
1961
+ if (!attemptSucceeded) {
1962
+ const afterCompactionSettlement = (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT];
1963
+ const abortRecovery = planAbortRecovery({
1964
+ messages: result.messages ?? [],
1965
+ error: result.error,
1966
+ processSignal: result.processSignal,
1967
+ sessionAvailable: Boolean(options.sessionFile && existsSync(options.sessionFile)),
1968
+ alreadyResumed: abortRecoveryAttempted,
1969
+ stopped: result.stopped || result.detached || options.signal?.aborted,
1970
+ interrupted: result.interrupted || intercomDetached || options.interruptSignal?.aborted,
1971
+ timedOut: result.timedOut,
1972
+ toolBudgetExhausted: result.toolBudgetBlocked,
1973
+ usageBudgetExhausted: false,
1974
+ structuredOutputFailed: result.structuredOutputFailed,
1975
+ acceptanceFailed: false,
1976
+ currentTool: result.progress?.currentTool,
1977
+ afterCompactionSettlement,
1978
+ });
1979
+ if (abortRecovery.action === "resume") {
1980
+ abortRecoveryAttempted = true;
1981
+ nextAttemptTask = abortRecovery.prompt;
1982
+ attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
1983
+ continue;
1984
+ }
1985
+ if (abortRecovery.diagnostic) {
1986
+ result.error = result.error ? `${result.error}\n${abortRecovery.diagnostic}` : abortRecovery.diagnostic;
1987
+ attempt.error = result.error;
1988
+ break modelAttemptsLoop;
1989
+ }
1990
+ }
1991
+ if (recoveringAbort && !attemptSucceeded) break modelAttemptsLoop;
1997
1992
  if (options.workflowChildPermitLaunch && !attemptSucceeded) break modelAttemptsLoop;
1998
1993
  // Preserve the legacy intercom handoff contract: once this logical run has
1999
1994
  // been handed to a supervisor, terminating that attempt must not launch a
2000
1995
  // startup retry or model fallback. Explicit user detach retains fallback.
2001
- if (intercomDetached || result.timedOut || result.turnBudgetExceeded) break modelAttemptsLoop;
1996
+ if (intercomDetached || result.timedOut) break modelAttemptsLoop;
2002
1997
  if (attemptSucceeded) break modelAttemptsLoop;
2003
1998
 
2004
1999
  const startupFailure = isRetryableSubagentStartupFailure({
@@ -2015,7 +2010,6 @@ async function runSyncCompletionInner(
2015
2010
  interrupted: result.interrupted,
2016
2011
  timedOut: result.timedOut,
2017
2012
  stopped: result.stopped,
2018
- turnBudgetExceeded: result.turnBudgetExceeded,
2019
2013
  });
2020
2014
  const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2021
2015
  if (startupFailure && retryDelayMs !== undefined) {
@@ -2089,11 +2083,13 @@ async function runSyncCompletionInner(
2089
2083
  usage: emptyUsage(),
2090
2084
  error: "Subagent did not produce a result.",
2091
2085
  } satisfies SingleResult, options.context);
2086
+ result.task = task;
2092
2087
 
2093
2088
  result.usage = aggregateUsage;
2094
2089
  result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
2095
2090
  result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
2096
2091
  result.progressSummary = {
2092
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
2097
2093
  toolCount: totalToolCount,
2098
2094
  tokens: aggregateUsage.input + aggregateUsage.output,
2099
2095
  durationMs: totalDurationMs,
@@ -2147,8 +2143,6 @@ async function runSyncCompletionInner(
2147
2143
  result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." });
2148
2144
  } else if (result.timedOut) {
2149
2145
  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
2146
  } else {
2153
2147
  result.acceptance = await evaluateAcceptance({
2154
2148
  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 } : {}),