@ricsam/r5d-worker 0.0.111 → 0.0.113

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
@@ -6,10 +6,12 @@ import fs from "node:fs";
6
6
  import { WorkerRecoveryStore, workerOperationDigest } from "./recovery-store.mjs";
7
7
  import { WorkerOutputWindow } from "./recovery-output-window.mjs";
8
8
  import {
9
- createWorkerJobResources,
10
- collectWorkerInlineOutput,
11
- WorkerJobCancelledError
12
- } from "./job-resources.mjs";
9
+ createWorkerCommandLauncher,
10
+ commandLauncherEnvironment,
11
+ CommandLaunchCancelledError,
12
+ CommandLauncherUnavailableError
13
+ } from "./command-launcher.mjs";
14
+ import { collectWorkerInlineOutput } from "./inline-output.mjs";
13
15
  import {
14
16
  WORKER_RESUMABLE_PROTOCOL,
15
17
  WORKER_RECOVERY_TIMEOUT_MS
@@ -173,7 +175,7 @@ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
173
175
  const PTY_FOREGROUND_POLL_MS = 1e3;
174
176
  const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
175
177
  const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
176
- let workerJobResources;
178
+ let workerCommandLauncher;
177
179
  const activeProcesses = /* @__PURE__ */ new Map();
178
180
  const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
179
181
  const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
@@ -193,6 +195,41 @@ let workerExecutionLeaseDeadline = 0;
193
195
  let workerExecutionLeaseTimer;
194
196
  let workerLeaseExpired = false;
195
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
+ }
196
233
  function isWorkerCommunicationFailure(message) {
197
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(
198
235
  message
@@ -222,22 +259,18 @@ function renewWorkerExecutionLease(leaseMs) {
222
259
  () => {
223
260
  if (Date.now() < workerExecutionLeaseDeadline) return;
224
261
  workerLeaseExpired = true;
225
- try {
226
- workerJobResources?.cancelMatching((id) => id.startsWith("pty:"));
227
- } catch (error) {
228
- process.stderr.write(`[r5d-worker] expired PTY resource termination failed: ${error}
262
+ void workerCommandLauncher?.cancelMatching((id) => id.startsWith("pty:")).catch((error) => {
263
+ process.stderr.write(`[r5d-worker] expired PTY launcher cancellation failed: ${error}
229
264
  `);
230
- }
265
+ });
231
266
  closeAllPtys();
232
267
  for (const operation of recoveryStore?.operations() ?? []) {
233
268
  if (operation.sessionId && operation.state !== "completed") {
234
269
  recoveryStore?.cancelSession(operation.sessionId);
235
- try {
236
- workerJobResources?.cancelSession(operation.sessionId);
237
- } catch (error) {
238
- process.stderr.write(`[r5d-worker] expired command resource termination failed: ${error}
270
+ void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
271
+ process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
239
272
  `);
240
- }
273
+ });
241
274
  }
242
275
  }
243
276
  for (const [runId, active] of activeProcesses) {
@@ -1602,7 +1635,9 @@ async function fenceCredentialGenerationAtConfigureEntry(input) {
1602
1635
  await input.terminatePreviousGeneration();
1603
1636
  return true;
1604
1637
  }
1605
- function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => process.exit(exitCode2), exitCode = 1) {
1638
+ function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => {
1639
+ void exitWorkerRuntime(exitCode2);
1640
+ }, exitCode = 1) {
1606
1641
  if (!required) return;
1607
1642
  fatalExit(exitCode);
1608
1643
  throw new Error("Credential generation restart callback returned unexpectedly");
@@ -2768,27 +2803,28 @@ async function prepareShellEnvForTarget(input) {
2768
2803
  ...targetProcessEnv(input.rootDir, input.target)
2769
2804
  };
2770
2805
  }
2771
- async function releaseWorkerJobResources(resources) {
2772
- if (!resources) return;
2773
- try {
2774
- await resources.release();
2775
- } catch (error) {
2776
- process.stderr.write(`[r5d-worker] job resource cleanup failed: ${error instanceof Error ? error.message : String(error)}
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)}
2777
2812
  `);
2778
- }
2813
+ });
2779
2814
  }
2780
- async function acquireWorkerCommandResources(ws, message, assertAdmission) {
2815
+ async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
2781
2816
  const id = message.type === "pty_open" ? `pty:${message.ptyId}` : message.runId;
2782
2817
  const operationId = message.type === "pty_open" ? id : `exec:${id}`;
2783
2818
  sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: true });
2784
2819
  try {
2785
- return await workerJobResources.acquire({
2820
+ return await workerCommandLauncher.acquire({
2786
2821
  id,
2787
- lane: message.type === "exec" && message.workspaceEffect === "none" ? "utility" : "general",
2822
+ kind: message.type,
2823
+ ..."workspaceEffect" in message && message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2788
2824
  ..."sessionId" in message && message.sessionId ? { sessionId: message.sessionId } : {},
2789
2825
  assertAdmission: () => {
2790
2826
  assertAdmission();
2791
- if (cancelledProcessRuns.has(id)) throw new WorkerJobCancelledError();
2827
+ if (cancelledProcessRuns.has(id)) throw new CommandLaunchCancelledError();
2792
2828
  }
2793
2829
  });
2794
2830
  } finally {
@@ -2847,7 +2883,7 @@ async function executeCommand(input) {
2847
2883
  settlePreparation();
2848
2884
  if (input.message.timeoutMs) {
2849
2885
  timeout = setTimeout(() => {
2850
- input.resources.kill();
2886
+ cancelWorkerCommandLaunch(input.resources);
2851
2887
  void terminateProcessTree(subprocess).catch((error) => {
2852
2888
  process.stderr.write(
2853
2889
  `[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
@@ -2861,6 +2897,7 @@ async function executeCommand(input) {
2861
2897
  collectWorkerInlineOutput(subprocess.stderr, void 0, outputAbort.signal)
2862
2898
  ];
2863
2899
  const [stdout, stderr, exitCode] = await Promise.all([outputReaders[0], outputReaders[1], subprocess.exited]);
2900
+ const diagnostic = await input.resources.diagnose();
2864
2901
  return {
2865
2902
  type: "exec_result",
2866
2903
  requestId: input.message.requestId,
@@ -2868,14 +2905,12 @@ async function executeCommand(input) {
2868
2905
  stderr,
2869
2906
  exitCode,
2870
2907
  durationMs: Date.now() - startedAt,
2871
- ...input.resources.memoryLimitExceeded() ? {
2872
- error: `Command exceeded its worker memory limit (${input.resources.memoryBytes} bytes). Reduce parallelism or memory use and retry.`
2873
- } : {}
2908
+ ...diagnostic ? { error: diagnostic } : {}
2874
2909
  };
2875
2910
  } catch (error) {
2876
2911
  outputAbort.abort(error);
2877
- if (error instanceof StaleWorkerAdmissionError) throw error;
2878
- input.resources.kill();
2912
+ if (error instanceof StaleWorkerAdmissionError || error instanceof CommandLauncherUnavailableError) throw error;
2913
+ cancelWorkerCommandLaunch(input.resources);
2879
2914
  return {
2880
2915
  type: "exec_result",
2881
2916
  requestId: input.message.requestId,
@@ -2895,12 +2930,14 @@ async function executeCommand(input) {
2895
2930
  }
2896
2931
  outputAbort.abort();
2897
2932
  await Promise.allSettled(outputReaders);
2933
+ await releaseWorkerCommandLaunch(input.resources);
2898
2934
  activeProcesses.delete(input.message.runId);
2899
2935
  }
2900
2936
  }
2901
2937
  async function executeStreamingCommand(input) {
2902
2938
  const startedAt = Date.now();
2903
2939
  let started = false;
2940
+ let terminal;
2904
2941
  let timedOut = false;
2905
2942
  let timeout;
2906
2943
  let spawnedProcess;
@@ -2912,7 +2949,7 @@ async function executeStreamingCommand(input) {
2912
2949
  };
2913
2950
  input.setPreparationHeartbeatLease(true);
2914
2951
  try {
2915
- if (cancelledProcessRuns.delete(input.message.runId)) {
2952
+ if (cancelledProcessRuns.has(input.message.runId)) {
2916
2953
  sendWorkerMessage(input.ws, {
2917
2954
  type: "exec_start_error",
2918
2955
  requestId: input.message.requestId,
@@ -2975,7 +3012,7 @@ async function executeStreamingCommand(input) {
2975
3012
  if (input.message.timeoutMs) {
2976
3013
  timeout = setTimeout(() => {
2977
3014
  timedOut = true;
2978
- input.resources.kill();
3015
+ cancelWorkerCommandLaunch(input.resources);
2979
3016
  closeProcessStdin(activeProcesses.get(input.message.runId));
2980
3017
  void terminateProcessTree(subprocess).catch((error) => {
2981
3018
  process.stderr.write(
@@ -3004,18 +3041,19 @@ async function executeStreamingCommand(input) {
3004
3041
  });
3005
3042
  })
3006
3043
  ]);
3007
- if (input.resources.memoryLimitExceeded()) {
3044
+ const diagnostic = await input.resources.diagnose();
3045
+ if (diagnostic) {
3008
3046
  sendWorkerMessage(input.ws, {
3009
3047
  type: "exec_output",
3010
3048
  runId: input.message.runId,
3011
3049
  stream: "stderr",
3012
3050
  data: `
3013
- [r5d-worker] Command exceeded its memory limit (${input.resources.memoryBytes} bytes). Reduce parallelism or memory use and retry.
3051
+ [r5d-worker] ${diagnostic}
3014
3052
  `
3015
3053
  });
3016
3054
  }
3017
3055
  const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
3018
- const terminal = {
3056
+ terminal = {
3019
3057
  type: "exec_exit",
3020
3058
  runId: input.message.runId,
3021
3059
  exitCode,
@@ -3024,14 +3062,12 @@ async function executeStreamingCommand(input) {
3024
3062
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
3025
3063
  ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
3026
3064
  };
3027
- pendingProcessTerminals.set(input.message.runId, terminal);
3028
- sendWorkerMessage(input.ws, terminal);
3029
3065
  } catch (error) {
3030
- if (!started && error instanceof StaleWorkerAdmissionError) throw error;
3066
+ if (error instanceof CommandLauncherUnavailableError || !started && error instanceof StaleWorkerAdmissionError) throw error;
3031
3067
  const message = error instanceof Error ? error.message : String(error);
3032
3068
  if (started) {
3033
3069
  const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
3034
- const terminal = {
3070
+ terminal = {
3035
3071
  type: "exec_error",
3036
3072
  runId: input.message.runId,
3037
3073
  error: message,
@@ -3039,8 +3075,6 @@ async function executeStreamingCommand(input) {
3039
3075
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
3040
3076
  ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
3041
3077
  };
3042
- pendingProcessTerminals.set(input.message.runId, terminal);
3043
- sendWorkerMessage(input.ws, terminal);
3044
3078
  } else {
3045
3079
  sendWorkerMessage(input.ws, {
3046
3080
  type: "exec_start_error",
@@ -3058,9 +3092,14 @@ async function executeStreamingCommand(input) {
3058
3092
  if (spawnedProcess) {
3059
3093
  await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
3060
3094
  }
3095
+ await releaseWorkerCommandLaunch(input.resources);
3061
3096
  activeProcesses.delete(input.message.runId);
3062
3097
  cancelledProcessRuns.delete(input.message.runId);
3063
3098
  }
3099
+ if (terminal) {
3100
+ pendingProcessTerminals.set(input.message.runId, terminal);
3101
+ sendWorkerMessage(input.ws, terminal);
3102
+ }
3064
3103
  }
3065
3104
  async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
3066
3105
  try {
@@ -3100,6 +3139,7 @@ function sendReplayWorkerMessage(ws, message) {
3100
3139
  return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
3101
3140
  }
3102
3141
  function sendWorkerMessage(ws, message) {
3142
+ if (launcherFailureInProgress) return;
3103
3143
  if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
3104
3144
  return;
3105
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;
@@ -3461,6 +3501,29 @@ async function openPty(input) {
3461
3501
  envFilesRemoved = true;
3462
3502
  removePtyEnvFiles(stagedEnvFiles.paths);
3463
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
+ };
3464
3527
  try {
3465
3528
  const ptyProcess = createNodePtyBridge({
3466
3529
  file: shellArgv[0],
@@ -3493,38 +3556,14 @@ async function openPty(input) {
3493
3556
  outputCoalescer.push(data);
3494
3557
  },
3495
3558
  onExit: (event) => {
3496
- if (input.resources.memoryLimitExceeded())
3497
- outputCoalescer.push(
3498
- `\r
3499
- [r5d-worker] Terminal exceeded its memory limit (${input.resources.memoryBytes} bytes). Reduce parallelism or memory use and retry.\r
3500
- `
3501
- );
3502
- outputCoalescer.flush();
3503
- input.releaseWorkspaceMutation?.();
3504
- activePtys.delete(input.message.ptyId);
3505
- cleanupEnvFiles();
3506
- input.onTerminal?.();
3507
- void releaseWorkerJobResources(input.resources);
3508
- sendWorkerMessage(input.ws, {
3509
- type: "pty_exit",
3510
- ptyId: input.message.ptyId,
3511
- exitCode: event.exitCode,
3512
- signal: event.signal
3513
- });
3559
+ void finishPty({ type: "pty_exit", ptyId: input.message.ptyId, exitCode: event.exitCode, signal: event.signal }).catch(
3560
+ reportPtyCleanupFailure
3561
+ );
3514
3562
  },
3515
3563
  onError: (error) => {
3516
- outputCoalescer.flush();
3517
- input.releaseWorkspaceMutation?.();
3518
- activePtys.delete(input.message.ptyId);
3519
- cleanupEnvFiles();
3520
- input.onTerminal?.();
3521
- void releaseWorkerJobResources(input.resources);
3522
- sendWorkerMessage(input.ws, {
3523
- type: "pty_error",
3524
- requestId: input.message.requestId,
3525
- ptyId: input.message.ptyId,
3526
- error: error.message
3527
- });
3564
+ void finishPty({ type: "pty_error", requestId: input.message.requestId, ptyId: input.message.ptyId, error: error.message }).catch(
3565
+ reportPtyCleanupFailure
3566
+ );
3528
3567
  },
3529
3568
  onTerminationError: (error) => {
3530
3569
  sendWorkerMessage(input.ws, {
@@ -3567,8 +3606,12 @@ function resizePty(message) {
3567
3606
  }
3568
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)));
3569
3608
  }
3570
- function closePty(message) {
3571
- workerJobResources?.cancel(`pty:${message.ptyId}`);
3609
+ async function closePty(message) {
3610
+ try {
3611
+ await workerCommandLauncher?.cancel(`pty:${message.ptyId}`);
3612
+ } finally {
3613
+ activePtys.get(message.ptyId)?.kill();
3614
+ }
3572
3615
  const ptyProcess = activePtys.get(message.ptyId);
3573
3616
  if (!ptyProcess) {
3574
3617
  return;
@@ -3660,9 +3703,15 @@ async function startWorker(options, projectRuntime = {
3660
3703
  initializeCredentialStoreDirectoryOnce();
3661
3704
  const label = requireValue(options.label, "Missing required --label <label>");
3662
3705
  validateLabel(label);
3663
- if (!workerJobResources) {
3664
- workerJobResources = createWorkerJobResources();
3665
- process.stdout.write(`[r5d-worker] job resources: ${workerJobResources.isolation}; ${JSON.stringify(workerJobResources.budget)}
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}
3666
3715
  `);
3667
3716
  }
3668
3717
  const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
@@ -4905,7 +4954,7 @@ async function startWorker(options, projectRuntime = {
4905
4954
  `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4906
4955
  `
4907
4956
  );
4908
- process.exit(exitCode);
4957
+ void exitWorkerRuntime(exitCode);
4909
4958
  },
4910
4959
  exitCode: 1
4911
4960
  });
@@ -5716,7 +5765,7 @@ async function startWorker(options, projectRuntime = {
5716
5765
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
5717
5766
  ws.close(1e3, "Worker shutting down");
5718
5767
  } else {
5719
- process.exit(0);
5768
+ await exitWorkerRuntime(0);
5720
5769
  }
5721
5770
  })().catch((error) => {
5722
5771
  process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
@@ -5869,14 +5918,14 @@ async function startWorker(options, projectRuntime = {
5869
5918
  }
5870
5919
  if (message.type === "cancel_session") {
5871
5920
  recoveryStore.cancelSession(message.sessionId);
5872
- workerJobResources?.cancelSession(message.sessionId);
5873
- await Promise.all(
5874
- [...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]) => {
5875
5924
  cancelledProcessRuns.add(runId);
5876
5925
  closeProcessStdin(active);
5877
5926
  await terminateProcessTree(active.process);
5878
5927
  })
5879
- );
5928
+ ]);
5880
5929
  sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
5881
5930
  return;
5882
5931
  }
@@ -6323,7 +6372,7 @@ async function startWorker(options, projectRuntime = {
6323
6372
  });
6324
6373
  return;
6325
6374
  }
6326
- 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) {
6327
6376
  sendWorkerMessage(ws, {
6328
6377
  type: "update_clis_result",
6329
6378
  requestId: message.requestId,
@@ -6358,7 +6407,7 @@ async function startWorker(options, projectRuntime = {
6358
6407
  if (message.type === "cancel") {
6359
6408
  let resourceCancellationError;
6360
6409
  try {
6361
- workerJobResources?.cancel(message.runId);
6410
+ await workerCommandLauncher?.cancel(message.runId);
6362
6411
  } catch (error) {
6363
6412
  resourceCancellationError = error instanceof Error ? error.message : String(error);
6364
6413
  }
@@ -6470,7 +6519,7 @@ async function startWorker(options, projectRuntime = {
6470
6519
  let releaseWorkspaceMutation;
6471
6520
  let mutationLeaseTransferred = false;
6472
6521
  try {
6473
- resources = await acquireWorkerCommandResources(ws, message, assertMessageAdmission);
6522
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6474
6523
  await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6475
6524
  workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6476
6525
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
@@ -6509,8 +6558,8 @@ async function startWorker(options, projectRuntime = {
6509
6558
  });
6510
6559
  } finally {
6511
6560
  workspaceSyncPriorityPtyTargets.delete(message.ptyId);
6561
+ if (!resourcesTransferred) await releaseWorkerCommandLaunch(resources);
6512
6562
  if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
6513
- if (!resourcesTransferred) await releaseWorkerJobResources(resources);
6514
6563
  }
6515
6564
  return;
6516
6565
  }
@@ -6539,7 +6588,7 @@ async function startWorker(options, projectRuntime = {
6539
6588
  }
6540
6589
  if (message.type === "pty_close") {
6541
6590
  const activePty = activePtys.get(message.ptyId);
6542
- closePty(message);
6591
+ await closePty(message);
6543
6592
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
6544
6593
  markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
6545
6594
  }
@@ -6573,7 +6622,7 @@ async function startWorker(options, projectRuntime = {
6573
6622
  let targetReserved = false;
6574
6623
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
6575
6624
  try {
6576
- resources = await acquireWorkerCommandResources(ws, message, assertMessageAdmission);
6625
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6577
6626
  if (hasWorkspaceEffect) {
6578
6627
  await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6579
6628
  workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
@@ -6627,7 +6676,7 @@ async function startWorker(options, projectRuntime = {
6627
6676
  };
6628
6677
  } finally {
6629
6678
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
6630
- await releaseWorkerJobResources(resources);
6679
+ await releaseWorkerCommandLaunch(resources);
6631
6680
  cancelledProcessRuns.delete(message.runId);
6632
6681
  }
6633
6682
  if (infrastructureStoppedRunIds.has(message.runId)) {
@@ -6701,7 +6750,7 @@ async function startWorker(options, projectRuntime = {
6701
6750
  const execution = (async () => {
6702
6751
  let targetReserved = false;
6703
6752
  try {
6704
- resources = await acquireWorkerCommandResources(ws, message, assertMessageAdmission);
6753
+ resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6705
6754
  if (hasWorkspaceEffect) {
6706
6755
  await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
6707
6756
  workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
@@ -6717,7 +6766,7 @@ async function startWorker(options, projectRuntime = {
6717
6766
  });
6718
6767
  } finally {
6719
6768
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
6720
- await releaseWorkerJobResources(resources);
6769
+ await releaseWorkerCommandLaunch(resources);
6721
6770
  cancelledProcessRuns.delete(message.runId);
6722
6771
  }
6723
6772
  })();
@@ -6849,11 +6898,10 @@ async function startWorker(options, projectRuntime = {
6849
6898
  const reason = event.reason ? `: ${event.reason}` : "";
6850
6899
  process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
6851
6900
  `);
6852
- if (reloadAfterClose) {
6853
- process.exit(WORKER_RELOAD_EXIT_CODE);
6854
- }
6855
- if (shutdownAfterClose) {
6856
- process.exit(0);
6901
+ if (launcherFailureInProgress) return;
6902
+ if (reloadAfterClose || shutdownAfterClose) {
6903
+ void exitWorkerRuntime(reloadAfterClose ? WORKER_RELOAD_EXIT_CODE : 0);
6904
+ return;
6857
6905
  }
6858
6906
  const reconnect = () => {
6859
6907
  const delay = workerReconnectDelayMs(Math.random, workerReconnectAttempt++);
@@ -6911,10 +6959,10 @@ async function main() {
6911
6959
  try {
6912
6960
  await startWorker(parsed.options);
6913
6961
  } catch (error) {
6914
- if (error instanceof WorkerServerUnavailableError) {
6962
+ if (error instanceof WorkerServerUnavailableError || error instanceof CommandLauncherUnavailableError) {
6915
6963
  process.stderr.write(`[r5d-worker] ${error.message}
6916
6964
  `);
6917
- process.exit(WORKER_RECONNECT_EXIT_CODE);
6965
+ await exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
6918
6966
  }
6919
6967
  throw error;
6920
6968
  }
@@ -6932,10 +6980,10 @@ function isCliEntrypoint() {
6932
6980
  return typeof require === "function" && typeof module !== "undefined" && require.main === module;
6933
6981
  }
6934
6982
  if (isCliEntrypoint()) {
6935
- main().catch((error) => {
6983
+ main().catch(async (error) => {
6936
6984
  process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
6937
6985
  `);
6938
- process.exit(1);
6986
+ await exitWorkerRuntime(1);
6939
6987
  });
6940
6988
  }
6941
6989
  export {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.111",
3
+ "version": "0.0.113",
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;
@@ -0,0 +1,61 @@
1
+ export type CommandLaunchRequest = {
2
+ id: string;
3
+ kind: "exec" | "exec_start" | "pty_open";
4
+ workspaceEffect?: "none";
5
+ sessionId?: string;
6
+ assertAdmission: () => void;
7
+ };
8
+ export type CommandLaunchLease = {
9
+ wrap(argv: string[]): string[];
10
+ cancel(): Promise<void>;
11
+ diagnose(): Promise<string | undefined>;
12
+ release(): Promise<void>;
13
+ };
14
+ export declare class CommandLauncherUnavailableError extends Error {
15
+ constructor(message: string, cause?: unknown);
16
+ }
17
+ export declare class CommandLaunchCancelledError extends Error {
18
+ constructor();
19
+ }
20
+ export declare function parseCommandLauncher(configuration: string | undefined): string[] | undefined;
21
+ /** Launcher configuration is operator-owned; do not inherit worker or package credentials. */
22
+ export declare function commandLauncherEnvironment(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
23
+ /** Generic optional JSONL controller; policy and OS-specific launch prefixes live outside the worker. */
24
+ export declare class WorkerCommandLauncher {
25
+ private readonly options;
26
+ readonly mode: "direct" | "external";
27
+ private readonly child?;
28
+ private readonly pending;
29
+ private readonly launches;
30
+ private readonly exited;
31
+ private failure?;
32
+ private closing;
33
+ private initialized;
34
+ private closePromise?;
35
+ private sequence;
36
+ constructor(options?: {
37
+ argv?: string[];
38
+ env?: NodeJS.ProcessEnv;
39
+ onFailure?: (error: Error, ids: string[]) => void;
40
+ onStderr?: (text: string) => void;
41
+ requestTimeoutMs?: number;
42
+ acquisitionTimeoutMs?: number;
43
+ });
44
+ initialize(): Promise<void>;
45
+ get activeIds(): string[];
46
+ acquire(request: CommandLaunchRequest): Promise<CommandLaunchLease>;
47
+ cancel(id: string): Promise<void>;
48
+ cancelSession(sessionId: string): Promise<void>;
49
+ cancelMatching(matches: (id: string, sessionId?: string) => boolean): Promise<void>;
50
+ close(): Promise<void>;
51
+ private waitForExit;
52
+ private releaseRecord;
53
+ private assertHealthy;
54
+ private rpc;
55
+ private receive;
56
+ private rejectPending;
57
+ private fail;
58
+ }
59
+ export declare function createWorkerCommandLauncher(options?: Omit<NonNullable<ConstructorParameters<typeof WorkerCommandLauncher>[0]>, "argv"> & {
60
+ configuration?: string;
61
+ }): Promise<WorkerCommandLauncher>;
@@ -0,0 +1,3 @@
1
+ export declare const WORKER_INLINE_OUTPUT_MAX_BYTES: number;
2
+ /** Bound inline RPC buffers in the worker regardless of child launch policy. */
3
+ export declare function collectWorkerInlineOutput(stream: ReadableStream<Uint8Array> | null, maxBytes?: number, signal?: AbortSignal): Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.111",
3
+ "version": "0.0.113",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",