pi-subagents 0.47.1 → 0.49.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 (66) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +1 -0
  4. package/docs/configuration.md +74 -7
  5. package/docs/missions.md +4 -2
  6. package/docs/observability.md +27 -3
  7. package/docs/tool-reference.md +1 -1
  8. package/package.json +1 -1
  9. package/src/agents/agents.ts +36 -15
  10. package/src/api/preflight.ts +1 -1
  11. package/src/extension/config.ts +26 -0
  12. package/src/extension/doctor.ts +40 -0
  13. package/src/extension/index.ts +39 -8
  14. package/src/extension/public-execution.ts +35 -4
  15. package/src/extension/rpc.ts +3 -9
  16. package/src/extension/schemas.ts +10 -9
  17. package/src/extension/tool-description.ts +10 -8
  18. package/src/inspectors/herdr/actions.ts +11 -2
  19. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  20. package/src/intercom/intercom-bridge.ts +7 -2
  21. package/src/missions/lifecycle.ts +4 -7
  22. package/src/missions/store.ts +12 -7
  23. package/src/missions/workflow-state.ts +6 -2
  24. package/src/runs/background/active-async-capacity.ts +431 -0
  25. package/src/runs/background/active-run-index.ts +9 -5
  26. package/src/runs/background/async-execution.ts +141 -65
  27. package/src/runs/background/async-job-tracker.ts +4 -0
  28. package/src/runs/background/async-resume.ts +18 -2
  29. package/src/runs/background/async-status.ts +11 -5
  30. package/src/runs/background/chain-append.ts +33 -15
  31. package/src/runs/background/fleet-view.ts +18 -5
  32. package/src/runs/background/owned-process-tree.ts +104 -0
  33. package/src/runs/background/process-terminal.ts +17 -3
  34. package/src/runs/background/resume-guidance.ts +27 -7
  35. package/src/runs/background/retained-children.ts +14 -6
  36. package/src/runs/background/run-status.ts +101 -5
  37. package/src/runs/background/stale-run-reconciler.ts +3 -3
  38. package/src/runs/background/subagent-runner.ts +60 -32
  39. package/src/runs/foreground/chain-execution.ts +37 -2
  40. package/src/runs/foreground/execution.ts +90 -19
  41. package/src/runs/foreground/foreground-control.ts +12 -0
  42. package/src/runs/foreground/prompt-audit.ts +172 -0
  43. package/src/runs/foreground/subagent-executor.ts +829 -203
  44. package/src/runs/shared/acceptance.ts +13 -4
  45. package/src/runs/shared/completion-guard.ts +17 -1
  46. package/src/runs/shared/llm-intent-arbiter.ts +302 -0
  47. package/src/runs/shared/parallel-utils.ts +2 -0
  48. package/src/runs/shared/pi-args.ts +44 -1
  49. package/src/runs/shared/run-fanout-budget.ts +280 -0
  50. package/src/runs/shared/single-output.ts +4 -2
  51. package/src/runs/shared/task-intent.ts +19 -3
  52. package/src/runs/shared/worktree.ts +17 -5
  53. package/src/shared/agent-stream-options.ts +5 -0
  54. package/src/shared/artifacts.ts +2 -6
  55. package/src/shared/display-text.ts +50 -0
  56. package/src/shared/node-executable.ts +21 -0
  57. package/src/shared/types.ts +115 -4
  58. package/src/shared/utils.ts +3 -1
  59. package/src/tui/fleet-status.ts +7 -5
  60. package/src/tui/fleet-transcript.ts +1 -48
  61. package/src/tui/fleet.ts +228 -13
  62. package/src/tui/render.ts +86 -39
  63. package/src/watchdog/permission-arbiter.ts +2 -1
  64. package/src/watchdog/review.ts +4 -3
  65. package/src/workflows/chat-progress.ts +8 -2
  66. package/src/workflows/scripted-workflow.ts +66 -10
@@ -25,11 +25,14 @@ import {
25
25
  type LaunchResolvedChildExtensionsV1,
26
26
  type RuntimeAcknowledgedChildExtensionsV1,
27
27
  type ModelAttempt,
28
+ type PiWriterProcessInstanceExitV1,
29
+ type ProcessTreeTerminalV1,
28
30
  type NestedRouteInfo,
29
31
  type NestedRunSummary,
30
32
  type ResolvedControlConfig,
31
33
  type ResolvedTurnBudget,
32
34
  type ResolvedToolBudget,
35
+ type RunFanoutBudgetDescriptor,
33
36
  type SubagentRunMode,
34
37
  type SubagentOutputState,
35
38
  type UsageBudgetConfig,
@@ -68,13 +71,14 @@ import {
68
71
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
69
72
  Semaphore,
70
73
  } from "../shared/parallel-utils.ts";
71
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
74
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
72
75
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
73
76
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
74
77
  import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
75
78
  import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
76
79
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
77
80
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
81
+ import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
78
82
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
79
83
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
80
84
  import {
@@ -85,9 +89,10 @@ import {
85
89
  waitForSubagentStartupRetry,
86
90
  } from "../shared/subagent-startup-retry.ts";
87
91
  import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
92
+ import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "./owned-process-tree.ts";
88
93
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
89
94
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
90
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
95
+ import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
91
96
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
92
97
  import {
93
98
  createMutatingFailureState,
@@ -138,6 +143,8 @@ import {
138
143
  type ChildWatchdogStateSnapshot,
139
144
  } from "../../watchdog/child-status.ts";
140
145
 
146
+ const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task completion.";
147
+
141
148
  interface SubagentRunConfig {
142
149
  id: string;
143
150
  steps: RunnerStep[];
@@ -177,6 +184,7 @@ interface SubagentRunConfig {
177
184
  /** Global cap on simultaneously-running subagent tasks within this run. */
178
185
  globalConcurrencyLimit?: number;
179
186
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
187
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
180
188
  launchContractDigest?: string;
181
189
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
182
190
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
@@ -232,7 +240,7 @@ interface StepResult {
232
240
  structuredOutputSchemaPath?: string;
233
241
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
234
242
  watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
235
- writerProcesses?: Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>;
243
+ writerProcesses?: PiWriterProcessInstanceExitV1[];
236
244
  writerAttemptCount?: number;
237
245
  runner?: ExternalCliRunnerStatus;
238
246
  externalProcess?: ExternalProcessStatus;
@@ -512,6 +520,7 @@ interface RunPiStreamingResult {
512
520
  processInstanceId: string;
513
521
  processCloseObservedAt?: number;
514
522
  processSignal?: string | null;
523
+ processTree: ProcessTreeTerminalV1;
515
524
  }
516
525
 
517
526
  function runPiStreaming(
@@ -548,7 +557,9 @@ function runPiStreaming(
548
557
  stdio: ["ignore", "pipe", "pipe"],
549
558
  env: spawnEnv,
550
559
  windowsHide: true,
560
+ detached: process.platform !== "win32",
551
561
  });
562
+ let processTreeController: OwnedProcessTreeController | undefined;
552
563
  const stderrTail = createBoundedByteTail();
553
564
  const rawStdoutTail = createBoundedByteTail();
554
565
  const messages: Message[] = [];
@@ -556,6 +567,7 @@ function runPiStreaming(
556
567
  let model: string | undefined;
557
568
  let writerRegistrationError: string | undefined;
558
569
  if (typeof child.pid === "number") {
570
+ processTreeController = createOwnedProcessTreeController(child.pid);
559
571
  try {
560
572
  onWriterProcess?.({ state: "running", pid: child.pid });
561
573
  } catch (writerError) {
@@ -703,7 +715,6 @@ function runPiStreaming(
703
715
  // a lingering stdio holder after `exit`, or a child that never exits.
704
716
  const FINAL_STOP_GRACE_MS = 1000;
705
717
  const HARD_KILL_MS = 3000;
706
- const TIMEOUT_HARD_KILL_MS = 3000;
707
718
  let childExited = false;
708
719
  let forcedTerminationSignal = false;
709
720
  let cleanTerminalAssistantStopReceived = false;
@@ -711,7 +722,6 @@ function runPiStreaming(
711
722
  let finalDrainTimer: NodeJS.Timeout | undefined;
712
723
  let finalHardKillTimer: NodeJS.Timeout | undefined;
713
724
  let watchdogTailTimer: NodeJS.Timeout | undefined;
714
- let timeoutHardKillTimer: NodeJS.Timeout | undefined;
715
725
  let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
716
726
  let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
717
727
  let protocolHardKillTimer: NodeJS.Timeout | undefined;
@@ -776,22 +786,16 @@ function runPiStreaming(
776
786
  timedOut = true;
777
787
  interrupted = false;
778
788
  error = timeoutMessage ?? "Subagent timed out.";
779
- trySignalChild(child, "SIGTERM");
780
- timeoutHardKillTimer = setTimeout(() => {
781
- if (!settled) trySignalChild(child, "SIGKILL");
782
- }, TIMEOUT_HARD_KILL_MS);
783
- timeoutHardKillTimer.unref?.();
789
+ if (processTreeController) void processTreeController.terminate();
790
+ else trySignalChild(child, "SIGTERM");
784
791
  });
785
792
  registerStop?.(() => {
786
793
  if (settled || timedOut || stopped) return;
787
794
  stopped = true;
788
795
  interrupted = false;
789
796
  error = stopMessage ?? "Subagent stopped by user.";
790
- trySignalChild(child, "SIGTERM");
791
- timeoutHardKillTimer = setTimeout(() => {
792
- if (!settled) trySignalChild(child, "SIGKILL");
793
- }, TIMEOUT_HARD_KILL_MS);
794
- timeoutHardKillTimer.unref?.();
797
+ if (processTreeController) void processTreeController.terminate();
798
+ else trySignalChild(child, "SIGTERM");
795
799
  });
796
800
  registerTurnBudgetAbort?.((message, state) => {
797
801
  if (settled || timedOut || stopped || turnBudgetExceeded) return;
@@ -820,10 +824,6 @@ function runPiStreaming(
820
824
  finalHardKillTimer = undefined;
821
825
  }
822
826
  clearWatchdogTailTimer();
823
- if (timeoutHardKillTimer) {
824
- clearTimeout(timeoutHardKillTimer);
825
- timeoutHardKillTimer = undefined;
826
- }
827
827
  if (turnBudgetTerminationTimer) {
828
828
  clearTimeout(turnBudgetTerminationTimer);
829
829
  turnBudgetTerminationTimer = undefined;
@@ -885,9 +885,12 @@ function runPiStreaming(
885
885
  childExited = true;
886
886
  clearDrainTimers();
887
887
  });
888
- child.on("close", (exitCode, signal) => {
888
+ child.on("close", async (exitCode, signal) => {
889
889
  settled = true;
890
890
  const processCloseObservedAt = Date.now();
891
+ const processTree = processTreeController
892
+ ? await processTreeController.finishAfterWriterClose()
893
+ : { state: "unknown" as const, reason: "verification-failed" as const, diagnostic: "Writer PID was unavailable." };
891
894
  try {
892
895
  onWriterProcess?.({ state: "none" });
893
896
  } catch {
@@ -939,6 +942,7 @@ function runPiStreaming(
939
942
  processInstanceId,
940
943
  processCloseObservedAt,
941
944
  processSignal: signal,
945
+ processTree,
942
946
  }));
943
947
  });
944
948
 
@@ -961,7 +965,7 @@ function runPiStreaming(
961
965
  const stderr = stderrTail.text();
962
966
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
963
967
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
964
- resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, structuredOutputToolInvoked, structuredOutputMessageStartIndex, watchdog: childWatchdogState, processInstanceId }));
968
+ resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, structuredOutputToolInvoked, structuredOutputMessageStartIndex, watchdog: childWatchdogState, processInstanceId, processTree: { state: "unknown", reason: "verification-failed", diagnostic: spawnErrorMessage } }));
965
969
  });
966
970
  });
967
971
  }
@@ -1105,6 +1109,7 @@ interface SingleStepContext {
1105
1109
  orchestratorIntercomTarget?: string;
1106
1110
  nestedRoute?: NestedRouteInfo;
1107
1111
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1112
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
1108
1113
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
1109
1114
  onChildEvent?: (event: ChildEvent) => void;
1110
1115
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
@@ -1206,7 +1211,7 @@ async function runSingleStep(
1206
1211
  artifactPaths = getArtifactPaths(ctx.artifactsDir, ctx.id, step.agent, index);
1207
1212
  fs.mkdirSync(ctx.artifactsDir, { recursive: true });
1208
1213
  if (ctx.artifactConfig?.includeInput !== false) {
1209
- fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
1214
+ fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${PROMPT_REDACTED}; live Prompt Audit only.\n`, "utf-8");
1210
1215
  }
1211
1216
  if (ctx.artifactConfig?.includeTranscript !== false) {
1212
1217
  transcriptWriter = createChildTranscriptWriter({
@@ -1219,7 +1224,7 @@ async function runSingleStep(
1219
1224
  });
1220
1225
  }
1221
1226
  }
1222
- transcriptWriter?.writeInitialUserMessage(task);
1227
+ transcriptWriter?.writeInitialUserMessage(`${PROMPT_REDACTED}; live Prompt Audit only.`);
1223
1228
 
1224
1229
  if (step.runner?.type === "external-cli") {
1225
1230
  const runner: ExternalCliRunnerStatus = {
@@ -1262,7 +1267,7 @@ async function runSingleStep(
1262
1267
  artifactPaths,
1263
1268
  artifactConfig: ctx.artifactConfig,
1264
1269
  output: formatOutputArtifactContent(omitUndefinedProperties({ output: resolvedOutput.fullOutput, error: external.error, metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath })),
1265
- metadata: { runId: ctx.id, agent: step.agent, task, runner, externalProcess: external.externalProcess, exitCode: external.exitCode, error: external.error, timestamp: Date.now() },
1270
+ metadata: { runId: ctx.id, agent: step.agent, task: PROMPT_REDACTED, runner, externalProcess: external.externalProcess, exitCode: external.exitCode, error: external.error, timestamp: Date.now() },
1266
1271
  })
1267
1272
  : {};
1268
1273
  return omitUndefinedProperties({
@@ -1292,7 +1297,7 @@ async function runSingleStep(
1292
1297
  let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1293
1298
  let launchResolvedExtensions = step.launchResolvedExtensions;
1294
1299
  const modelAttempts: ModelAttempt[] = [];
1295
- const writerProcesses: Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }> = [];
1300
+ const writerProcesses: PiWriterProcessInstanceExitV1[] = [];
1296
1301
  let writerAttemptCount = 0;
1297
1302
  const attemptNotes: string[] = [];
1298
1303
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
@@ -1306,6 +1311,9 @@ async function runSingleStep(
1306
1311
 
1307
1312
  let modelIndex = 0;
1308
1313
  let startupAttemptIndex = 0;
1314
+ // Escalated to "file" after an unexplained zero-activity startup failure so
1315
+ // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1316
+ let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1309
1317
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1310
1318
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1311
1319
  const candidate = candidates[modelIndex];
@@ -1331,6 +1339,7 @@ async function runSingleStep(
1331
1339
  parentSessionId: step.parentSessionId,
1332
1340
  baseArgs: ["--mode", "json", "-p"],
1333
1341
  task,
1342
+ taskDelivery: taskDeliveryOverride,
1334
1343
  sessionEnabled,
1335
1344
  sessionDir,
1336
1345
  sessionFile: step.sessionFile,
@@ -1356,6 +1365,10 @@ async function runSingleStep(
1356
1365
  parentControlInbox: ctx.nestedRoute?.controlInbox,
1357
1366
  parentRootRunId: ctx.nestedRoute?.rootRunId,
1358
1367
  parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
1368
+ runFanoutBudget: ctx.runFanoutBudget ? {
1369
+ ...ctx.runFanoutBudget,
1370
+ ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
1371
+ } : undefined,
1359
1372
  steerInboxDir: ctx.steerInboxDir,
1360
1373
  steerCapabilityPath: ctx.steerCapabilityPath,
1361
1374
  steerAckDir: ctx.steerAckDir,
@@ -1429,6 +1442,7 @@ async function runSingleStep(
1429
1442
  closeObservedAt: run.processCloseObservedAt,
1430
1443
  exitCode: run.exitCode,
1431
1444
  signal: run.processSignal ?? null,
1445
+ processTree: run.processTree,
1432
1446
  });
1433
1447
  }
1434
1448
  if (run.turnBudget) turnBudget = run.turnBudget;
@@ -1583,6 +1597,10 @@ async function runSingleStep(
1583
1597
  });
1584
1598
  const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [ctx.timeoutSignal, ctx.stopSignal]);
1585
1599
  if (!shouldRetry || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1600
+ if (!taskDeliveryOverride && run.processSignal === "SIGKILL") {
1601
+ taskDeliveryOverride = "file";
1602
+ attemptNotes.push("[startup-retry] retrying with file task delivery to keep the task text out of the child process argv.");
1603
+ }
1586
1604
  attempt.error = retryNote;
1587
1605
  attemptNotes.push(retryNote);
1588
1606
  startupAttemptIndex += 1;
@@ -1677,6 +1695,7 @@ async function runSingleStep(
1677
1695
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1678
1696
  const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
1679
1697
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1698
+ const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
1680
1699
  const effectiveFinalError = stoppedAfterAcceptance
1681
1700
  ? ctx.stopMessage ?? "Subagent stopped by user."
1682
1701
  : timedOutAfterAcceptance
@@ -1685,7 +1704,7 @@ async function runSingleStep(
1685
1704
  ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1686
1705
  : acceptanceCanFailRun
1687
1706
  ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1688
- : finalResult?.error;
1707
+ : finalResult?.error ?? (intercomDetachReceipt ? INTERCOM_DETACH_RECEIPT : undefined);
1689
1708
 
1690
1709
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1691
1710
  ? persistStepArtifacts({
@@ -1700,7 +1719,7 @@ async function runSingleStep(
1700
1719
  metadata: {
1701
1720
  runId: ctx.id,
1702
1721
  agent: step.agent,
1703
- task,
1722
+ task: PROMPT_REDACTED,
1704
1723
  exitCode: effectiveFinalExitCode,
1705
1724
  model: finalResult?.model,
1706
1725
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
@@ -2171,6 +2190,7 @@ async function runSubagent(
2171
2190
  ...(config.launchContractDigest ? { launchContractDigest: config.launchContractDigest } : {}),
2172
2191
  ...(config.launchResolvedExtensions ? { launchResolvedExtensions: config.launchResolvedExtensions } : {}),
2173
2192
  ...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
2193
+ ...(config.runFanoutBudget ? { runFanoutBudget: getRunFanoutBudgetSnapshot(config.runFanoutBudget) } : {}),
2174
2194
  ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
2175
2195
  ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
2176
2196
  ...(config.runnerProcessInstanceId ? { processTerminal: { version: 1 as const, state: "pending" as const, runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId } } : {}),
@@ -2260,10 +2280,10 @@ async function runSubagent(
2260
2280
  refreshWorkflowGraph();
2261
2281
  writeAtomicJson(statusPath, statusPayload);
2262
2282
  const activeState = isActiveAsyncState(statusPayload.state);
2263
- if (activeState !== lastIndexedActiveState) {
2283
+ if (activeState && activeState !== lastIndexedActiveState) {
2264
2284
  updateActiveRunIndex(asyncDir, statusPayload.state);
2265
- lastIndexedActiveState = activeState;
2266
2285
  }
2286
+ lastIndexedActiveState = activeState;
2267
2287
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
2268
2288
  };
2269
2289
  const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
@@ -3035,7 +3055,6 @@ async function runSubagent(
3035
3055
  if (stopped || timedOut || interrupted || statusPayload.state !== "running") return;
3036
3056
  stopped = true;
3037
3057
  const now = Date.now();
3038
- statusPayload.state = "stopped";
3039
3058
  statusPayload.stopped = true;
3040
3059
  statusPayload.error = stopMessage;
3041
3060
  currentActivityState = undefined;
@@ -3068,7 +3087,6 @@ async function runSubagent(
3068
3087
  timedOut = true;
3069
3088
  const now = Date.now();
3070
3089
  const message = timeoutMessage ?? "Subagent timed out.";
3071
- statusPayload.state = "failed";
3072
3090
  statusPayload.timedOut = true;
3073
3091
  statusPayload.error = message;
3074
3092
  currentActivityState = undefined;
@@ -3258,6 +3276,9 @@ async function runSubagent(
3258
3276
  throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} materialized ${materialized.parallel.length} items that resolve output to the same path: ${step.parallel.outputPath}. Remove the explicit output path or use an inherited relative agent output so each item can be isolated.`);
3259
3277
  }
3260
3278
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
3279
+ if (!config.runFanoutBudget) throw new Error("Async runner is missing its run fan-out budget identity.");
3280
+ const runFanoutBudget = claimRunFanoutBatch(config.runFanoutBudget, materialized.parallel.map((_, itemIndex) => `chain[${stepIndex}].expand[${itemIndex}]`));
3281
+ statusPayload.runFanoutBudget = runFanoutBudget;
3261
3282
  } catch (error) {
3262
3283
  const now = Date.now();
3263
3284
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
@@ -3364,6 +3385,7 @@ async function runSubagent(
3364
3385
  const materializedTask = step.parallel.namespaceOutputPath ? injectSingleOutputInstruction(taskText, outputPath, step.parallel) : taskText;
3365
3386
  return omitUndefinedProperties({
3366
3387
  ...step.parallel,
3388
+ runFanoutPath: `chain[${stepIndex}].expand[${itemIndex}]`,
3367
3389
  task: materializedTask,
3368
3390
  effectiveAcceptance: resolveEffectiveAcceptance(omitUndefinedProperties({
3369
3391
  explicit: step.parallel.acceptanceInput,
@@ -3523,6 +3545,7 @@ async function runSubagent(
3523
3545
  orchestratorIntercomTarget: config.controlIntercomTarget,
3524
3546
  nestedRoute: config.nestedRoute,
3525
3547
  capabilityCeiling: config.capabilityCeiling,
3548
+ runFanoutBudget: config.runFanoutBudget,
3526
3549
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
3527
3550
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3528
3551
  registerStop: (stop) => registerStepStop(fi, stop),
@@ -3905,6 +3928,7 @@ async function runSubagent(
3905
3928
  orchestratorIntercomTarget: config.controlIntercomTarget,
3906
3929
  nestedRoute: config.nestedRoute,
3907
3930
  capabilityCeiling: config.capabilityCeiling,
3931
+ runFanoutBudget: config.runFanoutBudget,
3908
3932
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
3909
3933
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3910
3934
  registerStop: (stop) => registerStepStop(fi, stop),
@@ -4194,6 +4218,7 @@ async function runSubagent(
4194
4218
  orchestratorIntercomTarget: config.controlIntercomTarget,
4195
4219
  nestedRoute: config.nestedRoute,
4196
4220
  capabilityCeiling: config.capabilityCeiling,
4221
+ runFanoutBudget: config.runFanoutBudget,
4197
4222
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
4198
4223
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
4199
4224
  registerStop: (stop) => registerStepStop(flatIndex, stop),
@@ -4438,6 +4463,9 @@ async function runSubagent(
4438
4463
  clearTimeout(timeoutTimer);
4439
4464
  timeoutTimer = undefined;
4440
4465
  }
4466
+ if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && results.some((result) => result.timedOut === true && result.error === timeoutMessage)) {
4467
+ timedOut = true;
4468
+ }
4441
4469
  const signalTerminated = !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4442
4470
  processSignal: result.processSignal,
4443
4471
  interrupted: result.interrupted,
@@ -4630,7 +4658,7 @@ async function runSubagent(
4630
4658
  shareError,
4631
4659
  }));
4632
4660
  if (config.runnerProcessInstanceId) {
4633
- const writers: Record<string, Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>> = {};
4661
+ const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
4634
4662
  const expectedWriters: Record<string, number> = {};
4635
4663
  for (const [index, result] of results.entries()) {
4636
4664
  writers[String(index)] = result.writerProcesses ?? [];
@@ -34,6 +34,7 @@ import {
34
34
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
35
35
  import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
36
36
  import { runSync } from "./execution.ts";
37
+ import { createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
37
38
  import { workflowForegroundSteeringLaunchOptions } from "./workflow-foreground-steering.ts";
38
39
  import {
39
40
  beginForegroundChild,
@@ -42,6 +43,7 @@ import {
42
43
  settleForegroundSchedulingOwner,
43
44
  updateForegroundChild,
44
45
  } from "./foreground-control.ts";
46
+ import { updateLiveEffectivePrompt } from "./prompt-audit.ts";
45
47
  import { buildChainSummary } from "../../shared/formatters.ts";
46
48
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
47
49
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
@@ -71,6 +73,7 @@ import {
71
73
  type ResolvedControlConfig,
72
74
  type ResolvedTurnBudget,
73
75
  type ResolvedToolBudget,
76
+ type RunFanoutBudgetDescriptor,
74
77
  type SingleResult,
75
78
  type ToolBudgetConfig,
76
79
  type ChainCheckpointState,
@@ -86,6 +89,7 @@ import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferen
86
89
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
87
90
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection, type DynamicCollectedResult } from "../shared/dynamic-fanout.ts";
88
91
  import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, resolveEffectiveAcceptance } from "../shared/acceptance.ts";
92
+ import { claimRunFanoutBatch, RunFanoutLimitError } from "../shared/run-fanout-budget.ts";
89
93
  import { isAgentContractV1 } from "../shared/agent-contract.ts";
90
94
  import type { ChainOutputMap } from "../../shared/types.ts";
91
95
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
@@ -167,6 +171,7 @@ interface ParallelChainRunInput {
167
171
  configToolBudget?: ToolBudgetConfig;
168
172
  globalSemaphore?: Semaphore;
169
173
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
174
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
170
175
  permissions?: PermissionConfig;
171
176
  dynamic?: boolean;
172
177
  }
@@ -368,7 +373,11 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
368
373
  beginForegroundChild(input.foregroundControl, {
369
374
  index: childIndex,
370
375
  agent: task.agent,
371
- description: cleanTask.trim(),
376
+ authoredTask: cleanTask,
377
+ effectivePrompt: taskStr,
378
+ cwd: taskCwd,
379
+ ...(outputPath ? { outputPath } : {}),
380
+ description: `${task.agent} child`,
372
381
  ...(effectiveModel ? { model: effectiveModel } : {}),
373
382
  ...(thinking ? { thinking } : {}),
374
383
  interrupt: () => {
@@ -388,8 +397,10 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
388
397
  result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
389
398
  permissions: input.permissions,
390
399
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
400
+ llmIntentArbiter: createTaskMutationArbiter(input.ctx),
391
401
  ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, childIndex),
392
402
  capabilityCeiling: input.capabilityCeiling,
403
+ runFanoutBudget: input.runFanoutBudget ? { ...input.runFanoutBudget, parentPath: `${input.runFanoutBudget.parentPath ? `${input.runFanoutBudget.parentPath}/` : ""}chain[${input.stepIndex}]${input.dynamic ? `.expand[${taskIndex}]` : `.parallel[${taskIndex}]`}` } : undefined,
393
404
  context: input.contextForAgent?.(task.agent),
394
405
  cwd: taskCwd,
395
406
  signal: input.signal,
@@ -422,6 +433,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
422
433
  agentContract,
423
434
  acceptance: task.acceptance,
424
435
  acceptanceContext: { mode: "chain", dynamic: input.dynamic && task.acceptance === undefined },
436
+ onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, childIndex, prompt) : undefined,
425
437
  timeoutMs: input.timeoutMs,
426
438
  deadlineAt: input.deadlineAt,
427
439
  turnBudget: input.turnBudget,
@@ -540,6 +552,7 @@ interface ChainExecutionParams {
540
552
  /** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
541
553
  globalConcurrencyLimit?: number;
542
554
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
555
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
543
556
  }
544
557
 
545
558
  interface ChainExecutionResult {
@@ -876,6 +889,7 @@ ${step.message}` : ""}` }],
876
889
  configToolBudget: params.configToolBudget,
877
890
  globalSemaphore,
878
891
  permissions: params.permissions,
892
+ runFanoutBudget: params.runFanoutBudget,
879
893
  });
880
894
  globalTaskIndex += step.parallel.length;
881
895
 
@@ -1002,6 +1016,19 @@ ${step.message}` : ""}` }],
1002
1016
  return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
1003
1017
  }
1004
1018
 
1019
+ try {
1020
+ if (params.runFanoutBudget) claimRunFanoutBatch(params.runFanoutBudget, materialized.parallel.map((_, itemIndex) => `chain[${stepIndex}].expand[${itemIndex}]`));
1021
+ } catch (error) {
1022
+ const message = error instanceof Error ? error.message : String(error);
1023
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
1024
+ const result = buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
1025
+ if (error instanceof RunFanoutLimitError) {
1026
+ result.details.runFanoutBudget = error.snapshot;
1027
+ result.details.runFanoutRejection = error.rejection;
1028
+ }
1029
+ return result;
1030
+ }
1031
+
1005
1032
  dynamicChildren[stepIndex] = materialized.items.map((item, itemIndex) => ({
1006
1033
  agent: step.parallel.agent,
1007
1034
  label: materialized.parallel[itemIndex]?.label,
@@ -1137,6 +1164,7 @@ ${step.message}` : ""}` }],
1137
1164
  configToolBudget: params.configToolBudget,
1138
1165
  globalSemaphore,
1139
1166
  permissions: params.permissions,
1167
+ runFanoutBudget: params.runFanoutBudget,
1140
1168
  dynamic: true,
1141
1169
  });
1142
1170
  globalTaskIndex = dynamicStartIndex + reservedDynamicItems;
@@ -1332,7 +1360,11 @@ ${step.message}` : ""}` }],
1332
1360
  beginForegroundChild(foregroundControl, {
1333
1361
  index: childIndex,
1334
1362
  agent: seqStep.agent,
1335
- description: cleanTask.trim(),
1363
+ authoredTask: cleanTask,
1364
+ effectivePrompt: stepTask,
1365
+ cwd,
1366
+ ...(outputPath ? { outputPath } : {}),
1367
+ description: `${seqStep.agent} child`,
1336
1368
  ...(effectiveModel ? { model: effectiveModel } : {}),
1337
1369
  ...(thinking ? { thinking } : {}),
1338
1370
  interrupt: () => {
@@ -1369,8 +1401,10 @@ ${step.message}` : ""}` }],
1369
1401
  r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1370
1402
  permissions: params.permissions,
1371
1403
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1404
+ llmIntentArbiter: createTaskMutationArbiter(ctx),
1372
1405
  ...workflowForegroundSteeringLaunchOptions(params.foregroundControl, childIndex),
1373
1406
  capabilityCeiling: params.capabilityCeiling,
1407
+ runFanoutBudget: params.runFanoutBudget ? { ...params.runFanoutBudget, parentPath: `${params.runFanoutBudget.parentPath ? `${params.runFanoutBudget.parentPath}/` : ""}chain[${stepIndex}]` } : undefined,
1374
1408
  context: params.contextForAgent?.(seqStep.agent),
1375
1409
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1376
1410
  signal,
@@ -1403,6 +1437,7 @@ ${step.message}` : ""}` }],
1403
1437
  agentContract,
1404
1438
  acceptance: seqStep.acceptance,
1405
1439
  acceptanceContext: { mode: "chain" },
1440
+ onEffectivePrompt: foregroundControl ? (prompt) => updateLiveEffectivePrompt(foregroundControl, childIndex, prompt) : undefined,
1406
1441
  timeoutMs: params.timeoutMs,
1407
1442
  deadlineAt,
1408
1443
  turnBudget: params.turnBudget,