@ricsam/r5d-worker 0.0.111 → 0.0.113
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 +11 -6
- package/dist/cjs/command-launcher.cjs +345 -0
- package/dist/cjs/inline-output.cjs +62 -0
- package/dist/cjs/main.cjs +149 -102
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/supervisor.cjs +1 -1
- package/dist/mjs/command-launcher.mjs +316 -0
- package/dist/mjs/inline-output.mjs +37 -0
- package/dist/mjs/main.mjs +151 -103
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/supervisor.mjs +1 -1
- package/dist/types/command-launcher.d.ts +61 -0
- package/dist/types/inline-output.d.ts +3 -0
- package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +1 -0
- package/package.json +1 -1
- package/dist/cjs/job-resources.cjs +0 -335
- package/dist/mjs/job-resources.mjs +0 -293
- package/dist/types/job-resources.d.ts +0 -61
package/dist/mjs/main.mjs
CHANGED
|
@@ -6,10 +6,12 @@ import fs from "node:fs";
|
|
|
6
6
|
import { WorkerRecoveryStore, workerOperationDigest } from "./recovery-store.mjs";
|
|
7
7
|
import { WorkerOutputWindow } from "./recovery-output-window.mjs";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
createWorkerCommandLauncher,
|
|
10
|
+
commandLauncherEnvironment,
|
|
11
|
+
CommandLaunchCancelledError,
|
|
12
|
+
CommandLauncherUnavailableError
|
|
13
|
+
} from "./command-launcher.mjs";
|
|
14
|
+
import { collectWorkerInlineOutput } from "./inline-output.mjs";
|
|
13
15
|
import {
|
|
14
16
|
WORKER_RESUMABLE_PROTOCOL,
|
|
15
17
|
WORKER_RECOVERY_TIMEOUT_MS
|
|
@@ -173,7 +175,7 @@ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
|
|
|
173
175
|
const PTY_FOREGROUND_POLL_MS = 1e3;
|
|
174
176
|
const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
|
|
175
177
|
const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
|
|
176
|
-
let
|
|
178
|
+
let workerCommandLauncher;
|
|
177
179
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
178
180
|
const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
|
|
179
181
|
const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
|
|
@@ -193,6 +195,41 @@ let workerExecutionLeaseDeadline = 0;
|
|
|
193
195
|
let workerExecutionLeaseTimer;
|
|
194
196
|
let workerLeaseExpired = false;
|
|
195
197
|
const infrastructureStoppedRunIds = /* @__PURE__ */ new Set();
|
|
198
|
+
let launcherFailureInProgress = false;
|
|
199
|
+
let runtimeExitPromise;
|
|
200
|
+
function fenceWorkerRuntimeOperations() {
|
|
201
|
+
launcherFailureInProgress = true;
|
|
202
|
+
workerAdmissionGeneration += 1;
|
|
203
|
+
for (const operation of recoveryStore?.operations() ?? []) {
|
|
204
|
+
if (operation.state !== "completed") recoveryStore?.unknown(operation.requestId);
|
|
205
|
+
}
|
|
206
|
+
for (const runId of workerCommandLauncher?.activeIds ?? []) {
|
|
207
|
+
infrastructureStoppedRunIds.add(runId);
|
|
208
|
+
cancelledProcessRuns.add(runId);
|
|
209
|
+
recoveryStore?.unknownRun(runId);
|
|
210
|
+
}
|
|
211
|
+
for (const runId of activeProcesses.keys()) infrastructureStoppedRunIds.add(runId);
|
|
212
|
+
}
|
|
213
|
+
function exitWorkerRuntime(code) {
|
|
214
|
+
if (runtimeExitPromise) return runtimeExitPromise;
|
|
215
|
+
fenceWorkerRuntimeOperations();
|
|
216
|
+
runtimeExitPromise = (async () => {
|
|
217
|
+
await Promise.race([
|
|
218
|
+
Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close()]),
|
|
219
|
+
Bun.sleep(4e3)
|
|
220
|
+
]);
|
|
221
|
+
process.exit(code);
|
|
222
|
+
})();
|
|
223
|
+
return runtimeExitPromise;
|
|
224
|
+
}
|
|
225
|
+
function handleWorkerCommandLauncherFailure(error) {
|
|
226
|
+
if (!workerCommandLauncher || launcherFailureInProgress) return;
|
|
227
|
+
fenceWorkerRuntimeOperations();
|
|
228
|
+
process.stderr.write(`[r5d-worker] ${error.message}; stopping runtime for communication recovery
|
|
229
|
+
`);
|
|
230
|
+
currentWorkerSocket?.close(1012, "Command launcher recovery");
|
|
231
|
+
void exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
|
|
232
|
+
}
|
|
196
233
|
function isWorkerCommunicationFailure(message) {
|
|
197
234
|
return /Could not resolve host|Couldn't connect|Failed to connect|Connection reset|Connection timed out|remote end hung up|Empty reply|requested URL returned error: (?:408|429|5\d\d)|fetch failed|socket connection|ECONNRESET|ECONNREFUSED|ETIMEDOUT|Unable to connect|network connection was lost|Worker communication recovery expired|operation was aborted due to timeout|operation timed out|Failed to (?:fetch|download).*\((?:408|429|5\d\d)\)/i.test(
|
|
198
235
|
message
|
|
@@ -222,22 +259,18 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
222
259
|
() => {
|
|
223
260
|
if (Date.now() < workerExecutionLeaseDeadline) return;
|
|
224
261
|
workerLeaseExpired = true;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
} catch (error) {
|
|
228
|
-
process.stderr.write(`[r5d-worker] expired PTY resource termination failed: ${error}
|
|
262
|
+
void workerCommandLauncher?.cancelMatching((id) => id.startsWith("pty:")).catch((error) => {
|
|
263
|
+
process.stderr.write(`[r5d-worker] expired PTY launcher cancellation failed: ${error}
|
|
229
264
|
`);
|
|
230
|
-
}
|
|
265
|
+
});
|
|
231
266
|
closeAllPtys();
|
|
232
267
|
for (const operation of recoveryStore?.operations() ?? []) {
|
|
233
268
|
if (operation.sessionId && operation.state !== "completed") {
|
|
234
269
|
recoveryStore?.cancelSession(operation.sessionId);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
} catch (error) {
|
|
238
|
-
process.stderr.write(`[r5d-worker] expired command resource termination failed: ${error}
|
|
270
|
+
void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
|
|
271
|
+
process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
|
|
239
272
|
`);
|
|
240
|
-
}
|
|
273
|
+
});
|
|
241
274
|
}
|
|
242
275
|
}
|
|
243
276
|
for (const [runId, active] of activeProcesses) {
|
|
@@ -1602,7 +1635,9 @@ async function fenceCredentialGenerationAtConfigureEntry(input) {
|
|
|
1602
1635
|
await input.terminatePreviousGeneration();
|
|
1603
1636
|
return true;
|
|
1604
1637
|
}
|
|
1605
|
-
function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) =>
|
|
1638
|
+
function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => {
|
|
1639
|
+
void exitWorkerRuntime(exitCode2);
|
|
1640
|
+
}, exitCode = 1) {
|
|
1606
1641
|
if (!required) return;
|
|
1607
1642
|
fatalExit(exitCode);
|
|
1608
1643
|
throw new Error("Credential generation restart callback returned unexpectedly");
|
|
@@ -2768,27 +2803,28 @@ async function prepareShellEnvForTarget(input) {
|
|
|
2768
2803
|
...targetProcessEnv(input.rootDir, input.target)
|
|
2769
2804
|
};
|
|
2770
2805
|
}
|
|
2771
|
-
async function
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
process.stderr.write(`[r5d-worker]
|
|
2806
|
+
async function releaseWorkerCommandLaunch(resources) {
|
|
2807
|
+
await resources?.release();
|
|
2808
|
+
}
|
|
2809
|
+
function cancelWorkerCommandLaunch(resources) {
|
|
2810
|
+
void resources.cancel().catch((error) => {
|
|
2811
|
+
process.stderr.write(`[r5d-worker] command launcher cancellation failed: ${error instanceof Error ? error.message : String(error)}
|
|
2777
2812
|
`);
|
|
2778
|
-
}
|
|
2813
|
+
});
|
|
2779
2814
|
}
|
|
2780
|
-
async function
|
|
2815
|
+
async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
|
|
2781
2816
|
const id = message.type === "pty_open" ? `pty:${message.ptyId}` : message.runId;
|
|
2782
2817
|
const operationId = message.type === "pty_open" ? id : `exec:${id}`;
|
|
2783
2818
|
sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: true });
|
|
2784
2819
|
try {
|
|
2785
|
-
return await
|
|
2820
|
+
return await workerCommandLauncher.acquire({
|
|
2786
2821
|
id,
|
|
2787
|
-
|
|
2822
|
+
kind: message.type,
|
|
2823
|
+
..."workspaceEffect" in message && message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
2788
2824
|
..."sessionId" in message && message.sessionId ? { sessionId: message.sessionId } : {},
|
|
2789
2825
|
assertAdmission: () => {
|
|
2790
2826
|
assertAdmission();
|
|
2791
|
-
if (cancelledProcessRuns.has(id)) throw new
|
|
2827
|
+
if (cancelledProcessRuns.has(id)) throw new CommandLaunchCancelledError();
|
|
2792
2828
|
}
|
|
2793
2829
|
});
|
|
2794
2830
|
} finally {
|
|
@@ -2847,7 +2883,7 @@ async function executeCommand(input) {
|
|
|
2847
2883
|
settlePreparation();
|
|
2848
2884
|
if (input.message.timeoutMs) {
|
|
2849
2885
|
timeout = setTimeout(() => {
|
|
2850
|
-
input.resources
|
|
2886
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2851
2887
|
void terminateProcessTree(subprocess).catch((error) => {
|
|
2852
2888
|
process.stderr.write(
|
|
2853
2889
|
`[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -2861,6 +2897,7 @@ async function executeCommand(input) {
|
|
|
2861
2897
|
collectWorkerInlineOutput(subprocess.stderr, void 0, outputAbort.signal)
|
|
2862
2898
|
];
|
|
2863
2899
|
const [stdout, stderr, exitCode] = await Promise.all([outputReaders[0], outputReaders[1], subprocess.exited]);
|
|
2900
|
+
const diagnostic = await input.resources.diagnose();
|
|
2864
2901
|
return {
|
|
2865
2902
|
type: "exec_result",
|
|
2866
2903
|
requestId: input.message.requestId,
|
|
@@ -2868,14 +2905,12 @@ async function executeCommand(input) {
|
|
|
2868
2905
|
stderr,
|
|
2869
2906
|
exitCode,
|
|
2870
2907
|
durationMs: Date.now() - startedAt,
|
|
2871
|
-
...
|
|
2872
|
-
error: `Command exceeded its worker memory limit (${input.resources.memoryBytes} bytes). Reduce parallelism or memory use and retry.`
|
|
2873
|
-
} : {}
|
|
2908
|
+
...diagnostic ? { error: diagnostic } : {}
|
|
2874
2909
|
};
|
|
2875
2910
|
} catch (error) {
|
|
2876
2911
|
outputAbort.abort(error);
|
|
2877
|
-
if (error instanceof StaleWorkerAdmissionError) throw error;
|
|
2878
|
-
input.resources
|
|
2912
|
+
if (error instanceof StaleWorkerAdmissionError || error instanceof CommandLauncherUnavailableError) throw error;
|
|
2913
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2879
2914
|
return {
|
|
2880
2915
|
type: "exec_result",
|
|
2881
2916
|
requestId: input.message.requestId,
|
|
@@ -2895,12 +2930,14 @@ async function executeCommand(input) {
|
|
|
2895
2930
|
}
|
|
2896
2931
|
outputAbort.abort();
|
|
2897
2932
|
await Promise.allSettled(outputReaders);
|
|
2933
|
+
await releaseWorkerCommandLaunch(input.resources);
|
|
2898
2934
|
activeProcesses.delete(input.message.runId);
|
|
2899
2935
|
}
|
|
2900
2936
|
}
|
|
2901
2937
|
async function executeStreamingCommand(input) {
|
|
2902
2938
|
const startedAt = Date.now();
|
|
2903
2939
|
let started = false;
|
|
2940
|
+
let terminal;
|
|
2904
2941
|
let timedOut = false;
|
|
2905
2942
|
let timeout;
|
|
2906
2943
|
let spawnedProcess;
|
|
@@ -2912,7 +2949,7 @@ async function executeStreamingCommand(input) {
|
|
|
2912
2949
|
};
|
|
2913
2950
|
input.setPreparationHeartbeatLease(true);
|
|
2914
2951
|
try {
|
|
2915
|
-
if (cancelledProcessRuns.
|
|
2952
|
+
if (cancelledProcessRuns.has(input.message.runId)) {
|
|
2916
2953
|
sendWorkerMessage(input.ws, {
|
|
2917
2954
|
type: "exec_start_error",
|
|
2918
2955
|
requestId: input.message.requestId,
|
|
@@ -2975,7 +3012,7 @@ async function executeStreamingCommand(input) {
|
|
|
2975
3012
|
if (input.message.timeoutMs) {
|
|
2976
3013
|
timeout = setTimeout(() => {
|
|
2977
3014
|
timedOut = true;
|
|
2978
|
-
input.resources
|
|
3015
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2979
3016
|
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
2980
3017
|
void terminateProcessTree(subprocess).catch((error) => {
|
|
2981
3018
|
process.stderr.write(
|
|
@@ -3004,18 +3041,19 @@ async function executeStreamingCommand(input) {
|
|
|
3004
3041
|
});
|
|
3005
3042
|
})
|
|
3006
3043
|
]);
|
|
3007
|
-
|
|
3044
|
+
const diagnostic = await input.resources.diagnose();
|
|
3045
|
+
if (diagnostic) {
|
|
3008
3046
|
sendWorkerMessage(input.ws, {
|
|
3009
3047
|
type: "exec_output",
|
|
3010
3048
|
runId: input.message.runId,
|
|
3011
3049
|
stream: "stderr",
|
|
3012
3050
|
data: `
|
|
3013
|
-
[r5d-worker]
|
|
3051
|
+
[r5d-worker] ${diagnostic}
|
|
3014
3052
|
`
|
|
3015
3053
|
});
|
|
3016
3054
|
}
|
|
3017
3055
|
const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
|
|
3018
|
-
|
|
3056
|
+
terminal = {
|
|
3019
3057
|
type: "exec_exit",
|
|
3020
3058
|
runId: input.message.runId,
|
|
3021
3059
|
exitCode,
|
|
@@ -3024,14 +3062,12 @@ async function executeStreamingCommand(input) {
|
|
|
3024
3062
|
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
3025
3063
|
...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
|
|
3026
3064
|
};
|
|
3027
|
-
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
3028
|
-
sendWorkerMessage(input.ws, terminal);
|
|
3029
3065
|
} catch (error) {
|
|
3030
|
-
if (!started && error instanceof StaleWorkerAdmissionError) throw error;
|
|
3066
|
+
if (error instanceof CommandLauncherUnavailableError || !started && error instanceof StaleWorkerAdmissionError) throw error;
|
|
3031
3067
|
const message = error instanceof Error ? error.message : String(error);
|
|
3032
3068
|
if (started) {
|
|
3033
3069
|
const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
|
|
3034
|
-
|
|
3070
|
+
terminal = {
|
|
3035
3071
|
type: "exec_error",
|
|
3036
3072
|
runId: input.message.runId,
|
|
3037
3073
|
error: message,
|
|
@@ -3039,8 +3075,6 @@ async function executeStreamingCommand(input) {
|
|
|
3039
3075
|
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
3040
3076
|
...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
|
|
3041
3077
|
};
|
|
3042
|
-
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
3043
|
-
sendWorkerMessage(input.ws, terminal);
|
|
3044
3078
|
} else {
|
|
3045
3079
|
sendWorkerMessage(input.ws, {
|
|
3046
3080
|
type: "exec_start_error",
|
|
@@ -3058,9 +3092,14 @@ async function executeStreamingCommand(input) {
|
|
|
3058
3092
|
if (spawnedProcess) {
|
|
3059
3093
|
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
3060
3094
|
}
|
|
3095
|
+
await releaseWorkerCommandLaunch(input.resources);
|
|
3061
3096
|
activeProcesses.delete(input.message.runId);
|
|
3062
3097
|
cancelledProcessRuns.delete(input.message.runId);
|
|
3063
3098
|
}
|
|
3099
|
+
if (terminal) {
|
|
3100
|
+
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
3101
|
+
sendWorkerMessage(input.ws, terminal);
|
|
3102
|
+
}
|
|
3064
3103
|
}
|
|
3065
3104
|
async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
|
|
3066
3105
|
try {
|
|
@@ -3100,6 +3139,7 @@ function sendReplayWorkerMessage(ws, message) {
|
|
|
3100
3139
|
return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
|
|
3101
3140
|
}
|
|
3102
3141
|
function sendWorkerMessage(ws, message) {
|
|
3142
|
+
if (launcherFailureInProgress) return;
|
|
3103
3143
|
if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
|
|
3104
3144
|
return;
|
|
3105
3145
|
const transportError = "error" in message && typeof message.error === "string" ? message.error : message.type === "workspace_sync_result" && message.result.outcome === "failed" ? message.result.error : void 0;
|
|
@@ -3461,6 +3501,29 @@ async function openPty(input) {
|
|
|
3461
3501
|
envFilesRemoved = true;
|
|
3462
3502
|
removePtyEnvFiles(stagedEnvFiles.paths);
|
|
3463
3503
|
};
|
|
3504
|
+
let terminalHandled = false;
|
|
3505
|
+
const finishPty = async (terminal) => {
|
|
3506
|
+
if (terminalHandled) return;
|
|
3507
|
+
terminalHandled = true;
|
|
3508
|
+
try {
|
|
3509
|
+
const diagnostic = await input.resources.diagnose();
|
|
3510
|
+
if (diagnostic) outputCoalescer.push(`\r
|
|
3511
|
+
[r5d-worker] ${diagnostic}\r
|
|
3512
|
+
`);
|
|
3513
|
+
} finally {
|
|
3514
|
+
cleanupEnvFiles();
|
|
3515
|
+
await releaseWorkerCommandLaunch(input.resources);
|
|
3516
|
+
outputCoalescer.flush();
|
|
3517
|
+
activePtys.delete(input.message.ptyId);
|
|
3518
|
+
input.releaseWorkspaceMutation?.();
|
|
3519
|
+
input.onTerminal?.();
|
|
3520
|
+
}
|
|
3521
|
+
sendWorkerMessage(input.ws, terminal);
|
|
3522
|
+
};
|
|
3523
|
+
const reportPtyCleanupFailure = (error) => {
|
|
3524
|
+
process.stderr.write(`[r5d-worker] PTY launcher cleanup failed: ${error instanceof Error ? error.message : String(error)}
|
|
3525
|
+
`);
|
|
3526
|
+
};
|
|
3464
3527
|
try {
|
|
3465
3528
|
const ptyProcess = createNodePtyBridge({
|
|
3466
3529
|
file: shellArgv[0],
|
|
@@ -3493,38 +3556,14 @@ async function openPty(input) {
|
|
|
3493
3556
|
outputCoalescer.push(data);
|
|
3494
3557
|
},
|
|
3495
3558
|
onExit: (event) => {
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
[r5d-worker] Terminal exceeded its memory limit (${input.resources.memoryBytes} bytes). Reduce parallelism or memory use and retry.\r
|
|
3500
|
-
`
|
|
3501
|
-
);
|
|
3502
|
-
outputCoalescer.flush();
|
|
3503
|
-
input.releaseWorkspaceMutation?.();
|
|
3504
|
-
activePtys.delete(input.message.ptyId);
|
|
3505
|
-
cleanupEnvFiles();
|
|
3506
|
-
input.onTerminal?.();
|
|
3507
|
-
void releaseWorkerJobResources(input.resources);
|
|
3508
|
-
sendWorkerMessage(input.ws, {
|
|
3509
|
-
type: "pty_exit",
|
|
3510
|
-
ptyId: input.message.ptyId,
|
|
3511
|
-
exitCode: event.exitCode,
|
|
3512
|
-
signal: event.signal
|
|
3513
|
-
});
|
|
3559
|
+
void finishPty({ type: "pty_exit", ptyId: input.message.ptyId, exitCode: event.exitCode, signal: event.signal }).catch(
|
|
3560
|
+
reportPtyCleanupFailure
|
|
3561
|
+
);
|
|
3514
3562
|
},
|
|
3515
3563
|
onError: (error) => {
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
cleanupEnvFiles();
|
|
3520
|
-
input.onTerminal?.();
|
|
3521
|
-
void releaseWorkerJobResources(input.resources);
|
|
3522
|
-
sendWorkerMessage(input.ws, {
|
|
3523
|
-
type: "pty_error",
|
|
3524
|
-
requestId: input.message.requestId,
|
|
3525
|
-
ptyId: input.message.ptyId,
|
|
3526
|
-
error: error.message
|
|
3527
|
-
});
|
|
3564
|
+
void finishPty({ type: "pty_error", requestId: input.message.requestId, ptyId: input.message.ptyId, error: error.message }).catch(
|
|
3565
|
+
reportPtyCleanupFailure
|
|
3566
|
+
);
|
|
3528
3567
|
},
|
|
3529
3568
|
onTerminationError: (error) => {
|
|
3530
3569
|
sendWorkerMessage(input.ws, {
|
|
@@ -3567,8 +3606,12 @@ function resizePty(message) {
|
|
|
3567
3606
|
}
|
|
3568
3607
|
ptyProcess.resize(Math.max(1, Math.min(Math.floor(message.cols || 80), 500)), Math.max(1, Math.min(Math.floor(message.rows || 24), 500)));
|
|
3569
3608
|
}
|
|
3570
|
-
function closePty(message) {
|
|
3571
|
-
|
|
3609
|
+
async function closePty(message) {
|
|
3610
|
+
try {
|
|
3611
|
+
await workerCommandLauncher?.cancel(`pty:${message.ptyId}`);
|
|
3612
|
+
} finally {
|
|
3613
|
+
activePtys.get(message.ptyId)?.kill();
|
|
3614
|
+
}
|
|
3572
3615
|
const ptyProcess = activePtys.get(message.ptyId);
|
|
3573
3616
|
if (!ptyProcess) {
|
|
3574
3617
|
return;
|
|
@@ -3660,9 +3703,15 @@ async function startWorker(options, projectRuntime = {
|
|
|
3660
3703
|
initializeCredentialStoreDirectoryOnce();
|
|
3661
3704
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
3662
3705
|
validateLabel(label);
|
|
3663
|
-
if (!
|
|
3664
|
-
|
|
3665
|
-
|
|
3706
|
+
if (!workerCommandLauncher) {
|
|
3707
|
+
const launcher = await createWorkerCommandLauncher({
|
|
3708
|
+
configuration: process.env.R5D_WORKER_COMMAND_LAUNCHER,
|
|
3709
|
+
env: commandLauncherEnvironment(),
|
|
3710
|
+
onFailure: handleWorkerCommandLauncherFailure
|
|
3711
|
+
});
|
|
3712
|
+
workerCommandLauncher = launcher;
|
|
3713
|
+
await launcher.initialize();
|
|
3714
|
+
process.stdout.write(`[r5d-worker] command launcher: ${launcher.mode}
|
|
3666
3715
|
`);
|
|
3667
3716
|
}
|
|
3668
3717
|
const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
@@ -4905,7 +4954,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4905
4954
|
`[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
|
|
4906
4955
|
`
|
|
4907
4956
|
);
|
|
4908
|
-
|
|
4957
|
+
void exitWorkerRuntime(exitCode);
|
|
4909
4958
|
},
|
|
4910
4959
|
exitCode: 1
|
|
4911
4960
|
});
|
|
@@ -5716,7 +5765,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5716
5765
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
5717
5766
|
ws.close(1e3, "Worker shutting down");
|
|
5718
5767
|
} else {
|
|
5719
|
-
|
|
5768
|
+
await exitWorkerRuntime(0);
|
|
5720
5769
|
}
|
|
5721
5770
|
})().catch((error) => {
|
|
5722
5771
|
process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -5869,14 +5918,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
5869
5918
|
}
|
|
5870
5919
|
if (message.type === "cancel_session") {
|
|
5871
5920
|
recoveryStore.cancelSession(message.sessionId);
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
5921
|
+
await Promise.all([
|
|
5922
|
+
workerCommandLauncher?.cancelSession(message.sessionId),
|
|
5923
|
+
...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
5875
5924
|
cancelledProcessRuns.add(runId);
|
|
5876
5925
|
closeProcessStdin(active);
|
|
5877
5926
|
await terminateProcessTree(active.process);
|
|
5878
5927
|
})
|
|
5879
|
-
);
|
|
5928
|
+
]);
|
|
5880
5929
|
sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
|
|
5881
5930
|
return;
|
|
5882
5931
|
}
|
|
@@ -6323,7 +6372,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6323
6372
|
});
|
|
6324
6373
|
return;
|
|
6325
6374
|
}
|
|
6326
|
-
if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
|
|
6375
|
+
if (activeProcesses.size > 0 || (workerCommandLauncher?.activeIds.length ?? 0) > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
|
|
6327
6376
|
sendWorkerMessage(ws, {
|
|
6328
6377
|
type: "update_clis_result",
|
|
6329
6378
|
requestId: message.requestId,
|
|
@@ -6358,7 +6407,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6358
6407
|
if (message.type === "cancel") {
|
|
6359
6408
|
let resourceCancellationError;
|
|
6360
6409
|
try {
|
|
6361
|
-
|
|
6410
|
+
await workerCommandLauncher?.cancel(message.runId);
|
|
6362
6411
|
} catch (error) {
|
|
6363
6412
|
resourceCancellationError = error instanceof Error ? error.message : String(error);
|
|
6364
6413
|
}
|
|
@@ -6470,7 +6519,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6470
6519
|
let releaseWorkspaceMutation;
|
|
6471
6520
|
let mutationLeaseTransferred = false;
|
|
6472
6521
|
try {
|
|
6473
|
-
resources = await
|
|
6522
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6474
6523
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
6475
6524
|
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
6476
6525
|
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
@@ -6509,8 +6558,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6509
6558
|
});
|
|
6510
6559
|
} finally {
|
|
6511
6560
|
workspaceSyncPriorityPtyTargets.delete(message.ptyId);
|
|
6561
|
+
if (!resourcesTransferred) await releaseWorkerCommandLaunch(resources);
|
|
6512
6562
|
if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
|
|
6513
|
-
if (!resourcesTransferred) await releaseWorkerJobResources(resources);
|
|
6514
6563
|
}
|
|
6515
6564
|
return;
|
|
6516
6565
|
}
|
|
@@ -6539,7 +6588,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6539
6588
|
}
|
|
6540
6589
|
if (message.type === "pty_close") {
|
|
6541
6590
|
const activePty = activePtys.get(message.ptyId);
|
|
6542
|
-
closePty(message);
|
|
6591
|
+
await closePty(message);
|
|
6543
6592
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6544
6593
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
6545
6594
|
}
|
|
@@ -6573,7 +6622,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6573
6622
|
let targetReserved = false;
|
|
6574
6623
|
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
6575
6624
|
try {
|
|
6576
|
-
resources = await
|
|
6625
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6577
6626
|
if (hasWorkspaceEffect) {
|
|
6578
6627
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
6579
6628
|
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
@@ -6627,7 +6676,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6627
6676
|
};
|
|
6628
6677
|
} finally {
|
|
6629
6678
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6630
|
-
await
|
|
6679
|
+
await releaseWorkerCommandLaunch(resources);
|
|
6631
6680
|
cancelledProcessRuns.delete(message.runId);
|
|
6632
6681
|
}
|
|
6633
6682
|
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
@@ -6701,7 +6750,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6701
6750
|
const execution = (async () => {
|
|
6702
6751
|
let targetReserved = false;
|
|
6703
6752
|
try {
|
|
6704
|
-
resources = await
|
|
6753
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6705
6754
|
if (hasWorkspaceEffect) {
|
|
6706
6755
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
6707
6756
|
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
@@ -6717,7 +6766,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6717
6766
|
});
|
|
6718
6767
|
} finally {
|
|
6719
6768
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6720
|
-
await
|
|
6769
|
+
await releaseWorkerCommandLaunch(resources);
|
|
6721
6770
|
cancelledProcessRuns.delete(message.runId);
|
|
6722
6771
|
}
|
|
6723
6772
|
})();
|
|
@@ -6849,11 +6898,10 @@ async function startWorker(options, projectRuntime = {
|
|
|
6849
6898
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6850
6899
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
6851
6900
|
`);
|
|
6852
|
-
if (
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
process.exit(0);
|
|
6901
|
+
if (launcherFailureInProgress) return;
|
|
6902
|
+
if (reloadAfterClose || shutdownAfterClose) {
|
|
6903
|
+
void exitWorkerRuntime(reloadAfterClose ? WORKER_RELOAD_EXIT_CODE : 0);
|
|
6904
|
+
return;
|
|
6857
6905
|
}
|
|
6858
6906
|
const reconnect = () => {
|
|
6859
6907
|
const delay = workerReconnectDelayMs(Math.random, workerReconnectAttempt++);
|
|
@@ -6911,10 +6959,10 @@ async function main() {
|
|
|
6911
6959
|
try {
|
|
6912
6960
|
await startWorker(parsed.options);
|
|
6913
6961
|
} catch (error) {
|
|
6914
|
-
if (error instanceof WorkerServerUnavailableError) {
|
|
6962
|
+
if (error instanceof WorkerServerUnavailableError || error instanceof CommandLauncherUnavailableError) {
|
|
6915
6963
|
process.stderr.write(`[r5d-worker] ${error.message}
|
|
6916
6964
|
`);
|
|
6917
|
-
|
|
6965
|
+
await exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
|
|
6918
6966
|
}
|
|
6919
6967
|
throw error;
|
|
6920
6968
|
}
|
|
@@ -6932,10 +6980,10 @@ function isCliEntrypoint() {
|
|
|
6932
6980
|
return typeof require === "function" && typeof module !== "undefined" && require.main === module;
|
|
6933
6981
|
}
|
|
6934
6982
|
if (isCliEntrypoint()) {
|
|
6935
|
-
main().catch((error) => {
|
|
6983
|
+
main().catch(async (error) => {
|
|
6936
6984
|
process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
|
|
6937
6985
|
`);
|
|
6938
|
-
|
|
6986
|
+
await exitWorkerRuntime(1);
|
|
6939
6987
|
});
|
|
6940
6988
|
}
|
|
6941
6989
|
export {
|
package/dist/mjs/package.json
CHANGED
package/dist/mjs/supervisor.mjs
CHANGED
|
@@ -22,7 +22,7 @@ function workerReconnectDelayMs(random = Math.random, attempt = 0) {
|
|
|
22
22
|
return floor + Math.floor(sample * (ceiling - floor));
|
|
23
23
|
}
|
|
24
24
|
async function defaultRuntimeReaper(runtime) {
|
|
25
|
-
await terminateProcessTree(runtime);
|
|
25
|
+
await terminateProcessTree(runtime, { graceMs: 6e3 });
|
|
26
26
|
}
|
|
27
27
|
function isRetryableWorkerServerStatus(status) {
|
|
28
28
|
return status === 408 || status === 429 || status >= 500;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type CommandLaunchRequest = {
|
|
2
|
+
id: string;
|
|
3
|
+
kind: "exec" | "exec_start" | "pty_open";
|
|
4
|
+
workspaceEffect?: "none";
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
assertAdmission: () => void;
|
|
7
|
+
};
|
|
8
|
+
export type CommandLaunchLease = {
|
|
9
|
+
wrap(argv: string[]): string[];
|
|
10
|
+
cancel(): Promise<void>;
|
|
11
|
+
diagnose(): Promise<string | undefined>;
|
|
12
|
+
release(): Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export declare class CommandLauncherUnavailableError extends Error {
|
|
15
|
+
constructor(message: string, cause?: unknown);
|
|
16
|
+
}
|
|
17
|
+
export declare class CommandLaunchCancelledError extends Error {
|
|
18
|
+
constructor();
|
|
19
|
+
}
|
|
20
|
+
export declare function parseCommandLauncher(configuration: string | undefined): string[] | undefined;
|
|
21
|
+
/** Launcher configuration is operator-owned; do not inherit worker or package credentials. */
|
|
22
|
+
export declare function commandLauncherEnvironment(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
23
|
+
/** Generic optional JSONL controller; policy and OS-specific launch prefixes live outside the worker. */
|
|
24
|
+
export declare class WorkerCommandLauncher {
|
|
25
|
+
private readonly options;
|
|
26
|
+
readonly mode: "direct" | "external";
|
|
27
|
+
private readonly child?;
|
|
28
|
+
private readonly pending;
|
|
29
|
+
private readonly launches;
|
|
30
|
+
private readonly exited;
|
|
31
|
+
private failure?;
|
|
32
|
+
private closing;
|
|
33
|
+
private initialized;
|
|
34
|
+
private closePromise?;
|
|
35
|
+
private sequence;
|
|
36
|
+
constructor(options?: {
|
|
37
|
+
argv?: string[];
|
|
38
|
+
env?: NodeJS.ProcessEnv;
|
|
39
|
+
onFailure?: (error: Error, ids: string[]) => void;
|
|
40
|
+
onStderr?: (text: string) => void;
|
|
41
|
+
requestTimeoutMs?: number;
|
|
42
|
+
acquisitionTimeoutMs?: number;
|
|
43
|
+
});
|
|
44
|
+
initialize(): Promise<void>;
|
|
45
|
+
get activeIds(): string[];
|
|
46
|
+
acquire(request: CommandLaunchRequest): Promise<CommandLaunchLease>;
|
|
47
|
+
cancel(id: string): Promise<void>;
|
|
48
|
+
cancelSession(sessionId: string): Promise<void>;
|
|
49
|
+
cancelMatching(matches: (id: string, sessionId?: string) => boolean): Promise<void>;
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
private waitForExit;
|
|
52
|
+
private releaseRecord;
|
|
53
|
+
private assertHealthy;
|
|
54
|
+
private rpc;
|
|
55
|
+
private receive;
|
|
56
|
+
private rejectPending;
|
|
57
|
+
private fail;
|
|
58
|
+
}
|
|
59
|
+
export declare function createWorkerCommandLauncher(options?: Omit<NonNullable<ConstructorParameters<typeof WorkerCommandLauncher>[0]>, "argv"> & {
|
|
60
|
+
configuration?: string;
|
|
61
|
+
}): Promise<WorkerCommandLauncher>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const WORKER_INLINE_OUTPUT_MAX_BYTES: number;
|
|
2
|
+
/** Bound inline RPC buffers in the worker regardless of child launch policy. */
|
|
3
|
+
export declare function collectWorkerInlineOutput(stream: ReadableStream<Uint8Array> | null, maxBytes?: number, signal?: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|