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.
Files changed (109) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. 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 encodeURIComponent(value);
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}.json`;
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), `${encodeSegment(runId)}.json`);
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), `${encodeSegment(runId)}.json`);
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), `${encodeSegment(runId)}.json`);
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, encodeSegment(toolCallId));
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), `${encodeSegment(runId)}.json`);
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
- try {
140
- fs.rmSync(resultIndexPath(resultsDir, sessionId, runId), { force: true });
141
- } catch {
142
- // Index cleanup must not affect result delivery.
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
- try {
145
- fs.rmSync(resultPendingPath(resultsDir, sessionId, runId), { force: true });
146
- } catch {
147
- // Pending cleanup must not affect result delivery.
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 existingResultFile(resultPendingPath(resultsDir, sessionId, runId));
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 = resultPendingPath(resultsDir, sessionId, runId);
199
- if (!existingResultFile(pendingPath)) return "none";
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
- fs.rmSync(resultPath, { force: true });
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 = resultPendingPath(resultsDir, sessionId, runId);
220
- return existingResultFile(pendingPath) && pendingResultPayloadMatches(pendingPath, sessionId, runId)
221
- ? { file, path: pendingPath, state: "pending" }
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 pendingResultLocationForIndexedRun(resultsDir: string, runId: string): ResultPayloadLocation | undefined {
226
- const root = path.join(resultsDir, RESULT_PENDING_DIR);
227
- let sessions: fs.Dirent[];
228
- try {
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
- try {
261
- const entry = parseResultIndexEntry(JSON.parse(fs.readFileSync(resultIndexPath(resultsDir, sessionId, runId), "utf-8")));
262
- if (!entry || entry.sessionId !== sessionId || entry.runId !== runId) return undefined;
263
- return entry;
264
- } catch (error) {
265
- const code = (error as NodeJS.ErrnoException).code;
266
- if (code !== "ENOENT" && code !== "ENOTDIR") console.error(`Ignoring invalid async result index for '${runId}':`, error);
267
- return 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
+ }
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 root = path.join(resultsDir, RESULT_INDEX_DIR, SESSION_INDEX_DIR);
290
- let sessions: fs.Dirent[] = [];
347
+ const entryPath = runIndexPath(resultsDir, runId);
291
348
  try {
292
- sessions = fs.readdirSync(root, { withFileTypes: true });
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") console.error(`Failed to inspect async result session index root '${root}':`, error);
296
- }
297
- for (const session of sessions) {
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 pendingResultLocationForIndexedRun(resultsDir, runId)?.path;
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
- return resultFilesFromIndexDir(resultsDir, sessionIndexDir(resultsDir, sessionId));
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 entry of entries) {
365
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
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
- const data = JSON.parse(fs.readFileSync(pendingPath, "utf-8")) as Record<string, unknown>;
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
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid pending async result '${pendingPath}':`, 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
+ }
373
434
  }
374
435
  }
375
436
  return [...files];
376
437
  }
377
438
 
378
439
  export function resultCandidateFilesForSession(resultsDir: string, sessionId: string): string[] {
379
- return [...new Set([
380
- ...resultFilesFromIndexDir(resultsDir, sessionIndexDir(resultsDir, sessionId), true),
381
- ...pendingResultFilesForSession(resultsDir, sessionId),
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
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
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) {