pi-subagents 0.57.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 (115) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/agents.md +18 -9
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +40 -1
  5. package/docs/models.md +27 -4
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +92 -6
  8. package/docs/workflows.md +115 -0
  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 +3 -1
  13. package/skills/pi-subagents/references/execution-controls.md +6 -2
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  16. package/src/agents/agent-management.ts +28 -17
  17. package/src/agents/agent-serializer.ts +9 -2
  18. package/src/agents/agents.ts +103 -27
  19. package/src/agents/runtime-agent-events.ts +70 -0
  20. package/src/agents/runtime-agent-registry.ts +19 -18
  21. package/src/api/agents.ts +10 -5
  22. package/src/api/background-work.ts +5 -1
  23. package/src/api/delegation.ts +0 -7
  24. package/src/api/preflight.ts +32 -10
  25. package/src/extension/fanout-child.ts +5 -3
  26. package/src/extension/index.ts +66 -27
  27. package/src/extension/public-execution.ts +15 -2
  28. package/src/extension/rpc.ts +35 -14
  29. package/src/extension/schemas.ts +37 -12
  30. package/src/extension/tool-description.ts +25 -6
  31. package/src/integrations/herdr-status.ts +5 -0
  32. package/src/intercom/result-intercom.ts +2 -0
  33. package/src/profiles/profiles.ts +5 -6
  34. package/src/runs/background/active-async-capacity.ts +2 -2
  35. package/src/runs/background/async-execution.ts +78 -46
  36. package/src/runs/background/async-job-tracker.ts +14 -12
  37. package/src/runs/background/async-resume.ts +29 -27
  38. package/src/runs/background/async-status-snapshot.ts +23 -261
  39. package/src/runs/background/async-status.ts +68 -7
  40. package/src/runs/background/chain-append.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +60 -8
  42. package/src/runs/background/fleet-view.ts +21 -11
  43. package/src/runs/background/notify.ts +184 -9
  44. package/src/runs/background/result-delivery-ownership.ts +45 -0
  45. package/src/runs/background/result-files.ts +2 -1
  46. package/src/runs/background/result-watcher.ts +29 -10
  47. package/src/runs/background/resume-guidance.ts +1 -1
  48. package/src/runs/background/retained-children.ts +1 -1
  49. package/src/runs/background/run-status.ts +18 -8
  50. package/src/runs/background/scheduled-runs.ts +86 -7
  51. package/src/runs/background/stale-run-reconciler.ts +10 -4
  52. package/src/runs/background/steering.ts +4 -14
  53. package/src/runs/background/subagent-runner.ts +389 -350
  54. package/src/runs/background/subagent-wait.ts +58 -10
  55. package/src/runs/background/terminal-run-index.ts +1 -1
  56. package/src/runs/background/wait-completions.ts +22 -1
  57. package/src/runs/background/wait-config.ts +23 -9
  58. package/src/runs/background/wait-tool.ts +9 -2
  59. package/src/runs/foreground/async-steering-action.ts +2 -2
  60. package/src/runs/foreground/execution.ts +134 -121
  61. package/src/runs/foreground/foreground-control.ts +3 -0
  62. package/src/runs/foreground/foreground-history.ts +1 -0
  63. package/src/runs/foreground/subagent-executor.ts +527 -257
  64. package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
  65. package/src/runs/shared/abort-recovery.ts +119 -0
  66. package/src/runs/shared/async-status-projection.ts +463 -0
  67. package/src/runs/shared/child-identity.ts +19 -4
  68. package/src/runs/shared/child-launch-plan.ts +151 -0
  69. package/src/runs/shared/completion-evidence.ts +89 -0
  70. package/src/runs/shared/completion-guard.ts +5 -4
  71. package/src/runs/shared/dynamic-fanout.ts +3 -3
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/host-step-status.ts +230 -0
  74. package/src/runs/shared/lane-metadata.ts +105 -0
  75. package/src/runs/shared/launch-cwd.ts +16 -0
  76. package/src/runs/shared/long-running-guard.ts +2 -1
  77. package/src/runs/shared/mcp-config-sources.ts +422 -0
  78. package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  79. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  80. package/src/runs/shared/model-exclusions.ts +12 -0
  81. package/src/runs/shared/model-fallback.ts +25 -6
  82. package/src/runs/shared/nested-events.ts +6 -2
  83. package/src/runs/shared/nested-render.ts +7 -3
  84. package/src/runs/shared/parallel-handoff.ts +419 -7
  85. package/src/runs/shared/parallel-utils.ts +15 -1
  86. package/src/runs/shared/pi-args.ts +81 -8
  87. package/src/runs/shared/single-output.ts +44 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  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 +11 -2
  94. package/src/shared/launch-contract.ts +5 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +240 -30
  97. package/src/shared/utils.ts +41 -84
  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/slash/subagents-admin.ts +3 -0
  104. package/src/tui/fleet-status.ts +125 -74
  105. package/src/tui/fleet.ts +11 -5
  106. package/src/tui/render.ts +330 -33
  107. package/src/watchdog/turn-delta.ts +1 -1
  108. package/src/workflows/chat-progress.ts +6 -3
  109. package/src/workflows/host-command.ts +230 -0
  110. package/src/workflows/scripted-workflow.ts +496 -35
  111. package/src/workflows/workflow-child-summary.ts +9 -5
  112. package/src/workflows/workflow-preflight.ts +270 -0
  113. package/src/workflows/workflow-receipt.ts +79 -5
  114. package/src/workflows/workflow-settlement.ts +246 -0
  115. 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,14 +56,18 @@ 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";
64
+ import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
61
65
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
62
66
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
63
67
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
64
68
  import { resolvePermissionRules } from "../shared/permissions.ts";
65
69
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
70
+ import { deriveChildSessionName } from "../../shared/child-session-name.ts";
66
71
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
67
72
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
68
73
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
@@ -102,7 +107,6 @@ import {
102
107
  import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport, validateAcceptanceInput } from "../shared/acceptance.ts";
103
108
  import { PROMPT_REDACTED } from "../../shared/utils.ts";
104
109
  import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
105
- import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
106
110
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
107
111
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
108
112
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
@@ -293,6 +297,9 @@ function snapshotStreamResult(result: SingleResult, progress: AgentProgress): Si
293
297
  return snapshot;
294
298
  }
295
299
 
300
+ const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
301
+ type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
302
+
296
303
  async function runSingleAttempt(
297
304
  runtimeCwd: string,
298
305
  agent: AgentConfig,
@@ -322,6 +329,10 @@ async function runSingleAttempt(
322
329
  assertThinkingWithinCeiling({ model: modelArg, configThinking: effectiveThinking, ceiling: options.thinkingCeiling, agent: agent.name, runId: options.runId });
323
330
  const expectedModelForVerification = shared.verifyModel ? modelArg : undefined;
324
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 });
325
336
  const watchdogConfig = resolveWatchdogConfig(options.cwd ?? runtimeCwd);
326
337
  const childWatchdog = watchdogConfig.ok
327
338
  ? resolveChildWatchdogConfig({
@@ -346,16 +357,19 @@ async function runSingleAttempt(
346
357
  thinking: effectiveThinking,
347
358
  systemPromptMode: agent.systemPromptMode,
348
359
  inheritProjectContext: agent.inheritProjectContext,
360
+ inheritGlobalContext: agent.inheritGlobalContext,
349
361
  inheritSkills: agent.inheritSkills,
350
362
  requireReadTool: Boolean(shared.resolvedSkillNames?.length),
351
363
  tools: agent.tools,
364
+ allowNestedSubagents: agent.allowNestedSubagents,
352
365
  extensions: agent.extensions,
353
366
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
354
- systemPrompt: appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget),
367
+ systemPrompt: shared.systemPrompt,
355
368
  mcpDirectTools: agent.mcpDirectTools,
356
369
  cwd: options.cwd ?? runtimeCwd,
357
370
  promptFileStem: agent.name,
358
371
  intercomSessionName: options.intercomSessionName,
372
+ sessionName: childSessionName,
359
373
  orchestratorIntercomTarget: options.orchestratorIntercomTarget,
360
374
  runId: options.runId,
361
375
  childAgentName: agent.name,
@@ -378,18 +392,21 @@ async function runSingleAttempt(
378
392
  permissionAuditPath,
379
393
  childWatchdog,
380
394
  waitToolEnabled: options.waitToolEnabled,
395
+ waitToolDefaultTimeoutMs: options.waitToolDefaultTimeoutMs,
381
396
  capabilityCeiling: options.capabilityCeiling,
382
397
  thinkingCeiling: options.thinkingCeiling,
383
398
  extensionBindings: options.extensionBindings,
399
+ runtimeSnapshotHost: options.runtimeSnapshotHost,
384
400
  });
385
401
  if (!shared.launchWarnings.emitted && warnings.length > 0) {
386
402
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
387
403
  shared.launchWarnings.emitted = true;
388
404
  }
389
405
 
390
- const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
406
+ const effectiveSystemPrompt = shared.systemPrompt;
391
407
  const toolPlan = resolvePiLaunchToolPlan({
392
408
  tools: agent.tools,
409
+ allowNestedSubagents: agent.allowNestedSubagents,
393
410
  extensions: agent.extensions,
394
411
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
395
412
  mcpDirectTools: agent.mcpDirectTools,
@@ -403,6 +420,7 @@ async function runSingleAttempt(
403
420
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
404
421
  agentName: agent.name,
405
422
  permissionRules,
423
+ runtimeSnapshotHost: options.runtimeSnapshotHost,
406
424
  });
407
425
  const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
408
426
  const contractError = validateImplementationToolContract({
@@ -421,6 +439,7 @@ async function runSingleAttempt(
421
439
  index: options.index ?? 0,
422
440
  agent: agent.name,
423
441
  task,
442
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
424
443
  messages: [],
425
444
  finalOutput: "",
426
445
  exitCode: 1,
@@ -446,6 +465,7 @@ async function runSingleAttempt(
446
465
  systemPrompt: effectiveSystemPrompt,
447
466
  systemPromptMode: agent.systemPromptMode,
448
467
  inheritProjectContext: agent.inheritProjectContext,
468
+ inheritGlobalContext: agent.inheritGlobalContext,
449
469
  inheritSkills: agent.inheritSkills,
450
470
  skills: shared.resolvedSkillNames ?? [],
451
471
  tools: toolPlan.effectiveToolAllowlist,
@@ -460,6 +480,7 @@ async function runSingleAttempt(
460
480
  index: options.index ?? 0,
461
481
  agent: agent.name,
462
482
  task: shared.originalTask ?? task,
483
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
463
484
  ...(options.agentContract ? { agentContract: options.agentContract } : {}),
464
485
  launchContractDigest,
465
486
  launchResolvedExtensions,
@@ -473,7 +494,6 @@ async function runSingleAttempt(
473
494
  transcriptPath: shared.transcriptWriter ? shared.artifactPaths?.transcriptPath : undefined,
474
495
  skills: shared.resolvedSkillNames,
475
496
  skillsWarning: shared.skillsWarning,
476
- ...(options.turnBudget ? { turnBudget: initialTurnBudgetState(options.turnBudget) } : {}),
477
497
  ...(options.toolBudget ? { toolBudget: initialToolBudgetState(options.toolBudget) } : {}),
478
498
  ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
479
499
  ...(capabilityAudit ? { capabilityAudit } : {}),
@@ -502,6 +522,7 @@ async function runSingleAttempt(
502
522
  const progress: AgentProgress = {
503
523
  index: options.index ?? 0,
504
524
  agent: agent.name,
525
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
505
526
  status: "running",
506
527
  task,
507
528
  skills: shared.resolvedSkillNames,
@@ -561,6 +582,7 @@ async function runSingleAttempt(
561
582
  return result;
562
583
  }
563
584
  }
585
+ let afterCompactionSettlement = false;
564
586
  const exitCode = await new Promise<number>((resolve) => {
565
587
  const proc = spawn(spawnSpec.command, spawnSpec.args, {
566
588
  cwd: options.cwd ?? runtimeCwd,
@@ -580,20 +602,7 @@ async function runSingleAttempt(
580
602
  let timeoutTimer: NodeJS.Timeout | undefined;
581
603
  let timeoutTerminationTimer: NodeJS.Timeout | undefined;
582
604
  let timeoutHardKillTimer: NodeJS.Timeout | undefined;
583
- let turnBudgetSoftReached = false;
584
- let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
585
- let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
586
605
  let protocolHardKillTimer: NodeJS.Timeout | undefined;
587
- const clearTurnBudgetTimers = () => {
588
- if (turnBudgetTerminationTimer) {
589
- clearTimeout(turnBudgetTerminationTimer);
590
- turnBudgetTerminationTimer = undefined;
591
- }
592
- if (turnBudgetHardKillTimer) {
593
- clearTimeout(turnBudgetHardKillTimer);
594
- turnBudgetHardKillTimer = undefined;
595
- }
596
- };
597
606
  const clearTimeoutTimers = () => {
598
607
  if (timeoutTimer) {
599
608
  clearTimeout(timeoutTimer);
@@ -656,6 +665,7 @@ async function runSingleAttempt(
656
665
  let forcedTerminationSignal = false;
657
666
  let cleanTerminalAssistantStopReceived = false;
658
667
  let agentSettledReceived = false;
668
+ let compactionStartedReceived = false;
659
669
  let finalDrainTimer: NodeJS.Timeout | undefined;
660
670
  let finalHardKillTimer: NodeJS.Timeout | undefined;
661
671
  let watchdogTailTimer: NodeJS.Timeout | undefined;
@@ -754,7 +764,6 @@ async function runSingleAttempt(
754
764
  clearStdioGuard();
755
765
  clearTimeoutTimers();
756
766
  clearAllToolTimeouts();
757
- clearTurnBudgetTimers();
758
767
  if (protocolHardKillTimer) {
759
768
  clearTimeout(protocolHardKillTimer);
760
769
  protocolHardKillTimer = undefined;
@@ -892,57 +901,6 @@ async function runSingleAttempt(
892
901
  }));
893
902
  return true;
894
903
  };
895
- const requestTurnBudgetAbort = (turnCount: number) => {
896
- const budget = options.turnBudget;
897
- if (!budget || result.timedOut || result.turnBudgetExceeded || interruptedByControl || processClosed || lifecycleFinished) return;
898
- const message = turnBudgetExceededMessage(budget, turnCount);
899
- result.turnBudgetExceeded = true;
900
- result.wrapUpRequested = true;
901
- result.turnBudget = turnBudgetState(budget, turnCount, true);
902
- result.error = message;
903
- result.finalOutput = message;
904
- progress.status = "failed";
905
- progress.error = message;
906
- progress.durationMs = Date.now() - startTime;
907
- fireUpdate();
908
- trySignalChild(proc, "SIGINT");
909
- turnBudgetTerminationTimer = setTimeout(() => {
910
- if (processClosed || lifecycleFinished || result.timedOut) return;
911
- trySignalChild(proc, "SIGTERM");
912
- }, 1000);
913
- turnBudgetTerminationTimer.unref?.();
914
- turnBudgetHardKillTimer = setTimeout(() => {
915
- if (processClosed || lifecycleFinished || result.timedOut) return;
916
- trySignalChild(proc, "SIGKILL");
917
- }, 4000);
918
- turnBudgetHardKillTimer.unref?.();
919
- };
920
-
921
- const updateTurnBudget = (turnCount: number, terminalAssistantStop: boolean, toolWorkActiveOrStarting: boolean) => {
922
- const budget = options.turnBudget;
923
- if (!budget || result.timedOut || result.turnBudgetExceeded) return;
924
- if (turnCount < budget.maxTurns) {
925
- result.turnBudget = { ...budget, outcome: "within-budget", turnCount };
926
- return;
927
- }
928
- if (!turnBudgetSoftReached) {
929
- turnBudgetSoftReached = true;
930
- result.wrapUpRequested = true;
931
- appendRecentOutput(progress, [turnBudgetSoftNote(budget, turnCount)]);
932
- }
933
- const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting, options.enforceHardTurnLimit);
934
- if (decision === "defer") {
935
- result.turnBudget = turnBudgetDeferredState(
936
- budget,
937
- turnCount,
938
- result.turnBudget?.terminationDeferredAtTurn,
939
- );
940
- return;
941
- }
942
- result.turnBudget = turnBudgetState(budget, turnCount, false);
943
- if (decision === "abort") requestTurnBudgetAbort(turnCount);
944
- };
945
-
946
904
  const updateActivityState = (now: number): boolean => {
947
905
  if (!controlConfig.enabled) return false;
948
906
  const idleState = deriveActivityState({
@@ -996,7 +954,7 @@ async function runSingleAttempt(
996
954
  const fireUpdate = () => {
997
955
  if (!options.onUpdate || processClosed) return;
998
956
  progress.durationMs = Date.now() - startTime;
999
- 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 ?? []);
1000
958
  emitUpdateSnapshot(output || "(running...)");
1001
959
  };
1002
960
 
@@ -1016,8 +974,20 @@ async function runSingleAttempt(
1016
974
  }
1017
975
  shared.transcriptWriter?.writeChildEvent(evt);
1018
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
+ }
1019
986
  const lifecycleAction = projectChildLifecycle(evt, false, childLifecycleState);
1020
- 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
+ }
1021
991
  applyChildLifecycle(lifecycleAction);
1022
992
 
1023
993
  if (isChildWatchdogStatusEvent(evt)) {
@@ -1064,7 +1034,7 @@ async function runSingleAttempt(
1064
1034
  if (options.toolBudget) {
1065
1035
  result.toolBudget = toolBudgetState(options.toolBudget, progress.toolCount);
1066
1036
  }
1067
- const mutates = isMutatingTool(evt.toolName, toolArgs);
1037
+ const mutates = isMutatingTool(evt.toolName, toolArgs, agent.mutationTools);
1068
1038
  observedMutationAttempt = observedMutationAttempt || mutates;
1069
1039
  pendingToolResult = { tool: evt.toolName ?? "tool", path: activeTool?.path, mutates, startedAt: now };
1070
1040
  fireUpdate();
@@ -1089,16 +1059,11 @@ async function runSingleAttempt(
1089
1059
  if (evt.message.role === "assistant") {
1090
1060
  result.usage.turns++;
1091
1061
  progress.turnCount = result.usage.turns;
1092
- const stopReason = (evt.message as { stopReason?: string }).stopReason;
1093
1062
  const toolCalls = Array.isArray(evt.message.content)
1094
1063
  ? evt.message.content.filter((part) => (part as { type?: string }).type === "toolCall")
1095
1064
  : [];
1096
1065
  const hasToolCall = toolCalls.length > 0;
1097
- const terminalAssistantStop = stopReason === "stop" && !hasToolCall;
1098
- const terminalStructuredOutputCall = Boolean(options.structuredOutput)
1099
- && toolCalls.length === 1
1100
- && (toolCalls[0] as { name?: string }).name === "structured_output";
1101
- updateTurnBudget(result.usage.turns, terminalAssistantStop || terminalStructuredOutputCall, hasToolCall || Boolean(progress.currentTool));
1066
+ const terminalAssistantStop = (evt.message as { stopReason?: string }).stopReason === "stop" && !hasToolCall;
1102
1067
  const u = evt.message.usage;
1103
1068
  if (u) {
1104
1069
  const window = (u.input || 0) + (u.cacheRead || 0);
@@ -1358,14 +1323,17 @@ async function runSingleAttempt(
1358
1323
  const rawStdout = rawStdoutTail.text();
1359
1324
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
1360
1325
  const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1326
+ const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(result.messages ?? []);
1361
1327
  if (signal) result.processSignal = signal;
1328
+ if (!closeError && forcedDrainAfterEmptyTerminal && stderr.trim()) {
1329
+ closeError = stderr.trim();
1330
+ }
1362
1331
  if (!closeError && isUnexplainedProcessSignal({
1363
1332
  processSignal: signal,
1364
1333
  interrupted: result.interrupted,
1365
1334
  timedOut: result.timedOut,
1366
1335
  stopped: result.stopped,
1367
- turnBudgetExceeded: result.turnBudgetExceeded,
1368
- forcedDrainAfterFinalSuccess,
1336
+ forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1369
1337
  })) {
1370
1338
  closeError = formatProcessSignalError(signal!);
1371
1339
  }
@@ -1375,7 +1343,7 @@ async function runSingleAttempt(
1375
1343
  if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
1376
1344
  closeError = stderr.trim();
1377
1345
  }
1378
- const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
1346
+ const finalCode = forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
1379
1347
  if (!result.error && closeError) result.error = closeError;
1380
1348
  finish(finalCode);
1381
1349
  });
@@ -1447,6 +1415,9 @@ async function runSingleAttempt(
1447
1415
  }
1448
1416
  });
1449
1417
  result.exitCode = exitCode;
1418
+ if (afterCompactionSettlement) {
1419
+ (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT] = true;
1420
+ }
1450
1421
  if (interruptedByControl) {
1451
1422
  result.exitCode = 0;
1452
1423
  result.interrupted = true;
@@ -1467,7 +1438,6 @@ async function runSingleAttempt(
1467
1438
  && !abortedBySignal
1468
1439
  && !result.timedOut
1469
1440
  && !result.stopped
1470
- && !result.turnBudgetExceeded
1471
1441
  && !result.protocolError
1472
1442
  && !toolAvailabilityError) {
1473
1443
  const processExitCode = result.exitCode;
@@ -1520,9 +1490,12 @@ async function runSingleAttempt(
1520
1490
  : messages;
1521
1491
  const errInfo = detectSubagentError(errorMessages);
1522
1492
  const missingOutput = !finalText?.trim() && !validatedStructuredOutput;
1523
- 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))) {
1524
1497
  result.exitCode = 1;
1525
- result.error = "Subagent produced no output (possible model cold-start or empty response).";
1498
+ result.error = formatEmptyTerminalAssistantResponseError(messages);
1526
1499
  } else if (errInfo.hasError) {
1527
1500
  result.exitCode = errInfo.exitCode ?? 1;
1528
1501
  result.error = errInfo.details
@@ -1541,6 +1514,7 @@ async function runSingleAttempt(
1541
1514
  }
1542
1515
 
1543
1516
  result.progressSummary = {
1517
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
1544
1518
  toolCount: progress.toolCount,
1545
1519
  tokens: progress.tokens,
1546
1520
  durationMs: progress.durationMs,
@@ -1566,14 +1540,6 @@ async function runSingleAttempt(
1566
1540
  fullOutput = fullOutput.trim()
1567
1541
  ? `${timeoutMessage}\n\n${result.timeoutRecovery.message}\n\nPartial output before timeout:\n${fullOutput}`
1568
1542
  : `${timeoutMessage}\n\n${result.timeoutRecovery.message}`;
1569
- } else if (result.turnBudgetExceeded && result.turnBudget) {
1570
- fullOutput = formatTurnBudgetOutput(turnBudgetExceededMessage(result.turnBudget, result.turnBudget.turnCount), fullOutput);
1571
- } else if (result.turnBudget?.outcome === "termination-deferred") {
1572
- const note = turnBudgetDeferredNote(result.turnBudget, result.turnBudget.terminationDeferredAtTurn ?? result.turnBudget.turnCount);
1573
- fullOutput = fullOutput.trim() ? `${note}\n\n${fullOutput}` : note;
1574
- } else if (result.wrapUpRequested && result.turnBudget?.outcome === "wrap-up-requested") {
1575
- const note = turnBudgetSoftNote(result.turnBudget, result.turnBudget.wrapUpRequestedAtTurn ?? result.turnBudget.turnCount);
1576
- fullOutput = fullOutput.trim() ? `${note}\n\n${fullOutput}` : note;
1577
1543
  }
1578
1544
  const completionGuardEnabled = isAgentContractV1(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
1579
1545
  const completionGuard = ((result.exitCode === 0 && !result.error) || toolAvailabilityError) && completionGuardEnabled
@@ -1583,13 +1549,13 @@ async function runSingleAttempt(
1583
1549
  messages: result.messages ?? [],
1584
1550
  tools: contractTools,
1585
1551
  mcpDirectTools: toolPlan.effectiveMcpTools,
1552
+ mutationTools: agent.mutationTools,
1586
1553
  toolAvailabilityError,
1587
1554
  mutationEvidence,
1588
1555
  })
1589
1556
  : undefined;
1590
1557
  const mutationAttemptObserved = observedMutationAttempt || mutationEvidence.attemptedMutation;
1591
1558
  let completionGuardTriggered = completionGuard?.triggered === true && !mutationAttemptObserved;
1592
- const completionGuardBlocked = completionGuard?.blocked === true;
1593
1559
  // The classifier is deliberately narrow, so a read-only review task can
1594
1560
  // still be misread as implementation. Arbitrate BEFORE any failure side
1595
1561
  // effect is published (effects, exit code, progress, notifications,
@@ -1606,31 +1572,26 @@ async function runSingleAttempt(
1606
1572
  completionGuardTriggered = arbitration.triggered;
1607
1573
  arbiterRescued = arbitration.rescued;
1608
1574
  }
1609
- 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) {
1610
1587
  result.effects = {
1611
1588
  ...(result.effects ?? {}),
1612
- fileMutation: {
1613
- status: completionGuardBlocked
1614
- ? "blocked"
1615
- : completionGuard.expectedMutation
1616
- ? completionGuardTriggered
1617
- ? "missing"
1618
- : arbiterRescued
1619
- ? "not-applicable"
1620
- : "observed"
1621
- : "not-applicable",
1622
- expected: completionGuard.expectedMutation,
1623
- attempted: completionGuardBlocked ? false : completionGuard.attemptedMutation || mutationAttemptObserved,
1624
- evidence: mutationEvidence,
1625
- ...(completionGuardBlocked && completionGuard.message ? { message: completionGuard.message } : {}),
1626
- ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1627
- ...(arbiterRescued ? { resolvedBy: "llm-intent-arbiter" } : {}),
1628
- },
1589
+ fileMutation: completionEvidence.fileMutation,
1629
1590
  };
1630
1591
  }
1631
- if (completionGuardTriggered && !isAgentContractV1(options.agentContract)) {
1592
+ if (completionEvidence.legacyFailureError) {
1632
1593
  result.exitCode = 1;
1633
- 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;
1634
1595
  progress.status = "failed";
1635
1596
  progress.error = result.error;
1636
1597
  emitControlEvent(buildControlEvent({
@@ -1645,10 +1606,14 @@ async function runSingleAttempt(
1645
1606
  }));
1646
1607
  }
1647
1608
  if (options.outputPath && result.exitCode === 0) {
1648
- const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
1609
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot, options.outputClaimPath);
1649
1610
  fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
1650
1611
  result.savedOutputPath = resolvedOutput.savedPath;
1651
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
+ }
1652
1617
  if (resolvedOutput.savedPath) {
1653
1618
  result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
1654
1619
  if (result.outputState === "absent") result.outputState = "unknown";
@@ -1688,10 +1653,24 @@ async function runSyncCompletionInner(
1688
1653
  task: string,
1689
1654
  options: RunSyncOptions,
1690
1655
  ): Promise<SingleResult> {
1656
+ const effectiveCwd = options.cwd ?? runtimeCwd;
1657
+ const cwdError = preflightLaunchCwd(options.requestedCwd ?? effectiveCwd, effectiveCwd);
1658
+ if (cwdError) {
1659
+ return redactResultPrompt(withRunContext({
1660
+ index: options.index ?? 0,
1661
+ agent: agentName,
1662
+ task,
1663
+ exitCode: 1,
1664
+ messages: [],
1665
+ usage: emptyUsage(),
1666
+ error: cwdError,
1667
+ }, options.context));
1668
+ }
1691
1669
  options = {
1692
1670
  ...options,
1693
1671
  capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])),
1694
1672
  };
1673
+ const childSessionName = deriveChildSessionName({ agent: agentName, task });
1695
1674
  const agent = agents.find((a) => a.name === agentName);
1696
1675
  if (!agent) {
1697
1676
  const diagnosticContext = options.unknownAgentDiagnosticContext
@@ -1934,12 +1913,16 @@ async function runSyncCompletionInner(
1934
1913
  // Escalated to "file" after an unexplained zero-activity startup failure so
1935
1914
  // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1936
1915
  let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1916
+ let abortRecoveryAttempted = false;
1917
+ let nextAttemptTask = taskWithAcceptance;
1937
1918
  modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1938
1919
  const candidate = modelsToTry[modelIndex];
1939
1920
  for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
1921
+ const recoveringAbort = abortRecoveryAttempted;
1922
+ const attemptTask = nextAttemptTask;
1940
1923
  const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
1941
1924
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1942
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, attemptOptions, {
1925
+ const result = await runSingleAttempt(runtimeCwd, agent, attemptTask, candidate, attemptOptions, {
1943
1926
  sessionEnabled,
1944
1927
  systemPrompt,
1945
1928
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
@@ -1959,7 +1942,7 @@ async function runSyncCompletionInner(
1959
1942
  verifyModel,
1960
1943
  });
1961
1944
  lastResult = result;
1962
- if (startupAttemptIndex === 0) {
1945
+ if (!recoveringAbort && startupAttemptIndex === 0) {
1963
1946
  if (result.model) attemptedModels.push(result.model);
1964
1947
  else if (candidate) attemptedModels.push(candidate);
1965
1948
  }
@@ -1975,11 +1958,42 @@ async function runSyncCompletionInner(
1975
1958
  usage: { ...result.usage },
1976
1959
  };
1977
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;
1978
1992
  if (options.workflowChildPermitLaunch && !attemptSucceeded) break modelAttemptsLoop;
1979
1993
  // Preserve the legacy intercom handoff contract: once this logical run has
1980
1994
  // been handed to a supervisor, terminating that attempt must not launch a
1981
1995
  // startup retry or model fallback. Explicit user detach retains fallback.
1982
- if (intercomDetached || result.timedOut || result.turnBudgetExceeded) break modelAttemptsLoop;
1996
+ if (intercomDetached || result.timedOut) break modelAttemptsLoop;
1983
1997
  if (attemptSucceeded) break modelAttemptsLoop;
1984
1998
 
1985
1999
  const startupFailure = isRetryableSubagentStartupFailure({
@@ -1996,7 +2010,6 @@ async function runSyncCompletionInner(
1996
2010
  interrupted: result.interrupted,
1997
2011
  timedOut: result.timedOut,
1998
2012
  stopped: result.stopped,
1999
- turnBudgetExceeded: result.turnBudgetExceeded,
2000
2013
  });
2001
2014
  const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2002
2015
  if (startupFailure && retryDelayMs !== undefined) {
@@ -2070,11 +2083,13 @@ async function runSyncCompletionInner(
2070
2083
  usage: emptyUsage(),
2071
2084
  error: "Subagent did not produce a result.",
2072
2085
  } satisfies SingleResult, options.context);
2086
+ result.task = task;
2073
2087
 
2074
2088
  result.usage = aggregateUsage;
2075
2089
  result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
2076
2090
  result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
2077
2091
  result.progressSummary = {
2092
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
2078
2093
  toolCount: totalToolCount,
2079
2094
  tokens: aggregateUsage.input + aggregateUsage.output,
2080
2095
  durationMs: totalDurationMs,
@@ -2128,8 +2143,6 @@ async function runSyncCompletionInner(
2128
2143
  result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." });
2129
2144
  } else if (result.timedOut) {
2130
2145
  result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." });
2131
- } else if (result.turnBudgetExceeded) {
2132
- result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." });
2133
2146
  } else {
2134
2147
  result.acceptance = await evaluateAcceptance({
2135
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 } : {}),