@ricsam/r5d-worker 0.0.108 → 0.0.109
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/dist/cjs/main.cjs +336 -125
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/recovery-output-window.cjs +77 -0
- package/dist/cjs/recovery-protocol.cjs +31 -0
- package/dist/cjs/recovery-store.cjs +229 -0
- package/dist/cjs/supervisor.cjs +11 -4
- package/dist/mjs/main.mjs +339 -125
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/recovery-output-window.mjs +53 -0
- package/dist/mjs/recovery-protocol.mjs +6 -0
- package/dist/mjs/recovery-store.mjs +194 -0
- package/dist/mjs/supervisor.mjs +10 -4
- package/dist/types/main.d.ts +9 -2
- package/dist/types/recovery-output-window.d.ts +19 -0
- package/dist/types/recovery-protocol.d.ts +59 -0
- package/dist/types/recovery-store.d.ts +38 -0
- package/dist/types/supervisor.d.ts +2 -1
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -3,6 +3,12 @@ import { getWorkerPlanPath, getWorkerPlansPath, validatePlanId } from "./plan-pa
|
|
|
3
3
|
import { createPlanMarkdown, updateTaskCheckbox } from "./plan-parser.mjs";
|
|
4
4
|
import { lintPlans, formatPlanLintErrors } from "./plan-lint.mjs";
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import { WorkerRecoveryStore, workerOperationDigest } from "./recovery-store.mjs";
|
|
7
|
+
import { WorkerOutputWindow } from "./recovery-output-window.mjs";
|
|
8
|
+
import {
|
|
9
|
+
WORKER_RESUMABLE_PROTOCOL,
|
|
10
|
+
WORKER_RECOVERY_TIMEOUT_MS
|
|
11
|
+
} from "./recovery-protocol.mjs";
|
|
6
12
|
import path from "node:path";
|
|
7
13
|
import { createHash } from "node:crypto";
|
|
8
14
|
import os, { hostname } from "node:os";
|
|
@@ -172,6 +178,63 @@ const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
|
172
178
|
const activePtys = /* @__PURE__ */ new Map();
|
|
173
179
|
const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
|
|
174
180
|
let currentWorkerSocket = null;
|
|
181
|
+
const workerRuntimeId = crypto.randomUUID();
|
|
182
|
+
const workerOutputWindow = new WorkerOutputWindow();
|
|
183
|
+
let recoveryStore;
|
|
184
|
+
let workerReconnectAttempt = 0;
|
|
185
|
+
let workerRecoveryId;
|
|
186
|
+
let workerExecutionLeaseDeadline = 0;
|
|
187
|
+
let workerExecutionLeaseTimer;
|
|
188
|
+
let workerLeaseExpired = false;
|
|
189
|
+
const infrastructureStoppedRunIds = /* @__PURE__ */ new Set();
|
|
190
|
+
function isWorkerCommunicationFailure(message) {
|
|
191
|
+
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
|
+
message
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
async function retryWorkerServerRead(read) {
|
|
196
|
+
let deadline;
|
|
197
|
+
let retry = 0;
|
|
198
|
+
while (true) {
|
|
199
|
+
try {
|
|
200
|
+
return await read();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (!recoveryStore || !isWorkerCommunicationFailure(error instanceof Error ? error.message : String(error))) throw error;
|
|
203
|
+
deadline ??= Date.now() + WORKER_RECOVERY_TIMEOUT_MS;
|
|
204
|
+
if (workerLeaseExpired || Date.now() >= deadline) throw new WorkerServerUnavailableError("Worker communication recovery expired");
|
|
205
|
+
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
206
|
+
await Bun.sleep(workerReconnectDelayMs(Math.random, retry++));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function renewWorkerExecutionLease(leaseMs) {
|
|
211
|
+
if (!Number.isFinite(leaseMs) || leaseMs <= 0) return;
|
|
212
|
+
workerLeaseExpired = false;
|
|
213
|
+
workerExecutionLeaseDeadline = Date.now() + Math.min(leaseMs, WORKER_RECOVERY_TIMEOUT_MS);
|
|
214
|
+
if (workerExecutionLeaseTimer) clearTimeout(workerExecutionLeaseTimer);
|
|
215
|
+
workerExecutionLeaseTimer = setTimeout(
|
|
216
|
+
() => {
|
|
217
|
+
if (Date.now() < workerExecutionLeaseDeadline) return;
|
|
218
|
+
workerLeaseExpired = true;
|
|
219
|
+
for (const operation of recoveryStore?.operations() ?? []) {
|
|
220
|
+
if (operation.sessionId && operation.state !== "completed") recoveryStore?.cancelSession(operation.sessionId);
|
|
221
|
+
}
|
|
222
|
+
for (const [runId, active] of activeProcesses) {
|
|
223
|
+
if (!active.sessionId) continue;
|
|
224
|
+
infrastructureStoppedRunIds.add(runId);
|
|
225
|
+
recoveryStore?.unknownRun(runId);
|
|
226
|
+
cancelledProcessRuns.add(runId);
|
|
227
|
+
closeProcessStdin(active);
|
|
228
|
+
void terminateProcessTree(active.process).catch(
|
|
229
|
+
(error) => process.stderr.write(`[r5d-worker] execution lease termination failed: ${error}
|
|
230
|
+
`)
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
Math.min(leaseMs, WORKER_RECOVERY_TIMEOUT_MS)
|
|
235
|
+
);
|
|
236
|
+
workerExecutionLeaseTimer.unref();
|
|
237
|
+
}
|
|
175
238
|
let workerAdmissionGeneration = 0;
|
|
176
239
|
const workspaceMutationGate = new WorkspaceMutationGate();
|
|
177
240
|
let workspaceSyncQueue = Promise.resolve();
|
|
@@ -630,40 +693,46 @@ function parseRemoteFileManifest(value, collectionKey, label) {
|
|
|
630
693
|
});
|
|
631
694
|
}
|
|
632
695
|
async function fetchRemoteFileManifest(input) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
696
|
+
return retryWorkerServerRead(async () => {
|
|
697
|
+
const response = await fetch(input.url, {
|
|
698
|
+
signal: AbortSignal.timeout(3e4),
|
|
699
|
+
headers: {
|
|
700
|
+
Authorization: `Bearer ${input.token}`
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
if (!response.ok) {
|
|
704
|
+
const detail = (await readResponseText(response)).trim();
|
|
705
|
+
throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
636
706
|
}
|
|
707
|
+
return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
|
|
637
708
|
});
|
|
638
|
-
if (!response.ok) {
|
|
639
|
-
const detail = (await readResponseText(response)).trim();
|
|
640
|
-
throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
641
|
-
}
|
|
642
|
-
return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
|
|
643
709
|
}
|
|
644
710
|
async function downloadRemoteSyncedFile(input) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
711
|
+
return retryWorkerServerRead(async () => {
|
|
712
|
+
const targetPath = path.join(input.targetDir, input.file.filename);
|
|
713
|
+
assertInsideRoot(input.targetDir, targetPath, input.label);
|
|
714
|
+
const response = await fetch(input.url, {
|
|
715
|
+
signal: AbortSignal.timeout(3e4),
|
|
716
|
+
headers: {
|
|
717
|
+
Authorization: `Bearer ${input.token}`
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
if (!response.ok) {
|
|
721
|
+
const detail = (await readResponseText(response)).trim();
|
|
722
|
+
throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
723
|
+
}
|
|
724
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
725
|
+
const receivedHash = createHash("sha256").update(buffer).digest("hex");
|
|
726
|
+
if (receivedHash !== input.file.sha256) {
|
|
727
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
|
|
650
728
|
}
|
|
729
|
+
if (buffer.length !== input.file.size) {
|
|
730
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
|
|
731
|
+
}
|
|
732
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
733
|
+
fs.writeFileSync(tempPath, buffer);
|
|
734
|
+
fs.renameSync(tempPath, targetPath);
|
|
651
735
|
});
|
|
652
|
-
if (!response.ok) {
|
|
653
|
-
const detail = (await readResponseText(response)).trim();
|
|
654
|
-
throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
655
|
-
}
|
|
656
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
657
|
-
const receivedHash = createHash("sha256").update(buffer).digest("hex");
|
|
658
|
-
if (receivedHash !== input.file.sha256) {
|
|
659
|
-
throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
|
|
660
|
-
}
|
|
661
|
-
if (buffer.length !== input.file.size) {
|
|
662
|
-
throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
|
|
663
|
-
}
|
|
664
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
665
|
-
fs.writeFileSync(tempPath, buffer);
|
|
666
|
-
fs.renameSync(tempPath, targetPath);
|
|
667
736
|
}
|
|
668
737
|
async function syncRemoteFileSet(input) {
|
|
669
738
|
fs.mkdirSync(input.targetDir, { recursive: true });
|
|
@@ -790,6 +859,7 @@ async function verifyR5dctlAuth(baseUrl, token, workerLabel) {
|
|
|
790
859
|
let response;
|
|
791
860
|
try {
|
|
792
861
|
response = await fetch(statusUrl, {
|
|
862
|
+
signal: AbortSignal.timeout(3e4),
|
|
793
863
|
headers: {
|
|
794
864
|
Authorization: `Bearer ${token}`
|
|
795
865
|
}
|
|
@@ -2953,23 +3023,46 @@ function closeProcessStdin(active) {
|
|
|
2953
3023
|
}
|
|
2954
3024
|
active.stdin = void 0;
|
|
2955
3025
|
}
|
|
2956
|
-
function
|
|
3026
|
+
function sendReplayWorkerMessage(ws, message) {
|
|
2957
3027
|
const target = currentWorkerSocket ?? ws;
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3028
|
+
if (target.readyState !== WebSocket.OPEN) return false;
|
|
3029
|
+
const send = (payload) => {
|
|
3030
|
+
try {
|
|
3031
|
+
target.send(JSON.stringify(payload));
|
|
3032
|
+
return true;
|
|
3033
|
+
} catch {
|
|
3034
|
+
return false;
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
|
|
3038
|
+
}
|
|
3039
|
+
function sendWorkerMessage(ws, message) {
|
|
3040
|
+
if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
|
|
3041
|
+
return;
|
|
3042
|
+
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;
|
|
3043
|
+
if (recoveryStore && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
|
|
3044
|
+
recoveryStore.unknown(message.requestId);
|
|
3045
|
+
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
3046
|
+
return;
|
|
2963
3047
|
}
|
|
3048
|
+
const recorded = recoveryStore?.record(message) ?? message;
|
|
3049
|
+
if ((message.type === "exec_exit" || message.type === "exec_error") && infrastructureStoppedRunIds.has(message.runId)) {
|
|
3050
|
+
recoveryStore?.unknownRun(message.runId);
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
if (message.type === "exec_output" && workerRecoveryId !== void 0) return;
|
|
3054
|
+
sendReplayWorkerMessage(ws, recorded);
|
|
3055
|
+
}
|
|
3056
|
+
function sendSerializedWorkerMessage(ws, serialized) {
|
|
3057
|
+
sendWorkerMessage(ws, JSON.parse(serialized));
|
|
2964
3058
|
}
|
|
2965
3059
|
function sendWorkerMessageFromCurrentSource(ws, message) {
|
|
2966
|
-
if (
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
} catch (error) {
|
|
2970
|
-
process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
|
|
2971
|
-
`);
|
|
3060
|
+
if ("requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_configured") {
|
|
3061
|
+
sendWorkerMessage(ws, message);
|
|
3062
|
+
return;
|
|
2972
3063
|
}
|
|
3064
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
3065
|
+
sendWorkerMessage(ws, message);
|
|
2973
3066
|
}
|
|
2974
3067
|
function buildActiveProcessReports() {
|
|
2975
3068
|
return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
|
|
@@ -3489,13 +3582,15 @@ async function startWorker(options, projectRuntime = {
|
|
|
3489
3582
|
reconciledProjectConfigFingerprints: /* @__PURE__ */ new Map(),
|
|
3490
3583
|
gitIdentity: null
|
|
3491
3584
|
}) {
|
|
3492
|
-
advanceWorkerAdmissionGeneration();
|
|
3493
3585
|
const config = readConfig(options.configPath);
|
|
3494
3586
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
3495
3587
|
initializeCredentialStoreDirectoryOnce();
|
|
3496
3588
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
3497
3589
|
validateLabel(label);
|
|
3498
3590
|
const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
3591
|
+
recoveryStore ??= new WorkerRecoveryStore(
|
|
3592
|
+
path.join(rootDir, ".worker-recovery", createHash("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
|
|
3593
|
+
);
|
|
3499
3594
|
const projectsRoot = path.join(rootDir, "projects");
|
|
3500
3595
|
const syncRoot = path.join(rootDir, "sync");
|
|
3501
3596
|
const workspaceShadowRoot = path.join(syncRoot, "workspace");
|
|
@@ -3516,7 +3611,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
3516
3611
|
`);
|
|
3517
3612
|
runGit(["--version"]);
|
|
3518
3613
|
await verifyR5dctlAuth(baseUrl, token, label);
|
|
3519
|
-
const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3614
|
+
const initializedWorkspaceState = projectRuntime.initializedWorkspaceState ?? await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3520
3615
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
3521
3616
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
3522
3617
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
@@ -3527,8 +3622,9 @@ async function startWorker(options, projectRuntime = {
|
|
|
3527
3622
|
});
|
|
3528
3623
|
return { store, state: store.read() };
|
|
3529
3624
|
});
|
|
3625
|
+
projectRuntime.initializedWorkspaceState = initializedWorkspaceState;
|
|
3530
3626
|
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3531
|
-
let projectWorkspaceState = initializedWorkspaceState.
|
|
3627
|
+
let projectWorkspaceState = initializedWorkspaceState.store.read();
|
|
3532
3628
|
const { projectConfigById, readyProjectIds, reconciledProjectConfigFingerprints } = projectRuntime;
|
|
3533
3629
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
3534
3630
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
@@ -3568,11 +3664,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
3568
3664
|
let workspaceConfigurationReceiptGeneration = 0;
|
|
3569
3665
|
let workspaceAutomaticTimer;
|
|
3570
3666
|
let workspacePeriodicTimer;
|
|
3571
|
-
let pendingAutomaticTrigger;
|
|
3667
|
+
let pendingAutomaticTrigger = projectRuntime.pendingAutomaticTrigger;
|
|
3572
3668
|
const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
|
|
3573
3669
|
const workspaceProjectionLedger = new WorkspaceProjectionLedger();
|
|
3574
3670
|
let automaticSyncInFlight = false;
|
|
3575
3671
|
let terminalReplayTimer;
|
|
3672
|
+
let outputReplayTimer;
|
|
3576
3673
|
let workspaceSyncRequestsInFlight = 0;
|
|
3577
3674
|
let lastWorkspaceSyncMirrorObservationMs = 0;
|
|
3578
3675
|
let lastInventoriedMissingCheckouts;
|
|
@@ -4664,7 +4761,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4664
4761
|
});
|
|
4665
4762
|
const workspaceSyncAdmissionSnapshotsMatch = (left, right) => left.generation === right.generation && left.activeIncidentId === right.activeIncidentId && left.deferredIncidentId === right.deferredIncidentId;
|
|
4666
4763
|
const assertWorkspaceSyncStillAdmitted = (admitted, trigger) => {
|
|
4667
|
-
if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) ||
|
|
4764
|
+
if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, trigger)) {
|
|
4668
4765
|
throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
|
|
4669
4766
|
}
|
|
4670
4767
|
};
|
|
@@ -4718,6 +4815,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4718
4815
|
bouncePending: unsafeWorkspaceSyncBouncePending,
|
|
4719
4816
|
invalidateExecution: () => {
|
|
4720
4817
|
workspaceConfigured = false;
|
|
4818
|
+
projectRuntime.configuration = void 0;
|
|
4721
4819
|
advanceWorkerAdmissionGeneration();
|
|
4722
4820
|
},
|
|
4723
4821
|
scheduleRecoveryBounce: () => {
|
|
@@ -4828,7 +4926,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4828
4926
|
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
4829
4927
|
throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
|
|
4830
4928
|
}
|
|
4831
|
-
ws
|
|
4929
|
+
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "workspace_sync_started", attemptId }));
|
|
4832
4930
|
}
|
|
4833
4931
|
let queueMs = 0;
|
|
4834
4932
|
let prepareMs = 0;
|
|
@@ -4880,6 +4978,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4880
4978
|
};
|
|
4881
4979
|
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
4882
4980
|
pendingAutomaticTrigger = trigger;
|
|
4981
|
+
projectRuntime.pendingAutomaticTrigger = trigger;
|
|
4883
4982
|
const pendingBranchDeferral = () => pendingCreatedBranchAutomaticSyncDeferral(
|
|
4884
4983
|
projectWorkspaceState.locallyPendingCreatedBranches,
|
|
4885
4984
|
activeWorkspaceMutationTargets(),
|
|
@@ -4902,6 +5001,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4902
5001
|
return;
|
|
4903
5002
|
}
|
|
4904
5003
|
pendingAutomaticTrigger = void 0;
|
|
5004
|
+
projectRuntime.pendingAutomaticTrigger = void 0;
|
|
4905
5005
|
automaticSyncInFlight = true;
|
|
4906
5006
|
void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
|
|
4907
5007
|
if (result.outcome === "failed") {
|
|
@@ -4925,9 +5025,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
4925
5025
|
workspaceAutomaticTimer.unref();
|
|
4926
5026
|
};
|
|
4927
5027
|
const markWorkspaceDirty = (trigger, force = false, target) => {
|
|
5028
|
+
if (projectRuntime.markWorkspaceDirty && projectRuntime.markWorkspaceDirty !== markWorkspaceDirty) {
|
|
5029
|
+
projectRuntime.markWorkspaceDirty(trigger, force, target);
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
4928
5032
|
if (target) recordVisibleWorkspaceMutation(target);
|
|
4929
5033
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
4930
5034
|
};
|
|
5035
|
+
projectRuntime.markWorkspaceDirty = markWorkspaceDirty;
|
|
4931
5036
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
4932
5037
|
const resolveMessageTarget = (target) => {
|
|
4933
5038
|
if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
|
|
@@ -5445,6 +5550,10 @@ async function startWorker(options, projectRuntime = {
|
|
|
5445
5550
|
Authorization: `Bearer ${token}`
|
|
5446
5551
|
}
|
|
5447
5552
|
});
|
|
5553
|
+
const connectionAttemptTimer = setTimeout(() => {
|
|
5554
|
+
if (ws.readyState === WebSocket.CONNECTING) ws.terminate();
|
|
5555
|
+
}, 3e4);
|
|
5556
|
+
connectionAttemptTimer.unref();
|
|
5448
5557
|
let lastServerControlMessageAtMs = performance.now();
|
|
5449
5558
|
let serverHeartbeatProbeStartedAtMs;
|
|
5450
5559
|
let lastServerHeartbeatSweepAtMs = performance.now();
|
|
@@ -5540,7 +5649,9 @@ async function startWorker(options, projectRuntime = {
|
|
|
5540
5649
|
process.on("SIGTERM", handleGracefulShutdown);
|
|
5541
5650
|
process.on("SIGINT", handleGracefulShutdown);
|
|
5542
5651
|
ws.addEventListener("open", () => {
|
|
5652
|
+
clearTimeout(connectionAttemptTimer);
|
|
5543
5653
|
currentWorkerSocket = ws;
|
|
5654
|
+
workerRecoveryId = "connecting";
|
|
5544
5655
|
noteServerControlActivity();
|
|
5545
5656
|
lastServerHeartbeatSweepAtMs = performance.now();
|
|
5546
5657
|
serverHeartbeatWatchdog = setInterval(() => {
|
|
@@ -5584,6 +5695,9 @@ async function startWorker(options, projectRuntime = {
|
|
|
5584
5695
|
});
|
|
5585
5696
|
const hello = {
|
|
5586
5697
|
type: "hello",
|
|
5698
|
+
resumableProtocol: WORKER_RESUMABLE_PROTOCOL,
|
|
5699
|
+
runtimeId: workerRuntimeId,
|
|
5700
|
+
ledgerId: recoveryStore.ledgerId,
|
|
5587
5701
|
startupReady: startupRecoveryComplete,
|
|
5588
5702
|
hostInfo: {
|
|
5589
5703
|
hostname: hostname(),
|
|
@@ -5608,15 +5722,17 @@ async function startWorker(options, projectRuntime = {
|
|
|
5608
5722
|
root: rootDir
|
|
5609
5723
|
}
|
|
5610
5724
|
};
|
|
5611
|
-
ws
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
terminalReplayTimer = setInterval(() => {
|
|
5617
|
-
for (const terminal of pendingProcessTerminals.values()) {
|
|
5618
|
-
sendWorkerMessage(ws, terminal);
|
|
5725
|
+
sendSerializedWorkerMessage(ws, JSON.stringify(hello));
|
|
5726
|
+
outputReplayTimer = setInterval(() => {
|
|
5727
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
|
|
5728
|
+
for (const output of recoveryStore.output()) {
|
|
5729
|
+
if (!sendReplayWorkerMessage(ws, output)) break;
|
|
5619
5730
|
}
|
|
5731
|
+
}, 250);
|
|
5732
|
+
outputReplayTimer.unref();
|
|
5733
|
+
terminalReplayTimer = setInterval(() => {
|
|
5734
|
+
if (workerRecoveryId !== void 0) return;
|
|
5735
|
+
for (const terminal of recoveryStore?.terminals() ?? []) sendReplayWorkerMessage(ws, terminal);
|
|
5620
5736
|
}, 5e3);
|
|
5621
5737
|
terminalReplayTimer.unref();
|
|
5622
5738
|
process.stdout.write("[r5d-worker] connected\n");
|
|
@@ -5624,18 +5740,103 @@ async function startWorker(options, projectRuntime = {
|
|
|
5624
5740
|
ws.addEventListener("message", (event) => {
|
|
5625
5741
|
noteServerControlActivity();
|
|
5626
5742
|
void (async () => {
|
|
5743
|
+
if (currentWorkerSocket !== ws) return;
|
|
5627
5744
|
const message = JSON.parse(String(event.data));
|
|
5745
|
+
if (message.type === "recovery_request") {
|
|
5746
|
+
workerRecoveryId = message.recoveryId;
|
|
5747
|
+
sendActiveProcessReport(ws);
|
|
5748
|
+
sendWorkerMessage(ws, {
|
|
5749
|
+
type: "recovery_state",
|
|
5750
|
+
recoveryId: message.recoveryId,
|
|
5751
|
+
runtimeId: workerRuntimeId,
|
|
5752
|
+
ledgerId: recoveryStore.ledgerId,
|
|
5753
|
+
operations: recoveryStore.operations()
|
|
5754
|
+
});
|
|
5755
|
+
const cursors = new Map(recoveryStore.outputCursors().map((cursor) => [cursor.runId, cursor]));
|
|
5756
|
+
for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
|
|
5757
|
+
workerOutputWindow.reset([...cursors.values()]);
|
|
5758
|
+
for (const response of recoveryStore.responses(message.requests)) sendReplayWorkerMessage(ws, response);
|
|
5759
|
+
for (const output of recoveryStore.output(message.outputOffsets)) {
|
|
5760
|
+
while (true) {
|
|
5761
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
|
|
5762
|
+
return;
|
|
5763
|
+
if (sendReplayWorkerMessage(ws, output)) break;
|
|
5764
|
+
await Bun.sleep(10);
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
5768
|
+
for (const terminal of recoveryStore.terminals()) sendReplayWorkerMessage(ws, terminal);
|
|
5769
|
+
sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
|
|
5770
|
+
return;
|
|
5771
|
+
}
|
|
5772
|
+
if (message.type === "recovery_ready") {
|
|
5773
|
+
if (workerRecoveryId !== message.recoveryId) return;
|
|
5774
|
+
workerRecoveryId = void 0;
|
|
5775
|
+
workerReconnectAttempt = 0;
|
|
5776
|
+
renewWorkerExecutionLease(message.leaseMs);
|
|
5777
|
+
return;
|
|
5778
|
+
}
|
|
5779
|
+
if (message.type === "worker_execution_lease") {
|
|
5780
|
+
renewWorkerExecutionLease(message.leaseMs);
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
if (message.type === "operation_result_ack") {
|
|
5784
|
+
recoveryStore.acknowledgeResult(message.requestId);
|
|
5785
|
+
return;
|
|
5786
|
+
}
|
|
5787
|
+
if (message.type === "exec_output_ack") {
|
|
5788
|
+
recoveryStore.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
|
|
5789
|
+
workerOutputWindow.acknowledge(message.runId, message.stream, message.nextOffset);
|
|
5790
|
+
return;
|
|
5791
|
+
}
|
|
5792
|
+
if (message.type === "cancel_session") {
|
|
5793
|
+
recoveryStore.cancelSession(message.sessionId);
|
|
5794
|
+
await Promise.all(
|
|
5795
|
+
[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
5796
|
+
cancelledProcessRuns.add(runId);
|
|
5797
|
+
closeProcessStdin(active);
|
|
5798
|
+
await terminateProcessTree(active.process);
|
|
5799
|
+
})
|
|
5800
|
+
);
|
|
5801
|
+
sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
|
|
5802
|
+
return;
|
|
5803
|
+
}
|
|
5628
5804
|
if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
|
|
5629
5805
|
if (!startupRecoveryComplete && message.type !== "ping") return;
|
|
5806
|
+
if (currentWorkerSocket !== ws) return;
|
|
5807
|
+
const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
|
|
5808
|
+
if (operationRequestId) {
|
|
5809
|
+
const admission = recoveryStore.admit({ ...message, requestId: operationRequestId });
|
|
5810
|
+
if (admission === "conflict") {
|
|
5811
|
+
ws.close(1008, "Operation identity conflict");
|
|
5812
|
+
return;
|
|
5813
|
+
}
|
|
5814
|
+
sendWorkerMessage(ws, { type: "operation_received", requestId: operationRequestId });
|
|
5815
|
+
if (admission !== "new") {
|
|
5816
|
+
const response = recoveryStore.response(operationRequestId);
|
|
5817
|
+
if (response && admission !== "unknown") sendReplayWorkerMessage(ws, response);
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5630
5821
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
5631
|
-
const
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5822
|
+
const admissionCredentials = configuredCredentialGenerationFingerprint;
|
|
5823
|
+
const assertMessageAdmission = () => {
|
|
5824
|
+
if (operationRequestId) {
|
|
5825
|
+
if (workerLeaseExpired || recoveryStore.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
|
|
5826
|
+
recoveryStore.unknown(operationRequestId);
|
|
5827
|
+
throw new StaleWorkerAdmissionError();
|
|
5828
|
+
}
|
|
5829
|
+
return;
|
|
5830
|
+
}
|
|
5831
|
+
assertWorkerChildAdmission({
|
|
5832
|
+
capturedGeneration: messageAdmissionGeneration,
|
|
5833
|
+
currentGeneration: workerAdmissionGeneration,
|
|
5834
|
+
sourceSocketIsCurrent: currentWorkerSocket === ws,
|
|
5835
|
+
sourceSocketOpen: ws.readyState === WebSocket.OPEN,
|
|
5836
|
+
workspaceConfigured,
|
|
5837
|
+
runtimeHealthy: !shutdownAfterClose
|
|
5838
|
+
});
|
|
5839
|
+
};
|
|
5639
5840
|
if (message.type === "connected") {
|
|
5640
5841
|
return;
|
|
5641
5842
|
}
|
|
@@ -5651,8 +5852,17 @@ async function startWorker(options, projectRuntime = {
|
|
|
5651
5852
|
if (message.type === "workspace_config") {
|
|
5652
5853
|
const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
|
|
5653
5854
|
const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
|
|
5654
|
-
|
|
5655
|
-
const
|
|
5855
|
+
const digest = workerOperationDigest(message);
|
|
5856
|
+
const cached = projectRuntime.configuration;
|
|
5857
|
+
if (cached?.digest !== digest) advanceWorkerAdmissionGeneration();
|
|
5858
|
+
const canReuseConfiguration = cached?.digest === digest && !unsafeWorkspaceSyncBouncePending && credentialGenerationFingerprint({ ...message, workerBaseUrl: baseUrl, workerAuthHeader: bearerAuthHeader }) === configuredCredentialGenerationFingerprint;
|
|
5859
|
+
const configured = canReuseConfiguration ? cached.configured : await configureWorkerWorkspace(message, receiptGeneration);
|
|
5860
|
+
if (canReuseConfiguration && configured.result.outcome !== "failed") {
|
|
5861
|
+
workspaceConfigured = true;
|
|
5862
|
+
activeWorkspaceIncidentId = cached.incidentId;
|
|
5863
|
+
deferredWorkspaceConfiguration = cached.deferred;
|
|
5864
|
+
}
|
|
5865
|
+
projectRuntime.configuration = configured.result.outcome !== "failed" ? { digest, configured, incidentId: activeWorkspaceIncidentId, deferred: deferredWorkspaceConfiguration } : void 0;
|
|
5656
5866
|
unsafeWorkspaceSyncBouncePending = false;
|
|
5657
5867
|
if (configured.result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
|
|
5658
5868
|
sendWorkerMessageFromCurrentSource(ws, {
|
|
@@ -5698,16 +5908,31 @@ async function startWorker(options, projectRuntime = {
|
|
|
5698
5908
|
return;
|
|
5699
5909
|
}
|
|
5700
5910
|
if (message.type === "sync_workspace") {
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5911
|
+
let retry = 0;
|
|
5912
|
+
let recoveryDeadline;
|
|
5913
|
+
while (true) {
|
|
5914
|
+
const result = await runWorkspaceSync({
|
|
5915
|
+
requestId: message.requestId,
|
|
5916
|
+
attemptId: message.attemptId,
|
|
5917
|
+
trigger: message.trigger,
|
|
5918
|
+
confirmedLargeDiff: message.confirmedLargeDiff,
|
|
5919
|
+
confirmationReason: message.confirmationReason,
|
|
5920
|
+
resetToCanonical: message.resetToCanonical,
|
|
5921
|
+
requiredAncestorHeads: message.requiredAncestorHeads,
|
|
5922
|
+
sendResult: false
|
|
5923
|
+
});
|
|
5924
|
+
if (result.outcome !== "failed" || !isWorkerCommunicationFailure(result.error ?? "")) {
|
|
5925
|
+
sendWorkspaceSyncResult(message.requestId, result);
|
|
5926
|
+
return;
|
|
5927
|
+
}
|
|
5928
|
+
recoveryDeadline ??= Date.now() + WORKER_RECOVERY_TIMEOUT_MS;
|
|
5929
|
+
if (workerLeaseExpired || Date.now() >= recoveryDeadline) {
|
|
5930
|
+
recoveryStore.unknown(message.requestId);
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5933
|
+
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
5934
|
+
await Bun.sleep(workerReconnectDelayMs(Math.random, retry++));
|
|
5935
|
+
}
|
|
5711
5936
|
}
|
|
5712
5937
|
if (message.type === "create_project_branch") {
|
|
5713
5938
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
@@ -5973,6 +6198,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5973
6198
|
}
|
|
5974
6199
|
if (message.type === "exec_terminal_ack") {
|
|
5975
6200
|
pendingProcessTerminals.delete(message.runId);
|
|
6201
|
+
recoveryStore.acknowledgeTerminal(message.runId);
|
|
5976
6202
|
return;
|
|
5977
6203
|
}
|
|
5978
6204
|
if (message.type === "port_forward_connect") {
|
|
@@ -6024,7 +6250,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6024
6250
|
return;
|
|
6025
6251
|
}
|
|
6026
6252
|
if (message.type === "ping") {
|
|
6027
|
-
ws
|
|
6253
|
+
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
|
|
6028
6254
|
return;
|
|
6029
6255
|
}
|
|
6030
6256
|
if (message.type === "cancel") {
|
|
@@ -6044,7 +6270,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6044
6270
|
cancelledProcessRuns.add(message.runId);
|
|
6045
6271
|
cancelMessage = "Cancellation queued before command start";
|
|
6046
6272
|
}
|
|
6047
|
-
|
|
6273
|
+
sendSerializedWorkerMessage(
|
|
6274
|
+
ws,
|
|
6048
6275
|
JSON.stringify({
|
|
6049
6276
|
type: "cancel_result",
|
|
6050
6277
|
requestId: message.requestId,
|
|
@@ -6056,7 +6283,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6056
6283
|
return;
|
|
6057
6284
|
}
|
|
6058
6285
|
if (message.type === "exec_stdin") {
|
|
6059
|
-
const sendAck = (payload) =>
|
|
6286
|
+
const sendAck = (payload) => sendSerializedWorkerMessage(
|
|
6287
|
+
ws,
|
|
6060
6288
|
JSON.stringify({
|
|
6061
6289
|
type: "operation_result",
|
|
6062
6290
|
requestId: message.requestId,
|
|
@@ -6179,16 +6407,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
6179
6407
|
ptyId: message.ptyId,
|
|
6180
6408
|
error: `Shell input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
|
|
6181
6409
|
});
|
|
6410
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6182
6411
|
return;
|
|
6183
6412
|
}
|
|
6184
6413
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6185
6414
|
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
|
|
6186
6415
|
}
|
|
6187
6416
|
writePty(ws, message);
|
|
6417
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6188
6418
|
return;
|
|
6189
6419
|
}
|
|
6190
6420
|
if (message.type === "pty_resize") {
|
|
6191
6421
|
resizePty(message);
|
|
6422
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6192
6423
|
return;
|
|
6193
6424
|
}
|
|
6194
6425
|
if (message.type === "pty_close") {
|
|
@@ -6197,6 +6428,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6197
6428
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6198
6429
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
6199
6430
|
}
|
|
6431
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6200
6432
|
return;
|
|
6201
6433
|
}
|
|
6202
6434
|
if (message.type === "exec") {
|
|
@@ -6278,7 +6510,11 @@ async function startWorker(options, projectRuntime = {
|
|
|
6278
6510
|
} finally {
|
|
6279
6511
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6280
6512
|
}
|
|
6281
|
-
|
|
6513
|
+
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6514
|
+
recoveryStore.unknown(message.requestId);
|
|
6515
|
+
return;
|
|
6516
|
+
}
|
|
6517
|
+
sendSerializedWorkerMessage(ws, JSON.stringify(result));
|
|
6282
6518
|
if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
|
|
6283
6519
|
markWorkspaceDirty(
|
|
6284
6520
|
{ type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
|
|
@@ -6401,7 +6637,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6401
6637
|
assertAdmission: assertMessageAdmission
|
|
6402
6638
|
});
|
|
6403
6639
|
});
|
|
6404
|
-
|
|
6640
|
+
sendSerializedWorkerMessage(
|
|
6641
|
+
ws,
|
|
6405
6642
|
JSON.stringify({
|
|
6406
6643
|
type: "operation_result",
|
|
6407
6644
|
requestId: message.requestId,
|
|
@@ -6418,7 +6655,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6418
6655
|
markWorkspaceDirty(dirtyTrigger, false, message.target);
|
|
6419
6656
|
}
|
|
6420
6657
|
} catch (error) {
|
|
6421
|
-
|
|
6658
|
+
sendSerializedWorkerMessage(
|
|
6659
|
+
ws,
|
|
6422
6660
|
JSON.stringify({
|
|
6423
6661
|
type: "operation_result",
|
|
6424
6662
|
requestId: message.requestId,
|
|
@@ -6437,7 +6675,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6437
6675
|
}
|
|
6438
6676
|
const unhandledMessage = message;
|
|
6439
6677
|
if (typeof unhandledMessage.requestId === "string") {
|
|
6440
|
-
|
|
6678
|
+
sendSerializedWorkerMessage(
|
|
6679
|
+
ws,
|
|
6441
6680
|
JSON.stringify({
|
|
6442
6681
|
type: "operation_result",
|
|
6443
6682
|
requestId: unhandledMessage.requestId,
|
|
@@ -6451,6 +6690,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6451
6690
|
});
|
|
6452
6691
|
});
|
|
6453
6692
|
ws.addEventListener("close", (event) => {
|
|
6693
|
+
clearTimeout(connectionAttemptTimer);
|
|
6454
6694
|
const processEvents = process;
|
|
6455
6695
|
processEvents.off("SIGTERM", handleGracefulShutdown);
|
|
6456
6696
|
processEvents.off("SIGINT", handleGracefulShutdown);
|
|
@@ -6467,15 +6707,20 @@ async function startWorker(options, projectRuntime = {
|
|
|
6467
6707
|
clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
|
|
6468
6708
|
deferredWorkspaceConfigurationRefreshTimer = void 0;
|
|
6469
6709
|
}
|
|
6710
|
+
if (outputReplayTimer) {
|
|
6711
|
+
clearInterval(outputReplayTimer);
|
|
6712
|
+
outputReplayTimer = void 0;
|
|
6713
|
+
}
|
|
6470
6714
|
if (terminalReplayTimer) {
|
|
6471
6715
|
clearInterval(terminalReplayTimer);
|
|
6472
6716
|
terminalReplayTimer = void 0;
|
|
6473
6717
|
}
|
|
6474
|
-
if (
|
|
6475
|
-
|
|
6476
|
-
|
|
6718
|
+
if (projectRuntime.configuration) {
|
|
6719
|
+
projectRuntime.configuration.incidentId = activeWorkspaceIncidentId;
|
|
6720
|
+
projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
|
|
6477
6721
|
}
|
|
6478
|
-
|
|
6722
|
+
if (currentWorkerSocket === ws) currentWorkerSocket = null;
|
|
6723
|
+
workerRecoveryId = "reconnecting";
|
|
6479
6724
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6480
6725
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
6481
6726
|
`);
|
|
@@ -6485,50 +6730,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
6485
6730
|
if (shutdownAfterClose) {
|
|
6486
6731
|
process.exit(0);
|
|
6487
6732
|
}
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
if (!startupRecoveryComplete) {
|
|
6492
|
-
process.exit(1);
|
|
6493
|
-
return;
|
|
6494
|
-
}
|
|
6495
|
-
const reconnectDelayMs = workerReconnectDelayMs();
|
|
6496
|
-
process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
|
|
6497
|
-
`);
|
|
6498
|
-
setTimeout(() => {
|
|
6499
|
-
void startWorker(options, projectRuntime).catch((error) => {
|
|
6500
|
-
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6733
|
+
const reconnect = () => {
|
|
6734
|
+
const delay = workerReconnectDelayMs(Math.random, workerReconnectAttempt++);
|
|
6735
|
+
process.stderr.write(`[r5d-worker] reconnecting in ${delay}ms
|
|
6501
6736
|
`);
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
}, reconnectDelayMs);
|
|
6505
|
-
});
|
|
6506
|
-
return;
|
|
6507
|
-
}
|
|
6508
|
-
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
6509
|
-
const scheduleReconnect = (reconnect2) => {
|
|
6510
|
-
const reconnectDelayMs = workerReconnectDelayMs();
|
|
6511
|
-
process.stderr.write(
|
|
6512
|
-
`[r5d-worker] ${activeProcesses.size} process(es), ${credentialBearingProcessGroups.size} owned process group(s), and ${activePtys.size} PTY(s) active with ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${reconnectDelayMs}ms
|
|
6513
|
-
`
|
|
6514
|
-
);
|
|
6515
|
-
setTimeout(reconnect2, reconnectDelayMs);
|
|
6516
|
-
};
|
|
6517
|
-
const reconnect = () => {
|
|
6518
|
-
void startWorker(options, projectRuntime).catch((error) => {
|
|
6737
|
+
setTimeout(() => {
|
|
6738
|
+
void startupRecoveryPromise.then(() => startWorker(options, projectRuntime)).catch((error) => {
|
|
6519
6739
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6520
6740
|
`);
|
|
6521
|
-
|
|
6522
|
-
scheduleReconnect(reconnect);
|
|
6523
|
-
return;
|
|
6524
|
-
}
|
|
6525
|
-
process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
|
|
6741
|
+
reconnect();
|
|
6526
6742
|
});
|
|
6527
|
-
};
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
}
|
|
6531
|
-
process.exit(WORKER_RECONNECT_EXIT_CODE);
|
|
6743
|
+
}, delay);
|
|
6744
|
+
};
|
|
6745
|
+
reconnect();
|
|
6532
6746
|
});
|
|
6533
6747
|
ws.addEventListener("error", (event) => {
|
|
6534
6748
|
const message = "message" in event && typeof event.message === "string" && event.message.trim() ? `: ${event.message.trim()}` : "";
|