@roamcode.ai/server 1.0.23 → 1.1.0

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.
@@ -0,0 +1,87 @@
1
+ // src/health-watchdog.ts
2
+ import { spawn } from "child_process";
3
+ import { fileURLToPath } from "url";
4
+ var HEALTH_INSTANCE_HEADER = "x-roamcode-instance";
5
+ var WATCHDOG_INITIAL_DELAY_MS = 5e3;
6
+ var WATCHDOG_INTERVAL_MS = 1e4;
7
+ var WATCHDOG_PROBE_TIMEOUT_MS = 3e3;
8
+ var WATCHDOG_FAILURE_THRESHOLD = 4;
9
+ var WATCHDOG_TERMINATION_GRACE_MS = 1e4;
10
+ function boundedInteger(value, min, max) {
11
+ if (!value || !/^\d+$/.test(value)) return void 0;
12
+ const parsed = Number(value);
13
+ return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : void 0;
14
+ }
15
+ function parseHealthWatchdogEnv(env) {
16
+ const parentPid = boundedInteger(env.ROAMCODE_WATCHDOG_PARENT_PID, 2, 2 ** 31 - 1);
17
+ const port = boundedInteger(env.ROAMCODE_WATCHDOG_PORT, 1, 65535);
18
+ const instanceId = env.ROAMCODE_WATCHDOG_INSTANCE_ID;
19
+ if (!parentPid || !port || !instanceId || !/^[A-Za-z0-9_-]{16,128}$/.test(instanceId)) return void 0;
20
+ return { parentPid, port, instanceId };
21
+ }
22
+ async function defaultProbe(config2) {
23
+ try {
24
+ const response = await fetch(`http://127.0.0.1:${config2.port}/health`, {
25
+ cache: "no-store",
26
+ headers: { connection: "close" },
27
+ signal: AbortSignal.timeout(config2.probeTimeoutMs ?? WATCHDOG_PROBE_TIMEOUT_MS)
28
+ });
29
+ const matches = response.status === 200 && response.headers.get(HEALTH_INSTANCE_HEADER) === config2.instanceId;
30
+ await response.body?.cancel().catch(() => void 0);
31
+ return matches;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ function defaultParentAlive(parentPid) {
37
+ if (process.ppid !== parentPid) return false;
38
+ try {
39
+ process.kill(parentPid, 0);
40
+ return true;
41
+ } catch (error) {
42
+ return error.code === "EPERM";
43
+ }
44
+ }
45
+ var defaultSleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
46
+ async function runHealthWatchdog(config2, deps = {}) {
47
+ const probe = deps.probe ?? (() => defaultProbe(config2));
48
+ const parentAlive = deps.parentAlive ?? (() => defaultParentAlive(config2.parentPid));
49
+ const terminateParent = deps.terminateParent ?? ((signal) => {
50
+ try {
51
+ process.kill(config2.parentPid, signal);
52
+ } catch {
53
+ }
54
+ });
55
+ const sleep = deps.sleep ?? defaultSleep;
56
+ const now = deps.now ?? Date.now;
57
+ const log = deps.log ?? ((message) => process.stderr.write(`${message}
58
+ `));
59
+ const threshold = Math.max(1, config2.failureThreshold ?? WATCHDOG_FAILURE_THRESHOLD);
60
+ const intervalMs = config2.intervalMs ?? WATCHDOG_INTERVAL_MS;
61
+ const graceMs = config2.terminationGraceMs ?? WATCHDOG_TERMINATION_GRACE_MS;
62
+ let consecutiveFailures = 0;
63
+ await sleep(config2.initialDelayMs ?? WATCHDOG_INITIAL_DELAY_MS);
64
+ while (parentAlive()) {
65
+ const healthy = await probe().catch(() => false);
66
+ consecutiveFailures = healthy ? 0 : consecutiveFailures + 1;
67
+ if (consecutiveFailures >= threshold) {
68
+ log(
69
+ `[roamcode] health watchdog observed ${consecutiveFailures} consecutive failed probes; restarting the unresponsive server`
70
+ );
71
+ terminateParent("SIGTERM");
72
+ const deadline = now() + graceMs;
73
+ while (parentAlive() && now() < deadline) await sleep(Math.min(250, Math.max(1, deadline - now())));
74
+ if (parentAlive()) terminateParent("SIGKILL");
75
+ return;
76
+ }
77
+ await sleep(intervalMs);
78
+ }
79
+ }
80
+
81
+ // src/health-watchdog-entry.ts
82
+ var config = parseHealthWatchdogEnv(process.env);
83
+ if (!config) process.exit(2);
84
+ void runHealthWatchdog(config).then(
85
+ () => process.exit(0),
86
+ () => process.exit(1)
87
+ );