pi-subagents 0.58.0 → 0.59.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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -15,9 +15,22 @@ import {
15
15
  resolveCompletionBatchConfig,
16
16
  } from "./completion-batcher.ts";
17
17
  import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type ScheduleOrigin, type SubagentState } from "../../shared/types.ts";
18
+ import { safeTerminalText } from "../../shared/display-text.ts";
19
+ import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
18
20
  import { isUnexplainedProcessSignal } from "../shared/process-signal.ts";
19
21
  import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
20
22
 
23
+ export interface SubagentNotifyChildOutput {
24
+ workflowKey?: string;
25
+ runId?: string;
26
+ agent?: string;
27
+ status: string;
28
+ savedOutputPath?: string;
29
+ preview: string;
30
+ previewTruncated?: boolean;
31
+ previewUnavailableReason?: string;
32
+ }
33
+
21
34
  export interface SubagentNotifyDetails {
22
35
  agent: string;
23
36
  status: "completed" | "failed" | "paused" | "stopped";
@@ -27,6 +40,7 @@ export interface SubagentNotifyDetails {
27
40
  durationMs?: number;
28
41
  workflowRunId?: string;
29
42
  childRuns?: Array<{ runId: string; workflowKey?: string; agent?: string; status?: string }>;
43
+ childOutputs?: SubagentNotifyChildOutput[];
30
44
  reconciledFromDetachedChild?: string;
31
45
  sessionLabel?: string;
32
46
  sessionValue?: string;
@@ -57,7 +71,14 @@ export interface CompletionNotification {
57
71
  workflowKey?: string;
58
72
  agent?: string;
59
73
  status?: string;
74
+ state?: string;
60
75
  success?: boolean;
76
+ output?: string;
77
+ structuredOutput?: unknown;
78
+ outputState?: "present" | "absent" | "unknown";
79
+ outputReference?: string | { path?: string };
80
+ artifactPaths?: { outputPath?: string };
81
+ detached?: boolean;
61
82
  exitCode?: number | null;
62
83
  processSignal?: string | null;
63
84
  interrupted?: boolean;
@@ -100,6 +121,115 @@ export interface CompletionNotifier {
100
121
  dispose(): void;
101
122
  }
102
123
 
124
+ const CHILD_OUTPUT_PREVIEW_MAX_BYTES = 4 * 1024;
125
+ const CHILD_OUTPUT_PREVIEW_COUNT = 8;
126
+ const PREVIEW_TRUNCATION_MARKER = "...[preview truncated]";
127
+ type CompletionChild = NonNullable<CompletionNotification["results"]>[number];
128
+
129
+ function truncateUtf8Head(value: string, maxBytes: number): { text: string; truncated: boolean } {
130
+ const bytes = Buffer.from(value, "utf8");
131
+ if (bytes.length <= maxBytes) return { text: value, truncated: false };
132
+ const markerBytes = Buffer.byteLength(PREVIEW_TRUNCATION_MARKER, "utf8");
133
+ const contentBytes = Math.max(0, maxBytes - markerBytes);
134
+ let end = contentBytes;
135
+ while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
136
+ return {
137
+ text: `${bytes.subarray(0, end).toString("utf8")}${PREVIEW_TRUNCATION_MARKER}`,
138
+ truncated: true,
139
+ };
140
+ }
141
+
142
+ function boundedSafeText(value: string, maxBytes = 1_024): string {
143
+ return truncateUtf8Head(safeTerminalText(value).replace(/\n/g, "\\n"), maxBytes).text;
144
+ }
145
+
146
+ function outputPathFromReference(value: CompletionChild["outputReference"]): string | undefined {
147
+ if (typeof value === "string" && value.trim()) return value.trim();
148
+ if (value && typeof value === "object" && typeof value.path === "string" && value.path.trim()) return value.path.trim();
149
+ return undefined;
150
+ }
151
+
152
+ function childSavedOutputPath(child: CompletionChild): string | undefined {
153
+ return outputPathFromReference(child.outputReference);
154
+ }
155
+
156
+ function childStatus(child: CompletionChild, workflowState?: string): string {
157
+ const knownStatus = child.status === "complete"
158
+ ? "completed"
159
+ : child.status === "completed" || child.status === "failed" || child.status === "paused" || child.status === "stopped" || child.status === "detached"
160
+ ? child.status
161
+ : undefined;
162
+ if (knownStatus) return knownStatus;
163
+ return resolveSubagentResultStatus({
164
+ success: child.success,
165
+ state: child.state ?? (child.success === undefined ? workflowState : undefined),
166
+ interrupted: child.interrupted,
167
+ detached: child.detached,
168
+ processSignal: child.processSignal,
169
+ timedOut: child.timedOut,
170
+ stopped: child.stopped,
171
+ turnBudgetExceeded: child.turnBudgetExceeded,
172
+ exitCode: typeof child.exitCode === "number" ? child.exitCode : undefined,
173
+ });
174
+ }
175
+
176
+ function structuredOutputText(value: unknown): string | undefined {
177
+ if (value === undefined) return undefined;
178
+ try {
179
+ const serialized = JSON.stringify(value, null, 2);
180
+ return typeof serialized === "string" ? serialized : undefined;
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ }
185
+
186
+ function childInlinePreview(child: CompletionChild): { preview?: string; truncated?: boolean; unavailableReason?: string } {
187
+ const output = typeof child.output === "string" ? child.output : "";
188
+ const outputReference = childSavedOutputPath(child);
189
+ const referenceOnly = Boolean(outputReference && output.trim().startsWith("Output saved to:"));
190
+ const raw = child.outputState === "absent" || referenceOnly ? structuredOutputText(child.structuredOutput) : output || structuredOutputText(child.structuredOutput);
191
+ if (!raw?.trim()) {
192
+ return { unavailableReason: referenceOnly ? "saved output is file-only" : "no safe inline output" };
193
+ }
194
+ const safe = safeTerminalText(raw);
195
+ if (!safe.trim()) return { unavailableReason: "no safe inline output" };
196
+ const bounded = truncateUtf8Head(safe, CHILD_OUTPUT_PREVIEW_MAX_BYTES);
197
+ return { preview: bounded.text, ...(bounded.truncated ? { truncated: true } : {}) };
198
+ }
199
+
200
+ function formatChildOutputBlock(children: SubagentNotifyChildOutput[] | undefined): string | undefined {
201
+ if (!children?.length) return undefined;
202
+ const lines = ["Child outputs:"];
203
+ for (let index = 0; index < children.length; index++) {
204
+ const child = children[index]!;
205
+ const key = child.workflowKey ? boundedSafeText(child.workflowKey) : "unavailable";
206
+ const runId = child.runId ? boundedSafeText(child.runId) : "unavailable";
207
+ const status = boundedSafeText(child.status) || "unavailable";
208
+ lines.push(`- key=${key} run=${runId} status=${status}`);
209
+ lines.push(` Saved output: ${child.savedOutputPath ? boundedSafeText(child.savedOutputPath) : "unavailable"}`);
210
+ if (index >= CHILD_OUTPUT_PREVIEW_COUNT) {
211
+ lines.push(" Preview: unavailable (notice preview budget exceeded)");
212
+ continue;
213
+ }
214
+ if (!child.preview) {
215
+ lines.push(` Preview: unavailable (${child.previewUnavailableReason ?? "no safe inline output"})`);
216
+ continue;
217
+ }
218
+ lines.push(" Preview:");
219
+ for (const line of child.preview.split("\n")) lines.push(` | ${line}`);
220
+ }
221
+ if (children.length > CHILD_OUTPUT_PREVIEW_COUNT) {
222
+ lines.push(`- ${children.length - CHILD_OUTPUT_PREVIEW_COUNT} additional child preview(s) omitted by notice budget; child run metadata is retained below.`);
223
+ }
224
+ return lines.join("\n");
225
+ }
226
+
227
+ function formatResultPreview(details: SubagentNotifyDetails): string {
228
+ const summary = details.resultPreview.trim() ? details.resultPreview : "(no output)";
229
+ const childOutputs = formatChildOutputBlock(details.childOutputs);
230
+ return childOutputs ? `${summary}\n\n${childOutputs}` : summary;
231
+ }
232
+
103
233
  function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
104
234
  if (!details.sessionValue) return undefined;
105
235
  return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;
@@ -131,7 +261,7 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
131
261
  "",
132
262
  scheduleLine,
133
263
  scheduleLine ? "" : undefined,
134
- details.resultPreview.trim() ? details.resultPreview : "(no output)",
264
+ formatResultPreview(details),
135
265
  details.handoffPath ? "" : undefined,
136
266
  details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,
137
267
  correlationLines.length && !details.handoffPath ? "" : undefined,
@@ -218,7 +348,7 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
218
348
  if (!detail) continue;
219
349
  const sessionLine = formatSessionLine(detail);
220
350
  blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}${detail.scheduleOrigin ? ` — scheduled run from ${detail.scheduleOrigin.name ?? detail.scheduleOrigin.id} (schedule ${detail.scheduleOrigin.id})` : ""}`);
221
- blocks.push(detail.resultPreview.trim() ? detail.resultPreview : "(no output)");
351
+ blocks.push(formatResultPreview(detail));
222
352
  if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);
223
353
  blocks.push(...formatCorrelationLines(detail));
224
354
  if (sessionLine) blocks.push(sessionLine);
@@ -275,6 +405,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
275
405
  const paused = !stopped && !result.success && (
276
406
  result.exitCode === 0
277
407
  || result.state === "paused"
408
+ || result.interrupted === true
278
409
  || summary.startsWith("Paused after interrupt.")
279
410
  );
280
411
  const status = stopped ? "stopped" : paused ? "paused" : result.success ? "completed" : "failed";
@@ -290,14 +421,34 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
290
421
  const rawRunId = typeof result.runId === "string" ? result.runId : typeof result.id === "string" ? result.id : undefined;
291
422
  const workflowRunId = (result.mode === "workflow" || agent === "workflow") && rawRunId ? rawRunId : undefined;
292
423
  const childRuns = result.results?.flatMap((child) => {
293
- if (typeof child.runId !== "string" || !child.runId.trim()) return [];
424
+ const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
425
+ const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
426
+ if (!runId && (!workflowRunId || !workflowKey)) return [];
294
427
  return [{
295
- runId: child.runId,
296
- ...(typeof child.workflowKey === "string" ? { workflowKey: child.workflowKey } : {}),
428
+ runId: runId ?? "unavailable",
429
+ ...(workflowKey ? { workflowKey } : {}),
297
430
  ...(typeof child.agent === "string" ? { agent: child.agent } : {}),
298
- ...(typeof child.status === "string" ? { status: child.status } : {}),
431
+ status: childStatus(child, workflowRunId ? result.state : undefined),
299
432
  }];
300
433
  }) ?? [];
434
+ const childOutputs = workflowRunId && result.results?.length
435
+ ? result.results.map((child) => {
436
+ const inline = childInlinePreview(child);
437
+ const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
438
+ const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
439
+ const savedOutputPath = childSavedOutputPath(child);
440
+ return {
441
+ ...(workflowKey ? { workflowKey } : {}),
442
+ ...(runId ? { runId } : {}),
443
+ ...(typeof child.agent === "string" ? { agent: child.agent } : {}),
444
+ status: childStatus(child, result.state),
445
+ ...(savedOutputPath ? { savedOutputPath } : {}),
446
+ ...(inline.preview ? { preview: inline.preview } : { preview: "" }),
447
+ ...(inline.truncated ? { previewTruncated: true } : {}),
448
+ ...(inline.unavailableReason ? { previewUnavailableReason: inline.unavailableReason } : {}),
449
+ };
450
+ })
451
+ : undefined;
301
452
  const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
302
453
  const session =
303
454
  result.shareUrl
@@ -322,6 +473,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
322
473
  ...(handoffPath ? { handoffPath } : {}),
323
474
  ...(workflowRunId ? { workflowRunId } : {}),
324
475
  ...(childRuns.length ? { childRuns } : {}),
476
+ ...(childOutputs?.length ? { childOutputs } : {}),
325
477
  ...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
326
478
  ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
327
479
  };
@@ -343,7 +343,8 @@ export function resultPayloadPathForMissionObserverRun(resultsDir: string, runId
343
343
  if (!entry || entry.runId !== runId) return undefined;
344
344
  return resultPayloadLocationFromIndex(resultsDir, entry)?.path;
345
345
  } catch (error) {
346
- if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
346
+ const code = (error as NodeJS.ErrnoException).code;
347
+ if (code !== "ENOENT" && code !== "ENOTDIR" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
347
348
  return undefined;
348
349
  }
349
350
  }
@@ -67,6 +67,7 @@ type ResultWatcherDeps = {
67
67
 
68
68
  type ResultFileChild = {
69
69
  agent?: string;
70
+ sessionName?: string;
70
71
  output?: string;
71
72
  structuredOutput?: unknown;
72
73
  outputState?: SubagentOutputState;
@@ -482,6 +483,7 @@ export function createResultWatcher(
482
483
  : undefined;
483
484
  return {
484
485
  agent: result.agent ?? data.agent ?? `step-${index + 1}`,
486
+ ...(result.sessionName ? { sessionName: result.sessionName } : {}),
485
487
  status: resolveSubagentResultStatus({
486
488
  success: result.success,
487
489
  state: childState,
@@ -39,7 +39,7 @@ export function formatResumeFirstFailedRunDetail(run: AsyncRunSummary): string |
39
39
  }
40
40
 
41
41
  export function formatResumeFirstFailedRunsNote(runs: AsyncRunSummary[]): string {
42
- const failedRuns = runs.filter((run) => run.state === "failed");
42
+ const failedRuns = runs.filter((run) => run.state === "failed" || run.state === "partial");
43
43
  const detachGuidance = failedRuns
44
44
  .map(formatIntercomDetachGuidance)
45
45
  .filter((guidance): guidance is string => Boolean(guidance));
@@ -27,7 +27,7 @@ export interface RetainedChild {
27
27
  }
28
28
 
29
29
  function isRetainedChildState(state: AsyncRunSummary["state"]): state is RetainedChildState {
30
- return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
30
+ return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped";
31
31
  }
32
32
 
33
33
  function isTerminalStepStatus(status: AsyncRunSummary["steps"][number]["status"]): boolean {
@@ -23,6 +23,7 @@ import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegis
23
23
  import { readMissionBinding } from "../../missions/lifecycle.ts";
24
24
  import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
25
25
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
26
+ import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
26
27
  import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
27
28
  import { getExternalJobProvider } from "../../api/external-job-provider.ts";
28
29
 
@@ -64,13 +65,14 @@ function formatCapacityOwner(inspect: ActiveAsyncCapacityInspection): string[] {
64
65
  }
65
66
 
66
67
  function formatWorkflowDebug(status: AsyncStatus): string[] {
67
- if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey) return [];
68
+ if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey && !status.lane) return [];
68
69
  const lines = [
69
70
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
70
71
  status.mode === "workflow" ? `Workflow children: ${(status.steps ?? []).length}` : undefined,
72
+ status.lane ? `Lane: ${status.lane.key}${status.lane.mode ? ` (${status.lane.mode})` : ""}` : undefined,
71
73
  ].filter((line): line is string => line !== undefined);
72
74
  for (const [index, step] of (status.steps ?? []).entries()) {
73
- lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${step.agent} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}`);
75
+ lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${runStatusStepDisplayName(step)} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}${step.lane ? ` · lane ${step.lane.key}` : ""}${step.worktreePath ? ` · worktree ${step.worktreePath} · branch ${step.branch ?? "unknown"}` : ""}`);
74
76
  }
75
77
  return lines;
76
78
  }
@@ -88,6 +90,7 @@ function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string;
88
90
  `Mode: ${status.mode}`,
89
91
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}` : undefined,
90
92
  status.workflowKey ? `Workflow key: ${status.workflowKey}` : undefined,
93
+ status.lane ? `Lane: ${status.lane.key}${status.lane.mode ? ` (${status.lane.mode})` : ""}` : undefined,
91
94
  `Status process terminal: ${formatProcessTerminal(overlayProcessTerminal)}`,
92
95
  `Sidecar process terminal: ${formatProcessTerminal(sidecarProcessTerminal)}`,
93
96
  ...formatCapacityOwner(capacity),
@@ -152,7 +155,12 @@ function stepLineLabel(status: AsyncStatus, index: number): string {
152
155
  return `Step ${index + 1}`;
153
156
  }
154
157
 
158
+ function runStatusStepDisplayName(step: { agent: string; sessionName?: string; label?: string }): string {
159
+ return step.sessionName?.trim() || (step.label ? `${step.label} (${step.agent})` : step.agent);
160
+ }
161
+
155
162
  function nestedRunDisplayName(run: NestedRunSummary): string {
163
+ if (run.sessionName?.trim()) return run.sessionName.trim();
156
164
  if (run.agent) return run.agent;
157
165
  if (run.agents?.length) return run.agents.join(", ");
158
166
  return run.id;
@@ -197,7 +205,7 @@ function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
197
205
  for (const child of run.children) {
198
206
  const output = rememberedForegroundChildOutput(child).trim().split(/\r?\n/).find((line) => line.trim());
199
207
  const parts = [
200
- `${child.index + 1}. ${child.agent} ${child.status}`,
208
+ `${child.index + 1}. ${child.sessionName?.trim() || child.agent} ${child.status}`,
201
209
  child.exitCode !== undefined ? `exit ${child.exitCode}` : undefined,
202
210
  child.detachedReason ? `detached: ${child.detachedReason}` : undefined,
203
211
  child.acceptance ? `acceptance: ${child.acceptance.status}` : undefined,
@@ -241,7 +249,7 @@ function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options:
241
249
  const lines = [
242
250
  `Run: ${run.runId}`,
243
251
  `State: ${child.status}`,
244
- `Child: ${index} (${child.agent})`,
252
+ `Child: ${index} (${child.sessionName?.trim() || child.agent})`,
245
253
  child.sessionFile ? `Session: ${child.sessionFile}` : undefined,
246
254
  child.transcriptPath ? `Transcript: ${child.transcriptPath}` : undefined,
247
255
  child.artifactPaths?.outputPath ? `Output: ${child.artifactPaths.outputPath}` : undefined,
@@ -277,7 +285,7 @@ function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): stri
277
285
  for (const [index, step] of run.steps.entries()) {
278
286
  const activity = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
279
287
  const budget = step.turnBudget ? `, turn budget: ${step.turnBudget.turnCount}/${step.turnBudget.maxTurns}+${step.turnBudget.graceTurns} (${step.turnBudget.outcome})` : "";
280
- lines.push(` ${index + 1}. ${step.agent} ${step.status}${activity ? `, ${activity}` : ""}${budget}${step.error ? `, error: ${step.error}` : ""}`);
288
+ lines.push(` ${index + 1}. ${step.sessionName?.trim() || step.agent} ${step.status}${activity ? `, ${activity}` : ""}${budget}${step.error ? `, error: ${step.error}` : ""}`);
281
289
  lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true }));
282
290
  }
283
291
  }
@@ -504,6 +512,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
504
512
  statusActivityText ? `Activity: ${statusActivityText}` : undefined,
505
513
  steeringText ? `Steering: ${steeringText}` : undefined,
506
514
  `Mode: ${status.mode}`,
515
+ ...(status.preflight ? [formatWorkflowPreflightPlanSummary(status.preflight)] : []),
516
+ ...(status.workflow?.preflightWarnings?.length ? [formatWorkflowPreflightWarningSummary(status.workflow.preflightWarnings)] : []),
507
517
  runFanoutBudget ? formatRunFanoutBudget(runFanoutBudget) : undefined,
508
518
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
509
519
  status.mode === "workflow" && workflowReturnPreview !== undefined ? `Return: ${workflowReturnPreview}` : undefined,
@@ -535,7 +545,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
535
545
  const errorText = step.error ? `, error: ${step.error}` : "";
536
546
  const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}` : "";
537
547
  const budgetText = step.turnBudget ? `, turn budget: ${step.turnBudget.turnCount}/${step.turnBudget.maxTurns}+${step.turnBudget.graceTurns} (${step.turnBudget.outcome})` : "";
538
- const display = step.label ? `${step.label} (${step.agent})` : step.agent;
548
+ const display = runStatusStepDisplayName(step);
539
549
  const phase = step.phase ? `[${step.phase}] ` : "";
540
550
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
541
551
  if (step.runner?.type === "external-cli") {
@@ -608,7 +618,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
608
618
 
609
619
  const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
610
620
  if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
611
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
621
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(status.preflight ? { preflight: status.preflight } : {}), ...(status.workflow?.preflightWarnings?.length ? { preflightWarnings: status.workflow.preflightWarnings } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
612
622
  }
613
623
  }
614
624
 
@@ -622,7 +632,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
622
632
  }
623
633
  try {
624
634
  const raw = fs.readFileSync(resultPath, "utf-8");
625
- const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
635
+ const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
626
636
  if (params.view === "transcript") {
627
637
  try {
628
638
  return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import * as fs from "node:fs";
3
4
  import * as path from "node:path";
@@ -11,6 +12,8 @@ import type { SubagentParamsLike } from "../foreground/subagent-executor.ts";
11
12
  import { validateExecutionAcceptance } from "../shared/acceptance.ts";
12
13
  import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
13
14
  import { previewSimpleWorkflowRun } from "../../workflows/scripted-workflow.ts";
15
+ import { resolveGitRepositoryIdentity } from "../../workflows/chat-progress.ts";
16
+ import { getConfigDirName } from "../../shared/utils.ts";
14
17
 
15
18
  export const SCHEDULED_RUN_ACTIONS = [
16
19
  "schedule.create",
@@ -142,11 +145,83 @@ function validateScheduleId(id: string): string {
142
145
  return id;
143
146
  }
144
147
 
148
+ function normalizedComparisonPath(value: string): string {
149
+ const absolute = path.resolve(value);
150
+ if (process.platform !== "win32") return absolute;
151
+ let normalized = absolute;
152
+ try { normalized = fs.realpathSync.native(absolute); } catch {}
153
+ normalized = normalized.replaceAll("/", "\\");
154
+ if (normalized.startsWith("\\\\?\\UNC\\")) normalized = `\\\\${normalized.slice(8)}`;
155
+ else if (normalized.startsWith("\\\\?\\")) normalized = normalized.slice(4);
156
+ normalized = path.win32.normalize(normalized).toLowerCase();
157
+ if (normalized.length > 3) normalized = normalized.replace(/[\\]+$/, "");
158
+ return normalized;
159
+ }
160
+
145
161
  function pathWithin(root: string, candidate: string): boolean {
146
- const relative = path.relative(root, candidate);
162
+ const relative = path.relative(normalizedComparisonPath(root), normalizedComparisonPath(candidate));
147
163
  return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
148
164
  }
149
165
 
166
+ function resolveGitCommonDirForCheckout(checkoutRoot: string): string | undefined {
167
+ const gitPath = path.join(checkoutRoot, ".git");
168
+ try {
169
+ const stat = fs.statSync(gitPath);
170
+ if (stat.isDirectory()) return fs.realpathSync.native(gitPath);
171
+ if (!stat.isFile()) return undefined;
172
+ const match = /^gitdir:[ \t]*([^\r\n]+)$/i.exec(fs.readFileSync(gitPath, "utf-8").trim());
173
+ return match?.[1] ? fs.realpathSync.native(path.resolve(checkoutRoot, match[1])) : undefined;
174
+ } catch {
175
+ return undefined;
176
+ }
177
+ }
178
+
179
+ function samePath(left: string, right: string): boolean {
180
+ return normalizedComparisonPath(left) === normalizedComparisonPath(right);
181
+ }
182
+
183
+ function resolveTrustedGitConfigRoot(checkoutRoot: string, commonDir: string): string | undefined {
184
+ const resolvedCommonDir = resolveGitCommonDirForCheckout(checkoutRoot);
185
+ if (!resolvedCommonDir || !samePath(resolvedCommonDir, commonDir)) return undefined;
186
+ const configRoot = path.join(checkoutRoot, getConfigDirName());
187
+ try {
188
+ const resolved = fs.realpathSync.native(configRoot);
189
+ return fs.statSync(resolved).isDirectory() && pathWithin(checkoutRoot, resolved) ? resolved : undefined;
190
+ } catch {
191
+ return undefined;
192
+ }
193
+ }
194
+
195
+ function registeredGitWorktreeRoots(projectCwd: string): string[] {
196
+ const result = spawnSync("git", ["-C", projectCwd, "worktree", "list", "--porcelain"], { encoding: "utf-8", windowsHide: true });
197
+ if (result.status !== 0 || typeof result.stdout !== "string") return [];
198
+ const roots: string[] = [];
199
+ for (const line of result.stdout.split(/\r?\n/)) {
200
+ if (!line.startsWith("worktree ")) continue;
201
+ try { roots.push(fs.realpathSync.native(line.slice("worktree ".length).trim())); } catch {}
202
+ }
203
+ return roots;
204
+ }
205
+
206
+ function resolveSharedGitConfigRoot(projectCwd: string): string | undefined {
207
+ const repository = resolveGitRepositoryIdentity(projectCwd);
208
+ if (!repository) return undefined;
209
+ let projectConfigRoot: string;
210
+ try {
211
+ projectConfigRoot = fs.realpathSync.native(path.join(projectCwd, getConfigDirName()));
212
+ } catch {
213
+ return undefined;
214
+ }
215
+ // Git may report a separate-git-dir primary checkout as its common git
216
+ // directory, not as the checkout root. Without a registered checkout root,
217
+ // do not infer a shared config path from that unprovable layout.
218
+ for (const checkoutRoot of new Set(registeredGitWorktreeRoots(projectCwd))) {
219
+ const resolved = resolveTrustedGitConfigRoot(checkoutRoot, repository.commonDir);
220
+ if (resolved && samePath(resolved, projectConfigRoot)) return resolved;
221
+ }
222
+ return undefined;
223
+ }
224
+
150
225
  function assertScheduleRoot(root: string, projectCwd: string | undefined, create: boolean): void {
151
226
  if (!projectCwd) {
152
227
  if (create) fs.mkdirSync(root, { recursive: true, mode: 0o700 });
@@ -154,7 +229,7 @@ function assertScheduleRoot(root: string, projectCwd: string | undefined, create
154
229
  }
155
230
  let projectPath: string;
156
231
  try {
157
- projectPath = fs.realpathSync(projectCwd);
232
+ projectPath = fs.realpathSync.native(projectCwd);
158
233
  } catch (error) {
159
234
  if (!create && (error as NodeJS.ErrnoException).code === "ENOENT") return;
160
235
  throw error;
@@ -165,10 +240,14 @@ function assertScheduleRoot(root: string, projectCwd: string | undefined, create
165
240
  if (parent === existing) break;
166
241
  existing = parent;
167
242
  }
168
- if (!pathWithin(projectPath, fs.realpathSync(existing))) throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
243
+ const existingPath = fs.realpathSync.native(existing);
244
+ const sharedGitConfigRoot = pathWithin(projectPath, existingPath) ? undefined : resolveSharedGitConfigRoot(projectCwd);
245
+ const isTrustedPath = (candidate: string): boolean => pathWithin(projectPath, candidate)
246
+ || (sharedGitConfigRoot !== undefined && pathWithin(sharedGitConfigRoot, candidate));
247
+ if (!isTrustedPath(existingPath)) throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
169
248
  if (!create) return;
170
249
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
171
- if (!pathWithin(projectPath, fs.realpathSync(root))) throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
250
+ if (!isTrustedPath(fs.realpathSync.native(root))) throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
172
251
  }
173
252
 
174
253
  function scheduleDir(root: string, id: string, create = false, projectCwd?: string): string {
@@ -182,9 +261,9 @@ function scheduleDir(root: string, id: string, create = false, projectCwd?: stri
182
261
  if (!create) return dir;
183
262
  fs.mkdirSync(dir, { mode: 0o700 });
184
263
  }
185
- const rootPath = fs.realpathSync(root);
186
- const dirPath = fs.realpathSync(dir);
187
- if (dirPath !== path.join(rootPath, id)) throw new Error(`Schedule path '${dir}' escapes the project schedule root.`);
264
+ const rootPath = fs.realpathSync.native(root);
265
+ const dirPath = fs.realpathSync.native(dir);
266
+ if (!samePath(dirPath, path.join(rootPath, id))) throw new Error(`Schedule path '${dir}' escapes the project schedule root.`);
188
267
  return dir;
189
268
  }
190
269
 
@@ -8,6 +8,7 @@ import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSu
8
8
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
9
9
  import { normalizeParallelGroups } from "./parallel-groups.ts";
10
10
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent, type NestedRoute } from "../shared/nested-events.ts";
11
+ import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
11
12
 
12
13
  export type PidLiveness = "alive" | "dead" | "unknown";
13
14
 
@@ -24,6 +25,7 @@ interface StartedRunMetadata {
24
25
  parallelGroups?: AsyncParallelGroupStatus[];
25
26
  startedAt?: number;
26
27
  sessionFile?: string;
28
+ sessionName?: string;
27
29
  }
28
30
 
29
31
  interface ReconcileAsyncRunOptions {
@@ -90,6 +92,7 @@ function appendJsonlBestEffort(filePath: string, payload: object): void {
90
92
 
91
93
  interface ResultChildOutcome {
92
94
  agent?: string;
95
+ sessionName?: string;
93
96
  success?: boolean;
94
97
  error?: string;
95
98
  sessionFile?: string;
@@ -101,7 +104,7 @@ interface ResultChildOutcome {
101
104
  }
102
105
 
103
106
  interface ResultRepairData {
104
- state: "complete" | "failed" | "paused" | "stopped" | "rejected";
107
+ state: "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
105
108
  results?: ResultChildOutcome[];
106
109
  }
107
110
 
@@ -117,7 +120,7 @@ function regularFileExists(filePath: string): boolean {
117
120
  function readResultRepairData(resultPath: string): ResultRepairData | undefined {
118
121
  try {
119
122
  const data = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { success?: boolean; state?: string; exitCode?: number; results?: unknown };
120
- const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "rejected" ? "rejected" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
123
+ const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "rejected" ? "rejected" : data.state === "partial" ? "partial" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
121
124
  const results = Array.isArray(data.results)
122
125
  ? data.results.map((entry, index) => {
123
126
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return {};
@@ -157,8 +160,9 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
157
160
  endedAt: step.endedAt ?? now,
158
161
  durationMs: step.startedAt !== undefined && step.durationMs === undefined ? Math.max(0, now - step.startedAt) : step.durationMs,
159
162
  exitCode: step.exitCode ?? (state === "complete" || state === "paused" ? 0 : 1),
160
- error: state === "failed" || state === "stopped" ? step.error ?? child?.error : step.error,
163
+ error: state === "failed" || state === "partial" || state === "stopped" ? step.error ?? child?.error : step.error,
161
164
  stopped: state === "stopped" ? true : step.stopped,
165
+ sessionName: step.sessionName ?? child?.sessionName,
162
166
  sessionFile: step.sessionFile ?? child?.sessionFile,
163
167
  model,
164
168
  thinking,
@@ -254,6 +258,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
254
258
  summary: message,
255
259
  results: repairedSteps.map((step) => ({
256
260
  agent: step.agent,
261
+ ...(step.sessionName ? { sessionName: step.sessionName } : {}),
257
262
  output: step.status === "complete" || step.status === "completed" ? "" : message,
258
263
  error: step.status === "complete" || step.status === "completed" ? undefined : step.error ?? message,
259
264
  success: step.status === "complete" || step.status === "completed",
@@ -290,7 +295,7 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
290
295
  }
291
296
 
292
297
  function terminal(state: AsyncStatus["state"]): boolean {
293
- return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
298
+ return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped" || state === "rejected";
294
299
  }
295
300
 
296
301
  function* nestedRuns(children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
@@ -353,6 +358,7 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
353
358
  const startedStatus = !status && options.startedRun ? buildStartedStatus(asyncDir, options.startedRun, now) : undefined;
354
359
  const effectiveStatus = status ?? startedStatus;
355
360
  if (!effectiveStatus) return { status: null, repaired: false };
361
+ assertWorkflowGraphHostSteps(effectiveStatus.workflowGraph, path.join(asyncDir, "status.json"), effectiveStatus.runId);
356
362
  const statusPath = path.join(asyncDir, "status.json");
357
363
  for (const [index, step] of (effectiveStatus.steps ?? []).entries()) {
358
364
  const stepRecord = step as Record<string, unknown>;
@@ -4,7 +4,6 @@ import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
4
4
  import type {
5
5
  AsyncStatus,
6
6
  ResolvedToolBudget,
7
- ResolvedTurnBudget,
8
7
  SteerActionResult,
9
8
  SteeringRecoveryDescriptor,
10
9
  SteeringRequestStatus,
@@ -195,26 +194,17 @@ export function readSteeringStatus(asyncDir: string): SteeringStatus | undefined
195
194
  }
196
195
 
197
196
  export function remainingSteeringRecoveryLimits(
198
- descriptor: Pick<SteeringRecoveryDescriptor, "absoluteDeadlineAt" | "initialTurnBudget" | "initialToolBudget">,
199
- status: Pick<AsyncStatus, "turnBudget" | "turnCount" | "toolBudget" | "toolCount">,
197
+ descriptor: Pick<SteeringRecoveryDescriptor, "absoluteDeadlineAt" | "initialToolBudget">,
198
+ status: Pick<AsyncStatus, "toolBudget" | "toolCount">,
200
199
  now = Date.now(),
201
- ): { timeoutMs?: number; absoluteDeadlineAt?: number; turnBudget?: ResolvedTurnBudget; toolBudget?: ResolvedToolBudget } {
202
- const limits: { timeoutMs?: number; absoluteDeadlineAt?: number; turnBudget?: ResolvedTurnBudget; toolBudget?: ResolvedToolBudget } = {};
200
+ ): { timeoutMs?: number; absoluteDeadlineAt?: number; toolBudget?: ResolvedToolBudget } {
201
+ const limits: { timeoutMs?: number; absoluteDeadlineAt?: number; toolBudget?: ResolvedToolBudget } = {};
203
202
  if (descriptor.absoluteDeadlineAt !== undefined) {
204
203
  const timeoutMs = descriptor.absoluteDeadlineAt - now;
205
204
  if (timeoutMs <= 0) throw new Error("Source run has no remaining deadline budget; it remains paused.");
206
205
  limits.timeoutMs = timeoutMs;
207
206
  limits.absoluteDeadlineAt = descriptor.absoluteDeadlineAt;
208
207
  }
209
- if (descriptor.initialTurnBudget) {
210
- const consumed = status.turnBudget?.turnCount ?? status.turnCount ?? 0;
211
- const totalRemaining = descriptor.initialTurnBudget.maxTurns + descriptor.initialTurnBudget.graceTurns - consumed;
212
- if (totalRemaining <= 0) throw new Error("Source run has no remaining turn budget; it remains paused.");
213
- const softRemaining = Math.max(0, descriptor.initialTurnBudget.maxTurns - consumed);
214
- limits.turnBudget = softRemaining > 0
215
- ? { maxTurns: softRemaining, graceTurns: totalRemaining - softRemaining }
216
- : { maxTurns: 1, graceTurns: totalRemaining - 1 };
217
- }
218
208
  if (descriptor.initialToolBudget) {
219
209
  const consumed = status.toolBudget?.toolCount ?? status.toolCount ?? 0;
220
210
  const hard = descriptor.initialToolBudget.hard - consumed;