@ricsam/r5d-worker 0.0.123 → 0.0.125
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/README.md +1 -1
- package/dist/cjs/command-launcher.cjs +6 -2
- package/dist/cjs/main.cjs +496 -238
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-checkout-garbage.cjs +32 -3
- package/dist/cjs/project-workspace-state.cjs +51 -35
- package/dist/cjs/project-worktrees.cjs +79 -34
- package/dist/cjs/recovery-journal-protocol.cjs +56 -0
- package/dist/cjs/recovery-journal-runtime.cjs +133 -0
- package/dist/cjs/recovery-journal-thread.cjs +9 -0
- package/dist/cjs/recovery-journal.cjs +735 -0
- package/dist/cjs/recovery-store.cjs +8 -0
- package/dist/cjs/session-file-mutations.cjs +61 -0
- package/dist/cjs/working-tree-mirror.cjs +1 -0
- package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
- package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
- package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
- package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
- package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
- package/dist/cjs/workspace-git-sync.cjs +275 -201
- package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
- package/dist/mjs/command-launcher.mjs +6 -2
- package/dist/mjs/main.mjs +499 -244
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-checkout-garbage.mjs +30 -2
- package/dist/mjs/project-workspace-state.mjs +51 -35
- package/dist/mjs/project-worktrees.mjs +74 -34
- package/dist/mjs/recovery-journal-protocol.mjs +30 -0
- package/dist/mjs/recovery-journal-runtime.mjs +112 -0
- package/dist/mjs/recovery-journal-thread.mjs +8 -0
- package/dist/mjs/recovery-journal.mjs +687 -0
- package/dist/mjs/recovery-store.mjs +8 -0
- package/dist/mjs/session-file-mutations.mjs +37 -0
- package/dist/mjs/working-tree-mirror.mjs +1 -0
- package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
- package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
- package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
- package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
- package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
- package/dist/mjs/workspace-git-sync.mjs +264 -202
- package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
- package/dist/types/command-launcher.d.ts +2 -1
- package/dist/types/main.d.ts +30 -19
- package/dist/types/project-checkout-garbage.d.ts +19 -2
- package/dist/types/project-workspace-state.d.ts +9 -9
- package/dist/types/project-worktrees.d.ts +49 -5
- package/dist/types/recovery-journal-protocol.d.ts +35 -0
- package/dist/types/recovery-journal-runtime.d.ts +12 -0
- package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
- package/dist/types/recovery-journal-thread.d.ts +1 -0
- package/dist/types/recovery-journal.d.ts +246 -0
- package/dist/types/recovery-store.d.ts +6 -0
- package/dist/types/session-file-mutations.d.ts +22 -0
- package/dist/types/workspace-command-sync-policy.d.ts +22 -7
- package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
- package/dist/types/workspace-filesystem-executor.d.ts +123 -0
- package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
- package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
- package/dist/types/workspace-git-sync.d.ts +113 -7
- package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
- package/package.json +1 -1
- package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
- package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
- package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
package/dist/mjs/main.mjs
CHANGED
|
@@ -4,7 +4,9 @@ import { getWorkerPlanPath, getWorkerPlansPath, validatePlanId } from "./plan-pa
|
|
|
4
4
|
import { createPlanMarkdown, updateTaskCheckbox } from "./plan-parser.mjs";
|
|
5
5
|
import { lintPlans, formatPlanLintErrors } from "./plan-lint.mjs";
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
-
import {
|
|
7
|
+
import { WorkerRecoveryJournal, WorkerRecoveryJournalBusyError } from "./recovery-journal.mjs";
|
|
8
|
+
import { SessionFileMutationTracker } from "./session-file-mutations.mjs";
|
|
9
|
+
import { workerOperationDigest } from "./recovery-store.mjs";
|
|
8
10
|
import { WorkerOutputWindow } from "./recovery-output-window.mjs";
|
|
9
11
|
import {
|
|
10
12
|
createWorkerCommandLauncher,
|
|
@@ -58,9 +60,11 @@ import {
|
|
|
58
60
|
runWorkspaceCommand
|
|
59
61
|
} from "./workspace-command-sync-policy.mjs";
|
|
60
62
|
import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
|
|
63
|
+
import { WorkspaceMountHoldAbortedError, WorkspaceMountHoldFence } from "./workspace-mount-hold-fence.mjs";
|
|
64
|
+
import { installWorkspaceFilesystemExecutorFailureHandler, workspaceFilesystemExecutor } from "./workspace-filesystem-executor.mjs";
|
|
61
65
|
import { WorkspaceProjectionLedger } from "./workspace-projection-ledger.mjs";
|
|
62
66
|
import { WorkspaceSyncCoalescer } from "./workspace-sync-coalescer.mjs";
|
|
63
|
-
import { checkoutPathMovePublicationComplete
|
|
67
|
+
import { checkoutPathMovePublicationComplete } from "./workspace-path-move.mjs";
|
|
64
68
|
import { busyProjectConfigurationChangeIds, deferredProjectConfigurationPendingBranches } from "./workspace-project-config-policy.mjs";
|
|
65
69
|
import { creatorLocalProjectBranchIsAuthorized, workerProjectBranchDisposition } from "./workspace-preserve-only-policy.mjs";
|
|
66
70
|
import { assertProjectBranchDeletionIncarnation } from "./workspace-branch-incarnation-policy.mjs";
|
|
@@ -90,6 +94,7 @@ import {
|
|
|
90
94
|
deleteProjectMirrorBranch,
|
|
91
95
|
ensureProjectWorktrees,
|
|
92
96
|
inventoryProjectCheckouts,
|
|
97
|
+
PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
93
98
|
observeProjectMirrorHeads,
|
|
94
99
|
projectBranchMayExistAfterCreateFailure,
|
|
95
100
|
projectOriginBranchName,
|
|
@@ -102,11 +107,6 @@ import {
|
|
|
102
107
|
parseProjectMirrorRefsTokensMessage,
|
|
103
108
|
planProjectMirrorObservationFetch
|
|
104
109
|
} from "./project-mirror-fetch-policy.mjs";
|
|
105
|
-
import {
|
|
106
|
-
PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
|
|
107
|
-
recoverProjectSnapshotsInChild,
|
|
108
|
-
runProjectSnapshotRecoveryHelper
|
|
109
|
-
} from "./project-snapshot-recovery-runner.mjs";
|
|
110
110
|
import {
|
|
111
111
|
ProjectWorkspacePendingBranchPathChangeError,
|
|
112
112
|
ProjectWorkspaceStateStore,
|
|
@@ -135,7 +135,7 @@ function workerServerHeartbeatAction(input) {
|
|
|
135
135
|
return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
|
|
136
136
|
}
|
|
137
137
|
const WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX = "hydration:";
|
|
138
|
-
function createWorkspaceHydrationLeaseHooks(send, options
|
|
138
|
+
function createWorkspaceHydrationLeaseHooks(send, options) {
|
|
139
139
|
const yieldToEventLoop = options.yieldToEventLoop ?? (() => new Promise((resolve) => setImmediate(resolve)));
|
|
140
140
|
const log = options.log ?? ((line) => process.stdout.write(`${line}
|
|
141
141
|
`));
|
|
@@ -156,13 +156,17 @@ function createWorkspaceHydrationLeaseHooks(send, options = {}) {
|
|
|
156
156
|
log(
|
|
157
157
|
`[r5d-worker] released hydration lease ${context.transactionId} after ${startedAt === void 0 ? "?" : Math.round(now() - startedAt)}ms`
|
|
158
158
|
);
|
|
159
|
+
},
|
|
160
|
+
holdMounts(mountIds, description) {
|
|
161
|
+
return options.fence.hold(mountIds, description);
|
|
159
162
|
}
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
165
|
const workerLifecycleTestHarness = {
|
|
163
166
|
boundedWorkerLifecycleProgress,
|
|
164
167
|
workerServerHeartbeatAction,
|
|
165
|
-
createWorkspaceHydrationLeaseHooks
|
|
168
|
+
createWorkspaceHydrationLeaseHooks,
|
|
169
|
+
workerCommandHasWorkspaceEffect
|
|
166
170
|
};
|
|
167
171
|
class ProjectWorkspaceConfigurationDeferredError extends Error {
|
|
168
172
|
}
|
|
@@ -215,7 +219,7 @@ const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
|
|
|
215
219
|
let currentWorkerSocket = null;
|
|
216
220
|
const workerRuntimeId = crypto.randomUUID();
|
|
217
221
|
const workerOutputWindow = new WorkerOutputWindow();
|
|
218
|
-
let
|
|
222
|
+
let recoveryJournal;
|
|
219
223
|
let workerReconnectAttempt = 0;
|
|
220
224
|
let workerRecoveryId;
|
|
221
225
|
let workerExecutionLeaseDeadline = 0;
|
|
@@ -227,13 +231,13 @@ let runtimeExitPromise;
|
|
|
227
231
|
function fenceWorkerRuntimeOperations() {
|
|
228
232
|
launcherFailureInProgress = true;
|
|
229
233
|
workerAdmissionGeneration += 1;
|
|
230
|
-
for (const operation of
|
|
231
|
-
if (operation.state !== "completed")
|
|
234
|
+
for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
|
|
235
|
+
if (operation.state !== "completed") recoveryJournal?.unknown(operation.requestId);
|
|
232
236
|
}
|
|
233
237
|
for (const runId of workerCommandLauncher?.activeIds ?? []) {
|
|
234
238
|
infrastructureStoppedRunIds.add(runId);
|
|
235
239
|
cancelledProcessRuns.add(runId);
|
|
236
|
-
|
|
240
|
+
recoveryJournal?.unknownRun(runId);
|
|
237
241
|
}
|
|
238
242
|
for (const runId of activeProcesses.keys()) infrastructureStoppedRunIds.add(runId);
|
|
239
243
|
}
|
|
@@ -242,13 +246,21 @@ function exitWorkerRuntime(code) {
|
|
|
242
246
|
fenceWorkerRuntimeOperations();
|
|
243
247
|
runtimeExitPromise = (async () => {
|
|
244
248
|
await Promise.race([
|
|
245
|
-
Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close()]),
|
|
249
|
+
Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close(), recoveryJournal?.flush()]),
|
|
246
250
|
Bun.sleep(4e3)
|
|
247
251
|
]);
|
|
248
252
|
process.exit(code);
|
|
249
253
|
})();
|
|
250
254
|
return runtimeExitPromise;
|
|
251
255
|
}
|
|
256
|
+
function handleRecoveryJournalFailure(error) {
|
|
257
|
+
if (launcherFailureInProgress) return;
|
|
258
|
+
fenceWorkerRuntimeOperations();
|
|
259
|
+
process.stderr.write(`[r5d-worker] ${error.message}; stopping runtime for recovery journal restart
|
|
260
|
+
`);
|
|
261
|
+
currentWorkerSocket?.close(1012, "Recovery journal failure");
|
|
262
|
+
void exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
|
|
263
|
+
}
|
|
252
264
|
function handleWorkerCommandLauncherFailure(error) {
|
|
253
265
|
if (!workerCommandLauncher || launcherFailureInProgress) return;
|
|
254
266
|
fenceWorkerRuntimeOperations();
|
|
@@ -269,7 +281,7 @@ async function retryWorkerServerRead(read) {
|
|
|
269
281
|
try {
|
|
270
282
|
return await read();
|
|
271
283
|
} catch (error) {
|
|
272
|
-
if (!
|
|
284
|
+
if (!recoveryJournal || !isWorkerCommunicationFailure(error instanceof Error ? error.message : String(error))) throw error;
|
|
273
285
|
deadline ??= Date.now() + WORKER_RECOVERY_TIMEOUT_MS;
|
|
274
286
|
if (workerLeaseExpired || Date.now() >= deadline) throw new WorkerServerUnavailableError("Worker communication recovery expired");
|
|
275
287
|
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
@@ -291,9 +303,10 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
291
303
|
`);
|
|
292
304
|
});
|
|
293
305
|
closeAllPtys();
|
|
294
|
-
for (const operation of
|
|
306
|
+
for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
|
|
295
307
|
if (operation.sessionId && operation.state !== "completed") {
|
|
296
|
-
|
|
308
|
+
void recoveryJournal?.cancelSession(operation.sessionId);
|
|
309
|
+
abortPendingReservations("Execution lease expired before command start", operation.sessionId);
|
|
297
310
|
void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
|
|
298
311
|
process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
|
|
299
312
|
`);
|
|
@@ -303,7 +316,7 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
303
316
|
for (const [runId, active] of activeProcesses) {
|
|
304
317
|
if (!active.sessionId) continue;
|
|
305
318
|
infrastructureStoppedRunIds.add(runId);
|
|
306
|
-
|
|
319
|
+
recoveryJournal?.unknownRun(runId);
|
|
307
320
|
cancelledProcessRuns.add(runId);
|
|
308
321
|
closeProcessStdin(active);
|
|
309
322
|
void terminateProcessTree(active.process).catch(
|
|
@@ -318,6 +331,32 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
318
331
|
}
|
|
319
332
|
let workerAdmissionGeneration = 0;
|
|
320
333
|
const workspaceMutationGate = new WorkspaceMutationGate();
|
|
334
|
+
const workspaceMountHoldFence = new WorkspaceMountHoldFence();
|
|
335
|
+
const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
|
|
336
|
+
const pendingReservationAborts = /* @__PURE__ */ new Map();
|
|
337
|
+
const sessionFileMutations = new SessionFileMutationTracker();
|
|
338
|
+
function registerPendingReservation(key, sessionId) {
|
|
339
|
+
abortPendingReservation(key, "Reservation superseded by a newer request with the same id");
|
|
340
|
+
const controller = new AbortController();
|
|
341
|
+
pendingReservationAborts.set(key, { controller, sessionId });
|
|
342
|
+
return controller;
|
|
343
|
+
}
|
|
344
|
+
function unregisterPendingReservation(key, controller) {
|
|
345
|
+
if (pendingReservationAborts.get(key)?.controller === controller) pendingReservationAborts.delete(key);
|
|
346
|
+
}
|
|
347
|
+
function abortPendingReservation(key, reason) {
|
|
348
|
+
const pending = pendingReservationAborts.get(key);
|
|
349
|
+
if (!pending) return false;
|
|
350
|
+
pendingReservationAborts.delete(key);
|
|
351
|
+
pending.controller.abort(new WorkspaceMountHoldAbortedError(reason));
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
function abortPendingReservations(reason, sessionId) {
|
|
355
|
+
for (const [key, pending] of [...pendingReservationAborts]) {
|
|
356
|
+
if (sessionId !== void 0 && pending.sessionId !== sessionId) continue;
|
|
357
|
+
abortPendingReservation(key, reason);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
321
360
|
let workspaceSyncQueue = Promise.resolve();
|
|
322
361
|
let startupProjectSnapshotRecoveryCompleted = false;
|
|
323
362
|
const checkoutGarbageCollectors = /* @__PURE__ */ new Map();
|
|
@@ -352,6 +391,17 @@ const workspaceSyncSingleFlight = {
|
|
|
352
391
|
},
|
|
353
392
|
afterCurrent() {
|
|
354
393
|
return workspaceSyncQueue;
|
|
394
|
+
},
|
|
395
|
+
mountHold(target, signal) {
|
|
396
|
+
if (target.type !== "project") return null;
|
|
397
|
+
const mountIds = [projectMountId(target.projectId, target.branchName)];
|
|
398
|
+
if (!workspaceMountHoldFence.isHeld(mountIds)) return null;
|
|
399
|
+
const holder = workspaceMountHoldFence.describeHold(mountIds);
|
|
400
|
+
process.stdout.write(
|
|
401
|
+
`[r5d-worker] command for ${target.projectId}/${target.branchName} waits for ${holder ?? "a workspace filesystem job"}
|
|
402
|
+
`
|
|
403
|
+
);
|
|
404
|
+
return workspaceMountHoldFence.waitForRelease(mountIds, { signal });
|
|
355
405
|
}
|
|
356
406
|
};
|
|
357
407
|
let githubCredential = null;
|
|
@@ -1008,14 +1058,15 @@ async function runGitAsync(args, options = {}) {
|
|
|
1008
1058
|
env: workerGitProcessEnvironment(),
|
|
1009
1059
|
...bounded ? { detached: true } : {}
|
|
1010
1060
|
});
|
|
1011
|
-
const completion = Promise.all([
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
subprocess.exited
|
|
1015
|
-
]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode }));
|
|
1061
|
+
const completion = Promise.all([new Response(subprocess.stdout).text(), new Response(subprocess.stderr).text(), subprocess.exited]).then(
|
|
1062
|
+
([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode })
|
|
1063
|
+
);
|
|
1016
1064
|
let timer;
|
|
1017
1065
|
const timedOut = bounded ? new Promise((_resolve, reject) => {
|
|
1018
|
-
timer = setTimeout(
|
|
1066
|
+
timer = setTimeout(
|
|
1067
|
+
() => reject(new GitTransportTimeoutError(`git ${args.join(" ")} timed out after ${options.timeoutMs}ms`)),
|
|
1068
|
+
options.timeoutMs
|
|
1069
|
+
);
|
|
1019
1070
|
}) : null;
|
|
1020
1071
|
try {
|
|
1021
1072
|
const { stdout, stderr, exitCode } = timedOut ? await Promise.race([completion, timedOut]) : await completion;
|
|
@@ -2104,12 +2155,12 @@ async function streamCommandOutput(stream, onData) {
|
|
|
2104
2155
|
}
|
|
2105
2156
|
const text = decoder.decode(chunk.value, { stream: true });
|
|
2106
2157
|
if (text.length > 0) {
|
|
2107
|
-
onData(text);
|
|
2158
|
+
await onData(text);
|
|
2108
2159
|
}
|
|
2109
2160
|
}
|
|
2110
2161
|
const trailing = decoder.decode();
|
|
2111
2162
|
if (trailing.length > 0) {
|
|
2112
|
-
onData(trailing);
|
|
2163
|
+
await onData(trailing);
|
|
2113
2164
|
}
|
|
2114
2165
|
} finally {
|
|
2115
2166
|
reader.releaseLock();
|
|
@@ -2248,12 +2299,21 @@ function mutationQueueKey(target, resolved) {
|
|
|
2248
2299
|
}
|
|
2249
2300
|
return `${describeWorkerSessionTarget(target)}:${path.posix.normalize(resolved.repoRelativePath)}`;
|
|
2250
2301
|
}
|
|
2251
|
-
function
|
|
2302
|
+
async function statIfExists(targetPath) {
|
|
2303
|
+
try {
|
|
2304
|
+
return await fs.promises.stat(targetPath);
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
const code = error.code;
|
|
2307
|
+
if (code === "ENOENT" || code === "ENOTDIR") return null;
|
|
2308
|
+
throw error;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
async function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
|
|
2252
2312
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2253
|
-
if (!
|
|
2313
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2254
2314
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2255
2315
|
}
|
|
2256
|
-
const buffer = fs.
|
|
2316
|
+
const buffer = await fs.promises.readFile(resolved.absolutePath);
|
|
2257
2317
|
const formatted = formatLineNumberedContent(buffer.toString("utf8"), offset, limit);
|
|
2258
2318
|
return {
|
|
2259
2319
|
type: "read",
|
|
@@ -2263,25 +2323,25 @@ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
|
|
|
2263
2323
|
...formatted
|
|
2264
2324
|
};
|
|
2265
2325
|
}
|
|
2266
|
-
function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
|
|
2326
|
+
async function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
|
|
2267
2327
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2268
|
-
fs.
|
|
2269
|
-
fs.
|
|
2328
|
+
await fs.promises.mkdir(path.dirname(resolved.absolutePath), { recursive: true });
|
|
2329
|
+
await fs.promises.writeFile(resolved.absolutePath, content, "utf8");
|
|
2270
2330
|
return {
|
|
2271
2331
|
type: "write",
|
|
2272
2332
|
file: resolved.displayPath,
|
|
2273
2333
|
gitBlobHash: getGitBlobHashForContent(content)
|
|
2274
2334
|
};
|
|
2275
2335
|
}
|
|
2276
|
-
function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
2336
|
+
async function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
2277
2337
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2278
2338
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
2279
2339
|
throw new Error("edit requires at least one replacement");
|
|
2280
2340
|
}
|
|
2281
|
-
if (!
|
|
2341
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2282
2342
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2283
2343
|
}
|
|
2284
|
-
const originalContent = fs.
|
|
2344
|
+
const originalContent = await fs.promises.readFile(resolved.absolutePath, "utf8");
|
|
2285
2345
|
const ranges = edits.map((edit, index) => {
|
|
2286
2346
|
if (edit.oldText === edit.newText) {
|
|
2287
2347
|
throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
|
|
@@ -2313,7 +2373,7 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
|
2313
2373
|
for (const range of [...sortedRanges].reverse()) {
|
|
2314
2374
|
nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
|
|
2315
2375
|
}
|
|
2316
|
-
fs.
|
|
2376
|
+
await fs.promises.writeFile(resolved.absolutePath, nextContent, "utf8");
|
|
2317
2377
|
return {
|
|
2318
2378
|
type: "edit",
|
|
2319
2379
|
file: resolved.displayPath,
|
|
@@ -2346,7 +2406,7 @@ async function executePlanOperation(input) {
|
|
|
2346
2406
|
let created = false;
|
|
2347
2407
|
if (message.createIfMissing) {
|
|
2348
2408
|
try {
|
|
2349
|
-
fs.
|
|
2409
|
+
await fs.promises.writeFile(resolved.absolutePath, createPlanMarkdown(message.planId), { encoding: "utf8", flag: "wx" });
|
|
2350
2410
|
created = true;
|
|
2351
2411
|
} catch (error) {
|
|
2352
2412
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -2355,11 +2415,16 @@ async function executePlanOperation(input) {
|
|
|
2355
2415
|
return {
|
|
2356
2416
|
type: "read_plan",
|
|
2357
2417
|
created,
|
|
2358
|
-
read: readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2418
|
+
read: await readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2359
2419
|
};
|
|
2360
2420
|
}
|
|
2361
|
-
const content = fs.
|
|
2362
|
-
writeWorkerTextFile(
|
|
2421
|
+
const content = await fs.promises.readFile(resolved.absolutePath, "utf8");
|
|
2422
|
+
await writeWorkerTextFile(
|
|
2423
|
+
resolvedTarget.rootPath,
|
|
2424
|
+
filePath,
|
|
2425
|
+
updateTaskCheckbox(content, message.taskId, message.checked),
|
|
2426
|
+
builtInPaths
|
|
2427
|
+
);
|
|
2363
2428
|
return {
|
|
2364
2429
|
type: "update_task_status",
|
|
2365
2430
|
planId: message.planId,
|
|
@@ -2380,7 +2445,11 @@ async function executeReadFileOperation(input) {
|
|
|
2380
2445
|
rootDir: input.rootDir,
|
|
2381
2446
|
access: "read"
|
|
2382
2447
|
});
|
|
2383
|
-
|
|
2448
|
+
const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
|
|
2449
|
+
return withFileMutationQueue(
|
|
2450
|
+
mutationQueueKey(input.resolvedTarget.target, resolved),
|
|
2451
|
+
() => readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths)
|
|
2452
|
+
);
|
|
2384
2453
|
}
|
|
2385
2454
|
async function executeWriteFileOperation(input) {
|
|
2386
2455
|
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
@@ -2433,16 +2502,16 @@ function wildcardToRegex(pattern) {
|
|
|
2433
2502
|
function normalizeFindPattern(pattern) {
|
|
2434
2503
|
return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
|
|
2435
2504
|
}
|
|
2436
|
-
function walkWorkerEntries(branchPath, start) {
|
|
2505
|
+
async function walkWorkerEntries(branchPath, start) {
|
|
2437
2506
|
const entries = [];
|
|
2438
2507
|
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2439
|
-
const realVirtualRoot = start.scope === "virtual" ? fs.
|
|
2440
|
-
const visit = (absolutePath, isRoot) => {
|
|
2508
|
+
const realVirtualRoot = start.scope === "virtual" ? await fs.promises.realpath(start.virtualRootPath) : null;
|
|
2509
|
+
const visit = async (absolutePath, isRoot) => {
|
|
2441
2510
|
let stat;
|
|
2442
2511
|
try {
|
|
2443
|
-
stat = fs.
|
|
2512
|
+
stat = await fs.promises.stat(absolutePath);
|
|
2444
2513
|
if (realVirtualRoot) {
|
|
2445
|
-
assertInsideRoot(realVirtualRoot, fs.
|
|
2514
|
+
assertInsideRoot(realVirtualRoot, await fs.promises.realpath(absolutePath), `${start.displayPath} path`);
|
|
2446
2515
|
}
|
|
2447
2516
|
} catch (error) {
|
|
2448
2517
|
if (isRoot) throw error;
|
|
@@ -2455,7 +2524,7 @@ function walkWorkerEntries(branchPath, start) {
|
|
|
2455
2524
|
if (!stat.isDirectory()) return;
|
|
2456
2525
|
let realDirectoryPath;
|
|
2457
2526
|
try {
|
|
2458
|
-
realDirectoryPath = fs.
|
|
2527
|
+
realDirectoryPath = await fs.promises.realpath(absolutePath);
|
|
2459
2528
|
} catch (error) {
|
|
2460
2529
|
if (isRoot) throw error;
|
|
2461
2530
|
return;
|
|
@@ -2464,25 +2533,25 @@ function walkWorkerEntries(branchPath, start) {
|
|
|
2464
2533
|
visitedDirectories.add(realDirectoryPath);
|
|
2465
2534
|
let children;
|
|
2466
2535
|
try {
|
|
2467
|
-
children = fs.
|
|
2536
|
+
children = (await fs.promises.readdir(absolutePath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2468
2537
|
} catch (error) {
|
|
2469
2538
|
if (isRoot) throw error;
|
|
2470
2539
|
return;
|
|
2471
2540
|
}
|
|
2472
2541
|
for (const child of children) {
|
|
2473
2542
|
if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
|
|
2474
|
-
visit(path.join(absolutePath, child.name), false);
|
|
2543
|
+
await visit(path.join(absolutePath, child.name), false);
|
|
2475
2544
|
}
|
|
2476
2545
|
};
|
|
2477
|
-
visit(start.absolutePath, true);
|
|
2546
|
+
await visit(start.absolutePath, true);
|
|
2478
2547
|
return entries;
|
|
2479
2548
|
}
|
|
2480
2549
|
function isProbablyText(bytes) {
|
|
2481
2550
|
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
2482
2551
|
}
|
|
2483
|
-
function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
2552
|
+
async function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
2484
2553
|
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
2485
|
-
if (!
|
|
2554
|
+
if (!await statIfExists(start.absolutePath)) {
|
|
2486
2555
|
throw new Error(`Path not found: ${start.displayPath}`);
|
|
2487
2556
|
}
|
|
2488
2557
|
const limit = normalizeToolLimit(input.limit, 100, 1e3);
|
|
@@ -2492,12 +2561,12 @@ function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
|
2492
2561
|
const lines = [];
|
|
2493
2562
|
let matchCount = 0;
|
|
2494
2563
|
let truncated = false;
|
|
2495
|
-
for (const entry of walkWorkerEntries(branchPath, start)) {
|
|
2564
|
+
for (const entry of await walkWorkerEntries(branchPath, start)) {
|
|
2496
2565
|
if (!entry.stat.isFile()) continue;
|
|
2497
2566
|
if (globRegex && !globRegex.test(entry.matchPath)) continue;
|
|
2498
2567
|
let bytes;
|
|
2499
2568
|
try {
|
|
2500
|
-
bytes = fs.
|
|
2569
|
+
bytes = await fs.promises.readFile(entry.absolutePath);
|
|
2501
2570
|
} catch {
|
|
2502
2571
|
continue;
|
|
2503
2572
|
}
|
|
@@ -2550,9 +2619,9 @@ async function executeGrepOperation(input) {
|
|
|
2550
2619
|
builtInPaths
|
|
2551
2620
|
);
|
|
2552
2621
|
}
|
|
2553
|
-
function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
2622
|
+
async function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
2554
2623
|
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
2555
|
-
if (!
|
|
2624
|
+
if (!(await statIfExists(start.absolutePath))?.isDirectory()) {
|
|
2556
2625
|
throw new Error(`Directory not found: ${start.displayPath}`);
|
|
2557
2626
|
}
|
|
2558
2627
|
const pattern = normalizeFindPattern(input.pattern ?? "*");
|
|
@@ -2562,7 +2631,7 @@ function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
|
2562
2631
|
const matches = [];
|
|
2563
2632
|
let count = 0;
|
|
2564
2633
|
let truncated = false;
|
|
2565
|
-
for (const entry of walkWorkerEntries(branchPath, start)) {
|
|
2634
|
+
for (const entry of await walkWorkerEntries(branchPath, start)) {
|
|
2566
2635
|
if (entry.absolutePath === start.absolutePath) continue;
|
|
2567
2636
|
const isDirectory = entry.stat.isDirectory();
|
|
2568
2637
|
if (entryType === "file" && !entry.stat.isFile()) continue;
|
|
@@ -2607,13 +2676,13 @@ async function executeFindOperation(input) {
|
|
|
2607
2676
|
builtInPaths
|
|
2608
2677
|
);
|
|
2609
2678
|
}
|
|
2610
|
-
function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
|
|
2679
|
+
async function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
|
|
2611
2680
|
const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
|
|
2612
|
-
if (!
|
|
2681
|
+
if (!(await statIfExists(resolved.absolutePath))?.isDirectory()) {
|
|
2613
2682
|
throw new Error(`Directory not found: ${resolved.displayPath}`);
|
|
2614
2683
|
}
|
|
2615
2684
|
const limit = normalizeToolLimit(inputLimit, 200, 1e3);
|
|
2616
|
-
const entries = fs.
|
|
2685
|
+
const entries = (await fs.promises.readdir(resolved.absolutePath, { withFileTypes: true })).filter((entry) => !IGNORED_ENTRY_NAMES.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
2617
2686
|
const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
2618
2687
|
const truncated = entries.length > displayed.length;
|
|
2619
2688
|
const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
|
|
@@ -2664,36 +2733,40 @@ function resolveWorkerCodePath(branchPath, inputPath) {
|
|
|
2664
2733
|
}
|
|
2665
2734
|
return { absolutePath, displayPath };
|
|
2666
2735
|
}
|
|
2667
|
-
function listWorkerCodeDirectory(branchPath, inputPath) {
|
|
2736
|
+
async function listWorkerCodeDirectory(branchPath, inputPath) {
|
|
2668
2737
|
const resolved = resolveWorkerCodePath(branchPath, inputPath);
|
|
2669
|
-
if (!fs.
|
|
2738
|
+
if (!(await fs.promises.stat(resolved.absolutePath)).isDirectory()) {
|
|
2670
2739
|
throw new Error(`Code directory not found: ${resolved.displayPath}`);
|
|
2671
2740
|
}
|
|
2672
|
-
const
|
|
2741
|
+
const listed = (await fs.promises.readdir(resolved.absolutePath, { withFileTypes: true })).filter(
|
|
2742
|
+
(entry) => entry.name.toLowerCase() !== ".git"
|
|
2743
|
+
);
|
|
2744
|
+
const entries = [];
|
|
2745
|
+
for (const entry of listed) {
|
|
2673
2746
|
const entryPath = path.posix.join(resolved.displayPath, entry.name);
|
|
2674
2747
|
try {
|
|
2675
2748
|
const resolvedEntry = resolveWorkerCodePath(branchPath, entryPath);
|
|
2676
|
-
const stats = fs.
|
|
2677
|
-
if (!stats.isFile() && !stats.isDirectory())
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
}
|
|
2685
|
-
];
|
|
2749
|
+
const stats = await fs.promises.stat(resolvedEntry.absolutePath);
|
|
2750
|
+
if (!stats.isFile() && !stats.isDirectory()) continue;
|
|
2751
|
+
entries.push({
|
|
2752
|
+
name: entry.name,
|
|
2753
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
2754
|
+
size: stats.isDirectory() ? null : stats.size,
|
|
2755
|
+
mtime: stats.mtime.toISOString()
|
|
2756
|
+
});
|
|
2686
2757
|
} catch {
|
|
2687
|
-
return [];
|
|
2688
2758
|
}
|
|
2689
|
-
}
|
|
2759
|
+
}
|
|
2760
|
+
entries.sort(
|
|
2761
|
+
(left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name)
|
|
2762
|
+
);
|
|
2690
2763
|
return { type: "code_list", path: resolved.displayPath, entries };
|
|
2691
2764
|
}
|
|
2692
|
-
function readWorkerCodeFile(branchPath, inputPath) {
|
|
2765
|
+
async function readWorkerCodeFile(branchPath, inputPath) {
|
|
2693
2766
|
const resolved = resolveWorkerCodePath(branchPath, inputPath);
|
|
2694
|
-
const stats = fs.
|
|
2767
|
+
const stats = await fs.promises.stat(resolved.absolutePath);
|
|
2695
2768
|
if (!stats.isFile()) throw new Error(`Code file not found: ${resolved.displayPath}`);
|
|
2696
|
-
const bytes = fs.
|
|
2769
|
+
const bytes = await fs.promises.readFile(resolved.absolutePath);
|
|
2697
2770
|
return {
|
|
2698
2771
|
type: "code_read",
|
|
2699
2772
|
path: resolved.displayPath,
|
|
@@ -2702,17 +2775,17 @@ function readWorkerCodeFile(branchPath, inputPath) {
|
|
|
2702
2775
|
base64: bytes.toString("base64")
|
|
2703
2776
|
};
|
|
2704
2777
|
}
|
|
2705
|
-
function executeCodeListOperation(input) {
|
|
2778
|
+
async function executeCodeListOperation(input) {
|
|
2706
2779
|
if (input.resolvedTarget.target.type !== "project") throw new Error("Code listing requires a project branch target");
|
|
2707
2780
|
return listWorkerCodeDirectory(input.resolvedTarget.rootPath, input.message.path);
|
|
2708
2781
|
}
|
|
2709
|
-
function executeCodeReadOperation(input) {
|
|
2782
|
+
async function executeCodeReadOperation(input) {
|
|
2710
2783
|
if (input.resolvedTarget.target.type !== "project") throw new Error("Code reading requires a project branch target");
|
|
2711
2784
|
return readWorkerCodeFile(input.resolvedTarget.rootPath, input.message.path);
|
|
2712
2785
|
}
|
|
2713
|
-
function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
2786
|
+
async function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
2714
2787
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2715
|
-
if (!
|
|
2788
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2716
2789
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2717
2790
|
}
|
|
2718
2791
|
const extension = path.extname(resolved.absolutePath).toLowerCase();
|
|
@@ -2720,7 +2793,7 @@ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
|
2720
2793
|
if (!mediaType) {
|
|
2721
2794
|
throw new Error("read supports PNG and JPEG image inputs only");
|
|
2722
2795
|
}
|
|
2723
|
-
const bytes = fs.
|
|
2796
|
+
const bytes = await fs.promises.readFile(resolved.absolutePath);
|
|
2724
2797
|
const dimensions = readImageDimensions(bytes, mediaType);
|
|
2725
2798
|
return {
|
|
2726
2799
|
type: "view_file_bytes",
|
|
@@ -3047,26 +3120,28 @@ async function executeStreamingCommand(input) {
|
|
|
3047
3120
|
}
|
|
3048
3121
|
const [exitCode] = await Promise.all([
|
|
3049
3122
|
subprocess.exited,
|
|
3050
|
-
streamCommandOutput(
|
|
3051
|
-
|
|
3123
|
+
streamCommandOutput(
|
|
3124
|
+
subprocess.stdout,
|
|
3125
|
+
(data) => sendWorkerOutput(input.ws, {
|
|
3052
3126
|
type: "exec_output",
|
|
3053
3127
|
runId: input.message.runId,
|
|
3054
3128
|
stream: "stdout",
|
|
3055
3129
|
data
|
|
3056
|
-
})
|
|
3057
|
-
|
|
3058
|
-
streamCommandOutput(
|
|
3059
|
-
|
|
3130
|
+
})
|
|
3131
|
+
),
|
|
3132
|
+
streamCommandOutput(
|
|
3133
|
+
subprocess.stderr,
|
|
3134
|
+
(data) => sendWorkerOutput(input.ws, {
|
|
3060
3135
|
type: "exec_output",
|
|
3061
3136
|
runId: input.message.runId,
|
|
3062
3137
|
stream: "stderr",
|
|
3063
3138
|
data
|
|
3064
|
-
})
|
|
3065
|
-
|
|
3139
|
+
})
|
|
3140
|
+
)
|
|
3066
3141
|
]);
|
|
3067
3142
|
const diagnostic = await input.resources.diagnose();
|
|
3068
3143
|
if (diagnostic) {
|
|
3069
|
-
|
|
3144
|
+
await sendWorkerOutput(input.ws, {
|
|
3070
3145
|
type: "exec_output",
|
|
3071
3146
|
runId: input.message.runId,
|
|
3072
3147
|
stream: "stderr",
|
|
@@ -3138,6 +3213,39 @@ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
|
|
|
3138
3213
|
);
|
|
3139
3214
|
}
|
|
3140
3215
|
}
|
|
3216
|
+
async function cancelProcessRun(runId, scope) {
|
|
3217
|
+
const spawned = activeProcesses.get(runId);
|
|
3218
|
+
if (scope === "unstarted" && spawned) return { cancelled: false, message: `${spawned.command} already started`, outcome: "started" };
|
|
3219
|
+
if (!spawned) cancelledProcessRuns.add(runId);
|
|
3220
|
+
const revocation = workerCommandLauncher?.cancel(runId);
|
|
3221
|
+
let resourceCancellationError;
|
|
3222
|
+
let launchRevoked = false;
|
|
3223
|
+
try {
|
|
3224
|
+
launchRevoked = await revocation ?? false;
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
resourceCancellationError = error instanceof Error ? error.message : String(error);
|
|
3227
|
+
}
|
|
3228
|
+
let cancelled = resourceCancellationError === void 0;
|
|
3229
|
+
let cancelMessage;
|
|
3230
|
+
let outcome;
|
|
3231
|
+
if (spawned) {
|
|
3232
|
+
outcome = "stopped";
|
|
3233
|
+
try {
|
|
3234
|
+
closeProcessStdin(spawned);
|
|
3235
|
+
await terminateProcessTree(spawned.process);
|
|
3236
|
+
cancelMessage = `Stopped ${spawned.command}`;
|
|
3237
|
+
} catch (error) {
|
|
3238
|
+
cancelled = false;
|
|
3239
|
+
cancelMessage = `Failed to stop ${spawned.command}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3240
|
+
}
|
|
3241
|
+
} else {
|
|
3242
|
+
const reservationAborted = abortPendingReservation(`run:${runId}`, "Cancellation queued before command start");
|
|
3243
|
+
outcome = launchRevoked || reservationAborted ? "unstarted" : "unknown";
|
|
3244
|
+
cancelMessage = "Cancellation queued before command start";
|
|
3245
|
+
}
|
|
3246
|
+
if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
|
|
3247
|
+
return { cancelled, message: cancelMessage, outcome };
|
|
3248
|
+
}
|
|
3141
3249
|
function closeProcessStdin(active) {
|
|
3142
3250
|
if (!active?.stdin) {
|
|
3143
3251
|
return;
|
|
@@ -3163,21 +3271,35 @@ function sendReplayWorkerMessage(ws, message) {
|
|
|
3163
3271
|
}
|
|
3164
3272
|
function sendWorkerMessage(ws, message) {
|
|
3165
3273
|
if (launcherFailureInProgress) return;
|
|
3166
|
-
if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
|
|
3167
|
-
return;
|
|
3168
3274
|
const transportError = "error" in message && typeof message.error === "string" ? message.error : message.type === "workspace_sync_result" && message.result.outcome === "failed" ? message.result.error : void 0;
|
|
3169
|
-
if (
|
|
3170
|
-
|
|
3275
|
+
if (recoveryJournal && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
|
|
3276
|
+
recoveryJournal.unknown(message.requestId);
|
|
3171
3277
|
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
3172
3278
|
return;
|
|
3173
3279
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
recoveryStore?.unknownRun(message.runId);
|
|
3280
|
+
if (!recoveryJournal) {
|
|
3281
|
+
sendReplayWorkerMessage(ws, message);
|
|
3177
3282
|
return;
|
|
3178
3283
|
}
|
|
3179
|
-
|
|
3180
|
-
|
|
3284
|
+
recoveryJournal.send(message);
|
|
3285
|
+
}
|
|
3286
|
+
function sendWorkerOutput(ws, frame) {
|
|
3287
|
+
if (launcherFailureInProgress) return Promise.resolve();
|
|
3288
|
+
if (!recoveryJournal) {
|
|
3289
|
+
sendReplayWorkerMessage(ws, frame);
|
|
3290
|
+
return Promise.resolve();
|
|
3291
|
+
}
|
|
3292
|
+
return recoveryJournal.sendOutput(frame);
|
|
3293
|
+
}
|
|
3294
|
+
function deliverJournaledWorkerMessage(message) {
|
|
3295
|
+
const target = currentWorkerSocket;
|
|
3296
|
+
if ((message.type === "exec_exit" || message.type === "exec_error") && typeof message.runId === "string" && infrastructureStoppedRunIds.has(message.runId)) {
|
|
3297
|
+
recoveryJournal?.unknownRun(message.runId);
|
|
3298
|
+
return true;
|
|
3299
|
+
}
|
|
3300
|
+
if (!target || target.readyState !== WebSocket.OPEN) return false;
|
|
3301
|
+
if (message.type === "exec_output" && workerRecoveryId !== void 0) return false;
|
|
3302
|
+
return sendReplayWorkerMessage(target, message);
|
|
3181
3303
|
}
|
|
3182
3304
|
function sendSerializedWorkerMessage(ws, serialized) {
|
|
3183
3305
|
sendWorkerMessage(ws, JSON.parse(serialized));
|
|
@@ -3740,9 +3862,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
3740
3862
|
`);
|
|
3741
3863
|
}
|
|
3742
3864
|
const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3865
|
+
if (!recoveryJournal) {
|
|
3866
|
+
const journal = await WorkerRecoveryJournal.open({
|
|
3867
|
+
filename: path.join(
|
|
3868
|
+
rootDir,
|
|
3869
|
+
".worker-recovery",
|
|
3870
|
+
createHash("sha256").update(`${baseUrl}\0${label}`).digest("hex"),
|
|
3871
|
+
"protocol.sqlite"
|
|
3872
|
+
)
|
|
3873
|
+
});
|
|
3874
|
+
journal.deliver = deliverJournaledWorkerMessage;
|
|
3875
|
+
journal.onFailure = handleRecoveryJournalFailure;
|
|
3876
|
+
recoveryJournal = journal;
|
|
3877
|
+
}
|
|
3746
3878
|
const projectsRoot = path.join(rootDir, "projects");
|
|
3747
3879
|
const syncRoot = path.join(rootDir, "sync");
|
|
3748
3880
|
const workspaceShadowRoot = path.join(syncRoot, "workspace");
|
|
@@ -3845,7 +3977,6 @@ async function startWorker(options, projectRuntime = {
|
|
|
3845
3977
|
const bearerAuthHeader = `Authorization: Bearer ${token}`;
|
|
3846
3978
|
const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
|
|
3847
3979
|
const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
3848
|
-
const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
|
|
3849
3980
|
const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
|
|
3850
3981
|
const workspaceProjectRelativePath = (projectId, branchName) => path.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
|
|
3851
3982
|
const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
|
|
@@ -3928,7 +4059,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
3928
4059
|
sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
|
|
3929
4060
|
planSourcePath: path.join(planRoot, project.projectId, ...branchName.split("/"))
|
|
3930
4061
|
}));
|
|
3931
|
-
removeProjectWorktrees({ projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
|
|
4062
|
+
const removed = removeProjectWorktrees({ projectsRoot, projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
|
|
4063
|
+
if (removed.stagedForCollectionPath) checkoutGarbageCollector.enqueue(removed.stagedForCollectionPath);
|
|
3932
4064
|
for (const branch of branches) {
|
|
3933
4065
|
fs.rmSync(branch.planSourcePath, { recursive: true, force: true });
|
|
3934
4066
|
const key = projectBranchKey(project.projectId, branch.branchName);
|
|
@@ -4016,14 +4148,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
4016
4148
|
)
|
|
4017
4149
|
};
|
|
4018
4150
|
};
|
|
4019
|
-
const stageDurableWorkspaceDeletions = () => {
|
|
4151
|
+
const stageDurableWorkspaceDeletions = async () => {
|
|
4020
4152
|
for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
|
|
4021
4153
|
const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
|
|
4022
4154
|
if (deletion.kind === "project") {
|
|
4023
4155
|
if (!deletion.projectDeleted) {
|
|
4024
4156
|
const nextProject = projectConfigById.get(deletion.projectId);
|
|
4025
4157
|
if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
|
|
4026
|
-
|
|
4158
|
+
await workspaceFilesystemExecutor().run("project_checkout_path_move", {
|
|
4027
4159
|
oldProjectRoot: deletion.managedPath,
|
|
4028
4160
|
newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
|
|
4029
4161
|
projectsDurabilityRoot: projectsRoot,
|
|
@@ -4390,7 +4522,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4390
4522
|
}
|
|
4391
4523
|
}
|
|
4392
4524
|
if (transitionReseedRequired) {
|
|
4393
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
|
|
4525
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordEnabledRepositoryTransition({
|
|
4394
4526
|
projectId: project.projectId,
|
|
4395
4527
|
transitionId: project.repositoryTransitionId
|
|
4396
4528
|
});
|
|
@@ -4559,7 +4691,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4559
4691
|
)
|
|
4560
4692
|
];
|
|
4561
4693
|
if (publishedTombstoneIds.length > 0) {
|
|
4562
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
4694
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
|
|
4563
4695
|
tombstoneIds: publishedTombstoneIds,
|
|
4564
4696
|
publishedHead
|
|
4565
4697
|
});
|
|
@@ -4578,7 +4710,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4578
4710
|
forgetLocalProjectMirrorRefsTokens(deletion.projectId, deletion.branchName);
|
|
4579
4711
|
}
|
|
4580
4712
|
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
4581
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
4713
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
4582
4714
|
tombstoneId: deletion.tombstoneId,
|
|
4583
4715
|
branchName: deletion.branchName
|
|
4584
4716
|
});
|
|
@@ -4775,7 +4907,10 @@ async function startWorker(options, projectRuntime = {
|
|
|
4775
4907
|
};
|
|
4776
4908
|
};
|
|
4777
4909
|
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4778
|
-
(message) => sendWorkerMessageFromCurrentSource(ws, message)
|
|
4910
|
+
(message) => sendWorkerMessageFromCurrentSource(ws, message),
|
|
4911
|
+
{
|
|
4912
|
+
fence: workspaceMountHoldFence
|
|
4913
|
+
}
|
|
4779
4914
|
);
|
|
4780
4915
|
const performWorkspaceSync = async (input) => {
|
|
4781
4916
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
@@ -5433,7 +5568,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5433
5568
|
(project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
|
|
5434
5569
|
);
|
|
5435
5570
|
if (!incidentDeferral.incidentId) {
|
|
5436
|
-
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
5571
|
+
projectWorkspaceState = await projectWorkspaceStateStore.reconcile({
|
|
5437
5572
|
desiredProjects: message.projects,
|
|
5438
5573
|
preserveOnlyBranches
|
|
5439
5574
|
});
|
|
@@ -5444,7 +5579,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5444
5579
|
(candidate) => candidate.projectId === project.projectId && candidate.branchName === branch.branchName
|
|
5445
5580
|
);
|
|
5446
5581
|
if (pending2 && pending2.branchId === branch.branchId) {
|
|
5447
|
-
projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
|
|
5582
|
+
projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
|
|
5448
5583
|
}
|
|
5449
5584
|
}
|
|
5450
5585
|
}
|
|
@@ -5612,7 +5747,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5612
5747
|
for (const projectId of [...readyProjectIds]) {
|
|
5613
5748
|
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
5614
5749
|
}
|
|
5615
|
-
stageDurableWorkspaceDeletions();
|
|
5750
|
+
await stageDurableWorkspaceDeletions();
|
|
5616
5751
|
const mountsBeforeGit = buildWorkspaceMounts();
|
|
5617
5752
|
await hydrateWorkspaceGitMounts(
|
|
5618
5753
|
workspaceShadowRoot,
|
|
@@ -5658,7 +5793,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
5658
5793
|
const connectSyncMs = Date.now() - connectSyncStartedAtMs;
|
|
5659
5794
|
const result = syncResult.telemetry ? syncResult : {
|
|
5660
5795
|
...syncResult,
|
|
5661
|
-
telemetry: {
|
|
5796
|
+
telemetry: {
|
|
5797
|
+
totalMs: connectSyncMs,
|
|
5798
|
+
queueMs: 0,
|
|
5799
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
5800
|
+
synchronizeMs: connectSyncMs
|
|
5801
|
+
}
|
|
5662
5802
|
};
|
|
5663
5803
|
assertConfigurationSyncStillAdmitted();
|
|
5664
5804
|
if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
|
|
@@ -5677,7 +5817,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5677
5817
|
) ? [deletion.tombstoneId] : []
|
|
5678
5818
|
);
|
|
5679
5819
|
if (layoutCleanupIds.length > 0) {
|
|
5680
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
5820
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
|
|
5681
5821
|
tombstoneIds: layoutCleanupIds,
|
|
5682
5822
|
publishedHead
|
|
5683
5823
|
});
|
|
@@ -5784,26 +5924,33 @@ async function startWorker(options, projectRuntime = {
|
|
|
5784
5924
|
if (startupProjectSnapshotRecoveryCompleted) return;
|
|
5785
5925
|
sendLifecycleProgress({ phase: "recovering", operationId, detail: "Checking interrupted workspace transactions" });
|
|
5786
5926
|
const snapshotRecovery = await workspaceSyncSingleFlight.runExclusive(
|
|
5787
|
-
async () => await
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5927
|
+
async () => await workspaceFilesystemExecutor().run(
|
|
5928
|
+
"project_snapshots_recover",
|
|
5929
|
+
{
|
|
5930
|
+
projectsRoot,
|
|
5931
|
+
temporaryRoot: projectWorktreeSnapshotsRoot,
|
|
5932
|
+
currentProcessId: process.pid,
|
|
5933
|
+
currentOwnerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
|
|
5934
|
+
},
|
|
5935
|
+
{
|
|
5936
|
+
onProgress: (progress) => {
|
|
5937
|
+
const lifecycleProgress = {
|
|
5938
|
+
phase: "recovering",
|
|
5939
|
+
operationId,
|
|
5940
|
+
detail: describeSnapshotProgress(progress),
|
|
5941
|
+
completedBytes: progress.completedBytes,
|
|
5942
|
+
completedItems: progress.completedBranches,
|
|
5943
|
+
totalItems: progress.totalBranches,
|
|
5944
|
+
...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
|
|
5945
|
+
};
|
|
5946
|
+
sendLifecycleProgress(lifecycleProgress);
|
|
5947
|
+
process.stdout.write(
|
|
5948
|
+
`[r5d-worker] ${lifecycleProgress.detail}: ${progress.completedBranches}/${progress.totalBranches} branch(es), ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes
|
|
5803
5949
|
`
|
|
5804
|
-
|
|
5950
|
+
);
|
|
5951
|
+
}
|
|
5805
5952
|
}
|
|
5806
|
-
|
|
5953
|
+
)
|
|
5807
5954
|
);
|
|
5808
5955
|
if (snapshotRecovery.restored.length > 0) {
|
|
5809
5956
|
process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
|
|
@@ -5898,7 +6045,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5898
6045
|
type: "hello",
|
|
5899
6046
|
resumableProtocol: WORKER_RESUMABLE_PROTOCOL,
|
|
5900
6047
|
runtimeId: workerRuntimeId,
|
|
5901
|
-
ledgerId:
|
|
6048
|
+
ledgerId: recoveryJournal.ledgerId,
|
|
5902
6049
|
startupReady: startupRecoveryComplete,
|
|
5903
6050
|
hostInfo: {
|
|
5904
6051
|
hostname: hostname(),
|
|
@@ -5926,16 +6073,39 @@ async function startWorker(options, projectRuntime = {
|
|
|
5926
6073
|
}
|
|
5927
6074
|
};
|
|
5928
6075
|
sendSerializedWorkerMessage(ws, JSON.stringify(hello));
|
|
6076
|
+
let outputPumpInFlight = false;
|
|
5929
6077
|
outputReplayTimer = setInterval(() => {
|
|
5930
|
-
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
6078
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws || outputPumpInFlight || !recoveryJournal) return;
|
|
6079
|
+
outputPumpInFlight = true;
|
|
6080
|
+
void (async () => {
|
|
6081
|
+
try {
|
|
6082
|
+
for await (const page of recoveryJournal.outputReplay(void 0)) {
|
|
6083
|
+
for (const output of page) {
|
|
6084
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws || !sendReplayWorkerMessage(ws, output)) return;
|
|
6085
|
+
}
|
|
6086
|
+
}
|
|
6087
|
+
} catch (error) {
|
|
6088
|
+
process.stderr.write(`[r5d-worker] output replay pump failed: ${error instanceof Error ? error.message : String(error)}
|
|
6089
|
+
`);
|
|
6090
|
+
} finally {
|
|
6091
|
+
outputPumpInFlight = false;
|
|
6092
|
+
}
|
|
6093
|
+
})();
|
|
5934
6094
|
}, 250);
|
|
5935
6095
|
outputReplayTimer.unref();
|
|
6096
|
+
let terminalPumpInFlight = false;
|
|
5936
6097
|
terminalReplayTimer = setInterval(() => {
|
|
5937
|
-
if (workerRecoveryId !== void 0) return;
|
|
5938
|
-
|
|
6098
|
+
if (workerRecoveryId !== void 0 || terminalPumpInFlight || !recoveryJournal) return;
|
|
6099
|
+
terminalPumpInFlight = true;
|
|
6100
|
+
void recoveryJournal.terminals().then((terminals) => {
|
|
6101
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
|
|
6102
|
+
for (const terminal of terminals) sendReplayWorkerMessage(ws, terminal);
|
|
6103
|
+
}).catch((error) => {
|
|
6104
|
+
process.stderr.write(`[r5d-worker] terminal replay pump failed: ${error instanceof Error ? error.message : String(error)}
|
|
6105
|
+
`);
|
|
6106
|
+
}).finally(() => {
|
|
6107
|
+
terminalPumpInFlight = false;
|
|
6108
|
+
});
|
|
5939
6109
|
}, 5e3);
|
|
5940
6110
|
terminalReplayTimer.unref();
|
|
5941
6111
|
process.stdout.write("[r5d-worker] connected\n");
|
|
@@ -5948,28 +6118,37 @@ async function startWorker(options, projectRuntime = {
|
|
|
5948
6118
|
if (message.type === "recovery_request") {
|
|
5949
6119
|
workerRecoveryId = message.recoveryId;
|
|
5950
6120
|
sendActiveProcessReport(ws);
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
6121
|
+
recoveryJournal.pauseDelivery();
|
|
6122
|
+
try {
|
|
6123
|
+
const state = await recoveryJournal.recoveryState(message.requests);
|
|
6124
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
6125
|
+
sendWorkerMessage(ws, {
|
|
6126
|
+
type: "recovery_state",
|
|
6127
|
+
recoveryId: message.recoveryId,
|
|
6128
|
+
runtimeId: workerRuntimeId,
|
|
6129
|
+
ledgerId: recoveryJournal.ledgerId,
|
|
6130
|
+
operations: state.operations
|
|
6131
|
+
});
|
|
6132
|
+
const cursors = new Map(state.cursors.map((cursor) => [cursor.runId, cursor]));
|
|
6133
|
+
for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
|
|
6134
|
+
workerOutputWindow.reset([...cursors.values()]);
|
|
6135
|
+
for (const response of state.responses) sendReplayWorkerMessage(ws, response);
|
|
6136
|
+
for await (const page of recoveryJournal.outputReplay(message.outputOffsets)) {
|
|
6137
|
+
for (const output of page) {
|
|
6138
|
+
while (true) {
|
|
6139
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
|
|
6140
|
+
return;
|
|
6141
|
+
if (sendReplayWorkerMessage(ws, output)) break;
|
|
6142
|
+
await Bun.sleep(10);
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
5968
6145
|
}
|
|
6146
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
6147
|
+
for (const terminal of state.terminals) sendReplayWorkerMessage(ws, terminal);
|
|
6148
|
+
sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
|
|
6149
|
+
} finally {
|
|
6150
|
+
recoveryJournal.resumeDelivery();
|
|
5969
6151
|
}
|
|
5970
|
-
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
5971
|
-
for (const terminal of recoveryStore.terminals()) sendReplayWorkerMessage(ws, terminal);
|
|
5972
|
-
sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
|
|
5973
6152
|
return;
|
|
5974
6153
|
}
|
|
5975
6154
|
if (message.type === "recovery_ready") {
|
|
@@ -5984,17 +6163,23 @@ async function startWorker(options, projectRuntime = {
|
|
|
5984
6163
|
return;
|
|
5985
6164
|
}
|
|
5986
6165
|
if (message.type === "operation_result_ack") {
|
|
5987
|
-
|
|
6166
|
+
recoveryJournal.acknowledgeResult(message.requestId);
|
|
5988
6167
|
return;
|
|
5989
6168
|
}
|
|
5990
6169
|
if (message.type === "exec_output_ack") {
|
|
5991
|
-
|
|
6170
|
+
recoveryJournal.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
|
|
5992
6171
|
workerOutputWindow.acknowledge(message.runId, message.stream, message.nextOffset);
|
|
5993
6172
|
return;
|
|
5994
6173
|
}
|
|
5995
6174
|
if (message.type === "cancel_session") {
|
|
5996
|
-
|
|
6175
|
+
const durableCancellation = recoveryJournal.cancelSession(message.sessionId);
|
|
6176
|
+
abortPendingReservations("Session cancelled before command start", message.sessionId);
|
|
5997
6177
|
await Promise.all([
|
|
6178
|
+
durableCancellation,
|
|
6179
|
+
// File writes already in flight for the session settle before the
|
|
6180
|
+
// acknowledgement; queued ones re-check the cancellation before
|
|
6181
|
+
// their first write and never start.
|
|
6182
|
+
sessionFileMutations.settled(message.sessionId),
|
|
5998
6183
|
workerCommandLauncher?.cancelSession(message.sessionId),
|
|
5999
6184
|
...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
6000
6185
|
cancelledProcessRuns.add(runId);
|
|
@@ -6009,27 +6194,75 @@ async function startWorker(options, projectRuntime = {
|
|
|
6009
6194
|
if (!startupRecoveryComplete && message.type !== "ping") return;
|
|
6010
6195
|
if (currentWorkerSocket !== ws) return;
|
|
6011
6196
|
const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
|
|
6197
|
+
if (message.type === "cancel") {
|
|
6198
|
+
const admission = recoveryJournal.admit({ ...message, requestId: message.requestId });
|
|
6199
|
+
const cancelled = await cancelProcessRun(message.runId, message.scope);
|
|
6200
|
+
let admitted;
|
|
6201
|
+
try {
|
|
6202
|
+
admitted = await admission;
|
|
6203
|
+
} catch (error) {
|
|
6204
|
+
if (!(error instanceof WorkerRecoveryJournalBusyError)) throw error;
|
|
6205
|
+
process.stderr.write(`[r5d-worker] cancel ${message.runId} stopped the run but was not durably admitted: ${error.message}
|
|
6206
|
+
`);
|
|
6207
|
+
return;
|
|
6208
|
+
}
|
|
6209
|
+
if (admitted.admission === "conflict") {
|
|
6210
|
+
ws.close(1008, "Operation identity conflict");
|
|
6211
|
+
return;
|
|
6212
|
+
}
|
|
6213
|
+
sendWorkerMessage(ws, { type: "operation_received", requestId: message.requestId });
|
|
6214
|
+
if (admitted.admission !== "new") {
|
|
6215
|
+
if (admitted.response && admitted.admission !== "unknown") sendReplayWorkerMessage(ws, admitted.response);
|
|
6216
|
+
return;
|
|
6217
|
+
}
|
|
6218
|
+
sendSerializedWorkerMessage(
|
|
6219
|
+
ws,
|
|
6220
|
+
JSON.stringify({
|
|
6221
|
+
type: "cancel_result",
|
|
6222
|
+
requestId: message.requestId,
|
|
6223
|
+
runId: message.runId,
|
|
6224
|
+
cancelled: cancelled.cancelled,
|
|
6225
|
+
message: cancelled.message,
|
|
6226
|
+
outcome: cancelled.outcome
|
|
6227
|
+
})
|
|
6228
|
+
);
|
|
6229
|
+
return;
|
|
6230
|
+
}
|
|
6012
6231
|
if (operationRequestId) {
|
|
6013
|
-
|
|
6014
|
-
|
|
6232
|
+
let admitted;
|
|
6233
|
+
try {
|
|
6234
|
+
admitted = await recoveryJournal.admit({ ...message, requestId: operationRequestId });
|
|
6235
|
+
} catch (error) {
|
|
6236
|
+
if (!(error instanceof WorkerRecoveryJournalBusyError)) throw error;
|
|
6237
|
+
process.stderr.write(`[r5d-worker] ${message.type} ${operationRequestId} was not admitted: ${error.message}
|
|
6238
|
+
`);
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6241
|
+
if (admitted.admission === "conflict") {
|
|
6015
6242
|
ws.close(1008, "Operation identity conflict");
|
|
6016
6243
|
return;
|
|
6017
6244
|
}
|
|
6018
6245
|
sendWorkerMessage(ws, { type: "operation_received", requestId: operationRequestId });
|
|
6019
|
-
if (admission !== "new") {
|
|
6020
|
-
|
|
6021
|
-
if (
|
|
6022
|
-
|
|
6246
|
+
if (admitted.admission !== "new") {
|
|
6247
|
+
if (admitted.response && admitted.admission !== "unknown") sendReplayWorkerMessage(ws, admitted.response);
|
|
6248
|
+
if (!(admitted.admission === "unknown" && message.type === "delete_project_branch" && await recoveryJournal.readmitUnknownBranchDeletion(
|
|
6249
|
+
operationRequestId,
|
|
6250
|
+
"sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0
|
|
6251
|
+
))) {
|
|
6023
6252
|
return;
|
|
6024
6253
|
}
|
|
6025
6254
|
}
|
|
6255
|
+
if (recoveryJournal.isRequestCancelled(operationRequestId)) {
|
|
6256
|
+
recoveryJournal.unknown(operationRequestId);
|
|
6257
|
+
return;
|
|
6258
|
+
}
|
|
6026
6259
|
}
|
|
6027
6260
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
6028
6261
|
const admissionCredentials = configuredCredentialGenerationFingerprint;
|
|
6029
6262
|
const assertMessageAdmission = () => {
|
|
6030
6263
|
if (operationRequestId) {
|
|
6031
|
-
if (workerLeaseExpired ||
|
|
6032
|
-
|
|
6264
|
+
if (workerLeaseExpired || recoveryJournal.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
|
|
6265
|
+
recoveryJournal.unknown(operationRequestId);
|
|
6033
6266
|
throw new StaleWorkerAdmissionError();
|
|
6034
6267
|
}
|
|
6035
6268
|
return;
|
|
@@ -6166,7 +6399,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6166
6399
|
}
|
|
6167
6400
|
recoveryDeadline ??= Date.now() + WORKER_RECOVERY_TIMEOUT_MS;
|
|
6168
6401
|
if (workerLeaseExpired || Date.now() >= recoveryDeadline) {
|
|
6169
|
-
|
|
6402
|
+
recoveryJournal.unknown(message.requestId);
|
|
6170
6403
|
return;
|
|
6171
6404
|
}
|
|
6172
6405
|
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
@@ -6196,7 +6429,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6196
6429
|
projectId: message.projectId,
|
|
6197
6430
|
branchName: message.targetBranch
|
|
6198
6431
|
};
|
|
6199
|
-
const created = await workspaceSyncSingleFlight.runBranchMutation(() => {
|
|
6432
|
+
const created = await workspaceSyncSingleFlight.runBranchMutation(async () => {
|
|
6200
6433
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6201
6434
|
throw new Error(
|
|
6202
6435
|
`Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
|
|
@@ -6211,7 +6444,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6211
6444
|
const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
|
|
6212
6445
|
({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
|
|
6213
6446
|
);
|
|
6214
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
|
|
6447
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
|
|
6215
6448
|
let gitBranchCreated = false;
|
|
6216
6449
|
try {
|
|
6217
6450
|
if (!readyProjectIds.has(project.projectId)) {
|
|
@@ -6232,7 +6465,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6232
6465
|
}
|
|
6233
6466
|
return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
|
|
6234
6467
|
}
|
|
6235
|
-
const createdBranch = createOrRetryLinkedProjectBranch({
|
|
6468
|
+
const createdBranch = await createOrRetryLinkedProjectBranch({
|
|
6236
6469
|
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
6237
6470
|
primaryBranchName: primaryProjectBranch(project),
|
|
6238
6471
|
sourceBranchName: message.sourceBranch,
|
|
@@ -6257,7 +6490,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6257
6490
|
return createdBranch;
|
|
6258
6491
|
} catch (error) {
|
|
6259
6492
|
if (!pendingWasRecorded && !gitBranchCreated && !projectBranchMayExistAfterCreateFailure(error)) {
|
|
6260
|
-
projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
|
|
6493
|
+
projectWorkspaceState = await projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
|
|
6261
6494
|
}
|
|
6262
6495
|
throw error;
|
|
6263
6496
|
}
|
|
@@ -6301,7 +6534,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6301
6534
|
});
|
|
6302
6535
|
try {
|
|
6303
6536
|
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
6304
|
-
const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(() => {
|
|
6537
|
+
const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(async () => {
|
|
6305
6538
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6306
6539
|
throw new Error(
|
|
6307
6540
|
`Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
|
|
@@ -6328,12 +6561,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
6328
6561
|
if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
|
|
6329
6562
|
return false;
|
|
6330
6563
|
}
|
|
6331
|
-
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
6564
|
+
projectWorkspaceState = await projectWorkspaceStateStore.beginBranchDeletion({
|
|
6332
6565
|
projectId: message.projectId,
|
|
6333
6566
|
branchName: message.branchName
|
|
6334
6567
|
});
|
|
6335
6568
|
if (pendingLocalBranch) {
|
|
6336
|
-
projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
|
|
6569
|
+
projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
|
|
6337
6570
|
}
|
|
6338
6571
|
pendingCreatedBranchPublicationNotBefore.delete(pendingCreatedBranchKey(message.projectId, message.branchName));
|
|
6339
6572
|
if (pendingMirrorDeletes.has(deletionKey)) return true;
|
|
@@ -6437,7 +6670,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6437
6670
|
}
|
|
6438
6671
|
if (message.type === "exec_terminal_ack") {
|
|
6439
6672
|
pendingProcessTerminals.delete(message.runId);
|
|
6440
|
-
|
|
6673
|
+
recoveryJournal.acknowledgeTerminal(message.runId);
|
|
6441
6674
|
return;
|
|
6442
6675
|
}
|
|
6443
6676
|
if (message.type === "port_forward_connect") {
|
|
@@ -6492,42 +6725,6 @@ async function startWorker(options, projectRuntime = {
|
|
|
6492
6725
|
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
|
|
6493
6726
|
return;
|
|
6494
6727
|
}
|
|
6495
|
-
if (message.type === "cancel") {
|
|
6496
|
-
let resourceCancellationError;
|
|
6497
|
-
try {
|
|
6498
|
-
await workerCommandLauncher?.cancel(message.runId);
|
|
6499
|
-
} catch (error) {
|
|
6500
|
-
resourceCancellationError = error instanceof Error ? error.message : String(error);
|
|
6501
|
-
}
|
|
6502
|
-
const active = activeProcesses.get(message.runId);
|
|
6503
|
-
let cancelled = resourceCancellationError === void 0;
|
|
6504
|
-
let cancelMessage;
|
|
6505
|
-
if (active) {
|
|
6506
|
-
try {
|
|
6507
|
-
closeProcessStdin(active);
|
|
6508
|
-
await terminateProcessTree(active.process);
|
|
6509
|
-
cancelMessage = `Stopped ${active.command}`;
|
|
6510
|
-
} catch (error) {
|
|
6511
|
-
cancelled = false;
|
|
6512
|
-
cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
|
|
6513
|
-
}
|
|
6514
|
-
} else {
|
|
6515
|
-
cancelledProcessRuns.add(message.runId);
|
|
6516
|
-
cancelMessage = "Cancellation queued before command start";
|
|
6517
|
-
}
|
|
6518
|
-
if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
|
|
6519
|
-
sendSerializedWorkerMessage(
|
|
6520
|
-
ws,
|
|
6521
|
-
JSON.stringify({
|
|
6522
|
-
type: "cancel_result",
|
|
6523
|
-
requestId: message.requestId,
|
|
6524
|
-
runId: message.runId,
|
|
6525
|
-
cancelled,
|
|
6526
|
-
message: cancelMessage
|
|
6527
|
-
})
|
|
6528
|
-
);
|
|
6529
|
-
return;
|
|
6530
|
-
}
|
|
6531
6728
|
if (message.type === "exec_stdin") {
|
|
6532
6729
|
const sendAck = (payload) => sendSerializedWorkerMessage(
|
|
6533
6730
|
ws,
|
|
@@ -6608,10 +6805,20 @@ async function startWorker(options, projectRuntime = {
|
|
|
6608
6805
|
let mutationLeaseTransferred = false;
|
|
6609
6806
|
try {
|
|
6610
6807
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6808
|
+
const reservationAbort = registerPendingReservation(`pty:${message.ptyId}`, void 0);
|
|
6809
|
+
try {
|
|
6810
|
+
await reserveWorkspaceCommandAfterCurrentSync(
|
|
6811
|
+
message.target,
|
|
6812
|
+
workspaceSyncSingleFlight,
|
|
6813
|
+
() => {
|
|
6814
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
6815
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6816
|
+
},
|
|
6817
|
+
{ signal: reservationAbort.signal }
|
|
6818
|
+
);
|
|
6819
|
+
} finally {
|
|
6820
|
+
unregisterPendingReservation(`pty:${message.ptyId}`, reservationAbort);
|
|
6821
|
+
}
|
|
6615
6822
|
releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
|
|
6616
6823
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6617
6824
|
throw new Error(
|
|
@@ -6676,6 +6883,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6676
6883
|
}
|
|
6677
6884
|
if (message.type === "pty_close") {
|
|
6678
6885
|
const activePty = activePtys.get(message.ptyId);
|
|
6886
|
+
abortPendingReservation(`pty:${message.ptyId}`, "Shell closed before it started");
|
|
6679
6887
|
await closePty(message);
|
|
6680
6888
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6681
6889
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
@@ -6712,11 +6920,21 @@ async function startWorker(options, projectRuntime = {
|
|
|
6712
6920
|
try {
|
|
6713
6921
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6714
6922
|
if (hasWorkspaceEffect) {
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6923
|
+
const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
|
|
6924
|
+
try {
|
|
6925
|
+
await reserveWorkspaceCommandAfterCurrentSync(
|
|
6926
|
+
message.target,
|
|
6927
|
+
workspaceSyncSingleFlight,
|
|
6928
|
+
() => {
|
|
6929
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
6930
|
+
targetReserved = true;
|
|
6931
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6932
|
+
},
|
|
6933
|
+
{ signal: reservationAbort.signal }
|
|
6934
|
+
);
|
|
6935
|
+
} finally {
|
|
6936
|
+
unregisterPendingReservation(`run:${message.runId}`, reservationAbort);
|
|
6937
|
+
}
|
|
6720
6938
|
}
|
|
6721
6939
|
const runCommand = async () => {
|
|
6722
6940
|
if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
|
|
@@ -6768,7 +6986,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6768
6986
|
cancelledProcessRuns.delete(message.runId);
|
|
6769
6987
|
}
|
|
6770
6988
|
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6771
|
-
|
|
6989
|
+
recoveryJournal.unknown(message.requestId);
|
|
6772
6990
|
return;
|
|
6773
6991
|
}
|
|
6774
6992
|
sendSerializedWorkerMessage(ws, JSON.stringify(result));
|
|
@@ -6842,11 +7060,21 @@ async function startWorker(options, projectRuntime = {
|
|
|
6842
7060
|
try {
|
|
6843
7061
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6844
7062
|
if (hasWorkspaceEffect) {
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
7063
|
+
const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
|
|
7064
|
+
try {
|
|
7065
|
+
await reserveWorkspaceCommandAfterCurrentSync(
|
|
7066
|
+
message.target,
|
|
7067
|
+
workspaceSyncSingleFlight,
|
|
7068
|
+
() => {
|
|
7069
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
7070
|
+
targetReserved = true;
|
|
7071
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
7072
|
+
},
|
|
7073
|
+
{ signal: reservationAbort.signal }
|
|
7074
|
+
);
|
|
7075
|
+
} finally {
|
|
7076
|
+
unregisterPendingReservation(`run:${message.runId}`, reservationAbort);
|
|
7077
|
+
}
|
|
6850
7078
|
}
|
|
6851
7079
|
await runReservedWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand, () => {
|
|
6852
7080
|
if (targetReserved) {
|
|
@@ -6881,16 +7109,40 @@ async function startWorker(options, projectRuntime = {
|
|
|
6881
7109
|
return;
|
|
6882
7110
|
}
|
|
6883
7111
|
const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
|
|
6884
|
-
const
|
|
7112
|
+
const mutatesFiles = message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing;
|
|
7113
|
+
const mutatesVisibleWorkspace = mutatesFiles && targetMayMutateVisibleWorkspace(message.target);
|
|
6885
7114
|
if (reservesVisibleWorkspace) {
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
7115
|
+
const reservationAbort = registerPendingReservation(
|
|
7116
|
+
`op:${message.requestId}`,
|
|
7117
|
+
"sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0
|
|
7118
|
+
);
|
|
7119
|
+
try {
|
|
7120
|
+
await reserveWorkspaceCommandAfterCurrentSync(
|
|
7121
|
+
message.target,
|
|
7122
|
+
workspaceSyncSingleFlight,
|
|
7123
|
+
() => {
|
|
7124
|
+
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
7125
|
+
if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
|
|
7126
|
+
},
|
|
7127
|
+
{ signal: reservationAbort.signal }
|
|
7128
|
+
);
|
|
7129
|
+
} catch (error) {
|
|
7130
|
+
sendSerializedWorkerMessage(
|
|
7131
|
+
ws,
|
|
7132
|
+
JSON.stringify({
|
|
7133
|
+
type: "operation_result",
|
|
7134
|
+
requestId: message.requestId,
|
|
7135
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7136
|
+
})
|
|
7137
|
+
);
|
|
7138
|
+
return;
|
|
7139
|
+
} finally {
|
|
7140
|
+
unregisterPendingReservation(`op:${message.requestId}`, reservationAbort);
|
|
7141
|
+
}
|
|
6890
7142
|
}
|
|
6891
7143
|
let dirtyTrigger;
|
|
6892
7144
|
try {
|
|
6893
|
-
const
|
|
7145
|
+
const operation = runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
|
|
6894
7146
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
6895
7147
|
return await executeOperation({
|
|
6896
7148
|
message,
|
|
@@ -6901,6 +7153,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6901
7153
|
assertAdmission: assertMessageAdmission
|
|
6902
7154
|
});
|
|
6903
7155
|
});
|
|
7156
|
+
if (mutatesFiles && typeof message.sessionId === "string") sessionFileMutations.track(message.sessionId, operation);
|
|
7157
|
+
const result = await operation;
|
|
6904
7158
|
sendSerializedWorkerMessage(
|
|
6905
7159
|
ws,
|
|
6906
7160
|
JSON.stringify({
|
|
@@ -6984,6 +7238,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6984
7238
|
projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
|
|
6985
7239
|
}
|
|
6986
7240
|
if (currentWorkerSocket === ws) currentWorkerSocket = null;
|
|
7241
|
+
abortPendingReservations("Worker connection closed before command start");
|
|
6987
7242
|
workerRecoveryId = "reconnecting";
|
|
6988
7243
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6989
7244
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
@@ -7017,6 +7272,11 @@ async function startWorker(options, projectRuntime = {
|
|
|
7017
7272
|
});
|
|
7018
7273
|
}
|
|
7019
7274
|
function installWorkerRuntimeFailureLogging() {
|
|
7275
|
+
installWorkspaceFilesystemExecutorFailureHandler((error) => {
|
|
7276
|
+
process.stderr.write(`[r5d-worker] ${error.message}; restarting the worker runtime
|
|
7277
|
+
`);
|
|
7278
|
+
void exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
|
|
7279
|
+
});
|
|
7020
7280
|
process.on("uncaughtException", (error) => {
|
|
7021
7281
|
process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
7022
7282
|
`);
|
|
@@ -7029,11 +7289,6 @@ function installWorkerRuntimeFailureLogging() {
|
|
|
7029
7289
|
});
|
|
7030
7290
|
}
|
|
7031
7291
|
async function main() {
|
|
7032
|
-
if (process.argv[2] === PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND) {
|
|
7033
|
-
const exitCode2 = runProjectSnapshotRecoveryHelper(process.argv.slice(3));
|
|
7034
|
-
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
7035
|
-
return;
|
|
7036
|
-
}
|
|
7037
7292
|
const parsed = parseArgs(process.argv.slice(2));
|
|
7038
7293
|
if (parsed.command === "help") {
|
|
7039
7294
|
printHelp();
|