@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/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,31 @@ 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;
|
|
350
|
+
pendingReservationAborts.delete(key);
|
|
351
|
+
pending.controller.abort(new WorkspaceMountHoldAbortedError(reason));
|
|
352
|
+
}
|
|
353
|
+
function abortPendingReservations(reason, sessionId) {
|
|
354
|
+
for (const [key, pending] of [...pendingReservationAborts]) {
|
|
355
|
+
if (sessionId !== void 0 && pending.sessionId !== sessionId) continue;
|
|
356
|
+
abortPendingReservation(key, reason);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
321
359
|
let workspaceSyncQueue = Promise.resolve();
|
|
322
360
|
let startupProjectSnapshotRecoveryCompleted = false;
|
|
323
361
|
const checkoutGarbageCollectors = /* @__PURE__ */ new Map();
|
|
@@ -352,6 +390,17 @@ const workspaceSyncSingleFlight = {
|
|
|
352
390
|
},
|
|
353
391
|
afterCurrent() {
|
|
354
392
|
return workspaceSyncQueue;
|
|
393
|
+
},
|
|
394
|
+
mountHold(target, signal) {
|
|
395
|
+
if (target.type !== "project") return null;
|
|
396
|
+
const mountIds = [projectMountId(target.projectId, target.branchName)];
|
|
397
|
+
if (!workspaceMountHoldFence.isHeld(mountIds)) return null;
|
|
398
|
+
const holder = workspaceMountHoldFence.describeHold(mountIds);
|
|
399
|
+
process.stdout.write(
|
|
400
|
+
`[r5d-worker] command for ${target.projectId}/${target.branchName} waits for ${holder ?? "a workspace filesystem job"}
|
|
401
|
+
`
|
|
402
|
+
);
|
|
403
|
+
return workspaceMountHoldFence.waitForRelease(mountIds, { signal });
|
|
355
404
|
}
|
|
356
405
|
};
|
|
357
406
|
let githubCredential = null;
|
|
@@ -1008,14 +1057,15 @@ async function runGitAsync(args, options = {}) {
|
|
|
1008
1057
|
env: workerGitProcessEnvironment(),
|
|
1009
1058
|
...bounded ? { detached: true } : {}
|
|
1010
1059
|
});
|
|
1011
|
-
const completion = Promise.all([
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
subprocess.exited
|
|
1015
|
-
]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode }));
|
|
1060
|
+
const completion = Promise.all([new Response(subprocess.stdout).text(), new Response(subprocess.stderr).text(), subprocess.exited]).then(
|
|
1061
|
+
([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode })
|
|
1062
|
+
);
|
|
1016
1063
|
let timer;
|
|
1017
1064
|
const timedOut = bounded ? new Promise((_resolve, reject) => {
|
|
1018
|
-
timer = setTimeout(
|
|
1065
|
+
timer = setTimeout(
|
|
1066
|
+
() => reject(new GitTransportTimeoutError(`git ${args.join(" ")} timed out after ${options.timeoutMs}ms`)),
|
|
1067
|
+
options.timeoutMs
|
|
1068
|
+
);
|
|
1019
1069
|
}) : null;
|
|
1020
1070
|
try {
|
|
1021
1071
|
const { stdout, stderr, exitCode } = timedOut ? await Promise.race([completion, timedOut]) : await completion;
|
|
@@ -2104,12 +2154,12 @@ async function streamCommandOutput(stream, onData) {
|
|
|
2104
2154
|
}
|
|
2105
2155
|
const text = decoder.decode(chunk.value, { stream: true });
|
|
2106
2156
|
if (text.length > 0) {
|
|
2107
|
-
onData(text);
|
|
2157
|
+
await onData(text);
|
|
2108
2158
|
}
|
|
2109
2159
|
}
|
|
2110
2160
|
const trailing = decoder.decode();
|
|
2111
2161
|
if (trailing.length > 0) {
|
|
2112
|
-
onData(trailing);
|
|
2162
|
+
await onData(trailing);
|
|
2113
2163
|
}
|
|
2114
2164
|
} finally {
|
|
2115
2165
|
reader.releaseLock();
|
|
@@ -2248,12 +2298,21 @@ function mutationQueueKey(target, resolved) {
|
|
|
2248
2298
|
}
|
|
2249
2299
|
return `${describeWorkerSessionTarget(target)}:${path.posix.normalize(resolved.repoRelativePath)}`;
|
|
2250
2300
|
}
|
|
2251
|
-
function
|
|
2301
|
+
async function statIfExists(targetPath) {
|
|
2302
|
+
try {
|
|
2303
|
+
return await fs.promises.stat(targetPath);
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
const code = error.code;
|
|
2306
|
+
if (code === "ENOENT" || code === "ENOTDIR") return null;
|
|
2307
|
+
throw error;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
async function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
|
|
2252
2311
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2253
|
-
if (!
|
|
2312
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2254
2313
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2255
2314
|
}
|
|
2256
|
-
const buffer = fs.
|
|
2315
|
+
const buffer = await fs.promises.readFile(resolved.absolutePath);
|
|
2257
2316
|
const formatted = formatLineNumberedContent(buffer.toString("utf8"), offset, limit);
|
|
2258
2317
|
return {
|
|
2259
2318
|
type: "read",
|
|
@@ -2263,25 +2322,25 @@ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
|
|
|
2263
2322
|
...formatted
|
|
2264
2323
|
};
|
|
2265
2324
|
}
|
|
2266
|
-
function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
|
|
2325
|
+
async function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
|
|
2267
2326
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2268
|
-
fs.
|
|
2269
|
-
fs.
|
|
2327
|
+
await fs.promises.mkdir(path.dirname(resolved.absolutePath), { recursive: true });
|
|
2328
|
+
await fs.promises.writeFile(resolved.absolutePath, content, "utf8");
|
|
2270
2329
|
return {
|
|
2271
2330
|
type: "write",
|
|
2272
2331
|
file: resolved.displayPath,
|
|
2273
2332
|
gitBlobHash: getGitBlobHashForContent(content)
|
|
2274
2333
|
};
|
|
2275
2334
|
}
|
|
2276
|
-
function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
2335
|
+
async function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
2277
2336
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2278
2337
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
2279
2338
|
throw new Error("edit requires at least one replacement");
|
|
2280
2339
|
}
|
|
2281
|
-
if (!
|
|
2340
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2282
2341
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2283
2342
|
}
|
|
2284
|
-
const originalContent = fs.
|
|
2343
|
+
const originalContent = await fs.promises.readFile(resolved.absolutePath, "utf8");
|
|
2285
2344
|
const ranges = edits.map((edit, index) => {
|
|
2286
2345
|
if (edit.oldText === edit.newText) {
|
|
2287
2346
|
throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
|
|
@@ -2313,7 +2372,7 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
|
2313
2372
|
for (const range of [...sortedRanges].reverse()) {
|
|
2314
2373
|
nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
|
|
2315
2374
|
}
|
|
2316
|
-
fs.
|
|
2375
|
+
await fs.promises.writeFile(resolved.absolutePath, nextContent, "utf8");
|
|
2317
2376
|
return {
|
|
2318
2377
|
type: "edit",
|
|
2319
2378
|
file: resolved.displayPath,
|
|
@@ -2346,7 +2405,7 @@ async function executePlanOperation(input) {
|
|
|
2346
2405
|
let created = false;
|
|
2347
2406
|
if (message.createIfMissing) {
|
|
2348
2407
|
try {
|
|
2349
|
-
fs.
|
|
2408
|
+
await fs.promises.writeFile(resolved.absolutePath, createPlanMarkdown(message.planId), { encoding: "utf8", flag: "wx" });
|
|
2350
2409
|
created = true;
|
|
2351
2410
|
} catch (error) {
|
|
2352
2411
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -2355,11 +2414,16 @@ async function executePlanOperation(input) {
|
|
|
2355
2414
|
return {
|
|
2356
2415
|
type: "read_plan",
|
|
2357
2416
|
created,
|
|
2358
|
-
read: readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2417
|
+
read: await readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2359
2418
|
};
|
|
2360
2419
|
}
|
|
2361
|
-
const content = fs.
|
|
2362
|
-
writeWorkerTextFile(
|
|
2420
|
+
const content = await fs.promises.readFile(resolved.absolutePath, "utf8");
|
|
2421
|
+
await writeWorkerTextFile(
|
|
2422
|
+
resolvedTarget.rootPath,
|
|
2423
|
+
filePath,
|
|
2424
|
+
updateTaskCheckbox(content, message.taskId, message.checked),
|
|
2425
|
+
builtInPaths
|
|
2426
|
+
);
|
|
2363
2427
|
return {
|
|
2364
2428
|
type: "update_task_status",
|
|
2365
2429
|
planId: message.planId,
|
|
@@ -2380,7 +2444,11 @@ async function executeReadFileOperation(input) {
|
|
|
2380
2444
|
rootDir: input.rootDir,
|
|
2381
2445
|
access: "read"
|
|
2382
2446
|
});
|
|
2383
|
-
|
|
2447
|
+
const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
|
|
2448
|
+
return withFileMutationQueue(
|
|
2449
|
+
mutationQueueKey(input.resolvedTarget.target, resolved),
|
|
2450
|
+
() => readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths)
|
|
2451
|
+
);
|
|
2384
2452
|
}
|
|
2385
2453
|
async function executeWriteFileOperation(input) {
|
|
2386
2454
|
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
@@ -2433,16 +2501,16 @@ function wildcardToRegex(pattern) {
|
|
|
2433
2501
|
function normalizeFindPattern(pattern) {
|
|
2434
2502
|
return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
|
|
2435
2503
|
}
|
|
2436
|
-
function walkWorkerEntries(branchPath, start) {
|
|
2504
|
+
async function walkWorkerEntries(branchPath, start) {
|
|
2437
2505
|
const entries = [];
|
|
2438
2506
|
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2439
|
-
const realVirtualRoot = start.scope === "virtual" ? fs.
|
|
2440
|
-
const visit = (absolutePath, isRoot) => {
|
|
2507
|
+
const realVirtualRoot = start.scope === "virtual" ? await fs.promises.realpath(start.virtualRootPath) : null;
|
|
2508
|
+
const visit = async (absolutePath, isRoot) => {
|
|
2441
2509
|
let stat;
|
|
2442
2510
|
try {
|
|
2443
|
-
stat = fs.
|
|
2511
|
+
stat = await fs.promises.stat(absolutePath);
|
|
2444
2512
|
if (realVirtualRoot) {
|
|
2445
|
-
assertInsideRoot(realVirtualRoot, fs.
|
|
2513
|
+
assertInsideRoot(realVirtualRoot, await fs.promises.realpath(absolutePath), `${start.displayPath} path`);
|
|
2446
2514
|
}
|
|
2447
2515
|
} catch (error) {
|
|
2448
2516
|
if (isRoot) throw error;
|
|
@@ -2455,7 +2523,7 @@ function walkWorkerEntries(branchPath, start) {
|
|
|
2455
2523
|
if (!stat.isDirectory()) return;
|
|
2456
2524
|
let realDirectoryPath;
|
|
2457
2525
|
try {
|
|
2458
|
-
realDirectoryPath = fs.
|
|
2526
|
+
realDirectoryPath = await fs.promises.realpath(absolutePath);
|
|
2459
2527
|
} catch (error) {
|
|
2460
2528
|
if (isRoot) throw error;
|
|
2461
2529
|
return;
|
|
@@ -2464,25 +2532,25 @@ function walkWorkerEntries(branchPath, start) {
|
|
|
2464
2532
|
visitedDirectories.add(realDirectoryPath);
|
|
2465
2533
|
let children;
|
|
2466
2534
|
try {
|
|
2467
|
-
children = fs.
|
|
2535
|
+
children = (await fs.promises.readdir(absolutePath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2468
2536
|
} catch (error) {
|
|
2469
2537
|
if (isRoot) throw error;
|
|
2470
2538
|
return;
|
|
2471
2539
|
}
|
|
2472
2540
|
for (const child of children) {
|
|
2473
2541
|
if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
|
|
2474
|
-
visit(path.join(absolutePath, child.name), false);
|
|
2542
|
+
await visit(path.join(absolutePath, child.name), false);
|
|
2475
2543
|
}
|
|
2476
2544
|
};
|
|
2477
|
-
visit(start.absolutePath, true);
|
|
2545
|
+
await visit(start.absolutePath, true);
|
|
2478
2546
|
return entries;
|
|
2479
2547
|
}
|
|
2480
2548
|
function isProbablyText(bytes) {
|
|
2481
2549
|
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
2482
2550
|
}
|
|
2483
|
-
function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
2551
|
+
async function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
2484
2552
|
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
2485
|
-
if (!
|
|
2553
|
+
if (!await statIfExists(start.absolutePath)) {
|
|
2486
2554
|
throw new Error(`Path not found: ${start.displayPath}`);
|
|
2487
2555
|
}
|
|
2488
2556
|
const limit = normalizeToolLimit(input.limit, 100, 1e3);
|
|
@@ -2492,12 +2560,12 @@ function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
|
2492
2560
|
const lines = [];
|
|
2493
2561
|
let matchCount = 0;
|
|
2494
2562
|
let truncated = false;
|
|
2495
|
-
for (const entry of walkWorkerEntries(branchPath, start)) {
|
|
2563
|
+
for (const entry of await walkWorkerEntries(branchPath, start)) {
|
|
2496
2564
|
if (!entry.stat.isFile()) continue;
|
|
2497
2565
|
if (globRegex && !globRegex.test(entry.matchPath)) continue;
|
|
2498
2566
|
let bytes;
|
|
2499
2567
|
try {
|
|
2500
|
-
bytes = fs.
|
|
2568
|
+
bytes = await fs.promises.readFile(entry.absolutePath);
|
|
2501
2569
|
} catch {
|
|
2502
2570
|
continue;
|
|
2503
2571
|
}
|
|
@@ -2550,9 +2618,9 @@ async function executeGrepOperation(input) {
|
|
|
2550
2618
|
builtInPaths
|
|
2551
2619
|
);
|
|
2552
2620
|
}
|
|
2553
|
-
function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
2621
|
+
async function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
2554
2622
|
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
2555
|
-
if (!
|
|
2623
|
+
if (!(await statIfExists(start.absolutePath))?.isDirectory()) {
|
|
2556
2624
|
throw new Error(`Directory not found: ${start.displayPath}`);
|
|
2557
2625
|
}
|
|
2558
2626
|
const pattern = normalizeFindPattern(input.pattern ?? "*");
|
|
@@ -2562,7 +2630,7 @@ function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
|
2562
2630
|
const matches = [];
|
|
2563
2631
|
let count = 0;
|
|
2564
2632
|
let truncated = false;
|
|
2565
|
-
for (const entry of walkWorkerEntries(branchPath, start)) {
|
|
2633
|
+
for (const entry of await walkWorkerEntries(branchPath, start)) {
|
|
2566
2634
|
if (entry.absolutePath === start.absolutePath) continue;
|
|
2567
2635
|
const isDirectory = entry.stat.isDirectory();
|
|
2568
2636
|
if (entryType === "file" && !entry.stat.isFile()) continue;
|
|
@@ -2607,13 +2675,13 @@ async function executeFindOperation(input) {
|
|
|
2607
2675
|
builtInPaths
|
|
2608
2676
|
);
|
|
2609
2677
|
}
|
|
2610
|
-
function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
|
|
2678
|
+
async function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
|
|
2611
2679
|
const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
|
|
2612
|
-
if (!
|
|
2680
|
+
if (!(await statIfExists(resolved.absolutePath))?.isDirectory()) {
|
|
2613
2681
|
throw new Error(`Directory not found: ${resolved.displayPath}`);
|
|
2614
2682
|
}
|
|
2615
2683
|
const limit = normalizeToolLimit(inputLimit, 200, 1e3);
|
|
2616
|
-
const entries = fs.
|
|
2684
|
+
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
2685
|
const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
2618
2686
|
const truncated = entries.length > displayed.length;
|
|
2619
2687
|
const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
|
|
@@ -2664,36 +2732,40 @@ function resolveWorkerCodePath(branchPath, inputPath) {
|
|
|
2664
2732
|
}
|
|
2665
2733
|
return { absolutePath, displayPath };
|
|
2666
2734
|
}
|
|
2667
|
-
function listWorkerCodeDirectory(branchPath, inputPath) {
|
|
2735
|
+
async function listWorkerCodeDirectory(branchPath, inputPath) {
|
|
2668
2736
|
const resolved = resolveWorkerCodePath(branchPath, inputPath);
|
|
2669
|
-
if (!fs.
|
|
2737
|
+
if (!(await fs.promises.stat(resolved.absolutePath)).isDirectory()) {
|
|
2670
2738
|
throw new Error(`Code directory not found: ${resolved.displayPath}`);
|
|
2671
2739
|
}
|
|
2672
|
-
const
|
|
2740
|
+
const listed = (await fs.promises.readdir(resolved.absolutePath, { withFileTypes: true })).filter(
|
|
2741
|
+
(entry) => entry.name.toLowerCase() !== ".git"
|
|
2742
|
+
);
|
|
2743
|
+
const entries = [];
|
|
2744
|
+
for (const entry of listed) {
|
|
2673
2745
|
const entryPath = path.posix.join(resolved.displayPath, entry.name);
|
|
2674
2746
|
try {
|
|
2675
2747
|
const resolvedEntry = resolveWorkerCodePath(branchPath, entryPath);
|
|
2676
|
-
const stats = fs.
|
|
2677
|
-
if (!stats.isFile() && !stats.isDirectory())
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
}
|
|
2685
|
-
];
|
|
2748
|
+
const stats = await fs.promises.stat(resolvedEntry.absolutePath);
|
|
2749
|
+
if (!stats.isFile() && !stats.isDirectory()) continue;
|
|
2750
|
+
entries.push({
|
|
2751
|
+
name: entry.name,
|
|
2752
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
2753
|
+
size: stats.isDirectory() ? null : stats.size,
|
|
2754
|
+
mtime: stats.mtime.toISOString()
|
|
2755
|
+
});
|
|
2686
2756
|
} catch {
|
|
2687
|
-
return [];
|
|
2688
2757
|
}
|
|
2689
|
-
}
|
|
2758
|
+
}
|
|
2759
|
+
entries.sort(
|
|
2760
|
+
(left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name)
|
|
2761
|
+
);
|
|
2690
2762
|
return { type: "code_list", path: resolved.displayPath, entries };
|
|
2691
2763
|
}
|
|
2692
|
-
function readWorkerCodeFile(branchPath, inputPath) {
|
|
2764
|
+
async function readWorkerCodeFile(branchPath, inputPath) {
|
|
2693
2765
|
const resolved = resolveWorkerCodePath(branchPath, inputPath);
|
|
2694
|
-
const stats = fs.
|
|
2766
|
+
const stats = await fs.promises.stat(resolved.absolutePath);
|
|
2695
2767
|
if (!stats.isFile()) throw new Error(`Code file not found: ${resolved.displayPath}`);
|
|
2696
|
-
const bytes = fs.
|
|
2768
|
+
const bytes = await fs.promises.readFile(resolved.absolutePath);
|
|
2697
2769
|
return {
|
|
2698
2770
|
type: "code_read",
|
|
2699
2771
|
path: resolved.displayPath,
|
|
@@ -2702,17 +2774,17 @@ function readWorkerCodeFile(branchPath, inputPath) {
|
|
|
2702
2774
|
base64: bytes.toString("base64")
|
|
2703
2775
|
};
|
|
2704
2776
|
}
|
|
2705
|
-
function executeCodeListOperation(input) {
|
|
2777
|
+
async function executeCodeListOperation(input) {
|
|
2706
2778
|
if (input.resolvedTarget.target.type !== "project") throw new Error("Code listing requires a project branch target");
|
|
2707
2779
|
return listWorkerCodeDirectory(input.resolvedTarget.rootPath, input.message.path);
|
|
2708
2780
|
}
|
|
2709
|
-
function executeCodeReadOperation(input) {
|
|
2781
|
+
async function executeCodeReadOperation(input) {
|
|
2710
2782
|
if (input.resolvedTarget.target.type !== "project") throw new Error("Code reading requires a project branch target");
|
|
2711
2783
|
return readWorkerCodeFile(input.resolvedTarget.rootPath, input.message.path);
|
|
2712
2784
|
}
|
|
2713
|
-
function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
2785
|
+
async function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
2714
2786
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
2715
|
-
if (!
|
|
2787
|
+
if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
|
|
2716
2788
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
2717
2789
|
}
|
|
2718
2790
|
const extension = path.extname(resolved.absolutePath).toLowerCase();
|
|
@@ -2720,7 +2792,7 @@ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
|
2720
2792
|
if (!mediaType) {
|
|
2721
2793
|
throw new Error("read supports PNG and JPEG image inputs only");
|
|
2722
2794
|
}
|
|
2723
|
-
const bytes = fs.
|
|
2795
|
+
const bytes = await fs.promises.readFile(resolved.absolutePath);
|
|
2724
2796
|
const dimensions = readImageDimensions(bytes, mediaType);
|
|
2725
2797
|
return {
|
|
2726
2798
|
type: "view_file_bytes",
|
|
@@ -3047,26 +3119,28 @@ async function executeStreamingCommand(input) {
|
|
|
3047
3119
|
}
|
|
3048
3120
|
const [exitCode] = await Promise.all([
|
|
3049
3121
|
subprocess.exited,
|
|
3050
|
-
streamCommandOutput(
|
|
3051
|
-
|
|
3122
|
+
streamCommandOutput(
|
|
3123
|
+
subprocess.stdout,
|
|
3124
|
+
(data) => sendWorkerOutput(input.ws, {
|
|
3052
3125
|
type: "exec_output",
|
|
3053
3126
|
runId: input.message.runId,
|
|
3054
3127
|
stream: "stdout",
|
|
3055
3128
|
data
|
|
3056
|
-
})
|
|
3057
|
-
|
|
3058
|
-
streamCommandOutput(
|
|
3059
|
-
|
|
3129
|
+
})
|
|
3130
|
+
),
|
|
3131
|
+
streamCommandOutput(
|
|
3132
|
+
subprocess.stderr,
|
|
3133
|
+
(data) => sendWorkerOutput(input.ws, {
|
|
3060
3134
|
type: "exec_output",
|
|
3061
3135
|
runId: input.message.runId,
|
|
3062
3136
|
stream: "stderr",
|
|
3063
3137
|
data
|
|
3064
|
-
})
|
|
3065
|
-
|
|
3138
|
+
})
|
|
3139
|
+
)
|
|
3066
3140
|
]);
|
|
3067
3141
|
const diagnostic = await input.resources.diagnose();
|
|
3068
3142
|
if (diagnostic) {
|
|
3069
|
-
|
|
3143
|
+
await sendWorkerOutput(input.ws, {
|
|
3070
3144
|
type: "exec_output",
|
|
3071
3145
|
runId: input.message.runId,
|
|
3072
3146
|
stream: "stderr",
|
|
@@ -3138,6 +3212,33 @@ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
|
|
|
3138
3212
|
);
|
|
3139
3213
|
}
|
|
3140
3214
|
}
|
|
3215
|
+
async function cancelProcessRun(runId) {
|
|
3216
|
+
let resourceCancellationError;
|
|
3217
|
+
try {
|
|
3218
|
+
await workerCommandLauncher?.cancel(runId);
|
|
3219
|
+
} catch (error) {
|
|
3220
|
+
resourceCancellationError = error instanceof Error ? error.message : String(error);
|
|
3221
|
+
}
|
|
3222
|
+
const active = activeProcesses.get(runId);
|
|
3223
|
+
let cancelled = resourceCancellationError === void 0;
|
|
3224
|
+
let cancelMessage;
|
|
3225
|
+
if (active) {
|
|
3226
|
+
try {
|
|
3227
|
+
closeProcessStdin(active);
|
|
3228
|
+
await terminateProcessTree(active.process);
|
|
3229
|
+
cancelMessage = `Stopped ${active.command}`;
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
cancelled = false;
|
|
3232
|
+
cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3233
|
+
}
|
|
3234
|
+
} else {
|
|
3235
|
+
cancelledProcessRuns.add(runId);
|
|
3236
|
+
abortPendingReservation(`run:${runId}`, "Cancellation queued before command start");
|
|
3237
|
+
cancelMessage = "Cancellation queued before command start";
|
|
3238
|
+
}
|
|
3239
|
+
if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
|
|
3240
|
+
return { cancelled, message: cancelMessage };
|
|
3241
|
+
}
|
|
3141
3242
|
function closeProcessStdin(active) {
|
|
3142
3243
|
if (!active?.stdin) {
|
|
3143
3244
|
return;
|
|
@@ -3163,21 +3264,35 @@ function sendReplayWorkerMessage(ws, message) {
|
|
|
3163
3264
|
}
|
|
3164
3265
|
function sendWorkerMessage(ws, message) {
|
|
3165
3266
|
if (launcherFailureInProgress) return;
|
|
3166
|
-
if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
|
|
3167
|
-
return;
|
|
3168
3267
|
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
|
-
|
|
3268
|
+
if (recoveryJournal && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
|
|
3269
|
+
recoveryJournal.unknown(message.requestId);
|
|
3171
3270
|
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
3172
3271
|
return;
|
|
3173
3272
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
recoveryStore?.unknownRun(message.runId);
|
|
3273
|
+
if (!recoveryJournal) {
|
|
3274
|
+
sendReplayWorkerMessage(ws, message);
|
|
3177
3275
|
return;
|
|
3178
3276
|
}
|
|
3179
|
-
|
|
3180
|
-
|
|
3277
|
+
recoveryJournal.send(message);
|
|
3278
|
+
}
|
|
3279
|
+
function sendWorkerOutput(ws, frame) {
|
|
3280
|
+
if (launcherFailureInProgress) return Promise.resolve();
|
|
3281
|
+
if (!recoveryJournal) {
|
|
3282
|
+
sendReplayWorkerMessage(ws, frame);
|
|
3283
|
+
return Promise.resolve();
|
|
3284
|
+
}
|
|
3285
|
+
return recoveryJournal.sendOutput(frame);
|
|
3286
|
+
}
|
|
3287
|
+
function deliverJournaledWorkerMessage(message) {
|
|
3288
|
+
const target = currentWorkerSocket;
|
|
3289
|
+
if ((message.type === "exec_exit" || message.type === "exec_error") && typeof message.runId === "string" && infrastructureStoppedRunIds.has(message.runId)) {
|
|
3290
|
+
recoveryJournal?.unknownRun(message.runId);
|
|
3291
|
+
return true;
|
|
3292
|
+
}
|
|
3293
|
+
if (!target || target.readyState !== WebSocket.OPEN) return false;
|
|
3294
|
+
if (message.type === "exec_output" && workerRecoveryId !== void 0) return false;
|
|
3295
|
+
return sendReplayWorkerMessage(target, message);
|
|
3181
3296
|
}
|
|
3182
3297
|
function sendSerializedWorkerMessage(ws, serialized) {
|
|
3183
3298
|
sendWorkerMessage(ws, JSON.parse(serialized));
|
|
@@ -3740,9 +3855,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
3740
3855
|
`);
|
|
3741
3856
|
}
|
|
3742
3857
|
const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3858
|
+
if (!recoveryJournal) {
|
|
3859
|
+
const journal = await WorkerRecoveryJournal.open({
|
|
3860
|
+
filename: path.join(
|
|
3861
|
+
rootDir,
|
|
3862
|
+
".worker-recovery",
|
|
3863
|
+
createHash("sha256").update(`${baseUrl}\0${label}`).digest("hex"),
|
|
3864
|
+
"protocol.sqlite"
|
|
3865
|
+
)
|
|
3866
|
+
});
|
|
3867
|
+
journal.deliver = deliverJournaledWorkerMessage;
|
|
3868
|
+
journal.onFailure = handleRecoveryJournalFailure;
|
|
3869
|
+
recoveryJournal = journal;
|
|
3870
|
+
}
|
|
3746
3871
|
const projectsRoot = path.join(rootDir, "projects");
|
|
3747
3872
|
const syncRoot = path.join(rootDir, "sync");
|
|
3748
3873
|
const workspaceShadowRoot = path.join(syncRoot, "workspace");
|
|
@@ -3845,7 +3970,6 @@ async function startWorker(options, projectRuntime = {
|
|
|
3845
3970
|
const bearerAuthHeader = `Authorization: Bearer ${token}`;
|
|
3846
3971
|
const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
|
|
3847
3972
|
const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
3848
|
-
const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
|
|
3849
3973
|
const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
|
|
3850
3974
|
const workspaceProjectRelativePath = (projectId, branchName) => path.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
|
|
3851
3975
|
const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
|
|
@@ -3928,7 +4052,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
3928
4052
|
sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
|
|
3929
4053
|
planSourcePath: path.join(planRoot, project.projectId, ...branchName.split("/"))
|
|
3930
4054
|
}));
|
|
3931
|
-
removeProjectWorktrees({ projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
|
|
4055
|
+
const removed = removeProjectWorktrees({ projectsRoot, projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
|
|
4056
|
+
if (removed.stagedForCollectionPath) checkoutGarbageCollector.enqueue(removed.stagedForCollectionPath);
|
|
3932
4057
|
for (const branch of branches) {
|
|
3933
4058
|
fs.rmSync(branch.planSourcePath, { recursive: true, force: true });
|
|
3934
4059
|
const key = projectBranchKey(project.projectId, branch.branchName);
|
|
@@ -4016,14 +4141,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
4016
4141
|
)
|
|
4017
4142
|
};
|
|
4018
4143
|
};
|
|
4019
|
-
const stageDurableWorkspaceDeletions = () => {
|
|
4144
|
+
const stageDurableWorkspaceDeletions = async () => {
|
|
4020
4145
|
for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
|
|
4021
4146
|
const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
|
|
4022
4147
|
if (deletion.kind === "project") {
|
|
4023
4148
|
if (!deletion.projectDeleted) {
|
|
4024
4149
|
const nextProject = projectConfigById.get(deletion.projectId);
|
|
4025
4150
|
if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
|
|
4026
|
-
|
|
4151
|
+
await workspaceFilesystemExecutor().run("project_checkout_path_move", {
|
|
4027
4152
|
oldProjectRoot: deletion.managedPath,
|
|
4028
4153
|
newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
|
|
4029
4154
|
projectsDurabilityRoot: projectsRoot,
|
|
@@ -4390,7 +4515,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4390
4515
|
}
|
|
4391
4516
|
}
|
|
4392
4517
|
if (transitionReseedRequired) {
|
|
4393
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
|
|
4518
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordEnabledRepositoryTransition({
|
|
4394
4519
|
projectId: project.projectId,
|
|
4395
4520
|
transitionId: project.repositoryTransitionId
|
|
4396
4521
|
});
|
|
@@ -4559,7 +4684,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4559
4684
|
)
|
|
4560
4685
|
];
|
|
4561
4686
|
if (publishedTombstoneIds.length > 0) {
|
|
4562
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
4687
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
|
|
4563
4688
|
tombstoneIds: publishedTombstoneIds,
|
|
4564
4689
|
publishedHead
|
|
4565
4690
|
});
|
|
@@ -4578,7 +4703,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4578
4703
|
forgetLocalProjectMirrorRefsTokens(deletion.projectId, deletion.branchName);
|
|
4579
4704
|
}
|
|
4580
4705
|
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
4581
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
4706
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
4582
4707
|
tombstoneId: deletion.tombstoneId,
|
|
4583
4708
|
branchName: deletion.branchName
|
|
4584
4709
|
});
|
|
@@ -4775,7 +4900,10 @@ async function startWorker(options, projectRuntime = {
|
|
|
4775
4900
|
};
|
|
4776
4901
|
};
|
|
4777
4902
|
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4778
|
-
(message) => sendWorkerMessageFromCurrentSource(ws, message)
|
|
4903
|
+
(message) => sendWorkerMessageFromCurrentSource(ws, message),
|
|
4904
|
+
{
|
|
4905
|
+
fence: workspaceMountHoldFence
|
|
4906
|
+
}
|
|
4779
4907
|
);
|
|
4780
4908
|
const performWorkspaceSync = async (input) => {
|
|
4781
4909
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
@@ -5433,7 +5561,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5433
5561
|
(project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
|
|
5434
5562
|
);
|
|
5435
5563
|
if (!incidentDeferral.incidentId) {
|
|
5436
|
-
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
5564
|
+
projectWorkspaceState = await projectWorkspaceStateStore.reconcile({
|
|
5437
5565
|
desiredProjects: message.projects,
|
|
5438
5566
|
preserveOnlyBranches
|
|
5439
5567
|
});
|
|
@@ -5444,7 +5572,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5444
5572
|
(candidate) => candidate.projectId === project.projectId && candidate.branchName === branch.branchName
|
|
5445
5573
|
);
|
|
5446
5574
|
if (pending2 && pending2.branchId === branch.branchId) {
|
|
5447
|
-
projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
|
|
5575
|
+
projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
|
|
5448
5576
|
}
|
|
5449
5577
|
}
|
|
5450
5578
|
}
|
|
@@ -5612,7 +5740,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5612
5740
|
for (const projectId of [...readyProjectIds]) {
|
|
5613
5741
|
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
5614
5742
|
}
|
|
5615
|
-
stageDurableWorkspaceDeletions();
|
|
5743
|
+
await stageDurableWorkspaceDeletions();
|
|
5616
5744
|
const mountsBeforeGit = buildWorkspaceMounts();
|
|
5617
5745
|
await hydrateWorkspaceGitMounts(
|
|
5618
5746
|
workspaceShadowRoot,
|
|
@@ -5658,7 +5786,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
5658
5786
|
const connectSyncMs = Date.now() - connectSyncStartedAtMs;
|
|
5659
5787
|
const result = syncResult.telemetry ? syncResult : {
|
|
5660
5788
|
...syncResult,
|
|
5661
|
-
telemetry: {
|
|
5789
|
+
telemetry: {
|
|
5790
|
+
totalMs: connectSyncMs,
|
|
5791
|
+
queueMs: 0,
|
|
5792
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
5793
|
+
synchronizeMs: connectSyncMs
|
|
5794
|
+
}
|
|
5662
5795
|
};
|
|
5663
5796
|
assertConfigurationSyncStillAdmitted();
|
|
5664
5797
|
if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
|
|
@@ -5677,7 +5810,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5677
5810
|
) ? [deletion.tombstoneId] : []
|
|
5678
5811
|
);
|
|
5679
5812
|
if (layoutCleanupIds.length > 0) {
|
|
5680
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
5813
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
|
|
5681
5814
|
tombstoneIds: layoutCleanupIds,
|
|
5682
5815
|
publishedHead
|
|
5683
5816
|
});
|
|
@@ -5784,26 +5917,33 @@ async function startWorker(options, projectRuntime = {
|
|
|
5784
5917
|
if (startupProjectSnapshotRecoveryCompleted) return;
|
|
5785
5918
|
sendLifecycleProgress({ phase: "recovering", operationId, detail: "Checking interrupted workspace transactions" });
|
|
5786
5919
|
const snapshotRecovery = await workspaceSyncSingleFlight.runExclusive(
|
|
5787
|
-
async () => await
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5920
|
+
async () => await workspaceFilesystemExecutor().run(
|
|
5921
|
+
"project_snapshots_recover",
|
|
5922
|
+
{
|
|
5923
|
+
projectsRoot,
|
|
5924
|
+
temporaryRoot: projectWorktreeSnapshotsRoot,
|
|
5925
|
+
currentProcessId: process.pid,
|
|
5926
|
+
currentOwnerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
|
|
5927
|
+
},
|
|
5928
|
+
{
|
|
5929
|
+
onProgress: (progress) => {
|
|
5930
|
+
const lifecycleProgress = {
|
|
5931
|
+
phase: "recovering",
|
|
5932
|
+
operationId,
|
|
5933
|
+
detail: describeSnapshotProgress(progress),
|
|
5934
|
+
completedBytes: progress.completedBytes,
|
|
5935
|
+
completedItems: progress.completedBranches,
|
|
5936
|
+
totalItems: progress.totalBranches,
|
|
5937
|
+
...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
|
|
5938
|
+
};
|
|
5939
|
+
sendLifecycleProgress(lifecycleProgress);
|
|
5940
|
+
process.stdout.write(
|
|
5941
|
+
`[r5d-worker] ${lifecycleProgress.detail}: ${progress.completedBranches}/${progress.totalBranches} branch(es), ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes
|
|
5803
5942
|
`
|
|
5804
|
-
|
|
5943
|
+
);
|
|
5944
|
+
}
|
|
5805
5945
|
}
|
|
5806
|
-
|
|
5946
|
+
)
|
|
5807
5947
|
);
|
|
5808
5948
|
if (snapshotRecovery.restored.length > 0) {
|
|
5809
5949
|
process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
|
|
@@ -5898,7 +6038,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5898
6038
|
type: "hello",
|
|
5899
6039
|
resumableProtocol: WORKER_RESUMABLE_PROTOCOL,
|
|
5900
6040
|
runtimeId: workerRuntimeId,
|
|
5901
|
-
ledgerId:
|
|
6041
|
+
ledgerId: recoveryJournal.ledgerId,
|
|
5902
6042
|
startupReady: startupRecoveryComplete,
|
|
5903
6043
|
hostInfo: {
|
|
5904
6044
|
hostname: hostname(),
|
|
@@ -5926,16 +6066,39 @@ async function startWorker(options, projectRuntime = {
|
|
|
5926
6066
|
}
|
|
5927
6067
|
};
|
|
5928
6068
|
sendSerializedWorkerMessage(ws, JSON.stringify(hello));
|
|
6069
|
+
let outputPumpInFlight = false;
|
|
5929
6070
|
outputReplayTimer = setInterval(() => {
|
|
5930
|
-
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
6071
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws || outputPumpInFlight || !recoveryJournal) return;
|
|
6072
|
+
outputPumpInFlight = true;
|
|
6073
|
+
void (async () => {
|
|
6074
|
+
try {
|
|
6075
|
+
for await (const page of recoveryJournal.outputReplay(void 0)) {
|
|
6076
|
+
for (const output of page) {
|
|
6077
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws || !sendReplayWorkerMessage(ws, output)) return;
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
} catch (error) {
|
|
6081
|
+
process.stderr.write(`[r5d-worker] output replay pump failed: ${error instanceof Error ? error.message : String(error)}
|
|
6082
|
+
`);
|
|
6083
|
+
} finally {
|
|
6084
|
+
outputPumpInFlight = false;
|
|
6085
|
+
}
|
|
6086
|
+
})();
|
|
5934
6087
|
}, 250);
|
|
5935
6088
|
outputReplayTimer.unref();
|
|
6089
|
+
let terminalPumpInFlight = false;
|
|
5936
6090
|
terminalReplayTimer = setInterval(() => {
|
|
5937
|
-
if (workerRecoveryId !== void 0) return;
|
|
5938
|
-
|
|
6091
|
+
if (workerRecoveryId !== void 0 || terminalPumpInFlight || !recoveryJournal) return;
|
|
6092
|
+
terminalPumpInFlight = true;
|
|
6093
|
+
void recoveryJournal.terminals().then((terminals) => {
|
|
6094
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
|
|
6095
|
+
for (const terminal of terminals) sendReplayWorkerMessage(ws, terminal);
|
|
6096
|
+
}).catch((error) => {
|
|
6097
|
+
process.stderr.write(`[r5d-worker] terminal replay pump failed: ${error instanceof Error ? error.message : String(error)}
|
|
6098
|
+
`);
|
|
6099
|
+
}).finally(() => {
|
|
6100
|
+
terminalPumpInFlight = false;
|
|
6101
|
+
});
|
|
5939
6102
|
}, 5e3);
|
|
5940
6103
|
terminalReplayTimer.unref();
|
|
5941
6104
|
process.stdout.write("[r5d-worker] connected\n");
|
|
@@ -5948,28 +6111,37 @@ async function startWorker(options, projectRuntime = {
|
|
|
5948
6111
|
if (message.type === "recovery_request") {
|
|
5949
6112
|
workerRecoveryId = message.recoveryId;
|
|
5950
6113
|
sendActiveProcessReport(ws);
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
6114
|
+
recoveryJournal.pauseDelivery();
|
|
6115
|
+
try {
|
|
6116
|
+
const state = await recoveryJournal.recoveryState(message.requests);
|
|
6117
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
6118
|
+
sendWorkerMessage(ws, {
|
|
6119
|
+
type: "recovery_state",
|
|
6120
|
+
recoveryId: message.recoveryId,
|
|
6121
|
+
runtimeId: workerRuntimeId,
|
|
6122
|
+
ledgerId: recoveryJournal.ledgerId,
|
|
6123
|
+
operations: state.operations
|
|
6124
|
+
});
|
|
6125
|
+
const cursors = new Map(state.cursors.map((cursor) => [cursor.runId, cursor]));
|
|
6126
|
+
for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
|
|
6127
|
+
workerOutputWindow.reset([...cursors.values()]);
|
|
6128
|
+
for (const response of state.responses) sendReplayWorkerMessage(ws, response);
|
|
6129
|
+
for await (const page of recoveryJournal.outputReplay(message.outputOffsets)) {
|
|
6130
|
+
for (const output of page) {
|
|
6131
|
+
while (true) {
|
|
6132
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
|
|
6133
|
+
return;
|
|
6134
|
+
if (sendReplayWorkerMessage(ws, output)) break;
|
|
6135
|
+
await Bun.sleep(10);
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
5968
6138
|
}
|
|
6139
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
6140
|
+
for (const terminal of state.terminals) sendReplayWorkerMessage(ws, terminal);
|
|
6141
|
+
sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
|
|
6142
|
+
} finally {
|
|
6143
|
+
recoveryJournal.resumeDelivery();
|
|
5969
6144
|
}
|
|
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
6145
|
return;
|
|
5974
6146
|
}
|
|
5975
6147
|
if (message.type === "recovery_ready") {
|
|
@@ -5984,17 +6156,23 @@ async function startWorker(options, projectRuntime = {
|
|
|
5984
6156
|
return;
|
|
5985
6157
|
}
|
|
5986
6158
|
if (message.type === "operation_result_ack") {
|
|
5987
|
-
|
|
6159
|
+
recoveryJournal.acknowledgeResult(message.requestId);
|
|
5988
6160
|
return;
|
|
5989
6161
|
}
|
|
5990
6162
|
if (message.type === "exec_output_ack") {
|
|
5991
|
-
|
|
6163
|
+
recoveryJournal.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
|
|
5992
6164
|
workerOutputWindow.acknowledge(message.runId, message.stream, message.nextOffset);
|
|
5993
6165
|
return;
|
|
5994
6166
|
}
|
|
5995
6167
|
if (message.type === "cancel_session") {
|
|
5996
|
-
|
|
6168
|
+
const durableCancellation = recoveryJournal.cancelSession(message.sessionId);
|
|
6169
|
+
abortPendingReservations("Session cancelled before command start", message.sessionId);
|
|
5997
6170
|
await Promise.all([
|
|
6171
|
+
durableCancellation,
|
|
6172
|
+
// File writes already in flight for the session settle before the
|
|
6173
|
+
// acknowledgement; queued ones re-check the cancellation before
|
|
6174
|
+
// their first write and never start.
|
|
6175
|
+
sessionFileMutations.settled(message.sessionId),
|
|
5998
6176
|
workerCommandLauncher?.cancelSession(message.sessionId),
|
|
5999
6177
|
...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
6000
6178
|
cancelledProcessRuns.add(runId);
|
|
@@ -6009,27 +6187,74 @@ async function startWorker(options, projectRuntime = {
|
|
|
6009
6187
|
if (!startupRecoveryComplete && message.type !== "ping") return;
|
|
6010
6188
|
if (currentWorkerSocket !== ws) return;
|
|
6011
6189
|
const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
|
|
6190
|
+
if (message.type === "cancel") {
|
|
6191
|
+
const admission = recoveryJournal.admit({ ...message, requestId: message.requestId });
|
|
6192
|
+
const cancelled = await cancelProcessRun(message.runId);
|
|
6193
|
+
let admitted;
|
|
6194
|
+
try {
|
|
6195
|
+
admitted = await admission;
|
|
6196
|
+
} catch (error) {
|
|
6197
|
+
if (!(error instanceof WorkerRecoveryJournalBusyError)) throw error;
|
|
6198
|
+
process.stderr.write(`[r5d-worker] cancel ${message.runId} stopped the run but was not durably admitted: ${error.message}
|
|
6199
|
+
`);
|
|
6200
|
+
return;
|
|
6201
|
+
}
|
|
6202
|
+
if (admitted.admission === "conflict") {
|
|
6203
|
+
ws.close(1008, "Operation identity conflict");
|
|
6204
|
+
return;
|
|
6205
|
+
}
|
|
6206
|
+
sendWorkerMessage(ws, { type: "operation_received", requestId: message.requestId });
|
|
6207
|
+
if (admitted.admission !== "new") {
|
|
6208
|
+
if (admitted.response && admitted.admission !== "unknown") sendReplayWorkerMessage(ws, admitted.response);
|
|
6209
|
+
return;
|
|
6210
|
+
}
|
|
6211
|
+
sendSerializedWorkerMessage(
|
|
6212
|
+
ws,
|
|
6213
|
+
JSON.stringify({
|
|
6214
|
+
type: "cancel_result",
|
|
6215
|
+
requestId: message.requestId,
|
|
6216
|
+
runId: message.runId,
|
|
6217
|
+
cancelled: cancelled.cancelled,
|
|
6218
|
+
message: cancelled.message
|
|
6219
|
+
})
|
|
6220
|
+
);
|
|
6221
|
+
return;
|
|
6222
|
+
}
|
|
6012
6223
|
if (operationRequestId) {
|
|
6013
|
-
|
|
6014
|
-
|
|
6224
|
+
let admitted;
|
|
6225
|
+
try {
|
|
6226
|
+
admitted = await recoveryJournal.admit({ ...message, requestId: operationRequestId });
|
|
6227
|
+
} catch (error) {
|
|
6228
|
+
if (!(error instanceof WorkerRecoveryJournalBusyError)) throw error;
|
|
6229
|
+
process.stderr.write(`[r5d-worker] ${message.type} ${operationRequestId} was not admitted: ${error.message}
|
|
6230
|
+
`);
|
|
6231
|
+
return;
|
|
6232
|
+
}
|
|
6233
|
+
if (admitted.admission === "conflict") {
|
|
6015
6234
|
ws.close(1008, "Operation identity conflict");
|
|
6016
6235
|
return;
|
|
6017
6236
|
}
|
|
6018
6237
|
sendWorkerMessage(ws, { type: "operation_received", requestId: operationRequestId });
|
|
6019
|
-
if (admission !== "new") {
|
|
6020
|
-
|
|
6021
|
-
if (
|
|
6022
|
-
|
|
6238
|
+
if (admitted.admission !== "new") {
|
|
6239
|
+
if (admitted.response && admitted.admission !== "unknown") sendReplayWorkerMessage(ws, admitted.response);
|
|
6240
|
+
if (!(admitted.admission === "unknown" && message.type === "delete_project_branch" && await recoveryJournal.readmitUnknownBranchDeletion(
|
|
6241
|
+
operationRequestId,
|
|
6242
|
+
"sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0
|
|
6243
|
+
))) {
|
|
6023
6244
|
return;
|
|
6024
6245
|
}
|
|
6025
6246
|
}
|
|
6247
|
+
if (recoveryJournal.isRequestCancelled(operationRequestId)) {
|
|
6248
|
+
recoveryJournal.unknown(operationRequestId);
|
|
6249
|
+
return;
|
|
6250
|
+
}
|
|
6026
6251
|
}
|
|
6027
6252
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
6028
6253
|
const admissionCredentials = configuredCredentialGenerationFingerprint;
|
|
6029
6254
|
const assertMessageAdmission = () => {
|
|
6030
6255
|
if (operationRequestId) {
|
|
6031
|
-
if (workerLeaseExpired ||
|
|
6032
|
-
|
|
6256
|
+
if (workerLeaseExpired || recoveryJournal.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
|
|
6257
|
+
recoveryJournal.unknown(operationRequestId);
|
|
6033
6258
|
throw new StaleWorkerAdmissionError();
|
|
6034
6259
|
}
|
|
6035
6260
|
return;
|
|
@@ -6166,7 +6391,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6166
6391
|
}
|
|
6167
6392
|
recoveryDeadline ??= Date.now() + WORKER_RECOVERY_TIMEOUT_MS;
|
|
6168
6393
|
if (workerLeaseExpired || Date.now() >= recoveryDeadline) {
|
|
6169
|
-
|
|
6394
|
+
recoveryJournal.unknown(message.requestId);
|
|
6170
6395
|
return;
|
|
6171
6396
|
}
|
|
6172
6397
|
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
@@ -6196,7 +6421,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6196
6421
|
projectId: message.projectId,
|
|
6197
6422
|
branchName: message.targetBranch
|
|
6198
6423
|
};
|
|
6199
|
-
const created = await workspaceSyncSingleFlight.runBranchMutation(() => {
|
|
6424
|
+
const created = await workspaceSyncSingleFlight.runBranchMutation(async () => {
|
|
6200
6425
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6201
6426
|
throw new Error(
|
|
6202
6427
|
`Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
|
|
@@ -6211,7 +6436,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6211
6436
|
const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
|
|
6212
6437
|
({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
|
|
6213
6438
|
);
|
|
6214
|
-
projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
|
|
6439
|
+
projectWorkspaceState = await projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
|
|
6215
6440
|
let gitBranchCreated = false;
|
|
6216
6441
|
try {
|
|
6217
6442
|
if (!readyProjectIds.has(project.projectId)) {
|
|
@@ -6232,7 +6457,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6232
6457
|
}
|
|
6233
6458
|
return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
|
|
6234
6459
|
}
|
|
6235
|
-
const createdBranch = createOrRetryLinkedProjectBranch({
|
|
6460
|
+
const createdBranch = await createOrRetryLinkedProjectBranch({
|
|
6236
6461
|
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
6237
6462
|
primaryBranchName: primaryProjectBranch(project),
|
|
6238
6463
|
sourceBranchName: message.sourceBranch,
|
|
@@ -6257,7 +6482,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6257
6482
|
return createdBranch;
|
|
6258
6483
|
} catch (error) {
|
|
6259
6484
|
if (!pendingWasRecorded && !gitBranchCreated && !projectBranchMayExistAfterCreateFailure(error)) {
|
|
6260
|
-
projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
|
|
6485
|
+
projectWorkspaceState = await projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
|
|
6261
6486
|
}
|
|
6262
6487
|
throw error;
|
|
6263
6488
|
}
|
|
@@ -6301,7 +6526,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6301
6526
|
});
|
|
6302
6527
|
try {
|
|
6303
6528
|
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
6304
|
-
const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(() => {
|
|
6529
|
+
const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(async () => {
|
|
6305
6530
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6306
6531
|
throw new Error(
|
|
6307
6532
|
`Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
|
|
@@ -6328,12 +6553,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
6328
6553
|
if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
|
|
6329
6554
|
return false;
|
|
6330
6555
|
}
|
|
6331
|
-
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
6556
|
+
projectWorkspaceState = await projectWorkspaceStateStore.beginBranchDeletion({
|
|
6332
6557
|
projectId: message.projectId,
|
|
6333
6558
|
branchName: message.branchName
|
|
6334
6559
|
});
|
|
6335
6560
|
if (pendingLocalBranch) {
|
|
6336
|
-
projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
|
|
6561
|
+
projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
|
|
6337
6562
|
}
|
|
6338
6563
|
pendingCreatedBranchPublicationNotBefore.delete(pendingCreatedBranchKey(message.projectId, message.branchName));
|
|
6339
6564
|
if (pendingMirrorDeletes.has(deletionKey)) return true;
|
|
@@ -6437,7 +6662,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6437
6662
|
}
|
|
6438
6663
|
if (message.type === "exec_terminal_ack") {
|
|
6439
6664
|
pendingProcessTerminals.delete(message.runId);
|
|
6440
|
-
|
|
6665
|
+
recoveryJournal.acknowledgeTerminal(message.runId);
|
|
6441
6666
|
return;
|
|
6442
6667
|
}
|
|
6443
6668
|
if (message.type === "port_forward_connect") {
|
|
@@ -6492,42 +6717,6 @@ async function startWorker(options, projectRuntime = {
|
|
|
6492
6717
|
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
|
|
6493
6718
|
return;
|
|
6494
6719
|
}
|
|
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
6720
|
if (message.type === "exec_stdin") {
|
|
6532
6721
|
const sendAck = (payload) => sendSerializedWorkerMessage(
|
|
6533
6722
|
ws,
|
|
@@ -6608,10 +6797,20 @@ async function startWorker(options, projectRuntime = {
|
|
|
6608
6797
|
let mutationLeaseTransferred = false;
|
|
6609
6798
|
try {
|
|
6610
6799
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6800
|
+
const reservationAbort = registerPendingReservation(`pty:${message.ptyId}`, void 0);
|
|
6801
|
+
try {
|
|
6802
|
+
await reserveWorkspaceCommandAfterCurrentSync(
|
|
6803
|
+
message.target,
|
|
6804
|
+
workspaceSyncSingleFlight,
|
|
6805
|
+
() => {
|
|
6806
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
6807
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6808
|
+
},
|
|
6809
|
+
{ signal: reservationAbort.signal }
|
|
6810
|
+
);
|
|
6811
|
+
} finally {
|
|
6812
|
+
unregisterPendingReservation(`pty:${message.ptyId}`, reservationAbort);
|
|
6813
|
+
}
|
|
6615
6814
|
releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
|
|
6616
6815
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6617
6816
|
throw new Error(
|
|
@@ -6676,6 +6875,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6676
6875
|
}
|
|
6677
6876
|
if (message.type === "pty_close") {
|
|
6678
6877
|
const activePty = activePtys.get(message.ptyId);
|
|
6878
|
+
abortPendingReservation(`pty:${message.ptyId}`, "Shell closed before it started");
|
|
6679
6879
|
await closePty(message);
|
|
6680
6880
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6681
6881
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
@@ -6712,11 +6912,21 @@ async function startWorker(options, projectRuntime = {
|
|
|
6712
6912
|
try {
|
|
6713
6913
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6714
6914
|
if (hasWorkspaceEffect) {
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6915
|
+
const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
|
|
6916
|
+
try {
|
|
6917
|
+
await reserveWorkspaceCommandAfterCurrentSync(
|
|
6918
|
+
message.target,
|
|
6919
|
+
workspaceSyncSingleFlight,
|
|
6920
|
+
() => {
|
|
6921
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
6922
|
+
targetReserved = true;
|
|
6923
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6924
|
+
},
|
|
6925
|
+
{ signal: reservationAbort.signal }
|
|
6926
|
+
);
|
|
6927
|
+
} finally {
|
|
6928
|
+
unregisterPendingReservation(`run:${message.runId}`, reservationAbort);
|
|
6929
|
+
}
|
|
6720
6930
|
}
|
|
6721
6931
|
const runCommand = async () => {
|
|
6722
6932
|
if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
|
|
@@ -6768,7 +6978,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6768
6978
|
cancelledProcessRuns.delete(message.runId);
|
|
6769
6979
|
}
|
|
6770
6980
|
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6771
|
-
|
|
6981
|
+
recoveryJournal.unknown(message.requestId);
|
|
6772
6982
|
return;
|
|
6773
6983
|
}
|
|
6774
6984
|
sendSerializedWorkerMessage(ws, JSON.stringify(result));
|
|
@@ -6842,11 +7052,21 @@ async function startWorker(options, projectRuntime = {
|
|
|
6842
7052
|
try {
|
|
6843
7053
|
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6844
7054
|
if (hasWorkspaceEffect) {
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
7055
|
+
const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
|
|
7056
|
+
try {
|
|
7057
|
+
await reserveWorkspaceCommandAfterCurrentSync(
|
|
7058
|
+
message.target,
|
|
7059
|
+
workspaceSyncSingleFlight,
|
|
7060
|
+
() => {
|
|
7061
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
7062
|
+
targetReserved = true;
|
|
7063
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
7064
|
+
},
|
|
7065
|
+
{ signal: reservationAbort.signal }
|
|
7066
|
+
);
|
|
7067
|
+
} finally {
|
|
7068
|
+
unregisterPendingReservation(`run:${message.runId}`, reservationAbort);
|
|
7069
|
+
}
|
|
6850
7070
|
}
|
|
6851
7071
|
await runReservedWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand, () => {
|
|
6852
7072
|
if (targetReserved) {
|
|
@@ -6881,16 +7101,40 @@ async function startWorker(options, projectRuntime = {
|
|
|
6881
7101
|
return;
|
|
6882
7102
|
}
|
|
6883
7103
|
const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
|
|
6884
|
-
const
|
|
7104
|
+
const mutatesFiles = message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing;
|
|
7105
|
+
const mutatesVisibleWorkspace = mutatesFiles && targetMayMutateVisibleWorkspace(message.target);
|
|
6885
7106
|
if (reservesVisibleWorkspace) {
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
7107
|
+
const reservationAbort = registerPendingReservation(
|
|
7108
|
+
`op:${message.requestId}`,
|
|
7109
|
+
"sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0
|
|
7110
|
+
);
|
|
7111
|
+
try {
|
|
7112
|
+
await reserveWorkspaceCommandAfterCurrentSync(
|
|
7113
|
+
message.target,
|
|
7114
|
+
workspaceSyncSingleFlight,
|
|
7115
|
+
() => {
|
|
7116
|
+
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
7117
|
+
if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
|
|
7118
|
+
},
|
|
7119
|
+
{ signal: reservationAbort.signal }
|
|
7120
|
+
);
|
|
7121
|
+
} catch (error) {
|
|
7122
|
+
sendSerializedWorkerMessage(
|
|
7123
|
+
ws,
|
|
7124
|
+
JSON.stringify({
|
|
7125
|
+
type: "operation_result",
|
|
7126
|
+
requestId: message.requestId,
|
|
7127
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7128
|
+
})
|
|
7129
|
+
);
|
|
7130
|
+
return;
|
|
7131
|
+
} finally {
|
|
7132
|
+
unregisterPendingReservation(`op:${message.requestId}`, reservationAbort);
|
|
7133
|
+
}
|
|
6890
7134
|
}
|
|
6891
7135
|
let dirtyTrigger;
|
|
6892
7136
|
try {
|
|
6893
|
-
const
|
|
7137
|
+
const operation = runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
|
|
6894
7138
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
6895
7139
|
return await executeOperation({
|
|
6896
7140
|
message,
|
|
@@ -6901,6 +7145,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6901
7145
|
assertAdmission: assertMessageAdmission
|
|
6902
7146
|
});
|
|
6903
7147
|
});
|
|
7148
|
+
if (mutatesFiles && typeof message.sessionId === "string") sessionFileMutations.track(message.sessionId, operation);
|
|
7149
|
+
const result = await operation;
|
|
6904
7150
|
sendSerializedWorkerMessage(
|
|
6905
7151
|
ws,
|
|
6906
7152
|
JSON.stringify({
|
|
@@ -6984,6 +7230,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6984
7230
|
projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
|
|
6985
7231
|
}
|
|
6986
7232
|
if (currentWorkerSocket === ws) currentWorkerSocket = null;
|
|
7233
|
+
abortPendingReservations("Worker connection closed before command start");
|
|
6987
7234
|
workerRecoveryId = "reconnecting";
|
|
6988
7235
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6989
7236
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
@@ -7017,6 +7264,11 @@ async function startWorker(options, projectRuntime = {
|
|
|
7017
7264
|
});
|
|
7018
7265
|
}
|
|
7019
7266
|
function installWorkerRuntimeFailureLogging() {
|
|
7267
|
+
installWorkspaceFilesystemExecutorFailureHandler((error) => {
|
|
7268
|
+
process.stderr.write(`[r5d-worker] ${error.message}; restarting the worker runtime
|
|
7269
|
+
`);
|
|
7270
|
+
void exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
|
|
7271
|
+
});
|
|
7020
7272
|
process.on("uncaughtException", (error) => {
|
|
7021
7273
|
process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
7022
7274
|
`);
|
|
@@ -7029,11 +7281,6 @@ function installWorkerRuntimeFailureLogging() {
|
|
|
7029
7281
|
});
|
|
7030
7282
|
}
|
|
7031
7283
|
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
7284
|
const parsed = parseArgs(process.argv.slice(2));
|
|
7038
7285
|
if (parsed.command === "help") {
|
|
7039
7286
|
printHelp();
|