pi-subagents 0.65.0 → 0.66.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 (104) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +17 -3
  5. package/docs/configuration.md +34 -0
  6. package/docs/extension-api.md +94 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +11 -5
  10. package/docs/workflows.md +22 -7
  11. package/package.json +4 -1
  12. package/runner-server-preload.mjs +13 -0
  13. package/skills/pi-subagents/SKILL.md +2 -1
  14. package/skills/pi-subagents/references/execution-controls.md +19 -2
  15. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  16. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  17. package/src/agents/advertised-agent-prompt.ts +63 -0
  18. package/src/agents/agent-management.ts +14 -1
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +8 -0
  21. package/src/api/preflight.ts +5 -1
  22. package/src/api/shared-types.ts +1 -1
  23. package/src/api/workflow-resources.ts +6 -0
  24. package/src/extension/config.ts +4 -2
  25. package/src/extension/index.ts +71 -4
  26. package/src/extension/public-execution.ts +0 -1
  27. package/src/extension/rpc.ts +4 -21
  28. package/src/extension/schemas.ts +9 -7
  29. package/src/extension/tool-description.ts +10 -5
  30. package/src/integrations/pi-web-session-liveness.ts +73 -0
  31. package/src/intercom/native-supervisor-channel.ts +102 -88
  32. package/src/intercom/supervisor-ui.ts +3 -2
  33. package/src/missions/workflow-state.ts +37 -16
  34. package/src/runs/background/active-async-capacity.ts +18 -18
  35. package/src/runs/background/async-execution.ts +8 -1
  36. package/src/runs/background/async-job-tracker.ts +35 -3
  37. package/src/runs/background/async-resume.ts +3 -1
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/async-status-snapshot.ts +10 -12
  40. package/src/runs/background/async-status.ts +17 -9
  41. package/src/runs/background/auto-drain.ts +40 -29
  42. package/src/runs/background/chain-root-attachment.ts +8 -0
  43. package/src/runs/background/control-channel.ts +78 -44
  44. package/src/runs/background/notify.ts +88 -12
  45. package/src/runs/background/owned-process-tree.ts +6 -6
  46. package/src/runs/background/process-terminal.ts +23 -23
  47. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  48. package/src/runs/background/run-child-session.ts +62 -33
  49. package/src/runs/background/run-status.ts +75 -5
  50. package/src/runs/background/runner-aliases.ts +46 -8
  51. package/src/runs/background/runner-child-launch.ts +86 -0
  52. package/src/runs/background/stale-run-reconciler.ts +3 -1
  53. package/src/runs/background/subagent-runner.ts +430 -208
  54. package/src/runs/background/subagent-wait.ts +3 -0
  55. package/src/runs/background/wait-completions.ts +4 -0
  56. package/src/runs/foreground/async-steering-action.ts +19 -0
  57. package/src/runs/foreground/execution.ts +116 -26
  58. package/src/runs/foreground/foreground-history.ts +3 -1
  59. package/src/runs/foreground/prompt-audit.ts +9 -5
  60. package/src/runs/foreground/subagent-executor.ts +531 -227
  61. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  62. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  63. package/src/runs/shared/acceptance.ts +16 -3
  64. package/src/runs/shared/agent-contract.ts +1 -1
  65. package/src/runs/shared/async-status-projection.ts +47 -47
  66. package/src/runs/shared/child-hooks.ts +151 -2
  67. package/src/runs/shared/child-launch.ts +18 -13
  68. package/src/runs/shared/child-session.ts +55 -24
  69. package/src/runs/shared/child-tool-plan.ts +2 -2
  70. package/src/runs/shared/completion-evidence.ts +2 -2
  71. package/src/runs/shared/completion-guard.ts +1 -0
  72. package/src/runs/shared/host-step-status.ts +11 -11
  73. package/src/runs/shared/llm-intent-arbiter.ts +30 -20
  74. package/src/runs/shared/model-exclusions.ts +2 -1
  75. package/src/runs/shared/model-fallback.ts +41 -8
  76. package/src/runs/shared/nested-events.ts +8 -8
  77. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  78. package/src/runs/shared/parallel-handoff.ts +57 -12
  79. package/src/runs/shared/parallel-utils.ts +3 -2
  80. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  81. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  82. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  83. package/src/runs/shared/run-fanout-budget.ts +8 -8
  84. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  85. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  86. package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
  87. package/src/runs/shared/worktree-setup-command.ts +190 -0
  88. package/src/runs/shared/worktree.ts +403 -210
  89. package/src/shared/model-response-aliases.ts +13 -0
  90. package/src/shared/types.ts +89 -60
  91. package/src/shared/utils.ts +10 -2
  92. package/src/shared/watch-strategy.ts +2 -0
  93. package/src/shared/workflow-child-permit.ts +18 -13
  94. package/src/tui/fleet-status.ts +1 -1
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +44 -15
  97. package/src/workflows/chat-progress.ts +3 -3
  98. package/src/workflows/scripted-workflow.ts +70 -16
  99. package/src/workflows/workflow-checklist.ts +15 -18
  100. package/src/workflows/workflow-child-summary.ts +57 -8
  101. package/src/workflows/workflow-preflight.ts +19 -19
  102. package/src/workflows/workflow-receipt.ts +3 -3
  103. package/src/workflows/workflow-resources.ts +96 -21
  104. package/src/workflows/workflow-settlement.ts +3 -0
@@ -94,7 +94,7 @@ function workflowResultChildren(status: AsyncStatus, childRunId: string, result:
94
94
  }));
95
95
  }
96
96
 
97
- function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, resolution: WorkflowTerminalResolution | undefined): WorkflowReceipt | undefined {
97
+ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, resolution: WorkflowTerminalResolution | undefined): { receipt: WorkflowReceipt; path: string } | undefined {
98
98
  const receiptRoot = path.dirname(asyncDir);
99
99
  const receiptPath = workflowReceiptPath(receiptRoot, status.runId);
100
100
  if (!fs.existsSync(receiptPath)) return undefined;
@@ -155,8 +155,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
155
155
  delete next.workflowResolution;
156
156
  delete next.recovery;
157
157
  }
158
- writeWorkflowReceipt(asyncDir, next);
159
- return next;
158
+ return { receipt: next, path: writeWorkflowReceipt(asyncDir, next) };
160
159
  }
161
160
 
162
161
  function appendDetachedWorkflowEvent(asyncDir: string, event: Record<string, unknown>): void {
@@ -209,10 +208,13 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
209
208
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
210
209
  }
211
210
  let receipt: WorkflowReceipt | undefined;
211
+ let receiptPath: string | undefined;
212
212
  let receiptError: string | undefined;
213
213
  const resolution = classifyWorkflowSettlement(next, input.result.interrupted);
214
214
  try {
215
- receipt = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir, resolution);
215
+ const publication = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir, resolution);
216
+ receipt = publication?.receipt;
217
+ receiptPath = publication?.path;
216
218
  } catch (error) {
217
219
  receiptError = `Failed to reconcile async workflow receipt: ${error instanceof Error ? error.message : String(error)}`;
218
220
  }
@@ -243,7 +245,7 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
243
245
  completionOwnerId: next.completionOwnerId,
244
246
  },
245
247
  receipt,
246
- receiptPath: path.join(asyncDir, "workflow-receipt.json"),
248
+ receiptPath,
247
249
  receiptPersistenceError: receiptError,
248
250
  resolution,
249
251
  terminalOutcome: receipt?.terminalOutcome,
@@ -283,6 +285,7 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
283
285
  agent: "workflow",
284
286
  success: plan.status.state === "complete",
285
287
  state: plan.status.state,
288
+ ...(plan.publicResult.workflowReceipt ? { workflowReceipt: plan.publicResult.workflowReceipt } : {}),
286
289
  ...(resolution ? { workflowResolution: resolution } : {}),
287
290
  ...(plan.receipt?.terminalOutcome ? { terminalOutcome: plan.receipt.terminalOutcome } : {}),
288
291
  recovery: plan.recovery,
@@ -3,8 +3,9 @@ import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import type { Details, ForegroundRunControl, SubagentState } from "../../shared/types.ts";
5
5
  import { readStatus } from "../../shared/utils.ts";
6
- import { steeringReceipt } from "../background/steering.ts";
7
- import type { SteerDeliveryMode } from "../background/control-channel.ts";
6
+ import { steeringReceipt, waitForSteeringAction } from "../background/steering.ts";
7
+ import { requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
8
+ import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
8
9
 
9
10
  export interface WorkflowForegroundSteeringTarget {
10
11
  control: ForegroundRunControl;
@@ -66,6 +67,58 @@ function managementError(message: string): AgentToolResult<Details> {
66
67
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
67
68
  }
68
69
 
70
+ /** Local controllers remain authoritative; recorded foreign ownership permits enqueue, not takeover or proof of liveness. */
71
+ export async function steerWorkflowRun(input: {
72
+ state: SubagentState;
73
+ runId: string;
74
+ asyncDir: string;
75
+ message: string;
76
+ mode?: SteerDeliveryMode;
77
+ index?: number;
78
+ signal?: AbortSignal;
79
+ ackTimeoutMs?: number;
80
+ }): Promise<AgentToolResult<Details>> {
81
+ const { state, runId, asyncDir } = input;
82
+ const status = readStatus(asyncDir);
83
+ if (!status || status.mode !== "workflow" || status.runId !== runId || path.basename(asyncDir) !== runId) {
84
+ return managementError(`Workflow '${runId}' does not match its resolved run directory.`);
85
+ }
86
+ if (state.workflowControllers?.has(runId)) {
87
+ const route = resolveWorkflowForegroundSteeringTarget({ state, workflowRunId: runId, asyncDirRoot: path.dirname(asyncDir) });
88
+ if (!route.ok) return managementError(route.message);
89
+ return steerWorkflowForegroundTarget({ target: route.target, message: input.message, mode: input.mode, index: input.index });
90
+ }
91
+ if (!state.currentSessionId) return managementError("Workflow steering requires an active parent session.");
92
+ if (status.sessionId !== state.currentSessionId) return managementError(`Workflow '${runId}' was not found in the active session.`);
93
+ if (status.state !== "running" && status.state !== "queued") return managementError(`Workflow '${runId}' is not running or queued and cannot be steered.`);
94
+ if (typeof status.completionOwnerId !== "string" || !status.completionOwnerId.trim()
95
+ || status.completionOwnerId === (state.completionOwnerId ?? currentCompletionOwnerId())
96
+ || [...state.foregroundControls.values()].some((control) => control.parentWorkflowRunId === runId)) {
97
+ return managementError(`Workflow '${runId}' has no live foreground child and is not a recorded foreign workflow.`);
98
+ }
99
+ if (input.index !== undefined) {
100
+ const step = status.steps?.[input.index];
101
+ if (!Number.isInteger(input.index) || input.index < 0 || !step) return managementError(`Workflow '${runId}' index ${input.index} is out of range or invalid.`);
102
+ if (step.status !== "running" && step.status !== "pending") return managementError(`Workflow '${runId}' child ${input.index} is ${step.status} and cannot be steered.`);
103
+ if (typeof step.workflowKey !== "string" || !step.workflowKey.trim()) return managementError(`Workflow '${runId}' child ${input.index} has no projected workflow key.`);
104
+ }
105
+ const requestId = randomUUID();
106
+ try {
107
+ requestAsyncSteer(asyncDir, { id: requestId, message: input.message, mode: input.mode, ...(input.index !== undefined ? { targetIndex: input.index } : {}), source: "steer-action" });
108
+ } catch (error) {
109
+ return managementError(`Failed to queue steering for workflow ${runId}: ${error instanceof Error ? error.message : String(error)}`);
110
+ }
111
+ // Only the owner writes the ledger. No acknowledgment leaves an honest, unaddressed queued receipt.
112
+ const steering = await waitForSteeringAction({ asyncDir, sourceRunId: runId, requestId, timeoutMs: input.ackTimeoutMs ?? 3_000, signal: input.signal })
113
+ ?? { requestId, state: "pending" as const, deliveryStatus: "queued" as const, sourceRunId: runId, targets: input.index === undefined ? [] : [{ index: input.index, state: "pending" as const }] };
114
+ const failed = steering.state === "failed" || steering.state === "partial";
115
+ return {
116
+ content: [{ type: "text", text: steeringReceipt(input.message, `Steering ${failed ? steering.state : steering.deliveryStatus === "delivered" ? "delivered" : "queued"} for workflow ${runId} (request ${requestId}).`) }],
117
+ ...(failed ? { isError: true } : {}),
118
+ details: { mode: "management", results: [], steering },
119
+ };
120
+ }
121
+
69
122
  /**
70
123
  * Steer a live workflow-owned foreground child through its in-process session.
71
124
  * `steer` (and `auto`) interrupt the child at its next safe point; `follow_up`
@@ -91,6 +144,7 @@ export async function steerWorkflowForegroundTarget(input: {
91
144
 
92
145
  const message = input.message.trim();
93
146
  const requestId = randomUUID();
147
+
94
148
  const outcome = await child.steer({ message, ...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}) });
95
149
  const target = outcome.state === "delivered"
96
150
  ? { index, state: "delivered" as const, deliveredAt: Date.now() }
@@ -24,7 +24,7 @@ import type {
24
24
  ChildWatchdogProgress,
25
25
  } from "../../shared/types.ts";
26
26
  import { unresolvedChildWatchdogBlockers } from "../../watchdog/child-status.ts";
27
- import { isAgentContractV1 } from "./agent-contract.ts";
27
+ import { isAgentContract } from "./agent-contract.ts";
28
28
  import { classifyTaskMutationIntent, stripSeverityCompounds, taskMayMutate } from "./task-intent.ts";
29
29
 
30
30
  const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
@@ -202,10 +202,23 @@ export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInp
202
202
  return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
203
203
  }
204
204
  if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
205
- if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
205
+ if (acceptance !== undefined && acceptance !== false) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(gate, acceptance) };
206
206
  return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
207
207
  }
208
208
 
209
+ export function describeGateAcceptanceConflict(gate: unknown, acceptance: unknown): string {
210
+ const render = (value: unknown): string => {
211
+ let encoded: string;
212
+ try {
213
+ encoded = JSON.stringify(value) ?? String(value);
214
+ } catch {
215
+ encoded = String(value);
216
+ }
217
+ return encoded.length > 120 ? `${encoded.slice(0, 120)}...` : encoded;
218
+ };
219
+ return ` Both fields were present: gate=${render(gate)} acceptance=${render(acceptance)}.`;
220
+ }
221
+
209
222
  function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
210
223
  return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
211
224
  }
@@ -422,7 +435,7 @@ export function resolveEffectiveAcceptance(input: {
422
435
  }): ResolvedAcceptanceConfig {
423
436
  const explicit = normalizeAcceptanceInput(input.explicit);
424
437
  const explicitLevel = normalizeLevel(explicit.level);
425
- if (isAgentContractV1(input.agentContract)) {
438
+ if (isAgentContract(input.agentContract)) {
426
439
  const level = explicitAcceptanceCanDisable(explicit) || explicitLevel === "auto"
427
440
  ? "none"
428
441
  : explicitLevel;
@@ -1,6 +1,6 @@
1
1
  import type { AgentContract, EffectsProjection, ExecutionProjection, ReviewProjection, SingleResult } from "../../shared/types.ts";
2
2
 
3
- export function isAgentContractV1(contract: AgentContract | undefined): boolean {
3
+ export function isAgentContract(contract: AgentContract | undefined): boolean {
4
4
  return contract?.version === 1;
5
5
  }
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-text.ts";
2
2
  import { formatModelThinking } from "../../shared/formatters.ts";
3
- import type { AsyncJobState, AsyncJobStep, HostStepFreshnessV1, HostStepMonitorKind, HostStepNodeV1, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../../shared/types.ts";
3
+ import type { AsyncJobState, AsyncJobStep, HostStepFreshness, HostStepMonitorKind, HostStepNode, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLane, WorkflowPreflight } from "../../shared/types.ts";
4
4
  import { HOST_STEP_MAX_COUNT, HOST_STEP_MAX_DETAIL_CHARS, HOST_STEP_MAX_LABEL_CHARS, HOST_STEP_MAX_PROVIDER_CHARS, HOST_STEP_MAX_REASON_CHARS, HOST_STEP_MAX_REF_CHARS, HOST_STEP_MAX_ROLE_CHARS, HOST_STEP_MAX_TARGET_CHARS, hostStepReportName, parseHostStepNode, validHostStepNodes } from "./host-step-status.ts";
5
5
  import { workflowPreflightLaneForRuntimeKey } from "../../workflows/workflow-preflight.ts";
6
6
  import { workflowGraphStageNodes } from "./workflow-graph.ts";
@@ -32,7 +32,7 @@ function isAsyncStatusSnapshotState(value: string): value is AsyncStatusSnapshot
32
32
  return Object.hasOwn(ASYNC_STATUS_SNAPSHOT_STATES, value);
33
33
  }
34
34
 
35
- export interface AsyncStatusSnapshotActivityV1 {
35
+ export interface AsyncStatusSnapshotActivity {
36
36
  state?: string;
37
37
  currentTool?: string;
38
38
  lastActivityAt?: number;
@@ -41,7 +41,7 @@ export interface AsyncStatusSnapshotActivityV1 {
41
41
  toolCount?: number;
42
42
  }
43
43
 
44
- export interface AsyncStatusSnapshotHostStepV1 {
44
+ export interface AsyncStatusSnapshotHostStep {
45
45
  kind: HostStepMonitorKind;
46
46
  provider?: string;
47
47
  role?: string;
@@ -54,7 +54,7 @@ export interface AsyncStatusSnapshotHostStepV1 {
54
54
  report?: string;
55
55
  }
56
56
 
57
- export interface AsyncStatusSnapshotNodeV1 {
57
+ export interface AsyncStatusSnapshotNode {
58
58
  id: string;
59
59
  kind: AsyncStatusSnapshotKind | "host-step";
60
60
  label: string;
@@ -62,12 +62,12 @@ export interface AsyncStatusSnapshotNodeV1 {
62
62
  startedAt?: number;
63
63
  updatedAt?: number;
64
64
  endedAt?: number;
65
- activity?: AsyncStatusSnapshotActivityV1;
66
- hostStep?: AsyncStatusSnapshotHostStepV1;
67
- children?: AsyncStatusSnapshotNodeV1[];
65
+ activity?: AsyncStatusSnapshotActivity;
66
+ hostStep?: AsyncStatusSnapshotHostStep;
67
+ children?: AsyncStatusSnapshotNode[];
68
68
  }
69
69
 
70
- export interface AsyncStatusSnapshotCapsV1 {
70
+ export interface AsyncStatusSnapshotCaps {
71
71
  maxRuns: number;
72
72
  maxChildrenPerNode: number;
73
73
  maxDepth: number;
@@ -75,19 +75,19 @@ export interface AsyncStatusSnapshotCapsV1 {
75
75
  maxSerializedBytes: number;
76
76
  }
77
77
 
78
- export interface AsyncStatusSnapshotOmittedV1 {
78
+ export interface AsyncStatusSnapshotOmitted {
79
79
  runs: number;
80
80
  children: number;
81
81
  byteLimitExceeded: boolean;
82
82
  }
83
83
 
84
- export interface AsyncStatusSnapshotV1 {
84
+ export interface AsyncStatusSnapshot {
85
85
  kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
86
86
  version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
87
87
  generatedAt: number;
88
- caps: AsyncStatusSnapshotCapsV1;
89
- omitted: AsyncStatusSnapshotOmittedV1;
90
- runs: AsyncStatusSnapshotNodeV1[];
88
+ caps: AsyncStatusSnapshotCaps;
89
+ omitted: AsyncStatusSnapshotOmitted;
90
+ runs: AsyncStatusSnapshotNode[];
91
91
  }
92
92
 
93
93
  export interface AsyncStatusSnapshotOptions {
@@ -117,19 +117,19 @@ export interface AsyncStatusWorkflowRow {
117
117
  reasonCode?: string;
118
118
  detail?: string;
119
119
  target?: string;
120
- freshness?: HostStepFreshnessV1;
120
+ freshness?: HostStepFreshness;
121
121
  reportPath?: string;
122
- preflight?: WorkflowPreflightLaneV1;
122
+ preflight?: WorkflowPreflightLane;
123
123
  }
124
124
 
125
125
  interface ProjectionContext {
126
- caps: AsyncStatusSnapshotCapsV1;
127
- omitted: AsyncStatusSnapshotOmittedV1;
126
+ caps: AsyncStatusSnapshotCaps;
127
+ omitted: AsyncStatusSnapshotOmitted;
128
128
  }
129
129
 
130
- function validHostStepList(source: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | undefined): HostStepNodeV1[] {
130
+ function validHostStepList(source: readonly HostStepNode[] | WorkflowGraphSnapshot | undefined): HostStepNode[] {
131
131
  if (source && "nodes" in source) return validHostStepNodes(source);
132
- const hostSteps: HostStepNodeV1[] = [];
132
+ const hostSteps: HostStepNode[] = [];
133
133
  for (const [index, value] of (source ?? []).slice(0, HOST_STEP_MAX_COUNT).entries()) {
134
134
  try {
135
135
  hostSteps.push(parseHostStepNode(value, `hostSteps[${index}]`));
@@ -140,7 +140,7 @@ function validHostStepList(source: readonly HostStepNodeV1[] | WorkflowGraphSnap
140
140
  return hostSteps;
141
141
  }
142
142
 
143
- function resolveCaps(options: AsyncStatusSnapshotOptions): AsyncStatusSnapshotCapsV1 {
143
+ function resolveCaps(options: AsyncStatusSnapshotOptions): AsyncStatusSnapshotCaps {
144
144
  return {
145
145
  maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
146
146
  maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
@@ -199,13 +199,13 @@ function activityFor(source: {
199
199
  currentToolStartedAt?: unknown;
200
200
  turnCount?: unknown;
201
201
  toolCount?: unknown;
202
- }, ctx: ProjectionContext): AsyncStatusSnapshotActivityV1 | undefined {
202
+ }, ctx: ProjectionContext): AsyncStatusSnapshotActivity | undefined {
203
203
  const currentTool = publicOptionalText(source.currentTool, ctx.caps.maxStringLength);
204
204
  const lastActivityAt = publicTime(source.lastActivityAt);
205
205
  const currentToolStartedAt = publicTime(source.currentToolStartedAt);
206
206
  const turnCount = publicCount(source.turnCount);
207
207
  const toolCount = publicCount(source.toolCount);
208
- const activity: AsyncStatusSnapshotActivityV1 = {
208
+ const activity: AsyncStatusSnapshotActivity = {
209
209
  ...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
210
210
  ...(currentTool ? { currentTool } : {}),
211
211
  ...(lastActivityAt !== undefined ? { lastActivityAt } : {}),
@@ -216,19 +216,19 @@ function activityFor(source: {
216
216
  return Object.keys(activity).length ? activity : undefined;
217
217
  }
218
218
 
219
- function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: ProjectionContext): void {
219
+ function appendBoundedChildren(children: AsyncStatusSnapshotNode[], source: readonly AsyncStatusSnapshotNode[], ctx: ProjectionContext): void {
220
220
  const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
221
221
  children.push(...source.slice(0, remaining));
222
222
  ctx.omitted.children += Math.max(0, source.length - remaining);
223
223
  }
224
224
 
225
- function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
225
+ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
226
226
  const state = normalizeState(step.status);
227
227
  const startedAt = publicTime(step.startedAt);
228
228
  const endedAt = publicTime(step.endedAt);
229
229
  const updatedAt = endedAt ?? publicTime(step.lastActivityAt) ?? startedAt;
230
230
  const activity = activityFor(step, ctx);
231
- const node: AsyncStatusSnapshotNodeV1 = {
231
+ const node: AsyncStatusSnapshotNode = {
232
232
  id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
233
233
  kind: "step",
234
234
  label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
@@ -240,7 +240,7 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
240
240
  };
241
241
  if (depth < ctx.caps.maxDepth && step.children?.length) {
242
242
  const nested = step.children.map((child, childIndex) => projectNestedRun(child, childIndex, depth + 1, ctx));
243
- const bounded: AsyncStatusSnapshotNodeV1[] = [];
243
+ const bounded: AsyncStatusSnapshotNode[] = [];
244
244
  appendBoundedChildren(bounded, nested, ctx);
245
245
  if (bounded.length) node.children = bounded;
246
246
  } else if (step.children?.length) {
@@ -249,7 +249,7 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
249
249
  return node;
250
250
  }
251
251
 
252
- function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
252
+ function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
253
253
  const step: AsyncJobStep = {
254
254
  agent: node.agent ?? node.label,
255
255
  status: workflowGraphStepStatus(node.status),
@@ -259,13 +259,13 @@ function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number],
259
259
  return projectStep(step, node.flatIndex ?? index, depth, ctx);
260
260
  }
261
261
 
262
- function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
262
+ function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
263
263
  const state = normalizeState(child.state);
264
264
  const startedAt = publicTime(child.startedAt);
265
265
  const endedAt = publicTime(child.endedAt);
266
266
  const updatedAt = publicTime(child.lastUpdate) ?? endedAt ?? publicTime(child.lastActivityAt) ?? startedAt;
267
267
  const activity = activityFor(child, ctx);
268
- const node: AsyncStatusSnapshotNodeV1 = {
268
+ const node: AsyncStatusSnapshotNode = {
269
269
  id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
270
270
  kind: kindForMode(child.mode),
271
271
  label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
@@ -278,7 +278,7 @@ function projectNestedRun(child: NestedRunSummary, index: number, depth: number,
278
278
  if (depth < ctx.caps.maxDepth) {
279
279
  const nestedSteps = child.steps?.map((step, stepIndex) => projectStep(step, stepIndex, depth + 1, ctx)) ?? [];
280
280
  const nestedChildren = child.children?.map((nested, childIndex) => projectNestedRun(nested, childIndex, depth + 1, ctx)) ?? [];
281
- const bounded: AsyncStatusSnapshotNodeV1[] = [];
281
+ const bounded: AsyncStatusSnapshotNode[] = [];
282
282
  appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
283
283
  if (bounded.length) node.children = bounded;
284
284
  } else {
@@ -295,11 +295,11 @@ function hostStepSnapshotState(state: HostStepState, verdict: HostStepVerdict |
295
295
  return verdict === undefined || verdict === "inconclusive" ? "partial" : "complete";
296
296
  }
297
297
 
298
- function projectHostStep(hostStep: HostStepNodeV1, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
298
+ function projectHostStep(hostStep: HostStepNode, ctx: ProjectionContext): AsyncStatusSnapshotNode {
299
299
  const state = hostStepSnapshotState(hostStep.state, hostStep.verdict);
300
300
  const detail = publicOptionalText(hostStep.detail, ctx.caps.maxStringLength);
301
301
  const report = publicOptionalText(hostStepReportName(hostStep.reportPath), ctx.caps.maxStringLength);
302
- const hostMetadata: AsyncStatusSnapshotHostStepV1 = {
302
+ const hostMetadata: AsyncStatusSnapshotHostStep = {
303
303
  kind: hostStep.monitorKind,
304
304
  state: hostStep.state,
305
305
  ...(hostStep.provider ? { provider: publicText(hostStep.provider, "provider", ctx.caps.maxStringLength) } : {}),
@@ -322,12 +322,12 @@ function projectHostStep(hostStep: HostStepNodeV1, ctx: ProjectionContext): Asyn
322
322
  };
323
323
  }
324
324
 
325
- function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
325
+ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnapshotNode {
326
326
  const state = normalizeState(job.status);
327
327
  const startedAt = publicTime(job.startedAt);
328
328
  const updatedAt = publicTime(job.updatedAt) ?? startedAt;
329
329
  const activity = activityFor(job, ctx);
330
- const node: AsyncStatusSnapshotNodeV1 = {
330
+ const node: AsyncStatusSnapshotNode = {
331
331
  id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
332
332
  kind: kindForMode(job.mode),
333
333
  label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
@@ -349,7 +349,7 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
349
349
  const ordinaryChildren = [...stepChildren, ...graphChildren, ...nestedChildren];
350
350
  const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
351
351
  const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
352
- const bounded: AsyncStatusSnapshotNodeV1[] = [];
352
+ const bounded: AsyncStatusSnapshotNode[] = [];
353
353
  bounded.push(...retainedOrdinaryChildren, ...retainedHostSteps);
354
354
  ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
355
355
  if (bounded.length) node.children = bounded;
@@ -362,11 +362,11 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
362
362
  return node;
363
363
  }
364
364
 
365
- function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
365
+ function snapshotBytes(snapshot: AsyncStatusSnapshot): number {
366
366
  return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
367
367
  }
368
368
 
369
- function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
369
+ function enforceByteLimit(snapshot: AsyncStatusSnapshot): void {
370
370
  if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
371
371
  snapshot.omitted.byteLimitExceeded = true;
372
372
  const runs = snapshot.runs;
@@ -401,7 +401,7 @@ function workflowStepName(step: AsyncJobStep, index: number): string {
401
401
  return `${phase}${key}${label} (${step.agent})`;
402
402
  }
403
403
 
404
- function hostStepRow(hostStep: HostStepNodeV1): AsyncStatusWorkflowRow {
404
+ function hostStepRow(hostStep: HostStepNode): AsyncStatusWorkflowRow {
405
405
  const freshness = hostStep.freshness
406
406
  ? {
407
407
  expectedRef: publicText(hostStep.freshness.expectedRef, "ref", HOST_STEP_MAX_REF_CHARS),
@@ -425,11 +425,11 @@ function hostStepRow(hostStep: HostStepNodeV1): AsyncStatusWorkflowRow {
425
425
  }
426
426
 
427
427
 
428
- function isWorkflowPreflight(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowPreflightV1 {
428
+ function isWorkflowPreflight(value: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight | undefined): value is WorkflowPreflight {
429
429
  return value !== undefined && !Array.isArray(value) && "lanes" in value;
430
430
  }
431
431
 
432
- function isWorkflowGraph(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowGraphSnapshot {
432
+ function isWorkflowGraph(value: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight | undefined): value is WorkflowGraphSnapshot {
433
433
  return value !== undefined && !Array.isArray(value) && "nodes" in value;
434
434
  }
435
435
 
@@ -465,7 +465,7 @@ function workflowGraphRowName(node: WorkflowGraphSnapshot["nodes"][number]): str
465
465
  return `${phase ? `${phase}: ` : ""}${key}${label && label !== node.id ? ` · ${label}` : ""}${agent ? ` (${agent})` : ""}`;
466
466
  }
467
467
 
468
- function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
468
+ function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLane): AsyncStatusWorkflowRow {
469
469
  return {
470
470
  name: workflowGraphRowName(node),
471
471
  state: workflowGraphRowState(node.status),
@@ -476,14 +476,14 @@ function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], p
476
476
  /** Project authoritative workflow facts into compact rows, annotated by preflight hints. */
477
477
  export function projectAsyncWorkflowRows(
478
478
  steps: readonly AsyncJobStep[] | undefined,
479
- hostStepsOrPreflight?: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1,
480
- preflightOverride?: WorkflowPreflightV1,
479
+ hostStepsOrPreflight?: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight,
480
+ preflightOverride?: WorkflowPreflight,
481
481
  ): AsyncStatusWorkflowRow[] {
482
482
  const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined);
483
483
  const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined;
484
484
  const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? undefined : hostStepsOrPreflight;
485
485
  const loaded = steps ?? [];
486
- const preflightForKey = (key: string, groupKeys: readonly (string | undefined)[] = []): WorkflowPreflightLaneV1 | undefined =>
486
+ const preflightForKey = (key: string, groupKeys: readonly (string | undefined)[] = []): WorkflowPreflightLane | undefined =>
487
487
  workflowPreflightLaneForRuntimeKey(preflight, key, groupKeys);
488
488
  if (graph) {
489
489
  const loadedIndexesByKey = new Map<string, number[]>();
@@ -526,7 +526,7 @@ export function projectAsyncWorkflowRows(
526
526
  ];
527
527
  }
528
528
 
529
- function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
529
+ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?: WorkflowPreflightLane): AsyncStatusWorkflowRow {
530
530
  const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
531
531
  const activity = workflowStepActivity(step);
532
532
  return {
@@ -543,7 +543,7 @@ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
543
543
  }
544
544
 
545
545
  /** Project already-loaded async status facts into the bounded public snapshot shape. */
546
- export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
546
+ export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshot {
547
547
  const caps = resolveCaps(options);
548
548
  const ctx: ProjectionContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
549
549
  const sorted = [...jobs].sort((left, right) => {
@@ -552,7 +552,7 @@ export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, option
552
552
  return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
553
553
  });
554
554
  ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
555
- const snapshot: AsyncStatusSnapshotV1 = {
555
+ const snapshot: AsyncStatusSnapshot = {
556
556
  kind: ASYNC_STATUS_SNAPSHOT_KIND,
557
557
  version: ASYNC_STATUS_SNAPSHOT_VERSION,
558
558
  generatedAt: options.generatedAt ?? Date.now(),