@ricsam/r5d-worker 0.0.110 → 0.0.112
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 +17 -0
- package/dist/cjs/command-launcher.cjs +345 -0
- package/dist/cjs/inline-output.cjs +62 -0
- package/dist/cjs/main.cjs +219 -74
- 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 +224 -74
- 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/mjs/main.mjs
CHANGED
|
@@ -5,6 +5,13 @@ import { lintPlans, formatPlanLintErrors } from "./plan-lint.mjs";
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import { WorkerRecoveryStore, workerOperationDigest } from "./recovery-store.mjs";
|
|
7
7
|
import { WorkerOutputWindow } from "./recovery-output-window.mjs";
|
|
8
|
+
import {
|
|
9
|
+
createWorkerCommandLauncher,
|
|
10
|
+
commandLauncherEnvironment,
|
|
11
|
+
CommandLaunchCancelledError,
|
|
12
|
+
CommandLauncherUnavailableError
|
|
13
|
+
} from "./command-launcher.mjs";
|
|
14
|
+
import { collectWorkerInlineOutput } from "./inline-output.mjs";
|
|
8
15
|
import {
|
|
9
16
|
WORKER_RESUMABLE_PROTOCOL,
|
|
10
17
|
WORKER_RECOVERY_TIMEOUT_MS
|
|
@@ -168,6 +175,7 @@ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
|
|
|
168
175
|
const PTY_FOREGROUND_POLL_MS = 1e3;
|
|
169
176
|
const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
|
|
170
177
|
const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
|
|
178
|
+
let workerCommandLauncher;
|
|
171
179
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
172
180
|
const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
|
|
173
181
|
const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
|
|
@@ -187,6 +195,41 @@ let workerExecutionLeaseDeadline = 0;
|
|
|
187
195
|
let workerExecutionLeaseTimer;
|
|
188
196
|
let workerLeaseExpired = false;
|
|
189
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
|
+
}
|
|
190
233
|
function isWorkerCommunicationFailure(message) {
|
|
191
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(
|
|
192
235
|
message
|
|
@@ -216,8 +259,19 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
216
259
|
() => {
|
|
217
260
|
if (Date.now() < workerExecutionLeaseDeadline) return;
|
|
218
261
|
workerLeaseExpired = true;
|
|
262
|
+
void workerCommandLauncher?.cancelMatching((id) => id.startsWith("pty:")).catch((error) => {
|
|
263
|
+
process.stderr.write(`[r5d-worker] expired PTY launcher cancellation failed: ${error}
|
|
264
|
+
`);
|
|
265
|
+
});
|
|
266
|
+
closeAllPtys();
|
|
219
267
|
for (const operation of recoveryStore?.operations() ?? []) {
|
|
220
|
-
if (operation.sessionId && operation.state !== "completed")
|
|
268
|
+
if (operation.sessionId && operation.state !== "completed") {
|
|
269
|
+
recoveryStore?.cancelSession(operation.sessionId);
|
|
270
|
+
void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
|
|
271
|
+
process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
|
|
272
|
+
`);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
221
275
|
}
|
|
222
276
|
for (const [runId, active] of activeProcesses) {
|
|
223
277
|
if (!active.sessionId) continue;
|
|
@@ -1581,7 +1635,9 @@ async function fenceCredentialGenerationAtConfigureEntry(input) {
|
|
|
1581
1635
|
await input.terminatePreviousGeneration();
|
|
1582
1636
|
return true;
|
|
1583
1637
|
}
|
|
1584
|
-
function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) =>
|
|
1638
|
+
function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => {
|
|
1639
|
+
void exitWorkerRuntime(exitCode2);
|
|
1640
|
+
}, exitCode = 1) {
|
|
1585
1641
|
if (!required) return;
|
|
1586
1642
|
fatalExit(exitCode);
|
|
1587
1643
|
throw new Error("Credential generation restart callback returned unexpectedly");
|
|
@@ -1611,7 +1667,7 @@ function credentialReapContractStatus(probe) {
|
|
|
1611
1667
|
if (process.platform !== "linux") return "direct";
|
|
1612
1668
|
try {
|
|
1613
1669
|
const cgroupText = fs.readFileSync("/proc/self/cgroup", "utf8");
|
|
1614
|
-
const unit =
|
|
1670
|
+
const unit = /(?:^|\/)r5d-worker\.service(?:\/|$|\n)/m.test(cgroupText) ? "r5d-worker.service" : void 0;
|
|
1615
1671
|
if (unit !== "r5d-worker.service") {
|
|
1616
1672
|
return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? "systemd_unverified" : "direct";
|
|
1617
1673
|
}
|
|
@@ -1645,7 +1701,7 @@ function credentialReapContractStatus(probe) {
|
|
|
1645
1701
|
}
|
|
1646
1702
|
}
|
|
1647
1703
|
if (resolved.platform !== "linux") return "direct";
|
|
1648
|
-
if (!/(?:^|\/)r5d-worker\.service(
|
|
1704
|
+
if (!/(?:^|\/)r5d-worker\.service(?:\/|$|\n)/m.test(resolved.cgroupText)) {
|
|
1649
1705
|
return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "") ? "systemd_unverified" : "direct";
|
|
1650
1706
|
}
|
|
1651
1707
|
if (!SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "")) return "systemd_unverified";
|
|
@@ -2027,12 +2083,6 @@ function resolveCommandCwd(branchPath, cwd) {
|
|
|
2027
2083
|
}
|
|
2028
2084
|
return resolved;
|
|
2029
2085
|
}
|
|
2030
|
-
async function collectStream(stream) {
|
|
2031
|
-
if (!stream) {
|
|
2032
|
-
return "";
|
|
2033
|
-
}
|
|
2034
|
-
return await new Response(stream).text();
|
|
2035
|
-
}
|
|
2036
2086
|
async function streamCommandOutput(stream, onData) {
|
|
2037
2087
|
if (!stream) {
|
|
2038
2088
|
return;
|
|
@@ -2753,8 +2803,38 @@ async function prepareShellEnvForTarget(input) {
|
|
|
2753
2803
|
...targetProcessEnv(input.rootDir, input.target)
|
|
2754
2804
|
};
|
|
2755
2805
|
}
|
|
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)}
|
|
2812
|
+
`);
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
|
|
2816
|
+
const id = message.type === "pty_open" ? `pty:${message.ptyId}` : message.runId;
|
|
2817
|
+
const operationId = message.type === "pty_open" ? id : `exec:${id}`;
|
|
2818
|
+
sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: true });
|
|
2819
|
+
try {
|
|
2820
|
+
return await workerCommandLauncher.acquire({
|
|
2821
|
+
id,
|
|
2822
|
+
kind: message.type,
|
|
2823
|
+
..."workspaceEffect" in message && message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
2824
|
+
..."sessionId" in message && message.sessionId ? { sessionId: message.sessionId } : {},
|
|
2825
|
+
assertAdmission: () => {
|
|
2826
|
+
assertAdmission();
|
|
2827
|
+
if (cancelledProcessRuns.has(id)) throw new CommandLaunchCancelledError();
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
} finally {
|
|
2831
|
+
sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: false });
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2756
2834
|
async function executeCommand(input) {
|
|
2757
2835
|
const startedAt = Date.now();
|
|
2836
|
+
const outputAbort = new AbortController();
|
|
2837
|
+
let outputReaders = [];
|
|
2758
2838
|
let timeout;
|
|
2759
2839
|
let spawnedProcess;
|
|
2760
2840
|
let preparationSettled = false;
|
|
@@ -2775,7 +2855,7 @@ async function executeCommand(input) {
|
|
|
2775
2855
|
});
|
|
2776
2856
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
2777
2857
|
input.assertAdmission();
|
|
2778
|
-
const subprocess = Bun.spawn(input.message.argv, {
|
|
2858
|
+
const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
|
|
2779
2859
|
cwd,
|
|
2780
2860
|
stdout: "pipe",
|
|
2781
2861
|
stderr: "pipe",
|
|
@@ -2803,6 +2883,7 @@ async function executeCommand(input) {
|
|
|
2803
2883
|
settlePreparation();
|
|
2804
2884
|
if (input.message.timeoutMs) {
|
|
2805
2885
|
timeout = setTimeout(() => {
|
|
2886
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2806
2887
|
void terminateProcessTree(subprocess).catch((error) => {
|
|
2807
2888
|
process.stderr.write(
|
|
2808
2889
|
`[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -2811,21 +2892,25 @@ async function executeCommand(input) {
|
|
|
2811
2892
|
});
|
|
2812
2893
|
}, input.message.timeoutMs);
|
|
2813
2894
|
}
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
]);
|
|
2895
|
+
outputReaders = [
|
|
2896
|
+
collectWorkerInlineOutput(subprocess.stdout, void 0, outputAbort.signal),
|
|
2897
|
+
collectWorkerInlineOutput(subprocess.stderr, void 0, outputAbort.signal)
|
|
2898
|
+
];
|
|
2899
|
+
const [stdout, stderr, exitCode] = await Promise.all([outputReaders[0], outputReaders[1], subprocess.exited]);
|
|
2900
|
+
const diagnostic = await input.resources.diagnose();
|
|
2819
2901
|
return {
|
|
2820
2902
|
type: "exec_result",
|
|
2821
2903
|
requestId: input.message.requestId,
|
|
2822
2904
|
stdout,
|
|
2823
2905
|
stderr,
|
|
2824
2906
|
exitCode,
|
|
2825
|
-
durationMs: Date.now() - startedAt
|
|
2907
|
+
durationMs: Date.now() - startedAt,
|
|
2908
|
+
...diagnostic ? { error: diagnostic } : {}
|
|
2826
2909
|
};
|
|
2827
2910
|
} catch (error) {
|
|
2828
|
-
|
|
2911
|
+
outputAbort.abort(error);
|
|
2912
|
+
if (error instanceof StaleWorkerAdmissionError || error instanceof CommandLauncherUnavailableError) throw error;
|
|
2913
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2829
2914
|
return {
|
|
2830
2915
|
type: "exec_result",
|
|
2831
2916
|
requestId: input.message.requestId,
|
|
@@ -2843,12 +2928,16 @@ async function executeCommand(input) {
|
|
|
2843
2928
|
if (spawnedProcess) {
|
|
2844
2929
|
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
2845
2930
|
}
|
|
2931
|
+
outputAbort.abort();
|
|
2932
|
+
await Promise.allSettled(outputReaders);
|
|
2933
|
+
await releaseWorkerCommandLaunch(input.resources);
|
|
2846
2934
|
activeProcesses.delete(input.message.runId);
|
|
2847
2935
|
}
|
|
2848
2936
|
}
|
|
2849
2937
|
async function executeStreamingCommand(input) {
|
|
2850
2938
|
const startedAt = Date.now();
|
|
2851
2939
|
let started = false;
|
|
2940
|
+
let terminal;
|
|
2852
2941
|
let timedOut = false;
|
|
2853
2942
|
let timeout;
|
|
2854
2943
|
let spawnedProcess;
|
|
@@ -2860,7 +2949,7 @@ async function executeStreamingCommand(input) {
|
|
|
2860
2949
|
};
|
|
2861
2950
|
input.setPreparationHeartbeatLease(true);
|
|
2862
2951
|
try {
|
|
2863
|
-
if (cancelledProcessRuns.
|
|
2952
|
+
if (cancelledProcessRuns.has(input.message.runId)) {
|
|
2864
2953
|
sendWorkerMessage(input.ws, {
|
|
2865
2954
|
type: "exec_start_error",
|
|
2866
2955
|
requestId: input.message.requestId,
|
|
@@ -2880,7 +2969,7 @@ async function executeStreamingCommand(input) {
|
|
|
2880
2969
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
2881
2970
|
const interactive = input.message.interactive === true;
|
|
2882
2971
|
input.assertAdmission();
|
|
2883
|
-
const subprocess = Bun.spawn(input.message.argv, {
|
|
2972
|
+
const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
|
|
2884
2973
|
cwd,
|
|
2885
2974
|
// Without an explicit stdin the process reads /dev/null and interactive
|
|
2886
2975
|
// prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
|
|
@@ -2923,6 +3012,7 @@ async function executeStreamingCommand(input) {
|
|
|
2923
3012
|
if (input.message.timeoutMs) {
|
|
2924
3013
|
timeout = setTimeout(() => {
|
|
2925
3014
|
timedOut = true;
|
|
3015
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2926
3016
|
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
2927
3017
|
void terminateProcessTree(subprocess).catch((error) => {
|
|
2928
3018
|
process.stderr.write(
|
|
@@ -2951,8 +3041,19 @@ async function executeStreamingCommand(input) {
|
|
|
2951
3041
|
});
|
|
2952
3042
|
})
|
|
2953
3043
|
]);
|
|
3044
|
+
const diagnostic = await input.resources.diagnose();
|
|
3045
|
+
if (diagnostic) {
|
|
3046
|
+
sendWorkerMessage(input.ws, {
|
|
3047
|
+
type: "exec_output",
|
|
3048
|
+
runId: input.message.runId,
|
|
3049
|
+
stream: "stderr",
|
|
3050
|
+
data: `
|
|
3051
|
+
[r5d-worker] ${diagnostic}
|
|
3052
|
+
`
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
2954
3055
|
const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
|
|
2955
|
-
|
|
3056
|
+
terminal = {
|
|
2956
3057
|
type: "exec_exit",
|
|
2957
3058
|
runId: input.message.runId,
|
|
2958
3059
|
exitCode,
|
|
@@ -2961,14 +3062,12 @@ async function executeStreamingCommand(input) {
|
|
|
2961
3062
|
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
2962
3063
|
...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
|
|
2963
3064
|
};
|
|
2964
|
-
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
2965
|
-
sendWorkerMessage(input.ws, terminal);
|
|
2966
3065
|
} catch (error) {
|
|
2967
|
-
if (!started && error instanceof StaleWorkerAdmissionError) throw error;
|
|
3066
|
+
if (error instanceof CommandLauncherUnavailableError || !started && error instanceof StaleWorkerAdmissionError) throw error;
|
|
2968
3067
|
const message = error instanceof Error ? error.message : String(error);
|
|
2969
3068
|
if (started) {
|
|
2970
3069
|
const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
|
|
2971
|
-
|
|
3070
|
+
terminal = {
|
|
2972
3071
|
type: "exec_error",
|
|
2973
3072
|
runId: input.message.runId,
|
|
2974
3073
|
error: message,
|
|
@@ -2976,8 +3075,6 @@ async function executeStreamingCommand(input) {
|
|
|
2976
3075
|
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
2977
3076
|
...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
|
|
2978
3077
|
};
|
|
2979
|
-
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
2980
|
-
sendWorkerMessage(input.ws, terminal);
|
|
2981
3078
|
} else {
|
|
2982
3079
|
sendWorkerMessage(input.ws, {
|
|
2983
3080
|
type: "exec_start_error",
|
|
@@ -2995,9 +3092,14 @@ async function executeStreamingCommand(input) {
|
|
|
2995
3092
|
if (spawnedProcess) {
|
|
2996
3093
|
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
2997
3094
|
}
|
|
3095
|
+
await releaseWorkerCommandLaunch(input.resources);
|
|
2998
3096
|
activeProcesses.delete(input.message.runId);
|
|
2999
3097
|
cancelledProcessRuns.delete(input.message.runId);
|
|
3000
3098
|
}
|
|
3099
|
+
if (terminal) {
|
|
3100
|
+
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
3101
|
+
sendWorkerMessage(input.ws, terminal);
|
|
3102
|
+
}
|
|
3001
3103
|
}
|
|
3002
3104
|
async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
|
|
3003
3105
|
try {
|
|
@@ -3037,6 +3139,7 @@ function sendReplayWorkerMessage(ws, message) {
|
|
|
3037
3139
|
return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
|
|
3038
3140
|
}
|
|
3039
3141
|
function sendWorkerMessage(ws, message) {
|
|
3142
|
+
if (launcherFailureInProgress) return;
|
|
3040
3143
|
if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
|
|
3041
3144
|
return;
|
|
3042
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;
|
|
@@ -3380,6 +3483,7 @@ async function openPty(input) {
|
|
|
3380
3483
|
}) : {};
|
|
3381
3484
|
const targetEnv = targetProcessEnv(input.rootDir, input.resolvedTarget.target);
|
|
3382
3485
|
const shell = resolveHostShell(input.message.command);
|
|
3486
|
+
const shellArgv = input.resources.wrap([shell.file, ...shell.args]);
|
|
3383
3487
|
const outputCoalescer = createPtyOutputCoalescer({
|
|
3384
3488
|
send: (data) => {
|
|
3385
3489
|
sendWorkerMessage(input.ws, {
|
|
@@ -3397,10 +3501,33 @@ async function openPty(input) {
|
|
|
3397
3501
|
envFilesRemoved = true;
|
|
3398
3502
|
removePtyEnvFiles(stagedEnvFiles.paths);
|
|
3399
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
|
+
};
|
|
3400
3527
|
try {
|
|
3401
3528
|
const ptyProcess = createNodePtyBridge({
|
|
3402
|
-
file:
|
|
3403
|
-
args:
|
|
3529
|
+
file: shellArgv[0],
|
|
3530
|
+
args: shellArgv.slice(1),
|
|
3404
3531
|
ptyOptions: {
|
|
3405
3532
|
name: "xterm-256color",
|
|
3406
3533
|
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
@@ -3429,30 +3556,14 @@ async function openPty(input) {
|
|
|
3429
3556
|
outputCoalescer.push(data);
|
|
3430
3557
|
},
|
|
3431
3558
|
onExit: (event) => {
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
cleanupEnvFiles();
|
|
3436
|
-
input.onTerminal?.();
|
|
3437
|
-
sendWorkerMessage(input.ws, {
|
|
3438
|
-
type: "pty_exit",
|
|
3439
|
-
ptyId: input.message.ptyId,
|
|
3440
|
-
exitCode: event.exitCode,
|
|
3441
|
-
signal: event.signal
|
|
3442
|
-
});
|
|
3559
|
+
void finishPty({ type: "pty_exit", ptyId: input.message.ptyId, exitCode: event.exitCode, signal: event.signal }).catch(
|
|
3560
|
+
reportPtyCleanupFailure
|
|
3561
|
+
);
|
|
3443
3562
|
},
|
|
3444
3563
|
onError: (error) => {
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
cleanupEnvFiles();
|
|
3449
|
-
input.onTerminal?.();
|
|
3450
|
-
sendWorkerMessage(input.ws, {
|
|
3451
|
-
type: "pty_error",
|
|
3452
|
-
requestId: input.message.requestId,
|
|
3453
|
-
ptyId: input.message.ptyId,
|
|
3454
|
-
error: error.message
|
|
3455
|
-
});
|
|
3564
|
+
void finishPty({ type: "pty_error", requestId: input.message.requestId, ptyId: input.message.ptyId, error: error.message }).catch(
|
|
3565
|
+
reportPtyCleanupFailure
|
|
3566
|
+
);
|
|
3456
3567
|
},
|
|
3457
3568
|
onTerminationError: (error) => {
|
|
3458
3569
|
sendWorkerMessage(input.ws, {
|
|
@@ -3495,7 +3606,12 @@ function resizePty(message) {
|
|
|
3495
3606
|
}
|
|
3496
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)));
|
|
3497
3608
|
}
|
|
3498
|
-
function closePty(message) {
|
|
3609
|
+
async function closePty(message) {
|
|
3610
|
+
try {
|
|
3611
|
+
await workerCommandLauncher?.cancel(`pty:${message.ptyId}`);
|
|
3612
|
+
} finally {
|
|
3613
|
+
activePtys.get(message.ptyId)?.kill();
|
|
3614
|
+
}
|
|
3499
3615
|
const ptyProcess = activePtys.get(message.ptyId);
|
|
3500
3616
|
if (!ptyProcess) {
|
|
3501
3617
|
return;
|
|
@@ -3514,7 +3630,7 @@ function closePtysBestEffort(ptys, reportFailure) {
|
|
|
3514
3630
|
function closeAllPtys() {
|
|
3515
3631
|
closePtysBestEffort(activePtys, (ptyId, error) => {
|
|
3516
3632
|
process.stderr.write(
|
|
3517
|
-
`[r5d-worker] failed to close PTY ${ptyId} after
|
|
3633
|
+
`[r5d-worker] failed to close PTY ${ptyId} after execution lease expiry: ${error instanceof Error ? error.message : String(error)}
|
|
3518
3634
|
`
|
|
3519
3635
|
);
|
|
3520
3636
|
});
|
|
@@ -3587,6 +3703,17 @@ async function startWorker(options, projectRuntime = {
|
|
|
3587
3703
|
initializeCredentialStoreDirectoryOnce();
|
|
3588
3704
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
3589
3705
|
validateLabel(label);
|
|
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}
|
|
3715
|
+
`);
|
|
3716
|
+
}
|
|
3590
3717
|
const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
3591
3718
|
recoveryStore ??= new WorkerRecoveryStore(
|
|
3592
3719
|
path.join(rootDir, ".worker-recovery", createHash("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
|
|
@@ -4827,7 +4954,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4827
4954
|
`[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
|
|
4828
4955
|
`
|
|
4829
4956
|
);
|
|
4830
|
-
|
|
4957
|
+
void exitWorkerRuntime(exitCode);
|
|
4831
4958
|
},
|
|
4832
4959
|
exitCode: 1
|
|
4833
4960
|
});
|
|
@@ -5638,7 +5765,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5638
5765
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
5639
5766
|
ws.close(1e3, "Worker shutting down");
|
|
5640
5767
|
} else {
|
|
5641
|
-
|
|
5768
|
+
await exitWorkerRuntime(0);
|
|
5642
5769
|
}
|
|
5643
5770
|
})().catch((error) => {
|
|
5644
5771
|
process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -5791,13 +5918,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
5791
5918
|
}
|
|
5792
5919
|
if (message.type === "cancel_session") {
|
|
5793
5920
|
recoveryStore.cancelSession(message.sessionId);
|
|
5794
|
-
await Promise.all(
|
|
5795
|
-
|
|
5921
|
+
await Promise.all([
|
|
5922
|
+
workerCommandLauncher?.cancelSession(message.sessionId),
|
|
5923
|
+
...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
5796
5924
|
cancelledProcessRuns.add(runId);
|
|
5797
5925
|
closeProcessStdin(active);
|
|
5798
5926
|
await terminateProcessTree(active.process);
|
|
5799
5927
|
})
|
|
5800
|
-
);
|
|
5928
|
+
]);
|
|
5801
5929
|
sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
|
|
5802
5930
|
return;
|
|
5803
5931
|
}
|
|
@@ -6244,7 +6372,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6244
6372
|
});
|
|
6245
6373
|
return;
|
|
6246
6374
|
}
|
|
6247
|
-
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) {
|
|
6248
6376
|
sendWorkerMessage(ws, {
|
|
6249
6377
|
type: "update_clis_result",
|
|
6250
6378
|
requestId: message.requestId,
|
|
@@ -6277,8 +6405,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
6277
6405
|
return;
|
|
6278
6406
|
}
|
|
6279
6407
|
if (message.type === "cancel") {
|
|
6408
|
+
let resourceCancellationError;
|
|
6409
|
+
try {
|
|
6410
|
+
await workerCommandLauncher?.cancel(message.runId);
|
|
6411
|
+
} catch (error) {
|
|
6412
|
+
resourceCancellationError = error instanceof Error ? error.message : String(error);
|
|
6413
|
+
}
|
|
6280
6414
|
const active = activeProcesses.get(message.runId);
|
|
6281
|
-
let cancelled =
|
|
6415
|
+
let cancelled = resourceCancellationError === void 0;
|
|
6282
6416
|
let cancelMessage;
|
|
6283
6417
|
if (active) {
|
|
6284
6418
|
try {
|
|
@@ -6293,6 +6427,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6293
6427
|
cancelledProcessRuns.add(message.runId);
|
|
6294
6428
|
cancelMessage = "Cancellation queued before command start";
|
|
6295
6429
|
}
|
|
6430
|
+
if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
|
|
6296
6431
|
sendSerializedWorkerMessage(
|
|
6297
6432
|
ws,
|
|
6298
6433
|
JSON.stringify({
|
|
@@ -6379,13 +6514,16 @@ async function startWorker(options, projectRuntime = {
|
|
|
6379
6514
|
});
|
|
6380
6515
|
return;
|
|
6381
6516
|
}
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6385
|
-
});
|
|
6517
|
+
let resources;
|
|
6518
|
+
let resourcesTransferred = false;
|
|
6386
6519
|
let releaseWorkspaceMutation;
|
|
6387
6520
|
let mutationLeaseTransferred = false;
|
|
6388
6521
|
try {
|
|
6522
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6523
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
6524
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
6525
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6526
|
+
});
|
|
6389
6527
|
releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
|
|
6390
6528
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6391
6529
|
throw new Error(
|
|
@@ -6396,6 +6534,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6396
6534
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
|
|
6397
6535
|
`);
|
|
6398
6536
|
await openPty({
|
|
6537
|
+
resources,
|
|
6399
6538
|
ws,
|
|
6400
6539
|
message,
|
|
6401
6540
|
resolvedTarget,
|
|
@@ -6408,6 +6547,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6408
6547
|
}
|
|
6409
6548
|
} : {}
|
|
6410
6549
|
});
|
|
6550
|
+
resourcesTransferred = true;
|
|
6411
6551
|
mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
|
|
6412
6552
|
} catch (error) {
|
|
6413
6553
|
sendWorkerMessageFromCurrentSource(ws, {
|
|
@@ -6418,6 +6558,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6418
6558
|
});
|
|
6419
6559
|
} finally {
|
|
6420
6560
|
workspaceSyncPriorityPtyTargets.delete(message.ptyId);
|
|
6561
|
+
if (!resourcesTransferred) await releaseWorkerCommandLaunch(resources);
|
|
6421
6562
|
if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
|
|
6422
6563
|
}
|
|
6423
6564
|
return;
|
|
@@ -6447,7 +6588,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6447
6588
|
}
|
|
6448
6589
|
if (message.type === "pty_close") {
|
|
6449
6590
|
const activePty = activePtys.get(message.ptyId);
|
|
6450
|
-
closePty(message);
|
|
6591
|
+
await closePty(message);
|
|
6451
6592
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6452
6593
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
6453
6594
|
}
|
|
@@ -6477,9 +6618,11 @@ async function startWorker(options, projectRuntime = {
|
|
|
6477
6618
|
return;
|
|
6478
6619
|
}
|
|
6479
6620
|
let result;
|
|
6621
|
+
let resources;
|
|
6480
6622
|
let targetReserved = false;
|
|
6481
6623
|
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
6482
6624
|
try {
|
|
6625
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6483
6626
|
if (hasWorkspaceEffect) {
|
|
6484
6627
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
6485
6628
|
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
@@ -6497,6 +6640,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6497
6640
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
6498
6641
|
`);
|
|
6499
6642
|
return await executeCommand({
|
|
6643
|
+
resources,
|
|
6500
6644
|
message,
|
|
6501
6645
|
resolvedTarget,
|
|
6502
6646
|
baseUrl,
|
|
@@ -6532,6 +6676,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6532
6676
|
};
|
|
6533
6677
|
} finally {
|
|
6534
6678
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6679
|
+
await releaseWorkerCommandLaunch(resources);
|
|
6680
|
+
cancelledProcessRuns.delete(message.runId);
|
|
6535
6681
|
}
|
|
6536
6682
|
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6537
6683
|
recoveryStore.unknown(message.requestId);
|
|
@@ -6563,12 +6709,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
6563
6709
|
runId: message.runId
|
|
6564
6710
|
});
|
|
6565
6711
|
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
6712
|
+
let resources;
|
|
6566
6713
|
const runCommand = async () => {
|
|
6567
6714
|
try {
|
|
6568
6715
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
6569
6716
|
process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
|
|
6570
6717
|
`);
|
|
6571
6718
|
await executeStreamingCommand({
|
|
6719
|
+
resources,
|
|
6572
6720
|
ws,
|
|
6573
6721
|
message,
|
|
6574
6722
|
resolvedTarget,
|
|
@@ -6602,6 +6750,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6602
6750
|
const execution = (async () => {
|
|
6603
6751
|
let targetReserved = false;
|
|
6604
6752
|
try {
|
|
6753
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6605
6754
|
if (hasWorkspaceEffect) {
|
|
6606
6755
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
6607
6756
|
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
@@ -6617,6 +6766,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6617
6766
|
});
|
|
6618
6767
|
} finally {
|
|
6619
6768
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6769
|
+
await releaseWorkerCommandLaunch(resources);
|
|
6770
|
+
cancelledProcessRuns.delete(message.runId);
|
|
6620
6771
|
}
|
|
6621
6772
|
})();
|
|
6622
6773
|
void execution.catch((error) => {
|
|
@@ -6747,11 +6898,10 @@ async function startWorker(options, projectRuntime = {
|
|
|
6747
6898
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6748
6899
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
6749
6900
|
`);
|
|
6750
|
-
if (
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
process.exit(0);
|
|
6901
|
+
if (launcherFailureInProgress) return;
|
|
6902
|
+
if (reloadAfterClose || shutdownAfterClose) {
|
|
6903
|
+
void exitWorkerRuntime(reloadAfterClose ? WORKER_RELOAD_EXIT_CODE : 0);
|
|
6904
|
+
return;
|
|
6755
6905
|
}
|
|
6756
6906
|
const reconnect = () => {
|
|
6757
6907
|
const delay = workerReconnectDelayMs(Math.random, workerReconnectAttempt++);
|
|
@@ -6809,10 +6959,10 @@ async function main() {
|
|
|
6809
6959
|
try {
|
|
6810
6960
|
await startWorker(parsed.options);
|
|
6811
6961
|
} catch (error) {
|
|
6812
|
-
if (error instanceof WorkerServerUnavailableError) {
|
|
6962
|
+
if (error instanceof WorkerServerUnavailableError || error instanceof CommandLauncherUnavailableError) {
|
|
6813
6963
|
process.stderr.write(`[r5d-worker] ${error.message}
|
|
6814
6964
|
`);
|
|
6815
|
-
|
|
6965
|
+
await exitWorkerRuntime(WORKER_RECONNECT_EXIT_CODE);
|
|
6816
6966
|
}
|
|
6817
6967
|
throw error;
|
|
6818
6968
|
}
|
|
@@ -6830,10 +6980,10 @@ function isCliEntrypoint() {
|
|
|
6830
6980
|
return typeof require === "function" && typeof module !== "undefined" && require.main === module;
|
|
6831
6981
|
}
|
|
6832
6982
|
if (isCliEntrypoint()) {
|
|
6833
|
-
main().catch((error) => {
|
|
6983
|
+
main().catch(async (error) => {
|
|
6834
6984
|
process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
|
|
6835
6985
|
`);
|
|
6836
|
-
|
|
6986
|
+
await exitWorkerRuntime(1);
|
|
6837
6987
|
});
|
|
6838
6988
|
}
|
|
6839
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;
|