@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 +2 -0
- package/dist/cjs/main.cjs +1 -27
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +1 -31
- package/dist/mjs/package.json +1 -1
- package/package.json +1 -1
- package/dist/cjs/heartbeat.cjs +0 -49
- package/dist/mjs/heartbeat.mjs +0 -21
- package/dist/types/heartbeat.d.ts +0 -10
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
|
|
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;
|
package/dist/cjs/package.json
CHANGED
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
|
|
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;
|
package/dist/mjs/package.json
CHANGED
package/package.json
CHANGED
package/dist/cjs/heartbeat.cjs
DELETED
|
@@ -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
|
-
});
|
package/dist/mjs/heartbeat.mjs
DELETED
|
@@ -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;
|