@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/cjs/main.cjs
CHANGED
|
@@ -62,6 +62,8 @@ var import_plan_lint = require("./plan-lint.cjs");
|
|
|
62
62
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
63
63
|
var import_recovery_store = require("./recovery-store.cjs");
|
|
64
64
|
var import_recovery_output_window = require("./recovery-output-window.cjs");
|
|
65
|
+
var import_command_launcher = require("./command-launcher.cjs");
|
|
66
|
+
var import_inline_output = require("./inline-output.cjs");
|
|
65
67
|
var import_recovery_protocol = require("./recovery-protocol.cjs");
|
|
66
68
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
67
69
|
var import_node_crypto = require("node:crypto");
|
|
@@ -154,6 +156,7 @@ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
|
|
|
154
156
|
const PTY_FOREGROUND_POLL_MS = 1e3;
|
|
155
157
|
const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
|
|
156
158
|
const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
|
|
159
|
+
let workerCommandLauncher;
|
|
157
160
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
158
161
|
const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
|
|
159
162
|
const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
|
|
@@ -173,6 +176,41 @@ let workerExecutionLeaseDeadline = 0;
|
|
|
173
176
|
let workerExecutionLeaseTimer;
|
|
174
177
|
let workerLeaseExpired = false;
|
|
175
178
|
const infrastructureStoppedRunIds = /* @__PURE__ */ new Set();
|
|
179
|
+
let launcherFailureInProgress = false;
|
|
180
|
+
let runtimeExitPromise;
|
|
181
|
+
function fenceWorkerRuntimeOperations() {
|
|
182
|
+
launcherFailureInProgress = true;
|
|
183
|
+
workerAdmissionGeneration += 1;
|
|
184
|
+
for (const operation of recoveryStore?.operations() ?? []) {
|
|
185
|
+
if (operation.state !== "completed") recoveryStore?.unknown(operation.requestId);
|
|
186
|
+
}
|
|
187
|
+
for (const runId of workerCommandLauncher?.activeIds ?? []) {
|
|
188
|
+
infrastructureStoppedRunIds.add(runId);
|
|
189
|
+
cancelledProcessRuns.add(runId);
|
|
190
|
+
recoveryStore?.unknownRun(runId);
|
|
191
|
+
}
|
|
192
|
+
for (const runId of activeProcesses.keys()) infrastructureStoppedRunIds.add(runId);
|
|
193
|
+
}
|
|
194
|
+
function exitWorkerRuntime(code) {
|
|
195
|
+
if (runtimeExitPromise) return runtimeExitPromise;
|
|
196
|
+
fenceWorkerRuntimeOperations();
|
|
197
|
+
runtimeExitPromise = (async () => {
|
|
198
|
+
await Promise.race([
|
|
199
|
+
Promise.allSettled([terminateActiveCredentialBearingChildren(), workerCommandLauncher?.close()]),
|
|
200
|
+
Bun.sleep(4e3)
|
|
201
|
+
]);
|
|
202
|
+
process.exit(code);
|
|
203
|
+
})();
|
|
204
|
+
return runtimeExitPromise;
|
|
205
|
+
}
|
|
206
|
+
function handleWorkerCommandLauncherFailure(error) {
|
|
207
|
+
if (!workerCommandLauncher || launcherFailureInProgress) return;
|
|
208
|
+
fenceWorkerRuntimeOperations();
|
|
209
|
+
process.stderr.write(`[r5d-worker] ${error.message}; stopping runtime for communication recovery
|
|
210
|
+
`);
|
|
211
|
+
currentWorkerSocket?.close(1012, "Command launcher recovery");
|
|
212
|
+
void exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
|
|
213
|
+
}
|
|
176
214
|
function isWorkerCommunicationFailure(message) {
|
|
177
215
|
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(
|
|
178
216
|
message
|
|
@@ -202,8 +240,19 @@ function renewWorkerExecutionLease(leaseMs) {
|
|
|
202
240
|
() => {
|
|
203
241
|
if (Date.now() < workerExecutionLeaseDeadline) return;
|
|
204
242
|
workerLeaseExpired = true;
|
|
243
|
+
void workerCommandLauncher?.cancelMatching((id) => id.startsWith("pty:")).catch((error) => {
|
|
244
|
+
process.stderr.write(`[r5d-worker] expired PTY launcher cancellation failed: ${error}
|
|
245
|
+
`);
|
|
246
|
+
});
|
|
247
|
+
closeAllPtys();
|
|
205
248
|
for (const operation of recoveryStore?.operations() ?? []) {
|
|
206
|
-
if (operation.sessionId && operation.state !== "completed")
|
|
249
|
+
if (operation.sessionId && operation.state !== "completed") {
|
|
250
|
+
recoveryStore?.cancelSession(operation.sessionId);
|
|
251
|
+
void workerCommandLauncher?.cancelSession(operation.sessionId).catch((error) => {
|
|
252
|
+
process.stderr.write(`[r5d-worker] expired command launcher cancellation failed: ${error}
|
|
253
|
+
`);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
207
256
|
}
|
|
208
257
|
for (const [runId, active] of activeProcesses) {
|
|
209
258
|
if (!active.sessionId) continue;
|
|
@@ -1567,7 +1616,9 @@ async function fenceCredentialGenerationAtConfigureEntry(input) {
|
|
|
1567
1616
|
await input.terminatePreviousGeneration();
|
|
1568
1617
|
return true;
|
|
1569
1618
|
}
|
|
1570
|
-
function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) =>
|
|
1619
|
+
function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => {
|
|
1620
|
+
void exitWorkerRuntime(exitCode2);
|
|
1621
|
+
}, exitCode = 1) {
|
|
1571
1622
|
if (!required) return;
|
|
1572
1623
|
fatalExit(exitCode);
|
|
1573
1624
|
throw new Error("Credential generation restart callback returned unexpectedly");
|
|
@@ -1597,7 +1648,7 @@ function credentialReapContractStatus(probe) {
|
|
|
1597
1648
|
if (process.platform !== "linux") return "direct";
|
|
1598
1649
|
try {
|
|
1599
1650
|
const cgroupText = import_node_fs.default.readFileSync("/proc/self/cgroup", "utf8");
|
|
1600
|
-
const unit =
|
|
1651
|
+
const unit = /(?:^|\/)r5d-worker\.service(?:\/|$|\n)/m.test(cgroupText) ? "r5d-worker.service" : void 0;
|
|
1601
1652
|
if (unit !== "r5d-worker.service") {
|
|
1602
1653
|
return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? "systemd_unverified" : "direct";
|
|
1603
1654
|
}
|
|
@@ -1631,7 +1682,7 @@ function credentialReapContractStatus(probe) {
|
|
|
1631
1682
|
}
|
|
1632
1683
|
}
|
|
1633
1684
|
if (resolved.platform !== "linux") return "direct";
|
|
1634
|
-
if (!/(?:^|\/)r5d-worker\.service(
|
|
1685
|
+
if (!/(?:^|\/)r5d-worker\.service(?:\/|$|\n)/m.test(resolved.cgroupText)) {
|
|
1635
1686
|
return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "") ? "systemd_unverified" : "direct";
|
|
1636
1687
|
}
|
|
1637
1688
|
if (!SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "")) return "systemd_unverified";
|
|
@@ -2013,12 +2064,6 @@ function resolveCommandCwd(branchPath, cwd) {
|
|
|
2013
2064
|
}
|
|
2014
2065
|
return resolved;
|
|
2015
2066
|
}
|
|
2016
|
-
async function collectStream(stream) {
|
|
2017
|
-
if (!stream) {
|
|
2018
|
-
return "";
|
|
2019
|
-
}
|
|
2020
|
-
return await new Response(stream).text();
|
|
2021
|
-
}
|
|
2022
2067
|
async function streamCommandOutput(stream, onData) {
|
|
2023
2068
|
if (!stream) {
|
|
2024
2069
|
return;
|
|
@@ -2739,8 +2784,38 @@ async function prepareShellEnvForTarget(input) {
|
|
|
2739
2784
|
...targetProcessEnv(input.rootDir, input.target)
|
|
2740
2785
|
};
|
|
2741
2786
|
}
|
|
2787
|
+
async function releaseWorkerCommandLaunch(resources) {
|
|
2788
|
+
await resources?.release();
|
|
2789
|
+
}
|
|
2790
|
+
function cancelWorkerCommandLaunch(resources) {
|
|
2791
|
+
void resources.cancel().catch((error) => {
|
|
2792
|
+
process.stderr.write(`[r5d-worker] command launcher cancellation failed: ${error instanceof Error ? error.message : String(error)}
|
|
2793
|
+
`);
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
async function acquireWorkerCommandLaunch(ws, message, assertAdmission) {
|
|
2797
|
+
const id = message.type === "pty_open" ? `pty:${message.ptyId}` : message.runId;
|
|
2798
|
+
const operationId = message.type === "pty_open" ? id : `exec:${id}`;
|
|
2799
|
+
sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: true });
|
|
2800
|
+
try {
|
|
2801
|
+
return await workerCommandLauncher.acquire({
|
|
2802
|
+
id,
|
|
2803
|
+
kind: message.type,
|
|
2804
|
+
..."workspaceEffect" in message && message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
2805
|
+
..."sessionId" in message && message.sessionId ? { sessionId: message.sessionId } : {},
|
|
2806
|
+
assertAdmission: () => {
|
|
2807
|
+
assertAdmission();
|
|
2808
|
+
if (cancelledProcessRuns.has(id)) throw new import_command_launcher.CommandLaunchCancelledError();
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
} finally {
|
|
2812
|
+
sendWorkerMessage(ws, { type: "heartbeat_lease", operationId, active: false });
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2742
2815
|
async function executeCommand(input) {
|
|
2743
2816
|
const startedAt = Date.now();
|
|
2817
|
+
const outputAbort = new AbortController();
|
|
2818
|
+
let outputReaders = [];
|
|
2744
2819
|
let timeout;
|
|
2745
2820
|
let spawnedProcess;
|
|
2746
2821
|
let preparationSettled = false;
|
|
@@ -2761,7 +2836,7 @@ async function executeCommand(input) {
|
|
|
2761
2836
|
});
|
|
2762
2837
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
2763
2838
|
input.assertAdmission();
|
|
2764
|
-
const subprocess = Bun.spawn(input.message.argv, {
|
|
2839
|
+
const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
|
|
2765
2840
|
cwd,
|
|
2766
2841
|
stdout: "pipe",
|
|
2767
2842
|
stderr: "pipe",
|
|
@@ -2789,6 +2864,7 @@ async function executeCommand(input) {
|
|
|
2789
2864
|
settlePreparation();
|
|
2790
2865
|
if (input.message.timeoutMs) {
|
|
2791
2866
|
timeout = setTimeout(() => {
|
|
2867
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2792
2868
|
void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
|
|
2793
2869
|
process.stderr.write(
|
|
2794
2870
|
`[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -2797,21 +2873,25 @@ async function executeCommand(input) {
|
|
|
2797
2873
|
});
|
|
2798
2874
|
}, input.message.timeoutMs);
|
|
2799
2875
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
]);
|
|
2876
|
+
outputReaders = [
|
|
2877
|
+
(0, import_inline_output.collectWorkerInlineOutput)(subprocess.stdout, void 0, outputAbort.signal),
|
|
2878
|
+
(0, import_inline_output.collectWorkerInlineOutput)(subprocess.stderr, void 0, outputAbort.signal)
|
|
2879
|
+
];
|
|
2880
|
+
const [stdout, stderr, exitCode] = await Promise.all([outputReaders[0], outputReaders[1], subprocess.exited]);
|
|
2881
|
+
const diagnostic = await input.resources.diagnose();
|
|
2805
2882
|
return {
|
|
2806
2883
|
type: "exec_result",
|
|
2807
2884
|
requestId: input.message.requestId,
|
|
2808
2885
|
stdout,
|
|
2809
2886
|
stderr,
|
|
2810
2887
|
exitCode,
|
|
2811
|
-
durationMs: Date.now() - startedAt
|
|
2888
|
+
durationMs: Date.now() - startedAt,
|
|
2889
|
+
...diagnostic ? { error: diagnostic } : {}
|
|
2812
2890
|
};
|
|
2813
2891
|
} catch (error) {
|
|
2814
|
-
|
|
2892
|
+
outputAbort.abort(error);
|
|
2893
|
+
if (error instanceof StaleWorkerAdmissionError || error instanceof import_command_launcher.CommandLauncherUnavailableError) throw error;
|
|
2894
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2815
2895
|
return {
|
|
2816
2896
|
type: "exec_result",
|
|
2817
2897
|
requestId: input.message.requestId,
|
|
@@ -2829,12 +2909,16 @@ async function executeCommand(input) {
|
|
|
2829
2909
|
if (spawnedProcess) {
|
|
2830
2910
|
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
2831
2911
|
}
|
|
2912
|
+
outputAbort.abort();
|
|
2913
|
+
await Promise.allSettled(outputReaders);
|
|
2914
|
+
await releaseWorkerCommandLaunch(input.resources);
|
|
2832
2915
|
activeProcesses.delete(input.message.runId);
|
|
2833
2916
|
}
|
|
2834
2917
|
}
|
|
2835
2918
|
async function executeStreamingCommand(input) {
|
|
2836
2919
|
const startedAt = Date.now();
|
|
2837
2920
|
let started = false;
|
|
2921
|
+
let terminal;
|
|
2838
2922
|
let timedOut = false;
|
|
2839
2923
|
let timeout;
|
|
2840
2924
|
let spawnedProcess;
|
|
@@ -2846,7 +2930,7 @@ async function executeStreamingCommand(input) {
|
|
|
2846
2930
|
};
|
|
2847
2931
|
input.setPreparationHeartbeatLease(true);
|
|
2848
2932
|
try {
|
|
2849
|
-
if (cancelledProcessRuns.
|
|
2933
|
+
if (cancelledProcessRuns.has(input.message.runId)) {
|
|
2850
2934
|
sendWorkerMessage(input.ws, {
|
|
2851
2935
|
type: "exec_start_error",
|
|
2852
2936
|
requestId: input.message.requestId,
|
|
@@ -2866,7 +2950,7 @@ async function executeStreamingCommand(input) {
|
|
|
2866
2950
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
2867
2951
|
const interactive = input.message.interactive === true;
|
|
2868
2952
|
input.assertAdmission();
|
|
2869
|
-
const subprocess = Bun.spawn(input.message.argv, {
|
|
2953
|
+
const subprocess = Bun.spawn(input.resources.wrap(input.message.argv), {
|
|
2870
2954
|
cwd,
|
|
2871
2955
|
// Without an explicit stdin the process reads /dev/null and interactive
|
|
2872
2956
|
// prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
|
|
@@ -2909,6 +2993,7 @@ async function executeStreamingCommand(input) {
|
|
|
2909
2993
|
if (input.message.timeoutMs) {
|
|
2910
2994
|
timeout = setTimeout(() => {
|
|
2911
2995
|
timedOut = true;
|
|
2996
|
+
cancelWorkerCommandLaunch(input.resources);
|
|
2912
2997
|
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
2913
2998
|
void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
|
|
2914
2999
|
process.stderr.write(
|
|
@@ -2937,8 +3022,19 @@ async function executeStreamingCommand(input) {
|
|
|
2937
3022
|
});
|
|
2938
3023
|
})
|
|
2939
3024
|
]);
|
|
3025
|
+
const diagnostic = await input.resources.diagnose();
|
|
3026
|
+
if (diagnostic) {
|
|
3027
|
+
sendWorkerMessage(input.ws, {
|
|
3028
|
+
type: "exec_output",
|
|
3029
|
+
runId: input.message.runId,
|
|
3030
|
+
stream: "stderr",
|
|
3031
|
+
data: `
|
|
3032
|
+
[r5d-worker] ${diagnostic}
|
|
3033
|
+
`
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
2940
3036
|
const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
|
|
2941
|
-
|
|
3037
|
+
terminal = {
|
|
2942
3038
|
type: "exec_exit",
|
|
2943
3039
|
runId: input.message.runId,
|
|
2944
3040
|
exitCode,
|
|
@@ -2947,14 +3043,12 @@ async function executeStreamingCommand(input) {
|
|
|
2947
3043
|
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
2948
3044
|
...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
|
|
2949
3045
|
};
|
|
2950
|
-
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
2951
|
-
sendWorkerMessage(input.ws, terminal);
|
|
2952
3046
|
} catch (error) {
|
|
2953
|
-
if (!started && error instanceof StaleWorkerAdmissionError) throw error;
|
|
3047
|
+
if (error instanceof import_command_launcher.CommandLauncherUnavailableError || !started && error instanceof StaleWorkerAdmissionError) throw error;
|
|
2954
3048
|
const message = error instanceof Error ? error.message : String(error);
|
|
2955
3049
|
if (started) {
|
|
2956
3050
|
const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
|
|
2957
|
-
|
|
3051
|
+
terminal = {
|
|
2958
3052
|
type: "exec_error",
|
|
2959
3053
|
runId: input.message.runId,
|
|
2960
3054
|
error: message,
|
|
@@ -2962,8 +3056,6 @@ async function executeStreamingCommand(input) {
|
|
|
2962
3056
|
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
|
|
2963
3057
|
...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
|
|
2964
3058
|
};
|
|
2965
|
-
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
2966
|
-
sendWorkerMessage(input.ws, terminal);
|
|
2967
3059
|
} else {
|
|
2968
3060
|
sendWorkerMessage(input.ws, {
|
|
2969
3061
|
type: "exec_start_error",
|
|
@@ -2981,9 +3073,14 @@ async function executeStreamingCommand(input) {
|
|
|
2981
3073
|
if (spawnedProcess) {
|
|
2982
3074
|
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
2983
3075
|
}
|
|
3076
|
+
await releaseWorkerCommandLaunch(input.resources);
|
|
2984
3077
|
activeProcesses.delete(input.message.runId);
|
|
2985
3078
|
cancelledProcessRuns.delete(input.message.runId);
|
|
2986
3079
|
}
|
|
3080
|
+
if (terminal) {
|
|
3081
|
+
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
3082
|
+
sendWorkerMessage(input.ws, terminal);
|
|
3083
|
+
}
|
|
2987
3084
|
}
|
|
2988
3085
|
async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
|
|
2989
3086
|
try {
|
|
@@ -3023,6 +3120,7 @@ function sendReplayWorkerMessage(ws, message) {
|
|
|
3023
3120
|
return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
|
|
3024
3121
|
}
|
|
3025
3122
|
function sendWorkerMessage(ws, message) {
|
|
3123
|
+
if (launcherFailureInProgress) return;
|
|
3026
3124
|
if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
|
|
3027
3125
|
return;
|
|
3028
3126
|
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;
|
|
@@ -3366,6 +3464,7 @@ async function openPty(input) {
|
|
|
3366
3464
|
}) : {};
|
|
3367
3465
|
const targetEnv = targetProcessEnv(input.rootDir, input.resolvedTarget.target);
|
|
3368
3466
|
const shell = resolveHostShell(input.message.command);
|
|
3467
|
+
const shellArgv = input.resources.wrap([shell.file, ...shell.args]);
|
|
3369
3468
|
const outputCoalescer = (0, import_pty_output_coalescer.createPtyOutputCoalescer)({
|
|
3370
3469
|
send: (data) => {
|
|
3371
3470
|
sendWorkerMessage(input.ws, {
|
|
@@ -3383,10 +3482,33 @@ async function openPty(input) {
|
|
|
3383
3482
|
envFilesRemoved = true;
|
|
3384
3483
|
removePtyEnvFiles(stagedEnvFiles.paths);
|
|
3385
3484
|
};
|
|
3485
|
+
let terminalHandled = false;
|
|
3486
|
+
const finishPty = async (terminal) => {
|
|
3487
|
+
if (terminalHandled) return;
|
|
3488
|
+
terminalHandled = true;
|
|
3489
|
+
try {
|
|
3490
|
+
const diagnostic = await input.resources.diagnose();
|
|
3491
|
+
if (diagnostic) outputCoalescer.push(`\r
|
|
3492
|
+
[r5d-worker] ${diagnostic}\r
|
|
3493
|
+
`);
|
|
3494
|
+
} finally {
|
|
3495
|
+
cleanupEnvFiles();
|
|
3496
|
+
await releaseWorkerCommandLaunch(input.resources);
|
|
3497
|
+
outputCoalescer.flush();
|
|
3498
|
+
activePtys.delete(input.message.ptyId);
|
|
3499
|
+
input.releaseWorkspaceMutation?.();
|
|
3500
|
+
input.onTerminal?.();
|
|
3501
|
+
}
|
|
3502
|
+
sendWorkerMessage(input.ws, terminal);
|
|
3503
|
+
};
|
|
3504
|
+
const reportPtyCleanupFailure = (error) => {
|
|
3505
|
+
process.stderr.write(`[r5d-worker] PTY launcher cleanup failed: ${error instanceof Error ? error.message : String(error)}
|
|
3506
|
+
`);
|
|
3507
|
+
};
|
|
3386
3508
|
try {
|
|
3387
3509
|
const ptyProcess = createNodePtyBridge({
|
|
3388
|
-
file:
|
|
3389
|
-
args:
|
|
3510
|
+
file: shellArgv[0],
|
|
3511
|
+
args: shellArgv.slice(1),
|
|
3390
3512
|
ptyOptions: {
|
|
3391
3513
|
name: "xterm-256color",
|
|
3392
3514
|
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
@@ -3415,30 +3537,14 @@ async function openPty(input) {
|
|
|
3415
3537
|
outputCoalescer.push(data);
|
|
3416
3538
|
},
|
|
3417
3539
|
onExit: (event) => {
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
cleanupEnvFiles();
|
|
3422
|
-
input.onTerminal?.();
|
|
3423
|
-
sendWorkerMessage(input.ws, {
|
|
3424
|
-
type: "pty_exit",
|
|
3425
|
-
ptyId: input.message.ptyId,
|
|
3426
|
-
exitCode: event.exitCode,
|
|
3427
|
-
signal: event.signal
|
|
3428
|
-
});
|
|
3540
|
+
void finishPty({ type: "pty_exit", ptyId: input.message.ptyId, exitCode: event.exitCode, signal: event.signal }).catch(
|
|
3541
|
+
reportPtyCleanupFailure
|
|
3542
|
+
);
|
|
3429
3543
|
},
|
|
3430
3544
|
onError: (error) => {
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
cleanupEnvFiles();
|
|
3435
|
-
input.onTerminal?.();
|
|
3436
|
-
sendWorkerMessage(input.ws, {
|
|
3437
|
-
type: "pty_error",
|
|
3438
|
-
requestId: input.message.requestId,
|
|
3439
|
-
ptyId: input.message.ptyId,
|
|
3440
|
-
error: error.message
|
|
3441
|
-
});
|
|
3545
|
+
void finishPty({ type: "pty_error", requestId: input.message.requestId, ptyId: input.message.ptyId, error: error.message }).catch(
|
|
3546
|
+
reportPtyCleanupFailure
|
|
3547
|
+
);
|
|
3442
3548
|
},
|
|
3443
3549
|
onTerminationError: (error) => {
|
|
3444
3550
|
sendWorkerMessage(input.ws, {
|
|
@@ -3481,7 +3587,12 @@ function resizePty(message) {
|
|
|
3481
3587
|
}
|
|
3482
3588
|
ptyProcess.resize(Math.max(1, Math.min(Math.floor(message.cols || 80), 500)), Math.max(1, Math.min(Math.floor(message.rows || 24), 500)));
|
|
3483
3589
|
}
|
|
3484
|
-
function closePty(message) {
|
|
3590
|
+
async function closePty(message) {
|
|
3591
|
+
try {
|
|
3592
|
+
await workerCommandLauncher?.cancel(`pty:${message.ptyId}`);
|
|
3593
|
+
} finally {
|
|
3594
|
+
activePtys.get(message.ptyId)?.kill();
|
|
3595
|
+
}
|
|
3485
3596
|
const ptyProcess = activePtys.get(message.ptyId);
|
|
3486
3597
|
if (!ptyProcess) {
|
|
3487
3598
|
return;
|
|
@@ -3500,7 +3611,7 @@ function closePtysBestEffort(ptys, reportFailure) {
|
|
|
3500
3611
|
function closeAllPtys() {
|
|
3501
3612
|
closePtysBestEffort(activePtys, (ptyId, error) => {
|
|
3502
3613
|
process.stderr.write(
|
|
3503
|
-
`[r5d-worker] failed to close PTY ${ptyId} after
|
|
3614
|
+
`[r5d-worker] failed to close PTY ${ptyId} after execution lease expiry: ${error instanceof Error ? error.message : String(error)}
|
|
3504
3615
|
`
|
|
3505
3616
|
);
|
|
3506
3617
|
});
|
|
@@ -3573,6 +3684,17 @@ async function startWorker(options, projectRuntime = {
|
|
|
3573
3684
|
initializeCredentialStoreDirectoryOnce();
|
|
3574
3685
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
3575
3686
|
validateLabel(label);
|
|
3687
|
+
if (!workerCommandLauncher) {
|
|
3688
|
+
const launcher = await (0, import_command_launcher.createWorkerCommandLauncher)({
|
|
3689
|
+
configuration: process.env.R5D_WORKER_COMMAND_LAUNCHER,
|
|
3690
|
+
env: (0, import_command_launcher.commandLauncherEnvironment)(),
|
|
3691
|
+
onFailure: handleWorkerCommandLauncherFailure
|
|
3692
|
+
});
|
|
3693
|
+
workerCommandLauncher = launcher;
|
|
3694
|
+
await launcher.initialize();
|
|
3695
|
+
process.stdout.write(`[r5d-worker] command launcher: ${launcher.mode}
|
|
3696
|
+
`);
|
|
3697
|
+
}
|
|
3576
3698
|
const rootDir = import_node_path.default.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
3577
3699
|
recoveryStore ??= new import_recovery_store.WorkerRecoveryStore(
|
|
3578
3700
|
import_node_path.default.join(rootDir, ".worker-recovery", (0, import_node_crypto.createHash)("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
|
|
@@ -4813,7 +4935,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4813
4935
|
`[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
|
|
4814
4936
|
`
|
|
4815
4937
|
);
|
|
4816
|
-
|
|
4938
|
+
void exitWorkerRuntime(exitCode);
|
|
4817
4939
|
},
|
|
4818
4940
|
exitCode: 1
|
|
4819
4941
|
});
|
|
@@ -5624,7 +5746,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5624
5746
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
5625
5747
|
ws.close(1e3, "Worker shutting down");
|
|
5626
5748
|
} else {
|
|
5627
|
-
|
|
5749
|
+
await exitWorkerRuntime(0);
|
|
5628
5750
|
}
|
|
5629
5751
|
})().catch((error) => {
|
|
5630
5752
|
process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -5777,13 +5899,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
5777
5899
|
}
|
|
5778
5900
|
if (message.type === "cancel_session") {
|
|
5779
5901
|
recoveryStore.cancelSession(message.sessionId);
|
|
5780
|
-
await Promise.all(
|
|
5781
|
-
|
|
5902
|
+
await Promise.all([
|
|
5903
|
+
workerCommandLauncher?.cancelSession(message.sessionId),
|
|
5904
|
+
...[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
5782
5905
|
cancelledProcessRuns.add(runId);
|
|
5783
5906
|
closeProcessStdin(active);
|
|
5784
5907
|
await (0, import_process_tree.terminateProcessTree)(active.process);
|
|
5785
5908
|
})
|
|
5786
|
-
);
|
|
5909
|
+
]);
|
|
5787
5910
|
sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
|
|
5788
5911
|
return;
|
|
5789
5912
|
}
|
|
@@ -6230,7 +6353,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6230
6353
|
});
|
|
6231
6354
|
return;
|
|
6232
6355
|
}
|
|
6233
|
-
if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
|
|
6356
|
+
if (activeProcesses.size > 0 || (workerCommandLauncher?.activeIds.length ?? 0) > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
|
|
6234
6357
|
sendWorkerMessage(ws, {
|
|
6235
6358
|
type: "update_clis_result",
|
|
6236
6359
|
requestId: message.requestId,
|
|
@@ -6263,8 +6386,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
6263
6386
|
return;
|
|
6264
6387
|
}
|
|
6265
6388
|
if (message.type === "cancel") {
|
|
6389
|
+
let resourceCancellationError;
|
|
6390
|
+
try {
|
|
6391
|
+
await workerCommandLauncher?.cancel(message.runId);
|
|
6392
|
+
} catch (error) {
|
|
6393
|
+
resourceCancellationError = error instanceof Error ? error.message : String(error);
|
|
6394
|
+
}
|
|
6266
6395
|
const active = activeProcesses.get(message.runId);
|
|
6267
|
-
let cancelled =
|
|
6396
|
+
let cancelled = resourceCancellationError === void 0;
|
|
6268
6397
|
let cancelMessage;
|
|
6269
6398
|
if (active) {
|
|
6270
6399
|
try {
|
|
@@ -6279,6 +6408,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6279
6408
|
cancelledProcessRuns.add(message.runId);
|
|
6280
6409
|
cancelMessage = "Cancellation queued before command start";
|
|
6281
6410
|
}
|
|
6411
|
+
if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
|
|
6282
6412
|
sendSerializedWorkerMessage(
|
|
6283
6413
|
ws,
|
|
6284
6414
|
JSON.stringify({
|
|
@@ -6365,13 +6495,16 @@ async function startWorker(options, projectRuntime = {
|
|
|
6365
6495
|
});
|
|
6366
6496
|
return;
|
|
6367
6497
|
}
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6371
|
-
});
|
|
6498
|
+
let resources;
|
|
6499
|
+
let resourcesTransferred = false;
|
|
6372
6500
|
let releaseWorkspaceMutation;
|
|
6373
6501
|
let mutationLeaseTransferred = false;
|
|
6374
6502
|
try {
|
|
6503
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6504
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
6505
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
6506
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
6507
|
+
});
|
|
6375
6508
|
releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
|
|
6376
6509
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6377
6510
|
throw new Error(
|
|
@@ -6382,6 +6515,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6382
6515
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
|
|
6383
6516
|
`);
|
|
6384
6517
|
await openPty({
|
|
6518
|
+
resources,
|
|
6385
6519
|
ws,
|
|
6386
6520
|
message,
|
|
6387
6521
|
resolvedTarget,
|
|
@@ -6394,6 +6528,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6394
6528
|
}
|
|
6395
6529
|
} : {}
|
|
6396
6530
|
});
|
|
6531
|
+
resourcesTransferred = true;
|
|
6397
6532
|
mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
|
|
6398
6533
|
} catch (error) {
|
|
6399
6534
|
sendWorkerMessageFromCurrentSource(ws, {
|
|
@@ -6404,6 +6539,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6404
6539
|
});
|
|
6405
6540
|
} finally {
|
|
6406
6541
|
workspaceSyncPriorityPtyTargets.delete(message.ptyId);
|
|
6542
|
+
if (!resourcesTransferred) await releaseWorkerCommandLaunch(resources);
|
|
6407
6543
|
if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
|
|
6408
6544
|
}
|
|
6409
6545
|
return;
|
|
@@ -6433,7 +6569,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6433
6569
|
}
|
|
6434
6570
|
if (message.type === "pty_close") {
|
|
6435
6571
|
const activePty = activePtys.get(message.ptyId);
|
|
6436
|
-
closePty(message);
|
|
6572
|
+
await closePty(message);
|
|
6437
6573
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6438
6574
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
6439
6575
|
}
|
|
@@ -6463,9 +6599,11 @@ async function startWorker(options, projectRuntime = {
|
|
|
6463
6599
|
return;
|
|
6464
6600
|
}
|
|
6465
6601
|
let result;
|
|
6602
|
+
let resources;
|
|
6466
6603
|
let targetReserved = false;
|
|
6467
6604
|
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
6468
6605
|
try {
|
|
6606
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6469
6607
|
if (hasWorkspaceEffect) {
|
|
6470
6608
|
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
6471
6609
|
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
@@ -6483,6 +6621,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6483
6621
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
6484
6622
|
`);
|
|
6485
6623
|
return await executeCommand({
|
|
6624
|
+
resources,
|
|
6486
6625
|
message,
|
|
6487
6626
|
resolvedTarget,
|
|
6488
6627
|
baseUrl,
|
|
@@ -6518,6 +6657,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6518
6657
|
};
|
|
6519
6658
|
} finally {
|
|
6520
6659
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6660
|
+
await releaseWorkerCommandLaunch(resources);
|
|
6661
|
+
cancelledProcessRuns.delete(message.runId);
|
|
6521
6662
|
}
|
|
6522
6663
|
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6523
6664
|
recoveryStore.unknown(message.requestId);
|
|
@@ -6549,12 +6690,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
6549
6690
|
runId: message.runId
|
|
6550
6691
|
});
|
|
6551
6692
|
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
6693
|
+
let resources;
|
|
6552
6694
|
const runCommand = async () => {
|
|
6553
6695
|
try {
|
|
6554
6696
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
6555
6697
|
process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
|
|
6556
6698
|
`);
|
|
6557
6699
|
await executeStreamingCommand({
|
|
6700
|
+
resources,
|
|
6558
6701
|
ws,
|
|
6559
6702
|
message,
|
|
6560
6703
|
resolvedTarget,
|
|
@@ -6588,6 +6731,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6588
6731
|
const execution = (async () => {
|
|
6589
6732
|
let targetReserved = false;
|
|
6590
6733
|
try {
|
|
6734
|
+
resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
|
|
6591
6735
|
if (hasWorkspaceEffect) {
|
|
6592
6736
|
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
6593
6737
|
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
@@ -6603,6 +6747,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6603
6747
|
});
|
|
6604
6748
|
} finally {
|
|
6605
6749
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6750
|
+
await releaseWorkerCommandLaunch(resources);
|
|
6751
|
+
cancelledProcessRuns.delete(message.runId);
|
|
6606
6752
|
}
|
|
6607
6753
|
})();
|
|
6608
6754
|
void execution.catch((error) => {
|
|
@@ -6733,11 +6879,10 @@ async function startWorker(options, projectRuntime = {
|
|
|
6733
6879
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6734
6880
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
6735
6881
|
`);
|
|
6736
|
-
if (
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
process.exit(0);
|
|
6882
|
+
if (launcherFailureInProgress) return;
|
|
6883
|
+
if (reloadAfterClose || shutdownAfterClose) {
|
|
6884
|
+
void exitWorkerRuntime(reloadAfterClose ? import_supervisor.WORKER_RELOAD_EXIT_CODE : 0);
|
|
6885
|
+
return;
|
|
6741
6886
|
}
|
|
6742
6887
|
const reconnect = () => {
|
|
6743
6888
|
const delay = (0, import_supervisor.workerReconnectDelayMs)(Math.random, workerReconnectAttempt++);
|
|
@@ -6795,10 +6940,10 @@ async function main() {
|
|
|
6795
6940
|
try {
|
|
6796
6941
|
await startWorker(parsed.options);
|
|
6797
6942
|
} catch (error) {
|
|
6798
|
-
if (error instanceof WorkerServerUnavailableError) {
|
|
6943
|
+
if (error instanceof WorkerServerUnavailableError || error instanceof import_command_launcher.CommandLauncherUnavailableError) {
|
|
6799
6944
|
process.stderr.write(`[r5d-worker] ${error.message}
|
|
6800
6945
|
`);
|
|
6801
|
-
|
|
6946
|
+
await exitWorkerRuntime(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
|
|
6802
6947
|
}
|
|
6803
6948
|
throw error;
|
|
6804
6949
|
}
|
|
@@ -6816,10 +6961,10 @@ function isCliEntrypoint() {
|
|
|
6816
6961
|
return typeof require === "function" && typeof module !== "undefined" && require.main === module;
|
|
6817
6962
|
}
|
|
6818
6963
|
if (isCliEntrypoint()) {
|
|
6819
|
-
main().catch((error) => {
|
|
6964
|
+
main().catch(async (error) => {
|
|
6820
6965
|
process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
|
|
6821
6966
|
`);
|
|
6822
|
-
|
|
6967
|
+
await exitWorkerRuntime(1);
|
|
6823
6968
|
});
|
|
6824
6969
|
}
|
|
6825
6970
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/cjs/package.json
CHANGED
package/dist/cjs/supervisor.cjs
CHANGED
|
@@ -54,7 +54,7 @@ function workerReconnectDelayMs(random = Math.random, attempt = 0) {
|
|
|
54
54
|
return floor + Math.floor(sample * (ceiling - floor));
|
|
55
55
|
}
|
|
56
56
|
async function defaultRuntimeReaper(runtime) {
|
|
57
|
-
await (0, import_process_tree.terminateProcessTree)(runtime);
|
|
57
|
+
await (0, import_process_tree.terminateProcessTree)(runtime, { graceMs: 6e3 });
|
|
58
58
|
}
|
|
59
59
|
function isRetryableWorkerServerStatus(status) {
|
|
60
60
|
return status === 408 || status === 429 || status >= 500;
|