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.
- package/CHANGELOG.md +65 -0
- package/docs/agents.md +5 -3
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +1 -1
- package/docs/models.md +24 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +89 -4
- package/docs/workflows.md +93 -2
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +1 -1
- package/skills/pi-subagents/SKILL.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +5 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
- package/src/agents/agent-management.ts +5 -17
- package/src/agents/agent-serializer.ts +4 -2
- package/src/agents/agents.ts +67 -26
- package/src/agents/runtime-agent-registry.ts +9 -16
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +6 -9
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +51 -23
- package/src/extension/public-execution.ts +15 -2
- package/src/extension/schemas.ts +35 -10
- package/src/extension/tool-description.ts +18 -3
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +50 -35
- package/src/runs/background/async-job-tracker.ts +14 -12
- package/src/runs/background/async-resume.ts +23 -25
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +66 -7
- package/src/runs/background/chain-append.ts +6 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +158 -6
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +2 -0
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +18 -8
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +346 -357
- package/src/runs/background/subagent-wait.ts +58 -10
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +22 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +114 -120
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +478 -239
- package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +463 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/mcp-config-sources.ts +42 -6
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
- package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
- package/src/runs/shared/model-fallback.ts +9 -2
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +20 -2
- package/src/runs/shared/single-output.ts +27 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +5 -2
- package/src/shared/launch-contract.ts +1 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +198 -29
- package/src/shared/utils.ts +35 -55
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/tui/fleet-status.ts +125 -74
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +316 -27
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +230 -0
- package/src/workflows/scripted-workflow.ts +452 -38
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +43 -4
- package/src/workflows/workflow-settlement.ts +246 -0
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
296
|
-
...(
|
|
428
|
+
runId: runId ?? "unavailable",
|
|
429
|
+
...(workflowKey ? { workflowKey } : {}),
|
|
297
430
|
...(typeof child.agent === "string" ? { agent: child.agent } : {}),
|
|
298
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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" | "
|
|
199
|
-
status: Pick<AsyncStatus, "
|
|
197
|
+
descriptor: Pick<SteeringRecoveryDescriptor, "absoluteDeadlineAt" | "initialToolBudget">,
|
|
198
|
+
status: Pick<AsyncStatus, "toolBudget" | "toolCount">,
|
|
200
199
|
now = Date.now(),
|
|
201
|
-
): { timeoutMs?: number; absoluteDeadlineAt?: number;
|
|
202
|
-
const limits: { timeoutMs?: number; absoluteDeadlineAt?: number;
|
|
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;
|