pi-subagents 0.50.0 → 0.52.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 +103 -0
- package/agents/oracle.md +3 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +2 -2
- package/agents/worker.md +2 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +40 -2
- package/docs/configuration.md +30 -12
- package/docs/extension-api.md +42 -1
- package/docs/models.md +2 -0
- package/docs/observability.md +41 -5
- package/docs/tool-reference.md +38 -39
- package/docs/workflows.md +171 -3
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +6 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
- package/skills/pi-subagents/references/execution-controls.md +29 -15
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
- package/src/agents/agent-management.ts +124 -351
- package/src/agents/agents.ts +157 -31
- package/src/agents/skills.ts +1 -1
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/preflight.ts +36 -8
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +3 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +166 -88
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/schemas.ts +12 -35
- package/src/extension/tool-description.ts +35 -24
- package/src/inspectors/herdr/actions.ts +2 -2
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +32 -10
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/missions/workflow-state.ts +2 -2
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +31 -8
- package/src/runs/background/async-execution.ts +68 -40
- package/src/runs/background/async-job-tracker.ts +20 -4
- package/src/runs/background/async-resume.ts +47 -15
- package/src/runs/background/async-retention.ts +886 -0
- package/src/runs/background/async-status.ts +39 -53
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/completion-dedupe.ts +5 -1
- package/src/runs/background/completion-replay.ts +22 -12
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +68 -19
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/inspect-rpc.ts +443 -0
- package/src/runs/background/notify.ts +31 -5
- package/src/runs/background/result-files.ts +158 -90
- package/src/runs/background/result-watcher.ts +116 -20
- package/src/runs/background/resume-guidance.ts +8 -5
- package/src/runs/background/retained-children.ts +13 -3
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +11 -9
- package/src/runs/background/run-status.ts +27 -18
- package/src/runs/background/scheduled-runs.ts +24 -4
- package/src/runs/background/stale-run-reconciler.ts +6 -3
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +253 -140
- package/src/runs/background/subagent-wait.ts +8 -8
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +21 -4
- package/src/runs/background/wait-subscriptions.ts +81 -2
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +666 -1579
- package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/acceptance.ts +4 -4
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/external-job-bridge.ts +444 -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 +98 -38
- package/src/runs/shared/orca-progress-tabs.ts +84 -22
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +4 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/session-lease.ts +0 -6
- package/src/runs/shared/subagent-control.ts +26 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +14 -2
- 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 +71 -26
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-bridge.ts +2 -1
- package/src/slash/slash-commands.ts +29 -3
- package/src/tui/fleet.ts +63 -15
- package/src/watchdog/change-signature.ts +1 -1
- package/src/watchdog/lsp-diagnostics.ts +1 -0
- package/src/workflows/chat-progress.ts +2 -2
- package/src/workflows/scripted-workflow.ts +220 -67
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1581
|
@@ -2,14 +2,18 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
4
|
import { MISSION_BINDING_FILE } from "../../missions/lifecycle.ts";
|
|
5
|
+
import { encodeIndexSegment, indexSegmentAliases, MAX_INDEX_SEGMENT_BYTES } from "./index-segment.ts";
|
|
5
6
|
|
|
6
7
|
const RESULT_INDEX_VERSION = 1;
|
|
7
8
|
const RESULT_INDEX_DIR = "result-index";
|
|
8
9
|
const SESSION_INDEX_DIR = "sessions";
|
|
10
|
+
const RUN_INDEX_DIR = "runs";
|
|
9
11
|
const OBSERVER_INDEX_DIR = "observers";
|
|
10
12
|
const TOOL_CALL_INDEX_DIR = "tool-calls";
|
|
11
13
|
const RESULT_PENDING_DIR = "result-pending";
|
|
12
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");
|
|
13
17
|
|
|
14
18
|
export interface ResultIndexEntry {
|
|
15
19
|
version: 1;
|
|
@@ -21,7 +25,15 @@ export interface ResultIndexEntry {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
function encodeSegment(value: string): string {
|
|
24
|
-
return
|
|
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}`);
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
function nonEmptyString(value: unknown): string | undefined {
|
|
@@ -29,7 +41,7 @@ function nonEmptyString(value: unknown): string | undefined {
|
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
export function resultFileName(runId: string): string {
|
|
32
|
-
return `${runId}
|
|
44
|
+
return `${runId}${JSON_EXTENSION}`;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
export function resultFilePath(resultsDir: string, runId: string): string {
|
|
@@ -40,12 +52,39 @@ function sessionIndexDir(resultsDir: string, sessionId: string): string {
|
|
|
40
52
|
return path.join(resultsDir, RESULT_INDEX_DIR, SESSION_INDEX_DIR, encodeSegment(sessionId));
|
|
41
53
|
}
|
|
42
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
|
+
|
|
43
59
|
function resultIndexPath(resultsDir: string, sessionId: string, runId: string): string {
|
|
44
|
-
return path.join(sessionIndexDir(resultsDir, sessionId),
|
|
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));
|
|
45
69
|
}
|
|
46
70
|
|
|
47
71
|
function resultPendingPath(resultsDir: string, sessionId: string, runId: string): string {
|
|
48
|
-
return path.join(resultsDir, RESULT_PENDING_DIR, encodeSegment(sessionId),
|
|
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;
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
function observerIndexDir(resultsDir: string, observer: string): string {
|
|
@@ -53,15 +92,15 @@ function observerIndexDir(resultsDir: string, observer: string): string {
|
|
|
53
92
|
}
|
|
54
93
|
|
|
55
94
|
function observerIndexPath(resultsDir: string, observer: string, runId: string): string {
|
|
56
|
-
return path.join(observerIndexDir(resultsDir, observer),
|
|
95
|
+
return path.join(observerIndexDir(resultsDir, observer), encodedJsonFileName(runId));
|
|
57
96
|
}
|
|
58
97
|
|
|
59
98
|
function toolCallIndexDir(resultsDir: string, toolCallId: string): string {
|
|
60
|
-
return path.join(resultsDir, RESULT_INDEX_DIR, TOOL_CALL_INDEX_DIR,
|
|
99
|
+
return path.join(resultsDir, RESULT_INDEX_DIR, TOOL_CALL_INDEX_DIR, encodeIndexSegment(toolCallId));
|
|
61
100
|
}
|
|
62
101
|
|
|
63
102
|
function toolCallIndexPath(resultsDir: string, toolCallId: string, runId: string): string {
|
|
64
|
-
return path.join(toolCallIndexDir(resultsDir, toolCallId),
|
|
103
|
+
return path.join(toolCallIndexDir(resultsDir, toolCallId), encodedJsonFileName(runId));
|
|
65
104
|
}
|
|
66
105
|
|
|
67
106
|
function parseResultIndexEntry(value: unknown): ResultIndexEntry | undefined {
|
|
@@ -97,6 +136,11 @@ export function writeResultIndexForData(resultPath: string, data: Record<string,
|
|
|
97
136
|
};
|
|
98
137
|
const resultsDir = path.dirname(resultPath);
|
|
99
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
|
+
}
|
|
100
144
|
const toolCallId = nonEmptyString(data.toolCallId);
|
|
101
145
|
try {
|
|
102
146
|
if (toolCallId) writeAtomicJson(toolCallIndexPath(resultsDir, toolCallId, runId), entry);
|
|
@@ -136,17 +180,26 @@ export function writeAsyncResultFile(resultPath: string, data: Record<string, un
|
|
|
136
180
|
export function removeResultIndex(resultsDir: string, sessionId: string | undefined, runId: string | undefined, toolCallId?: string): void {
|
|
137
181
|
if (!runId) return;
|
|
138
182
|
if (sessionId) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
}
|
|
143
189
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
}
|
|
148
196
|
}
|
|
149
197
|
}
|
|
198
|
+
try {
|
|
199
|
+
fs.rmSync(runIndexPath(resultsDir, runId), { force: true });
|
|
200
|
+
} catch {
|
|
201
|
+
// Index cleanup must not affect result delivery.
|
|
202
|
+
}
|
|
150
203
|
if (toolCallId) {
|
|
151
204
|
try {
|
|
152
205
|
fs.rmSync(toolCallIndexPath(resultsDir, toolCallId, runId), { force: true });
|
|
@@ -180,7 +233,7 @@ function existingResultFile(resultPath: string): boolean {
|
|
|
180
233
|
}
|
|
181
234
|
|
|
182
235
|
function pendingResultExists(resultsDir: string, sessionId: string, runId: string): boolean {
|
|
183
|
-
return
|
|
236
|
+
return firstExistingResultFile(resultPendingPaths(resultsDir, sessionId, runId)) !== undefined;
|
|
184
237
|
}
|
|
185
238
|
|
|
186
239
|
function pendingResultPayloadMatches(filePath: string, sessionId: string, runId: string): boolean {
|
|
@@ -193,16 +246,34 @@ function pendingResultPayloadMatches(filePath: string, sessionId: string, runId:
|
|
|
193
246
|
}
|
|
194
247
|
}
|
|
195
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
|
+
|
|
196
254
|
export function promotePendingResultFile(resultsDir: string, sessionId: string, runId: string, file = resultFileName(runId), options: { logFailure?: boolean } = {}): "none" | "promoted" | "pending" {
|
|
197
255
|
if (file !== path.basename(file) || !file.endsWith(".json")) return "none";
|
|
198
|
-
const pendingPath =
|
|
199
|
-
if (!
|
|
256
|
+
const pendingPath = firstExistingResultFile(resultPendingPaths(resultsDir, sessionId, runId));
|
|
257
|
+
if (!pendingPath) return "none";
|
|
200
258
|
const resultPath = path.join(resultsDir, file);
|
|
201
259
|
try {
|
|
202
|
-
|
|
260
|
+
// POSIX rename replaces the destination atomically. Deleting it first lets a
|
|
261
|
+
// losing promoter unlink the result that another promoter just published.
|
|
203
262
|
fs.renameSync(pendingPath, resultPath);
|
|
204
263
|
return existingResultFile(resultPath) ? "promoted" : "pending";
|
|
205
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
|
+
}
|
|
206
277
|
if (options.logFailure !== false) console.error(`Failed to promote pending async result '${pendingPath}' to '${resultPath}':`, error);
|
|
207
278
|
return "pending";
|
|
208
279
|
}
|
|
@@ -216,33 +287,15 @@ interface ResultPayloadLocation {
|
|
|
216
287
|
|
|
217
288
|
function pendingResultLocationForSessionRun(resultsDir: string, sessionId: string, runId: string, file = resultFileName(runId)): ResultPayloadLocation | undefined {
|
|
218
289
|
if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
|
|
219
|
-
const pendingPath =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
: 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" };
|
|
223
293
|
}
|
|
224
294
|
|
|
225
|
-
function
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
sessions = fs.readdirSync(root, { withFileTypes: true });
|
|
230
|
-
} catch (error) {
|
|
231
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to inspect pending async result root '${root}':`, error);
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
for (const session of sessions) {
|
|
235
|
-
if (!session.isDirectory()) continue;
|
|
236
|
-
let sessionId: string;
|
|
237
|
-
try {
|
|
238
|
-
sessionId = decodeURIComponent(session.name);
|
|
239
|
-
} catch {
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
const location = pendingResultLocationForSessionRun(resultsDir, sessionId, runId);
|
|
243
|
-
if (location) return location;
|
|
244
|
-
}
|
|
245
|
-
return undefined;
|
|
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;
|
|
246
299
|
}
|
|
247
300
|
|
|
248
301
|
function resultPayloadLocationFromIndex(resultsDir: string, entry: ResultIndexEntry): ResultPayloadLocation | undefined {
|
|
@@ -257,15 +310,20 @@ function resultPayloadLocationFromIndex(resultsDir: string, entry: ResultIndexEn
|
|
|
257
310
|
}
|
|
258
311
|
|
|
259
312
|
function readResultIndexForSessionRun(resultsDir: string, sessionId: string, runId: string): ResultIndexEntry | undefined {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
}
|
|
268
325
|
}
|
|
326
|
+
return undefined;
|
|
269
327
|
}
|
|
270
328
|
|
|
271
329
|
export function resultPayloadPathForSessionRun(resultsDir: string, sessionId: string, runId: string): string | undefined {
|
|
@@ -286,27 +344,24 @@ export function resultPayloadPathForMissionObserverRun(resultsDir: string, runId
|
|
|
286
344
|
}
|
|
287
345
|
|
|
288
346
|
export function resultPayloadPathForIndexedRun(resultsDir: string, runId: string): string | undefined {
|
|
289
|
-
const
|
|
290
|
-
let sessions: fs.Dirent[] = [];
|
|
347
|
+
const entryPath = runIndexPath(resultsDir, runId);
|
|
291
348
|
try {
|
|
292
|
-
|
|
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 });
|
|
293
357
|
} catch (error) {
|
|
294
358
|
const code = (error as NodeJS.ErrnoException).code;
|
|
295
|
-
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (!session.isDirectory()) continue;
|
|
299
|
-
const entryPath = path.join(root, session.name, `${encodeSegment(runId)}.json`);
|
|
300
|
-
try {
|
|
301
|
-
const entry = parseResultIndexEntry(JSON.parse(fs.readFileSync(entryPath, "utf-8")));
|
|
302
|
-
if (!entry || entry.runId !== runId) continue;
|
|
303
|
-
const location = resultPayloadLocationFromIndex(resultsDir, entry);
|
|
304
|
-
if (location) return location.path;
|
|
305
|
-
} catch (error) {
|
|
306
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result index '${entryPath}':`, error);
|
|
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 {}
|
|
307
362
|
}
|
|
308
363
|
}
|
|
309
|
-
return
|
|
364
|
+
return undefined;
|
|
310
365
|
}
|
|
311
366
|
|
|
312
367
|
function indexedResultFile(resultsDir: string, entry: ResultIndexEntry, includePending = false): string | undefined {
|
|
@@ -323,7 +378,7 @@ function listIndexFiles(dir: string): string[] {
|
|
|
323
378
|
.map((entry) => path.join(dir, entry.name));
|
|
324
379
|
} catch (error) {
|
|
325
380
|
const code = (error as NodeJS.ErrnoException).code;
|
|
326
|
-
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
381
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") return [];
|
|
327
382
|
throw error;
|
|
328
383
|
}
|
|
329
384
|
return files;
|
|
@@ -348,44 +403,56 @@ function resultFilesFromIndexDir(resultsDir: string, dir: string, includePending
|
|
|
348
403
|
}
|
|
349
404
|
|
|
350
405
|
export function resultFilesForSession(resultsDir: string, sessionId: string): string[] {
|
|
351
|
-
|
|
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];
|
|
352
411
|
}
|
|
353
412
|
|
|
354
413
|
function pendingResultFilesForSession(resultsDir: string, sessionId: string): string[] {
|
|
355
|
-
const dir = path.join(resultsDir, RESULT_PENDING_DIR, encodeSegment(sessionId));
|
|
356
|
-
let entries: fs.Dirent[];
|
|
357
|
-
try {
|
|
358
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
359
|
-
} catch (error) {
|
|
360
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
361
|
-
throw error;
|
|
362
|
-
}
|
|
363
414
|
const files = new Set<string>();
|
|
364
|
-
for (const
|
|
365
|
-
|
|
366
|
-
const pendingPath = path.join(dir, entry.name);
|
|
415
|
+
for (const dir of pendingSessionDirs(resultsDir, sessionId)) {
|
|
416
|
+
let entries: fs.Dirent[];
|
|
367
417
|
try {
|
|
368
|
-
|
|
369
|
-
const runId = nonEmptyString(data.runId) ?? nonEmptyString(data.id);
|
|
370
|
-
if (runId && nonEmptyString(data.sessionId) === sessionId) files.add(resultFileName(runId));
|
|
418
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
371
419
|
} catch (error) {
|
|
372
|
-
|
|
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
|
+
}
|
|
373
434
|
}
|
|
374
435
|
}
|
|
375
436
|
return [...files];
|
|
376
437
|
}
|
|
377
438
|
|
|
378
439
|
export function resultCandidateFilesForSession(resultsDir: string, sessionId: string): string[] {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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];
|
|
383
446
|
}
|
|
384
447
|
|
|
385
448
|
export function resultFilesForToolCall(resultsDir: string, toolCallId: string): string[] {
|
|
386
449
|
return resultFilesFromIndexDir(resultsDir, toolCallIndexDir(resultsDir, toolCallId));
|
|
387
450
|
}
|
|
388
451
|
|
|
452
|
+
export function resultCandidateFilesForToolCall(resultsDir: string, toolCallId: string): string[] {
|
|
453
|
+
return resultFilesFromIndexDir(resultsDir, toolCallIndexDir(resultsDir, toolCallId), true);
|
|
454
|
+
}
|
|
455
|
+
|
|
389
456
|
export function missionObserverResultFiles(resultsDir: string): string[] {
|
|
390
457
|
return resultFilesFromIndexDir(resultsDir, observerIndexDir(resultsDir, MISSION_OBSERVER));
|
|
391
458
|
}
|
|
@@ -403,7 +470,8 @@ export function cleanupResultIndexes(resultsDir: string, now = Date.now(), maxAg
|
|
|
403
470
|
try {
|
|
404
471
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
405
472
|
} catch (error) {
|
|
406
|
-
|
|
473
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
474
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") return;
|
|
407
475
|
throw error;
|
|
408
476
|
}
|
|
409
477
|
for (const entry of entries) {
|