@ricsam/r5d-worker 0.0.123 → 0.0.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/command-launcher.cjs +6 -2
  3. package/dist/cjs/main.cjs +496 -238
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  6. package/dist/cjs/project-workspace-state.cjs +51 -35
  7. package/dist/cjs/project-worktrees.cjs +79 -34
  8. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  9. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  10. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  11. package/dist/cjs/recovery-journal.cjs +735 -0
  12. package/dist/cjs/recovery-store.cjs +8 -0
  13. package/dist/cjs/session-file-mutations.cjs +61 -0
  14. package/dist/cjs/working-tree-mirror.cjs +1 -0
  15. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  16. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  17. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  18. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  19. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  20. package/dist/cjs/workspace-git-sync.cjs +275 -201
  21. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  22. package/dist/mjs/command-launcher.mjs +6 -2
  23. package/dist/mjs/main.mjs +499 -244
  24. package/dist/mjs/package.json +1 -1
  25. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  26. package/dist/mjs/project-workspace-state.mjs +51 -35
  27. package/dist/mjs/project-worktrees.mjs +74 -34
  28. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  29. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  30. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  31. package/dist/mjs/recovery-journal.mjs +687 -0
  32. package/dist/mjs/recovery-store.mjs +8 -0
  33. package/dist/mjs/session-file-mutations.mjs +37 -0
  34. package/dist/mjs/working-tree-mirror.mjs +1 -0
  35. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  36. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  37. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  38. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  39. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  40. package/dist/mjs/workspace-git-sync.mjs +264 -202
  41. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  42. package/dist/types/command-launcher.d.ts +2 -1
  43. package/dist/types/main.d.ts +30 -19
  44. package/dist/types/project-checkout-garbage.d.ts +19 -2
  45. package/dist/types/project-workspace-state.d.ts +9 -9
  46. package/dist/types/project-worktrees.d.ts +49 -5
  47. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  48. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  49. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  50. package/dist/types/recovery-journal-thread.d.ts +1 -0
  51. package/dist/types/recovery-journal.d.ts +246 -0
  52. package/dist/types/recovery-store.d.ts +6 -0
  53. package/dist/types/session-file-mutations.d.ts +22 -0
  54. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  55. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  56. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  57. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  58. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  59. package/dist/types/workspace-git-sync.d.ts +113 -7
  60. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  61. package/package.json +1 -1
  62. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  63. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  64. package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
package/dist/cjs/main.cjs CHANGED
@@ -61,6 +61,8 @@ var import_plan_paths = require("./plan-paths.cjs");
61
61
  var import_plan_parser = require("./plan-parser.cjs");
62
62
  var import_plan_lint = require("./plan-lint.cjs");
63
63
  var import_node_fs = __toESM(require("node:fs"), 1);
64
+ var import_recovery_journal = require("./recovery-journal.cjs");
65
+ var import_session_file_mutations = require("./session-file-mutations.cjs");
64
66
  var import_recovery_store = require("./recovery-store.cjs");
65
67
  var import_recovery_output_window = require("./recovery-output-window.cjs");
66
68
  var import_command_launcher = require("./command-launcher.cjs");
@@ -83,6 +85,8 @@ var import_workspace_incident_state = require("./workspace-incident-state.cjs");
83
85
  var import_workspace_automatic_sync_policy = require("./workspace-automatic-sync-policy.cjs");
84
86
  var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
85
87
  var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
88
+ var import_workspace_mount_hold_fence = require("./workspace-mount-hold-fence.cjs");
89
+ var import_workspace_filesystem_executor = require("./workspace-filesystem-executor.cjs");
86
90
  var import_workspace_projection_ledger = require("./workspace-projection-ledger.cjs");
87
91
  var import_workspace_sync_coalescer = require("./workspace-sync-coalescer.cjs");
88
92
  var import_workspace_path_move = require("./workspace-path-move.cjs");
@@ -95,7 +99,6 @@ var import_supervisor = require("./supervisor.cjs");
95
99
  var import_managed_paths = require("./managed-paths.cjs");
96
100
  var import_project_worktrees = require("./project-worktrees.cjs");
97
101
  var import_project_mirror_fetch_policy = require("./project-mirror-fetch-policy.cjs");
98
- var import_project_snapshot_recovery_runner = require("./project-snapshot-recovery-runner.cjs");
99
102
  var import_project_workspace_state = require("./project-workspace-state.cjs");
100
103
  var import_workspace_git_sync = require("./workspace-git-sync.cjs");
101
104
  const import_meta = {};
@@ -111,7 +114,7 @@ function workerServerHeartbeatAction(input) {
111
114
  return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
112
115
  }
113
116
  const WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX = "hydration:";
114
- function createWorkspaceHydrationLeaseHooks(send, options = {}) {
117
+ function createWorkspaceHydrationLeaseHooks(send, options) {
115
118
  const yieldToEventLoop = options.yieldToEventLoop ?? (() => new Promise((resolve) => setImmediate(resolve)));
116
119
  const log = options.log ?? ((line) => process.stdout.write(`${line}
117
120
  `));
@@ -132,13 +135,17 @@ function createWorkspaceHydrationLeaseHooks(send, options = {}) {
132
135
  log(
133
136
  `[r5d-worker] released hydration lease ${context.transactionId} after ${startedAt === void 0 ? "?" : Math.round(now() - startedAt)}ms`
134
137
  );
138
+ },
139
+ holdMounts(mountIds, description) {
140
+ return options.fence.hold(mountIds, description);
135
141
  }
136
142
  };
137
143
  }
138
144
  const workerLifecycleTestHarness = {
139
145
  boundedWorkerLifecycleProgress,
140
146
  workerServerHeartbeatAction,
141
- createWorkspaceHydrationLeaseHooks
147
+ createWorkspaceHydrationLeaseHooks,
148
+ workerCommandHasWorkspaceEffect
142
149
  };
143
150
  class ProjectWorkspaceConfigurationDeferredError extends Error {
144
151
  }
@@ -191,7 +198,7 @@ const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
191
198
  let currentWorkerSocket = null;
192
199
  const workerRuntimeId = crypto.randomUUID();
193
200
  const workerOutputWindow = new import_recovery_output_window.WorkerOutputWindow();
194
- let recoveryStore;
201
+ let recoveryJournal;
195
202
  let workerReconnectAttempt = 0;
196
203
  let workerRecoveryId;
197
204
  let workerExecutionLeaseDeadline = 0;
@@ -203,13 +210,13 @@ let runtimeExitPromise;
203
210
  function fenceWorkerRuntimeOperations() {
204
211
  launcherFailureInProgress = true;
205
212
  workerAdmissionGeneration += 1;
206
- for (const operation of recoveryStore?.operations() ?? []) {
207
- if (operation.state !== "completed") recoveryStore?.unknown(operation.requestId);
213
+ for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
214
+ if (operation.state !== "completed") recoveryJournal?.unknown(operation.requestId);
208
215
  }
209
216
  for (const runId of workerCommandLauncher?.activeIds ?? []) {
210
217
  infrastructureStoppedRunIds.add(runId);
211
218
  cancelledProcessRuns.add(runId);
212
- recoveryStore?.unknownRun(runId);
219
+ recoveryJournal?.unknownRun(runId);
213
220
  }
214
221
  for (const runId of activeProcesses.keys()) infrastructureStoppedRunIds.add(runId);
215
222
  }
@@ -218,13 +225,21 @@ function exitWorkerRuntime(code) {
218
225
  fenceWorkerRuntimeOperations();
219
226
  runtimeExitPromise = (async () => {
220
227
  await Promise.race([
221
- Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close()]),
228
+ Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close(), recoveryJournal?.flush()]),
222
229
  Bun.sleep(4e3)
223
230
  ]);
224
231
  process.exit(code);
225
232
  })();
226
233
  return runtimeExitPromise;
227
234
  }
235
+ function handleRecoveryJournalFailure(error) {
236
+ if (launcherFailureInProgress) return;
237
+ fenceWorkerRuntimeOperations();
238
+ process.stderr.write(`[r5d-worker] ${error.message}; stopping runtime for recovery journal restart
239
+ `);
240
+ currentWorkerSocket?.close(1012, "Recovery journal failure");
241
+ void exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
242
+ }
228
243
  function handleWorkerCommandLauncherFailure(error) {
229
244
  if (!workerCommandLauncher || launcherFailureInProgress) return;
230
245
  fenceWorkerRuntimeOperations();
@@ -245,7 +260,7 @@ async function retryWorkerServerRead(read) {
245
260
  try {
246
261
  return await read();
247
262
  } catch (error) {
248
- if (!recoveryStore || !isWorkerCommunicationFailure(error instanceof Error ? error.message : String(error))) throw error;
263
+ if (!recoveryJournal || !isWorkerCommunicationFailure(error instanceof Error ? error.message : String(error))) throw error;
249
264
  deadline ??= Date.now() + import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS;
250
265
  if (workerLeaseExpired || Date.now() >= deadline) throw new WorkerServerUnavailableError("Worker communication recovery expired");
251
266
  currentWorkerSocket?.close(1012, "Communication recovery");
@@ -267,9 +282,10 @@ function renewWorkerExecutionLease(leaseMs) {
267
282
  `);
268
283
  });
269
284
  closeAllPtys();
270
- for (const operation of recoveryStore?.operations() ?? []) {
285
+ for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
271
286
  if (operation.sessionId && operation.state !== "completed") {
272
- recoveryStore?.cancelSession(operation.sessionId);
287
+ void recoveryJournal?.cancelSession(operation.sessionId);
288
+ abortPendingReservations("Execution lease expired before command start", operation.sessionId);
273
289
  void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
274
290
  process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
275
291
  `);
@@ -279,7 +295,7 @@ function renewWorkerExecutionLease(leaseMs) {
279
295
  for (const [runId, active] of activeProcesses) {
280
296
  if (!active.sessionId) continue;
281
297
  infrastructureStoppedRunIds.add(runId);
282
- recoveryStore?.unknownRun(runId);
298
+ recoveryJournal?.unknownRun(runId);
283
299
  cancelledProcessRuns.add(runId);
284
300
  closeProcessStdin(active);
285
301
  void (0, import_process_tree.terminateProcessTree)(active.process).catch(
@@ -294,6 +310,32 @@ function renewWorkerExecutionLease(leaseMs) {
294
310
  }
295
311
  let workerAdmissionGeneration = 0;
296
312
  const workspaceMutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
313
+ const workspaceMountHoldFence = new import_workspace_mount_hold_fence.WorkspaceMountHoldFence();
314
+ const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
315
+ const pendingReservationAborts = /* @__PURE__ */ new Map();
316
+ const sessionFileMutations = new import_session_file_mutations.SessionFileMutationTracker();
317
+ function registerPendingReservation(key, sessionId) {
318
+ abortPendingReservation(key, "Reservation superseded by a newer request with the same id");
319
+ const controller = new AbortController();
320
+ pendingReservationAborts.set(key, { controller, sessionId });
321
+ return controller;
322
+ }
323
+ function unregisterPendingReservation(key, controller) {
324
+ if (pendingReservationAborts.get(key)?.controller === controller) pendingReservationAborts.delete(key);
325
+ }
326
+ function abortPendingReservation(key, reason) {
327
+ const pending = pendingReservationAborts.get(key);
328
+ if (!pending) return false;
329
+ pendingReservationAborts.delete(key);
330
+ pending.controller.abort(new import_workspace_mount_hold_fence.WorkspaceMountHoldAbortedError(reason));
331
+ return true;
332
+ }
333
+ function abortPendingReservations(reason, sessionId) {
334
+ for (const [key, pending] of [...pendingReservationAborts]) {
335
+ if (sessionId !== void 0 && pending.sessionId !== sessionId) continue;
336
+ abortPendingReservation(key, reason);
337
+ }
338
+ }
297
339
  let workspaceSyncQueue = Promise.resolve();
298
340
  let startupProjectSnapshotRecoveryCompleted = false;
299
341
  const checkoutGarbageCollectors = /* @__PURE__ */ new Map();
@@ -328,6 +370,17 @@ const workspaceSyncSingleFlight = {
328
370
  },
329
371
  afterCurrent() {
330
372
  return workspaceSyncQueue;
373
+ },
374
+ mountHold(target, signal) {
375
+ if (target.type !== "project") return null;
376
+ const mountIds = [projectMountId(target.projectId, target.branchName)];
377
+ if (!workspaceMountHoldFence.isHeld(mountIds)) return null;
378
+ const holder = workspaceMountHoldFence.describeHold(mountIds);
379
+ process.stdout.write(
380
+ `[r5d-worker] command for ${target.projectId}/${target.branchName} waits for ${holder ?? "a workspace filesystem job"}
381
+ `
382
+ );
383
+ return workspaceMountHoldFence.waitForRelease(mountIds, { signal });
331
384
  }
332
385
  };
333
386
  let githubCredential = null;
@@ -984,14 +1037,15 @@ async function runGitAsync(args, options = {}) {
984
1037
  env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
985
1038
  ...bounded ? { detached: true } : {}
986
1039
  });
987
- const completion = Promise.all([
988
- new Response(subprocess.stdout).text(),
989
- new Response(subprocess.stderr).text(),
990
- subprocess.exited
991
- ]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode }));
1040
+ const completion = Promise.all([new Response(subprocess.stdout).text(), new Response(subprocess.stderr).text(), subprocess.exited]).then(
1041
+ ([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode })
1042
+ );
992
1043
  let timer;
993
1044
  const timedOut = bounded ? new Promise((_resolve, reject) => {
994
- timer = setTimeout(() => reject(new GitTransportTimeoutError(`git ${args.join(" ")} timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
1045
+ timer = setTimeout(
1046
+ () => reject(new GitTransportTimeoutError(`git ${args.join(" ")} timed out after ${options.timeoutMs}ms`)),
1047
+ options.timeoutMs
1048
+ );
995
1049
  }) : null;
996
1050
  try {
997
1051
  const { stdout, stderr, exitCode } = timedOut ? await Promise.race([completion, timedOut]) : await completion;
@@ -2080,12 +2134,12 @@ async function streamCommandOutput(stream, onData) {
2080
2134
  }
2081
2135
  const text = decoder.decode(chunk.value, { stream: true });
2082
2136
  if (text.length > 0) {
2083
- onData(text);
2137
+ await onData(text);
2084
2138
  }
2085
2139
  }
2086
2140
  const trailing = decoder.decode();
2087
2141
  if (trailing.length > 0) {
2088
- onData(trailing);
2142
+ await onData(trailing);
2089
2143
  }
2090
2144
  } finally {
2091
2145
  reader.releaseLock();
@@ -2224,12 +2278,21 @@ function mutationQueueKey(target, resolved) {
2224
2278
  }
2225
2279
  return `${describeWorkerSessionTarget(target)}:${import_node_path.default.posix.normalize(resolved.repoRelativePath)}`;
2226
2280
  }
2227
- function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
2281
+ async function statIfExists(targetPath) {
2282
+ try {
2283
+ return await import_node_fs.default.promises.stat(targetPath);
2284
+ } catch (error) {
2285
+ const code = error.code;
2286
+ if (code === "ENOENT" || code === "ENOTDIR") return null;
2287
+ throw error;
2288
+ }
2289
+ }
2290
+ async function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
2228
2291
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
2229
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
2292
+ if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
2230
2293
  throw new Error(`File not found: ${resolved.displayPath}`);
2231
2294
  }
2232
- const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
2295
+ const buffer = await import_node_fs.default.promises.readFile(resolved.absolutePath);
2233
2296
  const formatted = formatLineNumberedContent(buffer.toString("utf8"), offset, limit);
2234
2297
  return {
2235
2298
  type: "read",
@@ -2239,25 +2302,25 @@ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
2239
2302
  ...formatted
2240
2303
  };
2241
2304
  }
2242
- function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
2305
+ async function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
2243
2306
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
2244
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
2245
- import_node_fs.default.writeFileSync(resolved.absolutePath, content, "utf8");
2307
+ await import_node_fs.default.promises.mkdir(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
2308
+ await import_node_fs.default.promises.writeFile(resolved.absolutePath, content, "utf8");
2246
2309
  return {
2247
2310
  type: "write",
2248
2311
  file: resolved.displayPath,
2249
2312
  gitBlobHash: getGitBlobHashForContent(content)
2250
2313
  };
2251
2314
  }
2252
- function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
2315
+ async function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
2253
2316
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
2254
2317
  if (!Array.isArray(edits) || edits.length === 0) {
2255
2318
  throw new Error("edit requires at least one replacement");
2256
2319
  }
2257
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
2320
+ if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
2258
2321
  throw new Error(`File not found: ${resolved.displayPath}`);
2259
2322
  }
2260
- const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
2323
+ const originalContent = await import_node_fs.default.promises.readFile(resolved.absolutePath, "utf8");
2261
2324
  const ranges = edits.map((edit, index) => {
2262
2325
  if (edit.oldText === edit.newText) {
2263
2326
  throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
@@ -2289,7 +2352,7 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
2289
2352
  for (const range of [...sortedRanges].reverse()) {
2290
2353
  nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
2291
2354
  }
2292
- import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
2355
+ await import_node_fs.default.promises.writeFile(resolved.absolutePath, nextContent, "utf8");
2293
2356
  return {
2294
2357
  type: "edit",
2295
2358
  file: resolved.displayPath,
@@ -2322,7 +2385,7 @@ async function executePlanOperation(input) {
2322
2385
  let created = false;
2323
2386
  if (message.createIfMissing) {
2324
2387
  try {
2325
- import_node_fs.default.writeFileSync(resolved.absolutePath, (0, import_plan_parser.createPlanMarkdown)(message.planId), { encoding: "utf8", flag: "wx" });
2388
+ await import_node_fs.default.promises.writeFile(resolved.absolutePath, (0, import_plan_parser.createPlanMarkdown)(message.planId), { encoding: "utf8", flag: "wx" });
2326
2389
  created = true;
2327
2390
  } catch (error) {
2328
2391
  if (error.code !== "EEXIST") throw error;
@@ -2331,11 +2394,16 @@ async function executePlanOperation(input) {
2331
2394
  return {
2332
2395
  type: "read_plan",
2333
2396
  created,
2334
- read: readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
2397
+ read: await readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
2335
2398
  };
2336
2399
  }
2337
- const content = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
2338
- writeWorkerTextFile(resolvedTarget.rootPath, filePath, (0, import_plan_parser.updateTaskCheckbox)(content, message.taskId, message.checked), builtInPaths);
2400
+ const content = await import_node_fs.default.promises.readFile(resolved.absolutePath, "utf8");
2401
+ await writeWorkerTextFile(
2402
+ resolvedTarget.rootPath,
2403
+ filePath,
2404
+ (0, import_plan_parser.updateTaskCheckbox)(content, message.taskId, message.checked),
2405
+ builtInPaths
2406
+ );
2339
2407
  return {
2340
2408
  type: "update_task_status",
2341
2409
  planId: message.planId,
@@ -2356,7 +2424,11 @@ async function executeReadFileOperation(input) {
2356
2424
  rootDir: input.rootDir,
2357
2425
  access: "read"
2358
2426
  });
2359
- return readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
2427
+ const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
2428
+ return withFileMutationQueue(
2429
+ mutationQueueKey(input.resolvedTarget.target, resolved),
2430
+ () => readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths)
2431
+ );
2360
2432
  }
2361
2433
  async function executeWriteFileOperation(input) {
2362
2434
  const builtInPaths = await prepareBuiltInToolPathsForTarget({
@@ -2409,16 +2481,16 @@ function wildcardToRegex(pattern) {
2409
2481
  function normalizeFindPattern(pattern) {
2410
2482
  return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
2411
2483
  }
2412
- function walkWorkerEntries(branchPath, start) {
2484
+ async function walkWorkerEntries(branchPath, start) {
2413
2485
  const entries = [];
2414
2486
  const visitedDirectories = /* @__PURE__ */ new Set();
2415
- const realVirtualRoot = start.scope === "virtual" ? import_node_fs.default.realpathSync(start.virtualRootPath) : null;
2416
- const visit = (absolutePath, isRoot) => {
2487
+ const realVirtualRoot = start.scope === "virtual" ? await import_node_fs.default.promises.realpath(start.virtualRootPath) : null;
2488
+ const visit = async (absolutePath, isRoot) => {
2417
2489
  let stat;
2418
2490
  try {
2419
- stat = import_node_fs.default.statSync(absolutePath);
2491
+ stat = await import_node_fs.default.promises.stat(absolutePath);
2420
2492
  if (realVirtualRoot) {
2421
- assertInsideRoot(realVirtualRoot, import_node_fs.default.realpathSync(absolutePath), `${start.displayPath} path`);
2493
+ assertInsideRoot(realVirtualRoot, await import_node_fs.default.promises.realpath(absolutePath), `${start.displayPath} path`);
2422
2494
  }
2423
2495
  } catch (error) {
2424
2496
  if (isRoot) throw error;
@@ -2431,7 +2503,7 @@ function walkWorkerEntries(branchPath, start) {
2431
2503
  if (!stat.isDirectory()) return;
2432
2504
  let realDirectoryPath;
2433
2505
  try {
2434
- realDirectoryPath = import_node_fs.default.realpathSync(absolutePath);
2506
+ realDirectoryPath = await import_node_fs.default.promises.realpath(absolutePath);
2435
2507
  } catch (error) {
2436
2508
  if (isRoot) throw error;
2437
2509
  return;
@@ -2440,25 +2512,25 @@ function walkWorkerEntries(branchPath, start) {
2440
2512
  visitedDirectories.add(realDirectoryPath);
2441
2513
  let children;
2442
2514
  try {
2443
- children = import_node_fs.default.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
2515
+ children = (await import_node_fs.default.promises.readdir(absolutePath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
2444
2516
  } catch (error) {
2445
2517
  if (isRoot) throw error;
2446
2518
  return;
2447
2519
  }
2448
2520
  for (const child of children) {
2449
2521
  if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
2450
- visit(import_node_path.default.join(absolutePath, child.name), false);
2522
+ await visit(import_node_path.default.join(absolutePath, child.name), false);
2451
2523
  }
2452
2524
  };
2453
- visit(start.absolutePath, true);
2525
+ await visit(start.absolutePath, true);
2454
2526
  return entries;
2455
2527
  }
2456
2528
  function isProbablyText(bytes) {
2457
2529
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
2458
2530
  }
2459
- function grepWorkerFiles(branchPath, input, builtInPaths) {
2531
+ async function grepWorkerFiles(branchPath, input, builtInPaths) {
2460
2532
  const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
2461
- if (!import_node_fs.default.existsSync(start.absolutePath)) {
2533
+ if (!await statIfExists(start.absolutePath)) {
2462
2534
  throw new Error(`Path not found: ${start.displayPath}`);
2463
2535
  }
2464
2536
  const limit = normalizeToolLimit(input.limit, 100, 1e3);
@@ -2468,12 +2540,12 @@ function grepWorkerFiles(branchPath, input, builtInPaths) {
2468
2540
  const lines = [];
2469
2541
  let matchCount = 0;
2470
2542
  let truncated = false;
2471
- for (const entry of walkWorkerEntries(branchPath, start)) {
2543
+ for (const entry of await walkWorkerEntries(branchPath, start)) {
2472
2544
  if (!entry.stat.isFile()) continue;
2473
2545
  if (globRegex && !globRegex.test(entry.matchPath)) continue;
2474
2546
  let bytes;
2475
2547
  try {
2476
- bytes = import_node_fs.default.readFileSync(entry.absolutePath);
2548
+ bytes = await import_node_fs.default.promises.readFile(entry.absolutePath);
2477
2549
  } catch {
2478
2550
  continue;
2479
2551
  }
@@ -2526,9 +2598,9 @@ async function executeGrepOperation(input) {
2526
2598
  builtInPaths
2527
2599
  );
2528
2600
  }
2529
- function findWorkerFiles(branchPath, input, builtInPaths) {
2601
+ async function findWorkerFiles(branchPath, input, builtInPaths) {
2530
2602
  const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
2531
- if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
2603
+ if (!(await statIfExists(start.absolutePath))?.isDirectory()) {
2532
2604
  throw new Error(`Directory not found: ${start.displayPath}`);
2533
2605
  }
2534
2606
  const pattern = normalizeFindPattern(input.pattern ?? "*");
@@ -2538,7 +2610,7 @@ function findWorkerFiles(branchPath, input, builtInPaths) {
2538
2610
  const matches = [];
2539
2611
  let count = 0;
2540
2612
  let truncated = false;
2541
- for (const entry of walkWorkerEntries(branchPath, start)) {
2613
+ for (const entry of await walkWorkerEntries(branchPath, start)) {
2542
2614
  if (entry.absolutePath === start.absolutePath) continue;
2543
2615
  const isDirectory = entry.stat.isDirectory();
2544
2616
  if (entryType === "file" && !entry.stat.isFile()) continue;
@@ -2583,13 +2655,13 @@ async function executeFindOperation(input) {
2583
2655
  builtInPaths
2584
2656
  );
2585
2657
  }
2586
- function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
2658
+ async function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
2587
2659
  const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
2588
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
2660
+ if (!(await statIfExists(resolved.absolutePath))?.isDirectory()) {
2589
2661
  throw new Error(`Directory not found: ${resolved.displayPath}`);
2590
2662
  }
2591
2663
  const limit = normalizeToolLimit(inputLimit, 200, 1e3);
2592
- const entries = import_node_fs.default.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));
2664
+ const entries = (await import_node_fs.default.promises.readdir(resolved.absolutePath, { withFileTypes: true })).filter((entry) => !IGNORED_ENTRY_NAMES.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
2593
2665
  const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
2594
2666
  const truncated = entries.length > displayed.length;
2595
2667
  const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
@@ -2640,36 +2712,40 @@ function resolveWorkerCodePath(branchPath, inputPath) {
2640
2712
  }
2641
2713
  return { absolutePath, displayPath };
2642
2714
  }
2643
- function listWorkerCodeDirectory(branchPath, inputPath) {
2715
+ async function listWorkerCodeDirectory(branchPath, inputPath) {
2644
2716
  const resolved = resolveWorkerCodePath(branchPath, inputPath);
2645
- if (!import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
2717
+ if (!(await import_node_fs.default.promises.stat(resolved.absolutePath)).isDirectory()) {
2646
2718
  throw new Error(`Code directory not found: ${resolved.displayPath}`);
2647
2719
  }
2648
- const entries = import_node_fs.default.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => entry.name.toLowerCase() !== ".git").flatMap((entry) => {
2720
+ const listed = (await import_node_fs.default.promises.readdir(resolved.absolutePath, { withFileTypes: true })).filter(
2721
+ (entry) => entry.name.toLowerCase() !== ".git"
2722
+ );
2723
+ const entries = [];
2724
+ for (const entry of listed) {
2649
2725
  const entryPath = import_node_path.default.posix.join(resolved.displayPath, entry.name);
2650
2726
  try {
2651
2727
  const resolvedEntry = resolveWorkerCodePath(branchPath, entryPath);
2652
- const stats = import_node_fs.default.statSync(resolvedEntry.absolutePath);
2653
- if (!stats.isFile() && !stats.isDirectory()) return [];
2654
- return [
2655
- {
2656
- name: entry.name,
2657
- type: stats.isDirectory() ? "directory" : "file",
2658
- size: stats.isDirectory() ? null : stats.size,
2659
- mtime: stats.mtime.toISOString()
2660
- }
2661
- ];
2728
+ const stats = await import_node_fs.default.promises.stat(resolvedEntry.absolutePath);
2729
+ if (!stats.isFile() && !stats.isDirectory()) continue;
2730
+ entries.push({
2731
+ name: entry.name,
2732
+ type: stats.isDirectory() ? "directory" : "file",
2733
+ size: stats.isDirectory() ? null : stats.size,
2734
+ mtime: stats.mtime.toISOString()
2735
+ });
2662
2736
  } catch {
2663
- return [];
2664
2737
  }
2665
- }).sort((left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name));
2738
+ }
2739
+ entries.sort(
2740
+ (left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name)
2741
+ );
2666
2742
  return { type: "code_list", path: resolved.displayPath, entries };
2667
2743
  }
2668
- function readWorkerCodeFile(branchPath, inputPath) {
2744
+ async function readWorkerCodeFile(branchPath, inputPath) {
2669
2745
  const resolved = resolveWorkerCodePath(branchPath, inputPath);
2670
- const stats = import_node_fs.default.statSync(resolved.absolutePath);
2746
+ const stats = await import_node_fs.default.promises.stat(resolved.absolutePath);
2671
2747
  if (!stats.isFile()) throw new Error(`Code file not found: ${resolved.displayPath}`);
2672
- const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
2748
+ const bytes = await import_node_fs.default.promises.readFile(resolved.absolutePath);
2673
2749
  return {
2674
2750
  type: "code_read",
2675
2751
  path: resolved.displayPath,
@@ -2678,17 +2754,17 @@ function readWorkerCodeFile(branchPath, inputPath) {
2678
2754
  base64: bytes.toString("base64")
2679
2755
  };
2680
2756
  }
2681
- function executeCodeListOperation(input) {
2757
+ async function executeCodeListOperation(input) {
2682
2758
  if (input.resolvedTarget.target.type !== "project") throw new Error("Code listing requires a project branch target");
2683
2759
  return listWorkerCodeDirectory(input.resolvedTarget.rootPath, input.message.path);
2684
2760
  }
2685
- function executeCodeReadOperation(input) {
2761
+ async function executeCodeReadOperation(input) {
2686
2762
  if (input.resolvedTarget.target.type !== "project") throw new Error("Code reading requires a project branch target");
2687
2763
  return readWorkerCodeFile(input.resolvedTarget.rootPath, input.message.path);
2688
2764
  }
2689
- function readWorkerImageFile(branchPath, filePath, builtInPaths) {
2765
+ async function readWorkerImageFile(branchPath, filePath, builtInPaths) {
2690
2766
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
2691
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
2767
+ if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
2692
2768
  throw new Error(`File not found: ${resolved.displayPath}`);
2693
2769
  }
2694
2770
  const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
@@ -2696,7 +2772,7 @@ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
2696
2772
  if (!mediaType) {
2697
2773
  throw new Error("read supports PNG and JPEG image inputs only");
2698
2774
  }
2699
- const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
2775
+ const bytes = await import_node_fs.default.promises.readFile(resolved.absolutePath);
2700
2776
  const dimensions = readImageDimensions(bytes, mediaType);
2701
2777
  return {
2702
2778
  type: "view_file_bytes",
@@ -3023,26 +3099,28 @@ async function executeStreamingCommand(input) {
3023
3099
  }
3024
3100
  const [exitCode] = await Promise.all([
3025
3101
  subprocess.exited,
3026
- streamCommandOutput(subprocess.stdout, (data) => {
3027
- sendWorkerMessage(input.ws, {
3102
+ streamCommandOutput(
3103
+ subprocess.stdout,
3104
+ (data) => sendWorkerOutput(input.ws, {
3028
3105
  type: "exec_output",
3029
3106
  runId: input.message.runId,
3030
3107
  stream: "stdout",
3031
3108
  data
3032
- });
3033
- }),
3034
- streamCommandOutput(subprocess.stderr, (data) => {
3035
- sendWorkerMessage(input.ws, {
3109
+ })
3110
+ ),
3111
+ streamCommandOutput(
3112
+ subprocess.stderr,
3113
+ (data) => sendWorkerOutput(input.ws, {
3036
3114
  type: "exec_output",
3037
3115
  runId: input.message.runId,
3038
3116
  stream: "stderr",
3039
3117
  data
3040
- });
3041
- })
3118
+ })
3119
+ )
3042
3120
  ]);
3043
3121
  const diagnostic = await input.resources.diagnose();
3044
3122
  if (diagnostic) {
3045
- sendWorkerMessage(input.ws, {
3123
+ await sendWorkerOutput(input.ws, {
3046
3124
  type: "exec_output",
3047
3125
  runId: input.message.runId,
3048
3126
  stream: "stderr",
@@ -3114,6 +3192,39 @@ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
3114
3192
  );
3115
3193
  }
3116
3194
  }
3195
+ async function cancelProcessRun(runId, scope) {
3196
+ const spawned = activeProcesses.get(runId);
3197
+ if (scope === "unstarted" && spawned) return { cancelled: false, message: `${spawned.command} already started`, outcome: "started" };
3198
+ if (!spawned) cancelledProcessRuns.add(runId);
3199
+ const revocation = workerCommandLauncher?.cancel(runId);
3200
+ let resourceCancellationError;
3201
+ let launchRevoked = false;
3202
+ try {
3203
+ launchRevoked = await revocation ?? false;
3204
+ } catch (error) {
3205
+ resourceCancellationError = error instanceof Error ? error.message : String(error);
3206
+ }
3207
+ let cancelled = resourceCancellationError === void 0;
3208
+ let cancelMessage;
3209
+ let outcome;
3210
+ if (spawned) {
3211
+ outcome = "stopped";
3212
+ try {
3213
+ closeProcessStdin(spawned);
3214
+ await (0, import_process_tree.terminateProcessTree)(spawned.process);
3215
+ cancelMessage = `Stopped ${spawned.command}`;
3216
+ } catch (error) {
3217
+ cancelled = false;
3218
+ cancelMessage = `Failed to stop ${spawned.command}: ${error instanceof Error ? error.message : String(error)}`;
3219
+ }
3220
+ } else {
3221
+ const reservationAborted = abortPendingReservation(`run:${runId}`, "Cancellation queued before command start");
3222
+ outcome = launchRevoked || reservationAborted ? "unstarted" : "unknown";
3223
+ cancelMessage = "Cancellation queued before command start";
3224
+ }
3225
+ if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
3226
+ return { cancelled, message: cancelMessage, outcome };
3227
+ }
3117
3228
  function closeProcessStdin(active) {
3118
3229
  if (!active?.stdin) {
3119
3230
  return;
@@ -3139,21 +3250,35 @@ function sendReplayWorkerMessage(ws, message) {
3139
3250
  }
3140
3251
  function sendWorkerMessage(ws, message) {
3141
3252
  if (launcherFailureInProgress) return;
3142
- if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
3143
- return;
3144
3253
  const transportError = "error" in message && typeof message.error === "string" ? message.error : message.type === "workspace_sync_result" && message.result.outcome === "failed" ? message.result.error : void 0;
3145
- if (recoveryStore && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
3146
- recoveryStore.unknown(message.requestId);
3254
+ if (recoveryJournal && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
3255
+ recoveryJournal.unknown(message.requestId);
3147
3256
  currentWorkerSocket?.close(1012, "Communication recovery");
3148
3257
  return;
3149
3258
  }
3150
- const recorded = recoveryStore?.record(message) ?? message;
3151
- if ((message.type === "exec_exit" || message.type === "exec_error") && infrastructureStoppedRunIds.has(message.runId)) {
3152
- recoveryStore?.unknownRun(message.runId);
3259
+ if (!recoveryJournal) {
3260
+ sendReplayWorkerMessage(ws, message);
3153
3261
  return;
3154
3262
  }
3155
- if (message.type === "exec_output" && workerRecoveryId !== void 0) return;
3156
- sendReplayWorkerMessage(ws, recorded);
3263
+ recoveryJournal.send(message);
3264
+ }
3265
+ function sendWorkerOutput(ws, frame) {
3266
+ if (launcherFailureInProgress) return Promise.resolve();
3267
+ if (!recoveryJournal) {
3268
+ sendReplayWorkerMessage(ws, frame);
3269
+ return Promise.resolve();
3270
+ }
3271
+ return recoveryJournal.sendOutput(frame);
3272
+ }
3273
+ function deliverJournaledWorkerMessage(message) {
3274
+ const target = currentWorkerSocket;
3275
+ if ((message.type === "exec_exit" || message.type === "exec_error") && typeof message.runId === "string" && infrastructureStoppedRunIds.has(message.runId)) {
3276
+ recoveryJournal?.unknownRun(message.runId);
3277
+ return true;
3278
+ }
3279
+ if (!target || target.readyState !== WebSocket.OPEN) return false;
3280
+ if (message.type === "exec_output" && workerRecoveryId !== void 0) return false;
3281
+ return sendReplayWorkerMessage(target, message);
3157
3282
  }
3158
3283
  function sendSerializedWorkerMessage(ws, serialized) {
3159
3284
  sendWorkerMessage(ws, JSON.parse(serialized));
@@ -3716,9 +3841,19 @@ async function startWorker(options, projectRuntime = {
3716
3841
  `);
3717
3842
  }
3718
3843
  const rootDir = import_node_path.default.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
3719
- recoveryStore ??= new import_recovery_store.WorkerRecoveryStore(
3720
- import_node_path.default.join(rootDir, ".worker-recovery", (0, import_node_crypto.createHash)("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
3721
- );
3844
+ if (!recoveryJournal) {
3845
+ const journal = await import_recovery_journal.WorkerRecoveryJournal.open({
3846
+ filename: import_node_path.default.join(
3847
+ rootDir,
3848
+ ".worker-recovery",
3849
+ (0, import_node_crypto.createHash)("sha256").update(`${baseUrl}\0${label}`).digest("hex"),
3850
+ "protocol.sqlite"
3851
+ )
3852
+ });
3853
+ journal.deliver = deliverJournaledWorkerMessage;
3854
+ journal.onFailure = handleRecoveryJournalFailure;
3855
+ recoveryJournal = journal;
3856
+ }
3722
3857
  const projectsRoot = import_node_path.default.join(rootDir, "projects");
3723
3858
  const syncRoot = import_node_path.default.join(rootDir, "sync");
3724
3859
  const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
@@ -3821,7 +3956,6 @@ async function startWorker(options, projectRuntime = {
3821
3956
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
3822
3957
  const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
3823
3958
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
3824
- const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
3825
3959
  const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
3826
3960
  const workspaceProjectRelativePath = (projectId, branchName) => import_node_path.default.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
3827
3961
  const workspacePlanRelativePath = (projectId, branchName) => import_node_path.default.posix.join("plans", projectId, encodeURIComponent(branchName));
@@ -3904,7 +4038,8 @@ async function startWorker(options, projectRuntime = {
3904
4038
  sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
3905
4039
  planSourcePath: import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"))
3906
4040
  }));
3907
- (0, import_project_worktrees.removeProjectWorktrees)({ projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
4041
+ const removed = (0, import_project_worktrees.removeProjectWorktrees)({ projectsRoot, projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
4042
+ if (removed.stagedForCollectionPath) checkoutGarbageCollector.enqueue(removed.stagedForCollectionPath);
3908
4043
  for (const branch of branches) {
3909
4044
  import_node_fs.default.rmSync(branch.planSourcePath, { recursive: true, force: true });
3910
4045
  const key = projectBranchKey(project.projectId, branch.branchName);
@@ -3992,14 +4127,14 @@ async function startWorker(options, projectRuntime = {
3992
4127
  )
3993
4128
  };
3994
4129
  };
3995
- const stageDurableWorkspaceDeletions = () => {
4130
+ const stageDurableWorkspaceDeletions = async () => {
3996
4131
  for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
3997
4132
  const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
3998
4133
  if (deletion.kind === "project") {
3999
4134
  if (!deletion.projectDeleted) {
4000
4135
  const nextProject = projectConfigById.get(deletion.projectId);
4001
4136
  if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
4002
- (0, import_workspace_path_move.preserveProjectCheckoutPathMove)({
4137
+ await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_checkout_path_move", {
4003
4138
  oldProjectRoot: deletion.managedPath,
4004
4139
  newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
4005
4140
  projectsDurabilityRoot: projectsRoot,
@@ -4366,7 +4501,7 @@ async function startWorker(options, projectRuntime = {
4366
4501
  }
4367
4502
  }
4368
4503
  if (transitionReseedRequired) {
4369
- projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
4504
+ projectWorkspaceState = await projectWorkspaceStateStore.recordEnabledRepositoryTransition({
4370
4505
  projectId: project.projectId,
4371
4506
  transitionId: project.repositoryTransitionId
4372
4507
  });
@@ -4535,7 +4670,7 @@ async function startWorker(options, projectRuntime = {
4535
4670
  )
4536
4671
  ];
4537
4672
  if (publishedTombstoneIds.length > 0) {
4538
- projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
4673
+ projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
4539
4674
  tombstoneIds: publishedTombstoneIds,
4540
4675
  publishedHead
4541
4676
  });
@@ -4554,7 +4689,7 @@ async function startWorker(options, projectRuntime = {
4554
4689
  forgetLocalProjectMirrorRefsTokens(deletion.projectId, deletion.branchName);
4555
4690
  }
4556
4691
  if (deletion.tombstoneId && !deletion.projectDeleted) {
4557
- projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
4692
+ projectWorkspaceState = await projectWorkspaceStateStore.recordMirrorRefDeletion({
4558
4693
  tombstoneId: deletion.tombstoneId,
4559
4694
  branchName: deletion.branchName
4560
4695
  });
@@ -4751,7 +4886,10 @@ async function startWorker(options, projectRuntime = {
4751
4886
  };
4752
4887
  };
4753
4888
  const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
4754
- (message) => sendWorkerMessageFromCurrentSource(ws, message)
4889
+ (message) => sendWorkerMessageFromCurrentSource(ws, message),
4890
+ {
4891
+ fence: workspaceMountHoldFence
4892
+ }
4755
4893
  );
4756
4894
  const performWorkspaceSync = async (input) => {
4757
4895
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
@@ -5409,7 +5547,7 @@ async function startWorker(options, projectRuntime = {
5409
5547
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
5410
5548
  );
5411
5549
  if (!incidentDeferral.incidentId) {
5412
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
5550
+ projectWorkspaceState = await projectWorkspaceStateStore.reconcile({
5413
5551
  desiredProjects: message.projects,
5414
5552
  preserveOnlyBranches
5415
5553
  });
@@ -5420,7 +5558,7 @@ async function startWorker(options, projectRuntime = {
5420
5558
  (candidate) => candidate.projectId === project.projectId && candidate.branchName === branch.branchName
5421
5559
  );
5422
5560
  if (pending2 && pending2.branchId === branch.branchId) {
5423
- projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
5561
+ projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
5424
5562
  }
5425
5563
  }
5426
5564
  }
@@ -5588,7 +5726,7 @@ async function startWorker(options, projectRuntime = {
5588
5726
  for (const projectId of [...readyProjectIds]) {
5589
5727
  if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
5590
5728
  }
5591
- stageDurableWorkspaceDeletions();
5729
+ await stageDurableWorkspaceDeletions();
5592
5730
  const mountsBeforeGit = buildWorkspaceMounts();
5593
5731
  await (0, import_workspace_git_sync.hydrateWorkspaceGitMounts)(
5594
5732
  workspaceShadowRoot,
@@ -5634,7 +5772,12 @@ async function startWorker(options, projectRuntime = {
5634
5772
  const connectSyncMs = Date.now() - connectSyncStartedAtMs;
5635
5773
  const result = syncResult.telemetry ? syncResult : {
5636
5774
  ...syncResult,
5637
- telemetry: { totalMs: connectSyncMs, queueMs: 0, prepareMs: lastWorkspaceSyncMirrorObservationMs, synchronizeMs: connectSyncMs }
5775
+ telemetry: {
5776
+ totalMs: connectSyncMs,
5777
+ queueMs: 0,
5778
+ prepareMs: lastWorkspaceSyncMirrorObservationMs,
5779
+ synchronizeMs: connectSyncMs
5780
+ }
5638
5781
  };
5639
5782
  assertConfigurationSyncStillAdmitted();
5640
5783
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
@@ -5653,7 +5796,7 @@ async function startWorker(options, projectRuntime = {
5653
5796
  ) ? [deletion.tombstoneId] : []
5654
5797
  );
5655
5798
  if (layoutCleanupIds.length > 0) {
5656
- projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
5799
+ projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
5657
5800
  tombstoneIds: layoutCleanupIds,
5658
5801
  publishedHead
5659
5802
  });
@@ -5760,26 +5903,33 @@ async function startWorker(options, projectRuntime = {
5760
5903
  if (startupProjectSnapshotRecoveryCompleted) return;
5761
5904
  sendLifecycleProgress({ phase: "recovering", operationId, detail: "Checking interrupted workspace transactions" });
5762
5905
  const snapshotRecovery = await workspaceSyncSingleFlight.runExclusive(
5763
- async () => await (0, import_project_snapshot_recovery_runner.recoverProjectSnapshotsInChild)({
5764
- projectsRoot,
5765
- snapshotRoot: projectWorktreeSnapshotsRoot,
5766
- onProgress: (progress) => {
5767
- const lifecycleProgress = {
5768
- phase: "recovering",
5769
- operationId,
5770
- detail: describeSnapshotProgress(progress),
5771
- completedBytes: progress.completedBytes,
5772
- completedItems: progress.completedBranches,
5773
- totalItems: progress.totalBranches,
5774
- ...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
5775
- };
5776
- sendLifecycleProgress(lifecycleProgress);
5777
- process.stdout.write(
5778
- `[r5d-worker] ${lifecycleProgress.detail}: ${progress.completedBranches}/${progress.totalBranches} branch(es), ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes
5906
+ async () => await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run(
5907
+ "project_snapshots_recover",
5908
+ {
5909
+ projectsRoot,
5910
+ temporaryRoot: projectWorktreeSnapshotsRoot,
5911
+ currentProcessId: process.pid,
5912
+ currentOwnerSessionId: import_project_worktrees.PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
5913
+ },
5914
+ {
5915
+ onProgress: (progress) => {
5916
+ const lifecycleProgress = {
5917
+ phase: "recovering",
5918
+ operationId,
5919
+ detail: describeSnapshotProgress(progress),
5920
+ completedBytes: progress.completedBytes,
5921
+ completedItems: progress.completedBranches,
5922
+ totalItems: progress.totalBranches,
5923
+ ...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
5924
+ };
5925
+ sendLifecycleProgress(lifecycleProgress);
5926
+ process.stdout.write(
5927
+ `[r5d-worker] ${lifecycleProgress.detail}: ${progress.completedBranches}/${progress.totalBranches} branch(es), ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes
5779
5928
  `
5780
- );
5929
+ );
5930
+ }
5781
5931
  }
5782
- })
5932
+ )
5783
5933
  );
5784
5934
  if (snapshotRecovery.restored.length > 0) {
5785
5935
  process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
@@ -5874,7 +6024,7 @@ async function startWorker(options, projectRuntime = {
5874
6024
  type: "hello",
5875
6025
  resumableProtocol: import_recovery_protocol.WORKER_RESUMABLE_PROTOCOL,
5876
6026
  runtimeId: workerRuntimeId,
5877
- ledgerId: recoveryStore.ledgerId,
6027
+ ledgerId: recoveryJournal.ledgerId,
5878
6028
  startupReady: startupRecoveryComplete,
5879
6029
  hostInfo: {
5880
6030
  hostname: (0, import_node_os.hostname)(),
@@ -5902,16 +6052,39 @@ async function startWorker(options, projectRuntime = {
5902
6052
  }
5903
6053
  };
5904
6054
  sendSerializedWorkerMessage(ws, JSON.stringify(hello));
6055
+ let outputPumpInFlight = false;
5905
6056
  outputReplayTimer = setInterval(() => {
5906
- if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
5907
- for (const output of recoveryStore.output()) {
5908
- if (!sendReplayWorkerMessage(ws, output)) break;
5909
- }
6057
+ if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws || outputPumpInFlight || !recoveryJournal) return;
6058
+ outputPumpInFlight = true;
6059
+ void (async () => {
6060
+ try {
6061
+ for await (const page of recoveryJournal.outputReplay(void 0)) {
6062
+ for (const output of page) {
6063
+ if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws || !sendReplayWorkerMessage(ws, output)) return;
6064
+ }
6065
+ }
6066
+ } catch (error) {
6067
+ process.stderr.write(`[r5d-worker] output replay pump failed: ${error instanceof Error ? error.message : String(error)}
6068
+ `);
6069
+ } finally {
6070
+ outputPumpInFlight = false;
6071
+ }
6072
+ })();
5910
6073
  }, 250);
5911
6074
  outputReplayTimer.unref();
6075
+ let terminalPumpInFlight = false;
5912
6076
  terminalReplayTimer = setInterval(() => {
5913
- if (workerRecoveryId !== void 0) return;
5914
- for (const terminal of recoveryStore?.terminals() ?? []) sendReplayWorkerMessage(ws, terminal);
6077
+ if (workerRecoveryId !== void 0 || terminalPumpInFlight || !recoveryJournal) return;
6078
+ terminalPumpInFlight = true;
6079
+ void recoveryJournal.terminals().then((terminals) => {
6080
+ if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
6081
+ for (const terminal of terminals) sendReplayWorkerMessage(ws, terminal);
6082
+ }).catch((error) => {
6083
+ process.stderr.write(`[r5d-worker] terminal replay pump failed: ${error instanceof Error ? error.message : String(error)}
6084
+ `);
6085
+ }).finally(() => {
6086
+ terminalPumpInFlight = false;
6087
+ });
5915
6088
  }, 5e3);
5916
6089
  terminalReplayTimer.unref();
5917
6090
  process.stdout.write("[r5d-worker] connected\n");
@@ -5924,28 +6097,37 @@ async function startWorker(options, projectRuntime = {
5924
6097
  if (message.type === "recovery_request") {
5925
6098
  workerRecoveryId = message.recoveryId;
5926
6099
  sendActiveProcessReport(ws);
5927
- sendWorkerMessage(ws, {
5928
- type: "recovery_state",
5929
- recoveryId: message.recoveryId,
5930
- runtimeId: workerRuntimeId,
5931
- ledgerId: recoveryStore.ledgerId,
5932
- operations: recoveryStore.operations()
5933
- });
5934
- const cursors = new Map(recoveryStore.outputCursors().map((cursor) => [cursor.runId, cursor]));
5935
- for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
5936
- workerOutputWindow.reset([...cursors.values()]);
5937
- for (const response of recoveryStore.responses(message.requests)) sendReplayWorkerMessage(ws, response);
5938
- for (const output of recoveryStore.output(message.outputOffsets)) {
5939
- while (true) {
5940
- if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
5941
- return;
5942
- if (sendReplayWorkerMessage(ws, output)) break;
5943
- await Bun.sleep(10);
6100
+ recoveryJournal.pauseDelivery();
6101
+ try {
6102
+ const state = await recoveryJournal.recoveryState(message.requests);
6103
+ if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
6104
+ sendWorkerMessage(ws, {
6105
+ type: "recovery_state",
6106
+ recoveryId: message.recoveryId,
6107
+ runtimeId: workerRuntimeId,
6108
+ ledgerId: recoveryJournal.ledgerId,
6109
+ operations: state.operations
6110
+ });
6111
+ const cursors = new Map(state.cursors.map((cursor) => [cursor.runId, cursor]));
6112
+ for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
6113
+ workerOutputWindow.reset([...cursors.values()]);
6114
+ for (const response of state.responses) sendReplayWorkerMessage(ws, response);
6115
+ for await (const page of recoveryJournal.outputReplay(message.outputOffsets)) {
6116
+ for (const output of page) {
6117
+ while (true) {
6118
+ if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
6119
+ return;
6120
+ if (sendReplayWorkerMessage(ws, output)) break;
6121
+ await Bun.sleep(10);
6122
+ }
6123
+ }
5944
6124
  }
6125
+ if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
6126
+ for (const terminal of state.terminals) sendReplayWorkerMessage(ws, terminal);
6127
+ sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
6128
+ } finally {
6129
+ recoveryJournal.resumeDelivery();
5945
6130
  }
5946
- if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
5947
- for (const terminal of recoveryStore.terminals()) sendReplayWorkerMessage(ws, terminal);
5948
- sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
5949
6131
  return;
5950
6132
  }
5951
6133
  if (message.type === "recovery_ready") {
@@ -5960,17 +6142,23 @@ async function startWorker(options, projectRuntime = {
5960
6142
  return;
5961
6143
  }
5962
6144
  if (message.type === "operation_result_ack") {
5963
- recoveryStore.acknowledgeResult(message.requestId);
6145
+ recoveryJournal.acknowledgeResult(message.requestId);
5964
6146
  return;
5965
6147
  }
5966
6148
  if (message.type === "exec_output_ack") {
5967
- recoveryStore.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
6149
+ recoveryJournal.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
5968
6150
  workerOutputWindow.acknowledge(message.runId, message.stream, message.nextOffset);
5969
6151
  return;
5970
6152
  }
5971
6153
  if (message.type === "cancel_session") {
5972
- recoveryStore.cancelSession(message.sessionId);
6154
+ const durableCancellation = recoveryJournal.cancelSession(message.sessionId);
6155
+ abortPendingReservations("Session cancelled before command start", message.sessionId);
5973
6156
  await Promise.all([
6157
+ durableCancellation,
6158
+ // File writes already in flight for the session settle before the
6159
+ // acknowledgement; queued ones re-check the cancellation before
6160
+ // their first write and never start.
6161
+ sessionFileMutations.settled(message.sessionId),
5974
6162
  workerCommandLauncher?.cancelSession(message.sessionId),
5975
6163
  ...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
5976
6164
  cancelledProcessRuns.add(runId);
@@ -5985,27 +6173,75 @@ async function startWorker(options, projectRuntime = {
5985
6173
  if (!startupRecoveryComplete && message.type !== "ping") return;
5986
6174
  if (currentWorkerSocket !== ws) return;
5987
6175
  const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
6176
+ if (message.type === "cancel") {
6177
+ const admission = recoveryJournal.admit({ ...message, requestId: message.requestId });
6178
+ const cancelled = await cancelProcessRun(message.runId, message.scope);
6179
+ let admitted;
6180
+ try {
6181
+ admitted = await admission;
6182
+ } catch (error) {
6183
+ if (!(error instanceof import_recovery_journal.WorkerRecoveryJournalBusyError)) throw error;
6184
+ process.stderr.write(`[r5d-worker] cancel ${message.runId} stopped the run but was not durably admitted: ${error.message}
6185
+ `);
6186
+ return;
6187
+ }
6188
+ if (admitted.admission === "conflict") {
6189
+ ws.close(1008, "Operation identity conflict");
6190
+ return;
6191
+ }
6192
+ sendWorkerMessage(ws, { type: "operation_received", requestId: message.requestId });
6193
+ if (admitted.admission !== "new") {
6194
+ if (admitted.response && admitted.admission !== "unknown") sendReplayWorkerMessage(ws, admitted.response);
6195
+ return;
6196
+ }
6197
+ sendSerializedWorkerMessage(
6198
+ ws,
6199
+ JSON.stringify({
6200
+ type: "cancel_result",
6201
+ requestId: message.requestId,
6202
+ runId: message.runId,
6203
+ cancelled: cancelled.cancelled,
6204
+ message: cancelled.message,
6205
+ outcome: cancelled.outcome
6206
+ })
6207
+ );
6208
+ return;
6209
+ }
5988
6210
  if (operationRequestId) {
5989
- const admission = recoveryStore.admit({ ...message, requestId: operationRequestId });
5990
- if (admission === "conflict") {
6211
+ let admitted;
6212
+ try {
6213
+ admitted = await recoveryJournal.admit({ ...message, requestId: operationRequestId });
6214
+ } catch (error) {
6215
+ if (!(error instanceof import_recovery_journal.WorkerRecoveryJournalBusyError)) throw error;
6216
+ process.stderr.write(`[r5d-worker] ${message.type} ${operationRequestId} was not admitted: ${error.message}
6217
+ `);
6218
+ return;
6219
+ }
6220
+ if (admitted.admission === "conflict") {
5991
6221
  ws.close(1008, "Operation identity conflict");
5992
6222
  return;
5993
6223
  }
5994
6224
  sendWorkerMessage(ws, { type: "operation_received", requestId: operationRequestId });
5995
- if (admission !== "new") {
5996
- const response = recoveryStore.response(operationRequestId);
5997
- if (response && admission !== "unknown") sendReplayWorkerMessage(ws, response);
5998
- if (!(admission === "unknown" && message.type === "delete_project_branch" && recoveryStore.readmitUnknownBranchDeletion(operationRequestId))) {
6225
+ if (admitted.admission !== "new") {
6226
+ if (admitted.response && admitted.admission !== "unknown") sendReplayWorkerMessage(ws, admitted.response);
6227
+ if (!(admitted.admission === "unknown" && message.type === "delete_project_branch" && await recoveryJournal.readmitUnknownBranchDeletion(
6228
+ operationRequestId,
6229
+ "sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0
6230
+ ))) {
5999
6231
  return;
6000
6232
  }
6001
6233
  }
6234
+ if (recoveryJournal.isRequestCancelled(operationRequestId)) {
6235
+ recoveryJournal.unknown(operationRequestId);
6236
+ return;
6237
+ }
6002
6238
  }
6003
6239
  const messageAdmissionGeneration = workerAdmissionGeneration;
6004
6240
  const admissionCredentials = configuredCredentialGenerationFingerprint;
6005
6241
  const assertMessageAdmission = () => {
6006
6242
  if (operationRequestId) {
6007
- if (workerLeaseExpired || recoveryStore.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
6008
- recoveryStore.unknown(operationRequestId);
6243
+ if (workerLeaseExpired || recoveryJournal.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
6244
+ recoveryJournal.unknown(operationRequestId);
6009
6245
  throw new StaleWorkerAdmissionError();
6010
6246
  }
6011
6247
  return;
@@ -6142,7 +6378,7 @@ async function startWorker(options, projectRuntime = {
6142
6378
  }
6143
6379
  recoveryDeadline ??= Date.now() + import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS;
6144
6380
  if (workerLeaseExpired || Date.now() >= recoveryDeadline) {
6145
- recoveryStore.unknown(message.requestId);
6381
+ recoveryJournal.unknown(message.requestId);
6146
6382
  return;
6147
6383
  }
6148
6384
  currentWorkerSocket?.close(1012, "Communication recovery");
@@ -6172,7 +6408,7 @@ async function startWorker(options, projectRuntime = {
6172
6408
  projectId: message.projectId,
6173
6409
  branchName: message.targetBranch
6174
6410
  };
6175
- const created = await workspaceSyncSingleFlight.runBranchMutation(() => {
6411
+ const created = await workspaceSyncSingleFlight.runBranchMutation(async () => {
6176
6412
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
6177
6413
  throw new Error(
6178
6414
  `Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
@@ -6187,7 +6423,7 @@ async function startWorker(options, projectRuntime = {
6187
6423
  const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
6188
6424
  ({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
6189
6425
  );
6190
- projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
6426
+ projectWorkspaceState = await projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
6191
6427
  let gitBranchCreated = false;
6192
6428
  try {
6193
6429
  if (!readyProjectIds.has(project.projectId)) {
@@ -6208,7 +6444,7 @@ async function startWorker(options, projectRuntime = {
6208
6444
  }
6209
6445
  return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
6210
6446
  }
6211
- const createdBranch = (0, import_project_worktrees.createOrRetryLinkedProjectBranch)({
6447
+ const createdBranch = await (0, import_project_worktrees.createOrRetryLinkedProjectBranch)({
6212
6448
  projectRoot: configuredProjectRoot(projectsRoot, project),
6213
6449
  primaryBranchName: primaryProjectBranch(project),
6214
6450
  sourceBranchName: message.sourceBranch,
@@ -6233,7 +6469,7 @@ async function startWorker(options, projectRuntime = {
6233
6469
  return createdBranch;
6234
6470
  } catch (error) {
6235
6471
  if (!pendingWasRecorded && !gitBranchCreated && !(0, import_project_worktrees.projectBranchMayExistAfterCreateFailure)(error)) {
6236
- projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
6472
+ projectWorkspaceState = await projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
6237
6473
  }
6238
6474
  throw error;
6239
6475
  }
@@ -6277,7 +6513,7 @@ async function startWorker(options, projectRuntime = {
6277
6513
  });
6278
6514
  try {
6279
6515
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
6280
- const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(() => {
6516
+ const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(async () => {
6281
6517
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
6282
6518
  throw new Error(
6283
6519
  `Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
@@ -6304,12 +6540,12 @@ async function startWorker(options, projectRuntime = {
6304
6540
  if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
6305
6541
  return false;
6306
6542
  }
6307
- projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
6543
+ projectWorkspaceState = await projectWorkspaceStateStore.beginBranchDeletion({
6308
6544
  projectId: message.projectId,
6309
6545
  branchName: message.branchName
6310
6546
  });
6311
6547
  if (pendingLocalBranch) {
6312
- projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
6548
+ projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
6313
6549
  }
6314
6550
  pendingCreatedBranchPublicationNotBefore.delete((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(message.projectId, message.branchName));
6315
6551
  if (pendingMirrorDeletes.has(deletionKey)) return true;
@@ -6413,7 +6649,7 @@ async function startWorker(options, projectRuntime = {
6413
6649
  }
6414
6650
  if (message.type === "exec_terminal_ack") {
6415
6651
  pendingProcessTerminals.delete(message.runId);
6416
- recoveryStore.acknowledgeTerminal(message.runId);
6652
+ recoveryJournal.acknowledgeTerminal(message.runId);
6417
6653
  return;
6418
6654
  }
6419
6655
  if (message.type === "port_forward_connect") {
@@ -6468,42 +6704,6 @@ async function startWorker(options, projectRuntime = {
6468
6704
  sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
6469
6705
  return;
6470
6706
  }
6471
- if (message.type === "cancel") {
6472
- let resourceCancellationError;
6473
- try {
6474
- await workerCommandLauncher?.cancel(message.runId);
6475
- } catch (error) {
6476
- resourceCancellationError = error instanceof Error ? error.message : String(error);
6477
- }
6478
- const active = activeProcesses.get(message.runId);
6479
- let cancelled = resourceCancellationError === void 0;
6480
- let cancelMessage;
6481
- if (active) {
6482
- try {
6483
- closeProcessStdin(active);
6484
- await (0, import_process_tree.terminateProcessTree)(active.process);
6485
- cancelMessage = `Stopped ${active.command}`;
6486
- } catch (error) {
6487
- cancelled = false;
6488
- cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
6489
- }
6490
- } else {
6491
- cancelledProcessRuns.add(message.runId);
6492
- cancelMessage = "Cancellation queued before command start";
6493
- }
6494
- if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
6495
- sendSerializedWorkerMessage(
6496
- ws,
6497
- JSON.stringify({
6498
- type: "cancel_result",
6499
- requestId: message.requestId,
6500
- runId: message.runId,
6501
- cancelled,
6502
- message: cancelMessage
6503
- })
6504
- );
6505
- return;
6506
- }
6507
6707
  if (message.type === "exec_stdin") {
6508
6708
  const sendAck = (payload) => sendSerializedWorkerMessage(
6509
6709
  ws,
@@ -6584,10 +6784,20 @@ async function startWorker(options, projectRuntime = {
6584
6784
  let mutationLeaseTransferred = false;
6585
6785
  try {
6586
6786
  resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6587
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6588
- workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6589
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6590
- });
6787
+ const reservationAbort = registerPendingReservation(`pty:${message.ptyId}`, void 0);
6788
+ try {
6789
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(
6790
+ message.target,
6791
+ workspaceSyncSingleFlight,
6792
+ () => {
6793
+ workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6794
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6795
+ },
6796
+ { signal: reservationAbort.signal }
6797
+ );
6798
+ } finally {
6799
+ unregisterPendingReservation(`pty:${message.ptyId}`, reservationAbort);
6800
+ }
6591
6801
  releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
6592
6802
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
6593
6803
  throw new Error(
@@ -6652,6 +6862,7 @@ async function startWorker(options, projectRuntime = {
6652
6862
  }
6653
6863
  if (message.type === "pty_close") {
6654
6864
  const activePty = activePtys.get(message.ptyId);
6865
+ abortPendingReservation(`pty:${message.ptyId}`, "Shell closed before it started");
6655
6866
  await closePty(message);
6656
6867
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
6657
6868
  markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
@@ -6688,11 +6899,21 @@ async function startWorker(options, projectRuntime = {
6688
6899
  try {
6689
6900
  resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6690
6901
  if (hasWorkspaceEffect) {
6691
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6692
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
6693
- targetReserved = true;
6694
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6695
- });
6902
+ const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
6903
+ try {
6904
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(
6905
+ message.target,
6906
+ workspaceSyncSingleFlight,
6907
+ () => {
6908
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
6909
+ targetReserved = true;
6910
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6911
+ },
6912
+ { signal: reservationAbort.signal }
6913
+ );
6914
+ } finally {
6915
+ unregisterPendingReservation(`run:${message.runId}`, reservationAbort);
6916
+ }
6696
6917
  }
6697
6918
  const runCommand = async () => {
6698
6919
  if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
@@ -6744,7 +6965,7 @@ async function startWorker(options, projectRuntime = {
6744
6965
  cancelledProcessRuns.delete(message.runId);
6745
6966
  }
6746
6967
  if (infrastructureStoppedRunIds.has(message.runId)) {
6747
- recoveryStore.unknown(message.requestId);
6968
+ recoveryJournal.unknown(message.requestId);
6748
6969
  return;
6749
6970
  }
6750
6971
  sendSerializedWorkerMessage(ws, JSON.stringify(result));
@@ -6818,11 +7039,21 @@ async function startWorker(options, projectRuntime = {
6818
7039
  try {
6819
7040
  resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6820
7041
  if (hasWorkspaceEffect) {
6821
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6822
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
6823
- targetReserved = true;
6824
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6825
- });
7042
+ const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
7043
+ try {
7044
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(
7045
+ message.target,
7046
+ workspaceSyncSingleFlight,
7047
+ () => {
7048
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
7049
+ targetReserved = true;
7050
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
7051
+ },
7052
+ { signal: reservationAbort.signal }
7053
+ );
7054
+ } finally {
7055
+ unregisterPendingReservation(`run:${message.runId}`, reservationAbort);
7056
+ }
6826
7057
  }
6827
7058
  await (0, import_workspace_command_sync_policy.runReservedWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand, () => {
6828
7059
  if (targetReserved) {
@@ -6857,16 +7088,40 @@ async function startWorker(options, projectRuntime = {
6857
7088
  return;
6858
7089
  }
6859
7090
  const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
6860
- const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing) && targetMayMutateVisibleWorkspace(message.target);
7091
+ const mutatesFiles = message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing;
7092
+ const mutatesVisibleWorkspace = mutatesFiles && targetMayMutateVisibleWorkspace(message.target);
6861
7093
  if (reservesVisibleWorkspace) {
6862
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6863
- workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
6864
- if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
6865
- });
7094
+ const reservationAbort = registerPendingReservation(
7095
+ `op:${message.requestId}`,
7096
+ "sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0
7097
+ );
7098
+ try {
7099
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(
7100
+ message.target,
7101
+ workspaceSyncSingleFlight,
7102
+ () => {
7103
+ workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
7104
+ if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
7105
+ },
7106
+ { signal: reservationAbort.signal }
7107
+ );
7108
+ } catch (error) {
7109
+ sendSerializedWorkerMessage(
7110
+ ws,
7111
+ JSON.stringify({
7112
+ type: "operation_result",
7113
+ requestId: message.requestId,
7114
+ error: error instanceof Error ? error.message : String(error)
7115
+ })
7116
+ );
7117
+ return;
7118
+ } finally {
7119
+ unregisterPendingReservation(`op:${message.requestId}`, reservationAbort);
7120
+ }
6866
7121
  }
6867
7122
  let dirtyTrigger;
6868
7123
  try {
6869
- const result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
7124
+ const operation = (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
6870
7125
  const resolvedTarget = resolveMessageTarget(message.target);
6871
7126
  return await executeOperation({
6872
7127
  message,
@@ -6877,6 +7132,8 @@ async function startWorker(options, projectRuntime = {
6877
7132
  assertAdmission: assertMessageAdmission
6878
7133
  });
6879
7134
  });
7135
+ if (mutatesFiles && typeof message.sessionId === "string") sessionFileMutations.track(message.sessionId, operation);
7136
+ const result = await operation;
6880
7137
  sendSerializedWorkerMessage(
6881
7138
  ws,
6882
7139
  JSON.stringify({
@@ -6960,6 +7217,7 @@ async function startWorker(options, projectRuntime = {
6960
7217
  projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
6961
7218
  }
6962
7219
  if (currentWorkerSocket === ws) currentWorkerSocket = null;
7220
+ abortPendingReservations("Worker connection closed before command start");
6963
7221
  workerRecoveryId = "reconnecting";
6964
7222
  const reason = event.reason ? `: ${event.reason}` : "";
6965
7223
  process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
@@ -6993,6 +7251,11 @@ async function startWorker(options, projectRuntime = {
6993
7251
  });
6994
7252
  }
6995
7253
  function installWorkerRuntimeFailureLogging() {
7254
+ (0, import_workspace_filesystem_executor.installWorkspaceFilesystemExecutorFailureHandler)((error) => {
7255
+ process.stderr.write(`[r5d-worker] ${error.message}; restarting the worker runtime
7256
+ `);
7257
+ void exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
7258
+ });
6996
7259
  process.on("uncaughtException", (error) => {
6997
7260
  process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
6998
7261
  `);
@@ -7005,11 +7268,6 @@ function installWorkerRuntimeFailureLogging() {
7005
7268
  });
7006
7269
  }
7007
7270
  async function main() {
7008
- if (process.argv[2] === import_project_snapshot_recovery_runner.PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND) {
7009
- const exitCode2 = (0, import_project_snapshot_recovery_runner.runProjectSnapshotRecoveryHelper)(process.argv.slice(3));
7010
- if (exitCode2 !== 0) process.exit(exitCode2);
7011
- return;
7012
- }
7013
7271
  const parsed = parseArgs(process.argv.slice(2));
7014
7272
  if (parsed.command === "help") {
7015
7273
  printHelp();