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
@@ -19,9 +19,10 @@ import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isPara
19
19
  import type { RunnerStep } from "../shared/parallel-utils.ts";
20
20
  import type { ContextMode } from "../shared/context-mode.ts";
21
21
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
22
+ import { resolveNodeExecutable } from "../../shared/node-executable.ts";
22
23
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
23
24
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
24
- import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, resolveChildCwd } from "../../shared/utils.ts";
25
+ import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
25
26
  import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
26
27
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
27
28
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
@@ -30,17 +31,21 @@ import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
30
31
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
31
32
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
32
33
  import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
34
+ import { createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
33
35
  import {
34
36
  type AcceptanceInput,
35
37
  type AgentContract,
38
+ type AsyncStatus,
36
39
  type ArtifactConfig,
37
40
  type Details,
41
+ type IntercomBridgeConfig,
38
42
  type JsonSchemaObject,
39
43
  type MaxOutputConfig,
40
44
  type NestedRouteInfo,
41
45
  type ResolvedControlConfig,
42
46
  type ResolvedTurnBudget,
43
47
  type ResolvedToolBudget,
48
+ type RunFanoutBudgetDescriptor,
44
49
  type ToolBudgetConfig,
45
50
  type SubagentRunMode,
46
51
  type SteeringRecoveryDescriptor,
@@ -59,6 +64,7 @@ import { usageBudgetState } from "../shared/usage-budget.ts";
59
64
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
60
65
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
61
66
  import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
67
+ import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
62
68
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
63
69
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
64
70
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
@@ -171,8 +177,10 @@ interface AsyncChainParams {
171
177
  /** Global cap on simultaneously-running subagent tasks within the async run. */
172
178
  globalConcurrencyLimit?: number;
173
179
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
180
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
174
181
  parentWorkflowRunId?: string;
175
182
  workflowKey?: string;
183
+ activeAsyncCapacity?: ActiveAsyncCapacityHandle;
176
184
  }
177
185
 
178
186
  interface AsyncSingleParams {
@@ -181,6 +189,8 @@ interface AsyncSingleParams {
181
189
  /** Raw caller-facing goal used only by the started event. */
182
190
  goal?: string;
183
191
  agentConfig: AgentConfig;
192
+ /** Agent contract before per-run bridge injection, used only for recovery persistence. */
193
+ recoveryAgentConfig?: AgentConfig;
184
194
  ctx: AsyncExecutionContext;
185
195
  cwd?: string;
186
196
  maxOutput?: MaxOutputConfig;
@@ -208,6 +218,7 @@ interface AsyncSingleParams {
208
218
  worktreeSetupHookTimeoutMs?: number;
209
219
  worktreeBaseDir?: string;
210
220
  controlConfig?: ResolvedControlConfig;
221
+ intercomBridge?: IntercomBridgeConfig;
211
222
  controlIntercomTarget?: string;
212
223
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
213
224
  nestedRoute?: NestedRouteInfo;
@@ -220,8 +231,10 @@ interface AsyncSingleParams {
220
231
  configToolBudget?: ResolvedToolBudget;
221
232
  allowZeroToolBudget?: boolean;
222
233
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
234
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
223
235
  parentWorkflowRunId?: string;
224
236
  workflowKey?: string;
237
+ activeAsyncCapacity?: ActiveAsyncCapacityHandle;
225
238
  }
226
239
 
227
240
  interface AsyncExecutionResult {
@@ -291,27 +304,6 @@ export function isAsyncAvailable(): boolean {
291
304
  return jitiCliPath !== undefined;
292
305
  }
293
306
 
294
- function isNodeExecutableName(execPath: string): boolean {
295
- const basename = path.basename(execPath).toLowerCase();
296
- return basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe";
297
- }
298
-
299
- function canUseCurrentNodeExecutable(execPath: string): boolean {
300
- try {
301
- fs.accessSync(execPath, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
302
- return true;
303
- } catch {
304
- return false;
305
- }
306
- }
307
-
308
- function resolveAsyncRunnerNodeCommand(): string {
309
- if (isNodeExecutableName(process.execPath) && canUseCurrentNodeExecutable(process.execPath)) {
310
- return process.execPath;
311
- }
312
- return process.platform === "win32" ? "node.exe" : "node";
313
- }
314
-
315
307
  export function resolveAsyncRunnerLogPaths(cfg: object): { stdoutPath: string; stderrPath: string } | undefined {
316
308
  const asyncDir = typeof (cfg as { asyncDir?: unknown }).asyncDir === "string"
317
309
  ? (cfg as { asyncDir: string }).asyncDir
@@ -343,7 +335,7 @@ type RunnerStartupState = "ready" | "acknowledged";
343
335
 
344
336
  type RunnerStartupWaitResult =
345
337
  | { ok: true; token: string }
346
- | { ok: false; error: string };
338
+ | { ok: false; error: string; startupDidNotProceed?: boolean };
347
339
 
348
340
  function waitForStartupInterval(delayMs = 20): void {
349
341
  if (RUNNER_STARTUP_WAIT_VIEW) {
@@ -360,14 +352,14 @@ function readRunnerStartup(startupPath: string, expectedState: RunnerStartupStat
360
352
  if (!fs.existsSync(startupPath)) return undefined;
361
353
  try {
362
354
  const payload = JSON.parse(fs.readFileSync(startupPath, "utf-8")) as { state?: unknown; token?: unknown; error?: unknown };
363
- if (payload.state === "error" && typeof payload.error === "string") return { ok: false, error: payload.error };
355
+ if (payload.state === "error" && typeof payload.error === "string") return { ok: false, error: payload.error, startupDidNotProceed: true };
364
356
  if (payload.state !== expectedState) return undefined;
365
357
  if (typeof payload.token !== "string" || (expectedToken !== undefined && payload.token !== expectedToken)) {
366
- return { ok: false, error: `Async runner wrote an invalid ${expectedState} startup handshake: ${startupPath}` };
358
+ return { ok: false, error: `Async runner wrote an invalid ${expectedState} startup handshake: ${startupPath}`, startupDidNotProceed: true };
367
359
  }
368
360
  return { ok: true, token: payload.token };
369
361
  } catch (error) {
370
- return { ok: false, error: `Failed to read async runner startup handshake '${startupPath}': ${error instanceof Error ? error.message : String(error)}` };
362
+ return { ok: false, error: `Failed to read async runner startup handshake '${startupPath}': ${error instanceof Error ? error.message : String(error)}`, startupDidNotProceed: true };
371
363
  }
372
364
  }
373
365
 
@@ -381,7 +373,7 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
381
373
  }
382
374
  const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
383
375
  if (finalResult) return finalResult;
384
- return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'.` };
376
+ return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'.`, startupDidNotProceed: true };
385
377
  }
386
378
 
387
379
  function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
@@ -402,20 +394,63 @@ function runnerIsAlive(pid: number): boolean {
402
394
  }
403
395
  }
404
396
 
405
- function terminateRunnerBeforeProceed(pid: number): void {
397
+ function terminateRunnerBeforeProceed(pid: number): boolean {
406
398
  for (const signal of ["SIGTERM", "SIGKILL"] as const) {
407
- if (!runnerIsAlive(pid)) return;
399
+ if (!runnerIsAlive(pid)) return true;
408
400
  try {
409
401
  process.kill(pid, signal);
410
402
  } catch {
411
- if (!runnerIsAlive(pid)) return;
403
+ if (!runnerIsAlive(pid)) return true;
412
404
  }
413
405
  const deadline = Date.now() + 1000;
414
406
  while (runnerIsAlive(pid) && Date.now() < deadline) waitForStartupInterval();
415
407
  }
408
+ return !runnerIsAlive(pid);
409
+ }
410
+
411
+ function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, message: string): void {
412
+ const now = Date.now();
413
+ try {
414
+ const statusPath = path.join(asyncDir, "status.json");
415
+ let status: Partial<AsyncStatus> = {};
416
+ try {
417
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as Partial<AsyncStatus>;
418
+ } catch {}
419
+ writePrivateAtomicJson(statusPath, {
420
+ ...status,
421
+ runId,
422
+ ...(sessionId ? { sessionId } : {}),
423
+ state: "failed",
424
+ lastUpdate: now,
425
+ error: message,
426
+ processTerminal: {
427
+ version: 1,
428
+ state: "not-started",
429
+ runId,
430
+ runnerProcessInstanceId,
431
+ },
432
+ });
433
+ writePrivateAtomicJson(path.join(asyncDir, "process-terminal-candidate.json"), {
434
+ version: 1,
435
+ runId,
436
+ runnerProcessInstanceId,
437
+ writers: {},
438
+ expectedWriters: { 0: 0 },
439
+ });
440
+ } catch {
441
+ // Startup failures must still return the original launch error.
442
+ }
443
+ }
444
+
445
+ interface SpawnRunnerResult {
446
+ pid?: number;
447
+ runnerProcessInstanceId?: string;
448
+ error?: string;
449
+ terminationObserved?: boolean;
450
+ startupDidNotProceed?: boolean;
416
451
  }
417
452
 
418
- function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void): { pid?: number; error?: string } {
453
+ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void): SpawnRunnerResult {
419
454
  if (!jitiCliPath) {
420
455
  return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
421
456
  }
@@ -435,9 +470,11 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
435
470
  const launchConfig = { ...cfg, runnerProcessInstanceId };
436
471
  fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
437
472
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
438
- const nodeCommand = resolveAsyncRunnerNodeCommand();
439
- const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; revivalLease?: unknown };
473
+ const nodeCommand = resolveNodeExecutable();
474
+ const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; revivalLease?: unknown };
440
475
  const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
476
+ const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
477
+ const launchSessionId = typeof launchForStartup.sessionId === "string" ? launchForStartup.sessionId : undefined;
441
478
  const startupPath = typeof launchForStartup.revivalLease === "object" && launchAsyncDir
442
479
  ? path.join(launchAsyncDir, "runner-startup.json")
443
480
  : undefined;
@@ -526,27 +563,36 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
526
563
  }
527
564
  proc.unref();
528
565
  if (startupPath && startupAckPath && startupProceedPath) {
566
+ const persistStartupFailure = (message: string) => {
567
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, message);
568
+ };
529
569
  const ready = waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS);
530
570
  if (ready.ok === false) {
531
- terminateRunnerBeforeProceed(proc.pid);
532
- return { error: ready.error };
571
+ persistStartupFailure(ready.error);
572
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
573
+ return { pid: proc.pid, runnerProcessInstanceId, error: ready.error, terminationObserved, startupDidNotProceed: ready.startupDidNotProceed };
533
574
  }
534
575
  try {
535
576
  writeRunnerStartupControl(startupAckPath, { action: "ack", token: ready.token });
536
577
  } catch (error) {
537
- terminateRunnerBeforeProceed(proc.pid);
538
- return { error: `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}` };
578
+ const message = `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}`;
579
+ persistStartupFailure(message);
580
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
581
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
539
582
  }
540
583
  const acknowledged = waitForRunnerStartup(startupPath, "acknowledged", RUNNER_STARTUP_TIMEOUT_MS, ready.token);
541
584
  if (acknowledged.ok === false) {
542
- terminateRunnerBeforeProceed(proc.pid);
543
- return { error: acknowledged.error };
585
+ persistStartupFailure(acknowledged.error);
586
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
587
+ return { pid: proc.pid, runnerProcessInstanceId, error: acknowledged.error, terminationObserved, startupDidNotProceed: acknowledged.startupDidNotProceed };
544
588
  }
545
589
  try {
546
590
  writeRunnerStartupControl(startupProceedPath, { action: "proceed", token: ready.token });
547
591
  } catch (error) {
548
- terminateRunnerBeforeProceed(proc.pid);
549
- return { error: `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}` };
592
+ const message = `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}`;
593
+ persistStartupFailure(message);
594
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
595
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
550
596
  }
551
597
  try {
552
598
  fs.rmSync(startupPath, { force: true });
@@ -554,7 +600,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
554
600
  // Proceed is the commit point; handshake cleanup cannot turn a running revival into a start error.
555
601
  }
556
602
  }
557
- return { pid: proc.pid };
603
+ return { pid: proc.pid, runnerProcessInstanceId };
558
604
  } catch (error) {
559
605
  closeFd(stdoutFd);
560
606
  closeFd(stderrFd);
@@ -648,7 +694,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
648
694
  ...(s.model !== undefined ? { model: s.model } : {}),
649
695
  };
650
696
  };
651
- const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }) => {
697
+ const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
652
698
  const a = agents.find((x) => x.name === s.agent)!;
653
699
  const externalRunner = a.runner?.type === "external-cli";
654
700
  if (externalRunner) {
@@ -748,6 +794,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
748
794
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
749
795
  permissionRules,
750
796
  ...(params.capabilityCeiling ? { capabilityCeiling: params.capabilityCeiling } : {}),
797
+ ...(runFanoutPath ? { runFanoutPath } : {}),
751
798
  agent: s.agent,
752
799
  task,
753
800
  ...(a.runner ? { runner: a.runner } : {}),
@@ -839,7 +886,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
839
886
  }
840
887
  }
841
888
  const staticStep = nextFlatStep();
842
- return buildSeqStep({ ...t, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex });
889
+ return buildSeqStep({ ...t, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex }, resultMode === "parallel" ? `tasks[${taskIndex}]` : `chain[${stepIndex}].parallel[${taskIndex}]`);
843
890
  }),
844
891
  concurrency: s.concurrency,
845
892
  failFast: s.failFast,
@@ -884,7 +931,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
884
931
  };
885
932
  }
886
933
  const staticStep = nextFlatStep();
887
- return buildSeqStep(s as SequentialStep, staticStep.sessionFile, undefined, false, undefined, staticStep.index);
934
+ return buildSeqStep(s as SequentialStep, staticStep.sessionFile, undefined, false, undefined, staticStep.index, undefined, `chain[${stepIndex}]`);
888
935
  });
889
936
  const steps = params.attachRoot
890
937
  ? [{
@@ -966,8 +1013,11 @@ export function executeAsyncChain(
966
1013
  const asyncDir = inheritedNestedRoute
967
1014
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
968
1015
  : path.join(DIRS.async, id);
1016
+ let runFanoutBudget: RunFanoutBudgetDescriptor;
969
1017
  try {
1018
+ runFanoutBudget = params.runFanoutBudget ?? createRunFanoutBudget(id, 64);
970
1019
  fs.mkdirSync(asyncDir, { recursive: true });
1020
+ writeRunFanoutBudgetDescriptor(asyncDir, runFanoutBudget);
971
1021
  } catch (error) {
972
1022
  const message = error instanceof Error ? error.message : String(error);
973
1023
  return {
@@ -1030,7 +1080,7 @@ export function executeAsyncChain(
1030
1080
  return "agent" in step ? [childIntercomTarget(step.agent, childTargetIndex++)] : [undefined];
1031
1081
  }) : undefined;
1032
1082
 
1033
- let spawnResult: { pid?: number; error?: string } = {};
1083
+ let spawnResult: SpawnRunnerResult = {};
1034
1084
  try {
1035
1085
  spawnResult = spawnRunner(
1036
1086
  {
@@ -1063,6 +1113,7 @@ export function executeAsyncChain(
1063
1113
  timeoutMs: params.timeoutMs,
1064
1114
  deadlineAt,
1065
1115
  globalConcurrencyLimit: params.globalConcurrencyLimit,
1116
+ runFanoutBudget,
1066
1117
  workflowGraph,
1067
1118
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1068
1119
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
@@ -1079,13 +1130,21 @@ export function executeAsyncChain(
1079
1130
  (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
1080
1131
  );
1081
1132
  } catch (error) {
1133
+ params.activeAsyncCapacity?.rollback();
1082
1134
  const message = error instanceof Error ? error.message : String(error);
1083
1135
  return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${message}`);
1084
1136
  }
1085
1137
 
1086
1138
  if (spawnResult.error) {
1139
+ if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
1140
+ else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1087
1141
  return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${spawnResult.error}`);
1088
1142
  }
1143
+ if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) {
1144
+ params.activeAsyncCapacity?.rollback();
1145
+ return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': runner identity unavailable`);
1146
+ }
1147
+ params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1089
1148
 
1090
1149
  if (spawnResult.pid) {
1091
1150
  const eventFirstStep = eventChain[0];
@@ -1166,8 +1225,8 @@ export function executeAsyncChain(
1166
1225
  mode: resultMode,
1167
1226
  agent: firstAgents[0],
1168
1227
  agents: flatAgents,
1169
- task: firstTask?.slice(0, 50),
1170
- goal: workflowGoal?.slice(0, 120),
1228
+ task: firstTask?.trim() ? PROMPT_REDACTED : undefined,
1229
+ goal: workflowGoal?.trim() ? PROMPT_REDACTED : undefined,
1171
1230
  chain: eventChain.map((s) =>
1172
1231
  isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
1173
1232
  ),
@@ -1176,6 +1235,7 @@ export function executeAsyncChain(
1176
1235
  workflowGraph,
1177
1236
  cwd: runnerCwd,
1178
1237
  asyncDir,
1238
+ ...(sessionRoot ? { sessionRoot } : {}),
1179
1239
  ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
1180
1240
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1181
1241
  ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
@@ -1279,8 +1339,11 @@ export function executeAsyncSingle(
1279
1339
  const asyncDir = inheritedNestedRoute
1280
1340
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
1281
1341
  : path.join(DIRS.async, id);
1342
+ let runFanoutBudget: RunFanoutBudgetDescriptor;
1282
1343
  try {
1344
+ runFanoutBudget = params.runFanoutBudget ?? createRunFanoutBudget(id, 64);
1283
1345
  fs.mkdirSync(asyncDir, { recursive: true });
1346
+ writeRunFanoutBudgetDescriptor(asyncDir, runFanoutBudget);
1284
1347
  } catch (error) {
1285
1348
  const message = error instanceof Error ? error.message : String(error);
1286
1349
  return {
@@ -1373,9 +1436,11 @@ export function executeAsyncSingle(
1373
1436
  async: true,
1374
1437
  agentContract: params.agentContract,
1375
1438
  });
1439
+ const recoveryAgentConfig = params.recoveryAgentConfig ?? agentConfig;
1376
1440
  const recoveryDescriptor: SteeringRecoveryDescriptor = {
1377
1441
  version: 1,
1378
1442
  launchContractDigest,
1443
+ runFanoutBudget,
1379
1444
  sourceRunId: id,
1380
1445
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1381
1446
  agent,
@@ -1383,30 +1448,31 @@ export function executeAsyncSingle(
1383
1448
  ...(sessionFile ? { sessionFile } : {}),
1384
1449
  cwd: runnerCwd,
1385
1450
  ...(model ? { model } : {}),
1386
- ...(agentConfig.fallbackModels ? { fallbackModels: [...agentConfig.fallbackModels] } : {}),
1451
+ ...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
1387
1452
  ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1388
- ...(agentConfig.tools ? { tools: [...agentConfig.tools] } : {}),
1389
- ...(agentConfig.extensions ? { extensions: [...agentConfig.extensions] } : {}),
1390
- ...(agentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...agentConfig.subagentOnlyExtensions] } : {}),
1391
- ...(agentConfig.mcpDirectTools ? { mcpDirectTools: [...agentConfig.mcpDirectTools] } : {}),
1392
- ...(agentConfig.systemPrompt ? { systemPrompt: agentConfig.systemPrompt } : {}),
1393
- systemPromptMode: agentConfig.systemPromptMode,
1394
- inheritProjectContext: agentConfig.inheritProjectContext,
1395
- inheritSkills: agentConfig.inheritSkills,
1453
+ ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1454
+ ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1455
+ ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
1456
+ ...(recoveryAgentConfig.mcpDirectTools ? { mcpDirectTools: [...recoveryAgentConfig.mcpDirectTools] } : {}),
1457
+ ...(recoveryAgentConfig.systemPrompt ? { systemPrompt: recoveryAgentConfig.systemPrompt } : {}),
1458
+ systemPromptMode: recoveryAgentConfig.systemPromptMode,
1459
+ inheritProjectContext: recoveryAgentConfig.inheritProjectContext,
1460
+ inheritSkills: recoveryAgentConfig.inheritSkills,
1396
1461
  ...(resolvedSkills.length ? { skills: resolvedSkills.map((skill) => skill.name) } : {}),
1397
- ...(agentConfig.skillPath ? { skillPath: [...agentConfig.skillPath] } : {}),
1398
- ...(agentConfig.filePath ? { agentFilePath: agentConfig.filePath } : {}),
1399
- ...(agentConfig.completionGuard !== undefined ? { completionGuard: agentConfig.completionGuard } : {}),
1400
- ...(agentConfig.memory ? { memory: { ...agentConfig.memory } } : {}),
1462
+ ...(recoveryAgentConfig.skillPath ? { skillPath: [...recoveryAgentConfig.skillPath] } : {}),
1463
+ ...(recoveryAgentConfig.filePath ? { agentFilePath: recoveryAgentConfig.filePath } : {}),
1464
+ ...(recoveryAgentConfig.completionGuard !== undefined ? { completionGuard: recoveryAgentConfig.completionGuard } : {}),
1465
+ ...(recoveryAgentConfig.memory ? { memory: { ...recoveryAgentConfig.memory } } : {}),
1401
1466
  ...(outputPath ? { outputPath } : {}),
1402
1467
  outputMode,
1403
1468
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1404
1469
  ...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
1405
1470
  ...(controlConfig ? { controlConfig } : {}),
1471
+ ...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
1406
1472
  ...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
1407
1473
  ...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
1408
1474
  ...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
1409
- maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1475
+ maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
1410
1476
  ...(maxOutput ? { maxOutput } : {}),
1411
1477
  share: shareEnabled,
1412
1478
  ...(resolvedSessionDir ? { sessionDir: resolvedSessionDir } : {}),
@@ -1421,7 +1487,7 @@ export function executeAsyncSingle(
1421
1487
  return formatAsyncStartError("single", `Failed to persist async recovery descriptor for '${id}': ${error instanceof Error ? error.message : String(error)}`);
1422
1488
  }
1423
1489
  }
1424
- let spawnResult: { pid?: number; error?: string } = {};
1490
+ let spawnResult: SpawnRunnerResult = {};
1425
1491
  try {
1426
1492
  spawnResult = spawnRunner(
1427
1493
  {
@@ -1494,6 +1560,7 @@ export function executeAsyncSingle(
1494
1560
  resultMode: "single",
1495
1561
  launchContractDigest,
1496
1562
  launchResolvedExtensions,
1563
+ runFanoutBudget,
1497
1564
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1498
1565
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1499
1566
  ...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
@@ -1510,13 +1577,21 @@ export function executeAsyncSingle(
1510
1577
  (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
1511
1578
  );
1512
1579
  } catch (error) {
1580
+ params.activeAsyncCapacity?.rollback();
1513
1581
  const message = error instanceof Error ? error.message : String(error);
1514
1582
  return formatAsyncStartError("single", `Failed to start async run '${id}': ${message}`);
1515
1583
  }
1516
1584
 
1517
1585
  if (spawnResult.error) {
1586
+ if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
1587
+ else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1518
1588
  return formatAsyncStartError("single", `Failed to start async run '${id}': ${spawnResult.error}`);
1519
1589
  }
1590
+ if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) {
1591
+ params.activeAsyncCapacity?.rollback();
1592
+ return formatAsyncStartError("single", `Failed to start async run '${id}': runner identity unavailable`);
1593
+ }
1594
+ params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1520
1595
 
1521
1596
  if (spawnResult.pid) {
1522
1597
  if (inheritedNestedRoute && nestedAddress) {
@@ -1562,10 +1637,11 @@ export function executeAsyncSingle(
1562
1637
  sessionId: ctx.currentSessionId,
1563
1638
  mode: "single",
1564
1639
  agent,
1565
- task: task?.slice(0, 50),
1566
- goal: (params.goal ?? task).slice(0, 120),
1640
+ task: task?.trim() ? PROMPT_REDACTED : undefined,
1641
+ goal: (params.goal ?? task).trim() ? PROMPT_REDACTED : undefined,
1567
1642
  cwd: runnerCwd,
1568
1643
  asyncDir,
1644
+ ...(sessionRoot ? { sessionRoot } : {}),
1569
1645
  launchContractDigest,
1570
1646
  launchResolvedExtensions,
1571
1647
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
@@ -557,10 +557,13 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
557
557
  const agents = firstGroupCount && firstGroupCount > 0
558
558
  ? rawAgents?.slice(0, firstGroupCount)
559
559
  : rawAgents;
560
+ const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
561
+ state.liveAsyncSessionRoots?.delete(info.id);
560
562
  state.asyncJobs.set(info.id, {
561
563
  asyncId: info.id,
562
564
  asyncDir,
563
565
  ...(typeof info.cwd === "string" ? { cwd: path.resolve(info.cwd) } : {}),
566
+ ...(sessionRoot ? { sessionRoot } : {}),
564
567
  status: "queued",
565
568
  pid: typeof info.pid === "number" ? info.pid : undefined,
566
569
  ...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
@@ -637,6 +640,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
637
640
  state.asyncJobs.clear();
638
641
  state.fleetJobs?.clear();
639
642
  state.foregroundControls?.clear();
643
+ state.liveAsyncSessionRoots?.clear();
640
644
  state.lastForegroundControlId = null;
641
645
  state.resultFileCoalescer.clear();
642
646
  if (ctx?.hasUI) {
@@ -5,6 +5,7 @@ import type { AgentConfig } from "../../agents/agents.ts";
5
5
  import { validateAcceptanceInput } from "../shared/acceptance.ts";
6
6
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
7
7
  import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
8
+ import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
8
9
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
9
10
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
10
11
 
@@ -306,8 +307,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
306
307
  "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
307
308
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
308
309
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
309
- "artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
310
- "launchResolvedExtensions",
310
+ "artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
311
+ "launchResolvedExtensions", "runFanoutBudget",
311
312
  ]);
312
313
  for (const field of Object.keys(parsed)) {
313
314
  if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
@@ -317,6 +318,11 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
317
318
  if (typeof parsed[field] !== "string" || !(parsed[field] as string).trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
318
319
  }
319
320
  if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
321
+ try {
322
+ parsed.runFanoutBudget = validateRunFanoutBudgetDescriptor(parsed.runFanoutBudget);
323
+ } catch (error) {
324
+ throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
325
+ }
320
326
  if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
321
327
  if (parsed.agentContract !== undefined) {
322
328
  if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
@@ -368,6 +374,16 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
368
374
  if (artifact.includeTranscript !== undefined && typeof artifact.includeTranscript !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.includeTranscript must be a boolean.`);
369
375
  if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
370
376
  }
377
+ if (parsed.intercomBridge !== undefined) {
378
+ if (!parsed.intercomBridge || typeof parsed.intercomBridge !== "object" || Array.isArray(parsed.intercomBridge)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge must be an object.`);
379
+ const bridge = parsed.intercomBridge as Record<string, unknown>;
380
+ for (const field of Object.keys(bridge)) {
381
+ if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
382
+ }
383
+ if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.mode is invalid.`);
384
+ if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
385
+ if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
386
+ }
371
387
  if (parsed.controlConfig !== undefined) {
372
388
  if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
373
389
  const control = parsed.controlConfig as Record<string, unknown>;
@@ -7,11 +7,12 @@ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from
7
7
  import { pruneStatusCacheForAsyncRoot, readStatus } from "../../shared/utils.ts";
8
8
  import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
9
9
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
10
+ import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
10
11
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
11
12
  import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
12
13
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
13
14
  import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
14
- import { ACTIVE_RUN_INDEX_DIR, isActiveAsyncState, readActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
15
+ import { ACTIVE_RUN_INDEX_DIR, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
15
16
 
16
17
  interface AsyncRunStepSummary {
17
18
  index: number;
@@ -106,6 +107,7 @@ export interface AsyncRunSummary {
106
107
  nestedChildren?: NestedRunSummary[];
107
108
  nestedWarnings?: string[];
108
109
  processTerminal?: AsyncStatus["processTerminal"];
110
+ runFanoutBudget?: AsyncStatus["runFanoutBudget"];
109
111
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
110
112
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
111
113
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
@@ -222,6 +224,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
222
224
  const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
223
225
  const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
224
226
  ?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
227
+ const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
228
+ const runFanoutBudget = runFanoutBudgetDescriptor ? getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor) : status.runFanoutBudget;
225
229
  const steps = status.steps ?? [];
226
230
  const chainStepCount = status.chainStepCount ?? steps.length;
227
231
  const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
@@ -327,6 +331,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
327
331
  ...(nestedChildren.length ? { nestedChildren } : {}),
328
332
  ...(nestedWarnings.length ? { nestedWarnings } : {}),
329
333
  ...(processTerminal ? { processTerminal } : {}),
334
+ ...(runFanoutBudget ? { runFanoutBudget } : {}),
330
335
  ...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
331
336
  ...(status.launchResolvedExtensions ? { launchResolvedExtensions: status.launchResolvedExtensions } : {}),
332
337
  ...(status.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: status.runtimeAcknowledgedExtensions } : {}),
@@ -452,11 +457,11 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
452
457
  if (usedActiveIndex) updateActiveRunIndex(asyncDir, "failed");
453
458
  continue;
454
459
  }
455
- if (status.displayDismissedAt !== undefined) {
456
- if (usedActiveIndex) updateActiveRunIndex(asyncDir, "complete");
457
- continue;
460
+ if (usedActiveIndex && !isActiveAsyncState(status.state)) {
461
+ const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
462
+ if (processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
458
463
  }
459
- if (usedActiveIndex && !isActiveAsyncState(status.state)) updateActiveRunIndex(asyncDir, status.state);
464
+ if (status.displayDismissedAt !== undefined) continue;
460
465
  // Filter before the nested-route lookup: the lookup builds an index over
461
466
  // the nested-events directory, so deferring it for filtered-out runs keeps
462
467
  // restoration at load from scanning that directory when no active runs
@@ -559,6 +564,7 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
559
564
  const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
560
565
  const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
561
566
  lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
567
+ if (run.runFanoutBudget) lines.push(` ${formatRunFanoutBudget(run.runFanoutBudget)}`);
562
568
  if (run.error) lines.push(` Error: ${run.error}`);
563
569
  for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
564
570
  const outputPath = formatAsyncRunOutputPath(run);