@ricsam/r5d-worker 0.0.89 → 0.0.90

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
@@ -41,6 +41,8 @@ Worker labels are mandatory and unique per user. Choose labels that describe hos
41
41
 
42
42
  The supervisor also reconnects automatically after server rollouts and transient network failures. A brief bounded delay prevents a tight retry loop while the server is unavailable. Pressing Ctrl+C still stops the worker, and agent-managed processes retain their existing in-process recovery path while disconnected.
43
43
 
44
+ 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.
45
+
44
46
  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.
45
47
 
46
48
  One-click updates use the current user's global npm prefix and never invoke `sudo`. If that prefix is not writable, User Settings provides the exact command to run manually.
package/dist/cjs/main.cjs CHANGED
@@ -63,7 +63,6 @@ var import_node_child_process = require("node:child_process");
63
63
  var import_bun_sqlite = require("bun:sqlite");
64
64
  var import_cli_update = require("./cli-update.cjs");
65
65
  var import_git_process_environment = require("./git-process-environment.cjs");
66
- var import_heartbeat = require("./heartbeat.cjs");
67
66
  var import_process_tree = require("./process-tree.cjs");
68
67
  var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
69
68
  var import_port_forward_client = require("./port-forward-client.cjs");
@@ -4501,7 +4500,6 @@ async function startWorker(options) {
4501
4500
  );
4502
4501
  }).finally(() => {
4503
4502
  automaticSyncInFlight = false;
4504
- heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
4505
4503
  if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4506
4504
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4507
4505
  }
@@ -5000,10 +4998,6 @@ async function startWorker(options) {
5000
4998
  Authorization: `Bearer ${token}`
5001
4999
  }
5002
5000
  });
5003
- let lastServerHeartbeatAt = null;
5004
- let heartbeatBusyGrace = null;
5005
- let heartbeatTimedOut = false;
5006
- let heartbeatWatchdog;
5007
5001
  const handleGracefulShutdown = () => {
5008
5002
  if (shutdownAfterClose) return;
5009
5003
  shutdownAfterClose = true;
@@ -5031,23 +5025,8 @@ async function startWorker(options) {
5031
5025
  };
5032
5026
  process.on("SIGTERM", handleGracefulShutdown);
5033
5027
  process.on("SIGINT", handleGracefulShutdown);
5034
- const stopHeartbeatWatchdog = () => {
5035
- if (heartbeatWatchdog) {
5036
- clearInterval(heartbeatWatchdog);
5037
- heartbeatWatchdog = void 0;
5038
- }
5039
- };
5040
5028
  ws.addEventListener("open", () => {
5041
5029
  currentWorkerSocket = ws;
5042
- heartbeatWatchdog = setInterval(() => {
5043
- if (heartbeatTimedOut || !(0, import_heartbeat.hasWorkerHeartbeatTimedOutWithBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace)) {
5044
- return;
5045
- }
5046
- heartbeatTimedOut = true;
5047
- stopHeartbeatWatchdog();
5048
- process.stderr.write("[r5d-worker] server heartbeat timed out; reconnecting\n");
5049
- ws.terminate();
5050
- }, import_heartbeat.WORKER_HEARTBEAT_INTERVAL_MS);
5051
5030
  const hello = {
5052
5031
  type: "hello",
5053
5032
  hostInfo: {
@@ -5094,7 +5073,7 @@ async function startWorker(options) {
5094
5073
  sourceSocketIsCurrent: currentWorkerSocket === ws,
5095
5074
  sourceSocketOpen: ws.readyState === WebSocket.OPEN,
5096
5075
  workspaceConfigured,
5097
- runtimeHealthy: !shutdownAfterClose && !heartbeatTimedOut
5076
+ runtimeHealthy: !shutdownAfterClose
5098
5077
  });
5099
5078
  if (message.type === "connected") {
5100
5079
  return;
@@ -5453,8 +5432,6 @@ async function startWorker(options) {
5453
5432
  return;
5454
5433
  }
5455
5434
  if (message.type === "ping") {
5456
- lastServerHeartbeatAt = Date.now();
5457
- heartbeatBusyGrace = null;
5458
5435
  ws.send(JSON.stringify({ type: "pong" }));
5459
5436
  return;
5460
5437
  }
@@ -5868,15 +5845,12 @@ async function startWorker(options) {
5868
5845
  })().catch((error) => {
5869
5846
  process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
5870
5847
  `);
5871
- }).finally(() => {
5872
- heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
5873
5848
  });
5874
5849
  });
5875
5850
  ws.addEventListener("close", (event) => {
5876
5851
  const processEvents = process;
5877
5852
  processEvents.off("SIGTERM", handleGracefulShutdown);
5878
5853
  processEvents.off("SIGINT", handleGracefulShutdown);
5879
- stopHeartbeatWatchdog();
5880
5854
  if (workspaceAutomaticTimer) {
5881
5855
  clearTimeout(workspaceAutomaticTimer);
5882
5856
  workspaceAutomaticTimer = void 0;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.89",
3
+ "version": "0.0.90",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -7,11 +7,6 @@ import { spawn as spawnChildProcess } from "node:child_process";
7
7
  import { Database } from "bun:sqlite";
8
8
  import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
9
9
  import { gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
10
- import {
11
- grantWorkerHeartbeatBusyGrace,
12
- hasWorkerHeartbeatTimedOutWithBusyGrace,
13
- WORKER_HEARTBEAT_INTERVAL_MS
14
- } from "./heartbeat.mjs";
15
10
  import { terminateProcessTree } from "./process-tree.mjs";
16
11
  import { createPtyOutputCoalescer } from "./pty-output-coalescer.mjs";
17
12
  import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
@@ -4509,7 +4504,6 @@ async function startWorker(options) {
4509
4504
  );
4510
4505
  }).finally(() => {
4511
4506
  automaticSyncInFlight = false;
4512
- heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
4513
4507
  if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4514
4508
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4515
4509
  }
@@ -5008,10 +5002,6 @@ async function startWorker(options) {
5008
5002
  Authorization: `Bearer ${token}`
5009
5003
  }
5010
5004
  });
5011
- let lastServerHeartbeatAt = null;
5012
- let heartbeatBusyGrace = null;
5013
- let heartbeatTimedOut = false;
5014
- let heartbeatWatchdog;
5015
5005
  const handleGracefulShutdown = () => {
5016
5006
  if (shutdownAfterClose) return;
5017
5007
  shutdownAfterClose = true;
@@ -5039,23 +5029,8 @@ async function startWorker(options) {
5039
5029
  };
5040
5030
  process.on("SIGTERM", handleGracefulShutdown);
5041
5031
  process.on("SIGINT", handleGracefulShutdown);
5042
- const stopHeartbeatWatchdog = () => {
5043
- if (heartbeatWatchdog) {
5044
- clearInterval(heartbeatWatchdog);
5045
- heartbeatWatchdog = void 0;
5046
- }
5047
- };
5048
5032
  ws.addEventListener("open", () => {
5049
5033
  currentWorkerSocket = ws;
5050
- heartbeatWatchdog = setInterval(() => {
5051
- if (heartbeatTimedOut || !hasWorkerHeartbeatTimedOutWithBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace)) {
5052
- return;
5053
- }
5054
- heartbeatTimedOut = true;
5055
- stopHeartbeatWatchdog();
5056
- process.stderr.write("[r5d-worker] server heartbeat timed out; reconnecting\n");
5057
- ws.terminate();
5058
- }, WORKER_HEARTBEAT_INTERVAL_MS);
5059
5034
  const hello = {
5060
5035
  type: "hello",
5061
5036
  hostInfo: {
@@ -5102,7 +5077,7 @@ async function startWorker(options) {
5102
5077
  sourceSocketIsCurrent: currentWorkerSocket === ws,
5103
5078
  sourceSocketOpen: ws.readyState === WebSocket.OPEN,
5104
5079
  workspaceConfigured,
5105
- runtimeHealthy: !shutdownAfterClose && !heartbeatTimedOut
5080
+ runtimeHealthy: !shutdownAfterClose
5106
5081
  });
5107
5082
  if (message.type === "connected") {
5108
5083
  return;
@@ -5461,8 +5436,6 @@ async function startWorker(options) {
5461
5436
  return;
5462
5437
  }
5463
5438
  if (message.type === "ping") {
5464
- lastServerHeartbeatAt = Date.now();
5465
- heartbeatBusyGrace = null;
5466
5439
  ws.send(JSON.stringify({ type: "pong" }));
5467
5440
  return;
5468
5441
  }
@@ -5876,15 +5849,12 @@ async function startWorker(options) {
5876
5849
  })().catch((error) => {
5877
5850
  process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
5878
5851
  `);
5879
- }).finally(() => {
5880
- heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
5881
5852
  });
5882
5853
  });
5883
5854
  ws.addEventListener("close", (event) => {
5884
5855
  const processEvents = process;
5885
5856
  processEvents.off("SIGTERM", handleGracefulShutdown);
5886
5857
  processEvents.off("SIGINT", handleGracefulShutdown);
5887
- stopHeartbeatWatchdog();
5888
5858
  if (workspaceAutomaticTimer) {
5889
5859
  clearTimeout(workspaceAutomaticTimer);
5890
5860
  workspaceAutomaticTimer = void 0;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.89",
3
+ "version": "0.0.90",
4
4
  "type": "module"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.89",
3
+ "version": "0.0.90",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",
@@ -1,49 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
- var heartbeat_exports = {};
20
- __export(heartbeat_exports, {
21
- WORKER_HEARTBEAT_INTERVAL_MS: () => WORKER_HEARTBEAT_INTERVAL_MS,
22
- WORKER_HEARTBEAT_TIMEOUT_MS: () => WORKER_HEARTBEAT_TIMEOUT_MS,
23
- grantWorkerHeartbeatBusyGrace: () => grantWorkerHeartbeatBusyGrace,
24
- hasWorkerHeartbeatTimedOut: () => hasWorkerHeartbeatTimedOut,
25
- hasWorkerHeartbeatTimedOutWithBusyGrace: () => hasWorkerHeartbeatTimedOutWithBusyGrace
26
- });
27
- module.exports = __toCommonJS(heartbeat_exports);
28
- const WORKER_HEARTBEAT_INTERVAL_MS = 15e3;
29
- const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
30
- function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
31
- return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
32
- }
33
- function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt, currentGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS, graceMs = WORKER_HEARTBEAT_INTERVAL_MS) {
34
- if (!hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs)) return currentGrace;
35
- if (currentGrace?.heartbeatAt === lastHeartbeatAt) return currentGrace;
36
- return { heartbeatAt: lastHeartbeatAt, expiresAt: now + graceMs };
37
- }
38
- function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt, busyGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
39
- if (lastHeartbeatAt !== null && busyGrace?.heartbeatAt === lastHeartbeatAt && now < busyGrace.expiresAt) return false;
40
- return hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs);
41
- }
42
- // Annotate the CommonJS export names for ESM import in node:
43
- 0 && (module.exports = {
44
- WORKER_HEARTBEAT_INTERVAL_MS,
45
- WORKER_HEARTBEAT_TIMEOUT_MS,
46
- grantWorkerHeartbeatBusyGrace,
47
- hasWorkerHeartbeatTimedOut,
48
- hasWorkerHeartbeatTimedOutWithBusyGrace
49
- });
@@ -1,21 +0,0 @@
1
- const WORKER_HEARTBEAT_INTERVAL_MS = 15e3;
2
- const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
3
- function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
4
- return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
5
- }
6
- function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt, currentGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS, graceMs = WORKER_HEARTBEAT_INTERVAL_MS) {
7
- if (!hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs)) return currentGrace;
8
- if (currentGrace?.heartbeatAt === lastHeartbeatAt) return currentGrace;
9
- return { heartbeatAt: lastHeartbeatAt, expiresAt: now + graceMs };
10
- }
11
- function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt, busyGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
12
- if (lastHeartbeatAt !== null && busyGrace?.heartbeatAt === lastHeartbeatAt && now < busyGrace.expiresAt) return false;
13
- return hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs);
14
- }
15
- export {
16
- WORKER_HEARTBEAT_INTERVAL_MS,
17
- WORKER_HEARTBEAT_TIMEOUT_MS,
18
- grantWorkerHeartbeatBusyGrace,
19
- hasWorkerHeartbeatTimedOut,
20
- hasWorkerHeartbeatTimedOutWithBusyGrace
21
- };
@@ -1,10 +0,0 @@
1
- export declare const WORKER_HEARTBEAT_INTERVAL_MS = 15000;
2
- export declare const WORKER_HEARTBEAT_TIMEOUT_MS = 45000;
3
- export declare function hasWorkerHeartbeatTimedOut(lastHeartbeatAt: number | null, now?: number, timeoutMs?: number): boolean;
4
- export type WorkerHeartbeatBusyGrace = {
5
- heartbeatAt: number;
6
- expiresAt: number;
7
- };
8
- /** Grant one bounded grace period for one actual server-heartbeat generation. */
9
- export declare function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt: number | null, currentGrace: WorkerHeartbeatBusyGrace | null, now?: number, timeoutMs?: number, graceMs?: number): WorkerHeartbeatBusyGrace | null;
10
- export declare function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt: number | null, busyGrace: WorkerHeartbeatBusyGrace | null, now?: number, timeoutMs?: number): boolean;