pi-subagents 0.56.0 → 0.58.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 +92 -0
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +124 -21
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +41 -2
- package/docs/models.md +3 -3
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +14 -3
- package/docs/workflows.md +44 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +36 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +257 -51
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +18 -4
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +28 -3
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +33 -8
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +55 -19
- package/src/extension/schemas.ts +6 -5
- package/src/extension/tool-description.ts +18 -12
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +51 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +127 -19
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +7 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +36 -15
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +239 -56
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +92 -11
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +322 -102
- package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +348 -55
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +69 -7
- package/src/runs/shared/model-fallback.ts +39 -5
- package/src/runs/shared/mutation-evidence.ts +7 -2
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +13 -1
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +160 -10
- package/src/shared/utils.ts +6 -29
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +55 -21
- package/src/workflows/scripted-workflow.ts +299 -31
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +155 -5
|
@@ -9,6 +9,7 @@ import type { AsyncStatus } from "../../shared/types.ts";
|
|
|
9
9
|
import { MISSION_BINDING_FILE } from "../../missions/lifecycle.ts";
|
|
10
10
|
import { ACTIVE_RUN_INDEX_DIR } from "./active-run-index.ts";
|
|
11
11
|
import { encodeIndexSegment } from "./index-segment.ts";
|
|
12
|
+
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
12
13
|
|
|
13
14
|
export const ASYNC_RETENTION_DAYS = 30;
|
|
14
15
|
export const ASYNC_RETENTION_BATCH_SIZE = 100;
|
|
@@ -96,11 +97,13 @@ export interface AsyncRetentionOptions {
|
|
|
96
97
|
lstatSync?: typeof fs.lstatSync;
|
|
97
98
|
signal?: AbortSignal;
|
|
98
99
|
discoveryWorkerUrl?: URL;
|
|
100
|
+
reconcileKill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
export interface AsyncRetentionResult {
|
|
102
104
|
acquired: boolean;
|
|
103
105
|
scanned: number;
|
|
106
|
+
repairedRuns: number;
|
|
104
107
|
deletedRuns: number;
|
|
105
108
|
deletedResults: number;
|
|
106
109
|
reapedTombstones: number;
|
|
@@ -135,7 +138,7 @@ function listDir(dir: string): fs.Dirent[] {
|
|
|
135
138
|
|
|
136
139
|
function readJson(filePath: string): Record<string, unknown> | undefined {
|
|
137
140
|
try {
|
|
138
|
-
const parsed = JSON.parse(fs.readFileSync(filePath
|
|
141
|
+
const parsed = JSON.parse(fs.readFileSync(filePath).toString("utf8")) as unknown;
|
|
139
142
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
|
|
140
143
|
} catch {
|
|
141
144
|
return undefined;
|
|
@@ -320,7 +323,7 @@ function readCursor(root: string): RetentionCursor {
|
|
|
320
323
|
function processStartIdentity(pid: number): string | undefined {
|
|
321
324
|
if (process.platform === "linux") {
|
|
322
325
|
try {
|
|
323
|
-
const stat = fs.readFileSync(`/proc/${pid}/stat
|
|
326
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`).toString("utf8");
|
|
324
327
|
const commandEnd = stat.lastIndexOf(")");
|
|
325
328
|
if (commandEnd === -1) return undefined;
|
|
326
329
|
const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
@@ -504,6 +507,7 @@ function appendMaintenanceLog(root: string, result: AsyncRetentionResult, now: n
|
|
|
504
507
|
at: new Date(now).toISOString(),
|
|
505
508
|
acquired: result.acquired,
|
|
506
509
|
scanned: result.scanned,
|
|
510
|
+
repairedRuns: result.repairedRuns,
|
|
507
511
|
deletedRuns: result.deletedRuns,
|
|
508
512
|
deletedResults: result.deletedResults,
|
|
509
513
|
reapedTombstones: result.reapedTombstones,
|
|
@@ -654,6 +658,7 @@ export async function cleanupAsyncRetention(options: AsyncRetentionOptions): Pro
|
|
|
654
658
|
const result: AsyncRetentionResult = {
|
|
655
659
|
acquired: false,
|
|
656
660
|
scanned: 0,
|
|
661
|
+
repairedRuns: 0,
|
|
657
662
|
deletedRuns: 0,
|
|
658
663
|
deletedResults: 0,
|
|
659
664
|
reapedTombstones: 0,
|
|
@@ -750,7 +755,19 @@ export async function cleanupAsyncRetention(options: AsyncRetentionOptions): Pro
|
|
|
750
755
|
increment(result.skipped, "unsafe-run-path");
|
|
751
756
|
continue;
|
|
752
757
|
}
|
|
753
|
-
|
|
758
|
+
let status = readStatus(runDir);
|
|
759
|
+
if (status?.state === "running"
|
|
760
|
+
&& validRunId(status.runId)
|
|
761
|
+
&& path.basename(runDir) === status.runId
|
|
762
|
+
&& !protectedRunIds.has(status.runId)) {
|
|
763
|
+
const reconciliation = reconcileAsyncRun(runDir, {
|
|
764
|
+
resultsDir: options.resultsDir,
|
|
765
|
+
kill: options.reconcileKill,
|
|
766
|
+
now,
|
|
767
|
+
});
|
|
768
|
+
status = reconciliation.status ?? undefined;
|
|
769
|
+
if (reconciliation.repaired) result.repairedRuns += 1;
|
|
770
|
+
}
|
|
754
771
|
const initialReason = runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
|
|
755
772
|
if (initialReason) {
|
|
756
773
|
increment(result.skipped, initialReason);
|
|
@@ -16,6 +16,7 @@ import { ACTIVE_RUN_INDEX_DIR, DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS, activeRu
|
|
|
16
16
|
import { readRecentTerminalRunIndex, TERMINAL_RUN_INDEX_DIR } from "./terminal-run-index.ts";
|
|
17
17
|
import { canScanAsyncRunPrefix } from "./run-id-query.ts";
|
|
18
18
|
import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
|
|
19
|
+
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
19
20
|
|
|
20
21
|
interface AsyncRunStepSummary {
|
|
21
22
|
index: number;
|
|
@@ -47,6 +48,7 @@ interface AsyncRunStepSummary {
|
|
|
47
48
|
totalCost?: CostSummary;
|
|
48
49
|
skills?: string[];
|
|
49
50
|
model?: string;
|
|
51
|
+
contextLimit?: number;
|
|
50
52
|
thinking?: string;
|
|
51
53
|
attemptedModels?: string[];
|
|
52
54
|
sessionFile?: string;
|
|
@@ -121,6 +123,7 @@ export interface AsyncRunSummary {
|
|
|
121
123
|
parentWorkflowRunId?: string;
|
|
122
124
|
workflowKey?: string;
|
|
123
125
|
workflow?: Details["workflow"];
|
|
126
|
+
workflowChildren?: Details["workflowChildren"];
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
interface AsyncRunListOptions {
|
|
@@ -229,6 +232,8 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
|
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = [], nestedRoute?: NestedRoute): AsyncRunSummary {
|
|
235
|
+
const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
|
|
236
|
+
if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': workflowChildren.workflowRunId does not match.`);
|
|
232
237
|
if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
|
|
233
238
|
throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
|
|
234
239
|
}
|
|
@@ -283,6 +288,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
283
288
|
...(step.totalCost ? { totalCost: step.totalCost } : {}),
|
|
284
289
|
...(step.skills ? { skills: step.skills } : {}),
|
|
285
290
|
...(step.model ? { model: step.model } : {}),
|
|
291
|
+
...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
|
|
286
292
|
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
287
293
|
...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
|
|
288
294
|
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
@@ -357,6 +363,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
357
363
|
...(status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {}),
|
|
358
364
|
...(status.workflowKey ? { workflowKey: status.workflowKey } : {}),
|
|
359
365
|
...(status.workflow ? { workflow: status.workflow } : {}),
|
|
366
|
+
...(workflowChildren ? { workflowChildren } : {}),
|
|
360
367
|
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
361
368
|
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
362
369
|
...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
|
|
@@ -172,6 +172,7 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
|
|
|
172
172
|
...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
|
|
173
173
|
skills: task.skills,
|
|
174
174
|
model: task.model,
|
|
175
|
+
...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
|
|
175
176
|
thinking: task.thinking,
|
|
176
177
|
attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
|
|
177
178
|
recentTools: [],
|
|
@@ -189,6 +190,7 @@ function statusStepsForRunnerStep(step: RunnerStep, stepIndex: number): StatusSt
|
|
|
189
190
|
label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
|
|
190
191
|
outputName: step.collect.as,
|
|
191
192
|
structured: Boolean(step.collect.outputSchema),
|
|
193
|
+
...(step.parallel.contextLimit !== undefined ? { contextLimit: step.parallel.contextLimit } : {}),
|
|
192
194
|
status: "pending",
|
|
193
195
|
recentTools: [],
|
|
194
196
|
recentOutput: [],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { resultFilePath } from "./result-files.ts";
|
|
3
|
-
import type { AcceptanceLedger, AsyncStatus, CostSummary, ModelAttempt } from "../../shared/types.ts";
|
|
3
|
+
import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, ModelAttempt } from "../../shared/types.ts";
|
|
4
4
|
import { readStatus } from "../../shared/utils.ts";
|
|
5
5
|
|
|
6
6
|
export interface ImportedAsyncRoot {
|
|
@@ -27,6 +27,10 @@ export interface ImportedAsyncRootResult {
|
|
|
27
27
|
structuredOutputPath?: string;
|
|
28
28
|
structuredOutputSchemaPath?: string;
|
|
29
29
|
acceptance?: AcceptanceLedger;
|
|
30
|
+
artifactPaths?: ArtifactPaths;
|
|
31
|
+
outputSaveError?: string;
|
|
32
|
+
transcriptPath?: string;
|
|
33
|
+
transcriptError?: string;
|
|
30
34
|
timedOut?: boolean;
|
|
31
35
|
stopped?: boolean;
|
|
32
36
|
}
|
|
@@ -56,6 +60,10 @@ interface AsyncResultFile {
|
|
|
56
60
|
structuredOutputPath?: string;
|
|
57
61
|
structuredOutputSchemaPath?: string;
|
|
58
62
|
acceptance?: AcceptanceLedger;
|
|
63
|
+
artifactPaths?: ArtifactPaths;
|
|
64
|
+
outputSaveError?: string;
|
|
65
|
+
transcriptPath?: string;
|
|
66
|
+
transcriptError?: string;
|
|
59
67
|
}>;
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -118,6 +126,7 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
118
126
|
...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
|
|
119
127
|
...(step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: step.structuredOutputSchemaPath } : {}),
|
|
120
128
|
...(step?.acceptance ? { acceptance: step.acceptance } : {}),
|
|
129
|
+
...(step?.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
121
130
|
};
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -136,6 +145,7 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
|
|
|
136
145
|
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
137
146
|
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
138
147
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
148
|
+
...(step?.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
139
149
|
};
|
|
140
150
|
}
|
|
141
151
|
|
|
@@ -168,6 +178,10 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
168
178
|
...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
|
|
169
179
|
...(child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath } : {}),
|
|
170
180
|
...(child?.acceptance ?? step?.acceptance ? { acceptance: child?.acceptance ?? step?.acceptance } : {}),
|
|
181
|
+
...(child?.artifactPaths ? { artifactPaths: child.artifactPaths } : {}),
|
|
182
|
+
...(child?.outputSaveError ? { outputSaveError: child.outputSaveError } : {}),
|
|
183
|
+
...(child?.transcriptPath ?? step?.transcriptPath ? { transcriptPath: child?.transcriptPath ?? step?.transcriptPath } : {}),
|
|
184
|
+
...(child?.transcriptError ? { transcriptError: child.transcriptError } : {}),
|
|
171
185
|
};
|
|
172
186
|
}
|
|
173
187
|
|
|
@@ -18,6 +18,7 @@ import { readStatus } from "../../shared/utils.ts";
|
|
|
18
18
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
19
19
|
import { contextModeLabel, summarizeContextModes } from "../shared/context-mode.ts";
|
|
20
20
|
import { formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
21
|
+
import { isTrustedRecordedSessionFile } from "../../shared/session-file-trust.ts";
|
|
21
22
|
|
|
22
23
|
const DEFAULT_TRANSCRIPT_LINES = 80;
|
|
23
24
|
const MAX_TRANSCRIPT_LINES = 500;
|
|
@@ -43,6 +44,8 @@ interface TranscriptOptions {
|
|
|
43
44
|
index?: number;
|
|
44
45
|
lines?: number;
|
|
45
46
|
sessionRoots?: string[];
|
|
47
|
+
trustedSessionFiles?: string[];
|
|
48
|
+
trustedSessionFileRoot?: string;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
interface TextTailResult {
|
|
@@ -120,10 +123,13 @@ function readTextTail(filePath: string, maxLines: number): TextTailResult {
|
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
function readContainedTextTail(filePath: string, maxLines: number, trustedRoots: string[], label: string): TextTailResult {
|
|
124
|
-
if (trustedRoots.length === 0) return { path: filePath, lines: [], truncated: false, error: `Refusing to read ${label} transcript path without a trusted root: ${filePath}` };
|
|
126
|
+
function readContainedTextTail(filePath: string, maxLines: number, trustedRoots: string[], label: string, trustedFiles: string[] = [], trustedFileRoot?: string): TextTailResult {
|
|
127
|
+
if (trustedRoots.length === 0 && (!trustedFileRoot || trustedFiles.length === 0)) return { path: filePath, lines: [], truncated: false, error: `Refusing to read ${label} transcript path without a trusted root: ${filePath}` };
|
|
125
128
|
const resolvedPath = path.resolve(filePath);
|
|
126
|
-
|
|
129
|
+
const recordedCandidate = trustedFileRoot
|
|
130
|
+
&& pathWithin(trustedFileRoot, resolvedPath)
|
|
131
|
+
&& trustedFiles.some((file) => path.resolve(file) === resolvedPath);
|
|
132
|
+
if (!trustedRoots.some((root) => pathWithin(root, resolvedPath)) && !recordedCandidate) {
|
|
127
133
|
return { path: filePath, lines: [], truncated: false, error: `Refusing to read ${label} transcript path outside trusted roots: ${filePath}` };
|
|
128
134
|
}
|
|
129
135
|
let lstat: fs.Stats;
|
|
@@ -143,7 +149,7 @@ function readContainedTextTail(filePath: string, maxLines: number, trustedRoots:
|
|
|
143
149
|
} catch (error) {
|
|
144
150
|
return { path: filePath, lines: [], truncated: false, error: getErrorMessage(error) };
|
|
145
151
|
}
|
|
146
|
-
if (!realRoots.some((root) => pathWithin(root, realPath))) {
|
|
152
|
+
if (!realRoots.some((root) => pathWithin(root, realPath)) && !isTrustedRecordedSessionFile(realPath, trustedFiles, trustedFileRoot)) {
|
|
147
153
|
return { path: filePath, lines: [], truncated: false, error: `Refusing to read ${label} transcript path outside trusted roots: ${filePath}` };
|
|
148
154
|
}
|
|
149
155
|
return readTextTail(resolvedPath, maxLines);
|
|
@@ -213,8 +219,8 @@ function sessionMessageLine(record: unknown): string | undefined {
|
|
|
213
219
|
|
|
214
220
|
/** Structured session tail: parsed content parts, newest last, bounded by
|
|
215
221
|
* maxMessages. Same trusted-root containment as the prose transcript tail. */
|
|
216
|
-
export function readSessionMessagesTail(sessionFile: string, maxMessages: number, trustedRoots: string[]): { messages: SessionTranscriptMessage[]; warnings: string[]; truncated: boolean } {
|
|
217
|
-
const tail = readContainedTextTail(sessionFile, Math.max(maxMessages * 4, maxMessages), trustedRoots, "session");
|
|
222
|
+
export function readSessionMessagesTail(sessionFile: string, maxMessages: number, trustedRoots: string[], trustedFiles: string[] = [], trustedFileRoot?: string): { messages: SessionTranscriptMessage[]; warnings: string[]; truncated: boolean } {
|
|
223
|
+
const tail = readContainedTextTail(sessionFile, Math.max(maxMessages * 4, maxMessages), trustedRoots, "session", trustedFiles, trustedFileRoot);
|
|
218
224
|
const warnings: string[] = [];
|
|
219
225
|
if (tail.error) warnings.push(`Session read failed for ${sessionFile}: ${tail.error}`);
|
|
220
226
|
const parsedMessages: SessionTranscriptMessage[] = [];
|
|
@@ -235,8 +241,8 @@ export function readSessionMessagesTail(sessionFile: string, maxMessages: number
|
|
|
235
241
|
return { messages, warnings, truncated: tail.truncated || parsedMessages.length > messages.length };
|
|
236
242
|
}
|
|
237
243
|
|
|
238
|
-
function readSessionTranscriptTail(sessionFile: string, maxLines: number, trustedRoots: string[]): { lines: string[]; warnings: string[] } {
|
|
239
|
-
const tail = readContainedTextTail(sessionFile, Math.max(maxLines * 4, maxLines), trustedRoots, "session");
|
|
244
|
+
function readSessionTranscriptTail(sessionFile: string, maxLines: number, trustedRoots: string[], trustedFiles: string[] = [], trustedFileRoot?: string): { lines: string[]; warnings: string[] } {
|
|
245
|
+
const tail = readContainedTextTail(sessionFile, Math.max(maxLines * 4, maxLines), trustedRoots, "session", trustedFiles, trustedFileRoot);
|
|
240
246
|
const warnings: string[] = [];
|
|
241
247
|
if (tail.error) warnings.push(`Session read failed for ${sessionFile}: ${tail.error}`);
|
|
242
248
|
const lines: string[] = [];
|
|
@@ -523,7 +529,7 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
|
|
|
523
529
|
transcriptSource = "Recent output from status.json";
|
|
524
530
|
}
|
|
525
531
|
if (transcriptLines.length === 0 && sessionFile) {
|
|
526
|
-
const sessionTail = readSessionTranscriptTail(sessionFile, lineLimit, options.sessionRoots ?? []);
|
|
532
|
+
const sessionTail = readSessionTranscriptTail(sessionFile, lineLimit, options.sessionRoots ?? [], options.trustedSessionFiles, options.trustedSessionFileRoot);
|
|
527
533
|
transcriptLines = sessionTail.lines;
|
|
528
534
|
warnings.push(...sessionTail.warnings);
|
|
529
535
|
if (transcriptLines.length > 0) transcriptSource = `Session transcript tail from ${sessionFile}`;
|
|
@@ -554,7 +560,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
|
|
|
554
560
|
appendTranscriptBody(lines, "Transcript tail", [], false);
|
|
555
561
|
return safeTranscriptLines(lines);
|
|
556
562
|
}
|
|
557
|
-
const sessionTail = readSessionTranscriptTail(run.sessionFile, lineLimit, options.sessionRoots ?? []);
|
|
563
|
+
const sessionTail = readSessionTranscriptTail(run.sessionFile, lineLimit, options.sessionRoots ?? [], options.trustedSessionFiles, options.trustedSessionFileRoot);
|
|
558
564
|
if (sessionTail.warnings.length) {
|
|
559
565
|
lines.push("Warnings:");
|
|
560
566
|
for (const warning of sessionTail.warnings) lines.push(` ${warning}`);
|
|
@@ -218,19 +218,19 @@ function readOutputArtifact(outputPath: string): { output?: string; errorText?:
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function readSessionBackedOutput(sessionPath: string, trustedRoots: string[]): { output?: string } {
|
|
222
|
-
if (trustedRoots.length === 0) return {};
|
|
221
|
+
function readSessionBackedOutput(sessionPath: string, trustedRoots: string[], trustedSessionFileRoot?: string): { output?: string } {
|
|
222
|
+
if (trustedRoots.length === 0 && !trustedSessionFileRoot) return {};
|
|
223
223
|
// The archive retains the child's session file as its output record. The
|
|
224
224
|
// final answer is the terminal assistant message's text — all text parts
|
|
225
225
|
// of that one session record, not just the last part.
|
|
226
|
-
const tail = readSessionMessagesTail(sessionPath, MAX_MESSAGE_LINES, trustedRoots);
|
|
226
|
+
const tail = readSessionMessagesTail(sessionPath, MAX_MESSAGE_LINES, trustedRoots, [sessionPath], trustedSessionFileRoot);
|
|
227
227
|
const last = tail.messages.findLast((message) => message.role === "assistant" && message.kind === "text");
|
|
228
228
|
if (!last) return {};
|
|
229
229
|
const parts = tail.messages.filter((message) => message.recordIndex === last.recordIndex && message.kind === "text");
|
|
230
230
|
return { output: parts.map((part) => part.text).join("\n") };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
function readResultOutput(resultsDir: string, sessionId: string, runId: string, stepIndex: number | undefined, trustedRoots: string[], stepAgent: string | undefined, now: () => number): { output?: string; errorText?: string } {
|
|
233
|
+
function readResultOutput(resultsDir: string, sessionId: string, runId: string, stepIndex: number | undefined, trustedRoots: string[], stepAgent: string | undefined, now: () => number, trustedSessionFileRoot?: string): { output?: string; errorText?: string } {
|
|
234
234
|
const resultPath = resultPayloadPathForSessionRun(resultsDir, sessionId, runId);
|
|
235
235
|
if (resultPath) return resultOutput(JSON.parse(fs.readFileSync(resultPath, "utf-8")) as unknown, stepIndex);
|
|
236
236
|
// Read the raw record first: readCompletionReplay best-effort deletes
|
|
@@ -284,7 +284,7 @@ function readResultOutput(resultsDir: string, sessionId: string, runId: string,
|
|
|
284
284
|
if (artifactPath) return readOutputArtifact(artifactPath);
|
|
285
285
|
const sessionEntry = archive?.entries.find((entry) => entry.source === "session" && matches(entry));
|
|
286
286
|
const sessionPath = sessionEntry?.source === "session" ? sessionEntry.path : undefined;
|
|
287
|
-
if (sessionPath) return readSessionBackedOutput(sessionPath, trustedRoots);
|
|
287
|
+
if (sessionPath) return readSessionBackedOutput(sessionPath, trustedRoots, trustedSessionFileRoot);
|
|
288
288
|
const output = archive?.entries.find((entry) => entry.source === "result-tail" && matches(entry))?.text;
|
|
289
289
|
return output ? { output } : {};
|
|
290
290
|
}
|
|
@@ -377,8 +377,8 @@ export function buildInspectReply(request: InspectRequest, deps: InspectDeps = {
|
|
|
377
377
|
|
|
378
378
|
let messages: InspectReplyMessage[] | undefined;
|
|
379
379
|
let task: string | undefined;
|
|
380
|
-
if (sessionFile && trustedRoots.length > 0) {
|
|
381
|
-
const tail = readSessionMessagesTail(sessionFile, lineLimit, trustedRoots);
|
|
380
|
+
if (sessionFile && (trustedRoots.length > 0 || deps.state?.trustedSessionFileRoot)) {
|
|
381
|
+
const tail = readSessionMessagesTail(sessionFile, lineLimit, trustedRoots, [sessionFile], deps.state?.trustedSessionFileRoot);
|
|
382
382
|
if (tail.messages.length > 0) {
|
|
383
383
|
messages = tail.messages.map(toReplyMessage);
|
|
384
384
|
// The delegated task is the child session's first user message, but
|
|
@@ -393,7 +393,7 @@ export function buildInspectReply(request: InspectRequest, deps: InspectDeps = {
|
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
const result = readResultOutput(resultsDir, currentSessionId, node.status.runId, node.stepIndex, trustedRoots, step?.agent, deps.now ?? Date.now);
|
|
396
|
+
const result = readResultOutput(resultsDir, currentSessionId, node.status.runId, node.stepIndex, trustedRoots, step?.agent, deps.now ?? Date.now, deps.state?.trustedSessionFileRoot);
|
|
397
397
|
const failedText = step?.error ?? node.status.error;
|
|
398
398
|
if (result.output === undefined && result.errorText === undefined && failedText !== undefined) {
|
|
399
399
|
return errorReply(request, "internal", "Inspection could not read the async run artifacts.");
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./completion-batcher.ts";
|
|
17
17
|
import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type ScheduleOrigin, type SubagentState } from "../../shared/types.ts";
|
|
18
18
|
import { isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
19
|
+
import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
|
|
19
20
|
|
|
20
21
|
export interface SubagentNotifyDetails {
|
|
21
22
|
agent: string;
|
|
@@ -91,6 +92,7 @@ export interface RegisterSubagentNotifyOptions {
|
|
|
91
92
|
batchConfig?: CompletionBatchConfig;
|
|
92
93
|
timers?: NotifyTimerApi;
|
|
93
94
|
now?: () => number;
|
|
95
|
+
ownership?: Pick<ResultDeliveryOwnership, "owns">;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export interface CompletionNotifier {
|
|
@@ -228,6 +230,8 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
|
|
|
228
230
|
interface PendingCompletion {
|
|
229
231
|
key: string;
|
|
230
232
|
details: SubagentNotifyDetails;
|
|
233
|
+
sessionId: string;
|
|
234
|
+
completionOwnerId: unknown;
|
|
231
235
|
triggerTurn: boolean;
|
|
232
236
|
resolve(accepted: boolean): void;
|
|
233
237
|
}
|
|
@@ -335,6 +339,10 @@ export default function registerSubagentNotify(
|
|
|
335
339
|
const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
|
|
336
340
|
const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
|
|
337
341
|
let disposed = false;
|
|
342
|
+
const ownsResult = options.ownership?.owns
|
|
343
|
+
?? ((sessionId: string, completionOwnerId: unknown) => sessionId === state.currentSessionId
|
|
344
|
+
&& typeof completionOwnerId === "string"
|
|
345
|
+
&& completionOwnerId === state.completionOwnerId);
|
|
338
346
|
|
|
339
347
|
const settle = (items: PendingCompletion[], accepted: boolean) => {
|
|
340
348
|
for (const item of items) {
|
|
@@ -343,7 +351,18 @@ export default function registerSubagentNotify(
|
|
|
343
351
|
item.resolve(accepted);
|
|
344
352
|
}
|
|
345
353
|
};
|
|
346
|
-
const emit = (items: PendingCompletion[]) =>
|
|
354
|
+
const emit = (items: PendingCompletion[]) => {
|
|
355
|
+
const accepted: PendingCompletion[] = [];
|
|
356
|
+
const rejected: PendingCompletion[] = [];
|
|
357
|
+
for (const item of items) {
|
|
358
|
+
const owned = item.details.source === "foreground"
|
|
359
|
+
? item.sessionId === state.currentSessionId
|
|
360
|
+
: ownsResult(item.sessionId, item.completionOwnerId);
|
|
361
|
+
(owned ? accepted : rejected).push(item);
|
|
362
|
+
}
|
|
363
|
+
settle(rejected, false);
|
|
364
|
+
settle(accepted, sendCompletion(pi, accepted));
|
|
365
|
+
};
|
|
347
366
|
const getBatcher = (result: CompletionNotification) => {
|
|
348
367
|
const key = completionBatchKey(result);
|
|
349
368
|
let batcher = batchers.get(key);
|
|
@@ -360,8 +379,10 @@ export default function registerSubagentNotify(
|
|
|
360
379
|
};
|
|
361
380
|
|
|
362
381
|
const deliver = (result: CompletionNotification): Promise<boolean> => {
|
|
363
|
-
if (disposed || typeof result.sessionId !== "string"
|
|
364
|
-
if (result.source
|
|
382
|
+
if (disposed || typeof result.sessionId !== "string") return Promise.resolve(false);
|
|
383
|
+
if (result.source === "foreground") {
|
|
384
|
+
if (result.sessionId !== state.currentSessionId) return Promise.resolve(false);
|
|
385
|
+
} else if (!ownsResult(result.sessionId, result.completionOwnerId)) return Promise.resolve(false);
|
|
365
386
|
if (result.intercomDelivered === true) return Promise.resolve(true);
|
|
366
387
|
const key = buildCompletionKey(result, "notify");
|
|
367
388
|
const seenAt = seen.get(key);
|
|
@@ -376,6 +397,8 @@ export default function registerSubagentNotify(
|
|
|
376
397
|
const item: PendingCompletion = {
|
|
377
398
|
key,
|
|
378
399
|
details,
|
|
400
|
+
sessionId: result.sessionId,
|
|
401
|
+
completionOwnerId: result.completionOwnerId,
|
|
379
402
|
triggerTurn: result.triggerTurn !== false,
|
|
380
403
|
resolve,
|
|
381
404
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SubagentState } from "../../shared/types.ts";
|
|
2
|
+
|
|
3
|
+
const MAX_CLAIMED_PREDECESSOR_SESSIONS = 8;
|
|
4
|
+
|
|
5
|
+
type ResultDeliveryState = Pick<SubagentState, "currentSessionId" | "completionOwnerId">;
|
|
6
|
+
|
|
7
|
+
export interface ResultDeliveryOwnership {
|
|
8
|
+
owns(sessionId: string, completionOwnerId: unknown): boolean;
|
|
9
|
+
claimedSessionIds(): readonly string[];
|
|
10
|
+
claimPredecessor(previousSessionFile: string | undefined, previousRuntimeSessionId: string | null): boolean;
|
|
11
|
+
clear(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createResultDeliveryOwnership(state: ResultDeliveryState): ResultDeliveryOwnership {
|
|
15
|
+
const claimed = new Map<string, string>();
|
|
16
|
+
|
|
17
|
+
const currentOwner = (): string | undefined => typeof state.completionOwnerId === "string" && state.completionOwnerId
|
|
18
|
+
? state.completionOwnerId
|
|
19
|
+
: undefined;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
owns(sessionId, completionOwnerId) {
|
|
23
|
+
const owner = currentOwner();
|
|
24
|
+
if (!owner || completionOwnerId !== owner) return false;
|
|
25
|
+
return sessionId === state.currentSessionId || claimed.get(sessionId) === owner;
|
|
26
|
+
},
|
|
27
|
+
claimedSessionIds() {
|
|
28
|
+
const owner = currentOwner();
|
|
29
|
+
if (!owner) return [];
|
|
30
|
+
return [...claimed].flatMap(([sessionId, claimedOwner]) => claimedOwner === owner ? [sessionId] : []);
|
|
31
|
+
},
|
|
32
|
+
claimPredecessor(previousSessionFile, previousRuntimeSessionId) {
|
|
33
|
+
const owner = currentOwner();
|
|
34
|
+
if (!owner || !previousSessionFile || !previousRuntimeSessionId) return false;
|
|
35
|
+
if (previousSessionFile !== previousRuntimeSessionId || state.currentSessionId !== previousRuntimeSessionId) return false;
|
|
36
|
+
claimed.delete(previousRuntimeSessionId);
|
|
37
|
+
claimed.set(previousRuntimeSessionId, owner);
|
|
38
|
+
while (claimed.size > MAX_CLAIMED_PREDECESSOR_SESSIONS) claimed.delete(claimed.keys().next().value!);
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
clear() {
|
|
42
|
+
claimed.clear();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -40,6 +40,11 @@ function nonEmptyString(value: unknown): string | undefined {
|
|
|
40
40
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** A legacy alias can be valid metadata while still being unaddressable on the current filesystem. */
|
|
44
|
+
function isUnaddressableResultCandidate(error: unknown): boolean {
|
|
45
|
+
return (error as NodeJS.ErrnoException)?.code === "ENAMETOOLONG";
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
export function resultFileName(runId: string): string {
|
|
44
49
|
return `${runId}${JSON_EXTENSION}`;
|
|
45
50
|
}
|
|
@@ -227,7 +232,7 @@ function existingResultFile(resultPath: string): boolean {
|
|
|
227
232
|
try {
|
|
228
233
|
return fs.statSync(resultPath).isFile();
|
|
229
234
|
} catch (error) {
|
|
230
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to inspect async result payload '${resultPath}':`, error);
|
|
235
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Failed to inspect async result payload '${resultPath}':`, error);
|
|
231
236
|
return false;
|
|
232
237
|
}
|
|
233
238
|
}
|
|
@@ -241,7 +246,7 @@ function pendingResultPayloadMatches(filePath: string, sessionId: string, runId:
|
|
|
241
246
|
const data = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record<string, unknown>;
|
|
242
247
|
return (nonEmptyString(data.runId) ?? nonEmptyString(data.id)) === runId && nonEmptyString(data.sessionId) === sessionId;
|
|
243
248
|
} catch (error) {
|
|
244
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid pending async result '${filePath}':`, error);
|
|
249
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid pending async result '${filePath}':`, error);
|
|
245
250
|
return false;
|
|
246
251
|
}
|
|
247
252
|
}
|
|
@@ -263,11 +268,11 @@ export function promotePendingResultFile(resultsDir: string, sessionId: string,
|
|
|
263
268
|
return existingResultFile(resultPath) ? "promoted" : "pending";
|
|
264
269
|
} catch (error) {
|
|
265
270
|
const code = (error as NodeJS.ErrnoException).code;
|
|
266
|
-
if (code === "ENOENT" || code === "EEXIST" || code === "EPERM" || code === "EACCES") {
|
|
271
|
+
if (code === "ENOENT" || code === "EEXIST" || code === "EPERM" || code === "EACCES" || isUnaddressableResultCandidate(error)) {
|
|
267
272
|
const pendingExists = existingResultFile(pendingPath);
|
|
268
273
|
const resultExists = existingResultFile(resultPath);
|
|
269
274
|
if (pendingExists) {
|
|
270
|
-
if (!resultExists && options.logFailure !== false) console.error(`Failed to promote pending async result '${pendingPath}' to '${resultPath}':`, error);
|
|
275
|
+
if (!resultExists && options.logFailure !== false && !isUnaddressableResultCandidate(error)) console.error(`Failed to promote pending async result '${pendingPath}' to '${resultPath}':`, error);
|
|
271
276
|
return "pending";
|
|
272
277
|
}
|
|
273
278
|
if (resultExists) return "promoted";
|
|
@@ -318,7 +323,7 @@ function readResultIndexForSessionRun(resultsDir: string, sessionId: string, run
|
|
|
318
323
|
} catch (error) {
|
|
319
324
|
const code = (error as NodeJS.ErrnoException).code;
|
|
320
325
|
if (code === "EPERM" || code === "EACCES") throw error;
|
|
321
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
326
|
+
if (code !== "ENOENT" && code !== "ENOTDIR" && !isUnaddressableResultCandidate(error)) {
|
|
322
327
|
console.error(`Ignoring invalid async result index for '${runId}':`, error);
|
|
323
328
|
}
|
|
324
329
|
}
|
|
@@ -338,7 +343,7 @@ export function resultPayloadPathForMissionObserverRun(resultsDir: string, runId
|
|
|
338
343
|
if (!entry || entry.runId !== runId) return undefined;
|
|
339
344
|
return resultPayloadLocationFromIndex(resultsDir, entry)?.path;
|
|
340
345
|
} catch (error) {
|
|
341
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
|
|
346
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
|
|
342
347
|
return undefined;
|
|
343
348
|
}
|
|
344
349
|
}
|
|
@@ -356,9 +361,12 @@ export function resultPayloadPathForIndexedRun(resultsDir: string, runId: string
|
|
|
356
361
|
fs.rmSync(entryPath, { force: true });
|
|
357
362
|
} catch (error) {
|
|
358
363
|
const code = (error as NodeJS.ErrnoException).code;
|
|
364
|
+
if (isUnaddressableResultCandidate(error)) return undefined;
|
|
359
365
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
360
366
|
console.error(`Ignoring invalid async result run index '${entryPath}':`, error);
|
|
361
|
-
try { fs.rmSync(entryPath, { force: true }); } catch {
|
|
367
|
+
try { fs.rmSync(entryPath, { force: true }); } catch {
|
|
368
|
+
// Invalid optional run-index cleanup is best-effort.
|
|
369
|
+
}
|
|
362
370
|
}
|
|
363
371
|
}
|
|
364
372
|
return undefined;
|
|
@@ -378,7 +386,7 @@ function listIndexFiles(dir: string): string[] {
|
|
|
378
386
|
.map((entry) => path.join(dir, entry.name));
|
|
379
387
|
} catch (error) {
|
|
380
388
|
const code = (error as NodeJS.ErrnoException).code;
|
|
381
|
-
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") return [];
|
|
389
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES" || isUnaddressableResultCandidate(error)) return [];
|
|
382
390
|
throw error;
|
|
383
391
|
}
|
|
384
392
|
return files;
|
|
@@ -396,7 +404,7 @@ function resultFilesFromIndexDir(resultsDir: string, dir: string, includePending
|
|
|
396
404
|
const resultFile = indexedResultFile(resultsDir, entry, includePending);
|
|
397
405
|
if (resultFile) candidates.add(resultFile);
|
|
398
406
|
} catch (error) {
|
|
399
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result index '${entryPath}':`, error);
|
|
407
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result index '${entryPath}':`, error);
|
|
400
408
|
}
|
|
401
409
|
}
|
|
402
410
|
return [...candidates];
|
|
@@ -418,7 +426,7 @@ function pendingResultFilesForSession(resultsDir: string, sessionId: string): st
|
|
|
418
426
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
419
427
|
} catch (error) {
|
|
420
428
|
const code = (error as NodeJS.ErrnoException).code;
|
|
421
|
-
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") continue;
|
|
429
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES" || isUnaddressableResultCandidate(error)) continue;
|
|
422
430
|
throw error;
|
|
423
431
|
}
|
|
424
432
|
for (const entry of entries) {
|
|
@@ -429,7 +437,7 @@ function pendingResultFilesForSession(resultsDir: string, sessionId: string): st
|
|
|
429
437
|
const runId = nonEmptyString(data.runId) ?? nonEmptyString(data.id);
|
|
430
438
|
if (runId && nonEmptyString(data.sessionId) === sessionId) files.add(resultFileName(runId));
|
|
431
439
|
} catch (error) {
|
|
432
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid pending async result '${pendingPath}':`, error);
|
|
440
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid pending async result '${pendingPath}':`, error);
|
|
433
441
|
}
|
|
434
442
|
}
|
|
435
443
|
}
|
|
@@ -471,14 +479,16 @@ export function cleanupResultIndexes(resultsDir: string, now = Date.now(), maxAg
|
|
|
471
479
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
472
480
|
} catch (error) {
|
|
473
481
|
const code = (error as NodeJS.ErrnoException).code;
|
|
474
|
-
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") return;
|
|
482
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES" || isUnaddressableResultCandidate(error)) return;
|
|
475
483
|
throw error;
|
|
476
484
|
}
|
|
477
485
|
for (const entry of entries) {
|
|
478
486
|
const fullPath = path.join(dir, entry.name);
|
|
479
487
|
if (entry.isDirectory()) {
|
|
480
488
|
visit(fullPath);
|
|
481
|
-
try { fs.rmdirSync(fullPath); } catch {
|
|
489
|
+
try { fs.rmdirSync(fullPath); } catch {
|
|
490
|
+
// Non-empty or concurrently removed index directories are left in place.
|
|
491
|
+
}
|
|
482
492
|
continue;
|
|
483
493
|
}
|
|
484
494
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
@@ -492,11 +502,14 @@ export function cleanupResultIndexes(resultsDir: string, now = Date.now(), maxAg
|
|
|
492
502
|
removed += 1;
|
|
493
503
|
}
|
|
494
504
|
} catch (error) {
|
|
505
|
+
if (isUnaddressableResultCandidate(error)) continue;
|
|
495
506
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result index '${fullPath}':`, error);
|
|
496
507
|
try {
|
|
497
508
|
fs.rmSync(fullPath, { force: true });
|
|
498
509
|
removed += 1;
|
|
499
|
-
} catch {
|
|
510
|
+
} catch {
|
|
511
|
+
// Invalid optional index cleanup is best-effort.
|
|
512
|
+
}
|
|
500
513
|
}
|
|
501
514
|
}
|
|
502
515
|
};
|