pi-subagents 0.49.0 → 0.50.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 +35 -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/docs/agents.md +2 -0
- package/docs/configuration.md +47 -3
- package/docs/extension-api.md +36 -0
- package/docs/missions.md +1 -1
- package/docs/observability.md +16 -0
- package/docs/tool-reference.md +19 -2
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +10 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +12 -6
- package/src/extension/config.ts +33 -0
- package/src/extension/index.ts +48 -5
- package/src/extension/public-execution.ts +3 -1
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +4 -2
- package/src/inspectors/herdr/actions.ts +3 -7
- package/src/inspectors/herdr/project-panes.ts +2 -6
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +19 -42
- package/src/missions/goal-driver.ts +3 -1
- package/src/runs/background/active-run-index.ts +71 -1
- package/src/runs/background/async-execution.ts +49 -7
- package/src/runs/background/async-job-tracker.ts +1 -0
- package/src/runs/background/async-resume.ts +3 -5
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/result-files.ts +437 -0
- package/src/runs/background/result-watcher.ts +188 -41
- package/src/runs/background/retained-children.ts +69 -20
- package/src/runs/background/run-id-resolver.ts +30 -24
- package/src/runs/background/run-status.ts +7 -2
- package/src/runs/background/scheduled-runs.ts +54 -28
- package/src/runs/background/stale-run-reconciler.ts +27 -13
- package/src/runs/background/subagent-runner.ts +294 -32
- package/src/runs/background/subagent-wait.ts +2 -0
- package/src/runs/background/wait-completions.ts +5 -2
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/chain-execution.ts +16 -0
- package/src/runs/foreground/execution.ts +219 -15
- package/src/runs/foreground/subagent-executor.ts +114 -28
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/model-fallback.ts +16 -2
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +375 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/subagent-control.ts +15 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/shared/types.ts +29 -2
- package/src/slash/slash-commands.ts +34 -25
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +125 -15
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +6 -4
- package/src/workflows/scripted-workflow.ts +227 -70
|
@@ -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
|
+
}
|
|
@@ -12,7 +12,7 @@ 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
16
|
|
|
17
17
|
interface AsyncRunStepSummary {
|
|
18
18
|
index: number;
|
|
@@ -25,6 +25,7 @@ interface AsyncRunStepSummary {
|
|
|
25
25
|
structured?: boolean;
|
|
26
26
|
checkpoint?: ChainCheckpointState;
|
|
27
27
|
status: AsyncJobStep["status"];
|
|
28
|
+
runner?: AsyncJobStep["runner"];
|
|
28
29
|
activityState?: ActivityState;
|
|
29
30
|
lastActivityAt?: number;
|
|
30
31
|
currentTool?: string;
|
|
@@ -128,6 +129,7 @@ interface AsyncRunListOptions {
|
|
|
128
129
|
now?: () => number;
|
|
129
130
|
reconcile?: boolean;
|
|
130
131
|
runId?: string;
|
|
132
|
+
includeNested?: boolean;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
function getErrorMessage(error: unknown): string {
|
|
@@ -253,6 +255,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
253
255
|
...(step.structured ? { structured: step.structured } : {}),
|
|
254
256
|
...(step.checkpoint ? { checkpoint: step.checkpoint } : {}),
|
|
255
257
|
status: step.status,
|
|
258
|
+
...(step.runner ? { runner: step.runner } : {}),
|
|
256
259
|
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
257
260
|
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
258
261
|
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
@@ -383,6 +386,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
383
386
|
&& options.states !== undefined
|
|
384
387
|
&& options.states.length > 0
|
|
385
388
|
&& options.states.every(isActiveAsyncState);
|
|
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);
|
|
@@ -443,6 +447,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
443
447
|
// entirely when no active runs match.
|
|
444
448
|
let nestedRouteIndex: Map<string, NestedRoute> | undefined;
|
|
445
449
|
const resolveNestedRoute = (rootRunId: string): NestedRoute | undefined => {
|
|
450
|
+
if (!includeNested) return undefined;
|
|
446
451
|
if (usedActiveIndex) return findNestedRouteForRootId(rootRunId);
|
|
447
452
|
if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
|
|
448
453
|
return nestedRouteIndex.get(rootRunId);
|
|
@@ -459,7 +464,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
459
464
|
}
|
|
460
465
|
if (usedActiveIndex && !isActiveAsyncState(status.state)) {
|
|
461
466
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
|
|
462
|
-
if (processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
467
|
+
if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) releaseActiveRunIndex(asyncDir);
|
|
463
468
|
}
|
|
464
469
|
if (status.displayDismissedAt !== undefined) continue;
|
|
465
470
|
// Filter before the nested-route lookup: the lookup builds an index over
|
|
@@ -470,7 +475,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
470
475
|
if (options.sessionId && status.sessionId !== options.sessionId) continue;
|
|
471
476
|
const nestedWarnings: string[] = [];
|
|
472
477
|
let nestedRoute: NestedRoute | undefined;
|
|
473
|
-
if (options.reconcile !== false) {
|
|
478
|
+
if (options.reconcile !== false && includeNested) {
|
|
474
479
|
try {
|
|
475
480
|
nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
|
|
476
481
|
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
@@ -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);
|
|
@@ -443,6 +443,8 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
|
|
|
443
443
|
: uniqueStrings([runOutputPath]);
|
|
444
444
|
const sessionFile = selected.index !== undefined ? selected.step?.sessionFile : status.sessionFile;
|
|
445
445
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
446
|
+
// Synthesized output paths can name files that were never written. Keep the unfiltered paths
|
|
447
|
+
// below so a present-but-unreadable file still reports its read error.
|
|
446
448
|
|
|
447
449
|
const context = contextModeLabel(status.context ?? summarizeContextModes((status.steps ?? []).map((step) => step.context)));
|
|
448
450
|
const lines = [
|
|
@@ -452,7 +454,7 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
|
|
|
452
454
|
stepStateLine(status.mode, selected.index, selected.step),
|
|
453
455
|
selected.hint,
|
|
454
456
|
].filter((line): line is string => Boolean(line));
|
|
455
|
-
appendKnownArtifacts(lines, { outputPaths, sessionFile, eventsPath: fs.existsSync(eventsPath) ? eventsPath : undefined, logPath: fs.existsSync(logPath) ? logPath : undefined });
|
|
457
|
+
appendKnownArtifacts(lines, { outputPaths: outputPaths.filter((outputPath) => fs.existsSync(outputPath)), sessionFile, eventsPath: fs.existsSync(eventsPath) ? eventsPath : undefined, logPath: fs.existsSync(logPath) ? logPath : undefined });
|
|
456
458
|
|
|
457
459
|
const warnings: string[] = [];
|
|
458
460
|
let transcriptLines: string[] = [];
|