pi-subagents 0.48.0 → 0.50.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 (88) hide show
  1. package/CHANGELOG.md +82 -19
  2. package/agents/oracle.md +7 -5
  3. package/agents/researcher.md +1 -1
  4. package/agents/reviewer.md +2 -2
  5. package/agents/scout.md +1 -1
  6. package/agents/worker.md +1 -1
  7. package/docs/agents.md +3 -0
  8. package/docs/configuration.md +75 -8
  9. package/docs/extension-api.md +36 -0
  10. package/docs/missions.md +5 -3
  11. package/docs/observability.md +42 -2
  12. package/docs/tool-reference.md +19 -2
  13. package/docs/workflows.md +2 -2
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  16. package/skills/pi-subagents/references/execution-controls.md +5 -4
  17. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +37 -12
  21. package/src/api/external-runs.ts +174 -84
  22. package/src/api/preflight.ts +13 -7
  23. package/src/extension/config.ts +53 -0
  24. package/src/extension/index.ts +62 -9
  25. package/src/extension/public-execution.ts +32 -4
  26. package/src/extension/rpc.ts +5 -1
  27. package/src/extension/schemas.ts +10 -8
  28. package/src/extension/tool-description.ts +13 -9
  29. package/src/inspectors/herdr/actions.ts +13 -8
  30. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  31. package/src/inspectors/herdr/project-panes.ts +2 -6
  32. package/src/inspectors/herdr/shell-command.ts +16 -0
  33. package/src/intercom/intercom-bridge.ts +5 -4
  34. package/src/intercom/native-supervisor-channel.ts +19 -42
  35. package/src/missions/goal-driver.ts +3 -1
  36. package/src/missions/store.ts +8 -3
  37. package/src/runs/background/active-async-capacity.ts +82 -25
  38. package/src/runs/background/active-run-index.ts +71 -1
  39. package/src/runs/background/async-execution.ts +73 -43
  40. package/src/runs/background/async-job-tracker.ts +5 -0
  41. package/src/runs/background/async-resume.ts +14 -6
  42. package/src/runs/background/async-status-snapshot.ts +277 -0
  43. package/src/runs/background/async-status.ts +8 -3
  44. package/src/runs/background/chain-root-attachment.ts +2 -2
  45. package/src/runs/background/completion-replay.ts +11 -1
  46. package/src/runs/background/fleet-view.ts +21 -6
  47. package/src/runs/background/result-files.ts +437 -0
  48. package/src/runs/background/result-watcher.ts +188 -41
  49. package/src/runs/background/resume-guidance.ts +27 -7
  50. package/src/runs/background/retained-children.ts +75 -18
  51. package/src/runs/background/run-id-resolver.ts +30 -24
  52. package/src/runs/background/run-status.ts +101 -4
  53. package/src/runs/background/scheduled-runs.ts +54 -28
  54. package/src/runs/background/stale-run-reconciler.ts +27 -13
  55. package/src/runs/background/subagent-runner.ts +298 -33
  56. package/src/runs/background/subagent-wait.ts +2 -0
  57. package/src/runs/background/wait-completions.ts +5 -2
  58. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  59. package/src/runs/foreground/chain-execution.ts +16 -0
  60. package/src/runs/foreground/execution.ts +219 -15
  61. package/src/runs/foreground/prompt-audit.ts +4 -3
  62. package/src/runs/foreground/subagent-executor.ts +328 -43
  63. package/src/runs/shared/completion-guard.ts +107 -1
  64. package/src/runs/shared/external-cli-runner.ts +4 -0
  65. package/src/runs/shared/llm-intent-arbiter.ts +39 -23
  66. package/src/runs/shared/model-fallback.ts +16 -2
  67. package/src/runs/shared/nested-events.ts +66 -62
  68. package/src/runs/shared/orca-progress-tabs.ts +375 -0
  69. package/src/runs/shared/parallel-utils.ts +2 -0
  70. package/src/runs/shared/subagent-control.ts +15 -0
  71. package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  72. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  73. package/src/runs/shared/tool-timeout.ts +93 -0
  74. package/src/shared/agent-stream-options.ts +5 -0
  75. package/src/shared/artifacts.ts +2 -6
  76. package/src/shared/display-text.ts +50 -0
  77. package/src/shared/node-executable.ts +21 -0
  78. package/src/shared/types.ts +51 -5
  79. package/src/slash/slash-commands.ts +34 -25
  80. package/src/slash/slash-live-state.ts +3 -0
  81. package/src/tui/fleet-status.ts +160 -45
  82. package/src/tui/fleet-transcript.ts +1 -48
  83. package/src/tui/fleet.ts +128 -16
  84. package/src/tui/render.ts +122 -44
  85. package/src/watchdog/permission-arbiter.ts +2 -1
  86. package/src/watchdog/review.ts +4 -3
  87. package/src/workflows/chat-progress.ts +10 -2
  88. package/src/workflows/scripted-workflow.ts +272 -76
@@ -19,10 +19,12 @@ 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
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";
27
+ import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
26
28
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
27
29
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
28
30
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
@@ -37,6 +39,7 @@ import {
37
39
  type AsyncStatus,
38
40
  type ArtifactConfig,
39
41
  type Details,
42
+ type IntercomBridgeConfig,
40
43
  type JsonSchemaObject,
41
44
  type MaxOutputConfig,
42
45
  type NestedRouteInfo,
@@ -56,6 +59,7 @@ import {
56
59
  resolveChildMaxSubagentDepth,
57
60
  } from "../../shared/types.ts";
58
61
  import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
62
+ import { resultFilePath } from "./result-files.ts";
59
63
  import { appendTurnBudgetSystemPrompt, initialTurnBudgetState } from "../shared/turn-budget.ts";
60
64
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
61
65
  import { usageBudgetState } from "../shared/usage-budget.ts";
@@ -172,6 +176,12 @@ interface AsyncChainParams {
172
176
  toolBudget?: ResolvedToolBudget;
173
177
  usageBudget?: UsageBudgetConfig;
174
178
  configToolBudget?: ResolvedToolBudget;
179
+ /** Optional per-call hard toolTimeoutMs override (highest precedence). */
180
+ callToolTimeoutMs?: number;
181
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
182
+ configToolTimeoutMs?: number;
183
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
184
+ toolTimeoutMsEnv?: string | undefined;
175
185
  /** Global cap on simultaneously-running subagent tasks within the async run. */
176
186
  globalConcurrencyLimit?: number;
177
187
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
@@ -187,6 +197,8 @@ interface AsyncSingleParams {
187
197
  /** Raw caller-facing goal used only by the started event. */
188
198
  goal?: string;
189
199
  agentConfig: AgentConfig;
200
+ /** Agent contract before per-run bridge injection, used only for recovery persistence. */
201
+ recoveryAgentConfig?: AgentConfig;
190
202
  ctx: AsyncExecutionContext;
191
203
  cwd?: string;
192
204
  maxOutput?: MaxOutputConfig;
@@ -214,16 +226,23 @@ interface AsyncSingleParams {
214
226
  worktreeSetupHookTimeoutMs?: number;
215
227
  worktreeBaseDir?: string;
216
228
  controlConfig?: ResolvedControlConfig;
229
+ intercomBridge?: IntercomBridgeConfig;
217
230
  controlIntercomTarget?: string;
218
231
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
219
232
  nestedRoute?: NestedRouteInfo;
220
233
  acceptance?: AcceptanceInput;
221
234
  timeoutMs?: number;
222
235
  absoluteDeadlineAt?: number;
236
+ /** Optional per-call hard toolTimeoutMs override (highest precedence). */
237
+ toolTimeoutMs?: number;
223
238
  turnBudget?: { maxTurns: number; graceTurns?: number };
224
239
  toolBudget?: ResolvedToolBudget | ToolBudgetConfig;
225
240
  usageBudget?: UsageBudgetConfig;
226
241
  configToolBudget?: ResolvedToolBudget;
242
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
243
+ configToolTimeoutMs?: number;
244
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
245
+ toolTimeoutMsEnv?: string | undefined;
227
246
  allowZeroToolBudget?: boolean;
228
247
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
229
248
  runFanoutBudget?: RunFanoutBudgetDescriptor;
@@ -262,6 +281,12 @@ export interface AsyncRunnerStepBuildParams {
262
281
  validateOutputBindings?: boolean;
263
282
  toolBudget?: ResolvedToolBudget;
264
283
  configToolBudget?: ResolvedToolBudget;
284
+ /** Optional per-call hard toolTimeoutMs override from the subagent invocation. */
285
+ callToolTimeoutMs?: number;
286
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
287
+ configToolTimeoutMs?: number;
288
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
289
+ toolTimeoutMsEnv?: string | undefined;
265
290
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
266
291
  }
267
292
 
@@ -299,27 +324,6 @@ export function isAsyncAvailable(): boolean {
299
324
  return jitiCliPath !== undefined;
300
325
  }
301
326
 
302
- function isNodeExecutableName(execPath: string): boolean {
303
- const basename = path.basename(execPath).toLowerCase();
304
- return basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe";
305
- }
306
-
307
- function canUseCurrentNodeExecutable(execPath: string): boolean {
308
- try {
309
- fs.accessSync(execPath, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
310
- return true;
311
- } catch {
312
- return false;
313
- }
314
- }
315
-
316
- function resolveAsyncRunnerNodeCommand(): string {
317
- if (isNodeExecutableName(process.execPath) && canUseCurrentNodeExecutable(process.execPath)) {
318
- return process.execPath;
319
- }
320
- return process.platform === "win32" ? "node.exe" : "node";
321
- }
322
-
323
327
  export function resolveAsyncRunnerLogPaths(cfg: object): { stdoutPath: string; stderrPath: string } | undefined {
324
328
  const asyncDir = typeof (cfg as { asyncDir?: unknown }).asyncDir === "string"
325
329
  ? (cfg as { asyncDir: string }).asyncDir
@@ -486,7 +490,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
486
490
  const launchConfig = { ...cfg, runnerProcessInstanceId };
487
491
  fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
488
492
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
489
- const nodeCommand = resolveAsyncRunnerNodeCommand();
493
+ const nodeCommand = resolveNodeExecutable();
490
494
  const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; revivalLease?: unknown };
491
495
  const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
492
496
  const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
@@ -730,6 +734,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
730
734
  const toolBudgetInput = s.toolBudget ?? params.toolBudget ?? a.toolBudget ?? params.configToolBudget;
731
735
  const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, s.toolBudget ? "toolBudget" : a.toolBudget ? "agent.toolBudget" : "config.toolBudget");
732
736
  if (resolvedToolBudget.error) throw new AsyncStartValidationError(resolvedToolBudget.error);
737
+ const resolvedToolTimeout = resolveToolTimeoutMs({
738
+ callValue: params.callToolTimeoutMs,
739
+ agentValue: a.defaultToolTimeoutMs,
740
+ configValue: params.configToolTimeoutMs,
741
+ envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
742
+ });
743
+ if (resolvedToolTimeout.error) throw new AsyncStartValidationError(resolvedToolTimeout.error);
733
744
  const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
734
745
  const instructionCwd = behaviorCwd ?? stepCwd;
735
746
  const readExistenceCwd = behaviorCwd ? stepCwd : instructionCwd;
@@ -843,6 +854,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
843
854
  sessionFile,
844
855
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
845
856
  timeoutMs: a.defaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS,
857
+ toolTimeoutMs: resolvedToolTimeout.toolTimeoutMs,
846
858
  waitToolEnabled: params.waitToolEnabled,
847
859
  effectiveAcceptance: resolveEffectiveAcceptance({
848
860
  explicit: s.acceptance,
@@ -1066,6 +1078,9 @@ export function executeAsyncChain(
1066
1078
  asyncDir,
1067
1079
  toolBudget: params.toolBudget,
1068
1080
  configToolBudget: params.configToolBudget,
1081
+ callToolTimeoutMs: params.callToolTimeoutMs,
1082
+ configToolTimeoutMs: params.configToolTimeoutMs,
1083
+ toolTimeoutMsEnv: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
1069
1084
  capabilityCeiling,
1070
1085
  });
1071
1086
  if ("error" in built) {
@@ -1102,7 +1117,7 @@ export function executeAsyncChain(
1102
1117
  {
1103
1118
  id,
1104
1119
  steps,
1105
- resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1120
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
1106
1121
  cwd: runnerCwd,
1107
1122
  placeholder: "{previous}",
1108
1123
  maxOutput,
@@ -1251,6 +1266,7 @@ export function executeAsyncChain(
1251
1266
  workflowGraph,
1252
1267
  cwd: runnerCwd,
1253
1268
  asyncDir,
1269
+ ...(sessionRoot ? { sessionRoot } : {}),
1254
1270
  ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
1255
1271
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1256
1272
  ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
@@ -1399,17 +1415,27 @@ export function executeAsyncSingle(
1399
1415
  ? deadlineAt - Date.now()
1400
1416
  : params.timeoutMs;
1401
1417
  if (timeoutMs !== undefined && timeoutMs <= 0) return formatAsyncStartError("single", "The source run's absolute deadline expired before recovery could launch.");
1418
+ const resolvedToolTimeout = resolveToolTimeoutMs({
1419
+ callValue: params.toolTimeoutMs,
1420
+ agentValue: agentConfig.defaultToolTimeoutMs,
1421
+ configValue: params.configToolTimeoutMs,
1422
+ envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
1423
+ });
1424
+ if (resolvedToolTimeout.error) return formatAsyncStartError("single", resolvedToolTimeout.error);
1425
+ const toolTimeoutMs = resolvedToolTimeout.toolTimeoutMs;
1402
1426
  const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
1403
1427
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1404
1428
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1405
1429
  const structuredOutput = params.structuredOutputSchema
1406
1430
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1407
1431
  : undefined;
1408
- const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
1409
- .flatMap((candidate) => {
1410
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1411
- return resolved ? [resolved] : [];
1412
- });
1432
+ const modelCandidates = externalRunner
1433
+ ? []
1434
+ : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
1435
+ .flatMap((candidate) => {
1436
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1437
+ return resolved ? [resolved] : [];
1438
+ });
1413
1439
  const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1414
1440
  const toolPlan = resolvePiLaunchToolPlan({
1415
1441
  tools: agentConfig.tools,
@@ -1451,6 +1477,7 @@ export function executeAsyncSingle(
1451
1477
  async: true,
1452
1478
  agentContract: params.agentContract,
1453
1479
  });
1480
+ const recoveryAgentConfig = params.recoveryAgentConfig ?? agentConfig;
1454
1481
  const recoveryDescriptor: SteeringRecoveryDescriptor = {
1455
1482
  version: 1,
1456
1483
  launchContractDigest,
@@ -1462,30 +1489,31 @@ export function executeAsyncSingle(
1462
1489
  ...(sessionFile ? { sessionFile } : {}),
1463
1490
  cwd: runnerCwd,
1464
1491
  ...(model ? { model } : {}),
1465
- ...(agentConfig.fallbackModels ? { fallbackModels: [...agentConfig.fallbackModels] } : {}),
1492
+ ...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
1466
1493
  ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1467
- ...(agentConfig.tools ? { tools: [...agentConfig.tools] } : {}),
1468
- ...(agentConfig.extensions ? { extensions: [...agentConfig.extensions] } : {}),
1469
- ...(agentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...agentConfig.subagentOnlyExtensions] } : {}),
1470
- ...(agentConfig.mcpDirectTools ? { mcpDirectTools: [...agentConfig.mcpDirectTools] } : {}),
1471
- ...(agentConfig.systemPrompt ? { systemPrompt: agentConfig.systemPrompt } : {}),
1472
- systemPromptMode: agentConfig.systemPromptMode,
1473
- inheritProjectContext: agentConfig.inheritProjectContext,
1474
- inheritSkills: agentConfig.inheritSkills,
1494
+ ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1495
+ ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1496
+ ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
1497
+ ...(recoveryAgentConfig.mcpDirectTools ? { mcpDirectTools: [...recoveryAgentConfig.mcpDirectTools] } : {}),
1498
+ ...(recoveryAgentConfig.systemPrompt ? { systemPrompt: recoveryAgentConfig.systemPrompt } : {}),
1499
+ systemPromptMode: recoveryAgentConfig.systemPromptMode,
1500
+ inheritProjectContext: recoveryAgentConfig.inheritProjectContext,
1501
+ inheritSkills: recoveryAgentConfig.inheritSkills,
1475
1502
  ...(resolvedSkills.length ? { skills: resolvedSkills.map((skill) => skill.name) } : {}),
1476
- ...(agentConfig.skillPath ? { skillPath: [...agentConfig.skillPath] } : {}),
1477
- ...(agentConfig.filePath ? { agentFilePath: agentConfig.filePath } : {}),
1478
- ...(agentConfig.completionGuard !== undefined ? { completionGuard: agentConfig.completionGuard } : {}),
1479
- ...(agentConfig.memory ? { memory: { ...agentConfig.memory } } : {}),
1503
+ ...(recoveryAgentConfig.skillPath ? { skillPath: [...recoveryAgentConfig.skillPath] } : {}),
1504
+ ...(recoveryAgentConfig.filePath ? { agentFilePath: recoveryAgentConfig.filePath } : {}),
1505
+ ...(recoveryAgentConfig.completionGuard !== undefined ? { completionGuard: recoveryAgentConfig.completionGuard } : {}),
1506
+ ...(recoveryAgentConfig.memory ? { memory: { ...recoveryAgentConfig.memory } } : {}),
1480
1507
  ...(outputPath ? { outputPath } : {}),
1481
1508
  outputMode,
1482
1509
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1483
1510
  ...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
1484
1511
  ...(controlConfig ? { controlConfig } : {}),
1512
+ ...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
1485
1513
  ...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
1486
1514
  ...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
1487
1515
  ...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
1488
- maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1516
+ maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
1489
1517
  ...(maxOutput ? { maxOutput } : {}),
1490
1518
  share: shareEnabled,
1491
1519
  ...(resolvedSessionDir ? { sessionDir: resolvedSessionDir } : {}),
@@ -1546,7 +1574,7 @@ export function executeAsyncSingle(
1546
1574
  ],
1547
1575
  resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
1548
1576
  ? workflowAwaitedAsyncResultPath(asyncDir)
1549
- : inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1577
+ : inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
1550
1578
  cwd: runnerCwd,
1551
1579
  placeholder: "{previous}",
1552
1580
  maxOutput,
@@ -1565,6 +1593,7 @@ export function executeAsyncSingle(
1565
1593
  controlConfig,
1566
1594
  timeoutMs,
1567
1595
  deadlineAt,
1596
+ toolTimeoutMs,
1568
1597
  turnBudget: params.turnBudget,
1569
1598
  toolBudget: params.toolBudget,
1570
1599
  usageBudget: params.usageBudget,
@@ -1654,6 +1683,7 @@ export function executeAsyncSingle(
1654
1683
  goal: (params.goal ?? task).trim() ? PROMPT_REDACTED : undefined,
1655
1684
  cwd: runnerCwd,
1656
1685
  asyncDir,
1686
+ ...(sessionRoot ? { sessionRoot } : {}),
1657
1687
  launchContractDigest,
1658
1688
  launchResolvedExtensions,
1659
1689
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
@@ -125,6 +125,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
125
125
  return {
126
126
  asyncId: run.id,
127
127
  asyncDir: run.asyncDir,
128
+ toolCallId: run.toolCallId,
128
129
  status: run.state,
129
130
  sessionId: run.sessionId,
130
131
  activityState: run.activityState,
@@ -557,10 +558,13 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
557
558
  const agents = firstGroupCount && firstGroupCount > 0
558
559
  ? rawAgents?.slice(0, firstGroupCount)
559
560
  : rawAgents;
561
+ const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
562
+ state.liveAsyncSessionRoots?.delete(info.id);
560
563
  state.asyncJobs.set(info.id, {
561
564
  asyncId: info.id,
562
565
  asyncDir,
563
566
  ...(typeof info.cwd === "string" ? { cwd: path.resolve(info.cwd) } : {}),
567
+ ...(sessionRoot ? { sessionRoot } : {}),
564
568
  status: "queued",
565
569
  pid: typeof info.pid === "number" ? info.pid : undefined,
566
570
  ...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
@@ -637,6 +641,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
637
641
  state.asyncJobs.clear();
638
642
  state.fleetJobs?.clear();
639
643
  state.foregroundControls?.clear();
644
+ state.liveAsyncSessionRoots?.clear();
640
645
  state.lastForegroundControlId = null;
641
646
  state.resultFileCoalescer.clear();
642
647
  if (ctx?.hasUI) {
@@ -8,6 +8,7 @@ import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, ty
8
8
  import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
9
9
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
10
10
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
11
+ import { resultFilePath } from "./result-files.ts";
11
12
 
12
13
  export interface AsyncResumeParams {
13
14
  id?: string;
@@ -172,7 +173,7 @@ function prefixedRunIds(dir: string, prefix: string, suffix = ""): string[] {
172
173
  }
173
174
 
174
175
  function exactResultPath(resultsDir: string, runId: string): string | null {
175
- const resultPath = path.join(resultsDir, `${runId}.json`);
176
+ const resultPath = resultFilePath(resultsDir, runId);
176
177
  assertInsideRoot(resultsDir, resultPath, "Async result file");
177
178
  return fs.existsSync(resultPath) ? resultPath : null;
178
179
  }
@@ -182,10 +183,7 @@ export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string,
182
183
  if (!requestedId) return [];
183
184
  const asyncRoot = path.resolve(asyncDirRoot);
184
185
  const resultRoot = path.resolve(resultsDir);
185
- const matchingIds = [...new Set([
186
- ...prefixedRunIds(asyncRoot, requestedId),
187
- ...prefixedRunIds(resultRoot, requestedId, ".json"),
188
- ])].sort();
186
+ const matchingIds = prefixedRunIds(asyncRoot, requestedId).sort();
189
187
  return matchingIds.map((id) => {
190
188
  const asyncDir = path.join(asyncRoot, id);
191
189
  assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
@@ -307,7 +305,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
307
305
  "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
308
306
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
309
307
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
310
- "artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
308
+ "artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
311
309
  "launchResolvedExtensions", "runFanoutBudget",
312
310
  ]);
313
311
  for (const field of Object.keys(parsed)) {
@@ -374,6 +372,16 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
374
372
  if (artifact.includeTranscript !== undefined && typeof artifact.includeTranscript !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.includeTranscript must be a boolean.`);
375
373
  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.`);
376
374
  }
375
+ if (parsed.intercomBridge !== undefined) {
376
+ if (!parsed.intercomBridge || typeof parsed.intercomBridge !== "object" || Array.isArray(parsed.intercomBridge)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge must be an object.`);
377
+ const bridge = parsed.intercomBridge as Record<string, unknown>;
378
+ for (const field of Object.keys(bridge)) {
379
+ if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
380
+ }
381
+ 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.`);
382
+ if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
383
+ if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
384
+ }
377
385
  if (parsed.controlConfig !== undefined) {
378
386
  if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
379
387
  const control = parsed.controlConfig as Record<string, unknown>;
@@ -0,0 +1,277 @@
1
+ import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-text.ts";
2
+ import type { AsyncJobState, AsyncJobStep, NestedRunSummary, NestedStepSummary, SubagentRunMode, SubagentState } from "../../shared/types.ts";
3
+
4
+ export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
5
+ export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
6
+ export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "PI_SUBAGENT_ASYNC_JSON:";
7
+
8
+ const DEFAULT_MAX_RUNS = 20;
9
+ const DEFAULT_MAX_CHILDREN_PER_NODE = 8;
10
+ const DEFAULT_MAX_DEPTH = 3;
11
+ const DEFAULT_MAX_STRING_LENGTH = 160;
12
+ const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
13
+
14
+ export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
15
+ export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
16
+
17
+ export interface AsyncStatusSnapshotActivityV1 {
18
+ state?: string;
19
+ currentTool?: string;
20
+ lastActivityAt?: number;
21
+ currentToolStartedAt?: number;
22
+ turnCount?: number;
23
+ toolCount?: number;
24
+ }
25
+
26
+ export interface AsyncStatusSnapshotNodeV1 {
27
+ id: string;
28
+ kind: AsyncStatusSnapshotKind;
29
+ label: string;
30
+ state: AsyncStatusSnapshotState;
31
+ startedAt?: number;
32
+ updatedAt?: number;
33
+ endedAt?: number;
34
+ activity?: AsyncStatusSnapshotActivityV1;
35
+ children?: AsyncStatusSnapshotNodeV1[];
36
+ }
37
+
38
+ export interface AsyncStatusSnapshotCapsV1 {
39
+ maxRuns: number;
40
+ maxChildrenPerNode: number;
41
+ maxDepth: number;
42
+ maxStringLength: number;
43
+ maxSerializedBytes: number;
44
+ }
45
+
46
+ export interface AsyncStatusSnapshotOmittedV1 {
47
+ runs: number;
48
+ children: number;
49
+ byteLimitExceeded: boolean;
50
+ }
51
+
52
+ export interface AsyncStatusSnapshotV1 {
53
+ kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
54
+ version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
55
+ generatedAt: number;
56
+ caps: AsyncStatusSnapshotCapsV1;
57
+ omitted: AsyncStatusSnapshotOmittedV1;
58
+ runs: AsyncStatusSnapshotNodeV1[];
59
+ }
60
+
61
+ export interface AsyncStatusSnapshotOptions {
62
+ generatedAt?: number;
63
+ maxRuns?: number;
64
+ maxChildrenPerNode?: number;
65
+ maxDepth?: number;
66
+ maxStringLength?: number;
67
+ maxSerializedBytes?: number;
68
+ }
69
+
70
+ interface BuildContext {
71
+ caps: AsyncStatusSnapshotCapsV1;
72
+ omitted: AsyncStatusSnapshotOmittedV1;
73
+ }
74
+
75
+ function resolveCaps(options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotCapsV1 {
76
+ return {
77
+ maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
78
+ maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
79
+ maxDepth: Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_MAX_DEPTH)),
80
+ maxStringLength: Math.max(0, Math.floor(options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH)),
81
+ maxSerializedBytes: Math.max(256, Math.floor(options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES)),
82
+ };
83
+ }
84
+
85
+ function publicText(value: unknown, fallback: string, maxLength: number): string {
86
+ if (typeof value !== "string") return fallback;
87
+ const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
88
+ return truncateDisplayText(normalized || fallback, maxLength);
89
+ }
90
+
91
+ function publicOptionalText(value: unknown, maxLength: number): string | undefined {
92
+ if (typeof value !== "string") return undefined;
93
+ const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
94
+ return normalized ? truncateDisplayText(normalized, maxLength) : undefined;
95
+ }
96
+
97
+ function publicTime(value: unknown): number | undefined {
98
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
99
+ }
100
+
101
+ function publicCount(value: unknown): number | undefined {
102
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
103
+ }
104
+
105
+ function normalizeState(value: string): AsyncStatusSnapshotState {
106
+ if (value === "completed") return "complete";
107
+ if (value === "pending") return "queued";
108
+ return value as AsyncStatusSnapshotState;
109
+ }
110
+
111
+ function terminalState(state: AsyncStatusSnapshotState): boolean {
112
+ return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
113
+ }
114
+
115
+ function kindForMode(mode: SubagentRunMode | undefined): AsyncStatusSnapshotKind {
116
+ return mode === "workflow" ? "workflow" : "subagent";
117
+ }
118
+
119
+ function labelForAgents(agents: readonly string[] | undefined, fallback: string, maxLength: number): string {
120
+ if (!agents?.length) return publicText(fallback, fallback, maxLength);
121
+ const visible = agents.slice(0, 3).map((agent) => publicText(agent, "agent", maxLength)).join(", ");
122
+ const suffix = agents.length > 3 ? `, +${agents.length - 3} more` : "";
123
+ return truncateDisplayText(`${visible}${suffix}`, maxLength);
124
+ }
125
+
126
+ function activityFor(source: {
127
+ activityState?: unknown;
128
+ currentTool?: unknown;
129
+ lastActivityAt?: unknown;
130
+ currentToolStartedAt?: unknown;
131
+ turnCount?: unknown;
132
+ toolCount?: unknown;
133
+ }, ctx: BuildContext): AsyncStatusSnapshotActivityV1 | undefined {
134
+ const activity: AsyncStatusSnapshotActivityV1 = {
135
+ ...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
136
+ ...(publicOptionalText(source.currentTool, ctx.caps.maxStringLength) ? { currentTool: publicOptionalText(source.currentTool, ctx.caps.maxStringLength) } : {}),
137
+ ...(publicTime(source.lastActivityAt) !== undefined ? { lastActivityAt: publicTime(source.lastActivityAt) } : {}),
138
+ ...(publicTime(source.currentToolStartedAt) !== undefined ? { currentToolStartedAt: publicTime(source.currentToolStartedAt) } : {}),
139
+ ...(publicCount(source.turnCount) !== undefined ? { turnCount: publicCount(source.turnCount) } : {}),
140
+ ...(publicCount(source.toolCount) !== undefined ? { toolCount: publicCount(source.toolCount) } : {}),
141
+ };
142
+ return Object.keys(activity).length ? activity : undefined;
143
+ }
144
+
145
+ function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: BuildContext): void {
146
+ const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
147
+ children.push(...source.slice(0, remaining));
148
+ ctx.omitted.children += Math.max(0, source.length - remaining);
149
+ }
150
+
151
+ function buildStepNode(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
152
+ const state = normalizeState(step.status);
153
+ const updatedAt = publicTime(step.endedAt) ?? publicTime(step.lastActivityAt) ?? publicTime(step.startedAt);
154
+ const node: AsyncStatusSnapshotNodeV1 = {
155
+ id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
156
+ kind: "step",
157
+ label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
158
+ state,
159
+ ...(publicTime(step.startedAt) !== undefined ? { startedAt: publicTime(step.startedAt) } : {}),
160
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
161
+ ...(terminalState(state) && publicTime(step.endedAt) !== undefined ? { endedAt: publicTime(step.endedAt) } : {}),
162
+ ...(activityFor(step, ctx) ? { activity: activityFor(step, ctx) } : {}),
163
+ };
164
+ if (depth < ctx.caps.maxDepth && step.children?.length) {
165
+ const nested = step.children.map((child, childIndex) => buildNestedNode(child, childIndex, depth + 1, ctx));
166
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
167
+ appendBoundedChildren(bounded, nested, ctx);
168
+ if (bounded.length) node.children = bounded;
169
+ } else if (step.children?.length) {
170
+ ctx.omitted.children += step.children.length;
171
+ }
172
+ return node;
173
+ }
174
+
175
+ function buildNestedNode(child: NestedRunSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
176
+ const state = normalizeState(child.state);
177
+ const updatedAt = publicTime(child.lastUpdate) ?? publicTime(child.endedAt) ?? publicTime(child.lastActivityAt) ?? publicTime(child.startedAt);
178
+ const node: AsyncStatusSnapshotNodeV1 = {
179
+ id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
180
+ kind: kindForMode(child.mode),
181
+ label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
182
+ state,
183
+ ...(publicTime(child.startedAt) !== undefined ? { startedAt: publicTime(child.startedAt) } : {}),
184
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
185
+ ...(terminalState(state) && publicTime(child.endedAt) !== undefined ? { endedAt: publicTime(child.endedAt) } : {}),
186
+ ...(activityFor(child, ctx) ? { activity: activityFor(child, ctx) } : {}),
187
+ };
188
+ if (depth < ctx.caps.maxDepth) {
189
+ const nestedSteps = child.steps?.map((step, stepIndex) => buildStepNode(step, stepIndex, depth + 1, ctx)) ?? [];
190
+ const nestedChildren = child.children?.map((nested, childIndex) => buildNestedNode(nested, childIndex, depth + 1, ctx)) ?? [];
191
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
192
+ appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
193
+ if (bounded.length) node.children = bounded;
194
+ } else {
195
+ ctx.omitted.children += (child.steps?.length ?? 0) + (child.children?.length ?? 0);
196
+ }
197
+ return node;
198
+ }
199
+
200
+ function buildRunNode(job: AsyncJobState, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
201
+ const state = normalizeState(job.status);
202
+ const updatedAt = publicTime(job.updatedAt) ?? publicTime(job.startedAt);
203
+ const node: AsyncStatusSnapshotNodeV1 = {
204
+ id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
205
+ kind: kindForMode(job.mode),
206
+ label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
207
+ state,
208
+ ...(publicTime(job.startedAt) !== undefined ? { startedAt: publicTime(job.startedAt) } : {}),
209
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
210
+ ...(terminalState(state) && updatedAt !== undefined ? { endedAt: updatedAt } : {}),
211
+ ...(activityFor(job, ctx) ? { activity: activityFor(job, ctx) } : {}),
212
+ };
213
+ if (ctx.caps.maxDepth > 0) {
214
+ const stepChildren = job.steps?.map((step, index) => buildStepNode(step, step.index ?? index, 1, ctx)) ?? [];
215
+ const nestedChildren = job.nestedChildren?.map((child, index) => buildNestedNode(child, index, 1, ctx)) ?? [];
216
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
217
+ appendBoundedChildren(bounded, [...stepChildren, ...nestedChildren], ctx);
218
+ if (bounded.length) node.children = bounded;
219
+ } else {
220
+ ctx.omitted.children += (job.steps?.length ?? 0) + (job.nestedChildren?.length ?? 0);
221
+ }
222
+ return node;
223
+ }
224
+
225
+ function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
226
+ return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
227
+ }
228
+
229
+ function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
230
+ while (snapshot.runs.length > 0 && snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) {
231
+ snapshot.runs.pop();
232
+ snapshot.omitted.runs += 1;
233
+ snapshot.omitted.byteLimitExceeded = true;
234
+ }
235
+ if (snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) snapshot.omitted.byteLimitExceeded = true;
236
+ }
237
+
238
+ export function buildAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
239
+ const caps = resolveCaps(options);
240
+ const ctx: BuildContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
241
+ const sorted = [...jobs].sort((left, right) => {
242
+ const leftUpdated = left.updatedAt ?? left.startedAt ?? 0;
243
+ const rightUpdated = right.updatedAt ?? right.startedAt ?? 0;
244
+ return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
245
+ });
246
+ ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
247
+ const snapshot: AsyncStatusSnapshotV1 = {
248
+ kind: ASYNC_STATUS_SNAPSHOT_KIND,
249
+ version: ASYNC_STATUS_SNAPSHOT_VERSION,
250
+ generatedAt: options.generatedAt ?? Date.now(),
251
+ caps,
252
+ omitted: ctx.omitted,
253
+ runs: sorted.slice(0, caps.maxRuns).map((job) => buildRunNode(job, ctx)),
254
+ };
255
+ enforceByteLimit(snapshot);
256
+ return snapshot;
257
+ }
258
+
259
+ export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
260
+ if (!state || !sessionId || state.currentSessionId !== sessionId) return [];
261
+ const jobs = new Map<string, AsyncJobState>();
262
+ for (const job of state.asyncJobs.values()) {
263
+ if (job.sessionId === sessionId) jobs.set(job.asyncId, job);
264
+ }
265
+ for (const job of state.fleetJobs?.values() ?? []) {
266
+ if (job.sessionId === sessionId && !jobs.has(job.asyncId)) jobs.set(job.asyncId, job);
267
+ }
268
+ return [...jobs.values()];
269
+ }
270
+
271
+ export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
272
+ return buildAsyncStatusSnapshot(asyncStatusSnapshotJobsForState(state, sessionId), options);
273
+ }
274
+
275
+ export function encodeAsyncStatusSnapshotWidget(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): string[] {
276
+ return [`${ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX}${JSON.stringify(buildAsyncStatusSnapshot(jobs, options))}`];
277
+ }