@ricsam/r5d-worker 0.0.91 → 0.0.92

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/README.md CHANGED
@@ -43,7 +43,7 @@ The supervisor also reconnects automatically after server rollouts and transient
43
43
 
44
44
  The runtime opens its control connection before recovering interrupted project snapshots. During recovery and initial project configuration it remains connected but non-dispatchable, reports its current phase plus branch/byte progress to the server, and prints the same progress locally. Recovery runs in a credential-sanitized helper process so the control loop can continue answering heartbeats. A durable `restoring` transaction state permits same-filesystem snapshots to be restored with one atomic directory move; cross-filesystem layouts retain the copy-and-fsync fallback.
45
45
 
46
- The server registry is the sole heartbeat timeout authority. The worker answers server pings but does not run an independent connection watchdog, because synchronous bounded workspace operations can temporarily occupy its event loop. The server keeps those operations leased for their bounded lifetime and closes genuinely stale connections after the lease ends.
46
+ Both sides use monotonic, two-stage heartbeat recovery. The server sends pings, grants bounded leases to synchronous workspace and command-preparation work, and requires a fresh missed probe window before force-evicting a stale socket. The worker answers pings and uses a deliberately slower backup watchdog only for a half-open connection; reconnect delays are jittered across workers. A delayed watchdog sweep renews the probe instead of disconnecting, so synchronous local work or a stalled server event loop cannot turn one overdue timer callback into a mass disconnect.
47
47
 
48
48
  Credential-generation restart fencing protects against stale or accidentally detached descendants. It is not a hostile same-UID sandbox: code running as the worker's OS user can read or tamper with that user's config and auth files, install a persistent user service, or return after reboot. Run untrusted agents in a disposable VM that is reset at the boundary, or use a separately privileged credential broker/service account. When rotating the worker's own local `r5dctl` credential, stop the worker and reboot/reset the host before writing the replacement credential so an old same-UID descendant cannot read the new config.
49
49
 
package/dist/cjs/main.cjs CHANGED
@@ -50,6 +50,7 @@ __export(main_exports, {
50
50
  syncSessionArtifacts: () => syncSessionArtifacts,
51
51
  workerChildProcessEnvironment: () => workerChildProcessEnvironment,
52
52
  workerGitSecurityTestHarness: () => workerGitSecurityTestHarness,
53
+ workerLifecycleTestHarness: () => workerLifecycleTestHarness,
53
54
  workerPtyBridgeTestHarness: () => workerPtyBridgeTestHarness,
54
55
  workerPtyTestHarness: () => workerPtyTestHarness,
55
56
  writeWorkerTextFile: () => writeWorkerTextFile
@@ -84,6 +85,21 @@ var import_project_snapshot_recovery_runner = require("./project-snapshot-recove
84
85
  var import_project_workspace_state = require("./project-workspace-state.cjs");
85
86
  var import_workspace_git_sync = require("./workspace-git-sync.cjs");
86
87
  const import_meta = {};
88
+ const WORKER_LIFECYCLE_DETAIL_MAX_LENGTH = 500;
89
+ const WORKER_SERVER_HEARTBEAT_INTERVAL_MS = 15e3;
90
+ const WORKER_SERVER_HEARTBEAT_TIMEOUT_MS = 2 * 60 * 1e3;
91
+ function boundedWorkerLifecycleProgress(progress) {
92
+ return progress.detail === void 0 ? progress : { ...progress, detail: progress.detail.slice(0, WORKER_LIFECYCLE_DETAIL_MAX_LENGTH) };
93
+ }
94
+ function workerServerHeartbeatAction(input) {
95
+ if (input.elapsedMs < input.timeoutMs) return "wait";
96
+ if (input.watchdogSweepDelayed || input.probeElapsedMs === void 0) return "start_probe";
97
+ return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
98
+ }
99
+ const workerLifecycleTestHarness = {
100
+ boundedWorkerLifecycleProgress,
101
+ workerServerHeartbeatAction
102
+ };
87
103
  class ProjectWorkspaceConfigurationDeferredError extends Error {
88
104
  }
89
105
  class WorkerServerUnavailableError extends Error {
@@ -2597,6 +2613,13 @@ async function executeCommand(input) {
2597
2613
  const startedAt = Date.now();
2598
2614
  let timeout;
2599
2615
  let spawnedProcess;
2616
+ let preparationSettled = false;
2617
+ const settlePreparation = () => {
2618
+ if (preparationSettled) return;
2619
+ preparationSettled = true;
2620
+ input.setPreparationHeartbeatLease(false);
2621
+ };
2622
+ input.setPreparationHeartbeatLease(true);
2600
2623
  try {
2601
2624
  const targetProcessEnv = await prepareShellEnvForTarget({
2602
2625
  target: input.resolvedTarget.target,
@@ -2634,6 +2657,7 @@ async function executeCommand(input) {
2634
2657
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2635
2658
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2636
2659
  });
2660
+ settlePreparation();
2637
2661
  if (input.message.timeoutMs) {
2638
2662
  timeout = setTimeout(() => {
2639
2663
  void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
@@ -2669,6 +2693,7 @@ async function executeCommand(input) {
2669
2693
  error: error instanceof Error ? error.message : String(error)
2670
2694
  };
2671
2695
  } finally {
2696
+ settlePreparation();
2672
2697
  if (timeout) {
2673
2698
  clearTimeout(timeout);
2674
2699
  }
@@ -2684,6 +2709,13 @@ async function executeStreamingCommand(input) {
2684
2709
  let timedOut = false;
2685
2710
  let timeout;
2686
2711
  let spawnedProcess;
2712
+ let preparationSettled = false;
2713
+ const settlePreparation = () => {
2714
+ if (preparationSettled) return;
2715
+ preparationSettled = true;
2716
+ input.setPreparationHeartbeatLease(false);
2717
+ };
2718
+ input.setPreparationHeartbeatLease(true);
2687
2719
  try {
2688
2720
  if (cancelledProcessRuns.delete(input.message.runId)) {
2689
2721
  sendWorkerMessage(input.ws, {
@@ -2736,6 +2768,7 @@ async function executeStreamingCommand(input) {
2736
2768
  ...interactive ? { interactive: true, stdin: subprocess.stdin } : {},
2737
2769
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2738
2770
  });
2771
+ settlePreparation();
2739
2772
  started = true;
2740
2773
  sendWorkerMessage(input.ws, {
2741
2774
  type: "exec_started",
@@ -2812,6 +2845,7 @@ async function executeStreamingCommand(input) {
2812
2845
  });
2813
2846
  }
2814
2847
  } finally {
2848
+ settlePreparation();
2815
2849
  if (timeout) {
2816
2850
  clearTimeout(timeout);
2817
2851
  }
@@ -3153,7 +3187,8 @@ function createNodePtyBridge(options) {
3153
3187
  };
3154
3188
  }
3155
3189
  const workerPtyBridgeTestHarness = {
3156
- create: createNodePtyBridge
3190
+ create: createNodePtyBridge,
3191
+ closeAllBestEffort: closePtysBestEffort
3157
3192
  };
3158
3193
  function resolveHostShell(command, platform = process.platform) {
3159
3194
  if (platform === "win32") {
@@ -3302,11 +3337,23 @@ function closePty(message) {
3302
3337
  }
3303
3338
  ptyProcess.kill();
3304
3339
  }
3305
- function closeAllPtys() {
3306
- for (const ptyProcess of activePtys.values()) {
3307
- ptyProcess.kill();
3340
+ function closePtysBestEffort(ptys, reportFailure) {
3341
+ for (const [ptyId, ptyProcess] of ptys) {
3342
+ try {
3343
+ ptyProcess.kill();
3344
+ } catch (error) {
3345
+ reportFailure(ptyId, error);
3346
+ }
3308
3347
  }
3309
3348
  }
3349
+ function closeAllPtys() {
3350
+ closePtysBestEffort(activePtys, (ptyId, error) => {
3351
+ process.stderr.write(
3352
+ `[r5d-worker] failed to close PTY ${ptyId} after control disconnect: ${error instanceof Error ? error.message : String(error)}
3353
+ `
3354
+ );
3355
+ });
3356
+ }
3310
3357
  async function terminateCredentialBearingChildren(children) {
3311
3358
  const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
3312
3359
  const failures = results.flatMap(
@@ -3366,16 +3413,6 @@ function websocketUrl(baseUrl, label) {
3366
3413
  }
3367
3414
  async function startWorker(options) {
3368
3415
  advanceWorkerAdmissionGeneration();
3369
- process.on("uncaughtException", (error) => {
3370
- process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
3371
- `);
3372
- });
3373
- process.on("unhandledRejection", (reason) => {
3374
- process.stderr.write(
3375
- `[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
3376
- `
3377
- );
3378
- });
3379
3416
  const config = readConfig(options.configPath);
3380
3417
  const { baseUrl, token } = resolveCredentials(options, config);
3381
3418
  initializeCredentialStoreDirectoryOnce();
@@ -5019,9 +5056,22 @@ async function startWorker(options) {
5019
5056
  Authorization: `Bearer ${token}`
5020
5057
  }
5021
5058
  });
5059
+ let lastServerControlMessageAtMs = performance.now();
5060
+ let serverHeartbeatProbeStartedAtMs;
5061
+ let lastServerHeartbeatSweepAtMs = performance.now();
5062
+ let serverHeartbeatWatchdog;
5063
+ const noteServerControlActivity = () => {
5064
+ lastServerControlMessageAtMs = performance.now();
5065
+ serverHeartbeatProbeStartedAtMs = void 0;
5066
+ };
5067
+ const stopServerHeartbeatWatchdog = () => {
5068
+ if (!serverHeartbeatWatchdog) return;
5069
+ clearInterval(serverHeartbeatWatchdog);
5070
+ serverHeartbeatWatchdog = void 0;
5071
+ };
5022
5072
  const sendLifecycleProgress = (progress) => {
5023
5073
  if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
5024
- sendWorkerMessage(ws, { type: "worker_progress", progress });
5074
+ sendWorkerMessage(ws, { type: "worker_progress", progress: boundedWorkerLifecycleProgress(progress) });
5025
5075
  };
5026
5076
  const describeSnapshotProgress = (progress) => {
5027
5077
  const project = import_node_path.default.relative(projectsRoot, progress.projectRoot) || import_node_path.default.basename(progress.projectRoot);
@@ -5102,6 +5152,29 @@ async function startWorker(options) {
5102
5152
  process.on("SIGINT", handleGracefulShutdown);
5103
5153
  ws.addEventListener("open", () => {
5104
5154
  currentWorkerSocket = ws;
5155
+ noteServerControlActivity();
5156
+ lastServerHeartbeatSweepAtMs = performance.now();
5157
+ serverHeartbeatWatchdog = setInterval(() => {
5158
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
5159
+ const nowMs = performance.now();
5160
+ const heartbeatAction = workerServerHeartbeatAction({
5161
+ elapsedMs: Math.max(0, nowMs - lastServerControlMessageAtMs),
5162
+ ...serverHeartbeatProbeStartedAtMs === void 0 ? {} : { probeElapsedMs: Math.max(0, nowMs - serverHeartbeatProbeStartedAtMs) },
5163
+ timeoutMs: WORKER_SERVER_HEARTBEAT_TIMEOUT_MS,
5164
+ watchdogSweepDelayed: nowMs - lastServerHeartbeatSweepAtMs >= WORKER_SERVER_HEARTBEAT_INTERVAL_MS * 2
5165
+ });
5166
+ lastServerHeartbeatSweepAtMs = nowMs;
5167
+ if (heartbeatAction === "start_probe") {
5168
+ serverHeartbeatProbeStartedAtMs = nowMs;
5169
+ return;
5170
+ }
5171
+ if (heartbeatAction === "terminate") {
5172
+ stopServerHeartbeatWatchdog();
5173
+ process.stderr.write("[r5d-worker] server control heartbeat timed out; reconnecting\n");
5174
+ ws.terminate();
5175
+ }
5176
+ }, WORKER_SERVER_HEARTBEAT_INTERVAL_MS);
5177
+ serverHeartbeatWatchdog.unref();
5105
5178
  const startupOperationId = crypto.randomUUID();
5106
5179
  startupRecoveryPromise = Promise.resolve().then(async () => await performStartupRecovery(startupOperationId)).then(() => {
5107
5180
  startupRecoveryComplete = true;
@@ -5158,6 +5231,7 @@ async function startWorker(options) {
5158
5231
  process.stdout.write("[r5d-worker] connected\n");
5159
5232
  });
5160
5233
  ws.addEventListener("message", (event) => {
5234
+ noteServerControlActivity();
5161
5235
  void (async () => {
5162
5236
  const message = JSON.parse(String(event.data));
5163
5237
  if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
@@ -5765,6 +5839,13 @@ async function startWorker(options) {
5765
5839
  token,
5766
5840
  artifactRoot,
5767
5841
  planRoot,
5842
+ setPreparationHeartbeatLease: (active) => {
5843
+ sendWorkerMessageFromCurrentSource(ws, {
5844
+ type: "heartbeat_lease",
5845
+ operationId: `exec:${message.runId}`,
5846
+ active
5847
+ });
5848
+ },
5768
5849
  assertAdmission: () => {
5769
5850
  if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5770
5851
  throw new Error(
@@ -5828,6 +5909,13 @@ async function startWorker(options) {
5828
5909
  token,
5829
5910
  artifactRoot,
5830
5911
  planRoot,
5912
+ setPreparationHeartbeatLease: (active) => {
5913
+ sendWorkerMessageFromCurrentSource(ws, {
5914
+ type: "heartbeat_lease",
5915
+ operationId: `exec:${message.runId}`,
5916
+ active
5917
+ });
5918
+ },
5831
5919
  assertAdmission: assertMessageAdmission
5832
5920
  });
5833
5921
  } finally {
@@ -5960,6 +6048,7 @@ async function startWorker(options) {
5960
6048
  const processEvents = process;
5961
6049
  processEvents.off("SIGTERM", handleGracefulShutdown);
5962
6050
  processEvents.off("SIGINT", handleGracefulShutdown);
6051
+ stopServerHeartbeatWatchdog();
5963
6052
  if (workspaceAutomaticTimer) {
5964
6053
  clearTimeout(workspaceAutomaticTimer);
5965
6054
  workspaceAutomaticTimer = void 0;
@@ -5997,31 +6086,40 @@ async function startWorker(options) {
5997
6086
  process.exit(1);
5998
6087
  return;
5999
6088
  }
6000
- void startWorker(options).catch((error) => {
6001
- process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
6089
+ const reconnectDelayMs = (0, import_supervisor.workerReconnectDelayMs)();
6090
+ process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
6002
6091
  `);
6003
- process.exit(error instanceof WorkerServerUnavailableError ? import_supervisor.WORKER_RECONNECT_EXIT_CODE : 1);
6004
- });
6092
+ setTimeout(() => {
6093
+ void startWorker(options).catch((error) => {
6094
+ process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
6095
+ `);
6096
+ process.exit(error instanceof WorkerServerUnavailableError ? import_supervisor.WORKER_RECONNECT_EXIT_CODE : 1);
6097
+ });
6098
+ }, reconnectDelayMs);
6005
6099
  });
6006
6100
  return;
6007
6101
  }
6008
6102
  if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
6009
- process.stderr.write(
6010
- `[r5d-worker] ${activeProcesses.size} process(es), ${credentialBearingProcessGroups.size} owned process group(s), and ${activePtys.size} PTY(s) active with ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${import_supervisor.WORKER_RECONNECT_DELAY_MS}ms
6103
+ const scheduleReconnect = (reconnect2) => {
6104
+ const reconnectDelayMs = (0, import_supervisor.workerReconnectDelayMs)();
6105
+ process.stderr.write(
6106
+ `[r5d-worker] ${activeProcesses.size} process(es), ${credentialBearingProcessGroups.size} owned process group(s), and ${activePtys.size} PTY(s) active with ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${reconnectDelayMs}ms
6011
6107
  `
6012
- );
6108
+ );
6109
+ setTimeout(reconnect2, reconnectDelayMs);
6110
+ };
6013
6111
  const reconnect = () => {
6014
6112
  void startWorker(options).catch((error) => {
6015
6113
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
6016
6114
  `);
6017
6115
  if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
6018
- setTimeout(reconnect, import_supervisor.WORKER_RECONNECT_DELAY_MS);
6116
+ scheduleReconnect(reconnect);
6019
6117
  return;
6020
6118
  }
6021
6119
  process.exit(error instanceof WorkerServerUnavailableError ? import_supervisor.WORKER_RECONNECT_EXIT_CODE : 1);
6022
6120
  });
6023
6121
  };
6024
- setTimeout(reconnect, import_supervisor.WORKER_RECONNECT_DELAY_MS);
6122
+ scheduleReconnect(reconnect);
6025
6123
  return;
6026
6124
  }
6027
6125
  process.exit(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
@@ -6031,7 +6129,20 @@ async function startWorker(options) {
6031
6129
  process.stderr.write(`[r5d-worker] websocket error${message}
6032
6130
  `);
6033
6131
  });
6034
- await new Promise(() => {
6132
+ await new Promise((resolve) => {
6133
+ ws.addEventListener("close", () => resolve(), { once: true });
6134
+ });
6135
+ }
6136
+ function installWorkerRuntimeFailureLogging() {
6137
+ process.on("uncaughtException", (error) => {
6138
+ process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
6139
+ `);
6140
+ });
6141
+ process.on("unhandledRejection", (reason) => {
6142
+ process.stderr.write(
6143
+ `[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
6144
+ `
6145
+ );
6035
6146
  });
6036
6147
  }
6037
6148
  async function main() {
@@ -6051,6 +6162,7 @@ async function main() {
6051
6162
  }
6052
6163
  if (process.env[import_supervisor.WORKER_RUNTIME_ENV] === "1") {
6053
6164
  delete process.env[import_supervisor.WORKER_RUNTIME_ENV];
6165
+ installWorkerRuntimeFailureLogging();
6054
6166
  try {
6055
6167
  await startWorker(parsed.options);
6056
6168
  } catch (error) {
@@ -6104,6 +6216,7 @@ if (isCliEntrypoint()) {
6104
6216
  syncSessionArtifacts,
6105
6217
  workerChildProcessEnvironment,
6106
6218
  workerGitSecurityTestHarness,
6219
+ workerLifecycleTestHarness,
6107
6220
  workerPtyBridgeTestHarness,
6108
6221
  workerPtyTestHarness,
6109
6222
  writeWorkerTextFile
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.91",
3
+ "version": "0.0.92",
4
4
  "type": "commonjs"
5
5
  }
@@ -21,10 +21,12 @@ __export(supervisor_exports, {
21
21
  WORKER_CREDENTIAL_RESTART_EXIT_CODE: () => WORKER_CREDENTIAL_RESTART_EXIT_CODE,
22
22
  WORKER_RECONNECT_DELAY_MS: () => WORKER_RECONNECT_DELAY_MS,
23
23
  WORKER_RECONNECT_EXIT_CODE: () => WORKER_RECONNECT_EXIT_CODE,
24
+ WORKER_RECONNECT_JITTER_MS: () => WORKER_RECONNECT_JITTER_MS,
24
25
  WORKER_RELOAD_EXIT_CODE: () => WORKER_RELOAD_EXIT_CODE,
25
26
  WORKER_RUNTIME_ENV: () => WORKER_RUNTIME_ENV,
26
27
  isRetryableWorkerServerStatus: () => isRetryableWorkerServerStatus,
27
- superviseWorkerRuntime: () => superviseWorkerRuntime
28
+ superviseWorkerRuntime: () => superviseWorkerRuntime,
29
+ workerReconnectDelayMs: () => workerReconnectDelayMs
28
30
  });
29
31
  module.exports = __toCommonJS(supervisor_exports);
30
32
  var import_node_os = require("node:os");
@@ -34,6 +36,7 @@ const WORKER_RELOAD_EXIT_CODE = 75;
34
36
  const WORKER_RECONNECT_EXIT_CODE = 76;
35
37
  const WORKER_CREDENTIAL_RESTART_EXIT_CODE = 77;
36
38
  const WORKER_RECONNECT_DELAY_MS = 2e3;
39
+ const WORKER_RECONNECT_JITTER_MS = 3e3;
37
40
  const SUPERVISOR_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
38
41
  function defaultRuntimeSpawner(argv, options) {
39
42
  return Bun.spawn(argv, options);
@@ -41,6 +44,10 @@ function defaultRuntimeSpawner(argv, options) {
41
44
  function defaultRuntimeDelay(delayMs) {
42
45
  return new Promise((resolve) => setTimeout(resolve, delayMs));
43
46
  }
47
+ function workerReconnectDelayMs(random = Math.random) {
48
+ const sample = Math.max(0, Math.min(1, random()));
49
+ return WORKER_RECONNECT_DELAY_MS + Math.floor(sample * WORKER_RECONNECT_JITTER_MS);
50
+ }
44
51
  async function defaultRuntimeReaper(runtime) {
45
52
  await (0, import_process_tree.terminateProcessTree)(runtime);
46
53
  }
@@ -53,7 +60,7 @@ function signalExitCode(signal) {
53
60
  function osSignalNumber(signal) {
54
61
  return import_node_os.constants.signals[signal];
55
62
  }
56
- async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner, waitBeforeReconnect = defaultRuntimeDelay, reapCredentialRuntime = defaultRuntimeReaper) {
63
+ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner, waitBeforeReconnect = defaultRuntimeDelay, reapCredentialRuntime = defaultRuntimeReaper, reconnectDelay = workerReconnectDelayMs) {
57
64
  if (!process.argv[1]) {
58
65
  throw new Error("Cannot locate the r5d-worker entrypoint");
59
66
  }
@@ -105,9 +112,10 @@ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = de
105
112
  continue;
106
113
  }
107
114
  if (exitCode === WORKER_RECONNECT_EXIT_CODE) {
108
- process.stderr.write(`[r5d-worker] reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
115
+ const reconnectDelayMs = reconnectDelay();
116
+ process.stderr.write(`[r5d-worker] reconnecting in ${reconnectDelayMs}ms
109
117
  `);
110
- await waitBeforeReconnect(WORKER_RECONNECT_DELAY_MS);
118
+ await waitBeforeReconnect(reconnectDelayMs);
111
119
  if (receivedSignal) return signalExitCode(receivedSignal);
112
120
  continue;
113
121
  }
@@ -123,8 +131,10 @@ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = de
123
131
  WORKER_CREDENTIAL_RESTART_EXIT_CODE,
124
132
  WORKER_RECONNECT_DELAY_MS,
125
133
  WORKER_RECONNECT_EXIT_CODE,
134
+ WORKER_RECONNECT_JITTER_MS,
126
135
  WORKER_RELOAD_EXIT_CODE,
127
136
  WORKER_RUNTIME_ENV,
128
137
  isRetryableWorkerServerStatus,
129
- superviseWorkerRuntime
138
+ superviseWorkerRuntime,
139
+ workerReconnectDelayMs
130
140
  });
package/dist/mjs/main.mjs CHANGED
@@ -49,10 +49,10 @@ import {
49
49
  isRetryableWorkerServerStatus,
50
50
  superviseWorkerRuntime,
51
51
  WORKER_CREDENTIAL_RESTART_EXIT_CODE,
52
- WORKER_RECONNECT_DELAY_MS,
53
52
  WORKER_RECONNECT_EXIT_CODE,
54
53
  WORKER_RELOAD_EXIT_CODE,
55
- WORKER_RUNTIME_ENV
54
+ WORKER_RUNTIME_ENV,
55
+ workerReconnectDelayMs
56
56
  } from "./supervisor.mjs";
57
57
  import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
58
58
  import {
@@ -91,6 +91,21 @@ import {
91
91
  workspaceGitMountsForRemediation,
92
92
  WorkspaceRemediationAncestryError
93
93
  } from "./workspace-git-sync.mjs";
94
+ const WORKER_LIFECYCLE_DETAIL_MAX_LENGTH = 500;
95
+ const WORKER_SERVER_HEARTBEAT_INTERVAL_MS = 15e3;
96
+ const WORKER_SERVER_HEARTBEAT_TIMEOUT_MS = 2 * 60 * 1e3;
97
+ function boundedWorkerLifecycleProgress(progress) {
98
+ return progress.detail === void 0 ? progress : { ...progress, detail: progress.detail.slice(0, WORKER_LIFECYCLE_DETAIL_MAX_LENGTH) };
99
+ }
100
+ function workerServerHeartbeatAction(input) {
101
+ if (input.elapsedMs < input.timeoutMs) return "wait";
102
+ if (input.watchdogSweepDelayed || input.probeElapsedMs === void 0) return "start_probe";
103
+ return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
104
+ }
105
+ const workerLifecycleTestHarness = {
106
+ boundedWorkerLifecycleProgress,
107
+ workerServerHeartbeatAction
108
+ };
94
109
  class ProjectWorkspaceConfigurationDeferredError extends Error {
95
110
  }
96
111
  class WorkerServerUnavailableError extends Error {
@@ -2604,6 +2619,13 @@ async function executeCommand(input) {
2604
2619
  const startedAt = Date.now();
2605
2620
  let timeout;
2606
2621
  let spawnedProcess;
2622
+ let preparationSettled = false;
2623
+ const settlePreparation = () => {
2624
+ if (preparationSettled) return;
2625
+ preparationSettled = true;
2626
+ input.setPreparationHeartbeatLease(false);
2627
+ };
2628
+ input.setPreparationHeartbeatLease(true);
2607
2629
  try {
2608
2630
  const targetProcessEnv = await prepareShellEnvForTarget({
2609
2631
  target: input.resolvedTarget.target,
@@ -2641,6 +2663,7 @@ async function executeCommand(input) {
2641
2663
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2642
2664
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2643
2665
  });
2666
+ settlePreparation();
2644
2667
  if (input.message.timeoutMs) {
2645
2668
  timeout = setTimeout(() => {
2646
2669
  void terminateProcessTree(subprocess).catch((error) => {
@@ -2676,6 +2699,7 @@ async function executeCommand(input) {
2676
2699
  error: error instanceof Error ? error.message : String(error)
2677
2700
  };
2678
2701
  } finally {
2702
+ settlePreparation();
2679
2703
  if (timeout) {
2680
2704
  clearTimeout(timeout);
2681
2705
  }
@@ -2691,6 +2715,13 @@ async function executeStreamingCommand(input) {
2691
2715
  let timedOut = false;
2692
2716
  let timeout;
2693
2717
  let spawnedProcess;
2718
+ let preparationSettled = false;
2719
+ const settlePreparation = () => {
2720
+ if (preparationSettled) return;
2721
+ preparationSettled = true;
2722
+ input.setPreparationHeartbeatLease(false);
2723
+ };
2724
+ input.setPreparationHeartbeatLease(true);
2694
2725
  try {
2695
2726
  if (cancelledProcessRuns.delete(input.message.runId)) {
2696
2727
  sendWorkerMessage(input.ws, {
@@ -2743,6 +2774,7 @@ async function executeStreamingCommand(input) {
2743
2774
  ...interactive ? { interactive: true, stdin: subprocess.stdin } : {},
2744
2775
  ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2745
2776
  });
2777
+ settlePreparation();
2746
2778
  started = true;
2747
2779
  sendWorkerMessage(input.ws, {
2748
2780
  type: "exec_started",
@@ -2819,6 +2851,7 @@ async function executeStreamingCommand(input) {
2819
2851
  });
2820
2852
  }
2821
2853
  } finally {
2854
+ settlePreparation();
2822
2855
  if (timeout) {
2823
2856
  clearTimeout(timeout);
2824
2857
  }
@@ -3160,7 +3193,8 @@ function createNodePtyBridge(options) {
3160
3193
  };
3161
3194
  }
3162
3195
  const workerPtyBridgeTestHarness = {
3163
- create: createNodePtyBridge
3196
+ create: createNodePtyBridge,
3197
+ closeAllBestEffort: closePtysBestEffort
3164
3198
  };
3165
3199
  function resolveHostShell(command, platform = process.platform) {
3166
3200
  if (platform === "win32") {
@@ -3309,11 +3343,23 @@ function closePty(message) {
3309
3343
  }
3310
3344
  ptyProcess.kill();
3311
3345
  }
3312
- function closeAllPtys() {
3313
- for (const ptyProcess of activePtys.values()) {
3314
- ptyProcess.kill();
3346
+ function closePtysBestEffort(ptys, reportFailure) {
3347
+ for (const [ptyId, ptyProcess] of ptys) {
3348
+ try {
3349
+ ptyProcess.kill();
3350
+ } catch (error) {
3351
+ reportFailure(ptyId, error);
3352
+ }
3315
3353
  }
3316
3354
  }
3355
+ function closeAllPtys() {
3356
+ closePtysBestEffort(activePtys, (ptyId, error) => {
3357
+ process.stderr.write(
3358
+ `[r5d-worker] failed to close PTY ${ptyId} after control disconnect: ${error instanceof Error ? error.message : String(error)}
3359
+ `
3360
+ );
3361
+ });
3362
+ }
3317
3363
  async function terminateCredentialBearingChildren(children) {
3318
3364
  const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
3319
3365
  const failures = results.flatMap(
@@ -3373,16 +3419,6 @@ function websocketUrl(baseUrl, label) {
3373
3419
  }
3374
3420
  async function startWorker(options) {
3375
3421
  advanceWorkerAdmissionGeneration();
3376
- process.on("uncaughtException", (error) => {
3377
- process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
3378
- `);
3379
- });
3380
- process.on("unhandledRejection", (reason) => {
3381
- process.stderr.write(
3382
- `[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
3383
- `
3384
- );
3385
- });
3386
3422
  const config = readConfig(options.configPath);
3387
3423
  const { baseUrl, token } = resolveCredentials(options, config);
3388
3424
  initializeCredentialStoreDirectoryOnce();
@@ -5026,9 +5062,22 @@ async function startWorker(options) {
5026
5062
  Authorization: `Bearer ${token}`
5027
5063
  }
5028
5064
  });
5065
+ let lastServerControlMessageAtMs = performance.now();
5066
+ let serverHeartbeatProbeStartedAtMs;
5067
+ let lastServerHeartbeatSweepAtMs = performance.now();
5068
+ let serverHeartbeatWatchdog;
5069
+ const noteServerControlActivity = () => {
5070
+ lastServerControlMessageAtMs = performance.now();
5071
+ serverHeartbeatProbeStartedAtMs = void 0;
5072
+ };
5073
+ const stopServerHeartbeatWatchdog = () => {
5074
+ if (!serverHeartbeatWatchdog) return;
5075
+ clearInterval(serverHeartbeatWatchdog);
5076
+ serverHeartbeatWatchdog = void 0;
5077
+ };
5029
5078
  const sendLifecycleProgress = (progress) => {
5030
5079
  if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
5031
- sendWorkerMessage(ws, { type: "worker_progress", progress });
5080
+ sendWorkerMessage(ws, { type: "worker_progress", progress: boundedWorkerLifecycleProgress(progress) });
5032
5081
  };
5033
5082
  const describeSnapshotProgress = (progress) => {
5034
5083
  const project = path.relative(projectsRoot, progress.projectRoot) || path.basename(progress.projectRoot);
@@ -5109,6 +5158,29 @@ async function startWorker(options) {
5109
5158
  process.on("SIGINT", handleGracefulShutdown);
5110
5159
  ws.addEventListener("open", () => {
5111
5160
  currentWorkerSocket = ws;
5161
+ noteServerControlActivity();
5162
+ lastServerHeartbeatSweepAtMs = performance.now();
5163
+ serverHeartbeatWatchdog = setInterval(() => {
5164
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
5165
+ const nowMs = performance.now();
5166
+ const heartbeatAction = workerServerHeartbeatAction({
5167
+ elapsedMs: Math.max(0, nowMs - lastServerControlMessageAtMs),
5168
+ ...serverHeartbeatProbeStartedAtMs === void 0 ? {} : { probeElapsedMs: Math.max(0, nowMs - serverHeartbeatProbeStartedAtMs) },
5169
+ timeoutMs: WORKER_SERVER_HEARTBEAT_TIMEOUT_MS,
5170
+ watchdogSweepDelayed: nowMs - lastServerHeartbeatSweepAtMs >= WORKER_SERVER_HEARTBEAT_INTERVAL_MS * 2
5171
+ });
5172
+ lastServerHeartbeatSweepAtMs = nowMs;
5173
+ if (heartbeatAction === "start_probe") {
5174
+ serverHeartbeatProbeStartedAtMs = nowMs;
5175
+ return;
5176
+ }
5177
+ if (heartbeatAction === "terminate") {
5178
+ stopServerHeartbeatWatchdog();
5179
+ process.stderr.write("[r5d-worker] server control heartbeat timed out; reconnecting\n");
5180
+ ws.terminate();
5181
+ }
5182
+ }, WORKER_SERVER_HEARTBEAT_INTERVAL_MS);
5183
+ serverHeartbeatWatchdog.unref();
5112
5184
  const startupOperationId = crypto.randomUUID();
5113
5185
  startupRecoveryPromise = Promise.resolve().then(async () => await performStartupRecovery(startupOperationId)).then(() => {
5114
5186
  startupRecoveryComplete = true;
@@ -5165,6 +5237,7 @@ async function startWorker(options) {
5165
5237
  process.stdout.write("[r5d-worker] connected\n");
5166
5238
  });
5167
5239
  ws.addEventListener("message", (event) => {
5240
+ noteServerControlActivity();
5168
5241
  void (async () => {
5169
5242
  const message = JSON.parse(String(event.data));
5170
5243
  if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
@@ -5772,6 +5845,13 @@ async function startWorker(options) {
5772
5845
  token,
5773
5846
  artifactRoot,
5774
5847
  planRoot,
5848
+ setPreparationHeartbeatLease: (active) => {
5849
+ sendWorkerMessageFromCurrentSource(ws, {
5850
+ type: "heartbeat_lease",
5851
+ operationId: `exec:${message.runId}`,
5852
+ active
5853
+ });
5854
+ },
5775
5855
  assertAdmission: () => {
5776
5856
  if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5777
5857
  throw new Error(
@@ -5835,6 +5915,13 @@ async function startWorker(options) {
5835
5915
  token,
5836
5916
  artifactRoot,
5837
5917
  planRoot,
5918
+ setPreparationHeartbeatLease: (active) => {
5919
+ sendWorkerMessageFromCurrentSource(ws, {
5920
+ type: "heartbeat_lease",
5921
+ operationId: `exec:${message.runId}`,
5922
+ active
5923
+ });
5924
+ },
5838
5925
  assertAdmission: assertMessageAdmission
5839
5926
  });
5840
5927
  } finally {
@@ -5967,6 +6054,7 @@ async function startWorker(options) {
5967
6054
  const processEvents = process;
5968
6055
  processEvents.off("SIGTERM", handleGracefulShutdown);
5969
6056
  processEvents.off("SIGINT", handleGracefulShutdown);
6057
+ stopServerHeartbeatWatchdog();
5970
6058
  if (workspaceAutomaticTimer) {
5971
6059
  clearTimeout(workspaceAutomaticTimer);
5972
6060
  workspaceAutomaticTimer = void 0;
@@ -6004,31 +6092,40 @@ async function startWorker(options) {
6004
6092
  process.exit(1);
6005
6093
  return;
6006
6094
  }
6007
- void startWorker(options).catch((error) => {
6008
- process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
6095
+ const reconnectDelayMs = workerReconnectDelayMs();
6096
+ process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
6009
6097
  `);
6010
- process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
6011
- });
6098
+ setTimeout(() => {
6099
+ void startWorker(options).catch((error) => {
6100
+ process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
6101
+ `);
6102
+ process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
6103
+ });
6104
+ }, reconnectDelayMs);
6012
6105
  });
6013
6106
  return;
6014
6107
  }
6015
6108
  if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
6016
- process.stderr.write(
6017
- `[r5d-worker] ${activeProcesses.size} process(es), ${credentialBearingProcessGroups.size} owned process group(s), and ${activePtys.size} PTY(s) active with ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
6109
+ const scheduleReconnect = (reconnect2) => {
6110
+ const reconnectDelayMs = workerReconnectDelayMs();
6111
+ process.stderr.write(
6112
+ `[r5d-worker] ${activeProcesses.size} process(es), ${credentialBearingProcessGroups.size} owned process group(s), and ${activePtys.size} PTY(s) active with ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${reconnectDelayMs}ms
6018
6113
  `
6019
- );
6114
+ );
6115
+ setTimeout(reconnect2, reconnectDelayMs);
6116
+ };
6020
6117
  const reconnect = () => {
6021
6118
  void startWorker(options).catch((error) => {
6022
6119
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
6023
6120
  `);
6024
6121
  if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
6025
- setTimeout(reconnect, WORKER_RECONNECT_DELAY_MS);
6122
+ scheduleReconnect(reconnect);
6026
6123
  return;
6027
6124
  }
6028
6125
  process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
6029
6126
  });
6030
6127
  };
6031
- setTimeout(reconnect, WORKER_RECONNECT_DELAY_MS);
6128
+ scheduleReconnect(reconnect);
6032
6129
  return;
6033
6130
  }
6034
6131
  process.exit(WORKER_RECONNECT_EXIT_CODE);
@@ -6038,7 +6135,20 @@ async function startWorker(options) {
6038
6135
  process.stderr.write(`[r5d-worker] websocket error${message}
6039
6136
  `);
6040
6137
  });
6041
- await new Promise(() => {
6138
+ await new Promise((resolve) => {
6139
+ ws.addEventListener("close", () => resolve(), { once: true });
6140
+ });
6141
+ }
6142
+ function installWorkerRuntimeFailureLogging() {
6143
+ process.on("uncaughtException", (error) => {
6144
+ process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
6145
+ `);
6146
+ });
6147
+ process.on("unhandledRejection", (reason) => {
6148
+ process.stderr.write(
6149
+ `[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
6150
+ `
6151
+ );
6042
6152
  });
6043
6153
  }
6044
6154
  async function main() {
@@ -6058,6 +6168,7 @@ async function main() {
6058
6168
  }
6059
6169
  if (process.env[WORKER_RUNTIME_ENV] === "1") {
6060
6170
  delete process.env[WORKER_RUNTIME_ENV];
6171
+ installWorkerRuntimeFailureLogging();
6061
6172
  try {
6062
6173
  await startWorker(parsed.options);
6063
6174
  } catch (error) {
@@ -6110,6 +6221,7 @@ export {
6110
6221
  syncSessionArtifacts,
6111
6222
  workerChildProcessEnvironment,
6112
6223
  workerGitSecurityTestHarness,
6224
+ workerLifecycleTestHarness,
6113
6225
  workerPtyBridgeTestHarness,
6114
6226
  workerPtyTestHarness,
6115
6227
  writeWorkerTextFile
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.91",
3
+ "version": "0.0.92",
4
4
  "type": "module"
5
5
  }
@@ -5,6 +5,7 @@ const WORKER_RELOAD_EXIT_CODE = 75;
5
5
  const WORKER_RECONNECT_EXIT_CODE = 76;
6
6
  const WORKER_CREDENTIAL_RESTART_EXIT_CODE = 77;
7
7
  const WORKER_RECONNECT_DELAY_MS = 2e3;
8
+ const WORKER_RECONNECT_JITTER_MS = 3e3;
8
9
  const SUPERVISOR_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
9
10
  function defaultRuntimeSpawner(argv, options) {
10
11
  return Bun.spawn(argv, options);
@@ -12,6 +13,10 @@ function defaultRuntimeSpawner(argv, options) {
12
13
  function defaultRuntimeDelay(delayMs) {
13
14
  return new Promise((resolve) => setTimeout(resolve, delayMs));
14
15
  }
16
+ function workerReconnectDelayMs(random = Math.random) {
17
+ const sample = Math.max(0, Math.min(1, random()));
18
+ return WORKER_RECONNECT_DELAY_MS + Math.floor(sample * WORKER_RECONNECT_JITTER_MS);
19
+ }
15
20
  async function defaultRuntimeReaper(runtime) {
16
21
  await terminateProcessTree(runtime);
17
22
  }
@@ -24,7 +29,7 @@ function signalExitCode(signal) {
24
29
  function osSignalNumber(signal) {
25
30
  return osConstants.signals[signal];
26
31
  }
27
- async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner, waitBeforeReconnect = defaultRuntimeDelay, reapCredentialRuntime = defaultRuntimeReaper) {
32
+ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner, waitBeforeReconnect = defaultRuntimeDelay, reapCredentialRuntime = defaultRuntimeReaper, reconnectDelay = workerReconnectDelayMs) {
28
33
  if (!process.argv[1]) {
29
34
  throw new Error("Cannot locate the r5d-worker entrypoint");
30
35
  }
@@ -76,9 +81,10 @@ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = de
76
81
  continue;
77
82
  }
78
83
  if (exitCode === WORKER_RECONNECT_EXIT_CODE) {
79
- process.stderr.write(`[r5d-worker] reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
84
+ const reconnectDelayMs = reconnectDelay();
85
+ process.stderr.write(`[r5d-worker] reconnecting in ${reconnectDelayMs}ms
80
86
  `);
81
- await waitBeforeReconnect(WORKER_RECONNECT_DELAY_MS);
87
+ await waitBeforeReconnect(reconnectDelayMs);
82
88
  if (receivedSignal) return signalExitCode(receivedSignal);
83
89
  continue;
84
90
  }
@@ -93,8 +99,10 @@ export {
93
99
  WORKER_CREDENTIAL_RESTART_EXIT_CODE,
94
100
  WORKER_RECONNECT_DELAY_MS,
95
101
  WORKER_RECONNECT_EXIT_CODE,
102
+ WORKER_RECONNECT_JITTER_MS,
96
103
  WORKER_RELOAD_EXIT_CODE,
97
104
  WORKER_RUNTIME_ENV,
98
105
  isRetryableWorkerServerStatus,
99
- superviseWorkerRuntime
106
+ superviseWorkerRuntime,
107
+ workerReconnectDelayMs
100
108
  };
@@ -23,6 +23,26 @@ type WorkerProjectConfig = {
23
23
  baseCommitHash: string;
24
24
  }>;
25
25
  };
26
+ type WorkerLifecycleProgress = {
27
+ phase: "starting" | "recovering" | "configuring" | "syncing" | "ready" | "failed";
28
+ operationId: string;
29
+ detail?: string;
30
+ completedBytes?: number;
31
+ totalBytes?: number;
32
+ completedItems?: number;
33
+ totalItems?: number;
34
+ };
35
+ declare function boundedWorkerLifecycleProgress(progress: WorkerLifecycleProgress): WorkerLifecycleProgress;
36
+ declare function workerServerHeartbeatAction(input: {
37
+ elapsedMs: number;
38
+ probeElapsedMs?: number;
39
+ timeoutMs: number;
40
+ watchdogSweepDelayed: boolean;
41
+ }): "wait" | "start_probe" | "terminate";
42
+ export declare const workerLifecycleTestHarness: {
43
+ boundedWorkerLifecycleProgress: typeof boundedWorkerLifecycleProgress;
44
+ workerServerHeartbeatAction: typeof workerServerHeartbeatAction;
45
+ };
26
46
  type WorkspaceSyncTrigger = {
27
47
  type: "connect" | "inbound_head" | "write" | "edit" | "shell_inline" | "process_terminal" | "process_cancel" | "periodic" | "manual" | "remediation" | "remediation_confirm" | "remediation_reset";
28
48
  sessionId?: string;
@@ -813,12 +833,16 @@ declare function createNodePtyBridge(options: {
813
833
  }): WorkerPty;
814
834
  export declare const workerPtyBridgeTestHarness: {
815
835
  create: typeof createNodePtyBridge;
836
+ closeAllBestEffort: typeof closePtysBestEffort;
816
837
  };
817
838
  export declare function resolveHostShell(command?: string, platform?: NodeJS.Platform): {
818
839
  file: string;
819
840
  args: string[];
820
841
  env?: Record<string, string>;
821
842
  };
843
+ declare function closePtysBestEffort(ptys: Iterable<readonly [string, {
844
+ kill: () => void;
845
+ }]>, reportFailure: (ptyId: string, error: unknown) => void): void;
822
846
  declare function terminateCredentialBearingChildren(children: readonly {
823
847
  label: string;
824
848
  terminate: () => void | Promise<void>;
@@ -4,6 +4,7 @@ export declare const WORKER_RELOAD_EXIT_CODE = 75;
4
4
  export declare const WORKER_RECONNECT_EXIT_CODE = 76;
5
5
  export declare const WORKER_CREDENTIAL_RESTART_EXIT_CODE = 77;
6
6
  export declare const WORKER_RECONNECT_DELAY_MS = 2000;
7
+ export declare const WORKER_RECONNECT_JITTER_MS = 3000;
7
8
  type RuntimeProcess = KillableSubprocess;
8
9
  export type RuntimeSpawner = (argv: string[], options: {
9
10
  stdin: "inherit";
@@ -14,6 +15,8 @@ export type RuntimeSpawner = (argv: string[], options: {
14
15
  }) => RuntimeProcess;
15
16
  export type RuntimeDelay = (delayMs: number) => Promise<void>;
16
17
  export type RuntimeReaper = (runtime: RuntimeProcess) => Promise<void>;
18
+ export type RuntimeReconnectDelay = () => number;
19
+ export declare function workerReconnectDelayMs(random?: () => number): number;
17
20
  export declare function isRetryableWorkerServerStatus(status: number): boolean;
18
- export declare function superviseWorkerRuntime(argv: string[], env?: Record<string, string | undefined>, spawnRuntime?: RuntimeSpawner, waitBeforeReconnect?: RuntimeDelay, reapCredentialRuntime?: RuntimeReaper): Promise<number>;
21
+ export declare function superviseWorkerRuntime(argv: string[], env?: Record<string, string | undefined>, spawnRuntime?: RuntimeSpawner, waitBeforeReconnect?: RuntimeDelay, reapCredentialRuntime?: RuntimeReaper, reconnectDelay?: RuntimeReconnectDelay): Promise<number>;
19
22
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.91",
3
+ "version": "0.0.92",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",