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
|
@@ -17,16 +17,29 @@ const MIRROR_FOOTER_RESERVE_BYTES = 16 * 1024;
|
|
|
17
17
|
const COUNTER_LOCK_STALE_MS = 30_000;
|
|
18
18
|
const COUNTER_LOCK_RETRIES = 200;
|
|
19
19
|
const COUNTER_LOCK_RETRY_MS = 10;
|
|
20
|
+
const ORCA_CREATE_WAIT_TIMEOUT_MS = ORCA_CREATE_TIMEOUT_MS + ORCA_KILL_GRACE_MS + 3_000;
|
|
20
21
|
|
|
21
22
|
const ORCA_CREATE_WATCHDOG_SCRIPT = [
|
|
22
23
|
"const {spawn}=require('node:child_process');",
|
|
23
|
-
"const
|
|
24
|
-
"const
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
24
|
+
"const fs=require('node:fs');",
|
|
25
|
+
"const timeout=Number(process.argv[1]),grace=Number(process.argv[2]),waitTimeout=Number(process.argv[3]);",
|
|
26
|
+
"const previous=process.argv[4],done=process.argv[5],command=process.argv[6],args=process.argv.slice(7);",
|
|
27
|
+
"function mark(){try{fs.writeFileSync(done.replace(/\\.pending$/,'.ready'),'')}catch{}}",
|
|
28
|
+
"function exists(file){try{return fs.existsSync(file)}catch{return false}}",
|
|
29
|
+
"function keepQueued(){try{const now=new Date();fs.utimesSync(done,now,now)}catch{}}",
|
|
30
|
+
"function predecessorReady(){if(previous==='-')return true;if(exists(previous.replace(/\\.pending$/,'.ready')))return true;try{return Date.now()-fs.statSync(previous).mtimeMs>=waitTimeout}catch{return true}}",
|
|
31
|
+
"function start(){",
|
|
32
|
+
" try{",
|
|
33
|
+
" const child=spawn(command,args,{stdio:'ignore',windowsHide:true});",
|
|
34
|
+
" let hardKill;",
|
|
35
|
+
" const timer=setTimeout(()=>{child.kill('SIGTERM');hardKill=setTimeout(()=>child.kill('SIGKILL'),grace)},timeout);",
|
|
36
|
+
" const clear=()=>{clearTimeout(timer);if(hardKill)clearTimeout(hardKill);mark()};",
|
|
37
|
+
" child.once('error',()=>{clear();process.exitCode=1});",
|
|
38
|
+
" child.once('close',code=>{clear();process.exitCode=code===0?0:1});",
|
|
39
|
+
" }catch{mark();process.exitCode=1}",
|
|
40
|
+
"}",
|
|
41
|
+
"if(predecessorReady())start();",
|
|
42
|
+
"else{(function waitPrev(){keepQueued();if(predecessorReady())return start();setTimeout(waitPrev,20)})();}",
|
|
30
43
|
].join("");
|
|
31
44
|
|
|
32
45
|
const ORCA_CLEANUP_WATCHDOG_SCRIPT = [
|
|
@@ -114,7 +127,7 @@ function progressRoot(): string {
|
|
|
114
127
|
function pruneStaleProgressFiles(root: string, now = Date.now()): void {
|
|
115
128
|
try {
|
|
116
129
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
117
|
-
if (!entry.isFile() || (!entry.name.endsWith(".log") && !entry.name.endsWith(".done"))) continue;
|
|
130
|
+
if (!entry.isFile() || (!entry.name.endsWith(".log") && !entry.name.endsWith(".done") && !entry.name.endsWith(".ready") && !entry.name.endsWith(".pending"))) continue;
|
|
118
131
|
const file = path.join(root, entry.name);
|
|
119
132
|
try {
|
|
120
133
|
if (now - fs.statSync(file).mtimeMs > STALE_PROGRESS_MAX_AGE_MS) fs.rmSync(file, { force: true });
|
|
@@ -148,10 +161,12 @@ function resolveSequenceScope(cwd: string): string {
|
|
|
148
161
|
}
|
|
149
162
|
}
|
|
150
163
|
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
function sequenceKey(cwd: string): string {
|
|
165
|
+
return createHash("sha256").update(resolveSequenceScope(cwd)).digest("hex").slice(0, 20);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function withSequenceLock<T>(root: string, key: string, fn: () => T): T | undefined {
|
|
169
|
+
const lockPath = path.join(root, `counter-${key}.lock`);
|
|
155
170
|
let locked = false;
|
|
156
171
|
for (let attempt = 0; attempt < COUNTER_LOCK_RETRIES; attempt++) {
|
|
157
172
|
try {
|
|
@@ -171,21 +186,44 @@ function reserveTabSequence(root: string, cwd: string): number | undefined {
|
|
|
171
186
|
}
|
|
172
187
|
if (!locked) return undefined;
|
|
173
188
|
try {
|
|
189
|
+
return fn();
|
|
190
|
+
} finally {
|
|
191
|
+
try { fs.rmSync(lockPath, { recursive: true, force: true }); } catch { /* stale lock cleanup handles crashes */ }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function predecessorMarker(pathValue: string | undefined): string | undefined {
|
|
196
|
+
if (!pathValue) return undefined;
|
|
197
|
+
const ready = pathValue.endsWith(".pending") ? pathValue.replace(/\.pending$/, ".ready") : pathValue;
|
|
198
|
+
const pending = ready.replace(/\.ready$/, ".pending");
|
|
199
|
+
if (fs.existsSync(ready) || fs.existsSync(pending)) return pending;
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function reserveTabSequence(root: string, cwd: string): { sequence: number; previousCreateDone?: string; createDone: string } | undefined {
|
|
204
|
+
const key = sequenceKey(cwd);
|
|
205
|
+
const counterPath = path.join(root, `counter-${key}`);
|
|
206
|
+
const createDone = path.join(root, `create-${key}-${randomUUID()}.pending`);
|
|
207
|
+
return withSequenceLock(root, key, () => {
|
|
174
208
|
let current = 0;
|
|
209
|
+
let previousCreateDone: string | undefined;
|
|
175
210
|
try {
|
|
176
|
-
const
|
|
211
|
+
const raw = fs.readFileSync(counterPath, "utf-8");
|
|
212
|
+
const [countLine, previousLine] = raw.split("\n");
|
|
213
|
+
const parsed = Number.parseInt(countLine ?? "", 10);
|
|
177
214
|
if (Number.isSafeInteger(parsed) && parsed >= 0) current = parsed;
|
|
215
|
+
previousCreateDone = predecessorMarker(previousLine?.trim());
|
|
178
216
|
} catch { /* first tab for this worktree */ }
|
|
179
217
|
const next = current + 1;
|
|
180
218
|
try {
|
|
181
|
-
fs.writeFileSync(
|
|
182
|
-
|
|
219
|
+
fs.writeFileSync(createDone, "", { encoding: "utf-8", mode: 0o600 });
|
|
220
|
+
fs.writeFileSync(counterPath, `${next}\n${createDone}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
221
|
+
return { sequence: next, previousCreateDone, createDone };
|
|
183
222
|
} catch {
|
|
223
|
+
try { fs.rmSync(createDone, { force: true }); } catch { /* best effort */ }
|
|
184
224
|
return undefined;
|
|
185
225
|
}
|
|
186
|
-
}
|
|
187
|
-
try { fs.rmSync(lockPath, { recursive: true, force: true }); } catch { /* stale lock cleanup handles crashes */ }
|
|
188
|
-
}
|
|
226
|
+
});
|
|
189
227
|
}
|
|
190
228
|
|
|
191
229
|
export function resolvePiSessionId(sessionFile: string | undefined): string | undefined {
|
|
@@ -263,16 +301,21 @@ export function createOrcaProgressTab(input: {
|
|
|
263
301
|
const agent = safeSegment(input.agent, "subagent");
|
|
264
302
|
const index = typeof input.index === "number" && Number.isInteger(input.index) && input.index >= 0 ? input.index : 0;
|
|
265
303
|
const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
304
|
+
const reservation = reserveTabSequence(root, cwd);
|
|
305
|
+
if (reservation === undefined) return undefined;
|
|
306
|
+
const tabSequence = reservation.sequence;
|
|
268
307
|
const stem = `${runId}-${index}-${randomUUID()}`;
|
|
269
308
|
const logPath = path.join(root, `${stem}.log`);
|
|
270
309
|
const donePath = path.join(root, `${stem}.done`);
|
|
271
310
|
const title = `subagent · ${agent} · ${tabSequence}`;
|
|
311
|
+
const markCreateReady = () => {
|
|
312
|
+
try { fs.writeFileSync(reservation.createDone.replace(/\.pending$/, ".ready"), ""); } catch { /* unblock later tabs */ }
|
|
313
|
+
};
|
|
272
314
|
try {
|
|
273
315
|
fs.writeFileSync(logPath, `pi-subagents / ${agent}\nrun ${runId} · child ${index + 1}\n${"─".repeat(48)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
274
316
|
fs.rmSync(donePath, { force: true });
|
|
275
317
|
} catch {
|
|
318
|
+
markCreateReady();
|
|
276
319
|
return undefined;
|
|
277
320
|
}
|
|
278
321
|
|
|
@@ -303,11 +346,21 @@ export function createOrcaProgressTab(input: {
|
|
|
303
346
|
failObserver();
|
|
304
347
|
}
|
|
305
348
|
};
|
|
349
|
+
let createSettled = false;
|
|
350
|
+
let cleanupPaths: string[] | undefined;
|
|
351
|
+
const scheduleDeferredCleanup = () => {
|
|
352
|
+
if (!createSettled || cleanupPaths === undefined) return;
|
|
353
|
+
scheduleCleanup(nodeExecutable, cleanupPaths);
|
|
354
|
+
cleanupPaths = undefined;
|
|
355
|
+
};
|
|
306
356
|
try {
|
|
307
357
|
const watchdog = spawn(nodeExecutable, [
|
|
308
358
|
"-e", ORCA_CREATE_WATCHDOG_SCRIPT,
|
|
309
359
|
String(ORCA_CREATE_TIMEOUT_MS),
|
|
310
360
|
String(ORCA_KILL_GRACE_MS),
|
|
361
|
+
String(ORCA_CREATE_WAIT_TIMEOUT_MS),
|
|
362
|
+
reservation.previousCreateDone ?? "-",
|
|
363
|
+
reservation.createDone,
|
|
311
364
|
command,
|
|
312
365
|
"terminal", "create",
|
|
313
366
|
"--worktree", `path:${path.resolve(cwd)}`,
|
|
@@ -322,11 +375,19 @@ export function createOrcaProgressTab(input: {
|
|
|
322
375
|
env: input.env ?? process.env,
|
|
323
376
|
});
|
|
324
377
|
watchdog.once("close", (code) => {
|
|
378
|
+
createSettled = true;
|
|
325
379
|
if (code !== 0) failObserver();
|
|
380
|
+
scheduleDeferredCleanup();
|
|
381
|
+
});
|
|
382
|
+
watchdog.once("error", () => {
|
|
383
|
+
markCreateReady();
|
|
384
|
+
createSettled = true;
|
|
385
|
+
failObserver();
|
|
386
|
+
scheduleDeferredCleanup();
|
|
326
387
|
});
|
|
327
|
-
watchdog.once("error", failObserver);
|
|
328
388
|
watchdog.unref();
|
|
329
389
|
} catch {
|
|
390
|
+
markCreateReady();
|
|
330
391
|
failObserver();
|
|
331
392
|
return undefined;
|
|
332
393
|
}
|
|
@@ -368,7 +429,8 @@ export function createOrcaProgressTab(input: {
|
|
|
368
429
|
logStream.end(footer, () => {
|
|
369
430
|
if (!available) return;
|
|
370
431
|
try { fs.writeFileSync(donePath, `${status}\n`, { encoding: "utf-8", mode: 0o600 }); } catch { /* best effort */ }
|
|
371
|
-
|
|
432
|
+
cleanupPaths = [logPath, donePath];
|
|
433
|
+
scheduleDeferredCleanup();
|
|
372
434
|
});
|
|
373
435
|
},
|
|
374
436
|
};
|
|
@@ -34,6 +34,48 @@ function readManifest(manifestPath: string): ParallelHandoffManifest | undefined
|
|
|
34
34
|
return parsed;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function resolveExistingPath(candidate: string): string {
|
|
38
|
+
try {
|
|
39
|
+
return fs.realpathSync(candidate);
|
|
40
|
+
} catch {
|
|
41
|
+
return path.resolve(candidate);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pathInside(root: string, candidate: string): boolean {
|
|
46
|
+
const relative = path.relative(root, candidate);
|
|
47
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveRetainedWorktreeCwd(manifestPath: string, runId: string, childIndex: number): string | undefined {
|
|
51
|
+
const manifest = readManifest(manifestPath);
|
|
52
|
+
if (!manifest) return undefined;
|
|
53
|
+
if (manifest.runId !== runId) throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${runId}'.`);
|
|
54
|
+
const match = manifest.groups
|
|
55
|
+
.flatMap((group) => group.children.map((child) => ({ group, child })))
|
|
56
|
+
.find(({ child }) => child.index === childIndex);
|
|
57
|
+
if (!match) return undefined;
|
|
58
|
+
const cleanup = match.group.cleanup.tasks.find((task) => task.index === match.child.taskIndex);
|
|
59
|
+
if (!cleanup) throw new Error(`Async run '${runId}' child ${childIndex} has no managed worktree cleanup record.`);
|
|
60
|
+
const relativeCwd = path.relative(resolveExistingPath(match.group.repoRoot), resolveExistingPath(manifest.cwd));
|
|
61
|
+
if (path.isAbsolute(relativeCwd) || relativeCwd === ".." || relativeCwd.startsWith(`..${path.sep}`)) throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
|
|
62
|
+
const requiredCwd = path.join(cleanup.path, relativeCwd);
|
|
63
|
+
if (cleanup.worktreeRemoved || cleanup.branchRemoved) throw new Error(`Async run '${runId}' required managed worktree was removed: ${requiredCwd}`);
|
|
64
|
+
let worktreeRoot = "";
|
|
65
|
+
let resolvedRequiredCwd = "";
|
|
66
|
+
try {
|
|
67
|
+
const rootStat = fs.lstatSync(cleanup.path);
|
|
68
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("worktree root is not a directory");
|
|
69
|
+
worktreeRoot = fs.realpathSync(cleanup.path);
|
|
70
|
+
if (!fs.statSync(requiredCwd).isDirectory()) throw new Error("path is not a directory");
|
|
71
|
+
resolvedRequiredCwd = fs.realpathSync(requiredCwd);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`Async run '${runId}' required managed worktree cwd is missing: ${requiredCwd}`, { cause: error instanceof Error ? error : undefined });
|
|
74
|
+
}
|
|
75
|
+
if (!pathInside(worktreeRoot, resolvedRequiredCwd)) throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
|
|
76
|
+
return requiredCwd;
|
|
77
|
+
}
|
|
78
|
+
|
|
37
79
|
function referenceFor(manifestPath: string, manifest: ParallelHandoffManifest): ParallelHandoffReference {
|
|
38
80
|
const children = manifest.groups.flatMap((group) => group.children);
|
|
39
81
|
return {
|
|
@@ -74,7 +116,7 @@ function missingDiff(input: { manifestPath: string; stepIndex: number; taskIndex
|
|
|
74
116
|
export function writeParallelHandoffGroup(input: {
|
|
75
117
|
manifestPath: string;
|
|
76
118
|
runId: string;
|
|
77
|
-
mode: "parallel" | "chain";
|
|
119
|
+
mode: "single" | "parallel" | "chain";
|
|
78
120
|
source: "foreground" | "async";
|
|
79
121
|
cwd: string;
|
|
80
122
|
stepIndex: number;
|
|
@@ -162,7 +204,7 @@ export function parallelHandoffPath(baseDir: string, runId?: string): string {
|
|
|
162
204
|
export function writePendingParallelHandoff(input: {
|
|
163
205
|
manifestPath: string;
|
|
164
206
|
runId: string;
|
|
165
|
-
mode: "parallel" | "chain";
|
|
207
|
+
mode: "single" | "parallel" | "chain";
|
|
166
208
|
source: "foreground" | "async";
|
|
167
209
|
cwd: string;
|
|
168
210
|
stepIndex: number;
|
|
@@ -173,11 +215,11 @@ export function writePendingParallelHandoff(input: {
|
|
|
173
215
|
}
|
|
174
216
|
|
|
175
217
|
export function formatParallelHandoffReference(reference: ParallelHandoffReference): string {
|
|
176
|
-
return `
|
|
218
|
+
return `Worktree handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;
|
|
177
219
|
}
|
|
178
220
|
|
|
179
221
|
export function formatParallelHandoffError(error: unknown): string {
|
|
180
|
-
return `
|
|
222
|
+
return `Worktree handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
181
223
|
}
|
|
182
224
|
|
|
183
225
|
export function discardPreservedWorktrees(
|
|
@@ -65,13 +65,8 @@ export interface RunnerSubagentStep {
|
|
|
65
65
|
capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
|
|
66
66
|
/** Private stable logical-child path for inherited run fan-out accounting. */
|
|
67
67
|
runFanoutPath?: string;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
export interface RunnerCheckpointStep {
|
|
71
|
-
checkpoint: string;
|
|
72
|
-
message?: string;
|
|
73
|
-
phase?: string;
|
|
74
|
-
label?: string;
|
|
68
|
+
/** Run this single child in one managed worktree. */
|
|
69
|
+
worktree?: boolean;
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
export interface ParallelStepGroup {
|
|
@@ -100,11 +95,7 @@ export interface DynamicRunnerGroup {
|
|
|
100
95
|
capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
|
|
101
96
|
}
|
|
102
97
|
|
|
103
|
-
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup
|
|
104
|
-
|
|
105
|
-
export function isCheckpointRunnerStep(step: RunnerStep): step is RunnerCheckpointStep {
|
|
106
|
-
return "checkpoint" in step;
|
|
107
|
-
}
|
|
98
|
+
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
|
|
108
99
|
|
|
109
100
|
export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
|
|
110
101
|
return "parallel" in step && Array.isArray(step.parallel);
|
|
@@ -117,9 +108,7 @@ export function isDynamicRunnerGroup(step: RunnerStep): step is DynamicRunnerGro
|
|
|
117
108
|
export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
|
|
118
109
|
const flat: RunnerSubagentStep[] = [];
|
|
119
110
|
for (const step of steps) {
|
|
120
|
-
if (
|
|
121
|
-
continue;
|
|
122
|
-
} else if (isParallelGroup(step)) {
|
|
111
|
+
if (isParallelGroup(step)) {
|
|
123
112
|
for (const task of step.parallel) flat.push(task);
|
|
124
113
|
} else if (isDynamicRunnerGroup(step)) {
|
|
125
114
|
continue;
|
|
@@ -14,6 +14,10 @@ const MAX_PREVIEW_BYTES = 2048;
|
|
|
14
14
|
const SECRET_KEY = /(?:authorization|cookie|credential|password|secret|token|api[-_]?key)/i;
|
|
15
15
|
const SECRET_VALUE = /\b(?:Bearer\s+\S+|(?:sk|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{8,})\b/gi;
|
|
16
16
|
|
|
17
|
+
export function redactSecretValues(value: string): string {
|
|
18
|
+
return value.replace(SECRET_VALUE, "[redacted]");
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
export function validatePermissionRules(value: unknown, label: string): PermissionRules | undefined {
|
|
18
22
|
if (value === undefined) return undefined;
|
|
19
23
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object mapping tool names to allow, ask, or deny.`);
|
|
@@ -66,7 +70,7 @@ function redact(value: unknown, key = "", depth = 0): unknown {
|
|
|
66
70
|
if (Array.isArray(value)) return value.slice(0, 10).map((item) => redact(item, "", depth + 1));
|
|
67
71
|
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value as Record<string, unknown>).slice(0, 20).map(([entryKey, entryValue]) => [entryKey, redact(entryValue, entryKey, depth + 1)]));
|
|
68
72
|
if (typeof value === "string") {
|
|
69
|
-
const redacted = value
|
|
73
|
+
const redacted = redactSecretValues(value);
|
|
70
74
|
return redacted.length > 500 ? `${redacted.slice(0, 500)}…` : redacted;
|
|
71
75
|
}
|
|
72
76
|
return value;
|
|
@@ -433,13 +433,20 @@ export function resolvePiLaunchToolPlan(
|
|
|
433
433
|
...internalTools,
|
|
434
434
|
]),
|
|
435
435
|
];
|
|
436
|
+
// Supervisor-coordination names stay in the --tools allowlist but are never
|
|
437
|
+
// strict requirements: children register contact_supervisor at runtime through
|
|
438
|
+
// the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
|
|
439
|
+
// appended intercom alongside contact_supervisor, so that exact pairing is
|
|
440
|
+
// legacy plumbing, not a user demand for an external intercom provider;
|
|
441
|
+
// a lone intercom entry stays strictly required (#1207).
|
|
442
|
+
const legacySupervisorPairing = declaredBuiltinTools.includes("contact_supervisor");
|
|
436
443
|
const requiredChildTools = explicitToolAllowlist
|
|
437
444
|
? [
|
|
438
445
|
...new Set([
|
|
439
446
|
...(input.tools !== undefined ? declaredBuiltinTools : []),
|
|
440
447
|
...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
|
|
441
448
|
...internalTools,
|
|
442
|
-
]),
|
|
449
|
+
].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
|
|
443
450
|
]
|
|
444
451
|
: [];
|
|
445
452
|
const permSystemExt = capabilityCeiling?.denyExtensions
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
2
|
import * as fs from "node:fs";
|
|
4
3
|
import * as os from "node:os";
|
|
@@ -79,11 +78,6 @@ function getProcessStartIdentity(pid: number): string | undefined {
|
|
|
79
78
|
return undefined;
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
|
-
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
83
|
-
const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf-8" });
|
|
84
|
-
const started = result.status === 0 ? result.stdout.trim() : "";
|
|
85
|
-
return started ? `${process.platform}:${started}` : undefined;
|
|
86
|
-
}
|
|
87
81
|
return undefined;
|
|
88
82
|
}
|
|
89
83
|
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
type ResolvedControlConfig,
|
|
8
8
|
} from "../../shared/types.ts";
|
|
9
9
|
import { isToolTimeoutExempt } from "./tool-timeout.ts";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { previewDisplayText } from "../../shared/display-text.ts";
|
|
10
12
|
|
|
11
13
|
const CONTROL_EVENT_TYPES: ControlEventType[] = ["active_long_running", "needs_attention"];
|
|
12
14
|
const CONTROL_NOTIFICATION_CHANNELS: ControlNotificationChannel[] = ["event", "async", "intercom"];
|
|
@@ -116,6 +118,10 @@ export function buildControlEvent(input: {
|
|
|
116
118
|
currentPath?: string;
|
|
117
119
|
elapsedMs?: number;
|
|
118
120
|
recentFailureSummary?: string;
|
|
121
|
+
workflowKey?: string;
|
|
122
|
+
phase?: string;
|
|
123
|
+
label?: string;
|
|
124
|
+
taskPreview?: string;
|
|
119
125
|
}): ControlEvent {
|
|
120
126
|
const ts = input.ts ?? Date.now();
|
|
121
127
|
const type = input.type ?? (input.to === "active_long_running" ? "active_long_running" : "needs_attention");
|
|
@@ -144,6 +150,10 @@ export function buildControlEvent(input: {
|
|
|
144
150
|
...(input.currentPath ? { currentPath: input.currentPath } : {}),
|
|
145
151
|
...(elapsedMs !== undefined ? { elapsedMs } : {}),
|
|
146
152
|
...(input.recentFailureSummary ? { recentFailureSummary: input.recentFailureSummary } : {}),
|
|
153
|
+
...(input.workflowKey ? { workflowKey: input.workflowKey } : {}),
|
|
154
|
+
...(input.phase ? { phase: input.phase } : {}),
|
|
155
|
+
...(input.label ? { label: input.label } : {}),
|
|
156
|
+
...(input.taskPreview ? { taskPreview: previewDisplayText(input.taskPreview, 160) } : {}),
|
|
147
157
|
};
|
|
148
158
|
}
|
|
149
159
|
|
|
@@ -153,7 +163,8 @@ export function shouldNotifyControlEvent(config: ResolvedControlConfig, event: C
|
|
|
153
163
|
|
|
154
164
|
export function controlNotificationKey(event: ControlEvent, childIntercomTarget?: string): string {
|
|
155
165
|
const childKey = childIntercomTarget ?? (event.index !== undefined ? `${event.runId}:${event.index}` : event.runId);
|
|
156
|
-
|
|
166
|
+
const contextHash = createHash("sha256").update(formatControlNudge(event)).digest("hex").slice(0, 8);
|
|
167
|
+
return `${childKey}:${event.type}:${event.reason ?? "idle"}:${contextHash}`;
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
export function claimControlNotification(config: ResolvedControlConfig, event: ControlEvent, seenKeys: Set<string>, childIntercomTarget?: string): boolean {
|
|
@@ -175,6 +186,17 @@ function formatLongRunningFacts(event: ControlEvent): string | undefined {
|
|
|
175
186
|
return facts.length > 0 ? facts.join(" | ") : undefined;
|
|
176
187
|
}
|
|
177
188
|
|
|
189
|
+
export function formatControlNudge(event: ControlEvent): string {
|
|
190
|
+
const scope = event.label ?? event.phase ?? event.workflowKey ?? event.taskPreview;
|
|
191
|
+
if (event.recentFailureSummary) return previewDisplayText(`Resolve the recent failure${scope ? ` for ${scope}` : ""}: ${event.recentFailureSummary}. Report the smallest next step or ask for a decision.`, 160);
|
|
192
|
+
if (event.currentTool || event.currentPath) {
|
|
193
|
+
const current = [event.currentTool ? `tool ${event.currentTool}` : undefined, event.currentPath ? `path ${event.currentPath}` : undefined].filter(Boolean).join(" at ");
|
|
194
|
+
return previewDisplayText(`Check ${current}${scope ? ` for ${scope}` : ""}. Report the smallest next step or ask for a decision.`, 160);
|
|
195
|
+
}
|
|
196
|
+
if (scope) return previewDisplayText(`Continue ${scope}. Report the smallest next step or ask for a decision.`, 160);
|
|
197
|
+
return "What are you blocked on? Reply with the smallest next step or ask for a decision.";
|
|
198
|
+
}
|
|
199
|
+
|
|
178
200
|
export function formatControlNoticeMessage(event: ControlEvent, childIntercomTarget?: string): string {
|
|
179
201
|
const runTarget = event.runId;
|
|
180
202
|
if (event.reason === "completion_guard") {
|
|
@@ -187,9 +209,9 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
|
|
|
187
209
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
188
210
|
}
|
|
189
211
|
|
|
190
|
-
const nudgeMessage =
|
|
191
|
-
const steerCommand = `subagent({ action: "steer", id: "${runTarget}", ${event.index !== undefined ? `index: ${event.index}, ` : ""}message:
|
|
192
|
-
const nestedResumeCommand = `subagent({ action: "resume", id: "${runTarget}", message:
|
|
212
|
+
const nudgeMessage = formatControlNudge(event);
|
|
213
|
+
const steerCommand = `subagent({ action: "steer", id: "${runTarget}", ${event.index !== undefined ? `index: ${event.index}, ` : ""}message: ${JSON.stringify(nudgeMessage)} })`;
|
|
214
|
+
const nestedResumeCommand = `subagent({ action: "resume", id: "${runTarget}", message: ${JSON.stringify(nudgeMessage)} })`;
|
|
193
215
|
if (event.type === "active_long_running") {
|
|
194
216
|
const facts = formatLongRunningFacts(event);
|
|
195
217
|
return [
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
|
|
6
|
+
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
6
7
|
import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, PERMISSION_POLICY_ENV } from "./permissions.ts";
|
|
7
8
|
import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
|
|
8
9
|
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
@@ -337,6 +338,7 @@ export function registerSteeringInbox(
|
|
|
337
338
|
nativeRealpath?: (filePath: string) => string;
|
|
338
339
|
legacySettleFallbackMs?: number;
|
|
339
340
|
safetyPollIntervalMs?: number;
|
|
341
|
+
platform?: NodeJS.Platform;
|
|
340
342
|
timers?: Pick<typeof globalThis, "setInterval" | "clearInterval">;
|
|
341
343
|
} = {},
|
|
342
344
|
): void {
|
|
@@ -449,13 +451,17 @@ export function registerSteeringInbox(
|
|
|
449
451
|
safetyInterval = (deps.timers?.setInterval ?? setInterval)(flush, deps.safetyPollIntervalMs ?? STEERING_SAFETY_POLL_INTERVAL_MS) as NodeJS.Timeout;
|
|
450
452
|
safetyInterval.unref?.();
|
|
451
453
|
};
|
|
452
|
-
|
|
453
|
-
watcher = (deps.watch ?? fs.watch)(resolveWatchPath(steerInbox, deps.nativeRealpath), () => flush());
|
|
454
|
-
watcher.on("error", startPolling);
|
|
455
|
-
startSafetyPolling();
|
|
456
|
-
} catch {
|
|
457
|
-
watcher = undefined;
|
|
454
|
+
if (!shouldUseNativeFsWatch("child-steering-inbox", deps.platform)) {
|
|
458
455
|
startPolling();
|
|
456
|
+
} else {
|
|
457
|
+
try {
|
|
458
|
+
watcher = (deps.watch ?? fs.watch)(resolveWatchPath(steerInbox, deps.nativeRealpath), () => flush());
|
|
459
|
+
watcher.on("error", startPolling);
|
|
460
|
+
startSafetyPolling();
|
|
461
|
+
} catch {
|
|
462
|
+
watcher = undefined;
|
|
463
|
+
startPolling();
|
|
464
|
+
}
|
|
459
465
|
}
|
|
460
466
|
};
|
|
461
467
|
const activate = (): undefined => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
|
|
2
2
|
import type { SingleResult, SubagentRunMode, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowNodeStatus } from "../../shared/types.ts";
|
|
3
3
|
|
|
4
4
|
export interface WorkflowGraphBuildInput {
|
|
@@ -81,28 +81,6 @@ export function buildWorkflowGraphSnapshot(input: WorkflowGraphBuildInput): Work
|
|
|
81
81
|
|
|
82
82
|
for (let stepIndex = 0; stepIndex < input.steps.length; stepIndex++) {
|
|
83
83
|
const step = input.steps[stepIndex]!;
|
|
84
|
-
if (isCheckpointStep(step)) {
|
|
85
|
-
const status = normalizeStatus(input.stepStatuses?.[flatIndex]?.status)
|
|
86
|
-
?? (input.currentStepIndex === stepIndex ? "paused" : "pending");
|
|
87
|
-
const checkpoint = { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: status === "rejected" ? "rejected" as const : status === "completed" ? "approved" as const : "pending" as const, stepIndex };
|
|
88
|
-
const id = `step-${stepIndex}`;
|
|
89
|
-
nodes.push({
|
|
90
|
-
id,
|
|
91
|
-
kind: "checkpoint",
|
|
92
|
-
phase: step.phase,
|
|
93
|
-
label: step.label?.trim() || step.checkpoint,
|
|
94
|
-
status,
|
|
95
|
-
flatIndex,
|
|
96
|
-
stepIndex,
|
|
97
|
-
checkpoint,
|
|
98
|
-
error: input.stepStatuses?.[flatIndex]?.error,
|
|
99
|
-
});
|
|
100
|
-
pushPhase(phases, step.phase, id);
|
|
101
|
-
if (status === "paused" || input.currentStepIndex === stepIndex) currentNodeId = id;
|
|
102
|
-
flatIndex++;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
84
|
if (isParallelStep(step)) {
|
|
107
85
|
const groupId = `step-${stepIndex}`;
|
|
108
86
|
const children: WorkflowGraphNode[] = [];
|
|
@@ -46,7 +46,7 @@ export interface WorktreeCleanupTask {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export type WorktreeCleanupIntent =
|
|
49
|
-
| { kind: "preserve"; capturedDiffs?: WorktreeDiff[]; handoffManifestPath?: string }
|
|
49
|
+
| { kind: "preserve"; capturedDiffs?: WorktreeDiff[]; handoffManifestPath?: string; cleanupBlocker?: string }
|
|
50
50
|
| {
|
|
51
51
|
kind: "discard";
|
|
52
52
|
authorization:
|
|
@@ -115,7 +115,7 @@ interface RepoState {
|
|
|
115
115
|
const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
|
|
116
116
|
|
|
117
117
|
function runGit(cwd: string, args: string[]): GitResult {
|
|
118
|
-
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
|
|
118
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", windowsHide: true });
|
|
119
119
|
return {
|
|
120
120
|
stdout: result.stdout ?? "",
|
|
121
121
|
stderr: result.stderr ?? "",
|
|
@@ -338,6 +338,7 @@ function runWorktreeSetupHook(
|
|
|
338
338
|
input: WorktreeSetupHookInput,
|
|
339
339
|
): string[] {
|
|
340
340
|
const result = spawnSync(hook.hookPath, [], {
|
|
341
|
+
windowsHide: true,
|
|
341
342
|
cwd: input.worktreePath,
|
|
342
343
|
encoding: "utf-8",
|
|
343
344
|
input: JSON.stringify(input),
|
|
@@ -571,6 +572,17 @@ function cleanupSingleWorktree(
|
|
|
571
572
|
const errors: string[] = [];
|
|
572
573
|
let worktreeRemoved = false;
|
|
573
574
|
let branchRemoved = false;
|
|
575
|
+
if (intent.kind === "preserve" && intent.cleanupBlocker) {
|
|
576
|
+
return {
|
|
577
|
+
index: worktree.index,
|
|
578
|
+
path: worktree.path,
|
|
579
|
+
branch: worktree.branch,
|
|
580
|
+
worktreeRemoved: false,
|
|
581
|
+
branchRemoved: false,
|
|
582
|
+
preserved: true,
|
|
583
|
+
reason: intent.cleanupBlocker,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
574
586
|
if (intent.kind !== "setup-rollback") {
|
|
575
587
|
try {
|
|
576
588
|
removeSyntheticPathsBeforeDiff(worktree);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, runFileSystemOperationWithRetry, waitForFileSystemRetry } from "./file-system-retry.ts";
|
|
4
5
|
|
|
5
6
|
type AtomicJsonFs = Pick<typeof fs, "mkdirSync" | "writeFileSync" | "renameSync" | "rmSync">;
|
|
6
7
|
|
|
8
|
+
const MAX_PATH_COMPONENT_BYTES = 255;
|
|
9
|
+
|
|
7
10
|
type AtomicJsonWriterOptions = {
|
|
8
11
|
fs?: AtomicJsonFs;
|
|
9
12
|
now?: () => number;
|
|
@@ -28,6 +31,13 @@ function renameWithRetry(
|
|
|
28
31
|
}, { retryDelaysMs, wait });
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
function tempBaseName(filePath: string, pid: number, nowMs: number, randomId: string): string {
|
|
35
|
+
const suffix = `.${pid}.${nowMs}.${randomId}.tmp`;
|
|
36
|
+
const preferred = `.${path.basename(filePath)}${suffix}`;
|
|
37
|
+
if (Buffer.byteLength(preferred, "utf-8") <= MAX_PATH_COMPONENT_BYTES) return preferred;
|
|
38
|
+
return `.${createHash("sha256").update(path.basename(filePath)).digest("hex")}${suffix}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (filePath: string, payload: object) => void {
|
|
32
42
|
const fsImpl = options.fs ?? fs;
|
|
33
43
|
const now = options.now ?? Date.now;
|
|
@@ -46,13 +56,23 @@ export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (
|
|
|
46
56
|
}, { retryDelaysMs: directoryRetryDelaysMs, wait });
|
|
47
57
|
const tempPath = path.join(
|
|
48
58
|
path.dirname(filePath),
|
|
49
|
-
|
|
59
|
+
tempBaseName(filePath, pid, now(), random().toString(36).slice(2)),
|
|
50
60
|
);
|
|
61
|
+
let writeError: unknown;
|
|
51
62
|
try {
|
|
52
63
|
fsImpl.writeFileSync(tempPath, JSON.stringify(payload, null, 2), mode === undefined ? "utf-8" : { encoding: "utf-8", mode });
|
|
53
64
|
renameWithRetry(fsImpl, tempPath, filePath, renameRetryDelaysMs, wait);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
writeError = error;
|
|
67
|
+
throw error;
|
|
54
68
|
} finally {
|
|
55
|
-
|
|
69
|
+
try {
|
|
70
|
+
fsImpl.rmSync(tempPath, { force: true });
|
|
71
|
+
} catch (cleanupError) {
|
|
72
|
+
// Preserve the write/rename failure: cleanup is best effort and must
|
|
73
|
+
// not hide the error callers need to classify or report.
|
|
74
|
+
if (writeError === undefined) throw cleanupError;
|
|
75
|
+
}
|
|
56
76
|
}
|
|
57
77
|
};
|
|
58
78
|
}
|