@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.
Files changed (60) hide show
  1. package/dist/cjs/main.cjs +488 -238
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  4. package/dist/cjs/project-workspace-state.cjs +51 -35
  5. package/dist/cjs/project-worktrees.cjs +79 -34
  6. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  7. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  8. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  9. package/dist/cjs/recovery-journal.cjs +735 -0
  10. package/dist/cjs/recovery-store.cjs +8 -0
  11. package/dist/cjs/session-file-mutations.cjs +61 -0
  12. package/dist/cjs/working-tree-mirror.cjs +1 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  14. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  15. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  16. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  17. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  18. package/dist/cjs/workspace-git-sync.cjs +275 -201
  19. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  20. package/dist/mjs/main.mjs +491 -244
  21. package/dist/mjs/package.json +1 -1
  22. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  23. package/dist/mjs/project-workspace-state.mjs +51 -35
  24. package/dist/mjs/project-worktrees.mjs +74 -34
  25. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  26. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  27. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  28. package/dist/mjs/recovery-journal.mjs +687 -0
  29. package/dist/mjs/recovery-store.mjs +8 -0
  30. package/dist/mjs/session-file-mutations.mjs +37 -0
  31. package/dist/mjs/working-tree-mirror.mjs +1 -0
  32. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  33. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  34. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  35. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  36. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  37. package/dist/mjs/workspace-git-sync.mjs +264 -202
  38. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  39. package/dist/types/main.d.ts +24 -19
  40. package/dist/types/project-checkout-garbage.d.ts +19 -2
  41. package/dist/types/project-workspace-state.d.ts +9 -9
  42. package/dist/types/project-worktrees.d.ts +49 -5
  43. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  44. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  45. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  46. package/dist/types/recovery-journal-thread.d.ts +1 -0
  47. package/dist/types/recovery-journal.d.ts +246 -0
  48. package/dist/types/recovery-store.d.ts +6 -0
  49. package/dist/types/session-file-mutations.d.ts +22 -0
  50. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  51. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  52. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  53. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  54. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  55. package/dist/types/workspace-git-sync.d.ts +113 -7
  56. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  57. package/package.json +1 -1
  58. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  59. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  60. 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 { WorkerRecoveryStore, workerOperationDigest } from "./recovery-store.mjs";
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, preserveProjectCheckoutPathMove } from "./workspace-path-move.mjs";
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 recoveryStore;
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 recoveryStore?.operations() ?? []) {
231
- if (operation.state !== "completed") recoveryStore?.unknown(operation.requestId);
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
- recoveryStore?.unknownRun(runId);
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 (!recoveryStore || !isWorkerCommunicationFailure(error instanceof Error ? error.message : String(error))) throw error;
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 recoveryStore?.operations() ?? []) {
306
+ for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
295
307
  if (operation.sessionId && operation.state !== "completed") {
296
- recoveryStore?.cancelSession(operation.sessionId);
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
- recoveryStore?.unknownRun(runId);
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
- new Response(subprocess.stdout).text(),
1013
- new Response(subprocess.stderr).text(),
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(() => reject(new GitTransportTimeoutError(`git ${args.join(" ")} timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
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 readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
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 (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
2312
+ if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
2254
2313
  throw new Error(`File not found: ${resolved.displayPath}`);
2255
2314
  }
2256
- const buffer = fs.readFileSync(resolved.absolutePath);
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.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
2269
- fs.writeFileSync(resolved.absolutePath, content, "utf8");
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 (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
2340
+ if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
2282
2341
  throw new Error(`File not found: ${resolved.displayPath}`);
2283
2342
  }
2284
- const originalContent = fs.readFileSync(resolved.absolutePath, "utf8");
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.writeFileSync(resolved.absolutePath, nextContent, "utf8");
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.writeFileSync(resolved.absolutePath, createPlanMarkdown(message.planId), { encoding: "utf8", flag: "wx" });
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.readFileSync(resolved.absolutePath, "utf8");
2362
- writeWorkerTextFile(resolvedTarget.rootPath, filePath, updateTaskCheckbox(content, message.taskId, message.checked), builtInPaths);
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
- return readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
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.realpathSync(start.virtualRootPath) : null;
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.statSync(absolutePath);
2511
+ stat = await fs.promises.stat(absolutePath);
2444
2512
  if (realVirtualRoot) {
2445
- assertInsideRoot(realVirtualRoot, fs.realpathSync(absolutePath), `${start.displayPath} path`);
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.realpathSync(absolutePath);
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.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
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 (!fs.existsSync(start.absolutePath)) {
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.readFileSync(entry.absolutePath);
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 (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
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 (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
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.readdirSync(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));
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.statSync(resolved.absolutePath).isDirectory()) {
2737
+ if (!(await fs.promises.stat(resolved.absolutePath)).isDirectory()) {
2670
2738
  throw new Error(`Code directory not found: ${resolved.displayPath}`);
2671
2739
  }
2672
- const entries = fs.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => entry.name.toLowerCase() !== ".git").flatMap((entry) => {
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.statSync(resolvedEntry.absolutePath);
2677
- if (!stats.isFile() && !stats.isDirectory()) return [];
2678
- return [
2679
- {
2680
- name: entry.name,
2681
- type: stats.isDirectory() ? "directory" : "file",
2682
- size: stats.isDirectory() ? null : stats.size,
2683
- mtime: stats.mtime.toISOString()
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
- }).sort((left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name));
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.statSync(resolved.absolutePath);
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.readFileSync(resolved.absolutePath);
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 (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
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.readFileSync(resolved.absolutePath);
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(subprocess.stdout, (data) => {
3051
- sendWorkerMessage(input.ws, {
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(subprocess.stderr, (data) => {
3059
- sendWorkerMessage(input.ws, {
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
- sendWorkerMessage(input.ws, {
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 (recoveryStore && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
3170
- recoveryStore.unknown(message.requestId);
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
- const recorded = recoveryStore?.record(message) ?? message;
3175
- if ((message.type === "exec_exit" || message.type === "exec_error") && infrastructureStoppedRunIds.has(message.runId)) {
3176
- recoveryStore?.unknownRun(message.runId);
3273
+ if (!recoveryJournal) {
3274
+ sendReplayWorkerMessage(ws, message);
3177
3275
  return;
3178
3276
  }
3179
- if (message.type === "exec_output" && workerRecoveryId !== void 0) return;
3180
- sendReplayWorkerMessage(ws, recorded);
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
- recoveryStore ??= new WorkerRecoveryStore(
3744
- path.join(rootDir, ".worker-recovery", createHash("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
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
- preserveProjectCheckoutPathMove({
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: { totalMs: connectSyncMs, queueMs: 0, prepareMs: lastWorkspaceSyncMirrorObservationMs, synchronizeMs: connectSyncMs }
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 recoverProjectSnapshotsInChild({
5788
- projectsRoot,
5789
- snapshotRoot: projectWorktreeSnapshotsRoot,
5790
- onProgress: (progress) => {
5791
- const lifecycleProgress = {
5792
- phase: "recovering",
5793
- operationId,
5794
- detail: describeSnapshotProgress(progress),
5795
- completedBytes: progress.completedBytes,
5796
- completedItems: progress.completedBranches,
5797
- totalItems: progress.totalBranches,
5798
- ...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
5799
- };
5800
- sendLifecycleProgress(lifecycleProgress);
5801
- process.stdout.write(
5802
- `[r5d-worker] ${lifecycleProgress.detail}: ${progress.completedBranches}/${progress.totalBranches} branch(es), ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes
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: recoveryStore.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
- for (const output of recoveryStore.output()) {
5932
- if (!sendReplayWorkerMessage(ws, output)) break;
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
- for (const terminal of recoveryStore?.terminals() ?? []) sendReplayWorkerMessage(ws, terminal);
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
- sendWorkerMessage(ws, {
5952
- type: "recovery_state",
5953
- recoveryId: message.recoveryId,
5954
- runtimeId: workerRuntimeId,
5955
- ledgerId: recoveryStore.ledgerId,
5956
- operations: recoveryStore.operations()
5957
- });
5958
- const cursors = new Map(recoveryStore.outputCursors().map((cursor) => [cursor.runId, cursor]));
5959
- for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
5960
- workerOutputWindow.reset([...cursors.values()]);
5961
- for (const response of recoveryStore.responses(message.requests)) sendReplayWorkerMessage(ws, response);
5962
- for (const output of recoveryStore.output(message.outputOffsets)) {
5963
- while (true) {
5964
- if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
5965
- return;
5966
- if (sendReplayWorkerMessage(ws, output)) break;
5967
- await Bun.sleep(10);
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
- recoveryStore.acknowledgeResult(message.requestId);
6159
+ recoveryJournal.acknowledgeResult(message.requestId);
5988
6160
  return;
5989
6161
  }
5990
6162
  if (message.type === "exec_output_ack") {
5991
- recoveryStore.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
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
- recoveryStore.cancelSession(message.sessionId);
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
- const admission = recoveryStore.admit({ ...message, requestId: operationRequestId });
6014
- if (admission === "conflict") {
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
- const response = recoveryStore.response(operationRequestId);
6021
- if (response && admission !== "unknown") sendReplayWorkerMessage(ws, response);
6022
- if (!(admission === "unknown" && message.type === "delete_project_branch" && recoveryStore.readmitUnknownBranchDeletion(operationRequestId))) {
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 || recoveryStore.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
6032
- recoveryStore.unknown(operationRequestId);
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
- recoveryStore.unknown(message.requestId);
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
- recoveryStore.acknowledgeTerminal(message.runId);
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
- await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6612
- workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6613
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
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
- await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6716
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
6717
- targetReserved = true;
6718
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
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
- recoveryStore.unknown(message.requestId);
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
- await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6846
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
6847
- targetReserved = true;
6848
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
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 mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing) && targetMayMutateVisibleWorkspace(message.target);
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
- await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6887
- workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
6888
- if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
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 result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
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();