@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/mjs/main.mjs CHANGED
@@ -5,6 +5,13 @@ import { lintPlans, formatPlanLintErrors } from "./plan-lint.mjs";
5
5
  import fs from "node:fs";
6
6
  import { WorkerRecoveryStore, workerOperationDigest } from "./recovery-store.mjs";
7
7
  import { WorkerOutputWindow } from "./recovery-output-window.mjs";
8
+ import {
9
+ createWorkerCommandLauncher,
10
+ commandLauncherEnvironment,
11
+ CommandLaunchCancelledError,
12
+ CommandLauncherUnavailableError
13
+ } from "./command-launcher.mjs";
14
+ import { collectWorkerInlineOutput } from "./inline-output.mjs";
8
15
  import {
9
16
  WORKER_RESUMABLE_PROTOCOL,
10
17
  WORKER_RECOVERY_TIMEOUT_MS
@@ -168,6 +175,7 @@ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
168
175
  const PTY_FOREGROUND_POLL_MS = 1e3;
169
176
  const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
170
177
  const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
178
+ let workerCommandLauncher;
171
179
  const activeProcesses = /* @__PURE__ */ new Map();
172
180
  const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
173
181
  const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
@@ -187,6 +195,41 @@ let workerExecutionLeaseDeadline = 0;
187
195
  let workerExecutionLeaseTimer;
188
196
  let workerLeaseExpired = false;
189
197
  const infrastructureStoppedRunIds = /* @__PURE__ */ new Set();
198
+ let launcherFailureInProgress = false;
199
+ let runtimeExitPromise;
200
+ function fenceWorkerRuntimeOperations() {
201
+ launcherFailureInProgress = true;
202
+ workerAdmissionGeneration += 1;
203
+ for (const operation of recoveryStore?.operations() ?? []) {
204
+ if (operation.state !== "completed") recoveryStore?.unknown(operation.requestId);
205
+ }
206
+ for (const runId of workerCommandLauncher?.activeIds ?? []) {
207
+ infrastructureStoppedRunIds.add(runId);
208
+ cancelledProcessRuns.add(runId);
209
+ recoveryStore?.unknownRun(runId);
210
+ }
211
+ for (const runId of activeProcesses.keys()) infrastructureStoppedRunIds.add(runId);
212
+ }
213
+ function exitWorkerRuntime(code) {
214
+ if (runtimeExitPromise) return runtimeExitPromise;
215
+ fenceWorkerRuntimeOperations();
216
+ runtimeExitPromise = (async () => {
217
+ await Promise.race([
218
+ Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close()]),
219
+ Bun.sleep(4e3)
220
+ ]);
221
+ process.exit(code);
222
+ })();
223
+ return runtimeExitPromise;
224
+ }
225
+ function handleWorkerCommandLauncherFailure(error) {
226
+ if (!workerCommandLauncher || launcherFailureInProgress) return;
227
+ fenceWorkerRuntimeOperations();
228
+ process.stderr.write(`[r5d-worker] ${error.message}; stopping runtime for communication recovery
229
+ `);
230
+ currentWorkerSocket?.close(1012, "Command launcher recovery");
231
+ void exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
232
+ }
190
233
  function isWorkerCommunicationFailure(message) {
191
234
  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(
192
235
  message
@@ -216,8 +259,19 @@ function renewWorkerExecutionLease(leaseMs) {
216
259
  () => {
217
260
  if (Date.now() < workerExecutionLeaseDeadline) return;
218
261
  workerLeaseExpired = true;
262
+ void workerCommandLauncher?.cancelMatching((id) => id.startsWith("pty:")).catch((error) => {
263
+ process.stderr.write(`[r5d-worker] expired PTY launcher cancellation failed: ${error}
264
+ `);
265
+ });
266
+ closeAllPtys();
219
267
  for (const operation of recoveryStore?.operations() ?? []) {
220
- if (operation.sessionId && operation.state !== "completed") recoveryStore?.cancelSession(operation.sessionId);
268
+ if (operation.sessionId && operation.state !== "completed") {
269
+ recoveryStore?.cancelSession(operation.sessionId);
270
+ void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
271
+ process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
272
+ `);
273
+ });
274
+ }
221
275
  }
222
276
  for (const [runId, active] of activeProcesses) {
223
277
  if (!active.sessionId) continue;
@@ -1581,7 +1635,9 @@ async function fenceCredentialGenerationAtConfigureEntry(input) {
1581
1635
  await input.terminatePreviousGeneration();
1582
1636
  return true;
1583
1637
  }
1584
- function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => process.exit(exitCode2), exitCode = 1) {
1638
+ function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => {
1639
+ void exitWorkerRuntime(exitCode2);
1640
+ }, exitCode = 1) {
1585
1641
  if (!required) return;
1586
1642
  fatalExit(exitCode);
1587
1643
  throw new Error("Credential generation restart callback returned unexpectedly");
@@ -1611,7 +1667,7 @@ function credentialReapContractStatus(probe) {
1611
1667
  if (process.platform !== "linux") return "direct";
1612
1668
  try {
1613
1669
  const cgroupText = fs.readFileSync("/proc/self/cgroup", "utf8");
1614
- const unit = cgroupText.match(/(?:^|\/)r5d-worker\.service(?:$|\n)/m)?.[0]?.replace(/^\//, "").trim();
1670
+ const unit = /(?:^|\/)r5d-worker\.service(?:\/|$|\n)/m.test(cgroupText) ? "r5d-worker.service" : void 0;
1615
1671
  if (unit !== "r5d-worker.service") {
1616
1672
  return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? "systemd_unverified" : "direct";
1617
1673
  }
@@ -1645,7 +1701,7 @@ function credentialReapContractStatus(probe) {
1645
1701
  }
1646
1702
  }
1647
1703
  if (resolved.platform !== "linux") return "direct";
1648
- if (!/(?:^|\/)r5d-worker\.service(?:$|\n)/m.test(resolved.cgroupText)) {
1704
+ if (!/(?:^|\/)r5d-worker\.service(?:\/|$|\n)/m.test(resolved.cgroupText)) {
1649
1705
  return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "") ? "systemd_unverified" : "direct";
1650
1706
  }
1651
1707
  if (!SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "")) return "systemd_unverified";
@@ -2027,12 +2083,6 @@ function resolveCommandCwd(branchPath, cwd) {
2027
2083
  }
2028
2084
  return resolved;
2029
2085
  }
2030
- async function collectStream(stream) {
2031
- if (!stream) {
2032
- return "";
2033
- }
2034
- return await new Response(stream).text();
2035
- }
2036
2086
  async function streamCommandOutput(stream, onData) {
2037
2087
  if (!stream) {
2038
2088
  return;
@@ -2753,8 +2803,38 @@ async function prepareShellEnvForTarget(input) {
2753
2803
  ...targetProcessEnv(input.rootDir, input.target)
2754
2804
  };
2755
2805
  }
2806
+ async function releaseWorkerCommandLaunch(resources) {
2807
+ await resources?.release();
2808
+ }
2809
+ function cancelWorkerCommandLaunch(resources) {
2810
+ void resources.cancel().catch((error) => {
2811
+ process.stderr.write(`[r5d-worker] command launcher cancellation failed: ${error instanceof Error ? error.message : String(error)}
2812
+ `);
2813
+ });
2814
+ }
2815
+ async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
2816
+ const id = message.type === "pty_open" ? `pty:${message.ptyId}` : message.runId;
2817
+ const operationId = message.type === "pty_open" ? id : `exec:${id}`;
2818
+ sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: true });
2819
+ try {
2820
+ return await workerCommandLauncher.acquire({
2821
+ id,
2822
+ kind: message.type,
2823
+ ..."workspaceEffect" in message && message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2824
+ ..."sessionId" in message && message.sessionId ? { sessionId: message.sessionId } : {},
2825
+ assertAdmission: () => {
2826
+ assertAdmission();
2827
+ if (cancelledProcessRuns.has(id)) throw new CommandLaunchCancelledError();
2828
+ }
2829
+ });
2830
+ } finally {
2831
+ sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: false });
2832
+ }
2833
+ }
2756
2834
  async function executeCommand(input) {
2757
2835
  const startedAt = Date.now();
2836
+ const outputAbort = new AbortController();
2837
+ let outputReaders = [];
2758
2838
  let timeout;
2759
2839
  let spawnedProcess;
2760
2840
  let preparationSettled = false;
@@ -2775,7 +2855,7 @@ async function executeCommand(input) {
2775
2855
  });
2776
2856
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
2777
2857
  input.assertAdmission();
2778
- const subprocess = Bun.spawn(input.message.argv, {
2858
+ const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
2779
2859
  cwd,
2780
2860
  stdout: "pipe",
2781
2861
  stderr: "pipe",
@@ -2803,6 +2883,7 @@ async function executeCommand(input) {
2803
2883
  settlePreparation();
2804
2884
  if (input.message.timeoutMs) {
2805
2885
  timeout = setTimeout(() => {
2886
+ cancelWorkerCommandLaunch(input.resources);
2806
2887
  void terminateProcessTree(subprocess).catch((error) => {
2807
2888
  process.stderr.write(
2808
2889
  `[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
@@ -2811,21 +2892,25 @@ async function executeCommand(input) {
2811
2892
  });
2812
2893
  }, input.message.timeoutMs);
2813
2894
  }
2814
- const [stdout, stderr, exitCode] = await Promise.all([
2815
- collectStream(subprocess.stdout),
2816
- collectStream(subprocess.stderr),
2817
- subprocess.exited
2818
- ]);
2895
+ outputReaders = [
2896
+ collectWorkerInlineOutput(subprocess.stdout, void 0, outputAbort.signal),
2897
+ collectWorkerInlineOutput(subprocess.stderr, void 0, outputAbort.signal)
2898
+ ];
2899
+ const [stdout, stderr, exitCode] = await Promise.all([outputReaders[0], outputReaders[1], subprocess.exited]);
2900
+ const diagnostic = await input.resources.diagnose();
2819
2901
  return {
2820
2902
  type: "exec_result",
2821
2903
  requestId: input.message.requestId,
2822
2904
  stdout,
2823
2905
  stderr,
2824
2906
  exitCode,
2825
- durationMs: Date.now() - startedAt
2907
+ durationMs: Date.now() - startedAt,
2908
+ ...diagnostic ? { error: diagnostic } : {}
2826
2909
  };
2827
2910
  } catch (error) {
2828
- if (error instanceof StaleWorkerAdmissionError) throw error;
2911
+ outputAbort.abort(error);
2912
+ if (error instanceof StaleWorkerAdmissionError || error instanceof CommandLauncherUnavailableError) throw error;
2913
+ cancelWorkerCommandLaunch(input.resources);
2829
2914
  return {
2830
2915
  type: "exec_result",
2831
2916
  requestId: input.message.requestId,
@@ -2843,12 +2928,16 @@ async function executeCommand(input) {
2843
2928
  if (spawnedProcess) {
2844
2929
  await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
2845
2930
  }
2931
+ outputAbort.abort();
2932
+ await Promise.allSettled(outputReaders);
2933
+ await releaseWorkerCommandLaunch(input.resources);
2846
2934
  activeProcesses.delete(input.message.runId);
2847
2935
  }
2848
2936
  }
2849
2937
  async function executeStreamingCommand(input) {
2850
2938
  const startedAt = Date.now();
2851
2939
  let started = false;
2940
+ let terminal;
2852
2941
  let timedOut = false;
2853
2942
  let timeout;
2854
2943
  let spawnedProcess;
@@ -2860,7 +2949,7 @@ async function executeStreamingCommand(input) {
2860
2949
  };
2861
2950
  input.setPreparationHeartbeatLease(true);
2862
2951
  try {
2863
- if (cancelledProcessRuns.delete(input.message.runId)) {
2952
+ if (cancelledProcessRuns.has(input.message.runId)) {
2864
2953
  sendWorkerMessage(input.ws, {
2865
2954
  type: "exec_start_error",
2866
2955
  requestId: input.message.requestId,
@@ -2880,7 +2969,7 @@ async function executeStreamingCommand(input) {
2880
2969
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
2881
2970
  const interactive = input.message.interactive === true;
2882
2971
  input.assertAdmission();
2883
- const subprocess = Bun.spawn(input.message.argv, {
2972
+ const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
2884
2973
  cwd,
2885
2974
  // Without an explicit stdin the process reads /dev/null and interactive
2886
2975
  // prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
@@ -2923,6 +3012,7 @@ async function executeStreamingCommand(input) {
2923
3012
  if (input.message.timeoutMs) {
2924
3013
  timeout = setTimeout(() => {
2925
3014
  timedOut = true;
3015
+ cancelWorkerCommandLaunch(input.resources);
2926
3016
  closeProcessStdin(activeProcesses.get(input.message.runId));
2927
3017
  void terminateProcessTree(subprocess).catch((error) => {
2928
3018
  process.stderr.write(
@@ -2951,8 +3041,19 @@ async function executeStreamingCommand(input) {
2951
3041
  });
2952
3042
  })
2953
3043
  ]);
3044
+ const diagnostic = await input.resources.diagnose();
3045
+ if (diagnostic) {
3046
+ sendWorkerMessage(input.ws, {
3047
+ type: "exec_output",
3048
+ runId: input.message.runId,
3049
+ stream: "stderr",
3050
+ data: `
3051
+ [r5d-worker] ${diagnostic}
3052
+ `
3053
+ });
3054
+ }
2954
3055
  const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2955
- const terminal = {
3056
+ terminal = {
2956
3057
  type: "exec_exit",
2957
3058
  runId: input.message.runId,
2958
3059
  exitCode,
@@ -2961,14 +3062,12 @@ async function executeStreamingCommand(input) {
2961
3062
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2962
3063
  ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2963
3064
  };
2964
- pendingProcessTerminals.set(input.message.runId, terminal);
2965
- sendWorkerMessage(input.ws, terminal);
2966
3065
  } catch (error) {
2967
- if (!started && error instanceof StaleWorkerAdmissionError) throw error;
3066
+ if (error instanceof CommandLauncherUnavailableError || !started && error instanceof StaleWorkerAdmissionError) throw error;
2968
3067
  const message = error instanceof Error ? error.message : String(error);
2969
3068
  if (started) {
2970
3069
  const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2971
- const terminal = {
3070
+ terminal = {
2972
3071
  type: "exec_error",
2973
3072
  runId: input.message.runId,
2974
3073
  error: message,
@@ -2976,8 +3075,6 @@ async function executeStreamingCommand(input) {
2976
3075
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2977
3076
  ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2978
3077
  };
2979
- pendingProcessTerminals.set(input.message.runId, terminal);
2980
- sendWorkerMessage(input.ws, terminal);
2981
3078
  } else {
2982
3079
  sendWorkerMessage(input.ws, {
2983
3080
  type: "exec_start_error",
@@ -2995,9 +3092,14 @@ async function executeStreamingCommand(input) {
2995
3092
  if (spawnedProcess) {
2996
3093
  await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
2997
3094
  }
3095
+ await releaseWorkerCommandLaunch(input.resources);
2998
3096
  activeProcesses.delete(input.message.runId);
2999
3097
  cancelledProcessRuns.delete(input.message.runId);
3000
3098
  }
3099
+ if (terminal) {
3100
+ pendingProcessTerminals.set(input.message.runId, terminal);
3101
+ sendWorkerMessage(input.ws, terminal);
3102
+ }
3001
3103
  }
3002
3104
  async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
3003
3105
  try {
@@ -3037,6 +3139,7 @@ function sendReplayWorkerMessage(ws, message) {
3037
3139
  return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
3038
3140
  }
3039
3141
  function sendWorkerMessage(ws, message) {
3142
+ if (launcherFailureInProgress) return;
3040
3143
  if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
3041
3144
  return;
3042
3145
  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;
@@ -3380,6 +3483,7 @@ async function openPty(input) {
3380
3483
  }) : {};
3381
3484
  const targetEnv = targetProcessEnv(input.rootDir, input.resolvedTarget.target);
3382
3485
  const shell = resolveHostShell(input.message.command);
3486
+ const shellArgv = input.resources.wrap([shell.file, ...shell.args]);
3383
3487
  const outputCoalescer = createPtyOutputCoalescer({
3384
3488
  send: (data) => {
3385
3489
  sendWorkerMessage(input.ws, {
@@ -3397,10 +3501,33 @@ async function openPty(input) {
3397
3501
  envFilesRemoved = true;
3398
3502
  removePtyEnvFiles(stagedEnvFiles.paths);
3399
3503
  };
3504
+ let terminalHandled = false;
3505
+ const finishPty = async (terminal) => {
3506
+ if (terminalHandled) return;
3507
+ terminalHandled = true;
3508
+ try {
3509
+ const diagnostic = await input.resources.diagnose();
3510
+ if (diagnostic) outputCoalescer.push(`\r
3511
+ [r5d-worker] ${diagnostic}\r
3512
+ `);
3513
+ } finally {
3514
+ cleanupEnvFiles();
3515
+ await releaseWorkerCommandLaunch(input.resources);
3516
+ outputCoalescer.flush();
3517
+ activePtys.delete(input.message.ptyId);
3518
+ input.releaseWorkspaceMutation?.();
3519
+ input.onTerminal?.();
3520
+ }
3521
+ sendWorkerMessage(input.ws, terminal);
3522
+ };
3523
+ const reportPtyCleanupFailure = (error) => {
3524
+ process.stderr.write(`[r5d-worker] PTY launcher cleanup failed: ${error instanceof Error ? error.message : String(error)}
3525
+ `);
3526
+ };
3400
3527
  try {
3401
3528
  const ptyProcess = createNodePtyBridge({
3402
- file: shell.file,
3403
- args: shell.args,
3529
+ file: shellArgv[0],
3530
+ args: shellArgv.slice(1),
3404
3531
  ptyOptions: {
3405
3532
  name: "xterm-256color",
3406
3533
  cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
@@ -3429,30 +3556,14 @@ async function openPty(input) {
3429
3556
  outputCoalescer.push(data);
3430
3557
  },
3431
3558
  onExit: (event) => {
3432
- outputCoalescer.flush();
3433
- input.releaseWorkspaceMutation?.();
3434
- activePtys.delete(input.message.ptyId);
3435
- cleanupEnvFiles();
3436
- input.onTerminal?.();
3437
- sendWorkerMessage(input.ws, {
3438
- type: "pty_exit",
3439
- ptyId: input.message.ptyId,
3440
- exitCode: event.exitCode,
3441
- signal: event.signal
3442
- });
3559
+ void finishPty({ type: "pty_exit", ptyId: input.message.ptyId, exitCode: event.exitCode, signal: event.signal }).catch(
3560
+ reportPtyCleanupFailure
3561
+ );
3443
3562
  },
3444
3563
  onError: (error) => {
3445
- outputCoalescer.flush();
3446
- input.releaseWorkspaceMutation?.();
3447
- activePtys.delete(input.message.ptyId);
3448
- cleanupEnvFiles();
3449
- input.onTerminal?.();
3450
- sendWorkerMessage(input.ws, {
3451
- type: "pty_error",
3452
- requestId: input.message.requestId,
3453
- ptyId: input.message.ptyId,
3454
- error: error.message
3455
- });
3564
+ void finishPty({ type: "pty_error", requestId: input.message.requestId, ptyId: input.message.ptyId, error: error.message }).catch(
3565
+ reportPtyCleanupFailure
3566
+ );
3456
3567
  },
3457
3568
  onTerminationError: (error) => {
3458
3569
  sendWorkerMessage(input.ws, {
@@ -3495,7 +3606,12 @@ function resizePty(message) {
3495
3606
  }
3496
3607
  ptyProcess.resize(Math.max(1, Math.min(Math.floor(message.cols || 80), 500)), Math.max(1, Math.min(Math.floor(message.rows || 24), 500)));
3497
3608
  }
3498
- function closePty(message) {
3609
+ async function closePty(message) {
3610
+ try {
3611
+ await workerCommandLauncher?.cancel(`pty:${message.ptyId}`);
3612
+ } finally {
3613
+ activePtys.get(message.ptyId)?.kill();
3614
+ }
3499
3615
  const ptyProcess = activePtys.get(message.ptyId);
3500
3616
  if (!ptyProcess) {
3501
3617
  return;
@@ -3514,7 +3630,7 @@ function closePtysBestEffort(ptys, reportFailure) {
3514
3630
  function closeAllPtys() {
3515
3631
  closePtysBestEffort(activePtys, (ptyId, error) => {
3516
3632
  process.stderr.write(
3517
- `[r5d-worker] failed to close PTY ${ptyId} after control disconnect: ${error instanceof Error ? error.message : String(error)}
3633
+ `[r5d-worker] failed to close PTY ${ptyId} after execution lease expiry: ${error instanceof Error ? error.message : String(error)}
3518
3634
  `
3519
3635
  );
3520
3636
  });
@@ -3587,6 +3703,17 @@ async function startWorker(options, projectRuntime = {
3587
3703
  initializeCredentialStoreDirectoryOnce();
3588
3704
  const label = requireValue(options.label, "Missing required --label <label>");
3589
3705
  validateLabel(label);
3706
+ if (!workerCommandLauncher) {
3707
+ const launcher = await createWorkerCommandLauncher({
3708
+ configuration: process.env.R5D_WORKER_COMMAND_LAUNCHER,
3709
+ env: commandLauncherEnvironment(),
3710
+ onFailure: handleWorkerCommandLauncherFailure
3711
+ });
3712
+ workerCommandLauncher = launcher;
3713
+ await launcher.initialize();
3714
+ process.stdout.write(`[r5d-worker] command launcher: ${launcher.mode}
3715
+ `);
3716
+ }
3590
3717
  const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
3591
3718
  recoveryStore ??= new WorkerRecoveryStore(
3592
3719
  path.join(rootDir, ".worker-recovery", createHash("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
@@ -4827,7 +4954,7 @@ async function startWorker(options, projectRuntime = {
4827
4954
  `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4828
4955
  `
4829
4956
  );
4830
- process.exit(exitCode);
4957
+ void exitWorkerRuntime(exitCode);
4831
4958
  },
4832
4959
  exitCode: 1
4833
4960
  });
@@ -5638,7 +5765,7 @@ async function startWorker(options, projectRuntime = {
5638
5765
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
5639
5766
  ws.close(1e3, "Worker shutting down");
5640
5767
  } else {
5641
- process.exit(0);
5768
+ await exitWorkerRuntime(0);
5642
5769
  }
5643
5770
  })().catch((error) => {
5644
5771
  process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
@@ -5791,13 +5918,14 @@ async function startWorker(options, projectRuntime = {
5791
5918
  }
5792
5919
  if (message.type === "cancel_session") {
5793
5920
  recoveryStore.cancelSession(message.sessionId);
5794
- await Promise.all(
5795
- [...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
5921
+ await Promise.all([
5922
+ workerCommandLauncher?.cancelSession(message.sessionId),
5923
+ ...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
5796
5924
  cancelledProcessRuns.add(runId);
5797
5925
  closeProcessStdin(active);
5798
5926
  await terminateProcessTree(active.process);
5799
5927
  })
5800
- );
5928
+ ]);
5801
5929
  sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
5802
5930
  return;
5803
5931
  }
@@ -6244,7 +6372,7 @@ async function startWorker(options, projectRuntime = {
6244
6372
  });
6245
6373
  return;
6246
6374
  }
6247
- if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
6375
+ if (activeProcesses.size > 0 || (workerCommandLauncher?.activeIds.length ?? 0) > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
6248
6376
  sendWorkerMessage(ws, {
6249
6377
  type: "update_clis_result",
6250
6378
  requestId: message.requestId,
@@ -6277,8 +6405,14 @@ async function startWorker(options, projectRuntime = {
6277
6405
  return;
6278
6406
  }
6279
6407
  if (message.type === "cancel") {
6408
+ let resourceCancellationError;
6409
+ try {
6410
+ await workerCommandLauncher?.cancel(message.runId);
6411
+ } catch (error) {
6412
+ resourceCancellationError = error instanceof Error ? error.message : String(error);
6413
+ }
6280
6414
  const active = activeProcesses.get(message.runId);
6281
- let cancelled = true;
6415
+ let cancelled = resourceCancellationError === void 0;
6282
6416
  let cancelMessage;
6283
6417
  if (active) {
6284
6418
  try {
@@ -6293,6 +6427,7 @@ async function startWorker(options, projectRuntime = {
6293
6427
  cancelledProcessRuns.add(message.runId);
6294
6428
  cancelMessage = "Cancellation queued before command start";
6295
6429
  }
6430
+ if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
6296
6431
  sendSerializedWorkerMessage(
6297
6432
  ws,
6298
6433
  JSON.stringify({
@@ -6379,13 +6514,16 @@ async function startWorker(options, projectRuntime = {
6379
6514
  });
6380
6515
  return;
6381
6516
  }
6382
- await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6383
- workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6384
- if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6385
- });
6517
+ let resources;
6518
+ let resourcesTransferred = false;
6386
6519
  let releaseWorkspaceMutation;
6387
6520
  let mutationLeaseTransferred = false;
6388
6521
  try {
6522
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6523
+ await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6524
+ workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6525
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6526
+ });
6389
6527
  releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
6390
6528
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
6391
6529
  throw new Error(
@@ -6396,6 +6534,7 @@ async function startWorker(options, projectRuntime = {
6396
6534
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
6397
6535
  `);
6398
6536
  await openPty({
6537
+ resources,
6399
6538
  ws,
6400
6539
  message,
6401
6540
  resolvedTarget,
@@ -6408,6 +6547,7 @@ async function startWorker(options, projectRuntime = {
6408
6547
  }
6409
6548
  } : {}
6410
6549
  });
6550
+ resourcesTransferred = true;
6411
6551
  mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
6412
6552
  } catch (error) {
6413
6553
  sendWorkerMessageFromCurrentSource(ws, {
@@ -6418,6 +6558,7 @@ async function startWorker(options, projectRuntime = {
6418
6558
  });
6419
6559
  } finally {
6420
6560
  workspaceSyncPriorityPtyTargets.delete(message.ptyId);
6561
+ if (!resourcesTransferred) await releaseWorkerCommandLaunch(resources);
6421
6562
  if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
6422
6563
  }
6423
6564
  return;
@@ -6447,7 +6588,7 @@ async function startWorker(options, projectRuntime = {
6447
6588
  }
6448
6589
  if (message.type === "pty_close") {
6449
6590
  const activePty = activePtys.get(message.ptyId);
6450
- closePty(message);
6591
+ await closePty(message);
6451
6592
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
6452
6593
  markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
6453
6594
  }
@@ -6477,9 +6618,11 @@ async function startWorker(options, projectRuntime = {
6477
6618
  return;
6478
6619
  }
6479
6620
  let result;
6621
+ let resources;
6480
6622
  let targetReserved = false;
6481
6623
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
6482
6624
  try {
6625
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6483
6626
  if (hasWorkspaceEffect) {
6484
6627
  await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6485
6628
  workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
@@ -6497,6 +6640,7 @@ async function startWorker(options, projectRuntime = {
6497
6640
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
6498
6641
  `);
6499
6642
  return await executeCommand({
6643
+ resources,
6500
6644
  message,
6501
6645
  resolvedTarget,
6502
6646
  baseUrl,
@@ -6532,6 +6676,8 @@ async function startWorker(options, projectRuntime = {
6532
6676
  };
6533
6677
  } finally {
6534
6678
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
6679
+ await releaseWorkerCommandLaunch(resources);
6680
+ cancelledProcessRuns.delete(message.runId);
6535
6681
  }
6536
6682
  if (infrastructureStoppedRunIds.has(message.runId)) {
6537
6683
  recoveryStore.unknown(message.requestId);
@@ -6563,12 +6709,14 @@ async function startWorker(options, projectRuntime = {
6563
6709
  runId: message.runId
6564
6710
  });
6565
6711
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
6712
+ let resources;
6566
6713
  const runCommand = async () => {
6567
6714
  try {
6568
6715
  const resolvedTarget = resolveMessageTarget(message.target);
6569
6716
  process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
6570
6717
  `);
6571
6718
  await executeStreamingCommand({
6719
+ resources,
6572
6720
  ws,
6573
6721
  message,
6574
6722
  resolvedTarget,
@@ -6602,6 +6750,7 @@ async function startWorker(options, projectRuntime = {
6602
6750
  const execution = (async () => {
6603
6751
  let targetReserved = false;
6604
6752
  try {
6753
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6605
6754
  if (hasWorkspaceEffect) {
6606
6755
  await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6607
6756
  workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
@@ -6617,6 +6766,8 @@ async function startWorker(options, projectRuntime = {
6617
6766
  });
6618
6767
  } finally {
6619
6768
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
6769
+ await releaseWorkerCommandLaunch(resources);
6770
+ cancelledProcessRuns.delete(message.runId);
6620
6771
  }
6621
6772
  })();
6622
6773
  void execution.catch((error) => {
@@ -6747,11 +6898,10 @@ async function startWorker(options, projectRuntime = {
6747
6898
  const reason = event.reason ? `: ${event.reason}` : "";
6748
6899
  process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
6749
6900
  `);
6750
- if (reloadAfterClose) {
6751
- process.exit(WORKER_RELOAD_EXIT_CODE);
6752
- }
6753
- if (shutdownAfterClose) {
6754
- process.exit(0);
6901
+ if (launcherFailureInProgress) return;
6902
+ if (reloadAfterClose || shutdownAfterClose) {
6903
+ void exitWorkerRuntime(reloadAfterClose ? WORKER_RELOAD_EXIT_CODE : 0);
6904
+ return;
6755
6905
  }
6756
6906
  const reconnect = () => {
6757
6907
  const delay = workerReconnectDelayMs(Math.random, workerReconnectAttempt++);
@@ -6809,10 +6959,10 @@ async function main() {
6809
6959
  try {
6810
6960
  await startWorker(parsed.options);
6811
6961
  } catch (error) {
6812
- if (error instanceof WorkerServerUnavailableError) {
6962
+ if (error instanceof WorkerServerUnavailableError || error instanceof CommandLauncherUnavailableError) {
6813
6963
  process.stderr.write(`[r5d-worker] ${error.message}
6814
6964
  `);
6815
- process.exit(WORKER_RECONNECT_EXIT_CODE);
6965
+ await exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
6816
6966
  }
6817
6967
  throw error;
6818
6968
  }
@@ -6830,10 +6980,10 @@ function isCliEntrypoint() {
6830
6980
  return typeof require === "function" && typeof module !== "undefined" && require.main === module;
6831
6981
  }
6832
6982
  if (isCliEntrypoint()) {
6833
- main().catch((error) => {
6983
+ main().catch(async (error) => {
6834
6984
  process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
6835
6985
  `);
6836
- process.exit(1);
6986
+ await exitWorkerRuntime(1);
6837
6987
  });
6838
6988
  }
6839
6989
  export {
@@ -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": "module"
5
5
  }
@@ -22,7 +22,7 @@ function workerReconnectDelayMs(random = Math.random, attempt = 0) {
22
22
  return floor + Math.floor(sample * (ceiling - floor));
23
23
  }
24
24
  async function defaultRuntimeReaper(runtime) {
25
- await terminateProcessTree(runtime);
25
+ await terminateProcessTree(runtime, { graceMs: 6e3 });
26
26
  }
27
27
  function isRetryableWorkerServerStatus(status) {
28
28
  return status === 408 || status === 429 || status >= 500;