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