@ricsam/r5d-worker 0.0.123 → 0.0.124
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/dist/cjs/main.cjs +488 -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/main.mjs +491 -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/main.d.ts +24 -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/cjs/main.cjs
CHANGED
|
@@ -61,6 +61,8 @@ var import_plan_paths = require("./plan-paths.cjs");
|
|
|
61
61
|
var import_plan_parser = require("./plan-parser.cjs");
|
|
62
62
|
var import_plan_lint = require("./plan-lint.cjs");
|
|
63
63
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
64
|
+
var import_recovery_journal = require("./recovery-journal.cjs");
|
|
65
|
+
var import_session_file_mutations = require("./session-file-mutations.cjs");
|
|
64
66
|
var import_recovery_store = require("./recovery-store.cjs");
|
|
65
67
|
var import_recovery_output_window = require("./recovery-output-window.cjs");
|
|
66
68
|
var import_command_launcher = require("./command-launcher.cjs");
|
|
@@ -83,6 +85,8 @@ var import_workspace_incident_state = require("./workspace-incident-state.cjs");
|
|
|
83
85
|
var import_workspace_automatic_sync_policy = require("./workspace-automatic-sync-policy.cjs");
|
|
84
86
|
var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
|
|
85
87
|
var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
|
|
88
|
+
var import_workspace_mount_hold_fence = require("./workspace-mount-hold-fence.cjs");
|
|
89
|
+
var import_workspace_filesystem_executor = require("./workspace-filesystem-executor.cjs");
|
|
86
90
|
var import_workspace_projection_ledger = require("./workspace-projection-ledger.cjs");
|
|
87
91
|
var import_workspace_sync_coalescer = require("./workspace-sync-coalescer.cjs");
|
|
88
92
|
var import_workspace_path_move = require("./workspace-path-move.cjs");
|
|
@@ -95,7 +99,6 @@ var import_supervisor = require("./supervisor.cjs");
|
|
|
95
99
|
var import_managed_paths = require("./managed-paths.cjs");
|
|
96
100
|
var import_project_worktrees = require("./project-worktrees.cjs");
|
|
97
101
|
var import_project_mirror_fetch_policy = require("./project-mirror-fetch-policy.cjs");
|
|
98
|
-
var import_project_snapshot_recovery_runner = require("./project-snapshot-recovery-runner.cjs");
|
|
99
102
|
var import_project_workspace_state = require("./project-workspace-state.cjs");
|
|
100
103
|
var import_workspace_git_sync = require("./workspace-git-sync.cjs");
|
|
101
104
|
const import_meta = {};
|
|
@@ -111,7 +114,7 @@ function workerServerHeartbeatAction(input) {
|
|
|
111
114
|
return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
|
|
112
115
|
}
|
|
113
116
|
const WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX = "hydration:";
|
|
114
|
-
function createWorkspaceHydrationLeaseHooks(send, options
|
|
117
|
+
function createWorkspaceHydrationLeaseHooks(send, options) {
|
|
115
118
|
const yieldToEventLoop = options.yieldToEventLoop ?? (() => new Promise((resolve) => setImmediate(resolve)));
|
|
116
119
|
const log = options.log ?? ((line) => process.stdout.write(`${line}
|
|
117
120
|
`));
|
|
@@ -132,13 +135,17 @@ function createWorkspaceHydrationLeaseHooks(send, options = {}) {
|
|
|
132
135
|
log(
|
|
133
136
|
`[r5d-worker] released hydration lease ${context.transactionId} after ${startedAt === void 0 ? "?" : Math.round(now() - startedAt)}ms`
|
|
134
137
|
);
|
|
138
|
+
},
|
|
139
|
+
holdMounts(mountIds, description) {
|
|
140
|
+
return options.fence.hold(mountIds, description);
|
|
135
141
|
}
|
|
136
142
|
};
|
|
137
143
|
}
|
|
138
144
|
const workerLifecycleTestHarness = {
|
|
139
145
|
boundedWorkerLifecycleProgress,
|
|
140
146
|
workerServerHeartbeatAction,
|
|
141
|
-
createWorkspaceHydrationLeaseHooks
|
|
147
|
+
createWorkspaceHydrationLeaseHooks,
|
|
148
|
+
workerCommandHasWorkspaceEffect
|
|
142
149
|
};
|
|
143
150
|
class ProjectWorkspaceConfigurationDeferredError extends Error {
|
|
144
151
|
}
|
|
@@ -191,7 +198,7 @@ const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
|
|
|
191
198
|
let currentWorkerSocket = null;
|
|
192
199
|
const workerRuntimeId = crypto.randomUUID();
|
|
193
200
|
const workerOutputWindow = new import_recovery_output_window.WorkerOutputWindow();
|
|
194
|
-
let
|
|
201
|
+
let recoveryJournal;
|
|
195
202
|
let workerReconnectAttempt = 0;
|
|
196
203
|
let workerRecoveryId;
|
|
197
204
|
let workerExecutionLeaseDeadline = 0;
|
|
@@ -203,13 +210,13 @@ let runtimeExitPromise;
|
|
|
203
210
|
function fenceWorkerRuntimeOperations() {
|
|
204
211
|
launcherFailureInProgress = true;
|
|
205
212
|
workerAdmissionGeneration += 1;
|
|
206
|
-
for (const operation of
|
|
207
|
-
if (operation.state !== "completed")
|
|
213
|
+
for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
|
|
214
|
+
if (operation.state !== "completed") recoveryJournal?.unknown(operation.requestId);
|
|
208
215
|
}
|
|
209
216
|
for (const runId of workerCommandLauncher?.activeIds ?? []) {
|
|
210
217
|
infrastructureStoppedRunIds.add(runId);
|
|
211
218
|
cancelledProcessRuns.add(runId);
|
|
212
|
-
|
|
219
|
+
recoveryJournal?.unknownRun(runId);
|
|
213
220
|
}
|
|
214
221
|
for (const runId of activeProcesses.keys()) infrastructureStoppedRunIds.add(runId);
|
|
215
222
|
}
|
|
@@ -218,13 +225,21 @@ function exitWorkerRuntime(code) {
|
|
|
218
225
|
fenceWorkerRuntimeOperations();
|
|
219
226
|
runtimeExitPromise = (async () => {
|
|
220
227
|
await Promise.race([
|
|
221
|
-
Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close()]),
|
|
228
|
+
Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close(), recoveryJournal?.flush()]),
|
|
222
229
|
Bun.sleep(4e3)
|
|
223
230
|
]);
|
|
224
231
|
process.exit(code);
|
|
225
232
|
})();
|
|
226
233
|
return runtimeExitPromise;
|
|
227
234
|
}
|
|
235
|
+
function handleRecoveryJournalFailure(error) {
|
|
236
|
+
if (launcherFailureInProgress) return;
|
|
237
|
+
fenceWorkerRuntimeOperations();
|
|
238
|
+
process.stderr.write(`[r5d-worker] ${error.message}; stopping runtime for recovery journal restart
|
|
239
|
+
`);
|
|
240
|
+
currentWorkerSocket?.close(1012, "Recovery journal failure");
|
|
241
|
+
void exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
|
|
242
|
+
}
|
|
228
243
|
function handleWorkerCommandLauncherFailure(error) {
|
|
229
244
|
if (!workerCommandLauncher || launcherFailureInProgress) return;
|
|
230
245
|
fenceWorkerRuntimeOperations();
|
|
@@ -245,7 +260,7 @@ async function retryWorkerServerRead(read) {
|
|
|
245
260
|
try {
|
|
246
261
|
return await read();
|
|
247
262
|
} catch (error) {
|
|
248
|
-
if (!
|
|
263
|
+
if (!recoveryJournal || !isWorkerCommunicationFailure(error instanceof Error ? error.message : String(error))) throw error;
|
|
249
264
|
deadline ??= Date.now() + import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS;
|
|
250
265
|
if (workerLeaseExpired || Date.now() >= deadline) throw new WorkerServerUnavailableError("Worker communication recovery expired");
|
|
251
266
|
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
@@ -267,9 +282,10 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
267
282
|
`);
|
|
268
283
|
});
|
|
269
284
|
closeAllPtys();
|
|
270
|
-
for (const operation of
|
|
285
|
+
for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
|
|
271
286
|
if (operation.sessionId && operation.state !== "completed") {
|
|
272
|
-
|
|
287
|
+
void recoveryJournal?.cancelSession(operation.sessionId);
|
|
288
|
+
abortPendingReservations("Execution lease expired before command start", operation.sessionId);
|
|
273
289
|
void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
|
|
274
290
|
process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
|
|
275
291
|
`);
|
|
@@ -279,7 +295,7 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
279
295
|
for (const [runId, active] of activeProcesses) {
|
|
280
296
|
if (!active.sessionId) continue;
|
|
281
297
|
infrastructureStoppedRunIds.add(runId);
|
|
282
|
-
|
|
298
|
+
recoveryJournal?.unknownRun(runId);
|
|
283
299
|
cancelledProcessRuns.add(runId);
|
|
284
300
|
closeProcessStdin(active);
|
|
285
301
|
void (0, import_process_tree.terminateProcessTree)(active.process).catch(
|
|
@@ -294,6 +310,31 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
294
310
|
}
|
|
295
311
|
let workerAdmissionGeneration = 0;
|
|
296
312
|
const workspaceMutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
|
|
313
|
+
const workspaceMountHoldFence = new import_workspace_mount_hold_fence.WorkspaceMountHoldFence();
|
|
314
|
+
const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
|
|
315
|
+
const pendingReservationAborts = /* @__PURE__ */ new Map();
|
|
316
|
+
const sessionFileMutations = new import_session_file_mutations.SessionFileMutationTracker();
|
|
317
|
+
function registerPendingReservation(key, sessionId) {
|
|
318
|
+
abortPendingReservation(key, "Reservation superseded by a newer request with the same id");
|
|
319
|
+
const controller = new AbortController();
|
|
320
|
+
pendingReservationAborts.set(key, { controller, sessionId });
|
|
321
|
+
return controller;
|
|
322
|
+
}
|
|
323
|
+
function unregisterPendingReservation(key, controller) {
|
|
324
|
+
if (pendingReservationAborts.get(key)?.controller === controller) pendingReservationAborts.delete(key);
|
|
325
|
+
}
|
|
326
|
+
function abortPendingReservation(key, reason) {
|
|
327
|
+
const pending = pendingReservationAborts.get(key);
|
|
328
|
+
if (!pending) return;
|
|
329
|
+
pendingReservationAborts.delete(key);
|
|
330
|
+
pending.controller.abort(new import_workspace_mount_hold_fence.WorkspaceMountHoldAbortedError(reason));
|
|
331
|
+
}
|
|
332
|
+
function abortPendingReservations(reason, sessionId) {
|
|
333
|
+
for (const [key, pending] of [...pendingReservationAborts]) {
|
|
334
|
+
if (sessionId !== void 0 && pending.sessionId !== sessionId) continue;
|
|
335
|
+
abortPendingReservation(key, reason);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
297
338
|
let workspaceSyncQueue = Promise.resolve();
|
|
298
339
|
let startupProjectSnapshotRecoveryCompleted = false;
|
|
299
340
|
const checkoutGarbageCollectors = /* @__PURE__ */ new Map();
|
|
@@ -328,6 +369,17 @@ const workspaceSyncSingleFlight = {
|
|
|
328
369
|
},
|
|
329
370
|
afterCurrent() {
|
|
330
371
|
return workspaceSyncQueue;
|
|
372
|
+
},
|
|
373
|
+
mountHold(target, signal) {
|
|
374
|
+
if (target.type !== "project") return null;
|
|
375
|
+
const mountIds = [projectMountId(target.projectId, target.branchName)];
|
|
376
|
+
if (!workspaceMountHoldFence.isHeld(mountIds)) return null;
|
|
377
|
+
const holder = workspaceMountHoldFence.describeHold(mountIds);
|
|
378
|
+
process.stdout.write(
|
|
379
|
+
`[r5d-worker] command for ${target.projectId}/${target.branchName} waits for ${holder ?? "a workspace filesystem job"}
|
|
380
|
+
`
|
|
381
|
+
);
|
|
382
|
+
return workspaceMountHoldFence.waitForRelease(mountIds, { signal });
|
|
331
383
|
}
|
|
332
384
|
};
|
|
333
385
|
let githubCredential = null;
|
|
@@ -984,14 +1036,15 @@ async function runGitAsync(args, options = {}) {
|
|
|
984
1036
|
env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
|
|
985
1037
|
...bounded ? { detached: true } : {}
|
|
986
1038
|
});
|
|
987
|
-
const completion = Promise.all([
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
subprocess.exited
|
|
991
|
-
]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode }));
|
|
1039
|
+
const completion = Promise.all([new Response(subprocess.stdout).text(), new Response(subprocess.stderr).text(), subprocess.exited]).then(
|
|
1040
|
+
([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode })
|
|
1041
|
+
);
|
|
992
1042
|
let timer;
|
|
993
1043
|
const timedOut = bounded ? new Promise((_resolve, reject) => {
|
|
994
|
-
timer = setTimeout(
|
|
1044
|
+
timer = setTimeout(
|
|
1045
|
+
() => reject(new GitTransportTimeoutError(`git ${args.join(" ")} timed out after ${options.timeoutMs}ms`)),
|
|
1046
|
+
options.timeoutMs
|
|
1047
|
+
);
|
|
995
1048
|
}) : null;
|
|
996
1049
|
try {
|
|
997
1050
|
const { stdout, stderr, exitCode } = timedOut ? await Promise.race([completion, timedOut]) : await completion;
|
|
@@ -2080,12 +2133,12 @@ async function streamCommandOutput(stream, onData) {
|
|
|
2080
2133
|
}
|
|
2081
2134
|
const text = decoder.decode(chunk.value, { stream: true });
|
|
2082
2135
|
if (text.length > 0) {
|
|
2083
|
-
onData(text);
|
|
2136
|
+
await onData(text);
|
|
2084
2137
|
}
|
|
2085
2138
|
}
|
|
2086
2139
|
const trailing = decoder.decode();
|
|
2087
2140
|
if (trailing.length > 0) {
|
|
2088
|
-
onData(trailing);
|
|
2141
|
+
await onData(trailing);
|
|
2089
2142
|
}
|
|
2090
2143
|
} finally {
|
|
2091
2144
|
reader.releaseLock();
|
|
@@ -2224,12 +2277,21 @@ function mutationQueueKey(target, resolved) {
|
|
|
2224
2277
|
}
|
|
2225
2278
|
return `${describeWorkerSessionTarget(target)}:${import_node_path.default.posix.normalize(resolved.repoRelativePath)}`;
|
|
2226
2279
|
}
|
|
2227
|
-
function
|
|
2280
|
+
async function statIfExists(targetPath) {
|
|
2281
|
+
try {
|
|
2282
|
+
return await import_node_fs.default.promises.stat(targetPath);
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
const code = error.code;
|
|
2285
|
+
if (code === "ENOENT" || code === "ENOTDIR") return null;
|
|
2286
|
+
throw error;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
async function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
|
|
2228
2290
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2229
|
-
if (!
|
|
2291
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2230
2292
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2231
2293
|
}
|
|
2232
|
-
const buffer = import_node_fs.default.
|
|
2294
|
+
const buffer = await import_node_fs.default.promises.readFile(resolved.absolutePath);
|
|
2233
2295
|
const formatted = formatLineNumberedContent(buffer.toString("utf8"), offset, limit);
|
|
2234
2296
|
return {
|
|
2235
2297
|
type: "read",
|
|
@@ -2239,25 +2301,25 @@ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
|
|
|
2239
2301
|
...formatted
|
|
2240
2302
|
};
|
|
2241
2303
|
}
|
|
2242
|
-
function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
|
|
2304
|
+
async function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
|
|
2243
2305
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2244
|
-
import_node_fs.default.
|
|
2245
|
-
import_node_fs.default.
|
|
2306
|
+
await import_node_fs.default.promises.mkdir(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
|
|
2307
|
+
await import_node_fs.default.promises.writeFile(resolved.absolutePath, content, "utf8");
|
|
2246
2308
|
return {
|
|
2247
2309
|
type: "write",
|
|
2248
2310
|
file: resolved.displayPath,
|
|
2249
2311
|
gitBlobHash: getGitBlobHashForContent(content)
|
|
2250
2312
|
};
|
|
2251
2313
|
}
|
|
2252
|
-
function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
2314
|
+
async function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
2253
2315
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2254
2316
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
2255
2317
|
throw new Error("edit requires at least one replacement");
|
|
2256
2318
|
}
|
|
2257
|
-
if (!
|
|
2319
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2258
2320
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2259
2321
|
}
|
|
2260
|
-
const originalContent = import_node_fs.default.
|
|
2322
|
+
const originalContent = await import_node_fs.default.promises.readFile(resolved.absolutePath, "utf8");
|
|
2261
2323
|
const ranges = edits.map((edit, index) => {
|
|
2262
2324
|
if (edit.oldText === edit.newText) {
|
|
2263
2325
|
throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
|
|
@@ -2289,7 +2351,7 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
|
2289
2351
|
for (const range of [...sortedRanges].reverse()) {
|
|
2290
2352
|
nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
|
|
2291
2353
|
}
|
|
2292
|
-
import_node_fs.default.
|
|
2354
|
+
await import_node_fs.default.promises.writeFile(resolved.absolutePath, nextContent, "utf8");
|
|
2293
2355
|
return {
|
|
2294
2356
|
type: "edit",
|
|
2295
2357
|
file: resolved.displayPath,
|
|
@@ -2322,7 +2384,7 @@ async function executePlanOperation(input) {
|
|
|
2322
2384
|
let created = false;
|
|
2323
2385
|
if (message.createIfMissing) {
|
|
2324
2386
|
try {
|
|
2325
|
-
import_node_fs.default.
|
|
2387
|
+
await import_node_fs.default.promises.writeFile(resolved.absolutePath, (0, import_plan_parser.createPlanMarkdown)(message.planId), { encoding: "utf8", flag: "wx" });
|
|
2326
2388
|
created = true;
|
|
2327
2389
|
} catch (error) {
|
|
2328
2390
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -2331,11 +2393,16 @@ async function executePlanOperation(input) {
|
|
|
2331
2393
|
return {
|
|
2332
2394
|
type: "read_plan",
|
|
2333
2395
|
created,
|
|
2334
|
-
read: readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2396
|
+
read: await readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2335
2397
|
};
|
|
2336
2398
|
}
|
|
2337
|
-
const content = import_node_fs.default.
|
|
2338
|
-
writeWorkerTextFile(
|
|
2399
|
+
const content = await import_node_fs.default.promises.readFile(resolved.absolutePath, "utf8");
|
|
2400
|
+
await writeWorkerTextFile(
|
|
2401
|
+
resolvedTarget.rootPath,
|
|
2402
|
+
filePath,
|
|
2403
|
+
(0, import_plan_parser.updateTaskCheckbox)(content, message.taskId, message.checked),
|
|
2404
|
+
builtInPaths
|
|
2405
|
+
);
|
|
2339
2406
|
return {
|
|
2340
2407
|
type: "update_task_status",
|
|
2341
2408
|
planId: message.planId,
|
|
@@ -2356,7 +2423,11 @@ async function executeReadFileOperation(input) {
|
|
|
2356
2423
|
rootDir: input.rootDir,
|
|
2357
2424
|
access: "read"
|
|
2358
2425
|
});
|
|
2359
|
-
|
|
2426
|
+
const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
|
|
2427
|
+
return withFileMutationQueue(
|
|
2428
|
+
mutationQueueKey(input.resolvedTarget.target, resolved),
|
|
2429
|
+
() => readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths)
|
|
2430
|
+
);
|
|
2360
2431
|
}
|
|
2361
2432
|
async function executeWriteFileOperation(input) {
|
|
2362
2433
|
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
@@ -2409,16 +2480,16 @@ function wildcardToRegex(pattern) {
|
|
|
2409
2480
|
function normalizeFindPattern(pattern) {
|
|
2410
2481
|
return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
|
|
2411
2482
|
}
|
|
2412
|
-
function walkWorkerEntries(branchPath, start) {
|
|
2483
|
+
async function walkWorkerEntries(branchPath, start) {
|
|
2413
2484
|
const entries = [];
|
|
2414
2485
|
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2415
|
-
const realVirtualRoot = start.scope === "virtual" ? import_node_fs.default.
|
|
2416
|
-
const visit = (absolutePath, isRoot) => {
|
|
2486
|
+
const realVirtualRoot = start.scope === "virtual" ? await import_node_fs.default.promises.realpath(start.virtualRootPath) : null;
|
|
2487
|
+
const visit = async (absolutePath, isRoot) => {
|
|
2417
2488
|
let stat;
|
|
2418
2489
|
try {
|
|
2419
|
-
stat = import_node_fs.default.
|
|
2490
|
+
stat = await import_node_fs.default.promises.stat(absolutePath);
|
|
2420
2491
|
if (realVirtualRoot) {
|
|
2421
|
-
assertInsideRoot(realVirtualRoot, import_node_fs.default.
|
|
2492
|
+
assertInsideRoot(realVirtualRoot, await import_node_fs.default.promises.realpath(absolutePath), `${start.displayPath} path`);
|
|
2422
2493
|
}
|
|
2423
2494
|
} catch (error) {
|
|
2424
2495
|
if (isRoot) throw error;
|
|
@@ -2431,7 +2502,7 @@ function walkWorkerEntries(branchPath, start) {
|
|
|
2431
2502
|
if (!stat.isDirectory()) return;
|
|
2432
2503
|
let realDirectoryPath;
|
|
2433
2504
|
try {
|
|
2434
|
-
realDirectoryPath = import_node_fs.default.
|
|
2505
|
+
realDirectoryPath = await import_node_fs.default.promises.realpath(absolutePath);
|
|
2435
2506
|
} catch (error) {
|
|
2436
2507
|
if (isRoot) throw error;
|
|
2437
2508
|
return;
|
|
@@ -2440,25 +2511,25 @@ function walkWorkerEntries(branchPath, start) {
|
|
|
2440
2511
|
visitedDirectories.add(realDirectoryPath);
|
|
2441
2512
|
let children;
|
|
2442
2513
|
try {
|
|
2443
|
-
children = import_node_fs.default.
|
|
2514
|
+
children = (await import_node_fs.default.promises.readdir(absolutePath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2444
2515
|
} catch (error) {
|
|
2445
2516
|
if (isRoot) throw error;
|
|
2446
2517
|
return;
|
|
2447
2518
|
}
|
|
2448
2519
|
for (const child of children) {
|
|
2449
2520
|
if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
|
|
2450
|
-
visit(import_node_path.default.join(absolutePath, child.name), false);
|
|
2521
|
+
await visit(import_node_path.default.join(absolutePath, child.name), false);
|
|
2451
2522
|
}
|
|
2452
2523
|
};
|
|
2453
|
-
visit(start.absolutePath, true);
|
|
2524
|
+
await visit(start.absolutePath, true);
|
|
2454
2525
|
return entries;
|
|
2455
2526
|
}
|
|
2456
2527
|
function isProbablyText(bytes) {
|
|
2457
2528
|
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
2458
2529
|
}
|
|
2459
|
-
function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
2530
|
+
async function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
2460
2531
|
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
2461
|
-
if (!
|
|
2532
|
+
if (!await statIfExists(start.absolutePath)) {
|
|
2462
2533
|
throw new Error(`Path not found: ${start.displayPath}`);
|
|
2463
2534
|
}
|
|
2464
2535
|
const limit = normalizeToolLimit(input.limit, 100, 1e3);
|
|
@@ -2468,12 +2539,12 @@ function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
|
2468
2539
|
const lines = [];
|
|
2469
2540
|
let matchCount = 0;
|
|
2470
2541
|
let truncated = false;
|
|
2471
|
-
for (const entry of walkWorkerEntries(branchPath, start)) {
|
|
2542
|
+
for (const entry of await walkWorkerEntries(branchPath, start)) {
|
|
2472
2543
|
if (!entry.stat.isFile()) continue;
|
|
2473
2544
|
if (globRegex && !globRegex.test(entry.matchPath)) continue;
|
|
2474
2545
|
let bytes;
|
|
2475
2546
|
try {
|
|
2476
|
-
bytes = import_node_fs.default.
|
|
2547
|
+
bytes = await import_node_fs.default.promises.readFile(entry.absolutePath);
|
|
2477
2548
|
} catch {
|
|
2478
2549
|
continue;
|
|
2479
2550
|
}
|
|
@@ -2526,9 +2597,9 @@ async function executeGrepOperation(input) {
|
|
|
2526
2597
|
builtInPaths
|
|
2527
2598
|
);
|
|
2528
2599
|
}
|
|
2529
|
-
function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
2600
|
+
async function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
2530
2601
|
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
2531
|
-
if (!
|
|
2602
|
+
if (!(await statIfExists(start.absolutePath))?.isDirectory()) {
|
|
2532
2603
|
throw new Error(`Directory not found: ${start.displayPath}`);
|
|
2533
2604
|
}
|
|
2534
2605
|
const pattern = normalizeFindPattern(input.pattern ?? "*");
|
|
@@ -2538,7 +2609,7 @@ function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
|
2538
2609
|
const matches = [];
|
|
2539
2610
|
let count = 0;
|
|
2540
2611
|
let truncated = false;
|
|
2541
|
-
for (const entry of walkWorkerEntries(branchPath, start)) {
|
|
2612
|
+
for (const entry of await walkWorkerEntries(branchPath, start)) {
|
|
2542
2613
|
if (entry.absolutePath === start.absolutePath) continue;
|
|
2543
2614
|
const isDirectory = entry.stat.isDirectory();
|
|
2544
2615
|
if (entryType === "file" && !entry.stat.isFile()) continue;
|
|
@@ -2583,13 +2654,13 @@ async function executeFindOperation(input) {
|
|
|
2583
2654
|
builtInPaths
|
|
2584
2655
|
);
|
|
2585
2656
|
}
|
|
2586
|
-
function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
|
|
2657
|
+
async function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
|
|
2587
2658
|
const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
|
|
2588
|
-
if (!
|
|
2659
|
+
if (!(await statIfExists(resolved.absolutePath))?.isDirectory()) {
|
|
2589
2660
|
throw new Error(`Directory not found: ${resolved.displayPath}`);
|
|
2590
2661
|
}
|
|
2591
2662
|
const limit = normalizeToolLimit(inputLimit, 200, 1e3);
|
|
2592
|
-
const entries = import_node_fs.default.
|
|
2663
|
+
const entries = (await import_node_fs.default.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));
|
|
2593
2664
|
const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
2594
2665
|
const truncated = entries.length > displayed.length;
|
|
2595
2666
|
const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
|
|
@@ -2640,36 +2711,40 @@ function resolveWorkerCodePath(branchPath, inputPath) {
|
|
|
2640
2711
|
}
|
|
2641
2712
|
return { absolutePath, displayPath };
|
|
2642
2713
|
}
|
|
2643
|
-
function listWorkerCodeDirectory(branchPath, inputPath) {
|
|
2714
|
+
async function listWorkerCodeDirectory(branchPath, inputPath) {
|
|
2644
2715
|
const resolved = resolveWorkerCodePath(branchPath, inputPath);
|
|
2645
|
-
if (!import_node_fs.default.
|
|
2716
|
+
if (!(await import_node_fs.default.promises.stat(resolved.absolutePath)).isDirectory()) {
|
|
2646
2717
|
throw new Error(`Code directory not found: ${resolved.displayPath}`);
|
|
2647
2718
|
}
|
|
2648
|
-
const
|
|
2719
|
+
const listed = (await import_node_fs.default.promises.readdir(resolved.absolutePath, { withFileTypes: true })).filter(
|
|
2720
|
+
(entry) => entry.name.toLowerCase() !== ".git"
|
|
2721
|
+
);
|
|
2722
|
+
const entries = [];
|
|
2723
|
+
for (const entry of listed) {
|
|
2649
2724
|
const entryPath = import_node_path.default.posix.join(resolved.displayPath, entry.name);
|
|
2650
2725
|
try {
|
|
2651
2726
|
const resolvedEntry = resolveWorkerCodePath(branchPath, entryPath);
|
|
2652
|
-
const stats = import_node_fs.default.
|
|
2653
|
-
if (!stats.isFile() && !stats.isDirectory())
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
}
|
|
2661
|
-
];
|
|
2727
|
+
const stats = await import_node_fs.default.promises.stat(resolvedEntry.absolutePath);
|
|
2728
|
+
if (!stats.isFile() && !stats.isDirectory()) continue;
|
|
2729
|
+
entries.push({
|
|
2730
|
+
name: entry.name,
|
|
2731
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
2732
|
+
size: stats.isDirectory() ? null : stats.size,
|
|
2733
|
+
mtime: stats.mtime.toISOString()
|
|
2734
|
+
});
|
|
2662
2735
|
} catch {
|
|
2663
|
-
return [];
|
|
2664
2736
|
}
|
|
2665
|
-
}
|
|
2737
|
+
}
|
|
2738
|
+
entries.sort(
|
|
2739
|
+
(left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name)
|
|
2740
|
+
);
|
|
2666
2741
|
return { type: "code_list", path: resolved.displayPath, entries };
|
|
2667
2742
|
}
|
|
2668
|
-
function readWorkerCodeFile(branchPath, inputPath) {
|
|
2743
|
+
async function readWorkerCodeFile(branchPath, inputPath) {
|
|
2669
2744
|
const resolved = resolveWorkerCodePath(branchPath, inputPath);
|
|
2670
|
-
const stats = import_node_fs.default.
|
|
2745
|
+
const stats = await import_node_fs.default.promises.stat(resolved.absolutePath);
|
|
2671
2746
|
if (!stats.isFile()) throw new Error(`Code file not found: ${resolved.displayPath}`);
|
|
2672
|
-
const bytes = import_node_fs.default.
|
|
2747
|
+
const bytes = await import_node_fs.default.promises.readFile(resolved.absolutePath);
|
|
2673
2748
|
return {
|
|
2674
2749
|
type: "code_read",
|
|
2675
2750
|
path: resolved.displayPath,
|
|
@@ -2678,17 +2753,17 @@ function readWorkerCodeFile(branchPath, inputPath) {
|
|
|
2678
2753
|
base64: bytes.toString("base64")
|
|
2679
2754
|
};
|
|
2680
2755
|
}
|
|
2681
|
-
function executeCodeListOperation(input) {
|
|
2756
|
+
async function executeCodeListOperation(input) {
|
|
2682
2757
|
if (input.resolvedTarget.target.type !== "project") throw new Error("Code listing requires a project branch target");
|
|
2683
2758
|
return listWorkerCodeDirectory(input.resolvedTarget.rootPath, input.message.path);
|
|
2684
2759
|
}
|
|
2685
|
-
function executeCodeReadOperation(input) {
|
|
2760
|
+
async function executeCodeReadOperation(input) {
|
|
2686
2761
|
if (input.resolvedTarget.target.type !== "project") throw new Error("Code reading requires a project branch target");
|
|
2687
2762
|
return readWorkerCodeFile(input.resolvedTarget.rootPath, input.message.path);
|
|
2688
2763
|
}
|
|
2689
|
-
function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
2764
|
+
async function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
2690
2765
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2691
|
-
if (!
|
|
2766
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2692
2767
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2693
2768
|
}
|
|
2694
2769
|
const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
|
|
@@ -2696,7 +2771,7 @@ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
|
2696
2771
|
if (!mediaType) {
|
|
2697
2772
|
throw new Error("read supports PNG and JPEG image inputs only");
|
|
2698
2773
|
}
|
|
2699
|
-
const bytes = import_node_fs.default.
|
|
2774
|
+
const bytes = await import_node_fs.default.promises.readFile(resolved.absolutePath);
|
|
2700
2775
|
const dimensions = readImageDimensions(bytes, mediaType);
|
|
2701
2776
|
return {
|
|
2702
2777
|
type: "view_file_bytes",
|
|
@@ -3023,26 +3098,28 @@ async function executeStreamingCommand(input) {
|
|
|
3023
3098
|
}
|
|
3024
3099
|
const [exitCode] = await Promise.all([
|
|
3025
3100
|
subprocess.exited,
|
|
3026
|
-
streamCommandOutput(
|
|
3027
|
-
|
|
3101
|
+
streamCommandOutput(
|
|
3102
|
+
subprocess.stdout,
|
|
3103
|
+
(data) => sendWorkerOutput(input.ws, {
|
|
3028
3104
|
type: "exec_output",
|
|
3029
3105
|
runId: input.message.runId,
|
|
3030
3106
|
stream: "stdout",
|
|
3031
3107
|
data
|
|
3032
|
-
})
|
|
3033
|
-
|
|
3034
|
-
streamCommandOutput(
|
|
3035
|
-
|
|
3108
|
+
})
|
|
3109
|
+
),
|
|
3110
|
+
streamCommandOutput(
|
|
3111
|
+
subprocess.stderr,
|
|
3112
|
+
(data) => sendWorkerOutput(input.ws, {
|
|
3036
3113
|
type: "exec_output",
|
|
3037
3114
|
runId: input.message.runId,
|
|
3038
3115
|
stream: "stderr",
|
|
3039
3116
|
data
|
|
3040
|
-
})
|
|
3041
|
-
|
|
3117
|
+
})
|
|
3118
|
+
)
|
|
3042
3119
|
]);
|
|
3043
3120
|
const diagnostic = await input.resources.diagnose();
|
|
3044
3121
|
if (diagnostic) {
|
|
3045
|
-
|
|
3122
|
+
await sendWorkerOutput(input.ws, {
|
|
3046
3123
|
type: "exec_output",
|
|
3047
3124
|
runId: input.message.runId,
|
|
3048
3125
|
stream: "stderr",
|
|
@@ -3114,6 +3191,33 @@ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
|
|
|
3114
3191
|
);
|
|
3115
3192
|
}
|
|
3116
3193
|
}
|
|
3194
|
+
async function cancelProcessRun(runId) {
|
|
3195
|
+
let resourceCancellationError;
|
|
3196
|
+
try {
|
|
3197
|
+
await workerCommandLauncher?.cancel(runId);
|
|
3198
|
+
} catch (error) {
|
|
3199
|
+
resourceCancellationError = error instanceof Error ? error.message : String(error);
|
|
3200
|
+
}
|
|
3201
|
+
const active = activeProcesses.get(runId);
|
|
3202
|
+
let cancelled = resourceCancellationError === void 0;
|
|
3203
|
+
let cancelMessage;
|
|
3204
|
+
if (active) {
|
|
3205
|
+
try {
|
|
3206
|
+
closeProcessStdin(active);
|
|
3207
|
+
await (0, import_process_tree.terminateProcessTree)(active.process);
|
|
3208
|
+
cancelMessage = `Stopped ${active.command}`;
|
|
3209
|
+
} catch (error) {
|
|
3210
|
+
cancelled = false;
|
|
3211
|
+
cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3212
|
+
}
|
|
3213
|
+
} else {
|
|
3214
|
+
cancelledProcessRuns.add(runId);
|
|
3215
|
+
abortPendingReservation(`run:${runId}`, "Cancellation queued before command start");
|
|
3216
|
+
cancelMessage = "Cancellation queued before command start";
|
|
3217
|
+
}
|
|
3218
|
+
if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
|
|
3219
|
+
return { cancelled, message: cancelMessage };
|
|
3220
|
+
}
|
|
3117
3221
|
function closeProcessStdin(active) {
|
|
3118
3222
|
if (!active?.stdin) {
|
|
3119
3223
|
return;
|
|
@@ -3139,21 +3243,35 @@ function sendReplayWorkerMessage(ws, message) {
|
|
|
3139
3243
|
}
|
|
3140
3244
|
function sendWorkerMessage(ws, message) {
|
|
3141
3245
|
if (launcherFailureInProgress) return;
|
|
3142
|
-
if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
|
|
3143
|
-
return;
|
|
3144
3246
|
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;
|
|
3145
|
-
if (
|
|
3146
|
-
|
|
3247
|
+
if (recoveryJournal && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
|
|
3248
|
+
recoveryJournal.unknown(message.requestId);
|
|
3147
3249
|
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
3148
3250
|
return;
|
|
3149
3251
|
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
recoveryStore?.unknownRun(message.runId);
|
|
3252
|
+
if (!recoveryJournal) {
|
|
3253
|
+
sendReplayWorkerMessage(ws, message);
|
|
3153
3254
|
return;
|
|
3154
3255
|
}
|
|
3155
|
-
|
|
3156
|
-
|
|
3256
|
+
recoveryJournal.send(message);
|
|
3257
|
+
}
|
|
3258
|
+
function sendWorkerOutput(ws, frame) {
|
|
3259
|
+
if (launcherFailureInProgress) return Promise.resolve();
|
|
3260
|
+
if (!recoveryJournal) {
|
|
3261
|
+
sendReplayWorkerMessage(ws, frame);
|
|
3262
|
+
return Promise.resolve();
|
|
3263
|
+
}
|
|
3264
|
+
return recoveryJournal.sendOutput(frame);
|
|
3265
|
+
}
|
|
3266
|
+
function deliverJournaledWorkerMessage(message) {
|
|
3267
|
+
const target = currentWorkerSocket;
|
|
3268
|
+
if ((message.type === "exec_exit" || message.type === "exec_error") && typeof message.runId === "string" && infrastructureStoppedRunIds.has(message.runId)) {
|
|
3269
|
+
recoveryJournal?.unknownRun(message.runId);
|
|
3270
|
+
return true;
|
|
3271
|
+
}
|
|
3272
|
+
if (!target || target.readyState !== WebSocket.OPEN) return false;
|
|
3273
|
+
if (message.type === "exec_output" && workerRecoveryId !== void 0) return false;
|
|
3274
|
+
return sendReplayWorkerMessage(target, message);
|
|
3157
3275
|
}
|
|
3158
3276
|
function sendSerializedWorkerMessage(ws, serialized) {
|
|
3159
3277
|
sendWorkerMessage(ws, JSON.parse(serialized));
|
|
@@ -3716,9 +3834,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
3716
3834
|
`);
|
|
3717
3835
|
}
|
|
3718
3836
|
const rootDir = import_node_path.default.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3837
|
+
if (!recoveryJournal) {
|
|
3838
|
+
const journal = await import_recovery_journal.WorkerRecoveryJournal.open({
|
|
3839
|
+
filename: import_node_path.default.join(
|
|
3840
|
+
rootDir,
|
|
3841
|
+
".worker-recovery",
|
|
3842
|
+
(0, import_node_crypto.createHash)("sha256").update(`${baseUrl}\0${label}`).digest("hex"),
|
|
3843
|
+
"protocol.sqlite"
|
|
3844
|
+
)
|
|
3845
|
+
});
|
|
3846
|
+
journal.deliver = deliverJournaledWorkerMessage;
|
|
3847
|
+
journal.onFailure = handleRecoveryJournalFailure;
|
|
3848
|
+
recoveryJournal = journal;
|
|
3849
|
+
}
|
|
3722
3850
|
const projectsRoot = import_node_path.default.join(rootDir, "projects");
|
|
3723
3851
|
const syncRoot = import_node_path.default.join(rootDir, "sync");
|
|
3724
3852
|
const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
|
|
@@ -3821,7 +3949,6 @@ async function startWorker(options, projectRuntime = {
|
|
|
3821
3949
|
const bearerAuthHeader = `Authorization: Bearer ${token}`;
|
|
3822
3950
|
const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
|
|
3823
3951
|
const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
3824
|
-
const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
|
|
3825
3952
|
const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
|
|
3826
3953
|
const workspaceProjectRelativePath = (projectId, branchName) => import_node_path.default.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
|
|
3827
3954
|
const workspacePlanRelativePath = (projectId, branchName) => import_node_path.default.posix.join("plans", projectId, encodeURIComponent(branchName));
|
|
@@ -3904,7 +4031,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
3904
4031
|
sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
|
|
3905
4032
|
planSourcePath: import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"))
|
|
3906
4033
|
}));
|
|
3907
|
-
(0, import_project_worktrees.removeProjectWorktrees)({ projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
|
|
4034
|
+
const removed = (0, import_project_worktrees.removeProjectWorktrees)({ projectsRoot, projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
|
|
4035
|
+
if (removed.stagedForCollectionPath) checkoutGarbageCollector.enqueue(removed.stagedForCollectionPath);
|
|
3908
4036
|
for (const branch of branches) {
|
|
3909
4037
|
import_node_fs.default.rmSync(branch.planSourcePath, { recursive: true, force: true });
|
|
3910
4038
|
const key = projectBranchKey(project.projectId, branch.branchName);
|
|
@@ -3992,14 +4120,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
3992
4120
|
)
|
|
3993
4121
|
};
|
|
3994
4122
|
};
|
|
3995
|
-
const stageDurableWorkspaceDeletions = () => {
|
|
4123
|
+
const stageDurableWorkspaceDeletions = async () => {
|
|
3996
4124
|
for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
|
|
3997
4125
|
const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
|
|
3998
4126
|
if (deletion.kind === "project") {
|
|
3999
4127
|
if (!deletion.projectDeleted) {
|
|
4000
4128
|
const nextProject = projectConfigById.get(deletion.projectId);
|
|
4001
4129
|
if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
|
|
4002
|
-
(0,
|
|
4130
|
+
await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_checkout_path_move", {
|
|
4003
4131
|
oldProjectRoot: deletion.managedPath,
|
|
4004
4132
|
newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
|
|
4005
4133
|
projectsDurabilityRoot: projectsRoot,
|
|
@@ -4366,7 +4494,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4366
4494
|
}
|
|
4367
4495
|
}
|
|
4368
4496
|
if (transitionReseedRequired) {
|
|
4369
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
|
|
4497
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordEnabledRepositoryTransition({
|
|
4370
4498
|
projectId: project.projectId,
|
|
4371
4499
|
transitionId: project.repositoryTransitionId
|
|
4372
4500
|
});
|
|
@@ -4535,7 +4663,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4535
4663
|
)
|
|
4536
4664
|
];
|
|
4537
4665
|
if (publishedTombstoneIds.length > 0) {
|
|
4538
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
4666
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
|
|
4539
4667
|
tombstoneIds: publishedTombstoneIds,
|
|
4540
4668
|
publishedHead
|
|
4541
4669
|
});
|
|
@@ -4554,7 +4682,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4554
4682
|
forgetLocalProjectMirrorRefsTokens(deletion.projectId, deletion.branchName);
|
|
4555
4683
|
}
|
|
4556
4684
|
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
4557
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
4685
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
4558
4686
|
tombstoneId: deletion.tombstoneId,
|
|
4559
4687
|
branchName: deletion.branchName
|
|
4560
4688
|
});
|
|
@@ -4751,7 +4879,10 @@ async function startWorker(options, projectRuntime = {
|
|
|
4751
4879
|
};
|
|
4752
4880
|
};
|
|
4753
4881
|
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4754
|
-
(message) => sendWorkerMessageFromCurrentSource(ws, message)
|
|
4882
|
+
(message) => sendWorkerMessageFromCurrentSource(ws, message),
|
|
4883
|
+
{
|
|
4884
|
+
fence: workspaceMountHoldFence
|
|
4885
|
+
}
|
|
4755
4886
|
);
|
|
4756
4887
|
const performWorkspaceSync = async (input) => {
|
|
4757
4888
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
@@ -5409,7 +5540,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5409
5540
|
(project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
|
|
5410
5541
|
);
|
|
5411
5542
|
if (!incidentDeferral.incidentId) {
|
|
5412
|
-
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
5543
|
+
projectWorkspaceState = await projectWorkspaceStateStore.reconcile({
|
|
5413
5544
|
desiredProjects: message.projects,
|
|
5414
5545
|
preserveOnlyBranches
|
|
5415
5546
|
});
|
|
@@ -5420,7 +5551,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5420
5551
|
(candidate) => candidate.projectId === project.projectId && candidate.branchName === branch.branchName
|
|
5421
5552
|
);
|
|
5422
5553
|
if (pending2 && pending2.branchId === branch.branchId) {
|
|
5423
|
-
projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
|
|
5554
|
+
projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
|
|
5424
5555
|
}
|
|
5425
5556
|
}
|
|
5426
5557
|
}
|
|
@@ -5588,7 +5719,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5588
5719
|
for (const projectId of [...readyProjectIds]) {
|
|
5589
5720
|
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
5590
5721
|
}
|
|
5591
|
-
stageDurableWorkspaceDeletions();
|
|
5722
|
+
await stageDurableWorkspaceDeletions();
|
|
5592
5723
|
const mountsBeforeGit = buildWorkspaceMounts();
|
|
5593
5724
|
await (0, import_workspace_git_sync.hydrateWorkspaceGitMounts)(
|
|
5594
5725
|
workspaceShadowRoot,
|
|
@@ -5634,7 +5765,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
5634
5765
|
const connectSyncMs = Date.now() - connectSyncStartedAtMs;
|
|
5635
5766
|
const result = syncResult.telemetry ? syncResult : {
|
|
5636
5767
|
...syncResult,
|
|
5637
|
-
telemetry: {
|
|
5768
|
+
telemetry: {
|
|
5769
|
+
totalMs: connectSyncMs,
|
|
5770
|
+
queueMs: 0,
|
|
5771
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
5772
|
+
synchronizeMs: connectSyncMs
|
|
5773
|
+
}
|
|
5638
5774
|
};
|
|
5639
5775
|
assertConfigurationSyncStillAdmitted();
|
|
5640
5776
|
if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
|
|
@@ -5653,7 +5789,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5653
5789
|
) ? [deletion.tombstoneId] : []
|
|
5654
5790
|
);
|
|
5655
5791
|
if (layoutCleanupIds.length > 0) {
|
|
5656
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
5792
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
|
|
5657
5793
|
tombstoneIds: layoutCleanupIds,
|
|
5658
5794
|
publishedHead
|
|
5659
5795
|
});
|
|
@@ -5760,26 +5896,33 @@ async function startWorker(options, projectRuntime = {
|
|
|
5760
5896
|
if (startupProjectSnapshotRecoveryCompleted) return;
|
|
5761
5897
|
sendLifecycleProgress({ phase: "recovering", operationId, detail: "Checking interrupted workspace transactions" });
|
|
5762
5898
|
const snapshotRecovery = await workspaceSyncSingleFlight.runExclusive(
|
|
5763
|
-
async () => await (0,
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5899
|
+
async () => await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run(
|
|
5900
|
+
"project_snapshots_recover",
|
|
5901
|
+
{
|
|
5902
|
+
projectsRoot,
|
|
5903
|
+
temporaryRoot: projectWorktreeSnapshotsRoot,
|
|
5904
|
+
currentProcessId: process.pid,
|
|
5905
|
+
currentOwnerSessionId: import_project_worktrees.PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
|
|
5906
|
+
},
|
|
5907
|
+
{
|
|
5908
|
+
onProgress: (progress) => {
|
|
5909
|
+
const lifecycleProgress = {
|
|
5910
|
+
phase: "recovering",
|
|
5911
|
+
operationId,
|
|
5912
|
+
detail: describeSnapshotProgress(progress),
|
|
5913
|
+
completedBytes: progress.completedBytes,
|
|
5914
|
+
completedItems: progress.completedBranches,
|
|
5915
|
+
totalItems: progress.totalBranches,
|
|
5916
|
+
...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
|
|
5917
|
+
};
|
|
5918
|
+
sendLifecycleProgress(lifecycleProgress);
|
|
5919
|
+
process.stdout.write(
|
|
5920
|
+
`[r5d-worker] ${lifecycleProgress.detail}: ${progress.completedBranches}/${progress.totalBranches} branch(es), ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes
|
|
5779
5921
|
`
|
|
5780
|
-
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5781
5924
|
}
|
|
5782
|
-
|
|
5925
|
+
)
|
|
5783
5926
|
);
|
|
5784
5927
|
if (snapshotRecovery.restored.length > 0) {
|
|
5785
5928
|
process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
|
|
@@ -5874,7 +6017,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5874
6017
|
type: "hello",
|
|
5875
6018
|
resumableProtocol: import_recovery_protocol.WORKER_RESUMABLE_PROTOCOL,
|
|
5876
6019
|
runtimeId: workerRuntimeId,
|
|
5877
|
-
ledgerId:
|
|
6020
|
+
ledgerId: recoveryJournal.ledgerId,
|
|
5878
6021
|
startupReady: startupRecoveryComplete,
|
|
5879
6022
|
hostInfo: {
|
|
5880
6023
|
hostname: (0, import_node_os.hostname)(),
|
|
@@ -5902,16 +6045,39 @@ async function startWorker(options, projectRuntime = {
|
|
|
5902
6045
|
}
|
|
5903
6046
|
};
|
|
5904
6047
|
sendSerializedWorkerMessage(ws, JSON.stringify(hello));
|
|
6048
|
+
let outputPumpInFlight = false;
|
|
5905
6049
|
outputReplayTimer = setInterval(() => {
|
|
5906
|
-
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
6050
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws || outputPumpInFlight || !recoveryJournal) return;
|
|
6051
|
+
outputPumpInFlight = true;
|
|
6052
|
+
void (async () => {
|
|
6053
|
+
try {
|
|
6054
|
+
for await (const page of recoveryJournal.outputReplay(void 0)) {
|
|
6055
|
+
for (const output of page) {
|
|
6056
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws || !sendReplayWorkerMessage(ws, output)) return;
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
} catch (error) {
|
|
6060
|
+
process.stderr.write(`[r5d-worker] output replay pump failed: ${error instanceof Error ? error.message : String(error)}
|
|
6061
|
+
`);
|
|
6062
|
+
} finally {
|
|
6063
|
+
outputPumpInFlight = false;
|
|
6064
|
+
}
|
|
6065
|
+
})();
|
|
5910
6066
|
}, 250);
|
|
5911
6067
|
outputReplayTimer.unref();
|
|
6068
|
+
let terminalPumpInFlight = false;
|
|
5912
6069
|
terminalReplayTimer = setInterval(() => {
|
|
5913
|
-
if (workerRecoveryId !== void 0) return;
|
|
5914
|
-
|
|
6070
|
+
if (workerRecoveryId !== void 0 || terminalPumpInFlight || !recoveryJournal) return;
|
|
6071
|
+
terminalPumpInFlight = true;
|
|
6072
|
+
void recoveryJournal.terminals().then((terminals) => {
|
|
6073
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
|
|
6074
|
+
for (const terminal of terminals) sendReplayWorkerMessage(ws, terminal);
|
|
6075
|
+
}).catch((error) => {
|
|
6076
|
+
process.stderr.write(`[r5d-worker] terminal replay pump failed: ${error instanceof Error ? error.message : String(error)}
|
|
6077
|
+
`);
|
|
6078
|
+
}).finally(() => {
|
|
6079
|
+
terminalPumpInFlight = false;
|
|
6080
|
+
});
|
|
5915
6081
|
}, 5e3);
|
|
5916
6082
|
terminalReplayTimer.unref();
|
|
5917
6083
|
process.stdout.write("[r5d-worker] connected\n");
|
|
@@ -5924,28 +6090,37 @@ async function startWorker(options, projectRuntime = {
|
|
|
5924
6090
|
if (message.type === "recovery_request") {
|
|
5925
6091
|
workerRecoveryId = message.recoveryId;
|
|
5926
6092
|
sendActiveProcessReport(ws);
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
6093
|
+
recoveryJournal.pauseDelivery();
|
|
6094
|
+
try {
|
|
6095
|
+
const state = await recoveryJournal.recoveryState(message.requests);
|
|
6096
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
6097
|
+
sendWorkerMessage(ws, {
|
|
6098
|
+
type: "recovery_state",
|
|
6099
|
+
recoveryId: message.recoveryId,
|
|
6100
|
+
runtimeId: workerRuntimeId,
|
|
6101
|
+
ledgerId: recoveryJournal.ledgerId,
|
|
6102
|
+
operations: state.operations
|
|
6103
|
+
});
|
|
6104
|
+
const cursors = new Map(state.cursors.map((cursor) => [cursor.runId, cursor]));
|
|
6105
|
+
for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
|
|
6106
|
+
workerOutputWindow.reset([...cursors.values()]);
|
|
6107
|
+
for (const response of state.responses) sendReplayWorkerMessage(ws, response);
|
|
6108
|
+
for await (const page of recoveryJournal.outputReplay(message.outputOffsets)) {
|
|
6109
|
+
for (const output of page) {
|
|
6110
|
+
while (true) {
|
|
6111
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
|
|
6112
|
+
return;
|
|
6113
|
+
if (sendReplayWorkerMessage(ws, output)) break;
|
|
6114
|
+
await Bun.sleep(10);
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
5944
6117
|
}
|
|
6118
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
6119
|
+
for (const terminal of state.terminals) sendReplayWorkerMessage(ws, terminal);
|
|
6120
|
+
sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
|
|
6121
|
+
} finally {
|
|
6122
|
+
recoveryJournal.resumeDelivery();
|
|
5945
6123
|
}
|
|
5946
|
-
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
5947
|
-
for (const terminal of recoveryStore.terminals()) sendReplayWorkerMessage(ws, terminal);
|
|
5948
|
-
sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
|
|
5949
6124
|
return;
|
|
5950
6125
|
}
|
|
5951
6126
|
if (message.type === "recovery_ready") {
|
|
@@ -5960,17 +6135,23 @@ async function startWorker(options, projectRuntime = {
|
|
|
5960
6135
|
return;
|
|
5961
6136
|
}
|
|
5962
6137
|
if (message.type === "operation_result_ack") {
|
|
5963
|
-
|
|
6138
|
+
recoveryJournal.acknowledgeResult(message.requestId);
|
|
5964
6139
|
return;
|
|
5965
6140
|
}
|
|
5966
6141
|
if (message.type === "exec_output_ack") {
|
|
5967
|
-
|
|
6142
|
+
recoveryJournal.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
|
|
5968
6143
|
workerOutputWindow.acknowledge(message.runId, message.stream, message.nextOffset);
|
|
5969
6144
|
return;
|
|
5970
6145
|
}
|
|
5971
6146
|
if (message.type === "cancel_session") {
|
|
5972
|
-
|
|
6147
|
+
const durableCancellation = recoveryJournal.cancelSession(message.sessionId);
|
|
6148
|
+
abortPendingReservations("Session cancelled before command start", message.sessionId);
|
|
5973
6149
|
await Promise.all([
|
|
6150
|
+
durableCancellation,
|
|
6151
|
+
// File writes already in flight for the session settle before the
|
|
6152
|
+
// acknowledgement; queued ones re-check the cancellation before
|
|
6153
|
+
// their first write and never start.
|
|
6154
|
+
sessionFileMutations.settled(message.sessionId),
|
|
5974
6155
|
workerCommandLauncher?.cancelSession(message.sessionId),
|
|
5975
6156
|
...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
5976
6157
|
cancelledProcessRuns.add(runId);
|
|
@@ -5985,27 +6166,74 @@ async function startWorker(options, projectRuntime = {
|
|
|
5985
6166
|
if (!startupRecoveryComplete && message.type !== "ping") return;
|
|
5986
6167
|
if (currentWorkerSocket !== ws) return;
|
|
5987
6168
|
const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
|
|
6169
|
+
if (message.type === "cancel") {
|
|
6170
|
+
const admission = recoveryJournal.admit({ ...message, requestId: message.requestId });
|
|
6171
|
+
const cancelled = await cancelProcessRun(message.runId);
|
|
6172
|
+
let admitted;
|
|
6173
|
+
try {
|
|
6174
|
+
admitted = await admission;
|
|
6175
|
+
} catch (error) {
|
|
6176
|
+
if (!(error instanceof import_recovery_journal.WorkerRecoveryJournalBusyError)) throw error;
|
|
6177
|
+
process.stderr.write(`[r5d-worker] cancel ${message.runId} stopped the run but was not durably admitted: ${error.message}
|
|
6178
|
+
`);
|
|
6179
|
+
return;
|
|
6180
|
+
}
|
|
6181
|
+
if (admitted.admission === "conflict") {
|
|
6182
|
+
ws.close(1008, "Operation identity conflict");
|
|
6183
|
+
return;
|
|
6184
|
+
}
|
|
6185
|
+
sendWorkerMessage(ws, { type: "operation_received", requestId: message.requestId });
|
|
6186
|
+
if (admitted.admission !== "new") {
|
|
6187
|
+
if (admitted.response && admitted.admission !== "unknown") sendReplayWorkerMessage(ws, admitted.response);
|
|
6188
|
+
return;
|
|
6189
|
+
}
|
|
6190
|
+
sendSerializedWorkerMessage(
|
|
6191
|
+
ws,
|
|
6192
|
+
JSON.stringify({
|
|
6193
|
+
type: "cancel_result",
|
|
6194
|
+
requestId: message.requestId,
|
|
6195
|
+
runId: message.runId,
|
|
6196
|
+
cancelled: cancelled.cancelled,
|
|
6197
|
+
message: cancelled.message
|
|
6198
|
+
})
|
|
6199
|
+
);
|
|
6200
|
+
return;
|
|
6201
|
+
}
|
|
5988
6202
|
if (operationRequestId) {
|
|
5989
|
-
|
|
5990
|
-
|
|
6203
|
+
let admitted;
|
|
6204
|
+
try {
|
|
6205
|
+
admitted = await recoveryJournal.admit({ ...message, requestId: operationRequestId });
|
|
6206
|
+
} catch (error) {
|
|
6207
|
+
if (!(error instanceof import_recovery_journal.WorkerRecoveryJournalBusyError)) throw error;
|
|
6208
|
+
process.stderr.write(`[r5d-worker] ${message.type} ${operationRequestId} was not admitted: ${error.message}
|
|
6209
|
+
`);
|
|
6210
|
+
return;
|
|
6211
|
+
}
|
|
6212
|
+
if (admitted.admission === "conflict") {
|
|
5991
6213
|
ws.close(1008, "Operation identity conflict");
|
|
5992
6214
|
return;
|
|
5993
6215
|
}
|
|
5994
6216
|
sendWorkerMessage(ws, { type: "operation_received", requestId: operationRequestId });
|
|
5995
|
-
if (admission !== "new") {
|
|
5996
|
-
|
|
5997
|
-
if (
|
|
5998
|
-
|
|
6217
|
+
if (admitted.admission !== "new") {
|
|
6218
|
+
if (admitted.response && admitted.admission !== "unknown") sendReplayWorkerMessage(ws, admitted.response);
|
|
6219
|
+
if (!(admitted.admission === "unknown" && message.type === "delete_project_branch" && await recoveryJournal.readmitUnknownBranchDeletion(
|
|
6220
|
+
operationRequestId,
|
|
6221
|
+
"sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0
|
|
6222
|
+
))) {
|
|
5999
6223
|
return;
|
|
6000
6224
|
}
|
|
6001
6225
|
}
|
|
6226
|
+
if (recoveryJournal.isRequestCancelled(operationRequestId)) {
|
|
6227
|
+
recoveryJournal.unknown(operationRequestId);
|
|
6228
|
+
return;
|
|
6229
|
+
}
|
|
6002
6230
|
}
|
|
6003
6231
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
6004
6232
|
const admissionCredentials = configuredCredentialGenerationFingerprint;
|
|
6005
6233
|
const assertMessageAdmission = () => {
|
|
6006
6234
|
if (operationRequestId) {
|
|
6007
|
-
if (workerLeaseExpired ||
|
|
6008
|
-
|
|
6235
|
+
if (workerLeaseExpired || recoveryJournal.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
|
|
6236
|
+
recoveryJournal.unknown(operationRequestId);
|
|
6009
6237
|
throw new StaleWorkerAdmissionError();
|
|
6010
6238
|
}
|
|
6011
6239
|
return;
|
|
@@ -6142,7 +6370,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6142
6370
|
}
|
|
6143
6371
|
recoveryDeadline ??= Date.now() + import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS;
|
|
6144
6372
|
if (workerLeaseExpired || Date.now() >= recoveryDeadline) {
|
|
6145
|
-
|
|
6373
|
+
recoveryJournal.unknown(message.requestId);
|
|
6146
6374
|
return;
|
|
6147
6375
|
}
|
|
6148
6376
|
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
@@ -6172,7 +6400,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6172
6400
|
projectId: message.projectId,
|
|
6173
6401
|
branchName: message.targetBranch
|
|
6174
6402
|
};
|
|
6175
|
-
const created = await workspaceSyncSingleFlight.runBranchMutation(() => {
|
|
6403
|
+
const created = await workspaceSyncSingleFlight.runBranchMutation(async () => {
|
|
6176
6404
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6177
6405
|
throw new Error(
|
|
6178
6406
|
`Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
|
|
@@ -6187,7 +6415,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6187
6415
|
const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
|
|
6188
6416
|
({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
|
|
6189
6417
|
);
|
|
6190
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
|
|
6418
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
|
|
6191
6419
|
let gitBranchCreated = false;
|
|
6192
6420
|
try {
|
|
6193
6421
|
if (!readyProjectIds.has(project.projectId)) {
|
|
@@ -6208,7 +6436,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6208
6436
|
}
|
|
6209
6437
|
return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
|
|
6210
6438
|
}
|
|
6211
|
-
const createdBranch = (0, import_project_worktrees.createOrRetryLinkedProjectBranch)({
|
|
6439
|
+
const createdBranch = await (0, import_project_worktrees.createOrRetryLinkedProjectBranch)({
|
|
6212
6440
|
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
6213
6441
|
primaryBranchName: primaryProjectBranch(project),
|
|
6214
6442
|
sourceBranchName: message.sourceBranch,
|
|
@@ -6233,7 +6461,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6233
6461
|
return createdBranch;
|
|
6234
6462
|
} catch (error) {
|
|
6235
6463
|
if (!pendingWasRecorded && !gitBranchCreated && !(0, import_project_worktrees.projectBranchMayExistAfterCreateFailure)(error)) {
|
|
6236
|
-
projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
|
|
6464
|
+
projectWorkspaceState = await projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
|
|
6237
6465
|
}
|
|
6238
6466
|
throw error;
|
|
6239
6467
|
}
|
|
@@ -6277,7 +6505,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6277
6505
|
});
|
|
6278
6506
|
try {
|
|
6279
6507
|
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
6280
|
-
const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(() => {
|
|
6508
|
+
const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(async () => {
|
|
6281
6509
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6282
6510
|
throw new Error(
|
|
6283
6511
|
`Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
|
|
@@ -6304,12 +6532,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
6304
6532
|
if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
|
|
6305
6533
|
return false;
|
|
6306
6534
|
}
|
|
6307
|
-
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
6535
|
+
projectWorkspaceState = await projectWorkspaceStateStore.beginBranchDeletion({
|
|
6308
6536
|
projectId: message.projectId,
|
|
6309
6537
|
branchName: message.branchName
|
|
6310
6538
|
});
|
|
6311
6539
|
if (pendingLocalBranch) {
|
|
6312
|
-
projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
|
|
6540
|
+
projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
|
|
6313
6541
|
}
|
|
6314
6542
|
pendingCreatedBranchPublicationNotBefore.delete((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(message.projectId, message.branchName));
|
|
6315
6543
|
if (pendingMirrorDeletes.has(deletionKey)) return true;
|
|
@@ -6413,7 +6641,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6413
6641
|
}
|
|
6414
6642
|
if (message.type === "exec_terminal_ack") {
|
|
6415
6643
|
pendingProcessTerminals.delete(message.runId);
|
|
6416
|
-
|
|
6644
|
+
recoveryJournal.acknowledgeTerminal(message.runId);
|
|
6417
6645
|
return;
|
|
6418
6646
|
}
|
|
6419
6647
|
if (message.type === "port_forward_connect") {
|
|
@@ -6468,42 +6696,6 @@ async function startWorker(options, projectRuntime = {
|
|
|
6468
6696
|
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
|
|
6469
6697
|
return;
|
|
6470
6698
|
}
|
|
6471
|
-
if (message.type === "cancel") {
|
|
6472
|
-
let resourceCancellationError;
|
|
6473
|
-
try {
|
|
6474
|
-
await workerCommandLauncher?.cancel(message.runId);
|
|
6475
|
-
} catch (error) {
|
|
6476
|
-
resourceCancellationError = error instanceof Error ? error.message : String(error);
|
|
6477
|
-
}
|
|
6478
|
-
const active = activeProcesses.get(message.runId);
|
|
6479
|
-
let cancelled = resourceCancellationError === void 0;
|
|
6480
|
-
let cancelMessage;
|
|
6481
|
-
if (active) {
|
|
6482
|
-
try {
|
|
6483
|
-
closeProcessStdin(active);
|
|
6484
|
-
await (0, import_process_tree.terminateProcessTree)(active.process);
|
|
6485
|
-
cancelMessage = `Stopped ${active.command}`;
|
|
6486
|
-
} catch (error) {
|
|
6487
|
-
cancelled = false;
|
|
6488
|
-
cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
|
|
6489
|
-
}
|
|
6490
|
-
} else {
|
|
6491
|
-
cancelledProcessRuns.add(message.runId);
|
|
6492
|
-
cancelMessage = "Cancellation queued before command start";
|
|
6493
|
-
}
|
|
6494
|
-
if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
|
|
6495
|
-
sendSerializedWorkerMessage(
|
|
6496
|
-
ws,
|
|
6497
|
-
JSON.stringify({
|
|
6498
|
-
type: "cancel_result",
|
|
6499
|
-
requestId: message.requestId,
|
|
6500
|
-
runId: message.runId,
|
|
6501
|
-
cancelled,
|
|
6502
|
-
message: cancelMessage
|
|
6503
|
-
})
|
|
6504
|
-
);
|
|
6505
|
-
return;
|
|
6506
|
-
}
|
|
6507
6699
|
if (message.type === "exec_stdin") {
|
|
6508
6700
|
const sendAck = (payload) => sendSerializedWorkerMessage(
|
|
6509
6701
|
ws,
|
|
@@ -6584,10 +6776,20 @@ async function startWorker(options, projectRuntime = {
|
|
|
6584
6776
|
let mutationLeaseTransferred = false;
|
|
6585
6777
|
try {
|
|
6586
6778
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6779
|
+
const reservationAbort = registerPendingReservation(`pty:${message.ptyId}`, void 0);
|
|
6780
|
+
try {
|
|
6781
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(
|
|
6782
|
+
message.target,
|
|
6783
|
+
workspaceSyncSingleFlight,
|
|
6784
|
+
() => {
|
|
6785
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
6786
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6787
|
+
},
|
|
6788
|
+
{ signal: reservationAbort.signal }
|
|
6789
|
+
);
|
|
6790
|
+
} finally {
|
|
6791
|
+
unregisterPendingReservation(`pty:${message.ptyId}`, reservationAbort);
|
|
6792
|
+
}
|
|
6591
6793
|
releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
|
|
6592
6794
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6593
6795
|
throw new Error(
|
|
@@ -6652,6 +6854,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6652
6854
|
}
|
|
6653
6855
|
if (message.type === "pty_close") {
|
|
6654
6856
|
const activePty = activePtys.get(message.ptyId);
|
|
6857
|
+
abortPendingReservation(`pty:${message.ptyId}`, "Shell closed before it started");
|
|
6655
6858
|
await closePty(message);
|
|
6656
6859
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6657
6860
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
@@ -6688,11 +6891,21 @@ async function startWorker(options, projectRuntime = {
|
|
|
6688
6891
|
try {
|
|
6689
6892
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6690
6893
|
if (hasWorkspaceEffect) {
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6894
|
+
const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
|
|
6895
|
+
try {
|
|
6896
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(
|
|
6897
|
+
message.target,
|
|
6898
|
+
workspaceSyncSingleFlight,
|
|
6899
|
+
() => {
|
|
6900
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
6901
|
+
targetReserved = true;
|
|
6902
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6903
|
+
},
|
|
6904
|
+
{ signal: reservationAbort.signal }
|
|
6905
|
+
);
|
|
6906
|
+
} finally {
|
|
6907
|
+
unregisterPendingReservation(`run:${message.runId}`, reservationAbort);
|
|
6908
|
+
}
|
|
6696
6909
|
}
|
|
6697
6910
|
const runCommand = async () => {
|
|
6698
6911
|
if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
|
|
@@ -6744,7 +6957,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6744
6957
|
cancelledProcessRuns.delete(message.runId);
|
|
6745
6958
|
}
|
|
6746
6959
|
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6747
|
-
|
|
6960
|
+
recoveryJournal.unknown(message.requestId);
|
|
6748
6961
|
return;
|
|
6749
6962
|
}
|
|
6750
6963
|
sendSerializedWorkerMessage(ws, JSON.stringify(result));
|
|
@@ -6818,11 +7031,21 @@ async function startWorker(options, projectRuntime = {
|
|
|
6818
7031
|
try {
|
|
6819
7032
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6820
7033
|
if (hasWorkspaceEffect) {
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
7034
|
+
const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
|
|
7035
|
+
try {
|
|
7036
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(
|
|
7037
|
+
message.target,
|
|
7038
|
+
workspaceSyncSingleFlight,
|
|
7039
|
+
() => {
|
|
7040
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
7041
|
+
targetReserved = true;
|
|
7042
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
7043
|
+
},
|
|
7044
|
+
{ signal: reservationAbort.signal }
|
|
7045
|
+
);
|
|
7046
|
+
} finally {
|
|
7047
|
+
unregisterPendingReservation(`run:${message.runId}`, reservationAbort);
|
|
7048
|
+
}
|
|
6826
7049
|
}
|
|
6827
7050
|
await (0, import_workspace_command_sync_policy.runReservedWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand, () => {
|
|
6828
7051
|
if (targetReserved) {
|
|
@@ -6857,16 +7080,40 @@ async function startWorker(options, projectRuntime = {
|
|
|
6857
7080
|
return;
|
|
6858
7081
|
}
|
|
6859
7082
|
const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
|
|
6860
|
-
const
|
|
7083
|
+
const mutatesFiles = message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing;
|
|
7084
|
+
const mutatesVisibleWorkspace = mutatesFiles && targetMayMutateVisibleWorkspace(message.target);
|
|
6861
7085
|
if (reservesVisibleWorkspace) {
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
7086
|
+
const reservationAbort = registerPendingReservation(
|
|
7087
|
+
`op:${message.requestId}`,
|
|
7088
|
+
"sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0
|
|
7089
|
+
);
|
|
7090
|
+
try {
|
|
7091
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(
|
|
7092
|
+
message.target,
|
|
7093
|
+
workspaceSyncSingleFlight,
|
|
7094
|
+
() => {
|
|
7095
|
+
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
7096
|
+
if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
|
|
7097
|
+
},
|
|
7098
|
+
{ signal: reservationAbort.signal }
|
|
7099
|
+
);
|
|
7100
|
+
} catch (error) {
|
|
7101
|
+
sendSerializedWorkerMessage(
|
|
7102
|
+
ws,
|
|
7103
|
+
JSON.stringify({
|
|
7104
|
+
type: "operation_result",
|
|
7105
|
+
requestId: message.requestId,
|
|
7106
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7107
|
+
})
|
|
7108
|
+
);
|
|
7109
|
+
return;
|
|
7110
|
+
} finally {
|
|
7111
|
+
unregisterPendingReservation(`op:${message.requestId}`, reservationAbort);
|
|
7112
|
+
}
|
|
6866
7113
|
}
|
|
6867
7114
|
let dirtyTrigger;
|
|
6868
7115
|
try {
|
|
6869
|
-
const
|
|
7116
|
+
const operation = (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
|
|
6870
7117
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
6871
7118
|
return await executeOperation({
|
|
6872
7119
|
message,
|
|
@@ -6877,6 +7124,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6877
7124
|
assertAdmission: assertMessageAdmission
|
|
6878
7125
|
});
|
|
6879
7126
|
});
|
|
7127
|
+
if (mutatesFiles && typeof message.sessionId === "string") sessionFileMutations.track(message.sessionId, operation);
|
|
7128
|
+
const result = await operation;
|
|
6880
7129
|
sendSerializedWorkerMessage(
|
|
6881
7130
|
ws,
|
|
6882
7131
|
JSON.stringify({
|
|
@@ -6960,6 +7209,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6960
7209
|
projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
|
|
6961
7210
|
}
|
|
6962
7211
|
if (currentWorkerSocket === ws) currentWorkerSocket = null;
|
|
7212
|
+
abortPendingReservations("Worker connection closed before command start");
|
|
6963
7213
|
workerRecoveryId = "reconnecting";
|
|
6964
7214
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6965
7215
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
@@ -6993,6 +7243,11 @@ async function startWorker(options, projectRuntime = {
|
|
|
6993
7243
|
});
|
|
6994
7244
|
}
|
|
6995
7245
|
function installWorkerRuntimeFailureLogging() {
|
|
7246
|
+
(0, import_workspace_filesystem_executor.installWorkspaceFilesystemExecutorFailureHandler)((error) => {
|
|
7247
|
+
process.stderr.write(`[r5d-worker] ${error.message}; restarting the worker runtime
|
|
7248
|
+
`);
|
|
7249
|
+
void exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
|
|
7250
|
+
});
|
|
6996
7251
|
process.on("uncaughtException", (error) => {
|
|
6997
7252
|
process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
6998
7253
|
`);
|
|
@@ -7005,11 +7260,6 @@ function installWorkerRuntimeFailureLogging() {
|
|
|
7005
7260
|
});
|
|
7006
7261
|
}
|
|
7007
7262
|
async function main() {
|
|
7008
|
-
if (process.argv[2] === import_project_snapshot_recovery_runner.PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND) {
|
|
7009
|
-
const exitCode2 = (0, import_project_snapshot_recovery_runner.runProjectSnapshotRecoveryHelper)(process.argv.slice(3));
|
|
7010
|
-
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
7011
|
-
return;
|
|
7012
|
-
}
|
|
7013
7263
|
const parsed = parseArgs(process.argv.slice(2));
|
|
7014
7264
|
if (parsed.command === "help") {
|
|
7015
7265
|
printHelp();
|