@ricsam/r5d-worker 0.0.108 → 0.0.110
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 +362 -128
- 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 +365 -128
- 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}` : ""}`);
|
|
650
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`);
|
|
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;
|
|
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;
|
|
2963
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,11 +5852,15 @@ 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
|
|
5656
|
-
|
|
5657
|
-
if (
|
|
5658
|
-
|
|
5855
|
+
const digest = workerOperationDigest(message);
|
|
5856
|
+
const cached = projectRuntime.configuration;
|
|
5857
|
+
const canReuseConfiguration = cached?.digest === digest && cached.workspaceSync?.remoteUrl === message.workspaceRemoteUrl && Boolean(cached.workspaceSync.credentialHelper) && cached.workspaceSync.credentialUsername === workerCredentialUsername && cached.workspaceSync.gitIdentity?.name === message.gitIdentity.name && cached.workspaceSync.gitIdentity?.email === message.gitIdentity.email && !unsafeWorkspaceSyncBouncePending && credentialGenerationFingerprint({ ...message, workerBaseUrl: baseUrl, workerAuthHeader: bearerAuthHeader }) === configuredCredentialGenerationFingerprint;
|
|
5858
|
+
if (!canReuseConfiguration) {
|
|
5859
|
+
advanceWorkerAdmissionGeneration();
|
|
5860
|
+
projectRuntime.configuration = void 0;
|
|
5861
|
+
}
|
|
5862
|
+
const configured = canReuseConfiguration ? cached.configured : await configureWorkerWorkspace(message, receiptGeneration);
|
|
5863
|
+
const sendConfigured = () => sendWorkerMessageFromCurrentSource(ws, {
|
|
5659
5864
|
type: "workspace_configured",
|
|
5660
5865
|
requestId: message.requestId,
|
|
5661
5866
|
result: configured.result,
|
|
@@ -5663,6 +5868,34 @@ async function startWorker(options, projectRuntime = {
|
|
|
5663
5868
|
aheadOfOriginBranches: configured.aheadOfOriginBranches,
|
|
5664
5869
|
...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
|
|
5665
5870
|
});
|
|
5871
|
+
if (receiptGeneration !== workspaceConfigurationReceiptGeneration || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
5872
|
+
sendConfigured();
|
|
5873
|
+
return;
|
|
5874
|
+
}
|
|
5875
|
+
if (canReuseConfiguration && configured.result.outcome !== "failed") {
|
|
5876
|
+
workspaceRemoteUrl = cached.workspaceSync.remoteUrl;
|
|
5877
|
+
workspaceCredentialHelper = cached.workspaceSync.credentialHelper;
|
|
5878
|
+
workspaceCredentialUsername = cached.workspaceSync.credentialUsername;
|
|
5879
|
+
workspaceGitIdentity = cached.workspaceSync.gitIdentity;
|
|
5880
|
+
workspaceConfigured = true;
|
|
5881
|
+
activeWorkspaceIncidentId = cached.incidentId;
|
|
5882
|
+
deferredWorkspaceConfiguration = cached.deferred;
|
|
5883
|
+
}
|
|
5884
|
+
projectRuntime.configuration = configured.result.outcome !== "failed" && workspaceRemoteUrl && workspaceCredentialHelper && workspaceCredentialUsername && workspaceGitIdentity ? {
|
|
5885
|
+
digest,
|
|
5886
|
+
workspaceSync: {
|
|
5887
|
+
remoteUrl: workspaceRemoteUrl,
|
|
5888
|
+
credentialHelper: workspaceCredentialHelper,
|
|
5889
|
+
credentialUsername: workspaceCredentialUsername,
|
|
5890
|
+
gitIdentity: workspaceGitIdentity
|
|
5891
|
+
},
|
|
5892
|
+
configured,
|
|
5893
|
+
incidentId: activeWorkspaceIncidentId,
|
|
5894
|
+
deferred: deferredWorkspaceConfiguration
|
|
5895
|
+
} : void 0;
|
|
5896
|
+
unsafeWorkspaceSyncBouncePending = false;
|
|
5897
|
+
if (configured.result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
|
|
5898
|
+
sendConfigured();
|
|
5666
5899
|
sendLifecycleProgress(
|
|
5667
5900
|
configured.result.outcome === "failed" ? {
|
|
5668
5901
|
phase: "failed",
|
|
@@ -5698,16 +5931,31 @@ async function startWorker(options, projectRuntime = {
|
|
|
5698
5931
|
return;
|
|
5699
5932
|
}
|
|
5700
5933
|
if (message.type === "sync_workspace") {
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5934
|
+
let retry = 0;
|
|
5935
|
+
let recoveryDeadline;
|
|
5936
|
+
while (true) {
|
|
5937
|
+
const result = await runWorkspaceSync({
|
|
5938
|
+
requestId: message.requestId,
|
|
5939
|
+
attemptId: message.attemptId,
|
|
5940
|
+
trigger: message.trigger,
|
|
5941
|
+
confirmedLargeDiff: message.confirmedLargeDiff,
|
|
5942
|
+
confirmationReason: message.confirmationReason,
|
|
5943
|
+
resetToCanonical: message.resetToCanonical,
|
|
5944
|
+
requiredAncestorHeads: message.requiredAncestorHeads,
|
|
5945
|
+
sendResult: false
|
|
5946
|
+
});
|
|
5947
|
+
if (result.outcome !== "failed" || !isWorkerCommunicationFailure(result.error ?? "")) {
|
|
5948
|
+
sendWorkspaceSyncResult(message.requestId, result);
|
|
5949
|
+
return;
|
|
5950
|
+
}
|
|
5951
|
+
recoveryDeadline ??= Date.now() + WORKER_RECOVERY_TIMEOUT_MS;
|
|
5952
|
+
if (workerLeaseExpired || Date.now() >= recoveryDeadline) {
|
|
5953
|
+
recoveryStore.unknown(message.requestId);
|
|
5954
|
+
return;
|
|
5955
|
+
}
|
|
5956
|
+
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
5957
|
+
await Bun.sleep(workerReconnectDelayMs(Math.random, retry++));
|
|
5958
|
+
}
|
|
5711
5959
|
}
|
|
5712
5960
|
if (message.type === "create_project_branch") {
|
|
5713
5961
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
@@ -5973,6 +6221,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5973
6221
|
}
|
|
5974
6222
|
if (message.type === "exec_terminal_ack") {
|
|
5975
6223
|
pendingProcessTerminals.delete(message.runId);
|
|
6224
|
+
recoveryStore.acknowledgeTerminal(message.runId);
|
|
5976
6225
|
return;
|
|
5977
6226
|
}
|
|
5978
6227
|
if (message.type === "port_forward_connect") {
|
|
@@ -6024,7 +6273,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6024
6273
|
return;
|
|
6025
6274
|
}
|
|
6026
6275
|
if (message.type === "ping") {
|
|
6027
|
-
ws
|
|
6276
|
+
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
|
|
6028
6277
|
return;
|
|
6029
6278
|
}
|
|
6030
6279
|
if (message.type === "cancel") {
|
|
@@ -6044,7 +6293,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6044
6293
|
cancelledProcessRuns.add(message.runId);
|
|
6045
6294
|
cancelMessage = "Cancellation queued before command start";
|
|
6046
6295
|
}
|
|
6047
|
-
|
|
6296
|
+
sendSerializedWorkerMessage(
|
|
6297
|
+
ws,
|
|
6048
6298
|
JSON.stringify({
|
|
6049
6299
|
type: "cancel_result",
|
|
6050
6300
|
requestId: message.requestId,
|
|
@@ -6056,7 +6306,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6056
6306
|
return;
|
|
6057
6307
|
}
|
|
6058
6308
|
if (message.type === "exec_stdin") {
|
|
6059
|
-
const sendAck = (payload) =>
|
|
6309
|
+
const sendAck = (payload) => sendSerializedWorkerMessage(
|
|
6310
|
+
ws,
|
|
6060
6311
|
JSON.stringify({
|
|
6061
6312
|
type: "operation_result",
|
|
6062
6313
|
requestId: message.requestId,
|
|
@@ -6179,16 +6430,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
6179
6430
|
ptyId: message.ptyId,
|
|
6180
6431
|
error: `Shell input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
|
|
6181
6432
|
});
|
|
6433
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6182
6434
|
return;
|
|
6183
6435
|
}
|
|
6184
6436
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6185
6437
|
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
|
|
6186
6438
|
}
|
|
6187
6439
|
writePty(ws, message);
|
|
6440
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6188
6441
|
return;
|
|
6189
6442
|
}
|
|
6190
6443
|
if (message.type === "pty_resize") {
|
|
6191
6444
|
resizePty(message);
|
|
6445
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6192
6446
|
return;
|
|
6193
6447
|
}
|
|
6194
6448
|
if (message.type === "pty_close") {
|
|
@@ -6197,6 +6451,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6197
6451
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6198
6452
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
6199
6453
|
}
|
|
6454
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6200
6455
|
return;
|
|
6201
6456
|
}
|
|
6202
6457
|
if (message.type === "exec") {
|
|
@@ -6278,7 +6533,11 @@ async function startWorker(options, projectRuntime = {
|
|
|
6278
6533
|
} finally {
|
|
6279
6534
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6280
6535
|
}
|
|
6281
|
-
|
|
6536
|
+
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6537
|
+
recoveryStore.unknown(message.requestId);
|
|
6538
|
+
return;
|
|
6539
|
+
}
|
|
6540
|
+
sendSerializedWorkerMessage(ws, JSON.stringify(result));
|
|
6282
6541
|
if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
|
|
6283
6542
|
markWorkspaceDirty(
|
|
6284
6543
|
{ type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
|
|
@@ -6401,7 +6660,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6401
6660
|
assertAdmission: assertMessageAdmission
|
|
6402
6661
|
});
|
|
6403
6662
|
});
|
|
6404
|
-
|
|
6663
|
+
sendSerializedWorkerMessage(
|
|
6664
|
+
ws,
|
|
6405
6665
|
JSON.stringify({
|
|
6406
6666
|
type: "operation_result",
|
|
6407
6667
|
requestId: message.requestId,
|
|
@@ -6418,7 +6678,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6418
6678
|
markWorkspaceDirty(dirtyTrigger, false, message.target);
|
|
6419
6679
|
}
|
|
6420
6680
|
} catch (error) {
|
|
6421
|
-
|
|
6681
|
+
sendSerializedWorkerMessage(
|
|
6682
|
+
ws,
|
|
6422
6683
|
JSON.stringify({
|
|
6423
6684
|
type: "operation_result",
|
|
6424
6685
|
requestId: message.requestId,
|
|
@@ -6437,7 +6698,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6437
6698
|
}
|
|
6438
6699
|
const unhandledMessage = message;
|
|
6439
6700
|
if (typeof unhandledMessage.requestId === "string") {
|
|
6440
|
-
|
|
6701
|
+
sendSerializedWorkerMessage(
|
|
6702
|
+
ws,
|
|
6441
6703
|
JSON.stringify({
|
|
6442
6704
|
type: "operation_result",
|
|
6443
6705
|
requestId: unhandledMessage.requestId,
|
|
@@ -6451,6 +6713,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6451
6713
|
});
|
|
6452
6714
|
});
|
|
6453
6715
|
ws.addEventListener("close", (event) => {
|
|
6716
|
+
clearTimeout(connectionAttemptTimer);
|
|
6454
6717
|
const processEvents = process;
|
|
6455
6718
|
processEvents.off("SIGTERM", handleGracefulShutdown);
|
|
6456
6719
|
processEvents.off("SIGINT", handleGracefulShutdown);
|
|
@@ -6467,15 +6730,20 @@ async function startWorker(options, projectRuntime = {
|
|
|
6467
6730
|
clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
|
|
6468
6731
|
deferredWorkspaceConfigurationRefreshTimer = void 0;
|
|
6469
6732
|
}
|
|
6733
|
+
if (outputReplayTimer) {
|
|
6734
|
+
clearInterval(outputReplayTimer);
|
|
6735
|
+
outputReplayTimer = void 0;
|
|
6736
|
+
}
|
|
6470
6737
|
if (terminalReplayTimer) {
|
|
6471
6738
|
clearInterval(terminalReplayTimer);
|
|
6472
6739
|
terminalReplayTimer = void 0;
|
|
6473
6740
|
}
|
|
6474
|
-
if (currentWorkerSocket === ws) {
|
|
6475
|
-
|
|
6476
|
-
|
|
6741
|
+
if (currentWorkerSocket === ws && projectRuntime.configuration) {
|
|
6742
|
+
projectRuntime.configuration.incidentId = activeWorkspaceIncidentId;
|
|
6743
|
+
projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
|
|
6477
6744
|
}
|
|
6478
|
-
|
|
6745
|
+
if (currentWorkerSocket === ws) currentWorkerSocket = null;
|
|
6746
|
+
workerRecoveryId = "reconnecting";
|
|
6479
6747
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6480
6748
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
6481
6749
|
`);
|
|
@@ -6485,50 +6753,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
6485
6753
|
if (shutdownAfterClose) {
|
|
6486
6754
|
process.exit(0);
|
|
6487
6755
|
}
|
|
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
|
|
6756
|
+
const reconnect = () => {
|
|
6757
|
+
const delay = workerReconnectDelayMs(Math.random, workerReconnectAttempt++);
|
|
6758
|
+
process.stderr.write(`[r5d-worker] reconnecting in ${delay}ms
|
|
6497
6759
|
`);
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6501
|
-
`);
|
|
6502
|
-
process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
|
|
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) => {
|
|
6760
|
+
setTimeout(() => {
|
|
6761
|
+
void startupRecoveryPromise.then(() => startWorker(options, projectRuntime)).catch((error) => {
|
|
6519
6762
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6520
6763
|
`);
|
|
6521
|
-
|
|
6522
|
-
scheduleReconnect(reconnect);
|
|
6523
|
-
return;
|
|
6524
|
-
}
|
|
6525
|
-
process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
|
|
6764
|
+
reconnect();
|
|
6526
6765
|
});
|
|
6527
|
-
};
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
}
|
|
6531
|
-
process.exit(WORKER_RECONNECT_EXIT_CODE);
|
|
6766
|
+
}, delay);
|
|
6767
|
+
};
|
|
6768
|
+
reconnect();
|
|
6532
6769
|
});
|
|
6533
6770
|
ws.addEventListener("error", (event) => {
|
|
6534
6771
|
const message = "message" in event && typeof event.message === "string" && event.message.trim() ? `: ${event.message.trim()}` : "";
|