pi-subagents 0.49.0 → 0.51.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 +97 -0
- package/agents/gpt-pro.md +17 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +37 -2
- package/docs/configuration.md +76 -14
- package/docs/extension-api.md +78 -1
- package/docs/missions.md +1 -1
- package/docs/observability.md +20 -4
- package/docs/tool-reference.md +55 -39
- package/docs/workflows.md +171 -5
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
- package/skills/pi-subagents/references/execution-controls.md +22 -18
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +135 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +42 -11
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +36 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +210 -90
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +14 -36
- package/src/extension/tool-description.ts +37 -24
- package/src/inspectors/herdr/actions.ts +5 -9
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +4 -8
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +49 -51
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +94 -1
- package/src/runs/background/async-execution.ts +98 -38
- package/src/runs/background/async-job-tracker.ts +21 -4
- package/src/runs/background/async-resume.ts +30 -17
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +47 -56
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +505 -0
- package/src/runs/background/result-watcher.ts +250 -51
- package/src/runs/background/retained-children.ts +79 -20
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +37 -29
- package/src/runs/background/run-status.ts +34 -20
- package/src/runs/background/scheduled-runs.ts +71 -27
- package/src/runs/background/stale-run-reconciler.ts +33 -16
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +535 -161
- package/src/runs/background/subagent-wait.ts +9 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +22 -2
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +221 -15
- package/src/runs/foreground/subagent-executor.ts +529 -1519
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +34 -3
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +437 -0
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +6 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +41 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +88 -27
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +45 -28
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +186 -28
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +7 -5
- package/src/workflows/scripted-workflow.ts +424 -125
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1565
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-text.ts";
|
|
2
|
+
import type { AsyncJobState, AsyncJobStep, NestedRunSummary, NestedStepSummary, SubagentRunMode, SubagentState } from "../../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
|
|
5
|
+
export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
|
|
6
|
+
export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "PI_SUBAGENT_ASYNC_JSON:";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_RUNS = 20;
|
|
9
|
+
const DEFAULT_MAX_CHILDREN_PER_NODE = 8;
|
|
10
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
11
|
+
const DEFAULT_MAX_STRING_LENGTH = 160;
|
|
12
|
+
const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
|
|
13
|
+
|
|
14
|
+
export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
15
|
+
export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
|
|
16
|
+
|
|
17
|
+
export interface AsyncStatusSnapshotActivityV1 {
|
|
18
|
+
state?: string;
|
|
19
|
+
currentTool?: string;
|
|
20
|
+
lastActivityAt?: number;
|
|
21
|
+
currentToolStartedAt?: number;
|
|
22
|
+
turnCount?: number;
|
|
23
|
+
toolCount?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AsyncStatusSnapshotNodeV1 {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: AsyncStatusSnapshotKind;
|
|
29
|
+
label: string;
|
|
30
|
+
state: AsyncStatusSnapshotState;
|
|
31
|
+
startedAt?: number;
|
|
32
|
+
updatedAt?: number;
|
|
33
|
+
endedAt?: number;
|
|
34
|
+
activity?: AsyncStatusSnapshotActivityV1;
|
|
35
|
+
children?: AsyncStatusSnapshotNodeV1[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AsyncStatusSnapshotCapsV1 {
|
|
39
|
+
maxRuns: number;
|
|
40
|
+
maxChildrenPerNode: number;
|
|
41
|
+
maxDepth: number;
|
|
42
|
+
maxStringLength: number;
|
|
43
|
+
maxSerializedBytes: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AsyncStatusSnapshotOmittedV1 {
|
|
47
|
+
runs: number;
|
|
48
|
+
children: number;
|
|
49
|
+
byteLimitExceeded: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AsyncStatusSnapshotV1 {
|
|
53
|
+
kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
|
|
54
|
+
version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
|
|
55
|
+
generatedAt: number;
|
|
56
|
+
caps: AsyncStatusSnapshotCapsV1;
|
|
57
|
+
omitted: AsyncStatusSnapshotOmittedV1;
|
|
58
|
+
runs: AsyncStatusSnapshotNodeV1[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AsyncStatusSnapshotOptions {
|
|
62
|
+
generatedAt?: number;
|
|
63
|
+
maxRuns?: number;
|
|
64
|
+
maxChildrenPerNode?: number;
|
|
65
|
+
maxDepth?: number;
|
|
66
|
+
maxStringLength?: number;
|
|
67
|
+
maxSerializedBytes?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface BuildContext {
|
|
71
|
+
caps: AsyncStatusSnapshotCapsV1;
|
|
72
|
+
omitted: AsyncStatusSnapshotOmittedV1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveCaps(options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotCapsV1 {
|
|
76
|
+
return {
|
|
77
|
+
maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
|
|
78
|
+
maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
|
|
79
|
+
maxDepth: Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_MAX_DEPTH)),
|
|
80
|
+
maxStringLength: Math.max(0, Math.floor(options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH)),
|
|
81
|
+
maxSerializedBytes: Math.max(256, Math.floor(options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function publicText(value: unknown, fallback: string, maxLength: number): string {
|
|
86
|
+
if (typeof value !== "string") return fallback;
|
|
87
|
+
const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
|
|
88
|
+
return truncateDisplayText(normalized || fallback, maxLength);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function publicOptionalText(value: unknown, maxLength: number): string | undefined {
|
|
92
|
+
if (typeof value !== "string") return undefined;
|
|
93
|
+
const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
|
|
94
|
+
return normalized ? truncateDisplayText(normalized, maxLength) : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function publicTime(value: unknown): number | undefined {
|
|
98
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function publicCount(value: unknown): number | undefined {
|
|
102
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeState(value: string): AsyncStatusSnapshotState {
|
|
106
|
+
if (value === "completed") return "complete";
|
|
107
|
+
if (value === "pending") return "queued";
|
|
108
|
+
return value as AsyncStatusSnapshotState;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function terminalState(state: AsyncStatusSnapshotState): boolean {
|
|
112
|
+
return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function kindForMode(mode: SubagentRunMode | undefined): AsyncStatusSnapshotKind {
|
|
116
|
+
return mode === "workflow" ? "workflow" : "subagent";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function labelForAgents(agents: readonly string[] | undefined, fallback: string, maxLength: number): string {
|
|
120
|
+
if (!agents?.length) return publicText(fallback, fallback, maxLength);
|
|
121
|
+
const visible = agents.slice(0, 3).map((agent) => publicText(agent, "agent", maxLength)).join(", ");
|
|
122
|
+
const suffix = agents.length > 3 ? `, +${agents.length - 3} more` : "";
|
|
123
|
+
return truncateDisplayText(`${visible}${suffix}`, maxLength);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function activityFor(source: {
|
|
127
|
+
activityState?: unknown;
|
|
128
|
+
currentTool?: unknown;
|
|
129
|
+
lastActivityAt?: unknown;
|
|
130
|
+
currentToolStartedAt?: unknown;
|
|
131
|
+
turnCount?: unknown;
|
|
132
|
+
toolCount?: unknown;
|
|
133
|
+
}, ctx: BuildContext): AsyncStatusSnapshotActivityV1 | undefined {
|
|
134
|
+
const activity: AsyncStatusSnapshotActivityV1 = {
|
|
135
|
+
...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
|
|
136
|
+
...(publicOptionalText(source.currentTool, ctx.caps.maxStringLength) ? { currentTool: publicOptionalText(source.currentTool, ctx.caps.maxStringLength) } : {}),
|
|
137
|
+
...(publicTime(source.lastActivityAt) !== undefined ? { lastActivityAt: publicTime(source.lastActivityAt) } : {}),
|
|
138
|
+
...(publicTime(source.currentToolStartedAt) !== undefined ? { currentToolStartedAt: publicTime(source.currentToolStartedAt) } : {}),
|
|
139
|
+
...(publicCount(source.turnCount) !== undefined ? { turnCount: publicCount(source.turnCount) } : {}),
|
|
140
|
+
...(publicCount(source.toolCount) !== undefined ? { toolCount: publicCount(source.toolCount) } : {}),
|
|
141
|
+
};
|
|
142
|
+
return Object.keys(activity).length ? activity : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: BuildContext): void {
|
|
146
|
+
const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
|
|
147
|
+
children.push(...source.slice(0, remaining));
|
|
148
|
+
ctx.omitted.children += Math.max(0, source.length - remaining);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildStepNode(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
152
|
+
const state = normalizeState(step.status);
|
|
153
|
+
const updatedAt = publicTime(step.endedAt) ?? publicTime(step.lastActivityAt) ?? publicTime(step.startedAt);
|
|
154
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
155
|
+
id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
|
|
156
|
+
kind: "step",
|
|
157
|
+
label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
|
|
158
|
+
state,
|
|
159
|
+
...(publicTime(step.startedAt) !== undefined ? { startedAt: publicTime(step.startedAt) } : {}),
|
|
160
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
161
|
+
...(terminalState(state) && publicTime(step.endedAt) !== undefined ? { endedAt: publicTime(step.endedAt) } : {}),
|
|
162
|
+
...(activityFor(step, ctx) ? { activity: activityFor(step, ctx) } : {}),
|
|
163
|
+
};
|
|
164
|
+
if (depth < ctx.caps.maxDepth && step.children?.length) {
|
|
165
|
+
const nested = step.children.map((child, childIndex) => buildNestedNode(child, childIndex, depth + 1, ctx));
|
|
166
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
167
|
+
appendBoundedChildren(bounded, nested, ctx);
|
|
168
|
+
if (bounded.length) node.children = bounded;
|
|
169
|
+
} else if (step.children?.length) {
|
|
170
|
+
ctx.omitted.children += step.children.length;
|
|
171
|
+
}
|
|
172
|
+
return node;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildNestedNode(child: NestedRunSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
176
|
+
const state = normalizeState(child.state);
|
|
177
|
+
const updatedAt = publicTime(child.lastUpdate) ?? publicTime(child.endedAt) ?? publicTime(child.lastActivityAt) ?? publicTime(child.startedAt);
|
|
178
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
179
|
+
id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
|
|
180
|
+
kind: kindForMode(child.mode),
|
|
181
|
+
label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
182
|
+
state,
|
|
183
|
+
...(publicTime(child.startedAt) !== undefined ? { startedAt: publicTime(child.startedAt) } : {}),
|
|
184
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
185
|
+
...(terminalState(state) && publicTime(child.endedAt) !== undefined ? { endedAt: publicTime(child.endedAt) } : {}),
|
|
186
|
+
...(activityFor(child, ctx) ? { activity: activityFor(child, ctx) } : {}),
|
|
187
|
+
};
|
|
188
|
+
if (depth < ctx.caps.maxDepth) {
|
|
189
|
+
const nestedSteps = child.steps?.map((step, stepIndex) => buildStepNode(step, stepIndex, depth + 1, ctx)) ?? [];
|
|
190
|
+
const nestedChildren = child.children?.map((nested, childIndex) => buildNestedNode(nested, childIndex, depth + 1, ctx)) ?? [];
|
|
191
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
192
|
+
appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
|
|
193
|
+
if (bounded.length) node.children = bounded;
|
|
194
|
+
} else {
|
|
195
|
+
ctx.omitted.children += (child.steps?.length ?? 0) + (child.children?.length ?? 0);
|
|
196
|
+
}
|
|
197
|
+
return node;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildRunNode(job: AsyncJobState, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
201
|
+
const state = normalizeState(job.status);
|
|
202
|
+
const updatedAt = publicTime(job.updatedAt) ?? publicTime(job.startedAt);
|
|
203
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
204
|
+
id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
|
|
205
|
+
kind: kindForMode(job.mode),
|
|
206
|
+
label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
207
|
+
state,
|
|
208
|
+
...(publicTime(job.startedAt) !== undefined ? { startedAt: publicTime(job.startedAt) } : {}),
|
|
209
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
210
|
+
...(terminalState(state) && updatedAt !== undefined ? { endedAt: updatedAt } : {}),
|
|
211
|
+
...(activityFor(job, ctx) ? { activity: activityFor(job, ctx) } : {}),
|
|
212
|
+
};
|
|
213
|
+
if (ctx.caps.maxDepth > 0) {
|
|
214
|
+
const stepChildren = job.steps?.map((step, index) => buildStepNode(step, step.index ?? index, 1, ctx)) ?? [];
|
|
215
|
+
const nestedChildren = job.nestedChildren?.map((child, index) => buildNestedNode(child, index, 1, ctx)) ?? [];
|
|
216
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
217
|
+
appendBoundedChildren(bounded, [...stepChildren, ...nestedChildren], ctx);
|
|
218
|
+
if (bounded.length) node.children = bounded;
|
|
219
|
+
} else {
|
|
220
|
+
ctx.omitted.children += (job.steps?.length ?? 0) + (job.nestedChildren?.length ?? 0);
|
|
221
|
+
}
|
|
222
|
+
return node;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
|
|
226
|
+
return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
|
|
230
|
+
while (snapshot.runs.length > 0 && snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) {
|
|
231
|
+
snapshot.runs.pop();
|
|
232
|
+
snapshot.omitted.runs += 1;
|
|
233
|
+
snapshot.omitted.byteLimitExceeded = true;
|
|
234
|
+
}
|
|
235
|
+
if (snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) snapshot.omitted.byteLimitExceeded = true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function buildAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
|
|
239
|
+
const caps = resolveCaps(options);
|
|
240
|
+
const ctx: BuildContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
|
|
241
|
+
const sorted = [...jobs].sort((left, right) => {
|
|
242
|
+
const leftUpdated = left.updatedAt ?? left.startedAt ?? 0;
|
|
243
|
+
const rightUpdated = right.updatedAt ?? right.startedAt ?? 0;
|
|
244
|
+
return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
|
|
245
|
+
});
|
|
246
|
+
ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
|
|
247
|
+
const snapshot: AsyncStatusSnapshotV1 = {
|
|
248
|
+
kind: ASYNC_STATUS_SNAPSHOT_KIND,
|
|
249
|
+
version: ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
250
|
+
generatedAt: options.generatedAt ?? Date.now(),
|
|
251
|
+
caps,
|
|
252
|
+
omitted: ctx.omitted,
|
|
253
|
+
runs: sorted.slice(0, caps.maxRuns).map((job) => buildRunNode(job, ctx)),
|
|
254
|
+
};
|
|
255
|
+
enforceByteLimit(snapshot);
|
|
256
|
+
return snapshot;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
|
|
260
|
+
if (!state || !sessionId || state.currentSessionId !== sessionId) return [];
|
|
261
|
+
const jobs = new Map<string, AsyncJobState>();
|
|
262
|
+
for (const job of state.asyncJobs.values()) {
|
|
263
|
+
if (job.sessionId === sessionId) jobs.set(job.asyncId, job);
|
|
264
|
+
}
|
|
265
|
+
for (const job of state.fleetJobs?.values() ?? []) {
|
|
266
|
+
if (job.sessionId === sessionId && !jobs.has(job.asyncId)) jobs.set(job.asyncId, job);
|
|
267
|
+
}
|
|
268
|
+
return [...jobs.values()];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
|
|
272
|
+
return buildAsyncStatusSnapshot(asyncStatusSnapshotJobsForState(state, sessionId), options);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function encodeAsyncStatusSnapshotWidget(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): string[] {
|
|
276
|
+
return [`${ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX}${JSON.stringify(buildAsyncStatusSnapshot(jobs, options))}`];
|
|
277
|
+
}
|
|
@@ -2,9 +2,9 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState
|
|
5
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState } from "../../shared/types.ts";
|
|
6
6
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
8
8
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
9
9
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
10
10
|
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
@@ -12,7 +12,9 @@ import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-grou
|
|
|
12
12
|
import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
|
|
13
13
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
14
14
|
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
15
|
-
import { ACTIVE_RUN_INDEX_DIR, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
15
|
+
import { ACTIVE_RUN_INDEX_DIR, DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS, activeRunMarkerAgeMs, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
16
|
+
import { readRecentTerminalRunIndex, TERMINAL_RUN_INDEX_DIR } from "./terminal-run-index.ts";
|
|
17
|
+
import { canScanAsyncRunPrefix } from "./run-id-query.ts";
|
|
16
18
|
|
|
17
19
|
interface AsyncRunStepSummary {
|
|
18
20
|
index: number;
|
|
@@ -23,8 +25,8 @@ interface AsyncRunStepSummary {
|
|
|
23
25
|
phase?: string;
|
|
24
26
|
outputName?: string;
|
|
25
27
|
structured?: boolean;
|
|
26
|
-
checkpoint?: ChainCheckpointState;
|
|
27
28
|
status: AsyncJobStep["status"];
|
|
29
|
+
runner?: AsyncJobStep["runner"];
|
|
28
30
|
activityState?: ActivityState;
|
|
29
31
|
lastActivityAt?: number;
|
|
30
32
|
currentTool?: string;
|
|
@@ -82,6 +84,7 @@ export interface AsyncRunSummary {
|
|
|
82
84
|
mode: SubagentRunMode;
|
|
83
85
|
context?: ContextSummary;
|
|
84
86
|
cwd?: string;
|
|
87
|
+
sessionRoot?: string;
|
|
85
88
|
startedAt: number;
|
|
86
89
|
lastUpdate?: number;
|
|
87
90
|
endedAt?: number;
|
|
@@ -97,7 +100,6 @@ export interface AsyncRunSummary {
|
|
|
97
100
|
pendingAppends?: number;
|
|
98
101
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
99
102
|
steps: AsyncRunStepSummary[];
|
|
100
|
-
checkpoint?: ChainCheckpointState;
|
|
101
103
|
sessionDir?: string;
|
|
102
104
|
outputFile?: string;
|
|
103
105
|
totalTokens?: TokenUsage;
|
|
@@ -121,13 +123,18 @@ interface AsyncRunListOptions {
|
|
|
121
123
|
states?: Array<AsyncRunSummary["state"]>;
|
|
122
124
|
sessionId?: string;
|
|
123
125
|
limit?: number;
|
|
124
|
-
/** Limits
|
|
126
|
+
/** Limits terminal candidates using the timestamp embedded in index marker names. */
|
|
125
127
|
entryLimit?: number;
|
|
126
128
|
resultsDir?: string;
|
|
127
129
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
128
130
|
now?: () => number;
|
|
129
131
|
reconcile?: boolean;
|
|
130
132
|
runId?: string;
|
|
133
|
+
/** The caller already holds a canonical run id; never interpret a miss as a prefix. */
|
|
134
|
+
exactRunId?: boolean;
|
|
135
|
+
includeNested?: boolean;
|
|
136
|
+
/** Explicit repair/debug escape hatch. Normal runtime paths must not set this. */
|
|
137
|
+
repairScan?: boolean;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
function getErrorMessage(error: unknown): string {
|
|
@@ -155,7 +162,7 @@ function isAsyncRunDir(root: string, entry: string): boolean {
|
|
|
155
162
|
|
|
156
163
|
type TargetedAsyncRunResolution =
|
|
157
164
|
| { kind: "exact"; id: string }
|
|
158
|
-
| { kind: "
|
|
165
|
+
| { kind: "prefix" }
|
|
159
166
|
| { kind: "reject" };
|
|
160
167
|
|
|
161
168
|
/**
|
|
@@ -163,13 +170,13 @@ type TargetedAsyncRunResolution =
|
|
|
163
170
|
* accepting a path whose canonical location escaped the async root.
|
|
164
171
|
*/
|
|
165
172
|
export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessionId?: string): TargetedAsyncRunResolution {
|
|
166
|
-
if (!id || id === "." || id === ".." || id === ACTIVE_RUN_INDEX_DIR || path.basename(id) !== id) return { kind: "reject" };
|
|
173
|
+
if (!id || id === "." || id === ".." || id === ACTIVE_RUN_INDEX_DIR || id === TERMINAL_RUN_INDEX_DIR || path.basename(id) !== id) return { kind: "reject" };
|
|
167
174
|
const asyncDir = path.join(asyncDirRoot, id);
|
|
168
175
|
let entryStat: fs.Stats;
|
|
169
176
|
try {
|
|
170
177
|
entryStat = fs.lstatSync(asyncDir);
|
|
171
178
|
} catch (error) {
|
|
172
|
-
if (isNotFoundError(error)) return { kind: "
|
|
179
|
+
if (isNotFoundError(error)) return canScanAsyncRunPrefix(id) ? { kind: "prefix" } : { kind: "reject" };
|
|
173
180
|
throw new Error(`Failed to inspect async run path '${asyncDir}': ${getErrorMessage(error)}`, {
|
|
174
181
|
cause: error instanceof Error ? error : undefined,
|
|
175
182
|
});
|
|
@@ -187,7 +194,7 @@ export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessio
|
|
|
187
194
|
}
|
|
188
195
|
if (sessionId !== undefined) {
|
|
189
196
|
const status = readStatus(asyncDir);
|
|
190
|
-
if (status?.sessionId !== sessionId) return { kind: "
|
|
197
|
+
if (status?.sessionId !== sessionId) return canScanAsyncRunPrefix(id) ? { kind: "prefix" } : { kind: "reject" };
|
|
191
198
|
}
|
|
192
199
|
return { kind: "exact", id };
|
|
193
200
|
}
|
|
@@ -251,8 +258,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
251
258
|
...(step.phase ? { phase: step.phase } : {}),
|
|
252
259
|
...(step.outputName ? { outputName: step.outputName } : {}),
|
|
253
260
|
...(step.structured ? { structured: step.structured } : {}),
|
|
254
|
-
...(step.checkpoint ? { checkpoint: step.checkpoint } : {}),
|
|
255
261
|
status: step.status,
|
|
262
|
+
...(step.runner ? { runner: step.runner } : {}),
|
|
256
263
|
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
257
264
|
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
258
265
|
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
@@ -312,6 +319,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
312
319
|
mode: status.mode,
|
|
313
320
|
...(summarizeContextModes(summarizedSteps.map((step) => step.context)) ? { context: summarizeContextModes(summarizedSteps.map((step) => step.context)) } : {}),
|
|
314
321
|
cwd: status.cwd,
|
|
322
|
+
...(status.sessionRoot ? { sessionRoot: status.sessionRoot } : {}),
|
|
315
323
|
startedAt: status.startedAt,
|
|
316
324
|
lastUpdate: status.lastUpdate,
|
|
317
325
|
endedAt: status.endedAt,
|
|
@@ -327,7 +335,6 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
327
335
|
...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
|
|
328
336
|
...(parallelGroups.length ? { parallelGroups } : {}),
|
|
329
337
|
steps: summarizedSteps,
|
|
330
|
-
...(status.checkpoint ? { checkpoint: status.checkpoint } : {}),
|
|
331
338
|
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
332
339
|
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
333
340
|
...(processTerminal ? { processTerminal } : {}),
|
|
@@ -376,34 +383,39 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
376
383
|
|
|
377
384
|
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
378
385
|
let entries: string[];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
&& options.states !== undefined
|
|
384
|
-
&& options.states.length > 0
|
|
385
|
-
&& options.states.every(isActiveAsyncState);
|
|
386
|
+
const activeEntries = new Set<string>();
|
|
387
|
+
const wantsActive = options.states === undefined || options.states.some(isActiveAsyncState);
|
|
388
|
+
const wantsTerminal = options.states === undefined || options.states.some((state) => !isActiveAsyncState(state));
|
|
389
|
+
const includeNested = options.includeNested !== false;
|
|
386
390
|
try {
|
|
387
391
|
if (options.runId !== undefined) {
|
|
388
392
|
const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
|
|
389
393
|
entries = resolution.kind === "exact"
|
|
390
394
|
? [resolution.id]
|
|
391
|
-
: resolution.kind === "
|
|
395
|
+
: resolution.kind === "prefix" && options.exactRunId !== true
|
|
392
396
|
? fs.readdirSync(asyncDirRoot).filter((entry) =>
|
|
393
397
|
(entry === options.runId || entry.startsWith(options.runId!))
|
|
394
398
|
&& resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
|
|
395
399
|
)
|
|
396
400
|
: [];
|
|
397
|
-
} else if (
|
|
398
|
-
entries = (
|
|
399
|
-
if (resolveTargetedAsyncRun(asyncDirRoot, entry).kind === "exact") return true;
|
|
400
|
-
updateActiveRunIndex(path.join(asyncDirRoot, entry), "failed");
|
|
401
|
-
return false;
|
|
402
|
-
});
|
|
403
|
-
usedActiveIndex = true;
|
|
401
|
+
} else if (options.repairScan === true) {
|
|
402
|
+
entries = fs.readdirSync(asyncDirRoot).filter((entry) => entry !== ACTIVE_RUN_INDEX_DIR && entry !== TERMINAL_RUN_INDEX_DIR && isAsyncRunDir(asyncDirRoot, entry));
|
|
404
403
|
} else {
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
const indexed = new Set<string>();
|
|
405
|
+
if (wantsActive) {
|
|
406
|
+
for (const entry of readActiveRunIndex(asyncDirRoot) ?? []) {
|
|
407
|
+
if (resolveTargetedAsyncRun(asyncDirRoot, entry).kind === "exact") {
|
|
408
|
+
indexed.add(entry);
|
|
409
|
+
activeEntries.add(entry);
|
|
410
|
+
} else {
|
|
411
|
+
updateActiveRunIndex(path.join(asyncDirRoot, entry), "failed");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (wantsTerminal) {
|
|
416
|
+
for (const entry of readRecentTerminalRunIndex(asyncDirRoot, { sessionId: options.sessionId, ...(options.entryLimit !== undefined ? { limit: options.entryLimit } : {}) })) indexed.add(entry);
|
|
417
|
+
}
|
|
418
|
+
entries = [...indexed];
|
|
407
419
|
}
|
|
408
420
|
} catch (error) {
|
|
409
421
|
if (isNotFoundError(error)) return [];
|
|
@@ -412,28 +424,6 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
412
424
|
});
|
|
413
425
|
}
|
|
414
426
|
|
|
415
|
-
if (options.entryLimit !== undefined) {
|
|
416
|
-
scannedCompleteRoot = false;
|
|
417
|
-
const limit = Math.max(0, Math.floor(options.entryLimit));
|
|
418
|
-
entries = entries
|
|
419
|
-
.map((entry) => {
|
|
420
|
-
try {
|
|
421
|
-
return { entry, mtimeMs: fs.statSync(path.join(asyncDirRoot, entry, "status.json")).mtimeMs };
|
|
422
|
-
} catch (error) {
|
|
423
|
-
if (isNotFoundError(error)) return undefined;
|
|
424
|
-
throw new Error(`Failed to inspect async status file for '${entry}': ${getErrorMessage(error)}`, {
|
|
425
|
-
cause: error instanceof Error ? error : undefined,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
})
|
|
429
|
-
.filter((candidate): candidate is { entry: string; mtimeMs: number } => candidate !== undefined)
|
|
430
|
-
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
431
|
-
.slice(0, limit)
|
|
432
|
-
.map((candidate) => candidate.entry);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (scannedCompleteRoot) pruneStatusCacheForAsyncRoot(asyncDirRoot, entries);
|
|
436
|
-
|
|
437
427
|
const allowedStates = options.states ? new Set(options.states) : undefined;
|
|
438
428
|
const runs: AsyncRunSummary[] = [];
|
|
439
429
|
// Route resolution for every run shares a single index built from the
|
|
@@ -443,7 +433,8 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
443
433
|
// entirely when no active runs match.
|
|
444
434
|
let nestedRouteIndex: Map<string, NestedRoute> | undefined;
|
|
445
435
|
const resolveNestedRoute = (rootRunId: string): NestedRoute | undefined => {
|
|
446
|
-
if (
|
|
436
|
+
if (!includeNested) return undefined;
|
|
437
|
+
if (activeEntries.has(rootRunId)) return findNestedRouteForRootId(rootRunId);
|
|
447
438
|
if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
|
|
448
439
|
return nestedRouteIndex.get(rootRunId);
|
|
449
440
|
};
|
|
@@ -454,12 +445,12 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
454
445
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
455
446
|
const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
456
447
|
if (!status) {
|
|
457
|
-
if (
|
|
448
|
+
if (activeEntries.has(entry)) updateActiveRunIndex(asyncDir, "failed");
|
|
458
449
|
continue;
|
|
459
450
|
}
|
|
460
|
-
if (
|
|
451
|
+
if (activeEntries.has(entry) && !isActiveAsyncState(status.state)) {
|
|
461
452
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
|
|
462
|
-
if (processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
453
|
+
if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) releaseActiveRunIndex(asyncDir);
|
|
463
454
|
}
|
|
464
455
|
if (status.displayDismissedAt !== undefined) continue;
|
|
465
456
|
// Filter before the nested-route lookup: the lookup builds an index over
|
|
@@ -470,7 +461,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
470
461
|
if (options.sessionId && status.sessionId !== options.sessionId) continue;
|
|
471
462
|
const nestedWarnings: string[] = [];
|
|
472
463
|
let nestedRoute: NestedRoute | undefined;
|
|
473
|
-
if (options.reconcile !== false) {
|
|
464
|
+
if (options.reconcile !== false && includeNested) {
|
|
474
465
|
try {
|
|
475
466
|
nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
|
|
476
467
|
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
@@ -5,8 +5,8 @@ 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
7
|
import { PROMPT_REDACTED, readStatus } from "../../shared/utils.ts";
|
|
8
|
-
import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep
|
|
9
|
-
import {
|
|
8
|
+
import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep } from "../shared/parallel-utils.ts";
|
|
9
|
+
import { isDynamicRunnerGroup, isParallelGroup } from "../shared/parallel-utils.ts";
|
|
10
10
|
|
|
11
11
|
const APPEND_REQUESTS_DIR = "append-requests";
|
|
12
12
|
|
|
@@ -52,9 +52,7 @@ export function countPendingChainAppendRequests(asyncDir: string): number {
|
|
|
52
52
|
export function runnerStepOutputNames(steps: RunnerStep[]): string[] {
|
|
53
53
|
const names: string[] = [];
|
|
54
54
|
for (const step of steps) {
|
|
55
|
-
if (
|
|
56
|
-
continue;
|
|
57
|
-
} else if (isParallelGroup(step)) {
|
|
55
|
+
if (isParallelGroup(step)) {
|
|
58
56
|
names.push(...step.parallel.map((task) => task.outputName).filter((name): name is string => Boolean(name)));
|
|
59
57
|
} else if (isDynamicRunnerGroup(step)) {
|
|
60
58
|
if (step.collect.as) names.push(step.collect.as);
|
|
@@ -181,20 +179,7 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
|
|
|
181
179
|
};
|
|
182
180
|
}
|
|
183
181
|
|
|
184
|
-
function statusStepForCheckpoint(step: RunnerCheckpointStep, stepIndex: number): StatusStep {
|
|
185
|
-
return {
|
|
186
|
-
agent: `checkpoint:${step.checkpoint}`,
|
|
187
|
-
phase: step.phase,
|
|
188
|
-
label: step.label ?? step.checkpoint,
|
|
189
|
-
status: "pending",
|
|
190
|
-
checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
|
|
191
|
-
recentTools: [],
|
|
192
|
-
recentOutput: [],
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
182
|
function statusStepsForRunnerStep(step: RunnerStep, stepIndex: number): StatusStep[] {
|
|
197
|
-
if (isCheckpointRunnerStep(step)) return [statusStepForCheckpoint(step, stepIndex)];
|
|
198
183
|
if (isParallelGroup(step)) return step.parallel.map(statusStepForTask);
|
|
199
184
|
if (isDynamicRunnerGroup(step)) {
|
|
200
185
|
return [{
|
|
@@ -286,21 +271,6 @@ function graphNodeForDynamic(step: DynamicRunnerGroup, stepIndex: number): Workf
|
|
|
286
271
|
|
|
287
272
|
function appendWorkflowNode(graph: WorkflowGraphSnapshot | undefined, step: RunnerStep, stepIndex: number, flatIndex: number): void {
|
|
288
273
|
if (!graph) return;
|
|
289
|
-
if (isCheckpointRunnerStep(step)) {
|
|
290
|
-
const node: WorkflowGraphNode = {
|
|
291
|
-
id: `step-${stepIndex}`,
|
|
292
|
-
kind: "checkpoint",
|
|
293
|
-
phase: step.phase,
|
|
294
|
-
label: step.label?.trim() || step.checkpoint,
|
|
295
|
-
status: "pending",
|
|
296
|
-
flatIndex,
|
|
297
|
-
stepIndex,
|
|
298
|
-
checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
|
|
299
|
-
};
|
|
300
|
-
graph.nodes.push(node);
|
|
301
|
-
pushPhase(graph, step.phase, node.id);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
274
|
if (isParallelGroup(step)) {
|
|
305
275
|
graph.nodes.push(graphNodeForParallel(step, stepIndex, flatIndex, graph));
|
|
306
276
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import
|
|
2
|
+
import { resultFilePath } from "./result-files.ts";
|
|
3
3
|
import type { AcceptanceLedger, AsyncStatus, CostSummary, ModelAttempt } from "../../shared/types.ts";
|
|
4
4
|
import { readStatus } from "../../shared/utils.ts";
|
|
5
5
|
|
|
@@ -195,5 +195,5 @@ export async function waitForImportedAsyncRoot(
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
export function resolveAsyncRootResultPath(resultsDir: string, runId: string): string {
|
|
198
|
-
return
|
|
198
|
+
return resultFilePath(resultsDir, runId);
|
|
199
199
|
}
|
|
@@ -9,6 +9,8 @@ const ARCHIVE_VERSION = 1;
|
|
|
9
9
|
const ARCHIVE_TEXT_LIMIT_BYTES = 64 * 1024;
|
|
10
10
|
const REPLAY_DIR_NAME = "completion-replay";
|
|
11
11
|
const ARCHIVE_DIR_NAME = "output-archives";
|
|
12
|
+
const CLEANUP_INTERVAL_MS = 60_000;
|
|
13
|
+
const lastCleanupByResultsDir = new Map<string, number>();
|
|
12
14
|
|
|
13
15
|
export interface CompletionArchiveEntry {
|
|
14
16
|
agent?: string;
|
|
@@ -194,7 +196,7 @@ export function writeCompletionReplay(input: {
|
|
|
194
196
|
archivePath,
|
|
195
197
|
};
|
|
196
198
|
writePrivateAtomicJson(completionReplayPath(input.resultsDir, input.runId), record);
|
|
197
|
-
|
|
199
|
+
cleanupCompletionReplayIfDue(input.resultsDir, input.now, input.ttlMs);
|
|
198
200
|
return record;
|
|
199
201
|
}
|
|
200
202
|
|
|
@@ -233,6 +235,14 @@ export function readCompletionArchive(archivePath: string): CompletionArchive |
|
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
237
|
|
|
238
|
+
export function cleanupCompletionReplayIfDue(resultsDir: string, now: number, maxAgeMs: number, intervalMs = CLEANUP_INTERVAL_MS): boolean {
|
|
239
|
+
const last = lastCleanupByResultsDir.get(resultsDir);
|
|
240
|
+
if (last !== undefined && now - last < intervalMs) return false;
|
|
241
|
+
lastCleanupByResultsDir.set(resultsDir, now);
|
|
242
|
+
cleanupCompletionReplay(resultsDir, now, maxAgeMs);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
236
246
|
/** Opportunistically remove expired replay and orphan archive files without affecting delivery. */
|
|
237
247
|
export function cleanupCompletionReplay(resultsDir: string, now: number, maxAgeMs: number): void {
|
|
238
248
|
const replayDir = path.join(resultsDir, REPLAY_DIR_NAME);
|