pi-subagents 0.35.1 → 0.37.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 (89) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +224 -31
  3. package/agents/advisor.md +73 -0
  4. package/agents/delegate.md +2 -0
  5. package/agents/worker.md +2 -0
  6. package/package.json +11 -13
  7. package/skills/pi-subagents/SKILL.md +42 -13
  8. package/src/agents/agent-management.ts +7 -1
  9. package/src/agents/agents.ts +121 -17
  10. package/src/api/capability-ceiling.ts +17 -0
  11. package/src/api/delegation.ts +127 -0
  12. package/src/api/preflight.ts +399 -0
  13. package/src/extension/config.ts +7 -1
  14. package/src/extension/index.ts +52 -38
  15. package/src/extension/rpc.ts +31 -2
  16. package/src/extension/schemas.ts +29 -3
  17. package/src/extension/tool-description.ts +4 -2
  18. package/src/intercom/intercom-bridge.ts +1 -1
  19. package/src/intercom/native-supervisor-channel.ts +45 -6
  20. package/src/intercom/result-intercom.ts +7 -0
  21. package/src/runs/background/async-execution.ts +158 -17
  22. package/src/runs/background/async-job-tracker.ts +4 -0
  23. package/src/runs/background/async-resume.ts +47 -8
  24. package/src/runs/background/async-status.ts +94 -3
  25. package/src/runs/background/chain-append.ts +2 -0
  26. package/src/runs/background/completion-batcher.ts +6 -4
  27. package/src/runs/background/completion-dedupe.ts +2 -11
  28. package/src/runs/background/fleet-view.ts +9 -4
  29. package/src/runs/background/notify.ts +132 -120
  30. package/src/runs/background/process-terminal.ts +280 -0
  31. package/src/runs/background/result-watcher.ts +138 -78
  32. package/src/runs/background/run-status.ts +10 -2
  33. package/src/runs/background/scheduled-runs.ts +6 -1
  34. package/src/runs/background/stale-run-reconciler.ts +6 -0
  35. package/src/runs/background/subagent-runner.ts +406 -54
  36. package/src/runs/background/subagent-wait.ts +130 -4
  37. package/src/runs/background/wait-tool.ts +2 -2
  38. package/src/runs/foreground/chain-execution.ts +181 -111
  39. package/src/runs/foreground/execution.ts +147 -47
  40. package/src/runs/foreground/foreground-control.ts +90 -0
  41. package/src/runs/foreground/subagent-executor.ts +426 -165
  42. package/src/runs/shared/acceptance.ts +94 -41
  43. package/src/runs/shared/agent-contract.ts +38 -0
  44. package/src/runs/shared/capability-ceiling.ts +177 -0
  45. package/src/runs/shared/child-protocol.ts +1 -1
  46. package/src/runs/shared/completion-guard.ts +36 -5
  47. package/src/runs/shared/context-mode.ts +44 -0
  48. package/src/runs/shared/dynamic-fanout.ts +5 -5
  49. package/src/runs/shared/long-running-guard.ts +4 -0
  50. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  51. package/src/runs/shared/nested-events.ts +35 -3
  52. package/src/runs/shared/parallel-handoff.ts +154 -0
  53. package/src/runs/shared/parallel-utils.ts +11 -0
  54. package/src/runs/shared/pi-args.ts +148 -56
  55. package/src/runs/shared/run-history.ts +90 -5
  56. package/src/runs/shared/session-lease.ts +25 -5
  57. package/src/runs/shared/structured-output.ts +112 -7
  58. package/src/runs/shared/subagent-control.ts +4 -0
  59. package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
  60. package/src/runs/shared/task-intent.ts +10 -5
  61. package/src/runs/shared/tool-availability.ts +21 -3
  62. package/src/runs/shared/tool-budget.ts +11 -5
  63. package/src/runs/shared/turn-budget.ts +2 -1
  64. package/src/runs/shared/worktree.ts +63 -14
  65. package/src/shared/accessible-dir.ts +25 -0
  66. package/src/shared/artifacts.ts +37 -7
  67. package/src/shared/atomic-json.ts +14 -42
  68. package/src/shared/child-transcript.ts +52 -0
  69. package/src/shared/file-system-retry.ts +47 -0
  70. package/src/shared/launch-contract.ts +123 -0
  71. package/src/shared/settings.ts +9 -1
  72. package/src/shared/types.ts +314 -28
  73. package/src/shared/utils.ts +17 -42
  74. package/src/slash/delegation-adapters.ts +153 -6
  75. package/src/slash/delegation-json.ts +108 -0
  76. package/src/slash/delegation-request.ts +182 -36
  77. package/src/slash/prompt-template-bridge.ts +222 -37
  78. package/src/slash/selector.ts +147 -0
  79. package/src/slash/slash-commands.ts +15 -6
  80. package/src/slash/slash-live-state.ts +2 -2
  81. package/src/slash/subagents-admin.ts +42 -42
  82. package/src/tui/fleet-status.ts +362 -0
  83. package/src/tui/fleet-transcript.ts +472 -0
  84. package/src/tui/fleet.ts +318 -59
  85. package/src/tui/render.ts +17 -15
  86. package/src/watchdog/change-signature.ts +105 -12
  87. package/src/watchdog/review.ts +7 -2
  88. package/src/watchdog/runtime.ts +5 -3
  89. package/src/slash/subagents-editor.ts +0 -86
@@ -1,9 +1,10 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
3
+ import { ASYNC_DIR, RESULTS_DIR, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
4
4
  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
+ import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
7
8
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
8
9
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
9
10
 
@@ -38,6 +39,8 @@ export type AsyncResumeTarget = {
38
39
  model?: string;
39
40
  thinking?: string;
40
41
  recoveryDescriptor?: SteeringRecoveryDescriptor;
42
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
43
+ launchContractDigest?: string;
41
44
  };
42
45
 
43
46
  interface AsyncResultFile {
@@ -52,7 +55,9 @@ interface AsyncResultFile {
52
55
  sessionFile?: string;
53
56
  model?: string;
54
57
  thinking?: string;
55
- results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string }>;
58
+ launchContractDigest?: string;
59
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
60
+ results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling }>;
56
61
  }
57
62
 
58
63
  export interface AsyncRunLocation {
@@ -92,9 +97,11 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
92
97
  const intercomTarget = validateOptionalString(child, "intercomTarget", resultPath, `results[${index}].intercomTarget`);
93
98
  const model = validateOptionalString(child, "model", resultPath, `results[${index}].model`);
94
99
  const thinking = validateOptionalString(child, "thinking", resultPath, `results[${index}].thinking`);
100
+ const launchContractDigest = validateOptionalString(child, "launchContractDigest", resultPath, `results[${index}].launchContractDigest`);
101
+ const capabilityCeiling = child.capabilityCeiling === undefined ? undefined : parseSubagentCapabilityCeiling(child.capabilityCeiling, `async result file '${resultPath}' results[${index}].capabilityCeiling`);
95
102
  const success = child.success;
96
103
  if (success !== undefined && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': results[${index}].success must be a boolean.`);
97
- return { agent, sessionFile, intercomTarget, model, thinking, ...(typeof success === "boolean" ? { success } : {}) };
104
+ return { agent, sessionFile, intercomTarget, model, thinking, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(typeof success === "boolean" ? { success } : {}) };
98
105
  });
99
106
  }
100
107
  const success = data.success;
@@ -110,6 +117,8 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
110
117
  sessionFile: validateOptionalString(data, "sessionFile", resultPath),
111
118
  model: validateOptionalString(data, "model", resultPath),
112
119
  thinking: validateOptionalString(data, "thinking", resultPath),
120
+ launchContractDigest: validateOptionalString(data, "launchContractDigest", resultPath),
121
+ ...(data.capabilityCeiling === undefined ? {} : { capabilityCeiling: parseSubagentCapabilityCeiling(data.capabilityCeiling, `async result file '${resultPath}' capabilityCeiling`) }),
113
122
  ...(typeof success === "boolean" ? { success } : {}),
114
123
  ...(results ? { results } : {}),
115
124
  };
@@ -237,6 +246,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
237
246
  if (status.sessionId !== undefined && typeof status.sessionId !== "string") throw new Error(`Invalid async status '${source}': sessionId must be a string.`);
238
247
  if (status.cwd !== undefined && typeof status.cwd !== "string") throw new Error(`Invalid async status '${source}': cwd must be a string.`);
239
248
  if (status.sessionFile !== undefined && typeof status.sessionFile !== "string") throw new Error(`Invalid async status '${source}': sessionFile must be a string.`);
249
+ if (status.capabilityCeiling !== undefined) status.capabilityCeiling = parseSubagentCapabilityCeiling(status.capabilityCeiling, `async status '${source}' capabilityCeiling`);
240
250
  if (status.steps !== undefined) {
241
251
  if (!Array.isArray(status.steps)) throw new Error(`Invalid async status '${source}': steps must be an array.`);
242
252
  status.steps.forEach((step, index) => {
@@ -246,10 +256,22 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
246
256
  if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
247
257
  if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
248
258
  if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
259
+ if (stepRecord.capabilityCeiling !== undefined) stepRecord.capabilityCeiling = parseSubagentCapabilityCeiling(stepRecord.capabilityCeiling, `async status '${source}' steps[${index}].capabilityCeiling`);
249
260
  });
250
261
  }
251
262
  }
252
263
 
264
+ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput | undefined {
265
+ if (value && typeof value === "object" && !Array.isArray(value) && ("explicit" in value || "inferredReason" in value)) {
266
+ const { explicit, inferredReason: _inferredReason, ...publicAcceptance } = value as Record<string, unknown>;
267
+ if (explicit === false) return undefined;
268
+ value = publicAcceptance;
269
+ }
270
+ const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
271
+ if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
272
+ return value as AcceptanceInput;
273
+ }
274
+
253
275
  export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
254
276
  if (!asyncDir) return undefined;
255
277
  const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
@@ -263,10 +285,10 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
263
285
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
264
286
  const parsed = value as Record<string, unknown>;
265
287
  const allowedFields = new Set([
266
- "version", "sourceRunId", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
288
+ "version", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
267
289
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
268
- "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "acceptance", "sessionDir", "artifactConfig",
269
- "artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share",
290
+ "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
291
+ "artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
270
292
  ]);
271
293
  for (const field of Object.keys(parsed)) {
272
294
  if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
@@ -276,6 +298,12 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
276
298
  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.`);
277
299
  }
278
300
  if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
301
+ if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
302
+ if (parsed.agentContract !== undefined) {
303
+ if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
304
+ const contract = parsed.agentContract as Record<string, unknown>;
305
+ if (contract.version !== 1 || Object.keys(contract).some((key) => key !== "version")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be { version: 1 }.`);
306
+ }
279
307
  if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
280
308
  if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
281
309
  for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
@@ -291,6 +319,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
291
319
  if (parsed[field] !== undefined && (typeof parsed[field] !== "string" || !(parsed[field] as string).trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
292
320
  }
293
321
  if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
322
+ if (parsed.structuredOutputSchema !== undefined && (!parsed.structuredOutputSchema || typeof parsed.structuredOutputSchema !== "object" || Array.isArray(parsed.structuredOutputSchema))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': structuredOutputSchema must be an object.`);
294
323
  if (parsed.memory !== undefined) {
295
324
  if (!parsed.memory || typeof parsed.memory !== "object" || Array.isArray(parsed.memory)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': memory must be an object.`);
296
325
  const memory = parsed.memory as Record<string, unknown>;
@@ -335,8 +364,9 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
335
364
  if (!Array.isArray(control.notifyChannels) || control.notifyChannels.some((item) => item !== "event" && item !== "async" && item !== "intercom")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyChannels is invalid.`);
336
365
  }
337
366
  if (parsed.acceptance !== undefined) {
338
- const errors = validateAcceptanceInput(parsed.acceptance, "recoveryDescriptor.acceptance");
339
- if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
367
+ const acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
368
+ if (acceptance === undefined) delete parsed.acceptance;
369
+ else parsed.acceptance = acceptance;
340
370
  }
341
371
  return parsed as unknown as SteeringRecoveryDescriptor;
342
372
  }
@@ -385,6 +415,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
385
415
  if (requestedIndex < 0 || requestedIndex >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${requestedIndex} is out of range.`);
386
416
  const selectedStep = statusSteps[requestedIndex];
387
417
  if (selectedStep?.status === "running") {
418
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selectedStep.capabilityCeiling);
388
419
  return {
389
420
  kind: "live",
390
421
  runId,
@@ -396,6 +427,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
396
427
  sessionFile: selectedStep.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
397
428
  model: selectedStep.model,
398
429
  thinking: selectedStep.thinking,
430
+ launchContractDigest: selectedStep.launchContractDigest ?? result?.results?.[requestedIndex]?.launchContractDigest ?? result?.launchContractDigest,
431
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
399
432
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
400
433
  };
401
434
  }
@@ -409,6 +442,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
409
442
  if (!selected) {
410
443
  throw new Error(`Async run '${runId}' has ${running.length} running children. Provide index to choose one.`);
411
444
  }
445
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selected.step.capabilityCeiling);
412
446
  return {
413
447
  kind: "live",
414
448
  runId,
@@ -420,6 +454,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
420
454
  sessionFile: selected.step.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
421
455
  model: selected.step.model,
422
456
  thinking: selected.step.thinking,
457
+ launchContractDigest: selected.step.launchContractDigest ?? result?.results?.[selected.index]?.launchContractDigest ?? result?.launchContractDigest,
458
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
423
459
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
424
460
  };
425
461
  }
@@ -441,6 +477,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
441
477
  const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
442
478
  const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
443
479
  const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
480
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
444
481
 
445
482
  return {
446
483
  kind: "revive",
@@ -453,6 +490,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
453
490
  ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
454
491
  ...(stepModel ? { model: stepModel } : {}),
455
492
  ...(stepThinking ? { thinking: stepThinking } : {}),
493
+ launchContractDigest: statusSteps[index]?.launchContractDigest ?? resultSteps[index]?.launchContractDigest ?? result?.launchContractDigest,
494
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
456
495
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
457
496
  };
458
497
  }
@@ -3,15 +3,19 @@ import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
5
  import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
6
+ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
6
7
  import { readStatus } from "../../shared/utils.ts";
7
8
  import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
8
9
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
9
10
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
11
+ import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
10
12
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
13
+ import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
11
14
 
12
15
  interface AsyncRunStepSummary {
13
16
  index: number;
14
17
  agent: string;
18
+ context?: ContextMode;
15
19
  label?: string;
16
20
  phase?: string;
17
21
  outputName?: string;
@@ -35,12 +39,22 @@ interface AsyncRunStepSummary {
35
39
  model?: string;
36
40
  thinking?: string;
37
41
  attemptedModels?: string[];
42
+ sessionFile?: string;
43
+ transcriptPath?: string;
38
44
  error?: string;
39
45
  timedOut?: boolean;
40
46
  stopped?: boolean;
41
47
  turnBudget?: TurnBudgetState;
42
48
  turnBudgetExceeded?: boolean;
43
49
  wrapUpRequested?: boolean;
50
+ acceptance?: AsyncJobStep["acceptance"];
51
+ agentContract?: AsyncJobStep["agentContract"];
52
+ execution?: AsyncJobStep["execution"];
53
+ review?: AsyncJobStep["review"];
54
+ effects?: AsyncJobStep["effects"];
55
+ processTerminal?: AsyncJobStep["processTerminal"];
56
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
57
+ capabilityAudit?: SubagentCapabilityAudit;
44
58
  children?: NestedRunSummary[];
45
59
  }
46
60
 
@@ -59,6 +73,7 @@ export interface AsyncRunSummary {
59
73
  toolCount?: number;
60
74
  steering?: SteeringStatus;
61
75
  mode: SubagentRunMode;
76
+ context?: ContextSummary;
62
77
  cwd?: string;
63
78
  startedAt: number;
64
79
  lastUpdate?: number;
@@ -82,6 +97,9 @@ export interface AsyncRunSummary {
82
97
  sessionFile?: string;
83
98
  nestedChildren?: NestedRunSummary[];
84
99
  nestedWarnings?: string[];
100
+ processTerminal?: AsyncStatus["processTerminal"];
101
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
102
+ capabilityAudit?: SubagentCapabilityAudit;
85
103
  }
86
104
 
87
105
  interface AsyncRunListOptions {
@@ -92,6 +110,7 @@ interface AsyncRunListOptions {
92
110
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
93
111
  now?: () => number;
94
112
  reconcile?: boolean;
113
+ runId?: string;
95
114
  }
96
115
 
97
116
  function getErrorMessage(error: unknown): string {
@@ -117,6 +136,45 @@ function isAsyncRunDir(root: string, entry: string): boolean {
117
136
  }
118
137
  }
119
138
 
139
+ type TargetedAsyncRunResolution =
140
+ | { kind: "exact"; id: string }
141
+ | { kind: "scan" }
142
+ | { kind: "reject" };
143
+
144
+ /**
145
+ * Resolve an exact targeted run without following a run-directory symlink or
146
+ * accepting a path whose canonical location escaped the async root.
147
+ */
148
+ export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessionId?: string): TargetedAsyncRunResolution {
149
+ if (!id || id === "." || id === ".." || path.basename(id) !== id) return { kind: "reject" };
150
+ const asyncDir = path.join(asyncDirRoot, id);
151
+ let entryStat: fs.Stats;
152
+ try {
153
+ entryStat = fs.lstatSync(asyncDir);
154
+ } catch (error) {
155
+ if (isNotFoundError(error)) return { kind: "scan" };
156
+ throw new Error(`Failed to inspect async run path '${asyncDir}': ${getErrorMessage(error)}`, {
157
+ cause: error instanceof Error ? error : undefined,
158
+ });
159
+ }
160
+ if (!entryStat.isDirectory() || entryStat.isSymbolicLink()) return { kind: "reject" };
161
+ try {
162
+ const canonicalRoot = fs.realpathSync(asyncDirRoot);
163
+ const canonicalDir = fs.realpathSync(asyncDir);
164
+ if (canonicalDir !== canonicalRoot && !canonicalDir.startsWith(`${canonicalRoot}${path.sep}`)) return { kind: "reject" };
165
+ } catch (error) {
166
+ if (isNotFoundError(error)) return { kind: "reject" };
167
+ throw new Error(`Failed to resolve async run path '${asyncDir}': ${getErrorMessage(error)}`, {
168
+ cause: error instanceof Error ? error : undefined,
169
+ });
170
+ }
171
+ if (sessionId !== undefined) {
172
+ const status = readStatus(asyncDir);
173
+ if (status?.sessionId !== sessionId) return { kind: "scan" };
174
+ }
175
+ return { kind: "exact", id };
176
+ }
177
+
120
178
  function outputFileMtime(outputFile: string | undefined): number | undefined {
121
179
  if (!outputFile) return undefined;
122
180
  try {
@@ -144,6 +202,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
144
202
  throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
145
203
  }
146
204
  const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
205
+ const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
206
+ ?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
147
207
  const steps = status.steps ?? [];
148
208
  const chainStepCount = status.chainStepCount ?? steps.length;
149
209
  const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
@@ -163,6 +223,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
163
223
  return {
164
224
  index,
165
225
  agent: step.agent,
226
+ ...(step.context ? { context: step.context } : {}),
166
227
  ...(step.label ? { label: step.label } : {}),
167
228
  ...(step.phase ? { phase: step.phase } : {}),
168
229
  ...(step.outputName ? { outputName: step.outputName } : {}),
@@ -186,12 +247,23 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
186
247
  ...(step.model ? { model: step.model } : {}),
187
248
  ...(step.thinking ? { thinking: step.thinking } : {}),
188
249
  ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
250
+ ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
251
+ ...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
189
252
  ...(step.error ? { error: step.error } : {}),
190
253
  ...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
191
254
  ...(step.stopped !== undefined ? { stopped: step.stopped } : {}),
192
255
  ...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
193
256
  ...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
194
257
  ...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
258
+ ...(step.acceptance ? { acceptance: step.acceptance } : {}),
259
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
260
+ ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
261
+ ...(step.execution ? { execution: step.execution } : {}),
262
+ ...(step.review ? { review: step.review } : {}),
263
+ ...(step.effects ? { effects: step.effects } : {}),
264
+ ...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
265
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
266
+ ...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
195
267
  ...(step.children?.length ? { children: step.children } : {}),
196
268
  };
197
269
  });
@@ -211,6 +283,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
211
283
  toolCount: status.toolCount,
212
284
  steering: status.steering,
213
285
  mode: status.mode,
286
+ ...(summarizeContextModes(summarizedSteps.map((step) => step.context)) ? { context: summarizeContextModes(summarizedSteps.map((step) => step.context)) } : {}),
214
287
  cwd: status.cwd,
215
288
  startedAt: status.startedAt,
216
289
  lastUpdate: status.lastUpdate,
@@ -229,6 +302,10 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
229
302
  steps: summarizedSteps,
230
303
  ...(nestedChildren.length ? { nestedChildren } : {}),
231
304
  ...(nestedWarnings.length ? { nestedWarnings } : {}),
305
+ ...(processTerminal ? { processTerminal } : {}),
306
+ ...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
307
+ ...(status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {}),
308
+ ...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
232
309
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
233
310
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
234
311
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
@@ -264,7 +341,19 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
264
341
  export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
265
342
  let entries: string[];
266
343
  try {
267
- entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
344
+ if (options.runId !== undefined) {
345
+ const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
346
+ entries = resolution.kind === "exact"
347
+ ? [resolution.id]
348
+ : resolution.kind === "scan"
349
+ ? fs.readdirSync(asyncDirRoot).filter((entry) =>
350
+ (entry === options.runId || entry.startsWith(options.runId!))
351
+ && resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
352
+ )
353
+ : [];
354
+ } else {
355
+ entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
356
+ }
268
357
  } catch (error) {
269
358
  if (isNotFoundError(error)) return [];
270
359
  throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
@@ -333,8 +422,9 @@ function formatActivityFacts(input: { activityState?: ActivityState; lastActivit
333
422
 
334
423
  function formatStepLine(step: AsyncRunStepSummary): string {
335
424
  const display = step.label ? `${step.label} (${step.agent})` : step.agent;
425
+ const context = contextModeLabel(step.context);
336
426
  const phase = step.phase ? `[${step.phase}] ` : "";
337
- const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
427
+ const parts = [`${step.index + 1}. ${phase}${display}${context ? ` ${context}` : ""}`, step.status];
338
428
  const activity = formatActivityFacts(step);
339
429
  if (activity) parts.push(activity);
340
430
  const modelThinking = formatModelThinking(step.model, step.thinking);
@@ -375,7 +465,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
375
465
  const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
376
466
  const activity = formatActivityFacts(run);
377
467
  const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
378
- return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel}${pending} | ${cwd}`;
468
+ const context = contextModeLabel(run.context);
469
+ return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${context ? ` ${context}` : ""} | ${stepLabel}${pending} | ${cwd}`;
379
470
  }
380
471
 
381
472
  export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
@@ -131,6 +131,7 @@ export function consumeChainAppendRequests(asyncDir: string): ChainAppendRequest
131
131
  function statusStepForTask(task: RunnerSubagentStep): StatusStep {
132
132
  return {
133
133
  agent: task.agent,
134
+ ...(task.context ? { context: task.context } : {}),
134
135
  phase: task.phase,
135
136
  label: task.label,
136
137
  outputName: task.outputName,
@@ -151,6 +152,7 @@ function statusStepsForRunnerStep(step: RunnerStep): StatusStep[] {
151
152
  if (isDynamicRunnerGroup(step)) {
152
153
  return [{
153
154
  agent: `expand:${step.parallel.agent}`,
155
+ ...(step.parallel.context ? { context: step.parallel.context } : {}),
154
156
  phase: step.phase ?? step.parallel.phase,
155
157
  label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
156
158
  outputName: step.collect.as,
@@ -89,8 +89,8 @@ export interface CompletionBatcher<T> {
89
89
  push(item: T): void;
90
90
  /** Emit any held items immediately as a single group. */
91
91
  flush(): void;
92
- /** Clear timers without emitting. */
93
- dispose(): void;
92
+ /** Clear timers and return items that were never emitted. */
93
+ dispose(): T[];
94
94
  }
95
95
 
96
96
  /**
@@ -109,7 +109,7 @@ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>)
109
109
  options.emit([item]);
110
110
  },
111
111
  flush() {},
112
- dispose() {},
112
+ dispose() { return []; },
113
113
  };
114
114
  }
115
115
 
@@ -160,7 +160,9 @@ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>)
160
160
  flush: emitGroup,
161
161
  dispose() {
162
162
  clearTimers();
163
+ const abandoned = pending;
163
164
  pending = [];
164
- },
165
+ return abandoned;
166
+ }
165
167
  };
166
168
  }
@@ -20,9 +20,9 @@ function asFiniteNumber(value: unknown): number | undefined {
20
20
  }
21
21
 
22
22
  export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
23
- const id = asNonEmptyString(data.id);
24
- if (id) return `id:${id}`;
25
23
  const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
24
+ const id = asNonEmptyString(data.id);
25
+ if (id) return `session:${sessionId}:id:${id}`;
26
26
  const agent = asNonEmptyString(data.agent) ?? "unknown";
27
27
  const timestamp = asFiniteNumber(data.timestamp);
28
28
  const taskIndex = asFiniteNumber(data.taskIndex);
@@ -52,12 +52,3 @@ export function markSeenWithTtl(seen: Map<string, number>, key: string, now: num
52
52
  seen.set(key, now);
53
53
  return false;
54
54
  }
55
-
56
- export function getGlobalSeenMap(storeKey: string): Map<string, number> {
57
- const globalStore = globalThis as Record<string, unknown>;
58
- const existing = globalStore[storeKey];
59
- if (existing instanceof Map) return existing as Map<string, number>;
60
- const map = new Map<string, number>();
61
- globalStore[storeKey] = map;
62
- return map;
63
- }
@@ -16,6 +16,7 @@ import {
16
16
  } from "../../shared/types.ts";
17
17
  import { readStatus } from "../../shared/utils.ts";
18
18
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
19
+ import { contextModeLabel, summarizeContextModes } from "../shared/context-mode.ts";
19
20
  import { formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
20
21
 
21
22
  const DEFAULT_TRANSCRIPT_LINES = 80;
@@ -277,15 +278,17 @@ function formatAsyncFleetLines(runs: AsyncRunSummary[]): string[] {
277
278
  const activity = formatActivityFacts(run);
278
279
  const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
279
280
  const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
280
- lines.push(`- ${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${progress}${pending} | ${cwd}`);
281
+ const runContext = contextModeLabel(run.context);
282
+ lines.push(`- ${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${runContext ? ` ${runContext}` : ""} | ${progress}${pending} | ${cwd}`);
281
283
  lines.push(` status: subagent({ action: "status", id: "${run.id}" })`);
282
284
  lines.push(` transcript: subagent({ action: "status", id: "${run.id}", view: "transcript" })`);
283
285
  for (const step of run.steps) {
284
286
  const display = step.label ? `${step.label} (${step.agent})` : step.agent;
287
+ const stepContext = contextModeLabel(step.context);
285
288
  const phase = step.phase ? `[${step.phase}] ` : "";
286
289
  const stepActivity = formatActivityFacts(step);
287
290
  const modelThinking = formatModelThinking(step.model, step.thinking);
288
- const parts = [`${step.index}. ${phase}${display}`, step.status, stepActivity, modelThinking].filter(Boolean);
291
+ const parts = [`${step.index}. ${phase}${display}${stepContext ? ` ${stepContext}` : ""}`, step.status, stepActivity, modelThinking].filter(Boolean);
289
292
  lines.push(` ${parts.join(" | ")}`);
290
293
  const output = path.join(run.asyncDir, `output-${step.index}.log`);
291
294
  if (fs.existsSync(output)) lines.push(` output: ${shortenPath(output)}`);
@@ -385,8 +388,9 @@ function selectTranscriptStep(status: AsyncStatus, options: TranscriptOptions):
385
388
  function stepStateLine(mode: SubagentRunMode, index: number | undefined, step: AsyncJobStep | undefined): string | undefined {
386
389
  if (index === undefined || !step) return undefined;
387
390
  const modelThinking = formatModelThinking(step.model, step.thinking);
391
+ const context = contextModeLabel(step.context);
388
392
  const parts = [
389
- `${mode === "parallel" ? "Agent" : "Step"}: ${index} (${step.agent})`,
393
+ `${mode === "parallel" ? "Agent" : "Step"}: ${index} (${step.agent})${context ? ` ${context}` : ""}`,
390
394
  step.status,
391
395
  formatActivityFacts(step),
392
396
  modelThinking,
@@ -428,10 +432,11 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
428
432
  const sessionFile = selected.index !== undefined ? selected.step?.sessionFile : status.sessionFile;
429
433
  const eventsPath = path.join(asyncDir, "events.jsonl");
430
434
 
435
+ const context = contextModeLabel(status.context ?? summarizeContextModes((status.steps ?? []).map((step) => step.context)));
431
436
  const lines = [
432
437
  `Run: ${status.runId}`,
433
438
  `State: ${status.state}`,
434
- `Mode: ${status.mode}`,
439
+ `Mode: ${status.mode}${context ? ` ${context}` : ""}`,
435
440
  stepStateLine(status.mode, selected.index, selected.step),
436
441
  selected.hint,
437
442
  ].filter((line): line is string => Boolean(line));