pi-subagents 0.47.1 → 0.49.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 +51 -0
- package/README.md +2 -0
- package/docs/agents.md +1 -0
- package/docs/configuration.md +74 -7
- package/docs/missions.md +4 -2
- package/docs/observability.md +27 -3
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agents.ts +36 -15
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +26 -0
- package/src/extension/doctor.ts +40 -0
- package/src/extension/index.ts +39 -8
- package/src/extension/public-execution.ts +35 -4
- package/src/extension/rpc.ts +3 -9
- package/src/extension/schemas.ts +10 -9
- package/src/extension/tool-description.ts +10 -8
- package/src/inspectors/herdr/actions.ts +11 -2
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/intercom/intercom-bridge.ts +7 -2
- package/src/missions/lifecycle.ts +4 -7
- package/src/missions/store.ts +12 -7
- package/src/missions/workflow-state.ts +6 -2
- package/src/runs/background/active-async-capacity.ts +431 -0
- package/src/runs/background/active-run-index.ts +9 -5
- package/src/runs/background/async-execution.ts +141 -65
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +18 -2
- package/src/runs/background/async-status.ts +11 -5
- package/src/runs/background/chain-append.ts +33 -15
- package/src/runs/background/fleet-view.ts +18 -5
- package/src/runs/background/owned-process-tree.ts +104 -0
- package/src/runs/background/process-terminal.ts +17 -3
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +14 -6
- package/src/runs/background/run-status.ts +101 -5
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +60 -32
- package/src/runs/foreground/chain-execution.ts +37 -2
- package/src/runs/foreground/execution.ts +90 -19
- package/src/runs/foreground/foreground-control.ts +12 -0
- package/src/runs/foreground/prompt-audit.ts +172 -0
- package/src/runs/foreground/subagent-executor.ts +829 -203
- package/src/runs/shared/acceptance.ts +13 -4
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +302 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/pi-args.ts +44 -1
- package/src/runs/shared/run-fanout-budget.ts +280 -0
- package/src/runs/shared/single-output.ts +4 -2
- package/src/runs/shared/task-intent.ts +19 -3
- package/src/runs/shared/worktree.ts +17 -5
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +115 -4
- package/src/shared/utils.ts +3 -1
- package/src/tui/fleet-status.ts +7 -5
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +228 -13
- package/src/tui/render.ts +86 -39
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +8 -2
- package/src/workflows/scripted-workflow.ts +66 -10
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
5
5
|
import { appendJsonl } from "../../shared/artifacts.ts";
|
|
6
6
|
import type { AsyncParallelGroupStatus, AsyncStatus, WorkflowGraphNode, WorkflowGraphSnapshot } from "../../shared/types.ts";
|
|
7
|
-
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import { PROMPT_REDACTED, readStatus } from "../../shared/utils.ts";
|
|
8
8
|
import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep, RunnerCheckpointStep } from "../shared/parallel-utils.ts";
|
|
9
9
|
import { isCheckpointRunnerStep, isDynamicRunnerGroup, isParallelGroup } from "../shared/parallel-utils.ts";
|
|
10
10
|
|
|
@@ -19,6 +19,7 @@ export interface ChainAppendRequest {
|
|
|
19
19
|
export interface ChainAppendResult {
|
|
20
20
|
request: ChainAppendRequest;
|
|
21
21
|
pendingCount: number;
|
|
22
|
+
bookkeepingError?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
type StatusStep = NonNullable<AsyncStatus["steps"]>[number];
|
|
@@ -69,6 +70,7 @@ export function enqueueChainAppendRequest(input: {
|
|
|
69
70
|
runId: string;
|
|
70
71
|
steps: RunnerStep[];
|
|
71
72
|
now?: number;
|
|
73
|
+
admit?: (persist: () => void) => void;
|
|
72
74
|
}): ChainAppendResult {
|
|
73
75
|
const status = readStatus(input.asyncDir);
|
|
74
76
|
if (!status) throw new Error(`No async run status found for '${input.runId}'.`);
|
|
@@ -85,20 +87,36 @@ export function enqueueChainAppendRequest(input: {
|
|
|
85
87
|
steps: input.steps,
|
|
86
88
|
};
|
|
87
89
|
fs.mkdirSync(appendDir(input.asyncDir), { recursive: true });
|
|
88
|
-
writeAtomicJson(appendRequestPath(input.asyncDir, request), request);
|
|
89
|
-
|
|
90
|
+
const persist = () => writeAtomicJson(appendRequestPath(input.asyncDir, request), request);
|
|
91
|
+
if (input.admit) input.admit(persist);
|
|
92
|
+
else persist();
|
|
93
|
+
let pendingCount = 1;
|
|
94
|
+
const bookkeepingErrors: string[] = [];
|
|
95
|
+
try {
|
|
96
|
+
pendingCount = countPendingChainAppendRequests(input.asyncDir);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
bookkeepingErrors.push(`pending append count failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
99
|
+
}
|
|
90
100
|
const statusPath = path.join(input.asyncDir, "status.json");
|
|
91
101
|
const updatedStatus = { ...status, pendingAppends: pendingCount, lastUpdate: request.createdAt };
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
try {
|
|
103
|
+
writeAtomicJson(statusPath, updatedStatus);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
bookkeepingErrors.push(`status update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
appendJsonl(path.join(input.asyncDir, "events.jsonl"), JSON.stringify({
|
|
109
|
+
type: "subagent.chain.append.requested",
|
|
110
|
+
ts: request.createdAt,
|
|
111
|
+
runId: input.runId,
|
|
112
|
+
requestId: request.id,
|
|
113
|
+
stepCount: input.steps.length,
|
|
114
|
+
pendingAppends: pendingCount,
|
|
115
|
+
}));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
bookkeepingErrors.push(`event append failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
}
|
|
119
|
+
return { request, pendingCount, ...(bookkeepingErrors.length > 0 ? { bookkeepingError: bookkeepingErrors.join("; ") } : {}) };
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
function readAppendRequest(filePath: string): ChainAppendRequest | undefined {
|
|
@@ -135,8 +153,8 @@ const MAX_STATUS_STEP_DESCRIPTION_CHARS = 160;
|
|
|
135
153
|
|
|
136
154
|
/** Bounded one-line per-step task description persisted into status.json for fleet display. */
|
|
137
155
|
export function statusStepDescription(task: string | undefined): string | undefined {
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
if (!task?.trim()) return undefined;
|
|
157
|
+
const description = PROMPT_REDACTED;
|
|
140
158
|
return description.length > MAX_STATUS_STEP_DESCRIPTION_CHARS
|
|
141
159
|
? `${description.slice(0, MAX_STATUS_STEP_DESCRIPTION_CHARS - 1)}…`
|
|
142
160
|
: description;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { safeTerminalText } from "../../shared/display-text.ts";
|
|
4
5
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
5
6
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
6
7
|
import {
|
|
@@ -196,7 +197,7 @@ function readSessionTranscriptTail(sessionFile: string, maxLines: number, truste
|
|
|
196
197
|
try {
|
|
197
198
|
const parsed = JSON.parse(line) as unknown;
|
|
198
199
|
const messageLine = sessionMessageLine(parsed);
|
|
199
|
-
if (messageLine) lines.push(messageLine);
|
|
200
|
+
if (messageLine) lines.push(...messageLine.split(/\r?\n/));
|
|
200
201
|
} catch {
|
|
201
202
|
malformed++;
|
|
202
203
|
}
|
|
@@ -410,6 +411,18 @@ function appendKnownArtifacts(lines: string[], input: { outputPaths: string[]; s
|
|
|
410
411
|
for (const artifact of artifacts) lines.push(` ${artifact}`);
|
|
411
412
|
}
|
|
412
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Sanitizes each assembled line independently.
|
|
416
|
+
*
|
|
417
|
+
* Sanitizing the joined response would let a single binary fragment in child
|
|
418
|
+
* output collapse the whole transcript, including run state, artifact paths, and
|
|
419
|
+
* warnings, into the binary placeholder. Per-line sanitization keeps that damage
|
|
420
|
+
* to the offending line.
|
|
421
|
+
*/
|
|
422
|
+
function safeTranscriptLines(lines: string[]): string {
|
|
423
|
+
return lines.map((line) => safeTerminalText(line)).join("\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
413
426
|
function appendTranscriptBody(lines: string[], sourceLabel: string, sourceLines: string[], truncated: boolean): void {
|
|
414
427
|
lines.push(`${sourceLabel}${truncated ? " (tail truncated)" : ""}:`);
|
|
415
428
|
if (sourceLines.length === 0) {
|
|
@@ -470,7 +483,7 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
|
|
|
470
483
|
for (const warning of warnings) lines.push(` ${warning}`);
|
|
471
484
|
}
|
|
472
485
|
appendTranscriptBody(lines, transcriptSource, transcriptLines, truncated);
|
|
473
|
-
return lines
|
|
486
|
+
return safeTranscriptLines(lines);
|
|
474
487
|
}
|
|
475
488
|
|
|
476
489
|
export function formatNestedRunTranscript(run: NestedRunSummary, options: TranscriptOptions = {}): string {
|
|
@@ -488,7 +501,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
|
|
|
488
501
|
appendKnownArtifacts(lines, { outputPaths: [], sessionFile: run.sessionFile });
|
|
489
502
|
if (!run.sessionFile) {
|
|
490
503
|
appendTranscriptBody(lines, "Transcript tail", [], false);
|
|
491
|
-
return lines
|
|
504
|
+
return safeTranscriptLines(lines);
|
|
492
505
|
}
|
|
493
506
|
const sessionTail = readSessionTranscriptTail(run.sessionFile, lineLimit, options.sessionRoots ?? []);
|
|
494
507
|
if (sessionTail.warnings.length) {
|
|
@@ -496,7 +509,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
|
|
|
496
509
|
for (const warning of sessionTail.warnings) lines.push(` ${warning}`);
|
|
497
510
|
}
|
|
498
511
|
appendTranscriptBody(lines, `Session transcript tail from ${run.sessionFile}`, sessionTail.lines, false);
|
|
499
|
-
return lines
|
|
512
|
+
return safeTranscriptLines(lines);
|
|
500
513
|
}
|
|
501
514
|
|
|
502
515
|
export function formatAsyncResultTranscript(data: {
|
|
@@ -536,5 +549,5 @@ export function formatAsyncResultTranscript(data: {
|
|
|
536
549
|
].filter((line): line is string => Boolean(line));
|
|
537
550
|
appendKnownArtifacts(lines, { outputPaths: [], sessionFile, resultPath });
|
|
538
551
|
appendTranscriptBody(lines, "Result transcript tail", transcriptLines.filter((line) => line.trim()), output.split(/\r?\n/).length > lineLimit);
|
|
539
|
-
return lines
|
|
552
|
+
return safeTranscriptLines(lines);
|
|
540
553
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import type { ProcessTreeTerminalV1 } from "../../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TERM_GRACE_MS = 3000;
|
|
5
|
+
const DEFAULT_KILL_VERIFY_MS = 1000;
|
|
6
|
+
const VERIFY_INTERVAL_MS = 25;
|
|
7
|
+
|
|
8
|
+
type SignalResult = "sent" | "absent" | { diagnostic: string };
|
|
9
|
+
|
|
10
|
+
function diagnostic(error: unknown): string {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function signalProcess(id: number, signal: NodeJS.Signals): SignalResult {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(id, signal);
|
|
17
|
+
return "sent";
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") return "absent";
|
|
20
|
+
return { diagnostic: diagnostic(error) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function activeProcessGroupMembers(processGroupId: number): number[] | { diagnostic: string } {
|
|
25
|
+
const result = spawnSync("ps", ["-axo", "pid=,pgid=,stat="], { encoding: "utf-8" });
|
|
26
|
+
if (result.error || result.status !== 0) {
|
|
27
|
+
return { diagnostic: result.error ? diagnostic(result.error) : (result.stderr.trim() || `ps exited with ${result.status}`) };
|
|
28
|
+
}
|
|
29
|
+
const members: number[] = [];
|
|
30
|
+
for (const line of result.stdout.split("\n")) {
|
|
31
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)/.exec(line);
|
|
32
|
+
if (!match || Number(match[2]) !== processGroupId || match[3]!.startsWith("Z")) continue;
|
|
33
|
+
members.push(Number(match[1]));
|
|
34
|
+
}
|
|
35
|
+
return members;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function waitUntilGroupTerminal(
|
|
39
|
+
processGroupId: number,
|
|
40
|
+
timeoutMs: number,
|
|
41
|
+
): Promise<false | { state: "enumeration-failed" | "still-active"; diagnostic: string }> {
|
|
42
|
+
const deadline = Date.now() + timeoutMs;
|
|
43
|
+
while (true) {
|
|
44
|
+
const members = activeProcessGroupMembers(processGroupId);
|
|
45
|
+
if (Array.isArray(members) && members.length === 0) return false;
|
|
46
|
+
const remaining = deadline - Date.now();
|
|
47
|
+
if (remaining <= 0) {
|
|
48
|
+
if (!Array.isArray(members)) return { state: "enumeration-failed", diagnostic: members.diagnostic };
|
|
49
|
+
return { state: "still-active", diagnostic: `Process group ${processGroupId} still has active members: ${members.join(", ")}.` };
|
|
50
|
+
}
|
|
51
|
+
await new Promise<void>((resolve) => setTimeout(resolve, Math.min(VERIFY_INTERVAL_MS, remaining)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function observed(processGroupId: number): ProcessTreeTerminalV1 {
|
|
56
|
+
return { state: "observed", mechanism: "posix-process-group", processGroupId, verifiedAt: Date.now() };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Owns one writer process group and arbitrates its cleanup exactly once. */
|
|
60
|
+
export interface OwnedProcessTreeController {
|
|
61
|
+
terminate(): Promise<ProcessTreeTerminalV1>;
|
|
62
|
+
finishAfterWriterClose(): Promise<ProcessTreeTerminalV1>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createOwnedProcessTreeController(
|
|
66
|
+
pid: number,
|
|
67
|
+
options: { termGraceMs?: number; killVerifyMs?: number } = {},
|
|
68
|
+
): OwnedProcessTreeController {
|
|
69
|
+
let termination: Promise<ProcessTreeTerminalV1> | undefined;
|
|
70
|
+
const posixGroupOwned = process.platform !== "win32";
|
|
71
|
+
const target = posixGroupOwned ? -pid : pid;
|
|
72
|
+
|
|
73
|
+
const terminate = (): Promise<ProcessTreeTerminalV1> => {
|
|
74
|
+
if (termination) return termination;
|
|
75
|
+
termination = (async () => {
|
|
76
|
+
if (!posixGroupOwned) {
|
|
77
|
+
signalProcess(target, "SIGTERM");
|
|
78
|
+
return { state: "unknown", reason: "unsupported-platform" };
|
|
79
|
+
}
|
|
80
|
+
const term = signalProcess(target, "SIGTERM");
|
|
81
|
+
if (term !== "sent" && term !== "absent") {
|
|
82
|
+
return { state: "unknown", reason: "signal-failed", diagnostic: term.diagnostic };
|
|
83
|
+
}
|
|
84
|
+
const termExit = await waitUntilGroupTerminal(pid, options.termGraceMs ?? DEFAULT_TERM_GRACE_MS);
|
|
85
|
+
if (termExit === false) return observed(pid);
|
|
86
|
+
|
|
87
|
+
const kill = signalProcess(target, "SIGKILL");
|
|
88
|
+
if (kill !== "sent" && kill !== "absent") {
|
|
89
|
+
const members = activeProcessGroupMembers(pid);
|
|
90
|
+
if (!Array.isArray(members) || members.length > 0) {
|
|
91
|
+
return { state: "unknown", reason: "signal-failed", diagnostic: kill.diagnostic };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const killExit = await waitUntilGroupTerminal(pid, options.killVerifyMs ?? DEFAULT_KILL_VERIFY_MS);
|
|
95
|
+
if (killExit !== false) {
|
|
96
|
+
return { state: "unknown", reason: "verification-failed", diagnostic: killExit.diagnostic };
|
|
97
|
+
}
|
|
98
|
+
return observed(pid);
|
|
99
|
+
})();
|
|
100
|
+
return termination;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return { terminate, finishAfterWriterClose: terminate };
|
|
104
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ProcessTerminalV1,
|
|
11
11
|
} from "../../shared/types.ts";
|
|
12
12
|
import { canonicalSessionId, inspectSessionLease } from "../shared/session-lease.ts";
|
|
13
|
+
import { releaseActiveRunIndex } from "./active-run-index.ts";
|
|
13
14
|
|
|
14
15
|
export interface ProcessTerminalCandidate {
|
|
15
16
|
version: 1;
|
|
@@ -40,9 +41,19 @@ function validProcessInstance(value: unknown, kind?: "runner" | "pi-writer"): va
|
|
|
40
41
|
if (typeof value.closeObservedAt !== "number" || !Number.isFinite(value.closeObservedAt)) return false;
|
|
41
42
|
if (typeof value.exitCode !== "number" && value.exitCode !== null) return false;
|
|
42
43
|
if (typeof value.signal !== "string" && value.signal !== null) return false;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if (value.kind === "runner") return value.attempt === undefined;
|
|
45
|
+
if (typeof value.attempt !== "number" || !Number.isInteger(value.attempt) || value.attempt < 0 || !isRecord(value.processTree)) return false;
|
|
46
|
+
if (value.processTree.state === "observed") {
|
|
47
|
+
return value.processTree.mechanism === "posix-process-group"
|
|
48
|
+
&& typeof value.processTree.processGroupId === "number"
|
|
49
|
+
&& Number.isInteger(value.processTree.processGroupId)
|
|
50
|
+
&& value.processTree.processGroupId > 0
|
|
51
|
+
&& typeof value.processTree.verifiedAt === "number"
|
|
52
|
+
&& Number.isFinite(value.processTree.verifiedAt);
|
|
53
|
+
}
|
|
54
|
+
return value.processTree.state === "unknown"
|
|
55
|
+
&& ["unsupported-platform", "signal-failed", "verification-failed"].includes(String(value.processTree.reason))
|
|
56
|
+
&& (value.processTree.diagnostic === undefined || typeof value.processTree.diagnostic === "string");
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
function validInstance(value: unknown): value is ProcessInstanceExitV1 {
|
|
@@ -249,6 +260,8 @@ export function finalizeProcessTerminal(
|
|
|
249
260
|
proof = unknownProof(runId, runnerClose.processInstanceId, "canonical-session-release-unverified");
|
|
250
261
|
} else if (inconsistentWriters || (allWriters.length === 0 && expectedEntries.length === 0)) {
|
|
251
262
|
proof = unknownProof(runId, runnerClose.processInstanceId, "writer-close-unverified");
|
|
263
|
+
} else if (allWriters.some((writer) => writer.kind === "pi-writer" && writer.processTree.state !== "observed")) {
|
|
264
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, "process-tree-unverified");
|
|
252
265
|
} else {
|
|
253
266
|
const runner: ProcessInstanceExitV1 = { kind: "runner", ...runnerClose };
|
|
254
267
|
const canonicalSession = session && sessionProjection(candidate, session);
|
|
@@ -271,6 +284,7 @@ export function finalizeProcessTerminal(
|
|
|
271
284
|
try {
|
|
272
285
|
writeAtomicJson(processTerminalPath(asyncDir), proof);
|
|
273
286
|
durable = true;
|
|
287
|
+
if (proof.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
274
288
|
overlayStatus(asyncDir, proof, candidateForOverlay);
|
|
275
289
|
fs.appendFileSync(path.join(asyncDir, "events.jsonl"), `${JSON.stringify({ type: "subagent.run.process_terminal", lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, ts: Date.now(), runId, processTerminal: proof })}\n`, "utf-8");
|
|
276
290
|
} catch {
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AsyncRunSummary } from "./async-status.ts";
|
|
3
3
|
|
|
4
|
+
const INTERCOM_DETACH_ERROR = "detached for intercom coordination";
|
|
5
|
+
|
|
6
|
+
function isIntercomDetached(run: AsyncRunSummary): boolean {
|
|
7
|
+
return run.steps.some((step) => step.execution?.status === "detached"
|
|
8
|
+
|| step.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true)
|
|
9
|
+
|| run.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatIntercomDetachGuidance(run: AsyncRunSummary): string | undefined {
|
|
13
|
+
if (!isIntercomDetached(run)) return undefined;
|
|
14
|
+
return `Run "${run.id}" detached for intercom coordination. Reply to the supervisor request first, then wait with subagent_wait({ id: "${run.id}" }). Use subagent({ action: "status", id: "${run.id}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefined {
|
|
5
18
|
const step = run.steps.find((candidate) => candidate.status === "failed" && candidate.sessionFile && fs.existsSync(candidate.sessionFile));
|
|
6
19
|
if (!step) {
|
|
@@ -15,19 +28,26 @@ export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefin
|
|
|
15
28
|
|
|
16
29
|
export function formatResumeFirstFailedRunDetail(run: AsyncRunSummary): string | undefined {
|
|
17
30
|
if (run.state !== "failed") return undefined;
|
|
31
|
+
const detachGuidance = formatIntercomDetachGuidance(run);
|
|
32
|
+
if (detachGuidance) return detachGuidance;
|
|
18
33
|
const command = formatAsyncReviveCommand(run);
|
|
19
34
|
if (!command) return undefined;
|
|
20
35
|
return `Resume-first: failed run "${run.id}" has a persisted child session. Revive the original run with ${command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
export function formatResumeFirstFailedRunsNote(runs: AsyncRunSummary[]): string {
|
|
24
|
-
const
|
|
25
|
-
|
|
39
|
+
const failedRuns = runs.filter((run) => run.state === "failed");
|
|
40
|
+
const detachGuidance = failedRuns
|
|
41
|
+
.map(formatIntercomDetachGuidance)
|
|
42
|
+
.filter((guidance): guidance is string => Boolean(guidance));
|
|
43
|
+
const resumable = failedRuns
|
|
44
|
+
.filter((run) => !isIntercomDetached(run))
|
|
26
45
|
.map((run) => ({ run, command: formatAsyncReviveCommand(run) }))
|
|
27
46
|
.filter((entry): entry is { run: AsyncRunSummary; command: string } => Boolean(entry.command));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
const resumeGuidance = resumable.length === 0
|
|
48
|
+
? ""
|
|
49
|
+
: resumable.length === 1
|
|
50
|
+
? ` Resume-first: failed run "${resumable[0]!.run.id}" has a persisted child session. Revive the original run with ${resumable[0]!.command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`
|
|
51
|
+
: ` Resume-first: ${resumable.length} failed runs have persisted child sessions. Inspect status and revive each original run before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
|
|
52
|
+
return `${detachGuidance.length > 0 ? ` ${detachGuidance.join(" ")}` : ""}${resumeGuidance}`;
|
|
33
53
|
}
|
|
@@ -8,6 +8,8 @@ const MAX_TASK_SUMMARY_LENGTH = 120;
|
|
|
8
8
|
export interface RetainedChild {
|
|
9
9
|
runId: string;
|
|
10
10
|
parentRunId?: string;
|
|
11
|
+
workflowKey?: string;
|
|
12
|
+
state: "complete" | "failed" | "paused";
|
|
11
13
|
agent: string;
|
|
12
14
|
taskSummary: string;
|
|
13
15
|
completedAt: number;
|
|
@@ -33,16 +35,20 @@ function boundedTaskSummary(value: string | undefined): string {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function listRetainedChildren(asyncDirRoot: string, sessionId: string): RetainedChild[] {
|
|
36
|
-
return listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete"], reconcile: false })
|
|
38
|
+
return listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete", "failed", "paused"], reconcile: false })
|
|
37
39
|
.flatMap((run) => {
|
|
38
40
|
if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
|
|
41
|
+
if (run.state !== "complete" && run.state !== "failed" && run.state !== "paused") return [];
|
|
39
42
|
const step = run.steps[0]!;
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
const terminalStep = step.status === "complete" || step.status === "completed" || step.status === "failed" || step.status === "paused";
|
|
44
|
+
if (!terminalStep || !retainedSessionFile(step.sessionFile ?? run.sessionFile)) return [];
|
|
45
|
+
const completedAt = run.endedAt ?? run.lastUpdate;
|
|
42
46
|
if (completedAt === undefined) return [];
|
|
43
47
|
return [{
|
|
44
48
|
runId: run.id,
|
|
45
49
|
...(run.parentWorkflowRunId ? { parentRunId: run.parentWorkflowRunId } : {}),
|
|
50
|
+
...(run.workflowKey ? { workflowKey: run.workflowKey } : {}),
|
|
51
|
+
state: run.state,
|
|
46
52
|
agent: step.agent,
|
|
47
53
|
taskSummary: boundedTaskSummary(step.description),
|
|
48
54
|
completedAt,
|
|
@@ -55,13 +61,15 @@ export function listRetainedChildren(asyncDirRoot: string, sessionId: string): R
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export function formatRetainedChildren(children: RetainedChild[]): string {
|
|
58
|
-
if (children.length === 0) return "No
|
|
64
|
+
if (children.length === 0) return "No recoverable retained children in the active parent session.";
|
|
59
65
|
return [
|
|
60
|
-
`
|
|
66
|
+
`Recoverable retained children (newest first, last ${MAX_RETAINED_CHILDREN}):`,
|
|
61
67
|
...children.flatMap((child) => [
|
|
62
|
-
`- ${child.runId} | ${child.agent} | ${new Date(child.completedAt).toISOString()}`,
|
|
68
|
+
`- ${child.runId} | ${child.agent} | ${child.state} | ${new Date(child.completedAt).toISOString()}`,
|
|
69
|
+
...(child.parentRunId ? [` workflow: ${child.parentRunId}${child.workflowKey ? ` (${child.workflowKey})` : ""}`] : []),
|
|
63
70
|
` task: ${child.taskSummary || "(no task summary)"}`,
|
|
64
71
|
` session: ${child.sessionPath}`,
|
|
72
|
+
` resume: subagent({ action: "resume", id: "${child.runId}", message: "..." })`,
|
|
65
73
|
...(child.tokenTotals ? [` tokens: input ${child.tokenTotals.input}, output ${child.tokenTotals.output}, total ${child.tokenTotals.total}`] : []),
|
|
66
74
|
]),
|
|
67
75
|
].join("\n");
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { safeTerminalText } from "../../shared/display-text.ts";
|
|
4
5
|
import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
|
|
5
6
|
import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
|
|
6
7
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
7
8
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
8
9
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
9
10
|
import { DIRS, type AsyncStatus, type Details, type ForegroundResumeRun, type NestedRunSummary, type SteeringStatus, type SubagentState } from "../../shared/types.ts";
|
|
11
|
+
import { inspectActiveAsyncCapacityOwner, type ActiveAsyncCapacityInspection } from "./active-async-capacity.ts";
|
|
10
12
|
import { readStatus } from "../../shared/utils.ts";
|
|
11
13
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
12
14
|
import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
|
|
@@ -19,6 +21,7 @@ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-
|
|
|
19
21
|
import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
20
22
|
import { readMissionBinding } from "../../missions/lifecycle.ts";
|
|
21
23
|
import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
|
|
24
|
+
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
22
25
|
|
|
23
26
|
interface RunStatusParams {
|
|
24
27
|
action?: string;
|
|
@@ -30,6 +33,66 @@ interface RunStatusParams {
|
|
|
30
33
|
lines?: number;
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
function formatProcessTerminal(value: AsyncStatus["processTerminal"] | undefined): string {
|
|
37
|
+
if (!value) return "missing";
|
|
38
|
+
return `${value.state}${value.reason ? ` (${value.reason})` : ""}${value.runnerProcessInstanceId ? ` · runner ${value.runnerProcessInstanceId}` : ""}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function debugProcessTerminal(asyncDir: string, status: AsyncStatus): { sidecar?: AsyncStatus["processTerminal"]; overlay?: AsyncStatus["processTerminal"] } {
|
|
42
|
+
const expected = { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId };
|
|
43
|
+
return {
|
|
44
|
+
sidecar: readProcessTerminal(asyncDir, expected),
|
|
45
|
+
overlay: sanitizeProcessTerminal(status.processTerminal, expected, path.join(asyncDir, "status.json")),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatCapacityOwner(inspect: ActiveAsyncCapacityInspection): string[] {
|
|
50
|
+
if (!inspect.owner) return [`Active capacity: ${inspect.release.state} — ${inspect.release.reason}`];
|
|
51
|
+
const owner = inspect.owner;
|
|
52
|
+
return [
|
|
53
|
+
`Active capacity: ${inspect.release.state} — ${inspect.release.reason}`,
|
|
54
|
+
`Capacity owner: ${inspect.relation} slot ${owner.slot}, ${owner.kind}, generation ${owner.generation}`,
|
|
55
|
+
`Capacity session: ${owner.ownerSessionId}`,
|
|
56
|
+
owner.sourceRunId ? `Capacity source run: ${owner.sourceRunId}` : undefined,
|
|
57
|
+
`Capacity async dir: ${owner.asyncDir}`,
|
|
58
|
+
owner.runnerProcessInstanceId ? `Capacity runner: ${owner.runnerProcessInstanceId}` : undefined,
|
|
59
|
+
owner.runnerStartedAt !== undefined ? `Capacity runner started: ${new Date(owner.runnerStartedAt).toISOString()}` : undefined,
|
|
60
|
+
].filter((line): line is string => line !== undefined);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatWorkflowDebug(status: AsyncStatus): string[] {
|
|
64
|
+
if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey) return [];
|
|
65
|
+
const lines = [
|
|
66
|
+
status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
|
|
67
|
+
status.mode === "workflow" ? `Workflow children: ${(status.steps ?? []).length}` : undefined,
|
|
68
|
+
].filter((line): line is string => line !== undefined);
|
|
69
|
+
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
70
|
+
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}` : ""}`);
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string; sidecarProcessTerminal: AsyncStatus["processTerminal"] | undefined; overlayProcessTerminal: AsyncStatus["processTerminal"] | undefined; capacity: ActiveAsyncCapacityInspection }): string {
|
|
76
|
+
const { status, asyncDir, sidecarProcessTerminal, overlayProcessTerminal, capacity } = input;
|
|
77
|
+
const lines = [
|
|
78
|
+
"Run lifecycle debug",
|
|
79
|
+
`Run: ${status.runId}`,
|
|
80
|
+
`Dir: ${asyncDir}`,
|
|
81
|
+
`Status file: ${path.join(asyncDir, "status.json")}`,
|
|
82
|
+
`Process terminal file: ${path.join(asyncDir, "process-terminal.json")}`,
|
|
83
|
+
`Session: ${status.sessionId ?? "unknown"}`,
|
|
84
|
+
`State: ${status.state}`,
|
|
85
|
+
`Mode: ${status.mode}`,
|
|
86
|
+
status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}` : undefined,
|
|
87
|
+
status.workflowKey ? `Workflow key: ${status.workflowKey}` : undefined,
|
|
88
|
+
`Status process terminal: ${formatProcessTerminal(overlayProcessTerminal)}`,
|
|
89
|
+
`Sidecar process terminal: ${formatProcessTerminal(sidecarProcessTerminal)}`,
|
|
90
|
+
...formatCapacityOwner(capacity),
|
|
91
|
+
...formatWorkflowDebug(status),
|
|
92
|
+
].filter((line): line is string => line !== undefined);
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
33
96
|
interface RunStatusDeps {
|
|
34
97
|
asyncDirRoot?: string;
|
|
35
98
|
resultsDir?: string;
|
|
@@ -38,6 +101,7 @@ interface RunStatusDeps {
|
|
|
38
101
|
state?: SubagentState;
|
|
39
102
|
nested?: NestedRunResolutionScope;
|
|
40
103
|
sessionRoots?: string[];
|
|
104
|
+
activeCapacityRoot?: string;
|
|
41
105
|
}
|
|
42
106
|
|
|
43
107
|
function hasExistingSessionFile(value: unknown): value is string {
|
|
@@ -49,12 +113,16 @@ function formatCheckpointGuidance(runId: string | undefined, checkpoint: AsyncSt
|
|
|
49
113
|
return `Checkpoint: ${checkpoint.name}${checkpoint.message ? ` — ${checkpoint.message}` : ""}\nApprove: subagent({ action: "approve-checkpoint", id: "${runId}" })\nReject: subagent({ action: "reject-checkpoint", id: "${runId}" })`;
|
|
50
114
|
}
|
|
51
115
|
|
|
52
|
-
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
116
|
+
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown; runId?: unknown; workflowKey?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
53
117
|
if (options.stopped) return "Resume: unavailable; stopped runs are not resumable. Start a new run instead.";
|
|
54
118
|
const knownChildren = children
|
|
55
119
|
.map((child, index) => ({ child, index }))
|
|
56
120
|
.filter(({ child }) => typeof child.agent === "string");
|
|
57
121
|
if (!runId || knownChildren.length === 0) return "Resume: unavailable; no child session file was persisted.";
|
|
122
|
+
const workflowChildren = knownChildren.filter(({ child }) => typeof child.runId === "string" && child.runId.trim() && hasExistingSessionFile(child.sessionFile));
|
|
123
|
+
if (workflowChildren.length > 0) {
|
|
124
|
+
return workflowChildren.map(({ child }) => `Revive workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: subagent({ action: "resume", id: "${child.runId}", message: "..." })`).join("\n");
|
|
125
|
+
}
|
|
58
126
|
const singleSessionFile = knownChildren[0]?.child.sessionFile ?? fallbackSessionFile;
|
|
59
127
|
if (children.length === 1 && knownChildren.length === 1 && hasExistingSessionFile(singleSessionFile)) {
|
|
60
128
|
return `Revive: subagent({ action: "resume", id: "${runId}", message: "..." })`;
|
|
@@ -177,7 +245,7 @@ function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options:
|
|
|
177
245
|
lines.push("Result transcript tail:");
|
|
178
246
|
if (outputLines.length === 0) lines.push(" (no recovered final output available yet)");
|
|
179
247
|
else for (const line of outputLines) lines.push(` ${line}`);
|
|
180
|
-
return lines.join("\n");
|
|
248
|
+
return lines.map((line) => safeTerminalText(line)).join("\n");
|
|
181
249
|
}
|
|
182
250
|
|
|
183
251
|
function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): string {
|
|
@@ -262,7 +330,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
262
330
|
let location;
|
|
263
331
|
try {
|
|
264
332
|
const requestedId = params.id ?? params.runId;
|
|
265
|
-
if (
|
|
333
|
+
if (params.action === "debug.run") {
|
|
334
|
+
location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
|
|
335
|
+
} else if (!params.dir && requestedId) {
|
|
266
336
|
const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
267
337
|
if (resolved?.kind === "foreground") {
|
|
268
338
|
const run = deps.state?.foregroundRuns?.get(resolved.id);
|
|
@@ -330,6 +400,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
330
400
|
}
|
|
331
401
|
const status = reconciliation.status;
|
|
332
402
|
if (!status && diskStatus?.displayDismissedAt !== undefined) {
|
|
403
|
+
if (params.action === "debug.run") {
|
|
404
|
+
const { sidecar, overlay } = debugProcessTerminal(asyncDir, diskStatus);
|
|
405
|
+
const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
|
|
406
|
+
return {
|
|
407
|
+
content: [{ type: "text", text: formatRunLifecycleDebug({ status: diskStatus, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
|
|
408
|
+
details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
|
|
409
|
+
};
|
|
410
|
+
}
|
|
333
411
|
if (params.view === "transcript") {
|
|
334
412
|
if (currentSessionId && diskStatus.sessionId !== currentSessionId) {
|
|
335
413
|
return {
|
|
@@ -349,6 +427,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
349
427
|
const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
|
|
350
428
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
351
429
|
if (status) {
|
|
430
|
+
if (params.action === "debug.run") {
|
|
431
|
+
const { sidecar, overlay } = debugProcessTerminal(asyncDir, status);
|
|
432
|
+
const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: "text", text: formatRunLifecycleDebug({ status, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
|
|
435
|
+
details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
|
|
436
|
+
};
|
|
437
|
+
}
|
|
352
438
|
if (params.view === "transcript") {
|
|
353
439
|
if (currentSessionId && status.sessionId !== currentSessionId) {
|
|
354
440
|
return {
|
|
@@ -389,6 +475,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
389
475
|
const steeringText = formatSteeringSummary(status);
|
|
390
476
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
391
477
|
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
478
|
+
const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
|
|
479
|
+
const runFanoutBudget = runFanoutBudgetDescriptor ? getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor) : status.runFanoutBudget;
|
|
392
480
|
let missionId: string | undefined;
|
|
393
481
|
try {
|
|
394
482
|
missionId = readMissionBinding(asyncDir)?.missionId;
|
|
@@ -410,6 +498,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
410
498
|
statusActivityText ? `Activity: ${statusActivityText}` : undefined,
|
|
411
499
|
steeringText ? `Steering: ${steeringText}` : undefined,
|
|
412
500
|
`Mode: ${status.mode}`,
|
|
501
|
+
runFanoutBudget ? formatRunFanoutBudget(runFanoutBudget) : undefined,
|
|
413
502
|
status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
|
|
414
503
|
status.mode === "workflow" && workflowReturnPreview !== undefined ? `Return: ${workflowReturnPreview}` : undefined,
|
|
415
504
|
status.mode === "workflow" && workflowEmitPreview !== undefined ? `Latest emit: ${workflowEmitPreview}` : undefined,
|
|
@@ -466,14 +555,21 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
466
555
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
467
556
|
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
468
557
|
|
|
469
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
|
|
558
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
|
|
470
559
|
}
|
|
471
560
|
}
|
|
472
561
|
|
|
473
562
|
if (resultPath) {
|
|
563
|
+
if (params.action === "debug.run") {
|
|
564
|
+
return {
|
|
565
|
+
content: [{ type: "text", text: "Run lifecycle debug needs an async run directory with status.json." }],
|
|
566
|
+
isError: true,
|
|
567
|
+
details: { mode: "single", results: [] },
|
|
568
|
+
};
|
|
569
|
+
}
|
|
474
570
|
try {
|
|
475
571
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
476
|
-
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; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
|
|
572
|
+
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 }> };
|
|
477
573
|
if (params.view === "transcript") {
|
|
478
574
|
try {
|
|
479
575
|
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
-
import { updateActiveRunIndex } from "./active-run-index.ts";
|
|
4
|
+
import { releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
5
5
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
6
|
import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
|
|
7
7
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
@@ -261,7 +261,7 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
|
|
|
261
261
|
const repair = buildFailedRepair(status, asyncDir, now, reason);
|
|
262
262
|
writeAtomicJson(resultPath, repair.result);
|
|
263
263
|
writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
|
|
264
|
-
|
|
264
|
+
if (repair.status.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
265
265
|
appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
|
|
266
266
|
type: "subagent.run.repaired_stale",
|
|
267
267
|
ts: now,
|
|
@@ -351,7 +351,7 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
|
|
|
351
351
|
: undefined;
|
|
352
352
|
if (terminalStatus) {
|
|
353
353
|
writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
|
|
354
|
-
|
|
354
|
+
if (terminalStatus.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
355
355
|
return { status: terminalStatus, repaired: true, resultPath, message: "Existing async result file was used to repair stale running status." };
|
|
356
356
|
}
|
|
357
357
|
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath };
|