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,505 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import { MISSION_BINDING_FILE } from "../../missions/lifecycle.ts";
|
|
5
|
+
import { encodeIndexSegment, indexSegmentAliases, MAX_INDEX_SEGMENT_BYTES } from "./index-segment.ts";
|
|
6
|
+
|
|
7
|
+
const RESULT_INDEX_VERSION = 1;
|
|
8
|
+
const RESULT_INDEX_DIR = "result-index";
|
|
9
|
+
const SESSION_INDEX_DIR = "sessions";
|
|
10
|
+
const RUN_INDEX_DIR = "runs";
|
|
11
|
+
const OBSERVER_INDEX_DIR = "observers";
|
|
12
|
+
const TOOL_CALL_INDEX_DIR = "tool-calls";
|
|
13
|
+
const RESULT_PENDING_DIR = "result-pending";
|
|
14
|
+
const MISSION_OBSERVER = "mission";
|
|
15
|
+
const JSON_EXTENSION = ".json";
|
|
16
|
+
const MAX_JSON_FILE_STEM_BYTES = MAX_INDEX_SEGMENT_BYTES - Buffer.byteLength(JSON_EXTENSION, "utf-8");
|
|
17
|
+
|
|
18
|
+
export interface ResultIndexEntry {
|
|
19
|
+
version: 1;
|
|
20
|
+
runId: string;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
file: string;
|
|
23
|
+
writtenAt: number;
|
|
24
|
+
asyncDir?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function encodeSegment(value: string): string {
|
|
28
|
+
return encodeIndexSegment(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function encodedJsonFileName(value: string): string {
|
|
32
|
+
return `${encodeIndexSegment(value, MAX_JSON_FILE_STEM_BYTES)}${JSON_EXTENSION}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function encodedJsonFileNames(value: string): string[] {
|
|
36
|
+
return indexSegmentAliases(value, MAX_JSON_FILE_STEM_BYTES).map((segment) => `${segment}${JSON_EXTENSION}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
40
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resultFileName(runId: string): string {
|
|
44
|
+
return `${runId}${JSON_EXTENSION}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resultFilePath(resultsDir: string, runId: string): string {
|
|
48
|
+
return path.join(resultsDir, resultFileName(runId));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sessionIndexDir(resultsDir: string, sessionId: string): string {
|
|
52
|
+
return path.join(resultsDir, RESULT_INDEX_DIR, SESSION_INDEX_DIR, encodeSegment(sessionId));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sessionIndexDirs(resultsDir: string, sessionId: string): string[] {
|
|
56
|
+
return indexSegmentAliases(sessionId).map((segment) => path.join(resultsDir, RESULT_INDEX_DIR, SESSION_INDEX_DIR, segment));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resultIndexPath(resultsDir: string, sessionId: string, runId: string): string {
|
|
60
|
+
return path.join(sessionIndexDir(resultsDir, sessionId), encodedJsonFileName(runId));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resultIndexPaths(resultsDir: string, sessionId: string, runId: string): string[] {
|
|
64
|
+
return sessionIndexDirs(resultsDir, sessionId).flatMap((dir) => encodedJsonFileNames(runId).map((fileName) => path.join(dir, fileName)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runIndexPath(resultsDir: string, runId: string): string {
|
|
68
|
+
return path.join(resultsDir, RESULT_INDEX_DIR, RUN_INDEX_DIR, encodedJsonFileName(runId));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resultPendingPath(resultsDir: string, sessionId: string, runId: string): string {
|
|
72
|
+
return path.join(resultsDir, RESULT_PENDING_DIR, encodeSegment(sessionId), encodedJsonFileName(runId));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resultPendingPaths(resultsDir: string, sessionId: string, runId: string): string[] {
|
|
76
|
+
return indexSegmentAliases(sessionId).flatMap((segment) => encodedJsonFileNames(runId).map((fileName) => path.join(resultsDir, RESULT_PENDING_DIR, segment, fileName)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pendingSessionDirs(resultsDir: string, sessionId: string): string[] {
|
|
80
|
+
return indexSegmentAliases(sessionId).map((segment) => path.join(resultsDir, RESULT_PENDING_DIR, segment));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function firstExistingResultFile(paths: string[]): string | undefined {
|
|
84
|
+
for (const filePath of paths) {
|
|
85
|
+
if (existingResultFile(filePath)) return filePath;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function observerIndexDir(resultsDir: string, observer: string): string {
|
|
91
|
+
return path.join(resultsDir, RESULT_INDEX_DIR, OBSERVER_INDEX_DIR, observer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function observerIndexPath(resultsDir: string, observer: string, runId: string): string {
|
|
95
|
+
return path.join(observerIndexDir(resultsDir, observer), encodedJsonFileName(runId));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toolCallIndexDir(resultsDir: string, toolCallId: string): string {
|
|
99
|
+
return path.join(resultsDir, RESULT_INDEX_DIR, TOOL_CALL_INDEX_DIR, encodeIndexSegment(toolCallId));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toolCallIndexPath(resultsDir: string, toolCallId: string, runId: string): string {
|
|
103
|
+
return path.join(toolCallIndexDir(resultsDir, toolCallId), encodedJsonFileName(runId));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseResultIndexEntry(value: unknown): ResultIndexEntry | undefined {
|
|
107
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
108
|
+
const record = value as Partial<ResultIndexEntry>;
|
|
109
|
+
if (record.version !== RESULT_INDEX_VERSION
|
|
110
|
+
|| typeof record.runId !== "string"
|
|
111
|
+
|| typeof record.sessionId !== "string"
|
|
112
|
+
|| typeof record.file !== "string"
|
|
113
|
+
|| typeof record.writtenAt !== "number") return undefined;
|
|
114
|
+
return {
|
|
115
|
+
version: RESULT_INDEX_VERSION,
|
|
116
|
+
runId: record.runId,
|
|
117
|
+
sessionId: record.sessionId,
|
|
118
|
+
file: record.file,
|
|
119
|
+
writtenAt: record.writtenAt,
|
|
120
|
+
...(typeof record.asyncDir === "string" ? { asyncDir: record.asyncDir } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function writeResultIndexForData(resultPath: string, data: Record<string, unknown>): void {
|
|
125
|
+
const runId = nonEmptyString(data.runId) ?? nonEmptyString(data.id) ?? path.basename(resultPath, ".json");
|
|
126
|
+
const sessionId = nonEmptyString(data.sessionId);
|
|
127
|
+
if (!runId || !sessionId) return;
|
|
128
|
+
const file = path.basename(resultPath);
|
|
129
|
+
const entry: ResultIndexEntry = {
|
|
130
|
+
version: RESULT_INDEX_VERSION,
|
|
131
|
+
runId,
|
|
132
|
+
sessionId,
|
|
133
|
+
file,
|
|
134
|
+
writtenAt: Date.now(),
|
|
135
|
+
...(nonEmptyString(data.asyncDir) ? { asyncDir: nonEmptyString(data.asyncDir)! } : {}),
|
|
136
|
+
};
|
|
137
|
+
const resultsDir = path.dirname(resultPath);
|
|
138
|
+
writeAtomicJson(resultIndexPath(resultsDir, sessionId, runId), entry);
|
|
139
|
+
try {
|
|
140
|
+
writeAtomicJson(runIndexPath(resultsDir, runId), entry);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`Failed to write async result run index for '${resultPath}':`, error);
|
|
143
|
+
}
|
|
144
|
+
const toolCallId = nonEmptyString(data.toolCallId);
|
|
145
|
+
try {
|
|
146
|
+
if (toolCallId) writeAtomicJson(toolCallIndexPath(resultsDir, toolCallId, runId), entry);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`Failed to write async result tool-call index for '${resultPath}':`, error);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
if (entry.asyncDir && fs.existsSync(path.join(entry.asyncDir, MISSION_BINDING_FILE))) {
|
|
152
|
+
writeAtomicJson(observerIndexPath(resultsDir, MISSION_OBSERVER, runId), entry);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`Failed to write async result observer index for '${resultPath}':`, error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeIndexedPendingResultFile(resultPath: string, data: Record<string, unknown>): { runId: string; sessionId: string; resultsDir: string } {
|
|
160
|
+
const runId = nonEmptyString(data.runId) ?? nonEmptyString(data.id) ?? path.basename(resultPath, ".json");
|
|
161
|
+
const sessionId = nonEmptyString(data.sessionId);
|
|
162
|
+
if (!sessionId) throw new Error(`Cannot write async result '${resultPath}' without a sessionId.`);
|
|
163
|
+
const resultsDir = path.dirname(resultPath);
|
|
164
|
+
writeAtomicJson(resultPendingPath(resultsDir, sessionId, runId), data);
|
|
165
|
+
writeResultIndexForData(resultPath, data);
|
|
166
|
+
return { runId, sessionId, resultsDir };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function writePendingAsyncResultFile(resultPath: string, data: Record<string, unknown>): void {
|
|
170
|
+
writeIndexedPendingResultFile(resultPath, data);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function writeAsyncResultFile(resultPath: string, data: Record<string, unknown>): { state: "public" | "pending" } {
|
|
174
|
+
const { runId, sessionId, resultsDir } = writeIndexedPendingResultFile(resultPath, data);
|
|
175
|
+
return promotePendingResultFile(resultsDir, sessionId, runId, path.basename(resultPath), { logFailure: false }) === "promoted"
|
|
176
|
+
? { state: "public" }
|
|
177
|
+
: { state: "pending" };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function removeResultIndex(resultsDir: string, sessionId: string | undefined, runId: string | undefined, toolCallId?: string): void {
|
|
181
|
+
if (!runId) return;
|
|
182
|
+
if (sessionId) {
|
|
183
|
+
for (const indexPath of resultIndexPaths(resultsDir, sessionId, runId)) {
|
|
184
|
+
try {
|
|
185
|
+
fs.rmSync(indexPath, { force: true });
|
|
186
|
+
} catch {
|
|
187
|
+
// Index cleanup must not affect result delivery.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const pendingPath of resultPendingPaths(resultsDir, sessionId, runId)) {
|
|
191
|
+
try {
|
|
192
|
+
fs.rmSync(pendingPath, { force: true });
|
|
193
|
+
} catch {
|
|
194
|
+
// Pending cleanup must not affect result delivery.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
fs.rmSync(runIndexPath(resultsDir, runId), { force: true });
|
|
200
|
+
} catch {
|
|
201
|
+
// Index cleanup must not affect result delivery.
|
|
202
|
+
}
|
|
203
|
+
if (toolCallId) {
|
|
204
|
+
try {
|
|
205
|
+
fs.rmSync(toolCallIndexPath(resultsDir, toolCallId, runId), { force: true });
|
|
206
|
+
} catch {
|
|
207
|
+
// Index cleanup must not affect result delivery.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
fs.rmSync(observerIndexPath(resultsDir, MISSION_OBSERVER, runId), { force: true });
|
|
212
|
+
} catch {
|
|
213
|
+
// Index cleanup must not affect result delivery.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function removeMissionObserverIndex(resultsDir: string, runId: string | undefined): void {
|
|
218
|
+
if (!runId) return;
|
|
219
|
+
try {
|
|
220
|
+
fs.rmSync(observerIndexPath(resultsDir, MISSION_OBSERVER, runId), { force: true });
|
|
221
|
+
} catch {
|
|
222
|
+
// Observer index cleanup must not affect result delivery.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function existingResultFile(resultPath: string): boolean {
|
|
227
|
+
try {
|
|
228
|
+
return fs.statSync(resultPath).isFile();
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to inspect async result payload '${resultPath}':`, error);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function pendingResultExists(resultsDir: string, sessionId: string, runId: string): boolean {
|
|
236
|
+
return firstExistingResultFile(resultPendingPaths(resultsDir, sessionId, runId)) !== undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function pendingResultPayloadMatches(filePath: string, sessionId: string, runId: string): boolean {
|
|
240
|
+
try {
|
|
241
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record<string, unknown>;
|
|
242
|
+
return (nonEmptyString(data.runId) ?? nonEmptyString(data.id)) === runId && nonEmptyString(data.sessionId) === sessionId;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid pending async result '${filePath}':`, error);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function assertPendingResultPayloadMatches(filePath: string, sessionId: string, runId: string): boolean {
|
|
250
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record<string, unknown>;
|
|
251
|
+
return (nonEmptyString(data.runId) ?? nonEmptyString(data.id)) === runId && nonEmptyString(data.sessionId) === sessionId;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function promotePendingResultFile(resultsDir: string, sessionId: string, runId: string, file = resultFileName(runId), options: { logFailure?: boolean } = {}): "none" | "promoted" | "pending" {
|
|
255
|
+
if (file !== path.basename(file) || !file.endsWith(".json")) return "none";
|
|
256
|
+
const pendingPath = firstExistingResultFile(resultPendingPaths(resultsDir, sessionId, runId));
|
|
257
|
+
if (!pendingPath) return "none";
|
|
258
|
+
const resultPath = path.join(resultsDir, file);
|
|
259
|
+
try {
|
|
260
|
+
// POSIX rename replaces the destination atomically. Deleting it first lets a
|
|
261
|
+
// losing promoter unlink the result that another promoter just published.
|
|
262
|
+
fs.renameSync(pendingPath, resultPath);
|
|
263
|
+
return existingResultFile(resultPath) ? "promoted" : "pending";
|
|
264
|
+
} catch (error) {
|
|
265
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
266
|
+
if (code === "ENOENT" || code === "EEXIST" || code === "EPERM" || code === "EACCES") {
|
|
267
|
+
const pendingExists = existingResultFile(pendingPath);
|
|
268
|
+
const resultExists = existingResultFile(resultPath);
|
|
269
|
+
if (pendingExists) {
|
|
270
|
+
if (!resultExists && options.logFailure !== false) console.error(`Failed to promote pending async result '${pendingPath}' to '${resultPath}':`, error);
|
|
271
|
+
return "pending";
|
|
272
|
+
}
|
|
273
|
+
if (resultExists) return "promoted";
|
|
274
|
+
if (options.logFailure !== false) console.error(`Pending async result '${pendingPath}' disappeared without a promoted result at '${resultPath}'.`);
|
|
275
|
+
return "none";
|
|
276
|
+
}
|
|
277
|
+
if (options.logFailure !== false) console.error(`Failed to promote pending async result '${pendingPath}' to '${resultPath}':`, error);
|
|
278
|
+
return "pending";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface ResultPayloadLocation {
|
|
283
|
+
file: string;
|
|
284
|
+
path: string;
|
|
285
|
+
state: "public" | "pending";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function pendingResultLocationForSessionRun(resultsDir: string, sessionId: string, runId: string, file = resultFileName(runId)): ResultPayloadLocation | undefined {
|
|
289
|
+
if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
|
|
290
|
+
const pendingPath = firstExistingResultFile(resultPendingPaths(resultsDir, sessionId, runId));
|
|
291
|
+
if (!pendingPath || !pendingResultPayloadMatches(pendingPath, sessionId, runId)) return undefined;
|
|
292
|
+
return { file, path: pendingPath, state: "pending" };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function fallbackResultPayloadPathForSessionRun(resultsDir: string, sessionId: string, runId: string): string | undefined {
|
|
296
|
+
const pendingPath = firstExistingResultFile(resultPendingPaths(resultsDir, sessionId, runId));
|
|
297
|
+
if (!pendingPath || !assertPendingResultPayloadMatches(pendingPath, sessionId, runId)) return undefined;
|
|
298
|
+
return pendingPath;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function resultPayloadLocationFromIndex(resultsDir: string, entry: ResultIndexEntry): ResultPayloadLocation | undefined {
|
|
302
|
+
if (entry.file !== path.basename(entry.file) || !entry.file.endsWith(".json")) return undefined;
|
|
303
|
+
const pendingState = promotePendingResultFile(resultsDir, entry.sessionId, entry.runId, entry.file);
|
|
304
|
+
if (pendingState === "pending") {
|
|
305
|
+
return pendingResultLocationForSessionRun(resultsDir, entry.sessionId, entry.runId, entry.file);
|
|
306
|
+
}
|
|
307
|
+
const resultPath = path.join(resultsDir, entry.file);
|
|
308
|
+
if (pendingState === "promoted" || existingResultFile(resultPath)) return { file: entry.file, path: resultPath, state: "public" };
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function readResultIndexForSessionRun(resultsDir: string, sessionId: string, runId: string): ResultIndexEntry | undefined {
|
|
313
|
+
for (const indexPath of resultIndexPaths(resultsDir, sessionId, runId)) {
|
|
314
|
+
try {
|
|
315
|
+
const entry = parseResultIndexEntry(JSON.parse(fs.readFileSync(indexPath, "utf-8")));
|
|
316
|
+
if (!entry || entry.sessionId !== sessionId || entry.runId !== runId) continue;
|
|
317
|
+
return entry;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
320
|
+
if (code === "EPERM" || code === "EACCES") throw error;
|
|
321
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
322
|
+
console.error(`Ignoring invalid async result index for '${runId}':`, error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function resultPayloadPathForSessionRun(resultsDir: string, sessionId: string, runId: string): string | undefined {
|
|
330
|
+
const entry = readResultIndexForSessionRun(resultsDir, sessionId, runId);
|
|
331
|
+
return (entry ? resultPayloadLocationFromIndex(resultsDir, entry) : undefined)?.path
|
|
332
|
+
?? pendingResultLocationForSessionRun(resultsDir, sessionId, runId)?.path;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function resultPayloadPathForMissionObserverRun(resultsDir: string, runId: string): string | undefined {
|
|
336
|
+
try {
|
|
337
|
+
const entry = parseResultIndexEntry(JSON.parse(fs.readFileSync(observerIndexPath(resultsDir, MISSION_OBSERVER, runId), "utf-8")));
|
|
338
|
+
if (!entry || entry.runId !== runId) return undefined;
|
|
339
|
+
return resultPayloadLocationFromIndex(resultsDir, entry)?.path;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function resultPayloadPathForIndexedRun(resultsDir: string, runId: string): string | undefined {
|
|
347
|
+
const entryPath = runIndexPath(resultsDir, runId);
|
|
348
|
+
try {
|
|
349
|
+
const entry = parseResultIndexEntry(JSON.parse(fs.readFileSync(entryPath, "utf-8")));
|
|
350
|
+
if (!entry || entry.runId !== runId) {
|
|
351
|
+
fs.rmSync(entryPath, { force: true });
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
const location = resultPayloadLocationFromIndex(resultsDir, entry);
|
|
355
|
+
if (location) return location.path;
|
|
356
|
+
fs.rmSync(entryPath, { force: true });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
359
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
360
|
+
console.error(`Ignoring invalid async result run index '${entryPath}':`, error);
|
|
361
|
+
try { fs.rmSync(entryPath, { force: true }); } catch {}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function indexedResultFile(resultsDir: string, entry: ResultIndexEntry, includePending = false): string | undefined {
|
|
368
|
+
const location = resultPayloadLocationFromIndex(resultsDir, entry);
|
|
369
|
+
if (!location) return undefined;
|
|
370
|
+
return includePending || location.state === "public" ? location.file : undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function listIndexFiles(dir: string): string[] {
|
|
374
|
+
let files: string[];
|
|
375
|
+
try {
|
|
376
|
+
files = fs.readdirSync(dir, { withFileTypes: true })
|
|
377
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
378
|
+
.map((entry) => path.join(dir, entry.name));
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
381
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") return [];
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
return files;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function resultFilesFromIndexDir(resultsDir: string, dir: string, includePending = false): string[] {
|
|
388
|
+
const candidates = new Set<string>();
|
|
389
|
+
for (const entryPath of listIndexFiles(dir)) {
|
|
390
|
+
try {
|
|
391
|
+
const entry = parseResultIndexEntry(JSON.parse(fs.readFileSync(entryPath, "utf-8")));
|
|
392
|
+
if (!entry) {
|
|
393
|
+
fs.rmSync(entryPath, { force: true });
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const resultFile = indexedResultFile(resultsDir, entry, includePending);
|
|
397
|
+
if (resultFile) candidates.add(resultFile);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result index '${entryPath}':`, error);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return [...candidates];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function resultFilesForSession(resultsDir: string, sessionId: string): string[] {
|
|
406
|
+
const files = new Set<string>();
|
|
407
|
+
for (const dir of sessionIndexDirs(resultsDir, sessionId)) {
|
|
408
|
+
for (const file of resultFilesFromIndexDir(resultsDir, dir)) files.add(file);
|
|
409
|
+
}
|
|
410
|
+
return [...files];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function pendingResultFilesForSession(resultsDir: string, sessionId: string): string[] {
|
|
414
|
+
const files = new Set<string>();
|
|
415
|
+
for (const dir of pendingSessionDirs(resultsDir, sessionId)) {
|
|
416
|
+
let entries: fs.Dirent[];
|
|
417
|
+
try {
|
|
418
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
419
|
+
} catch (error) {
|
|
420
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
421
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") continue;
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
426
|
+
const pendingPath = path.join(dir, entry.name);
|
|
427
|
+
try {
|
|
428
|
+
const data = JSON.parse(fs.readFileSync(pendingPath, "utf-8")) as Record<string, unknown>;
|
|
429
|
+
const runId = nonEmptyString(data.runId) ?? nonEmptyString(data.id);
|
|
430
|
+
if (runId && nonEmptyString(data.sessionId) === sessionId) files.add(resultFileName(runId));
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid pending async result '${pendingPath}':`, error);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return [...files];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function resultCandidateFilesForSession(resultsDir: string, sessionId: string): string[] {
|
|
440
|
+
const files = new Set<string>();
|
|
441
|
+
for (const dir of sessionIndexDirs(resultsDir, sessionId)) {
|
|
442
|
+
for (const file of resultFilesFromIndexDir(resultsDir, dir, true)) files.add(file);
|
|
443
|
+
}
|
|
444
|
+
for (const file of pendingResultFilesForSession(resultsDir, sessionId)) files.add(file);
|
|
445
|
+
return [...files];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function resultFilesForToolCall(resultsDir: string, toolCallId: string): string[] {
|
|
449
|
+
return resultFilesFromIndexDir(resultsDir, toolCallIndexDir(resultsDir, toolCallId));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function resultCandidateFilesForToolCall(resultsDir: string, toolCallId: string): string[] {
|
|
453
|
+
return resultFilesFromIndexDir(resultsDir, toolCallIndexDir(resultsDir, toolCallId), true);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function missionObserverResultFiles(resultsDir: string): string[] {
|
|
457
|
+
return resultFilesFromIndexDir(resultsDir, observerIndexDir(resultsDir, MISSION_OBSERVER));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function missionObserverResultCandidateFiles(resultsDir: string): string[] {
|
|
461
|
+
return resultFilesFromIndexDir(resultsDir, observerIndexDir(resultsDir, MISSION_OBSERVER), true);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function cleanupResultIndexes(resultsDir: string, now = Date.now(), maxAgeMs = 24 * 60 * 60 * 1000): number {
|
|
465
|
+
const root = path.join(resultsDir, RESULT_INDEX_DIR);
|
|
466
|
+
const cutoff = now - maxAgeMs;
|
|
467
|
+
let removed = 0;
|
|
468
|
+
const visit = (dir: string): void => {
|
|
469
|
+
let entries: fs.Dirent[];
|
|
470
|
+
try {
|
|
471
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
472
|
+
} catch (error) {
|
|
473
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
474
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") return;
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
const fullPath = path.join(dir, entry.name);
|
|
479
|
+
if (entry.isDirectory()) {
|
|
480
|
+
visit(fullPath);
|
|
481
|
+
try { fs.rmdirSync(fullPath); } catch {}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
485
|
+
try {
|
|
486
|
+
const stat = fs.statSync(fullPath);
|
|
487
|
+
const index = parseResultIndexEntry(JSON.parse(fs.readFileSync(fullPath, "utf-8")));
|
|
488
|
+
const resultFile = index ? indexedResultFile(resultsDir, index) : undefined;
|
|
489
|
+
const pendingFile = index ? pendingResultExists(resultsDir, index.sessionId, index.runId) : false;
|
|
490
|
+
if (!index || (!resultFile && !pendingFile && stat.mtimeMs <= cutoff)) {
|
|
491
|
+
fs.rmSync(fullPath, { force: true });
|
|
492
|
+
removed += 1;
|
|
493
|
+
}
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result index '${fullPath}':`, error);
|
|
496
|
+
try {
|
|
497
|
+
fs.rmSync(fullPath, { force: true });
|
|
498
|
+
removed += 1;
|
|
499
|
+
} catch {}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
visit(root);
|
|
504
|
+
return removed;
|
|
505
|
+
}
|