@ricsam/r5d-worker 0.0.110 → 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,6 +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_command_launcher = require("./command-launcher.cjs");
66
+ var import_inline_output = require("./inline-output.cjs");
65
67
  var import_recovery_protocol = require("./recovery-protocol.cjs");
66
68
  var import_node_path = __toESM(require("node:path"), 1);
67
69
  var import_node_crypto = require("node:crypto");
@@ -154,6 +156,7 @@ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
154
156
  const PTY_FOREGROUND_POLL_MS = 1e3;
155
157
  const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
156
158
  const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
159
+ let workerCommandLauncher;
157
160
  const activeProcesses = /* @__PURE__ */ new Map();
158
161
  const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
159
162
  const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
@@ -173,6 +176,41 @@ let workerExecutionLeaseDeadline = 0;
173
176
  let workerExecutionLeaseTimer;
174
177
  let workerLeaseExpired = false;
175
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
+ }
176
214
  function isWorkerCommunicationFailure(message) {
177
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(
178
216
  message
@@ -202,8 +240,19 @@ function renewWorkerExecutionLease(leaseMs) {
202
240
  () => {
203
241
  if (Date.now() < workerExecutionLeaseDeadline) return;
204
242
  workerLeaseExpired = true;
243
+ void workerCommandLauncher?.cancelMatching((id) => id.startsWith("pty:")).catch((error) => {
244
+ process.stderr.write(`[r5d-worker] expired PTY launcher cancellation failed: ${error}
245
+ `);
246
+ });
247
+ closeAllPtys();
205
248
  for (const operation of recoveryStore?.operations() ?? []) {
206
- if (operation.sessionId && operation.state !== "completed") recoveryStore?.cancelSession(operation.sessionId);
249
+ if (operation.sessionId && operation.state !== "completed") {
250
+ recoveryStore?.cancelSession(operation.sessionId);
251
+ void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
252
+ process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
253
+ `);
254
+ });
255
+ }
207
256
  }
208
257
  for (const [runId, active] of activeProcesses) {
209
258
  if (!active.sessionId) continue;
@@ -1567,7 +1616,9 @@ async function fenceCredentialGenerationAtConfigureEntry(input) {
1567
1616
  await input.terminatePreviousGeneration();
1568
1617
  return true;
1569
1618
  }
1570
- function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => process.exit(exitCode2), exitCode = 1) {
1619
+ function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => {
1620
+ void exitWorkerRuntime(exitCode2);
1621
+ }, exitCode = 1) {
1571
1622
  if (!required) return;
1572
1623
  fatalExit(exitCode);
1573
1624
  throw new Error("Credential generation restart callback returned unexpectedly");
@@ -1597,7 +1648,7 @@ function credentialReapContractStatus(probe) {
1597
1648
  if (process.platform !== "linux") return "direct";
1598
1649
  try {
1599
1650
  const cgroupText = import_node_fs.default.readFileSync("/proc/self/cgroup", "utf8");
1600
- const unit = cgroupText.match(/(?:^|\/)r5d-worker\.service(?:$|\n)/m)?.[0]?.replace(/^\//, "").trim();
1651
+ const unit = /(?:^|\/)r5d-worker\.service(?:\/|$|\n)/m.test(cgroupText) ? "r5d-worker.service" : void 0;
1601
1652
  if (unit !== "r5d-worker.service") {
1602
1653
  return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? "systemd_unverified" : "direct";
1603
1654
  }
@@ -1631,7 +1682,7 @@ function credentialReapContractStatus(probe) {
1631
1682
  }
1632
1683
  }
1633
1684
  if (resolved.platform !== "linux") return "direct";
1634
- if (!/(?:^|\/)r5d-worker\.service(?:$|\n)/m.test(resolved.cgroupText)) {
1685
+ if (!/(?:^|\/)r5d-worker\.service(?:\/|$|\n)/m.test(resolved.cgroupText)) {
1635
1686
  return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "") ? "systemd_unverified" : "direct";
1636
1687
  }
1637
1688
  if (!SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "")) return "systemd_unverified";
@@ -2013,12 +2064,6 @@ function resolveCommandCwd(branchPath, cwd) {
2013
2064
  }
2014
2065
  return resolved;
2015
2066
  }
2016
- async function collectStream(stream) {
2017
- if (!stream) {
2018
- return "";
2019
- }
2020
- return await new Response(stream).text();
2021
- }
2022
2067
  async function streamCommandOutput(stream, onData) {
2023
2068
  if (!stream) {
2024
2069
  return;
@@ -2739,8 +2784,38 @@ async function prepareShellEnvForTarget(input) {
2739
2784
  ...targetProcessEnv(input.rootDir, input.target)
2740
2785
  };
2741
2786
  }
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)}
2793
+ `);
2794
+ });
2795
+ }
2796
+ async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
2797
+ const id = message.type === "pty_open" ? `pty:${message.ptyId}` : message.runId;
2798
+ const operationId = message.type === "pty_open" ? id : `exec:${id}`;
2799
+ sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: true });
2800
+ try {
2801
+ return await workerCommandLauncher.acquire({
2802
+ id,
2803
+ kind: message.type,
2804
+ ..."workspaceEffect" in message && message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2805
+ ..."sessionId" in message && message.sessionId ? { sessionId: message.sessionId } : {},
2806
+ assertAdmission: () => {
2807
+ assertAdmission();
2808
+ if (cancelledProcessRuns.has(id)) throw new import_command_launcher.CommandLaunchCancelledError();
2809
+ }
2810
+ });
2811
+ } finally {
2812
+ sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: false });
2813
+ }
2814
+ }
2742
2815
  async function executeCommand(input) {
2743
2816
  const startedAt = Date.now();
2817
+ const outputAbort = new AbortController();
2818
+ let outputReaders = [];
2744
2819
  let timeout;
2745
2820
  let spawnedProcess;
2746
2821
  let preparationSettled = false;
@@ -2761,7 +2836,7 @@ async function executeCommand(input) {
2761
2836
  });
2762
2837
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
2763
2838
  input.assertAdmission();
2764
- const subprocess = Bun.spawn(input.message.argv, {
2839
+ const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
2765
2840
  cwd,
2766
2841
  stdout: "pipe",
2767
2842
  stderr: "pipe",
@@ -2789,6 +2864,7 @@ async function executeCommand(input) {
2789
2864
  settlePreparation();
2790
2865
  if (input.message.timeoutMs) {
2791
2866
  timeout = setTimeout(() => {
2867
+ cancelWorkerCommandLaunch(input.resources);
2792
2868
  void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
2793
2869
  process.stderr.write(
2794
2870
  `[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
@@ -2797,21 +2873,25 @@ async function executeCommand(input) {
2797
2873
  });
2798
2874
  }, input.message.timeoutMs);
2799
2875
  }
2800
- const [stdout, stderr, exitCode] = await Promise.all([
2801
- collectStream(subprocess.stdout),
2802
- collectStream(subprocess.stderr),
2803
- subprocess.exited
2804
- ]);
2876
+ outputReaders = [
2877
+ (0, import_inline_output.collectWorkerInlineOutput)(subprocess.stdout, void 0, outputAbort.signal),
2878
+ (0, import_inline_output.collectWorkerInlineOutput)(subprocess.stderr, void 0, outputAbort.signal)
2879
+ ];
2880
+ const [stdout, stderr, exitCode] = await Promise.all([outputReaders[0], outputReaders[1], subprocess.exited]);
2881
+ const diagnostic = await input.resources.diagnose();
2805
2882
  return {
2806
2883
  type: "exec_result",
2807
2884
  requestId: input.message.requestId,
2808
2885
  stdout,
2809
2886
  stderr,
2810
2887
  exitCode,
2811
- durationMs: Date.now() - startedAt
2888
+ durationMs: Date.now() - startedAt,
2889
+ ...diagnostic ? { error: diagnostic } : {}
2812
2890
  };
2813
2891
  } catch (error) {
2814
- if (error instanceof StaleWorkerAdmissionError) throw error;
2892
+ outputAbort.abort(error);
2893
+ if (error instanceof StaleWorkerAdmissionError || error instanceof import_command_launcher.CommandLauncherUnavailableError) throw error;
2894
+ cancelWorkerCommandLaunch(input.resources);
2815
2895
  return {
2816
2896
  type: "exec_result",
2817
2897
  requestId: input.message.requestId,
@@ -2829,12 +2909,16 @@ async function executeCommand(input) {
2829
2909
  if (spawnedProcess) {
2830
2910
  await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
2831
2911
  }
2912
+ outputAbort.abort();
2913
+ await Promise.allSettled(outputReaders);
2914
+ await releaseWorkerCommandLaunch(input.resources);
2832
2915
  activeProcesses.delete(input.message.runId);
2833
2916
  }
2834
2917
  }
2835
2918
  async function executeStreamingCommand(input) {
2836
2919
  const startedAt = Date.now();
2837
2920
  let started = false;
2921
+ let terminal;
2838
2922
  let timedOut = false;
2839
2923
  let timeout;
2840
2924
  let spawnedProcess;
@@ -2846,7 +2930,7 @@ async function executeStreamingCommand(input) {
2846
2930
  };
2847
2931
  input.setPreparationHeartbeatLease(true);
2848
2932
  try {
2849
- if (cancelledProcessRuns.delete(input.message.runId)) {
2933
+ if (cancelledProcessRuns.has(input.message.runId)) {
2850
2934
  sendWorkerMessage(input.ws, {
2851
2935
  type: "exec_start_error",
2852
2936
  requestId: input.message.requestId,
@@ -2866,7 +2950,7 @@ async function executeStreamingCommand(input) {
2866
2950
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
2867
2951
  const interactive = input.message.interactive === true;
2868
2952
  input.assertAdmission();
2869
- const subprocess = Bun.spawn(input.message.argv, {
2953
+ const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
2870
2954
  cwd,
2871
2955
  // Without an explicit stdin the process reads /dev/null and interactive
2872
2956
  // prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
@@ -2909,6 +2993,7 @@ async function executeStreamingCommand(input) {
2909
2993
  if (input.message.timeoutMs) {
2910
2994
  timeout = setTimeout(() => {
2911
2995
  timedOut = true;
2996
+ cancelWorkerCommandLaunch(input.resources);
2912
2997
  closeProcessStdin(activeProcesses.get(input.message.runId));
2913
2998
  void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
2914
2999
  process.stderr.write(
@@ -2937,8 +3022,19 @@ async function executeStreamingCommand(input) {
2937
3022
  });
2938
3023
  })
2939
3024
  ]);
3025
+ const diagnostic = await input.resources.diagnose();
3026
+ if (diagnostic) {
3027
+ sendWorkerMessage(input.ws, {
3028
+ type: "exec_output",
3029
+ runId: input.message.runId,
3030
+ stream: "stderr",
3031
+ data: `
3032
+ [r5d-worker] ${diagnostic}
3033
+ `
3034
+ });
3035
+ }
2940
3036
  const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2941
- const terminal = {
3037
+ terminal = {
2942
3038
  type: "exec_exit",
2943
3039
  runId: input.message.runId,
2944
3040
  exitCode,
@@ -2947,14 +3043,12 @@ async function executeStreamingCommand(input) {
2947
3043
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2948
3044
  ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2949
3045
  };
2950
- pendingProcessTerminals.set(input.message.runId, terminal);
2951
- sendWorkerMessage(input.ws, terminal);
2952
3046
  } catch (error) {
2953
- if (!started && error instanceof StaleWorkerAdmissionError) throw error;
3047
+ if (error instanceof import_command_launcher.CommandLauncherUnavailableError || !started && error instanceof StaleWorkerAdmissionError) throw error;
2954
3048
  const message = error instanceof Error ? error.message : String(error);
2955
3049
  if (started) {
2956
3050
  const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2957
- const terminal = {
3051
+ terminal = {
2958
3052
  type: "exec_error",
2959
3053
  runId: input.message.runId,
2960
3054
  error: message,
@@ -2962,8 +3056,6 @@ async function executeStreamingCommand(input) {
2962
3056
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2963
3057
  ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2964
3058
  };
2965
- pendingProcessTerminals.set(input.message.runId, terminal);
2966
- sendWorkerMessage(input.ws, terminal);
2967
3059
  } else {
2968
3060
  sendWorkerMessage(input.ws, {
2969
3061
  type: "exec_start_error",
@@ -2981,9 +3073,14 @@ async function executeStreamingCommand(input) {
2981
3073
  if (spawnedProcess) {
2982
3074
  await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
2983
3075
  }
3076
+ await releaseWorkerCommandLaunch(input.resources);
2984
3077
  activeProcesses.delete(input.message.runId);
2985
3078
  cancelledProcessRuns.delete(input.message.runId);
2986
3079
  }
3080
+ if (terminal) {
3081
+ pendingProcessTerminals.set(input.message.runId, terminal);
3082
+ sendWorkerMessage(input.ws, terminal);
3083
+ }
2987
3084
  }
2988
3085
  async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
2989
3086
  try {
@@ -3023,6 +3120,7 @@ function sendReplayWorkerMessage(ws, message) {
3023
3120
  return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
3024
3121
  }
3025
3122
  function sendWorkerMessage(ws, message) {
3123
+ if (launcherFailureInProgress) return;
3026
3124
  if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
3027
3125
  return;
3028
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;
@@ -3366,6 +3464,7 @@ async function openPty(input) {
3366
3464
  }) : {};
3367
3465
  const targetEnv = targetProcessEnv(input.rootDir, input.resolvedTarget.target);
3368
3466
  const shell = resolveHostShell(input.message.command);
3467
+ const shellArgv = input.resources.wrap([shell.file, ...shell.args]);
3369
3468
  const outputCoalescer = (0, import_pty_output_coalescer.createPtyOutputCoalescer)({
3370
3469
  send: (data) => {
3371
3470
  sendWorkerMessage(input.ws, {
@@ -3383,10 +3482,33 @@ async function openPty(input) {
3383
3482
  envFilesRemoved = true;
3384
3483
  removePtyEnvFiles(stagedEnvFiles.paths);
3385
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
+ };
3386
3508
  try {
3387
3509
  const ptyProcess = createNodePtyBridge({
3388
- file: shell.file,
3389
- args: shell.args,
3510
+ file: shellArgv[0],
3511
+ args: shellArgv.slice(1),
3390
3512
  ptyOptions: {
3391
3513
  name: "xterm-256color",
3392
3514
  cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
@@ -3415,30 +3537,14 @@ async function openPty(input) {
3415
3537
  outputCoalescer.push(data);
3416
3538
  },
3417
3539
  onExit: (event) => {
3418
- outputCoalescer.flush();
3419
- input.releaseWorkspaceMutation?.();
3420
- activePtys.delete(input.message.ptyId);
3421
- cleanupEnvFiles();
3422
- input.onTerminal?.();
3423
- sendWorkerMessage(input.ws, {
3424
- type: "pty_exit",
3425
- ptyId: input.message.ptyId,
3426
- exitCode: event.exitCode,
3427
- signal: event.signal
3428
- });
3540
+ void finishPty({ type: "pty_exit", ptyId: input.message.ptyId, exitCode: event.exitCode, signal: event.signal }).catch(
3541
+ reportPtyCleanupFailure
3542
+ );
3429
3543
  },
3430
3544
  onError: (error) => {
3431
- outputCoalescer.flush();
3432
- input.releaseWorkspaceMutation?.();
3433
- activePtys.delete(input.message.ptyId);
3434
- cleanupEnvFiles();
3435
- input.onTerminal?.();
3436
- sendWorkerMessage(input.ws, {
3437
- type: "pty_error",
3438
- requestId: input.message.requestId,
3439
- ptyId: input.message.ptyId,
3440
- error: error.message
3441
- });
3545
+ void finishPty({ type: "pty_error", requestId: input.message.requestId, ptyId: input.message.ptyId, error: error.message }).catch(
3546
+ reportPtyCleanupFailure
3547
+ );
3442
3548
  },
3443
3549
  onTerminationError: (error) => {
3444
3550
  sendWorkerMessage(input.ws, {
@@ -3481,7 +3587,12 @@ function resizePty(message) {
3481
3587
  }
3482
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)));
3483
3589
  }
3484
- function closePty(message) {
3590
+ async function closePty(message) {
3591
+ try {
3592
+ await workerCommandLauncher?.cancel(`pty:${message.ptyId}`);
3593
+ } finally {
3594
+ activePtys.get(message.ptyId)?.kill();
3595
+ }
3485
3596
  const ptyProcess = activePtys.get(message.ptyId);
3486
3597
  if (!ptyProcess) {
3487
3598
  return;
@@ -3500,7 +3611,7 @@ function closePtysBestEffort(ptys, reportFailure) {
3500
3611
  function closeAllPtys() {
3501
3612
  closePtysBestEffort(activePtys, (ptyId, error) => {
3502
3613
  process.stderr.write(
3503
- `[r5d-worker] failed to close PTY ${ptyId} after control disconnect: ${error instanceof Error ? error.message : String(error)}
3614
+ `[r5d-worker] failed to close PTY ${ptyId} after execution lease expiry: ${error instanceof Error ? error.message : String(error)}
3504
3615
  `
3505
3616
  );
3506
3617
  });
@@ -3573,6 +3684,17 @@ async function startWorker(options, projectRuntime = {
3573
3684
  initializeCredentialStoreDirectoryOnce();
3574
3685
  const label = requireValue(options.label, "Missing required --label <label>");
3575
3686
  validateLabel(label);
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}
3696
+ `);
3697
+ }
3576
3698
  const rootDir = import_node_path.default.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
3577
3699
  recoveryStore ??= new import_recovery_store.WorkerRecoveryStore(
3578
3700
  import_node_path.default.join(rootDir, ".worker-recovery", (0, import_node_crypto.createHash)("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
@@ -4813,7 +4935,7 @@ async function startWorker(options, projectRuntime = {
4813
4935
  `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4814
4936
  `
4815
4937
  );
4816
- process.exit(exitCode);
4938
+ void exitWorkerRuntime(exitCode);
4817
4939
  },
4818
4940
  exitCode: 1
4819
4941
  });
@@ -5624,7 +5746,7 @@ async function startWorker(options, projectRuntime = {
5624
5746
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
5625
5747
  ws.close(1e3, "Worker shutting down");
5626
5748
  } else {
5627
- process.exit(0);
5749
+ await exitWorkerRuntime(0);
5628
5750
  }
5629
5751
  })().catch((error) => {
5630
5752
  process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
@@ -5777,13 +5899,14 @@ async function startWorker(options, projectRuntime = {
5777
5899
  }
5778
5900
  if (message.type === "cancel_session") {
5779
5901
  recoveryStore.cancelSession(message.sessionId);
5780
- await Promise.all(
5781
- [...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]) => {
5782
5905
  cancelledProcessRuns.add(runId);
5783
5906
  closeProcessStdin(active);
5784
5907
  await (0, import_process_tree.terminateProcessTree)(active.process);
5785
5908
  })
5786
- );
5909
+ ]);
5787
5910
  sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
5788
5911
  return;
5789
5912
  }
@@ -6230,7 +6353,7 @@ async function startWorker(options, projectRuntime = {
6230
6353
  });
6231
6354
  return;
6232
6355
  }
6233
- 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) {
6234
6357
  sendWorkerMessage(ws, {
6235
6358
  type: "update_clis_result",
6236
6359
  requestId: message.requestId,
@@ -6263,8 +6386,14 @@ async function startWorker(options, projectRuntime = {
6263
6386
  return;
6264
6387
  }
6265
6388
  if (message.type === "cancel") {
6389
+ let resourceCancellationError;
6390
+ try {
6391
+ await workerCommandLauncher?.cancel(message.runId);
6392
+ } catch (error) {
6393
+ resourceCancellationError = error instanceof Error ? error.message : String(error);
6394
+ }
6266
6395
  const active = activeProcesses.get(message.runId);
6267
- let cancelled = true;
6396
+ let cancelled = resourceCancellationError === void 0;
6268
6397
  let cancelMessage;
6269
6398
  if (active) {
6270
6399
  try {
@@ -6279,6 +6408,7 @@ async function startWorker(options, projectRuntime = {
6279
6408
  cancelledProcessRuns.add(message.runId);
6280
6409
  cancelMessage = "Cancellation queued before command start";
6281
6410
  }
6411
+ if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
6282
6412
  sendSerializedWorkerMessage(
6283
6413
  ws,
6284
6414
  JSON.stringify({
@@ -6365,13 +6495,16 @@ async function startWorker(options, projectRuntime = {
6365
6495
  });
6366
6496
  return;
6367
6497
  }
6368
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6369
- workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6370
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6371
- });
6498
+ let resources;
6499
+ let resourcesTransferred = false;
6372
6500
  let releaseWorkspaceMutation;
6373
6501
  let mutationLeaseTransferred = false;
6374
6502
  try {
6503
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6504
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6505
+ workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6506
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6507
+ });
6375
6508
  releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
6376
6509
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
6377
6510
  throw new Error(
@@ -6382,6 +6515,7 @@ async function startWorker(options, projectRuntime = {
6382
6515
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
6383
6516
  `);
6384
6517
  await openPty({
6518
+ resources,
6385
6519
  ws,
6386
6520
  message,
6387
6521
  resolvedTarget,
@@ -6394,6 +6528,7 @@ async function startWorker(options, projectRuntime = {
6394
6528
  }
6395
6529
  } : {}
6396
6530
  });
6531
+ resourcesTransferred = true;
6397
6532
  mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
6398
6533
  } catch (error) {
6399
6534
  sendWorkerMessageFromCurrentSource(ws, {
@@ -6404,6 +6539,7 @@ async function startWorker(options, projectRuntime = {
6404
6539
  });
6405
6540
  } finally {
6406
6541
  workspaceSyncPriorityPtyTargets.delete(message.ptyId);
6542
+ if (!resourcesTransferred) await releaseWorkerCommandLaunch(resources);
6407
6543
  if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
6408
6544
  }
6409
6545
  return;
@@ -6433,7 +6569,7 @@ async function startWorker(options, projectRuntime = {
6433
6569
  }
6434
6570
  if (message.type === "pty_close") {
6435
6571
  const activePty = activePtys.get(message.ptyId);
6436
- closePty(message);
6572
+ await closePty(message);
6437
6573
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
6438
6574
  markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
6439
6575
  }
@@ -6463,9 +6599,11 @@ async function startWorker(options, projectRuntime = {
6463
6599
  return;
6464
6600
  }
6465
6601
  let result;
6602
+ let resources;
6466
6603
  let targetReserved = false;
6467
6604
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
6468
6605
  try {
6606
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6469
6607
  if (hasWorkspaceEffect) {
6470
6608
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6471
6609
  workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
@@ -6483,6 +6621,7 @@ async function startWorker(options, projectRuntime = {
6483
6621
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
6484
6622
  `);
6485
6623
  return await executeCommand({
6624
+ resources,
6486
6625
  message,
6487
6626
  resolvedTarget,
6488
6627
  baseUrl,
@@ -6518,6 +6657,8 @@ async function startWorker(options, projectRuntime = {
6518
6657
  };
6519
6658
  } finally {
6520
6659
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
6660
+ await releaseWorkerCommandLaunch(resources);
6661
+ cancelledProcessRuns.delete(message.runId);
6521
6662
  }
6522
6663
  if (infrastructureStoppedRunIds.has(message.runId)) {
6523
6664
  recoveryStore.unknown(message.requestId);
@@ -6549,12 +6690,14 @@ async function startWorker(options, projectRuntime = {
6549
6690
  runId: message.runId
6550
6691
  });
6551
6692
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
6693
+ let resources;
6552
6694
  const runCommand = async () => {
6553
6695
  try {
6554
6696
  const resolvedTarget = resolveMessageTarget(message.target);
6555
6697
  process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
6556
6698
  `);
6557
6699
  await executeStreamingCommand({
6700
+ resources,
6558
6701
  ws,
6559
6702
  message,
6560
6703
  resolvedTarget,
@@ -6588,6 +6731,7 @@ async function startWorker(options, projectRuntime = {
6588
6731
  const execution = (async () => {
6589
6732
  let targetReserved = false;
6590
6733
  try {
6734
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6591
6735
  if (hasWorkspaceEffect) {
6592
6736
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
6593
6737
  workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
@@ -6603,6 +6747,8 @@ async function startWorker(options, projectRuntime = {
6603
6747
  });
6604
6748
  } finally {
6605
6749
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
6750
+ await releaseWorkerCommandLaunch(resources);
6751
+ cancelledProcessRuns.delete(message.runId);
6606
6752
  }
6607
6753
  })();
6608
6754
  void execution.catch((error) => {
@@ -6733,11 +6879,10 @@ async function startWorker(options, projectRuntime = {
6733
6879
  const reason = event.reason ? `: ${event.reason}` : "";
6734
6880
  process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
6735
6881
  `);
6736
- if (reloadAfterClose) {
6737
- process.exit(import_supervisor.WORKER_RELOAD_EXIT_CODE);
6738
- }
6739
- if (shutdownAfterClose) {
6740
- process.exit(0);
6882
+ if (launcherFailureInProgress) return;
6883
+ if (reloadAfterClose || shutdownAfterClose) {
6884
+ void exitWorkerRuntime(reloadAfterClose ? import_supervisor.WORKER_RELOAD_EXIT_CODE : 0);
6885
+ return;
6741
6886
  }
6742
6887
  const reconnect = () => {
6743
6888
  const delay = (0, import_supervisor.workerReconnectDelayMs)(Math.random, workerReconnectAttempt++);
@@ -6795,10 +6940,10 @@ async function main() {
6795
6940
  try {
6796
6941
  await startWorker(parsed.options);
6797
6942
  } catch (error) {
6798
- if (error instanceof WorkerServerUnavailableError) {
6943
+ if (error instanceof WorkerServerUnavailableError || error instanceof import_command_launcher.CommandLauncherUnavailableError) {
6799
6944
  process.stderr.write(`[r5d-worker] ${error.message}
6800
6945
  `);
6801
- process.exit(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
6946
+ await exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
6802
6947
  }
6803
6948
  throw error;
6804
6949
  }
@@ -6816,10 +6961,10 @@ function isCliEntrypoint() {
6816
6961
  return typeof require === "function" && typeof module !== "undefined" && require.main === module;
6817
6962
  }
6818
6963
  if (isCliEntrypoint()) {
6819
- main().catch((error) => {
6964
+ main().catch(async (error) => {
6820
6965
  process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
6821
6966
  `);
6822
- process.exit(1);
6967
+ await exitWorkerRuntime(1);
6823
6968
  });
6824
6969
  }
6825
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.110",
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;