@ricsam/r5d-worker 0.0.122 → 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 (67) hide show
  1. package/README.md +6 -3
  2. package/dist/cjs/command-launcher.cjs +76 -2
  3. package/dist/cjs/control-command-policy.cjs +177 -0
  4. package/dist/cjs/main.cjs +541 -247
  5. package/dist/cjs/package.json +1 -1
  6. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  7. package/dist/cjs/project-workspace-state.cjs +51 -35
  8. package/dist/cjs/project-worktrees.cjs +79 -34
  9. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  10. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  11. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  12. package/dist/cjs/recovery-journal.cjs +735 -0
  13. package/dist/cjs/recovery-store.cjs +8 -0
  14. package/dist/cjs/session-file-mutations.cjs +61 -0
  15. package/dist/cjs/working-tree-mirror.cjs +1 -0
  16. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  17. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  18. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  19. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  20. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  21. package/dist/cjs/workspace-git-sync.cjs +275 -201
  22. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  23. package/dist/mjs/command-launcher.mjs +75 -2
  24. package/dist/mjs/control-command-policy.mjs +146 -0
  25. package/dist/mjs/main.mjs +549 -253
  26. package/dist/mjs/package.json +1 -1
  27. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  28. package/dist/mjs/project-workspace-state.mjs +51 -35
  29. package/dist/mjs/project-worktrees.mjs +74 -34
  30. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  31. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  32. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  33. package/dist/mjs/recovery-journal.mjs +687 -0
  34. package/dist/mjs/recovery-store.mjs +8 -0
  35. package/dist/mjs/session-file-mutations.mjs +37 -0
  36. package/dist/mjs/working-tree-mirror.mjs +1 -0
  37. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  38. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  39. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  40. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  41. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  42. package/dist/mjs/workspace-git-sync.mjs +264 -202
  43. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  44. package/dist/types/command-launcher.d.ts +42 -0
  45. package/dist/types/control-command-policy.d.ts +53 -0
  46. package/dist/types/main.d.ts +31 -19
  47. package/dist/types/project-checkout-garbage.d.ts +19 -2
  48. package/dist/types/project-workspace-state.d.ts +9 -9
  49. package/dist/types/project-worktrees.d.ts +49 -5
  50. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  51. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  52. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  53. package/dist/types/recovery-journal-thread.d.ts +1 -0
  54. package/dist/types/recovery-journal.d.ts +246 -0
  55. package/dist/types/recovery-store.d.ts +6 -0
  56. package/dist/types/session-file-mutations.d.ts +22 -0
  57. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  58. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  59. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  60. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  61. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  62. package/dist/types/workspace-git-sync.d.ts +113 -7
  63. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  64. package/package.json +1 -1
  65. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  66. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  67. 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");
@@ -75,6 +77,7 @@ var import_cli_update = require("./cli-update.cjs");
75
77
  var import_git_process_environment = require("./git-process-environment.cjs");
76
78
  var import_process_tree = require("./process-tree.cjs");
77
79
  var import_project_checkout_garbage = require("./project-checkout-garbage.cjs");
80
+ var import_control_command_policy = require("./control-command-policy.cjs");
78
81
  var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
79
82
  var import_port_forward_client = require("./port-forward-client.cjs");
80
83
  var import_registry_auth = require("./registry-auth.cjs");
@@ -82,6 +85,8 @@ var import_workspace_incident_state = require("./workspace-incident-state.cjs");
82
85
  var import_workspace_automatic_sync_policy = require("./workspace-automatic-sync-policy.cjs");
83
86
  var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
84
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");
85
90
  var import_workspace_projection_ledger = require("./workspace-projection-ledger.cjs");
86
91
  var import_workspace_sync_coalescer = require("./workspace-sync-coalescer.cjs");
87
92
  var import_workspace_path_move = require("./workspace-path-move.cjs");
@@ -94,7 +99,6 @@ var import_supervisor = require("./supervisor.cjs");
94
99
  var import_managed_paths = require("./managed-paths.cjs");
95
100
  var import_project_worktrees = require("./project-worktrees.cjs");
96
101
  var import_project_mirror_fetch_policy = require("./project-mirror-fetch-policy.cjs");
97
- var import_project_snapshot_recovery_runner = require("./project-snapshot-recovery-runner.cjs");
98
102
  var import_project_workspace_state = require("./project-workspace-state.cjs");
99
103
  var import_workspace_git_sync = require("./workspace-git-sync.cjs");
100
104
  const import_meta = {};
@@ -110,7 +114,7 @@ function workerServerHeartbeatAction(input) {
110
114
  return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
111
115
  }
112
116
  const WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX = "hydration:";
113
- function createWorkspaceHydrationLeaseHooks(send, options = {}) {
117
+ function createWorkspaceHydrationLeaseHooks(send, options) {
114
118
  const yieldToEventLoop = options.yieldToEventLoop ?? (() => new Promise((resolve) => setImmediate(resolve)));
115
119
  const log = options.log ?? ((line) => process.stdout.write(`${line}
116
120
  `));
@@ -131,13 +135,17 @@ function createWorkspaceHydrationLeaseHooks(send, options = {}) {
131
135
  log(
132
136
  `[r5d-worker] released hydration lease ${context.transactionId} after ${startedAt === void 0 ? "?" : Math.round(now() - startedAt)}ms`
133
137
  );
138
+ },
139
+ holdMounts(mountIds, description) {
140
+ return options.fence.hold(mountIds, description);
134
141
  }
135
142
  };
136
143
  }
137
144
  const workerLifecycleTestHarness = {
138
145
  boundedWorkerLifecycleProgress,
139
146
  workerServerHeartbeatAction,
140
- createWorkspaceHydrationLeaseHooks
147
+ createWorkspaceHydrationLeaseHooks,
148
+ workerCommandHasWorkspaceEffect
141
149
  };
142
150
  class ProjectWorkspaceConfigurationDeferredError extends Error {
143
151
  }
@@ -158,6 +166,26 @@ const PTY_FOREGROUND_POLL_MS = 1e3;
158
166
  const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
159
167
  const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
160
168
  let workerCommandLauncher;
169
+ const trustedControlPath = process.env.PATH;
170
+ let capacityReportTimer;
171
+ let capacityReportSocket = null;
172
+ let lastCapacityReport;
173
+ function scheduleCapacityReport(force = false) {
174
+ if (force) lastCapacityReport = void 0;
175
+ if (capacityReportTimer) return;
176
+ capacityReportTimer = setTimeout(() => {
177
+ capacityReportTimer = void 0;
178
+ const ws = capacityReportSocket;
179
+ if (!ws || ws.readyState !== WebSocket.OPEN || !workerCommandLauncher) return;
180
+ const capacity = { ...workerCommandLauncher.capacityReport(), reportedAt: (/* @__PURE__ */ new Date()).toISOString() };
181
+ const { reportedAt: _reportedAt, ...comparable } = capacity;
182
+ const serialized = JSON.stringify(comparable);
183
+ if (serialized === lastCapacityReport) return;
184
+ lastCapacityReport = serialized;
185
+ sendWorkerMessage(ws, { type: "capacity_report", capacity });
186
+ }, 100);
187
+ capacityReportTimer.unref?.();
188
+ }
161
189
  const activeProcesses = /* @__PURE__ */ new Map();
162
190
  const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
163
191
  const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
@@ -170,7 +198,7 @@ const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
170
198
  let currentWorkerSocket = null;
171
199
  const workerRuntimeId = crypto.randomUUID();
172
200
  const workerOutputWindow = new import_recovery_output_window.WorkerOutputWindow();
173
- let recoveryStore;
201
+ let recoveryJournal;
174
202
  let workerReconnectAttempt = 0;
175
203
  let workerRecoveryId;
176
204
  let workerExecutionLeaseDeadline = 0;
@@ -182,13 +210,13 @@ let runtimeExitPromise;
182
210
  function fenceWorkerRuntimeOperations() {
183
211
  launcherFailureInProgress = true;
184
212
  workerAdmissionGeneration += 1;
185
- for (const operation of recoveryStore?.operations() ?? []) {
186
- if (operation.state !== "completed") recoveryStore?.unknown(operation.requestId);
213
+ for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
214
+ if (operation.state !== "completed") recoveryJournal?.unknown(operation.requestId);
187
215
  }
188
216
  for (const runId of workerCommandLauncher?.activeIds ?? []) {
189
217
  infrastructureStoppedRunIds.add(runId);
190
218
  cancelledProcessRuns.add(runId);
191
- recoveryStore?.unknownRun(runId);
219
+ recoveryJournal?.unknownRun(runId);
192
220
  }
193
221
  for (const runId of activeProcesses.keys()) infrastructureStoppedRunIds.add(runId);
194
222
  }
@@ -197,13 +225,21 @@ function exitWorkerRuntime(code) {
197
225
  fenceWorkerRuntimeOperations();
198
226
  runtimeExitPromise = (async () => {
199
227
  await Promise.race([
200
- Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close()]),
228
+ Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close(), recoveryJournal?.flush()]),
201
229
  Bun.sleep(4e3)
202
230
  ]);
203
231
  process.exit(code);
204
232
  })();
205
233
  return runtimeExitPromise;
206
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
+ }
207
243
  function handleWorkerCommandLauncherFailure(error) {
208
244
  if (!workerCommandLauncher || launcherFailureInProgress) return;
209
245
  fenceWorkerRuntimeOperations();
@@ -224,7 +260,7 @@ async function retryWorkerServerRead(read) {
224
260
  try {
225
261
  return await read();
226
262
  } catch (error) {
227
- 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;
228
264
  deadline ??= Date.now() + import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS;
229
265
  if (workerLeaseExpired || Date.now() >= deadline) throw new WorkerServerUnavailableError("Worker communication recovery expired");
230
266
  currentWorkerSocket?.close(1012, "Communication recovery");
@@ -246,9 +282,10 @@ function renewWorkerExecutionLease(leaseMs) {
246
282
  `);
247
283
  });
248
284
  closeAllPtys();
249
- for (const operation of recoveryStore?.operations() ?? []) {
285
+ for (const operation of recoveryJournal?.activeAndPendingOperations() ?? []) {
250
286
  if (operation.sessionId && operation.state !== "completed") {
251
- recoveryStore?.cancelSession(operation.sessionId);
287
+ void recoveryJournal?.cancelSession(operation.sessionId);
288
+ abortPendingReservations("Execution lease expired before command start", operation.sessionId);
252
289
  void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
253
290
  process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
254
291
  `);
@@ -258,7 +295,7 @@ function renewWorkerExecutionLease(leaseMs) {
258
295
  for (const [runId, active] of activeProcesses) {
259
296
  if (!active.sessionId) continue;
260
297
  infrastructureStoppedRunIds.add(runId);
261
- recoveryStore?.unknownRun(runId);
298
+ recoveryJournal?.unknownRun(runId);
262
299
  cancelledProcessRuns.add(runId);
263
300
  closeProcessStdin(active);
264
301
  void (0, import_process_tree.terminateProcessTree)(active.process).catch(
@@ -273,6 +310,31 @@ function renewWorkerExecutionLease(leaseMs) {
273
310
  }
274
311
  let workerAdmissionGeneration = 0;
275
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
+ }
276
338
  let workspaceSyncQueue = Promise.resolve();
277
339
  let startupProjectSnapshotRecoveryCompleted = false;
278
340
  const checkoutGarbageCollectors = /* @__PURE__ */ new Map();
@@ -307,6 +369,17 @@ const workspaceSyncSingleFlight = {
307
369
  },
308
370
  afterCurrent() {
309
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 });
310
383
  }
311
384
  };
312
385
  let githubCredential = null;
@@ -963,14 +1036,15 @@ async function runGitAsync(args, options = {}) {
963
1036
  env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
964
1037
  ...bounded ? { detached: true } : {}
965
1038
  });
966
- const completion = Promise.all([
967
- new Response(subprocess.stdout).text(),
968
- new Response(subprocess.stderr).text(),
969
- subprocess.exited
970
- ]).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
+ );
971
1042
  let timer;
972
1043
  const timedOut = bounded ? new Promise((_resolve, reject) => {
973
- 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
+ );
974
1048
  }) : null;
975
1049
  try {
976
1050
  const { stdout, stderr, exitCode } = timedOut ? await Promise.race([completion, timedOut]) : await completion;
@@ -2059,12 +2133,12 @@ async function streamCommandOutput(stream, onData) {
2059
2133
  }
2060
2134
  const text = decoder.decode(chunk.value, { stream: true });
2061
2135
  if (text.length > 0) {
2062
- onData(text);
2136
+ await onData(text);
2063
2137
  }
2064
2138
  }
2065
2139
  const trailing = decoder.decode();
2066
2140
  if (trailing.length > 0) {
2067
- onData(trailing);
2141
+ await onData(trailing);
2068
2142
  }
2069
2143
  } finally {
2070
2144
  reader.releaseLock();
@@ -2203,12 +2277,21 @@ function mutationQueueKey(target, resolved) {
2203
2277
  }
2204
2278
  return `${describeWorkerSessionTarget(target)}:${import_node_path.default.posix.normalize(resolved.repoRelativePath)}`;
2205
2279
  }
2206
- 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) {
2207
2290
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
2208
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
2291
+ if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
2209
2292
  throw new Error(`File not found: ${resolved.displayPath}`);
2210
2293
  }
2211
- const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
2294
+ const buffer = await import_node_fs.default.promises.readFile(resolved.absolutePath);
2212
2295
  const formatted = formatLineNumberedContent(buffer.toString("utf8"), offset, limit);
2213
2296
  return {
2214
2297
  type: "read",
@@ -2218,25 +2301,25 @@ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
2218
2301
  ...formatted
2219
2302
  };
2220
2303
  }
2221
- function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
2304
+ async function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
2222
2305
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
2223
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
2224
- 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");
2225
2308
  return {
2226
2309
  type: "write",
2227
2310
  file: resolved.displayPath,
2228
2311
  gitBlobHash: getGitBlobHashForContent(content)
2229
2312
  };
2230
2313
  }
2231
- function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
2314
+ async function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
2232
2315
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
2233
2316
  if (!Array.isArray(edits) || edits.length === 0) {
2234
2317
  throw new Error("edit requires at least one replacement");
2235
2318
  }
2236
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
2319
+ if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
2237
2320
  throw new Error(`File not found: ${resolved.displayPath}`);
2238
2321
  }
2239
- const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
2322
+ const originalContent = await import_node_fs.default.promises.readFile(resolved.absolutePath, "utf8");
2240
2323
  const ranges = edits.map((edit, index) => {
2241
2324
  if (edit.oldText === edit.newText) {
2242
2325
  throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
@@ -2268,7 +2351,7 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
2268
2351
  for (const range of [...sortedRanges].reverse()) {
2269
2352
  nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
2270
2353
  }
2271
- import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
2354
+ await import_node_fs.default.promises.writeFile(resolved.absolutePath, nextContent, "utf8");
2272
2355
  return {
2273
2356
  type: "edit",
2274
2357
  file: resolved.displayPath,
@@ -2301,7 +2384,7 @@ async function executePlanOperation(input) {
2301
2384
  let created = false;
2302
2385
  if (message.createIfMissing) {
2303
2386
  try {
2304
- 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" });
2305
2388
  created = true;
2306
2389
  } catch (error) {
2307
2390
  if (error.code !== "EEXIST") throw error;
@@ -2310,11 +2393,16 @@ async function executePlanOperation(input) {
2310
2393
  return {
2311
2394
  type: "read_plan",
2312
2395
  created,
2313
- read: readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
2396
+ read: await readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
2314
2397
  };
2315
2398
  }
2316
- const content = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
2317
- 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
+ );
2318
2406
  return {
2319
2407
  type: "update_task_status",
2320
2408
  planId: message.planId,
@@ -2335,7 +2423,11 @@ async function executeReadFileOperation(input) {
2335
2423
  rootDir: input.rootDir,
2336
2424
  access: "read"
2337
2425
  });
2338
- 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
+ );
2339
2431
  }
2340
2432
  async function executeWriteFileOperation(input) {
2341
2433
  const builtInPaths = await prepareBuiltInToolPathsForTarget({
@@ -2388,16 +2480,16 @@ function wildcardToRegex(pattern) {
2388
2480
  function normalizeFindPattern(pattern) {
2389
2481
  return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
2390
2482
  }
2391
- function walkWorkerEntries(branchPath, start) {
2483
+ async function walkWorkerEntries(branchPath, start) {
2392
2484
  const entries = [];
2393
2485
  const visitedDirectories = /* @__PURE__ */ new Set();
2394
- const realVirtualRoot = start.scope === "virtual" ? import_node_fs.default.realpathSync(start.virtualRootPath) : null;
2395
- 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) => {
2396
2488
  let stat;
2397
2489
  try {
2398
- stat = import_node_fs.default.statSync(absolutePath);
2490
+ stat = await import_node_fs.default.promises.stat(absolutePath);
2399
2491
  if (realVirtualRoot) {
2400
- 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`);
2401
2493
  }
2402
2494
  } catch (error) {
2403
2495
  if (isRoot) throw error;
@@ -2410,7 +2502,7 @@ function walkWorkerEntries(branchPath, start) {
2410
2502
  if (!stat.isDirectory()) return;
2411
2503
  let realDirectoryPath;
2412
2504
  try {
2413
- realDirectoryPath = import_node_fs.default.realpathSync(absolutePath);
2505
+ realDirectoryPath = await import_node_fs.default.promises.realpath(absolutePath);
2414
2506
  } catch (error) {
2415
2507
  if (isRoot) throw error;
2416
2508
  return;
@@ -2419,25 +2511,25 @@ function walkWorkerEntries(branchPath, start) {
2419
2511
  visitedDirectories.add(realDirectoryPath);
2420
2512
  let children;
2421
2513
  try {
2422
- 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));
2423
2515
  } catch (error) {
2424
2516
  if (isRoot) throw error;
2425
2517
  return;
2426
2518
  }
2427
2519
  for (const child of children) {
2428
2520
  if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
2429
- visit(import_node_path.default.join(absolutePath, child.name), false);
2521
+ await visit(import_node_path.default.join(absolutePath, child.name), false);
2430
2522
  }
2431
2523
  };
2432
- visit(start.absolutePath, true);
2524
+ await visit(start.absolutePath, true);
2433
2525
  return entries;
2434
2526
  }
2435
2527
  function isProbablyText(bytes) {
2436
2528
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
2437
2529
  }
2438
- function grepWorkerFiles(branchPath, input, builtInPaths) {
2530
+ async function grepWorkerFiles(branchPath, input, builtInPaths) {
2439
2531
  const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
2440
- if (!import_node_fs.default.existsSync(start.absolutePath)) {
2532
+ if (!await statIfExists(start.absolutePath)) {
2441
2533
  throw new Error(`Path not found: ${start.displayPath}`);
2442
2534
  }
2443
2535
  const limit = normalizeToolLimit(input.limit, 100, 1e3);
@@ -2447,12 +2539,12 @@ function grepWorkerFiles(branchPath, input, builtInPaths) {
2447
2539
  const lines = [];
2448
2540
  let matchCount = 0;
2449
2541
  let truncated = false;
2450
- for (const entry of walkWorkerEntries(branchPath, start)) {
2542
+ for (const entry of await walkWorkerEntries(branchPath, start)) {
2451
2543
  if (!entry.stat.isFile()) continue;
2452
2544
  if (globRegex && !globRegex.test(entry.matchPath)) continue;
2453
2545
  let bytes;
2454
2546
  try {
2455
- bytes = import_node_fs.default.readFileSync(entry.absolutePath);
2547
+ bytes = await import_node_fs.default.promises.readFile(entry.absolutePath);
2456
2548
  } catch {
2457
2549
  continue;
2458
2550
  }
@@ -2505,9 +2597,9 @@ async function executeGrepOperation(input) {
2505
2597
  builtInPaths
2506
2598
  );
2507
2599
  }
2508
- function findWorkerFiles(branchPath, input, builtInPaths) {
2600
+ async function findWorkerFiles(branchPath, input, builtInPaths) {
2509
2601
  const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
2510
- if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
2602
+ if (!(await statIfExists(start.absolutePath))?.isDirectory()) {
2511
2603
  throw new Error(`Directory not found: ${start.displayPath}`);
2512
2604
  }
2513
2605
  const pattern = normalizeFindPattern(input.pattern ?? "*");
@@ -2517,7 +2609,7 @@ function findWorkerFiles(branchPath, input, builtInPaths) {
2517
2609
  const matches = [];
2518
2610
  let count = 0;
2519
2611
  let truncated = false;
2520
- for (const entry of walkWorkerEntries(branchPath, start)) {
2612
+ for (const entry of await walkWorkerEntries(branchPath, start)) {
2521
2613
  if (entry.absolutePath === start.absolutePath) continue;
2522
2614
  const isDirectory = entry.stat.isDirectory();
2523
2615
  if (entryType === "file" && !entry.stat.isFile()) continue;
@@ -2562,13 +2654,13 @@ async function executeFindOperation(input) {
2562
2654
  builtInPaths
2563
2655
  );
2564
2656
  }
2565
- function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
2657
+ async function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
2566
2658
  const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
2567
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
2659
+ if (!(await statIfExists(resolved.absolutePath))?.isDirectory()) {
2568
2660
  throw new Error(`Directory not found: ${resolved.displayPath}`);
2569
2661
  }
2570
2662
  const limit = normalizeToolLimit(inputLimit, 200, 1e3);
2571
- 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));
2572
2664
  const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
2573
2665
  const truncated = entries.length > displayed.length;
2574
2666
  const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
@@ -2619,36 +2711,40 @@ function resolveWorkerCodePath(branchPath, inputPath) {
2619
2711
  }
2620
2712
  return { absolutePath, displayPath };
2621
2713
  }
2622
- function listWorkerCodeDirectory(branchPath, inputPath) {
2714
+ async function listWorkerCodeDirectory(branchPath, inputPath) {
2623
2715
  const resolved = resolveWorkerCodePath(branchPath, inputPath);
2624
- if (!import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
2716
+ if (!(await import_node_fs.default.promises.stat(resolved.absolutePath)).isDirectory()) {
2625
2717
  throw new Error(`Code directory not found: ${resolved.displayPath}`);
2626
2718
  }
2627
- 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) {
2628
2724
  const entryPath = import_node_path.default.posix.join(resolved.displayPath, entry.name);
2629
2725
  try {
2630
2726
  const resolvedEntry = resolveWorkerCodePath(branchPath, entryPath);
2631
- const stats = import_node_fs.default.statSync(resolvedEntry.absolutePath);
2632
- if (!stats.isFile() && !stats.isDirectory()) return [];
2633
- return [
2634
- {
2635
- name: entry.name,
2636
- type: stats.isDirectory() ? "directory" : "file",
2637
- size: stats.isDirectory() ? null : stats.size,
2638
- mtime: stats.mtime.toISOString()
2639
- }
2640
- ];
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
+ });
2641
2735
  } catch {
2642
- return [];
2643
2736
  }
2644
- }).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
+ );
2645
2741
  return { type: "code_list", path: resolved.displayPath, entries };
2646
2742
  }
2647
- function readWorkerCodeFile(branchPath, inputPath) {
2743
+ async function readWorkerCodeFile(branchPath, inputPath) {
2648
2744
  const resolved = resolveWorkerCodePath(branchPath, inputPath);
2649
- const stats = import_node_fs.default.statSync(resolved.absolutePath);
2745
+ const stats = await import_node_fs.default.promises.stat(resolved.absolutePath);
2650
2746
  if (!stats.isFile()) throw new Error(`Code file not found: ${resolved.displayPath}`);
2651
- const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
2747
+ const bytes = await import_node_fs.default.promises.readFile(resolved.absolutePath);
2652
2748
  return {
2653
2749
  type: "code_read",
2654
2750
  path: resolved.displayPath,
@@ -2657,17 +2753,17 @@ function readWorkerCodeFile(branchPath, inputPath) {
2657
2753
  base64: bytes.toString("base64")
2658
2754
  };
2659
2755
  }
2660
- function executeCodeListOperation(input) {
2756
+ async function executeCodeListOperation(input) {
2661
2757
  if (input.resolvedTarget.target.type !== "project") throw new Error("Code listing requires a project branch target");
2662
2758
  return listWorkerCodeDirectory(input.resolvedTarget.rootPath, input.message.path);
2663
2759
  }
2664
- function executeCodeReadOperation(input) {
2760
+ async function executeCodeReadOperation(input) {
2665
2761
  if (input.resolvedTarget.target.type !== "project") throw new Error("Code reading requires a project branch target");
2666
2762
  return readWorkerCodeFile(input.resolvedTarget.rootPath, input.message.path);
2667
2763
  }
2668
- function readWorkerImageFile(branchPath, filePath, builtInPaths) {
2764
+ async function readWorkerImageFile(branchPath, filePath, builtInPaths) {
2669
2765
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
2670
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
2766
+ if (!(await statIfExists(resolved.absolutePath))?.isFile()) {
2671
2767
  throw new Error(`File not found: ${resolved.displayPath}`);
2672
2768
  }
2673
2769
  const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
@@ -2675,7 +2771,7 @@ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
2675
2771
  if (!mediaType) {
2676
2772
  throw new Error("read supports PNG and JPEG image inputs only");
2677
2773
  }
2678
- const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
2774
+ const bytes = await import_node_fs.default.promises.readFile(resolved.absolutePath);
2679
2775
  const dimensions = readImageDimensions(bytes, mediaType);
2680
2776
  return {
2681
2777
  type: "view_file_bytes",
@@ -2775,6 +2871,10 @@ function cancelWorkerCommandLaunch(resources) {
2775
2871
  });
2776
2872
  }
2777
2873
  async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
2874
+ if (message.type === "exec_start" && message.commandClass === "control") {
2875
+ (0, import_control_command_policy.assertControlCommandArgv)(message.argv);
2876
+ if (message.interactive) throw new Error("Control commands cannot be interactive");
2877
+ }
2778
2878
  const id = message.type === "pty_open" ? `pty:${message.ptyId}` : message.runId;
2779
2879
  const operationId = message.type === "pty_open" ? id : `exec:${id}`;
2780
2880
  sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: true });
@@ -2783,6 +2883,7 @@ async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
2783
2883
  id,
2784
2884
  kind: message.type,
2785
2885
  ..."workspaceEffect" in message && message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2886
+ ...message.type === "exec_start" && message.commandClass === "control" ? { commandClass: "control" } : {},
2786
2887
  ..."sessionId" in message && message.sessionId ? { sessionId: message.sessionId } : {},
2787
2888
  assertAdmission: () => {
2788
2889
  assertAdmission();
@@ -2930,8 +3031,17 @@ async function executeStreamingCommand(input) {
2930
3031
  });
2931
3032
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
2932
3033
  const interactive = input.message.interactive === true;
3034
+ const control = input.message.commandClass === "control";
3035
+ let argv = input.message.argv;
3036
+ let environment = workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2]);
3037
+ if (control) {
3038
+ (0, import_control_command_policy.assertControlCommandArgv)(argv);
3039
+ if (interactive) throw new Error("Control commands cannot be interactive");
3040
+ argv = [(0, import_control_command_policy.resolveControlCommandExecutable)(trustedControlPath, (program, options) => Bun.which(program, options)), ...argv.slice(1)];
3041
+ environment = (0, import_control_command_policy.controlCommandEnvironment)(environment, trustedControlPath);
3042
+ }
2933
3043
  input.assertAdmission();
2934
- const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
3044
+ const subprocess = Bun.spawn(input.resources.wrap(argv), {
2935
3045
  cwd,
2936
3046
  // Without an explicit stdin the process reads /dev/null and interactive
2937
3047
  // prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
@@ -2939,7 +3049,7 @@ async function executeStreamingCommand(input) {
2939
3049
  stdout: "pipe",
2940
3050
  stderr: "pipe",
2941
3051
  detached: true,
2942
- env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2])
3052
+ env: environment
2943
3053
  });
2944
3054
  spawnedProcess = subprocess;
2945
3055
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
@@ -2954,12 +3064,13 @@ async function executeStreamingCommand(input) {
2954
3064
  pid: subprocess.pid,
2955
3065
  processGroupId: process.platform === "win32" ? void 0 : subprocess.pid,
2956
3066
  credentialId: input.message.credentialId,
2957
- argv: input.message.argv,
3067
+ argv,
2958
3068
  command: input.message.command,
2959
3069
  cwd,
2960
3070
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2961
3071
  ...interactive ? { interactive: true, stdin: subprocess.stdin } : {},
2962
- ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
3072
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
3073
+ ...control ? { commandClass: "control" } : {}
2963
3074
  });
2964
3075
  settlePreparation();
2965
3076
  started = true;
@@ -2971,7 +3082,8 @@ async function executeStreamingCommand(input) {
2971
3082
  pid: subprocess.pid,
2972
3083
  ...process.platform === "win32" ? {} : { processGroupId: subprocess.pid }
2973
3084
  });
2974
- if (input.message.timeoutMs) {
3085
+ const timeoutMs = control ? Math.min(input.message.timeoutMs ?? import_control_command_policy.CONTROL_COMMAND_RUNTIME_MS, import_control_command_policy.CONTROL_COMMAND_RUNTIME_MS) : input.message.timeoutMs;
3086
+ if (timeoutMs) {
2975
3087
  timeout = setTimeout(() => {
2976
3088
  timedOut = true;
2977
3089
  cancelWorkerCommandLaunch(input.resources);
@@ -2982,30 +3094,32 @@ async function executeStreamingCommand(input) {
2982
3094
  `
2983
3095
  );
2984
3096
  });
2985
- }, input.message.timeoutMs);
3097
+ }, timeoutMs);
2986
3098
  }
2987
3099
  const [exitCode] = await Promise.all([
2988
3100
  subprocess.exited,
2989
- streamCommandOutput(subprocess.stdout, (data) => {
2990
- sendWorkerMessage(input.ws, {
3101
+ streamCommandOutput(
3102
+ subprocess.stdout,
3103
+ (data) => sendWorkerOutput(input.ws, {
2991
3104
  type: "exec_output",
2992
3105
  runId: input.message.runId,
2993
3106
  stream: "stdout",
2994
3107
  data
2995
- });
2996
- }),
2997
- streamCommandOutput(subprocess.stderr, (data) => {
2998
- sendWorkerMessage(input.ws, {
3108
+ })
3109
+ ),
3110
+ streamCommandOutput(
3111
+ subprocess.stderr,
3112
+ (data) => sendWorkerOutput(input.ws, {
2999
3113
  type: "exec_output",
3000
3114
  runId: input.message.runId,
3001
3115
  stream: "stderr",
3002
3116
  data
3003
- });
3004
- })
3117
+ })
3118
+ )
3005
3119
  ]);
3006
3120
  const diagnostic = await input.resources.diagnose();
3007
3121
  if (diagnostic) {
3008
- sendWorkerMessage(input.ws, {
3122
+ await sendWorkerOutput(input.ws, {
3009
3123
  type: "exec_output",
3010
3124
  runId: input.message.runId,
3011
3125
  stream: "stderr",
@@ -3077,6 +3191,33 @@ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
3077
3191
  );
3078
3192
  }
3079
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
+ }
3080
3221
  function closeProcessStdin(active) {
3081
3222
  if (!active?.stdin) {
3082
3223
  return;
@@ -3102,21 +3243,35 @@ function sendReplayWorkerMessage(ws, message) {
3102
3243
  }
3103
3244
  function sendWorkerMessage(ws, message) {
3104
3245
  if (launcherFailureInProgress) return;
3105
- if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
3106
- return;
3107
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;
3108
- if (recoveryStore && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
3109
- recoveryStore.unknown(message.requestId);
3247
+ if (recoveryJournal && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
3248
+ recoveryJournal.unknown(message.requestId);
3110
3249
  currentWorkerSocket?.close(1012, "Communication recovery");
3111
3250
  return;
3112
3251
  }
3113
- const recorded = recoveryStore?.record(message) ?? message;
3114
- if ((message.type === "exec_exit" || message.type === "exec_error") && infrastructureStoppedRunIds.has(message.runId)) {
3115
- recoveryStore?.unknownRun(message.runId);
3252
+ if (!recoveryJournal) {
3253
+ sendReplayWorkerMessage(ws, message);
3116
3254
  return;
3117
3255
  }
3118
- if (message.type === "exec_output" && workerRecoveryId !== void 0) return;
3119
- 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);
3120
3275
  }
3121
3276
  function sendSerializedWorkerMessage(ws, serialized) {
3122
3277
  sendWorkerMessage(ws, JSON.parse(serialized));
@@ -3145,7 +3300,8 @@ function buildActiveProcessReports() {
3145
3300
  ...active.cwd ? { cwd: active.cwd } : {},
3146
3301
  startedAt: active.startedAt,
3147
3302
  ...active.interactive ? { interactive: true } : {},
3148
- ...active.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
3303
+ ...active.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
3304
+ ...active.commandClass ? { commandClass: active.commandClass } : {}
3149
3305
  }));
3150
3306
  }
3151
3307
  function sendActiveProcessReport(ws) {
@@ -3667,6 +3823,7 @@ async function startWorker(options, projectRuntime = {
3667
3823
  validateLabel(label);
3668
3824
  if (!workerCommandLauncher) {
3669
3825
  const launcher = await (0, import_command_launcher.createWorkerCommandLauncher)({
3826
+ onCapacityChange: () => scheduleCapacityReport(),
3670
3827
  configuration: process.env.R5D_WORKER_COMMAND_LAUNCHER,
3671
3828
  env: (0, import_command_launcher.commandLauncherEnvironment)(),
3672
3829
  onFailure: handleWorkerCommandLauncherFailure
@@ -3677,9 +3834,19 @@ async function startWorker(options, projectRuntime = {
3677
3834
  `);
3678
3835
  }
3679
3836
  const rootDir = import_node_path.default.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
3680
- recoveryStore ??= new import_recovery_store.WorkerRecoveryStore(
3681
- import_node_path.default.join(rootDir, ".worker-recovery", (0, import_node_crypto.createHash)("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
3682
- );
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
+ }
3683
3850
  const projectsRoot = import_node_path.default.join(rootDir, "projects");
3684
3851
  const syncRoot = import_node_path.default.join(rootDir, "sync");
3685
3852
  const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
@@ -3782,7 +3949,6 @@ async function startWorker(options, projectRuntime = {
3782
3949
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
3783
3950
  const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
3784
3951
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
3785
- const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
3786
3952
  const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
3787
3953
  const workspaceProjectRelativePath = (projectId, branchName) => import_node_path.default.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
3788
3954
  const workspacePlanRelativePath = (projectId, branchName) => import_node_path.default.posix.join("plans", projectId, encodeURIComponent(branchName));
@@ -3865,7 +4031,8 @@ async function startWorker(options, projectRuntime = {
3865
4031
  sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
3866
4032
  planSourcePath: import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"))
3867
4033
  }));
3868
- (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);
3869
4036
  for (const branch of branches) {
3870
4037
  import_node_fs.default.rmSync(branch.planSourcePath, { recursive: true, force: true });
3871
4038
  const key = projectBranchKey(project.projectId, branch.branchName);
@@ -3953,14 +4120,14 @@ async function startWorker(options, projectRuntime = {
3953
4120
  )
3954
4121
  };
3955
4122
  };
3956
- const stageDurableWorkspaceDeletions = () => {
4123
+ const stageDurableWorkspaceDeletions = async () => {
3957
4124
  for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
3958
4125
  const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
3959
4126
  if (deletion.kind === "project") {
3960
4127
  if (!deletion.projectDeleted) {
3961
4128
  const nextProject = projectConfigById.get(deletion.projectId);
3962
4129
  if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
3963
- (0, import_workspace_path_move.preserveProjectCheckoutPathMove)({
4130
+ await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_checkout_path_move", {
3964
4131
  oldProjectRoot: deletion.managedPath,
3965
4132
  newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
3966
4133
  projectsDurabilityRoot: projectsRoot,
@@ -4327,7 +4494,7 @@ async function startWorker(options, projectRuntime = {
4327
4494
  }
4328
4495
  }
4329
4496
  if (transitionReseedRequired) {
4330
- projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
4497
+ projectWorkspaceState = await projectWorkspaceStateStore.recordEnabledRepositoryTransition({
4331
4498
  projectId: project.projectId,
4332
4499
  transitionId: project.repositoryTransitionId
4333
4500
  });
@@ -4496,7 +4663,7 @@ async function startWorker(options, projectRuntime = {
4496
4663
  )
4497
4664
  ];
4498
4665
  if (publishedTombstoneIds.length > 0) {
4499
- projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
4666
+ projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
4500
4667
  tombstoneIds: publishedTombstoneIds,
4501
4668
  publishedHead
4502
4669
  });
@@ -4515,7 +4682,7 @@ async function startWorker(options, projectRuntime = {
4515
4682
  forgetLocalProjectMirrorRefsTokens(deletion.projectId, deletion.branchName);
4516
4683
  }
4517
4684
  if (deletion.tombstoneId && !deletion.projectDeleted) {
4518
- projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
4685
+ projectWorkspaceState = await projectWorkspaceStateStore.recordMirrorRefDeletion({
4519
4686
  tombstoneId: deletion.tombstoneId,
4520
4687
  branchName: deletion.branchName
4521
4688
  });
@@ -4712,7 +4879,10 @@ async function startWorker(options, projectRuntime = {
4712
4879
  };
4713
4880
  };
4714
4881
  const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
4715
- (message) => sendWorkerMessageFromCurrentSource(ws, message)
4882
+ (message) => sendWorkerMessageFromCurrentSource(ws, message),
4883
+ {
4884
+ fence: workspaceMountHoldFence
4885
+ }
4716
4886
  );
4717
4887
  const performWorkspaceSync = async (input) => {
4718
4888
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
@@ -5370,7 +5540,7 @@ async function startWorker(options, projectRuntime = {
5370
5540
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
5371
5541
  );
5372
5542
  if (!incidentDeferral.incidentId) {
5373
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
5543
+ projectWorkspaceState = await projectWorkspaceStateStore.reconcile({
5374
5544
  desiredProjects: message.projects,
5375
5545
  preserveOnlyBranches
5376
5546
  });
@@ -5381,7 +5551,7 @@ async function startWorker(options, projectRuntime = {
5381
5551
  (candidate) => candidate.projectId === project.projectId && candidate.branchName === branch.branchName
5382
5552
  );
5383
5553
  if (pending2 && pending2.branchId === branch.branchId) {
5384
- projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
5554
+ projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
5385
5555
  }
5386
5556
  }
5387
5557
  }
@@ -5549,7 +5719,7 @@ async function startWorker(options, projectRuntime = {
5549
5719
  for (const projectId of [...readyProjectIds]) {
5550
5720
  if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
5551
5721
  }
5552
- stageDurableWorkspaceDeletions();
5722
+ await stageDurableWorkspaceDeletions();
5553
5723
  const mountsBeforeGit = buildWorkspaceMounts();
5554
5724
  await (0, import_workspace_git_sync.hydrateWorkspaceGitMounts)(
5555
5725
  workspaceShadowRoot,
@@ -5595,7 +5765,12 @@ async function startWorker(options, projectRuntime = {
5595
5765
  const connectSyncMs = Date.now() - connectSyncStartedAtMs;
5596
5766
  const result = syncResult.telemetry ? syncResult : {
5597
5767
  ...syncResult,
5598
- telemetry: { totalMs: connectSyncMs, queueMs: 0, prepareMs: lastWorkspaceSyncMirrorObservationMs, synchronizeMs: connectSyncMs }
5768
+ telemetry: {
5769
+ totalMs: connectSyncMs,
5770
+ queueMs: 0,
5771
+ prepareMs: lastWorkspaceSyncMirrorObservationMs,
5772
+ synchronizeMs: connectSyncMs
5773
+ }
5599
5774
  };
5600
5775
  assertConfigurationSyncStillAdmitted();
5601
5776
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
@@ -5614,7 +5789,7 @@ async function startWorker(options, projectRuntime = {
5614
5789
  ) ? [deletion.tombstoneId] : []
5615
5790
  );
5616
5791
  if (layoutCleanupIds.length > 0) {
5617
- projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
5792
+ projectWorkspaceState = await projectWorkspaceStateStore.recordTreePublication({
5618
5793
  tombstoneIds: layoutCleanupIds,
5619
5794
  publishedHead
5620
5795
  });
@@ -5721,26 +5896,33 @@ async function startWorker(options, projectRuntime = {
5721
5896
  if (startupProjectSnapshotRecoveryCompleted) return;
5722
5897
  sendLifecycleProgress({ phase: "recovering", operationId, detail: "Checking interrupted workspace transactions" });
5723
5898
  const snapshotRecovery = await workspaceSyncSingleFlight.runExclusive(
5724
- async () => await (0, import_project_snapshot_recovery_runner.recoverProjectSnapshotsInChild)({
5725
- projectsRoot,
5726
- snapshotRoot: projectWorktreeSnapshotsRoot,
5727
- onProgress: (progress) => {
5728
- const lifecycleProgress = {
5729
- phase: "recovering",
5730
- operationId,
5731
- detail: describeSnapshotProgress(progress),
5732
- completedBytes: progress.completedBytes,
5733
- completedItems: progress.completedBranches,
5734
- totalItems: progress.totalBranches,
5735
- ...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
5736
- };
5737
- sendLifecycleProgress(lifecycleProgress);
5738
- process.stdout.write(
5739
- `[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
5740
5921
  `
5741
- );
5922
+ );
5923
+ }
5742
5924
  }
5743
- })
5925
+ )
5744
5926
  );
5745
5927
  if (snapshotRecovery.restored.length > 0) {
5746
5928
  process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
@@ -5835,7 +6017,7 @@ async function startWorker(options, projectRuntime = {
5835
6017
  type: "hello",
5836
6018
  resumableProtocol: import_recovery_protocol.WORKER_RESUMABLE_PROTOCOL,
5837
6019
  runtimeId: workerRuntimeId,
5838
- ledgerId: recoveryStore.ledgerId,
6020
+ ledgerId: recoveryJournal.ledgerId,
5839
6021
  startupReady: startupRecoveryComplete,
5840
6022
  hostInfo: {
5841
6023
  hostname: (0, import_node_os.hostname)(),
@@ -5854,6 +6036,7 @@ async function startWorker(options, projectRuntime = {
5854
6036
  workspaceConfigResetToCanonicalV1: true,
5855
6037
  projectBranchDeletionFastAckV1: true,
5856
6038
  projectBranchDeletionStagedRemovalV1: true,
6039
+ commandControlLaneV1: workerCommandLauncher?.supportsControlLane === true,
5857
6040
  projectMirrorLeaseV1: true,
5858
6041
  projectMirrorRefsTokensV1: true,
5859
6042
  projectBranchWorkingTreeModeV1: true
@@ -5862,16 +6045,39 @@ async function startWorker(options, projectRuntime = {
5862
6045
  }
5863
6046
  };
5864
6047
  sendSerializedWorkerMessage(ws, JSON.stringify(hello));
6048
+ let outputPumpInFlight = false;
5865
6049
  outputReplayTimer = setInterval(() => {
5866
- if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
5867
- for (const output of recoveryStore.output()) {
5868
- if (!sendReplayWorkerMessage(ws, output)) break;
5869
- }
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
+ })();
5870
6066
  }, 250);
5871
6067
  outputReplayTimer.unref();
6068
+ let terminalPumpInFlight = false;
5872
6069
  terminalReplayTimer = setInterval(() => {
5873
- if (workerRecoveryId !== void 0) return;
5874
- 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
+ });
5875
6081
  }, 5e3);
5876
6082
  terminalReplayTimer.unref();
5877
6083
  process.stdout.write("[r5d-worker] connected\n");
@@ -5884,28 +6090,37 @@ async function startWorker(options, projectRuntime = {
5884
6090
  if (message.type === "recovery_request") {
5885
6091
  workerRecoveryId = message.recoveryId;
5886
6092
  sendActiveProcessReport(ws);
5887
- sendWorkerMessage(ws, {
5888
- type: "recovery_state",
5889
- recoveryId: message.recoveryId,
5890
- runtimeId: workerRuntimeId,
5891
- ledgerId: recoveryStore.ledgerId,
5892
- operations: recoveryStore.operations()
5893
- });
5894
- const cursors = new Map(recoveryStore.outputCursors().map((cursor) => [cursor.runId, cursor]));
5895
- for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
5896
- workerOutputWindow.reset([...cursors.values()]);
5897
- for (const response of recoveryStore.responses(message.requests)) sendReplayWorkerMessage(ws, response);
5898
- for (const output of recoveryStore.output(message.outputOffsets)) {
5899
- while (true) {
5900
- if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
5901
- return;
5902
- if (sendReplayWorkerMessage(ws, output)) break;
5903
- 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
+ }
5904
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();
5905
6123
  }
5906
- if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
5907
- for (const terminal of recoveryStore.terminals()) sendReplayWorkerMessage(ws, terminal);
5908
- sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
5909
6124
  return;
5910
6125
  }
5911
6126
  if (message.type === "recovery_ready") {
@@ -5920,17 +6135,23 @@ async function startWorker(options, projectRuntime = {
5920
6135
  return;
5921
6136
  }
5922
6137
  if (message.type === "operation_result_ack") {
5923
- recoveryStore.acknowledgeResult(message.requestId);
6138
+ recoveryJournal.acknowledgeResult(message.requestId);
5924
6139
  return;
5925
6140
  }
5926
6141
  if (message.type === "exec_output_ack") {
5927
- recoveryStore.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
6142
+ recoveryJournal.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
5928
6143
  workerOutputWindow.acknowledge(message.runId, message.stream, message.nextOffset);
5929
6144
  return;
5930
6145
  }
5931
6146
  if (message.type === "cancel_session") {
5932
- recoveryStore.cancelSession(message.sessionId);
6147
+ const durableCancellation = recoveryJournal.cancelSession(message.sessionId);
6148
+ abortPendingReservations("Session cancelled before command start", message.sessionId);
5933
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),
5934
6155
  workerCommandLauncher?.cancelSession(message.sessionId),
5935
6156
  ...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
5936
6157
  cancelledProcessRuns.add(runId);
@@ -5945,27 +6166,74 @@ async function startWorker(options, projectRuntime = {
5945
6166
  if (!startupRecoveryComplete && message.type !== "ping") return;
5946
6167
  if (currentWorkerSocket !== ws) return;
5947
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
+ }
5948
6202
  if (operationRequestId) {
5949
- const admission = recoveryStore.admit({ ...message, requestId: operationRequestId });
5950
- 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") {
5951
6213
  ws.close(1008, "Operation identity conflict");
5952
6214
  return;
5953
6215
  }
5954
6216
  sendWorkerMessage(ws, { type: "operation_received", requestId: operationRequestId });
5955
- if (admission !== "new") {
5956
- const response = recoveryStore.response(operationRequestId);
5957
- if (response && admission !== "unknown") sendReplayWorkerMessage(ws, response);
5958
- 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
+ ))) {
5959
6223
  return;
5960
6224
  }
5961
6225
  }
6226
+ if (recoveryJournal.isRequestCancelled(operationRequestId)) {
6227
+ recoveryJournal.unknown(operationRequestId);
6228
+ return;
6229
+ }
5962
6230
  }
5963
6231
  const messageAdmissionGeneration = workerAdmissionGeneration;
5964
6232
  const admissionCredentials = configuredCredentialGenerationFingerprint;
5965
6233
  const assertMessageAdmission = () => {
5966
6234
  if (operationRequestId) {
5967
- if (workerLeaseExpired || recoveryStore.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
5968
- recoveryStore.unknown(operationRequestId);
6235
+ if (workerLeaseExpired || recoveryJournal.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
6236
+ recoveryJournal.unknown(operationRequestId);
5969
6237
  throw new StaleWorkerAdmissionError();
5970
6238
  }
5971
6239
  return;
@@ -5980,6 +6248,8 @@ async function startWorker(options, projectRuntime = {
5980
6248
  });
5981
6249
  };
5982
6250
  if (message.type === "connected") {
6251
+ capacityReportSocket = ws;
6252
+ scheduleCapacityReport(true);
5983
6253
  return;
5984
6254
  }
5985
6255
  if (message.type === "project_mirror_refs_tokens") {
@@ -6100,7 +6370,7 @@ async function startWorker(options, projectRuntime = {
6100
6370
  }
6101
6371
  recoveryDeadline ??= Date.now() + import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS;
6102
6372
  if (workerLeaseExpired || Date.now() >= recoveryDeadline) {
6103
- recoveryStore.unknown(message.requestId);
6373
+ recoveryJournal.unknown(message.requestId);
6104
6374
  return;
6105
6375
  }
6106
6376
  currentWorkerSocket?.close(1012, "Communication recovery");
@@ -6130,7 +6400,7 @@ async function startWorker(options, projectRuntime = {
6130
6400
  projectId: message.projectId,
6131
6401
  branchName: message.targetBranch
6132
6402
  };
6133
- const created = await workspaceSyncSingleFlight.runBranchMutation(() => {
6403
+ const created = await workspaceSyncSingleFlight.runBranchMutation(async () => {
6134
6404
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
6135
6405
  throw new Error(
6136
6406
  `Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
@@ -6145,7 +6415,7 @@ async function startWorker(options, projectRuntime = {
6145
6415
  const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
6146
6416
  ({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
6147
6417
  );
6148
- projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
6418
+ projectWorkspaceState = await projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
6149
6419
  let gitBranchCreated = false;
6150
6420
  try {
6151
6421
  if (!readyProjectIds.has(project.projectId)) {
@@ -6166,7 +6436,7 @@ async function startWorker(options, projectRuntime = {
6166
6436
  }
6167
6437
  return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
6168
6438
  }
6169
- const createdBranch = (0, import_project_worktrees.createOrRetryLinkedProjectBranch)({
6439
+ const createdBranch = await (0, import_project_worktrees.createOrRetryLinkedProjectBranch)({
6170
6440
  projectRoot: configuredProjectRoot(projectsRoot, project),
6171
6441
  primaryBranchName: primaryProjectBranch(project),
6172
6442
  sourceBranchName: message.sourceBranch,
@@ -6191,7 +6461,7 @@ async function startWorker(options, projectRuntime = {
6191
6461
  return createdBranch;
6192
6462
  } catch (error) {
6193
6463
  if (!pendingWasRecorded && !gitBranchCreated && !(0, import_project_worktrees.projectBranchMayExistAfterCreateFailure)(error)) {
6194
- projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
6464
+ projectWorkspaceState = await projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
6195
6465
  }
6196
6466
  throw error;
6197
6467
  }
@@ -6235,7 +6505,7 @@ async function startWorker(options, projectRuntime = {
6235
6505
  });
6236
6506
  try {
6237
6507
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
6238
- const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(() => {
6508
+ const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(async () => {
6239
6509
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
6240
6510
  throw new Error(
6241
6511
  `Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
@@ -6262,12 +6532,12 @@ async function startWorker(options, projectRuntime = {
6262
6532
  if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
6263
6533
  return false;
6264
6534
  }
6265
- projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
6535
+ projectWorkspaceState = await projectWorkspaceStateStore.beginBranchDeletion({
6266
6536
  projectId: message.projectId,
6267
6537
  branchName: message.branchName
6268
6538
  });
6269
6539
  if (pendingLocalBranch) {
6270
- projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
6540
+ projectWorkspaceState = await projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
6271
6541
  }
6272
6542
  pendingCreatedBranchPublicationNotBefore.delete((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(message.projectId, message.branchName));
6273
6543
  if (pendingMirrorDeletes.has(deletionKey)) return true;
@@ -6371,7 +6641,7 @@ async function startWorker(options, projectRuntime = {
6371
6641
  }
6372
6642
  if (message.type === "exec_terminal_ack") {
6373
6643
  pendingProcessTerminals.delete(message.runId);
6374
- recoveryStore.acknowledgeTerminal(message.runId);
6644
+ recoveryJournal.acknowledgeTerminal(message.runId);
6375
6645
  return;
6376
6646
  }
6377
6647
  if (message.type === "port_forward_connect") {
@@ -6426,42 +6696,6 @@ async function startWorker(options, projectRuntime = {
6426
6696
  sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
6427
6697
  return;
6428
6698
  }
6429
- if (message.type === "cancel") {
6430
- let resourceCancellationError;
6431
- try {
6432
- await workerCommandLauncher?.cancel(message.runId);
6433
- } catch (error) {
6434
- resourceCancellationError = error instanceof Error ? error.message : String(error);
6435
- }
6436
- const active = activeProcesses.get(message.runId);
6437
- let cancelled = resourceCancellationError === void 0;
6438
- let cancelMessage;
6439
- if (active) {
6440
- try {
6441
- closeProcessStdin(active);
6442
- await (0, import_process_tree.terminateProcessTree)(active.process);
6443
- cancelMessage = `Stopped ${active.command}`;
6444
- } catch (error) {
6445
- cancelled = false;
6446
- cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
6447
- }
6448
- } else {
6449
- cancelledProcessRuns.add(message.runId);
6450
- cancelMessage = "Cancellation queued before command start";
6451
- }
6452
- if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
6453
- sendSerializedWorkerMessage(
6454
- ws,
6455
- JSON.stringify({
6456
- type: "cancel_result",
6457
- requestId: message.requestId,
6458
- runId: message.runId,
6459
- cancelled,
6460
- message: cancelMessage
6461
- })
6462
- );
6463
- return;
6464
- }
6465
6699
  if (message.type === "exec_stdin") {
6466
6700
  const sendAck = (payload) => sendSerializedWorkerMessage(
6467
6701
  ws,
@@ -6542,10 +6776,20 @@ async function startWorker(options, projectRuntime = {
6542
6776
  let mutationLeaseTransferred = false;
6543
6777
  try {
6544
6778
  resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6545
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6546
- workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6547
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6548
- });
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
+ }
6549
6793
  releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
6550
6794
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
6551
6795
  throw new Error(
@@ -6610,6 +6854,7 @@ async function startWorker(options, projectRuntime = {
6610
6854
  }
6611
6855
  if (message.type === "pty_close") {
6612
6856
  const activePty = activePtys.get(message.ptyId);
6857
+ abortPendingReservation(`pty:${message.ptyId}`, "Shell closed before it started");
6613
6858
  await closePty(message);
6614
6859
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
6615
6860
  markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
@@ -6646,11 +6891,21 @@ async function startWorker(options, projectRuntime = {
6646
6891
  try {
6647
6892
  resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6648
6893
  if (hasWorkspaceEffect) {
6649
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6650
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
6651
- targetReserved = true;
6652
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6653
- });
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
+ }
6654
6909
  }
6655
6910
  const runCommand = async () => {
6656
6911
  if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
@@ -6702,7 +6957,7 @@ async function startWorker(options, projectRuntime = {
6702
6957
  cancelledProcessRuns.delete(message.runId);
6703
6958
  }
6704
6959
  if (infrastructureStoppedRunIds.has(message.runId)) {
6705
- recoveryStore.unknown(message.requestId);
6960
+ recoveryJournal.unknown(message.requestId);
6706
6961
  return;
6707
6962
  }
6708
6963
  sendSerializedWorkerMessage(ws, JSON.stringify(result));
@@ -6735,8 +6990,10 @@ async function startWorker(options, projectRuntime = {
6735
6990
  const runCommand = async () => {
6736
6991
  try {
6737
6992
  const resolvedTarget = resolveMessageTarget(message.target);
6738
- process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
6739
- `);
6993
+ process.stdout.write(
6994
+ `[r5d-worker] exec_start ${message.runId}${message.commandClass === "control" ? " (control)" : ""}: ${message.argv.join(" ")}
6995
+ `
6996
+ );
6740
6997
  await executeStreamingCommand({
6741
6998
  resources,
6742
6999
  ws,
@@ -6774,11 +7031,21 @@ async function startWorker(options, projectRuntime = {
6774
7031
  try {
6775
7032
  resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6776
7033
  if (hasWorkspaceEffect) {
6777
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6778
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
6779
- targetReserved = true;
6780
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6781
- });
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
+ }
6782
7049
  }
6783
7050
  await (0, import_workspace_command_sync_policy.runReservedWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand, () => {
6784
7051
  if (targetReserved) {
@@ -6813,16 +7080,40 @@ async function startWorker(options, projectRuntime = {
6813
7080
  return;
6814
7081
  }
6815
7082
  const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
6816
- 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);
6817
7085
  if (reservesVisibleWorkspace) {
6818
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6819
- workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
6820
- if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
6821
- });
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
+ }
6822
7113
  }
6823
7114
  let dirtyTrigger;
6824
7115
  try {
6825
- 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 () => {
6826
7117
  const resolvedTarget = resolveMessageTarget(message.target);
6827
7118
  return await executeOperation({
6828
7119
  message,
@@ -6833,6 +7124,8 @@ async function startWorker(options, projectRuntime = {
6833
7124
  assertAdmission: assertMessageAdmission
6834
7125
  });
6835
7126
  });
7127
+ if (mutatesFiles && typeof message.sessionId === "string") sessionFileMutations.track(message.sessionId, operation);
7128
+ const result = await operation;
6836
7129
  sendSerializedWorkerMessage(
6837
7130
  ws,
6838
7131
  JSON.stringify({
@@ -6916,6 +7209,7 @@ async function startWorker(options, projectRuntime = {
6916
7209
  projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
6917
7210
  }
6918
7211
  if (currentWorkerSocket === ws) currentWorkerSocket = null;
7212
+ abortPendingReservations("Worker connection closed before command start");
6919
7213
  workerRecoveryId = "reconnecting";
6920
7214
  const reason = event.reason ? `: ${event.reason}` : "";
6921
7215
  process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
@@ -6949,6 +7243,11 @@ async function startWorker(options, projectRuntime = {
6949
7243
  });
6950
7244
  }
6951
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
+ });
6952
7251
  process.on("uncaughtException", (error) => {
6953
7252
  process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
6954
7253
  `);
@@ -6961,11 +7260,6 @@ function installWorkerRuntimeFailureLogging() {
6961
7260
  });
6962
7261
  }
6963
7262
  async function main() {
6964
- if (process.argv[2] === import_project_snapshot_recovery_runner.PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND) {
6965
- const exitCode2 = (0, import_project_snapshot_recovery_runner.runProjectSnapshotRecoveryHelper)(process.argv.slice(3));
6966
- if (exitCode2 !== 0) process.exit(exitCode2);
6967
- return;
6968
- }
6969
7263
  const parsed = parseArgs(process.argv.slice(2));
6970
7264
  if (parsed.command === "help") {
6971
7265
  printHelp();