@ricsam/r5d-worker 0.0.111 → 0.0.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/main.cjs CHANGED
@@ -62,7 +62,8 @@ var import_plan_lint = require("./plan-lint.cjs");
62
62
  var import_node_fs = __toESM(require("node:fs"), 1);
63
63
  var import_recovery_store = require("./recovery-store.cjs");
64
64
  var import_recovery_output_window = require("./recovery-output-window.cjs");
65
- var import_job_resources = require("./job-resources.cjs");
65
+ var import_command_launcher = require("./command-launcher.cjs");
66
+ var import_inline_output = require("./inline-output.cjs");
66
67
  var import_recovery_protocol = require("./recovery-protocol.cjs");
67
68
  var import_node_path = __toESM(require("node:path"), 1);
68
69
  var import_node_crypto = require("node:crypto");
@@ -155,7 +156,7 @@ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
155
156
  const PTY_FOREGROUND_POLL_MS = 1e3;
156
157
  const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
157
158
  const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
158
- let workerJobResources;
159
+ let workerCommandLauncher;
159
160
  const activeProcesses = /* @__PURE__ */ new Map();
160
161
  const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
161
162
  const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
@@ -175,6 +176,41 @@ let workerExecutionLeaseDeadline = 0;
175
176
  let workerExecutionLeaseTimer;
176
177
  let workerLeaseExpired = false;
177
178
  const infrastructureStoppedRunIds = /* @__PURE__ */ new Set();
179
+ let launcherFailureInProgress = false;
180
+ let runtimeExitPromise;
181
+ function fenceWorkerRuntimeOperations() {
182
+ launcherFailureInProgress = true;
183
+ workerAdmissionGeneration += 1;
184
+ for (const operation of recoveryStore?.operations() ?? []) {
185
+ if (operation.state !== "completed") recoveryStore?.unknown(operation.requestId);
186
+ }
187
+ for (const runId of workerCommandLauncher?.activeIds ?? []) {
188
+ infrastructureStoppedRunIds.add(runId);
189
+ cancelledProcessRuns.add(runId);
190
+ recoveryStore?.unknownRun(runId);
191
+ }
192
+ for (const runId of activeProcesses.keys()) infrastructureStoppedRunIds.add(runId);
193
+ }
194
+ function exitWorkerRuntime(code) {
195
+ if (runtimeExitPromise) return runtimeExitPromise;
196
+ fenceWorkerRuntimeOperations();
197
+ runtimeExitPromise = (async () => {
198
+ await Promise.race([
199
+ Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close()]),
200
+ Bun.sleep(4e3)
201
+ ]);
202
+ process.exit(code);
203
+ })();
204
+ return runtimeExitPromise;
205
+ }
206
+ function handleWorkerCommandLauncherFailure(error) {
207
+ if (!workerCommandLauncher || launcherFailureInProgress) return;
208
+ fenceWorkerRuntimeOperations();
209
+ process.stderr.write(`[r5d-worker] ${error.message}; stopping runtime for communication recovery
210
+ `);
211
+ currentWorkerSocket?.close(1012, "Command launcher recovery");
212
+ void exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
213
+ }
178
214
  function isWorkerCommunicationFailure(message) {
179
215
  return /Could not resolve host|Couldn't connect|Failed to connect|Connection reset|Connection timed out|remote end hung up|Empty reply|requested URL returned error: (?:408|429|5\d\d)|fetch failed|socket connection|ECONNRESET|ECONNREFUSED|ETIMEDOUT|Unable to connect|network connection was lost|Worker communication recovery expired|operation was aborted due to timeout|operation timed out|Failed to (?:fetch|download).*\((?:408|429|5\d\d)\)/i.test(
180
216
  message
@@ -204,22 +240,18 @@ function renewWorkerExecutionLease(leaseMs) {
204
240
  () => {
205
241
  if (Date.now() < workerExecutionLeaseDeadline) return;
206
242
  workerLeaseExpired = true;
207
- try {
208
- workerJobResources?.cancelMatching((id) => id.startsWith("pty:"));
209
- } catch (error) {
210
- process.stderr.write(`[r5d-worker] expired PTY resource termination failed: ${error}
243
+ void workerCommandLauncher?.cancelMatching((id) => id.startsWith("pty:")).catch((error) => {
244
+ process.stderr.write(`[r5d-worker] expired PTY launcher cancellation failed: ${error}
211
245
  `);
212
- }
246
+ });
213
247
  closeAllPtys();
214
248
  for (const operation of recoveryStore?.operations() ?? []) {
215
249
  if (operation.sessionId && operation.state !== "completed") {
216
250
  recoveryStore?.cancelSession(operation.sessionId);
217
- try {
218
- workerJobResources?.cancelSession(operation.sessionId);
219
- } catch (error) {
220
- process.stderr.write(`[r5d-worker] expired command resource termination failed: ${error}
251
+ void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
252
+ process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
221
253
  `);
222
- }
254
+ });
223
255
  }
224
256
  }
225
257
  for (const [runId, active] of activeProcesses) {
@@ -1584,7 +1616,9 @@ async function fenceCredentialGenerationAtConfigureEntry(input) {
1584
1616
  await input.terminatePreviousGeneration();
1585
1617
  return true;
1586
1618
  }
1587
- function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => process.exit(exitCode2), exitCode = 1) {
1619
+ function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => {
1620
+ void exitWorkerRuntime(exitCode2);
1621
+ }, exitCode = 1) {
1588
1622
  if (!required) return;
1589
1623
  fatalExit(exitCode);
1590
1624
  throw new Error("Credential generation restart callback returned unexpectedly");
@@ -2750,27 +2784,28 @@ async function prepareShellEnvForTarget(input) {
2750
2784
  ...targetProcessEnv(input.rootDir, input.target)
2751
2785
  };
2752
2786
  }
2753
- async function releaseWorkerJobResources(resources) {
2754
- if (!resources) return;
2755
- try {
2756
- await resources.release();
2757
- } catch (error) {
2758
- process.stderr.write(`[r5d-worker] job resource cleanup failed: ${error instanceof Error ? error.message : String(error)}
2787
+ async function releaseWorkerCommandLaunch(resources) {
2788
+ await resources?.release();
2789
+ }
2790
+ function cancelWorkerCommandLaunch(resources) {
2791
+ void resources.cancel().catch((error) => {
2792
+ process.stderr.write(`[r5d-worker] command launcher cancellation failed: ${error instanceof Error ? error.message : String(error)}
2759
2793
  `);
2760
- }
2794
+ });
2761
2795
  }
2762
- async function acquireWorkerCommandResources(ws, message, assertAdmission) {
2796
+ async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
2763
2797
  const id = message.type === "pty_open" ? `pty:${message.ptyId}` : message.runId;
2764
2798
  const operationId = message.type === "pty_open" ? id : `exec:${id}`;
2765
2799
  sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: true });
2766
2800
  try {
2767
- return await workerJobResources.acquire({
2801
+ return await workerCommandLauncher.acquire({
2768
2802
  id,
2769
- lane: message.type === "exec" && message.workspaceEffect === "none" ? "utility" : "general",
2803
+ kind: message.type,
2804
+ ..."workspaceEffect" in message && message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2770
2805
  ..."sessionId" in message && message.sessionId ? { sessionId: message.sessionId } : {},
2771
2806
  assertAdmission: () => {
2772
2807
  assertAdmission();
2773
- if (cancelledProcessRuns.has(id)) throw new import_job_resources.WorkerJobCancelledError();
2808
+ if (cancelledProcessRuns.has(id)) throw new import_command_launcher.CommandLaunchCancelledError();
2774
2809
  }
2775
2810
  });
2776
2811
  } finally {
@@ -2829,7 +2864,7 @@ async function executeCommand(input) {
2829
2864
  settlePreparation();
2830
2865
  if (input.message.timeoutMs) {
2831
2866
  timeout = setTimeout(() => {
2832
- input.resources.kill();
2867
+ cancelWorkerCommandLaunch(input.resources);
2833
2868
  void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
2834
2869
  process.stderr.write(
2835
2870
  `[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
@@ -2839,10 +2874,11 @@ async function executeCommand(input) {
2839
2874
  }, input.message.timeoutMs);
2840
2875
  }
2841
2876
  outputReaders = [
2842
- (0, import_job_resources.collectWorkerInlineOutput)(subprocess.stdout, void 0, outputAbort.signal),
2843
- (0, import_job_resources.collectWorkerInlineOutput)(subprocess.stderr, void 0, outputAbort.signal)
2877
+ (0, import_inline_output.collectWorkerInlineOutput)(subprocess.stdout, void 0, outputAbort.signal),
2878
+ (0, import_inline_output.collectWorkerInlineOutput)(subprocess.stderr, void 0, outputAbort.signal)
2844
2879
  ];
2845
2880
  const [stdout, stderr, exitCode] = await Promise.all([outputReaders[0], outputReaders[1], subprocess.exited]);
2881
+ const diagnostic = await input.resources.diagnose();
2846
2882
  return {
2847
2883
  type: "exec_result",
2848
2884
  requestId: input.message.requestId,
@@ -2850,14 +2886,12 @@ async function executeCommand(input) {
2850
2886
  stderr,
2851
2887
  exitCode,
2852
2888
  durationMs: Date.now() - startedAt,
2853
- ...input.resources.memoryLimitExceeded() ? {
2854
- error: `Command exceeded its worker memory limit (${input.resources.memoryBytes} bytes). Reduce parallelism or memory use and retry.`
2855
- } : {}
2889
+ ...diagnostic ? { error: diagnostic } : {}
2856
2890
  };
2857
2891
  } catch (error) {
2858
2892
  outputAbort.abort(error);
2859
- if (error instanceof StaleWorkerAdmissionError) throw error;
2860
- input.resources.kill();
2893
+ if (error instanceof StaleWorkerAdmissionError || error instanceof import_command_launcher.CommandLauncherUnavailableError) throw error;
2894
+ cancelWorkerCommandLaunch(input.resources);
2861
2895
  return {
2862
2896
  type: "exec_result",
2863
2897
  requestId: input.message.requestId,
@@ -2877,12 +2911,14 @@ async function executeCommand(input) {
2877
2911
  }
2878
2912
  outputAbort.abort();
2879
2913
  await Promise.allSettled(outputReaders);
2914
+ await releaseWorkerCommandLaunch(input.resources);
2880
2915
  activeProcesses.delete(input.message.runId);
2881
2916
  }
2882
2917
  }
2883
2918
  async function executeStreamingCommand(input) {
2884
2919
  const startedAt = Date.now();
2885
2920
  let started = false;
2921
+ let terminal;
2886
2922
  let timedOut = false;
2887
2923
  let timeout;
2888
2924
  let spawnedProcess;
@@ -2894,7 +2930,7 @@ async function executeStreamingCommand(input) {
2894
2930
  };
2895
2931
  input.setPreparationHeartbeatLease(true);
2896
2932
  try {
2897
- if (cancelledProcessRuns.delete(input.message.runId)) {
2933
+ if (cancelledProcessRuns.has(input.message.runId)) {
2898
2934
  sendWorkerMessage(input.ws, {
2899
2935
  type: "exec_start_error",
2900
2936
  requestId: input.message.requestId,
@@ -2957,7 +2993,7 @@ async function executeStreamingCommand(input) {
2957
2993
  if (input.message.timeoutMs) {
2958
2994
  timeout = setTimeout(() => {
2959
2995
  timedOut = true;
2960
- input.resources.kill();
2996
+ cancelWorkerCommandLaunch(input.resources);
2961
2997
  closeProcessStdin(activeProcesses.get(input.message.runId));
2962
2998
  void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
2963
2999
  process.stderr.write(
@@ -2986,18 +3022,19 @@ async function executeStreamingCommand(input) {
2986
3022
  });
2987
3023
  })
2988
3024
  ]);
2989
- if (input.resources.memoryLimitExceeded()) {
3025
+ const diagnostic = await input.resources.diagnose();
3026
+ if (diagnostic) {
2990
3027
  sendWorkerMessage(input.ws, {
2991
3028
  type: "exec_output",
2992
3029
  runId: input.message.runId,
2993
3030
  stream: "stderr",
2994
3031
  data: `
2995
- [r5d-worker] Command exceeded its memory limit (${input.resources.memoryBytes} bytes). Reduce parallelism or memory use and retry.
3032
+ [r5d-worker] ${diagnostic}
2996
3033
  `
2997
3034
  });
2998
3035
  }
2999
3036
  const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
3000
- const terminal = {
3037
+ terminal = {
3001
3038
  type: "exec_exit",
3002
3039
  runId: input.message.runId,
3003
3040
  exitCode,
@@ -3006,14 +3043,12 @@ async function executeStreamingCommand(input) {
3006
3043
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
3007
3044
  ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
3008
3045
  };
3009
- pendingProcessTerminals.set(input.message.runId, terminal);
3010
- sendWorkerMessage(input.ws, terminal);
3011
3046
  } catch (error) {
3012
- if (!started && error instanceof StaleWorkerAdmissionError) throw error;
3047
+ if (error instanceof import_command_launcher.CommandLauncherUnavailableError || !started && error instanceof StaleWorkerAdmissionError) throw error;
3013
3048
  const message = error instanceof Error ? error.message : String(error);
3014
3049
  if (started) {
3015
3050
  const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
3016
- const terminal = {
3051
+ terminal = {
3017
3052
  type: "exec_error",
3018
3053
  runId: input.message.runId,
3019
3054
  error: message,
@@ -3021,8 +3056,6 @@ async function executeStreamingCommand(input) {
3021
3056
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
3022
3057
  ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
3023
3058
  };
3024
- pendingProcessTerminals.set(input.message.runId, terminal);
3025
- sendWorkerMessage(input.ws, terminal);
3026
3059
  } else {
3027
3060
  sendWorkerMessage(input.ws, {
3028
3061
  type: "exec_start_error",
@@ -3040,9 +3073,14 @@ async function executeStreamingCommand(input) {
3040
3073
  if (spawnedProcess) {
3041
3074
  await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
3042
3075
  }
3076
+ await releaseWorkerCommandLaunch(input.resources);
3043
3077
  activeProcesses.delete(input.message.runId);
3044
3078
  cancelledProcessRuns.delete(input.message.runId);
3045
3079
  }
3080
+ if (terminal) {
3081
+ pendingProcessTerminals.set(input.message.runId, terminal);
3082
+ sendWorkerMessage(input.ws, terminal);
3083
+ }
3046
3084
  }
3047
3085
  async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
3048
3086
  try {
@@ -3082,6 +3120,7 @@ function sendReplayWorkerMessage(ws, message) {
3082
3120
  return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
3083
3121
  }
3084
3122
  function sendWorkerMessage(ws, message) {
3123
+ if (launcherFailureInProgress) return;
3085
3124
  if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
3086
3125
  return;
3087
3126
  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;
@@ -3443,6 +3482,29 @@ async function openPty(input) {
3443
3482
  envFilesRemoved = true;
3444
3483
  removePtyEnvFiles(stagedEnvFiles.paths);
3445
3484
  };
3485
+ let terminalHandled = false;
3486
+ const finishPty = async (terminal) => {
3487
+ if (terminalHandled) return;
3488
+ terminalHandled = true;
3489
+ try {
3490
+ const diagnostic = await input.resources.diagnose();
3491
+ if (diagnostic) outputCoalescer.push(`\r
3492
+ [r5d-worker] ${diagnostic}\r
3493
+ `);
3494
+ } finally {
3495
+ cleanupEnvFiles();
3496
+ await releaseWorkerCommandLaunch(input.resources);
3497
+ outputCoalescer.flush();
3498
+ activePtys.delete(input.message.ptyId);
3499
+ input.releaseWorkspaceMutation?.();
3500
+ input.onTerminal?.();
3501
+ }
3502
+ sendWorkerMessage(input.ws, terminal);
3503
+ };
3504
+ const reportPtyCleanupFailure = (error) => {
3505
+ process.stderr.write(`[r5d-worker] PTY launcher cleanup failed: ${error instanceof Error ? error.message : String(error)}
3506
+ `);
3507
+ };
3446
3508
  try {
3447
3509
  const ptyProcess = createNodePtyBridge({
3448
3510
  file: shellArgv[0],
@@ -3475,38 +3537,14 @@ async function openPty(input) {
3475
3537
  outputCoalescer.push(data);
3476
3538
  },
3477
3539
  onExit: (event) => {
3478
- if (input.resources.memoryLimitExceeded())
3479
- outputCoalescer.push(
3480
- `\r
3481
- [r5d-worker] Terminal exceeded its memory limit (${input.resources.memoryBytes} bytes). Reduce parallelism or memory use and retry.\r
3482
- `
3483
- );
3484
- outputCoalescer.flush();
3485
- input.releaseWorkspaceMutation?.();
3486
- activePtys.delete(input.message.ptyId);
3487
- cleanupEnvFiles();
3488
- input.onTerminal?.();
3489
- void releaseWorkerJobResources(input.resources);
3490
- sendWorkerMessage(input.ws, {
3491
- type: "pty_exit",
3492
- ptyId: input.message.ptyId,
3493
- exitCode: event.exitCode,
3494
- signal: event.signal
3495
- });
3540
+ void finishPty({ type: "pty_exit", ptyId: input.message.ptyId, exitCode: event.exitCode, signal: event.signal }).catch(
3541
+ reportPtyCleanupFailure
3542
+ );
3496
3543
  },
3497
3544
  onError: (error) => {
3498
- outputCoalescer.flush();
3499
- input.releaseWorkspaceMutation?.();
3500
- activePtys.delete(input.message.ptyId);
3501
- cleanupEnvFiles();
3502
- input.onTerminal?.();
3503
- void releaseWorkerJobResources(input.resources);
3504
- sendWorkerMessage(input.ws, {
3505
- type: "pty_error",
3506
- requestId: input.message.requestId,
3507
- ptyId: input.message.ptyId,
3508
- error: error.message
3509
- });
3545
+ void finishPty({ type: "pty_error", requestId: input.message.requestId, ptyId: input.message.ptyId, error: error.message }).catch(
3546
+ reportPtyCleanupFailure
3547
+ );
3510
3548
  },
3511
3549
  onTerminationError: (error) => {
3512
3550
  sendWorkerMessage(input.ws, {
@@ -3549,8 +3587,12 @@ function resizePty(message) {
3549
3587
  }
3550
3588
  ptyProcess.resize(Math.max(1, Math.min(Math.floor(message.cols || 80), 500)), Math.max(1, Math.min(Math.floor(message.rows || 24), 500)));
3551
3589
  }
3552
- function closePty(message) {
3553
- workerJobResources?.cancel(`pty:${message.ptyId}`);
3590
+ async function closePty(message) {
3591
+ try {
3592
+ await workerCommandLauncher?.cancel(`pty:${message.ptyId}`);
3593
+ } finally {
3594
+ activePtys.get(message.ptyId)?.kill();
3595
+ }
3554
3596
  const ptyProcess = activePtys.get(message.ptyId);
3555
3597
  if (!ptyProcess) {
3556
3598
  return;
@@ -3642,9 +3684,15 @@ async function startWorker(options, projectRuntime = {
3642
3684
  initializeCredentialStoreDirectoryOnce();
3643
3685
  const label = requireValue(options.label, "Missing required --label <label>");
3644
3686
  validateLabel(label);
3645
- if (!workerJobResources) {
3646
- workerJobResources = (0, import_job_resources.createWorkerJobResources)();
3647
- process.stdout.write(`[r5d-worker] job resources: ${workerJobResources.isolation}; ${JSON.stringify(workerJobResources.budget)}
3687
+ if (!workerCommandLauncher) {
3688
+ const launcher = await (0, import_command_launcher.createWorkerCommandLauncher)({
3689
+ configuration: process.env.R5D_WORKER_COMMAND_LAUNCHER,
3690
+ env: (0, import_command_launcher.commandLauncherEnvironment)(),
3691
+ onFailure: handleWorkerCommandLauncherFailure
3692
+ });
3693
+ workerCommandLauncher = launcher;
3694
+ await launcher.initialize();
3695
+ process.stdout.write(`[r5d-worker] command launcher: ${launcher.mode}
3648
3696
  `);
3649
3697
  }
3650
3698
  const rootDir = import_node_path.default.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
@@ -4887,7 +4935,7 @@ async function startWorker(options, projectRuntime = {
4887
4935
  `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4888
4936
  `
4889
4937
  );
4890
- process.exit(exitCode);
4938
+ void exitWorkerRuntime(exitCode);
4891
4939
  },
4892
4940
  exitCode: 1
4893
4941
  });
@@ -5698,7 +5746,7 @@ async function startWorker(options, projectRuntime = {
5698
5746
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
5699
5747
  ws.close(1e3, "Worker shutting down");
5700
5748
  } else {
5701
- process.exit(0);
5749
+ await exitWorkerRuntime(0);
5702
5750
  }
5703
5751
  })().catch((error) => {
5704
5752
  process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
@@ -5851,14 +5899,14 @@ async function startWorker(options, projectRuntime = {
5851
5899
  }
5852
5900
  if (message.type === "cancel_session") {
5853
5901
  recoveryStore.cancelSession(message.sessionId);
5854
- workerJobResources?.cancelSession(message.sessionId);
5855
- await Promise.all(
5856
- [...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
5902
+ await Promise.all([
5903
+ workerCommandLauncher?.cancelSession(message.sessionId),
5904
+ ...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
5857
5905
  cancelledProcessRuns.add(runId);
5858
5906
  closeProcessStdin(active);
5859
5907
  await (0, import_process_tree.terminateProcessTree)(active.process);
5860
5908
  })
5861
- );
5909
+ ]);
5862
5910
  sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
5863
5911
  return;
5864
5912
  }
@@ -6305,7 +6353,7 @@ async function startWorker(options, projectRuntime = {
6305
6353
  });
6306
6354
  return;
6307
6355
  }
6308
- if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
6356
+ if (activeProcesses.size > 0 || (workerCommandLauncher?.activeIds.length ?? 0) > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
6309
6357
  sendWorkerMessage(ws, {
6310
6358
  type: "update_clis_result",
6311
6359
  requestId: message.requestId,
@@ -6340,7 +6388,7 @@ async function startWorker(options, projectRuntime = {
6340
6388
  if (message.type === "cancel") {
6341
6389
  let resourceCancellationError;
6342
6390
  try {
6343
- workerJobResources?.cancel(message.runId);
6391
+ await workerCommandLauncher?.cancel(message.runId);
6344
6392
  } catch (error) {
6345
6393
  resourceCancellationError = error instanceof Error ? error.message : String(error);
6346
6394
  }
@@ -6452,7 +6500,7 @@ async function startWorker(options, projectRuntime = {
6452
6500
  let releaseWorkspaceMutation;
6453
6501
  let mutationLeaseTransferred = false;
6454
6502
  try {
6455
- resources = await acquireWorkerCommandResources(ws, message, assertMessageAdmission);
6503
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6456
6504
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6457
6505
  workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6458
6506
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
@@ -6491,8 +6539,8 @@ async function startWorker(options, projectRuntime = {
6491
6539
  });
6492
6540
  } finally {
6493
6541
  workspaceSyncPriorityPtyTargets.delete(message.ptyId);
6542
+ if (!resourcesTransferred) await releaseWorkerCommandLaunch(resources);
6494
6543
  if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
6495
- if (!resourcesTransferred) await releaseWorkerJobResources(resources);
6496
6544
  }
6497
6545
  return;
6498
6546
  }
@@ -6521,7 +6569,7 @@ async function startWorker(options, projectRuntime = {
6521
6569
  }
6522
6570
  if (message.type === "pty_close") {
6523
6571
  const activePty = activePtys.get(message.ptyId);
6524
- closePty(message);
6572
+ await closePty(message);
6525
6573
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
6526
6574
  markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
6527
6575
  }
@@ -6555,7 +6603,7 @@ async function startWorker(options, projectRuntime = {
6555
6603
  let targetReserved = false;
6556
6604
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
6557
6605
  try {
6558
- resources = await acquireWorkerCommandResources(ws, message, assertMessageAdmission);
6606
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6559
6607
  if (hasWorkspaceEffect) {
6560
6608
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6561
6609
  workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
@@ -6609,7 +6657,7 @@ async function startWorker(options, projectRuntime = {
6609
6657
  };
6610
6658
  } finally {
6611
6659
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
6612
- await releaseWorkerJobResources(resources);
6660
+ await releaseWorkerCommandLaunch(resources);
6613
6661
  cancelledProcessRuns.delete(message.runId);
6614
6662
  }
6615
6663
  if (infrastructureStoppedRunIds.has(message.runId)) {
@@ -6683,7 +6731,7 @@ async function startWorker(options, projectRuntime = {
6683
6731
  const execution = (async () => {
6684
6732
  let targetReserved = false;
6685
6733
  try {
6686
- resources = await acquireWorkerCommandResources(ws, message, assertMessageAdmission);
6734
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6687
6735
  if (hasWorkspaceEffect) {
6688
6736
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6689
6737
  workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
@@ -6699,7 +6747,7 @@ async function startWorker(options, projectRuntime = {
6699
6747
  });
6700
6748
  } finally {
6701
6749
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
6702
- await releaseWorkerJobResources(resources);
6750
+ await releaseWorkerCommandLaunch(resources);
6703
6751
  cancelledProcessRuns.delete(message.runId);
6704
6752
  }
6705
6753
  })();
@@ -6831,11 +6879,10 @@ async function startWorker(options, projectRuntime = {
6831
6879
  const reason = event.reason ? `: ${event.reason}` : "";
6832
6880
  process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
6833
6881
  `);
6834
- if (reloadAfterClose) {
6835
- process.exit(import_supervisor.WORKER_RELOAD_EXIT_CODE);
6836
- }
6837
- if (shutdownAfterClose) {
6838
- process.exit(0);
6882
+ if (launcherFailureInProgress) return;
6883
+ if (reloadAfterClose || shutdownAfterClose) {
6884
+ void exitWorkerRuntime(reloadAfterClose ? import_supervisor.WORKER_RELOAD_EXIT_CODE : 0);
6885
+ return;
6839
6886
  }
6840
6887
  const reconnect = () => {
6841
6888
  const delay = (0, import_supervisor.workerReconnectDelayMs)(Math.random, workerReconnectAttempt++);
@@ -6893,10 +6940,10 @@ async function main() {
6893
6940
  try {
6894
6941
  await startWorker(parsed.options);
6895
6942
  } catch (error) {
6896
- if (error instanceof WorkerServerUnavailableError) {
6943
+ if (error instanceof WorkerServerUnavailableError || error instanceof import_command_launcher.CommandLauncherUnavailableError) {
6897
6944
  process.stderr.write(`[r5d-worker] ${error.message}
6898
6945
  `);
6899
- process.exit(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
6946
+ await exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
6900
6947
  }
6901
6948
  throw error;
6902
6949
  }
@@ -6914,10 +6961,10 @@ function isCliEntrypoint() {
6914
6961
  return typeof require === "function" && typeof module !== "undefined" && require.main === module;
6915
6962
  }
6916
6963
  if (isCliEntrypoint()) {
6917
- main().catch((error) => {
6964
+ main().catch(async (error) => {
6918
6965
  process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
6919
6966
  `);
6920
- process.exit(1);
6967
+ await exitWorkerRuntime(1);
6921
6968
  });
6922
6969
  }
6923
6970
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.111",
3
+ "version": "0.0.112",
4
4
  "type": "commonjs"
5
5
  }
@@ -54,7 +54,7 @@ function workerReconnectDelayMs(random = Math.random, attempt = 0) {
54
54
  return floor + Math.floor(sample * (ceiling - floor));
55
55
  }
56
56
  async function defaultRuntimeReaper(runtime) {
57
- await (0, import_process_tree.terminateProcessTree)(runtime);
57
+ await (0, import_process_tree.terminateProcessTree)(runtime, { graceMs: 6e3 });
58
58
  }
59
59
  function isRetryableWorkerServerStatus(status) {
60
60
  return status === 408 || status === 429 || status >= 500;