@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 +1 -1
- package/dist/cjs/main.cjs +138 -25
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/supervisor.cjs +15 -5
- package/dist/mjs/main.mjs +139 -27
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/supervisor.mjs +12 -4
- package/dist/types/main.d.ts +24 -0
- package/dist/types/supervisor.d.ts +4 -1
- package/package.json +1 -1
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
|
|
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
|
|
3306
|
-
for (const ptyProcess of
|
|
3307
|
-
|
|
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
|
-
|
|
6001
|
-
|
|
6089
|
+
const reconnectDelayMs = (0, import_supervisor.workerReconnectDelayMs)();
|
|
6090
|
+
process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
|
|
6002
6091
|
`);
|
|
6003
|
-
|
|
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
|
-
|
|
6010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/cjs/package.json
CHANGED
package/dist/cjs/supervisor.cjs
CHANGED
|
@@ -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
|
-
|
|
115
|
+
const reconnectDelayMs = reconnectDelay();
|
|
116
|
+
process.stderr.write(`[r5d-worker] reconnecting in ${reconnectDelayMs}ms
|
|
109
117
|
`);
|
|
110
|
-
await waitBeforeReconnect(
|
|
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
|
|
3313
|
-
for (const ptyProcess of
|
|
3314
|
-
|
|
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
|
-
|
|
6008
|
-
|
|
6095
|
+
const reconnectDelayMs = workerReconnectDelayMs();
|
|
6096
|
+
process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
|
|
6009
6097
|
`);
|
|
6010
|
-
|
|
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
|
-
|
|
6017
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/mjs/package.json
CHANGED
package/dist/mjs/supervisor.mjs
CHANGED
|
@@ -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
|
-
|
|
84
|
+
const reconnectDelayMs = reconnectDelay();
|
|
85
|
+
process.stderr.write(`[r5d-worker] reconnecting in ${reconnectDelayMs}ms
|
|
80
86
|
`);
|
|
81
|
-
await waitBeforeReconnect(
|
|
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
|
};
|
package/dist/types/main.d.ts
CHANGED
|
@@ -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 {};
|