@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/cjs/main.cjs
CHANGED
|
@@ -60,6 +60,9 @@ var import_plan_paths = require("./plan-paths.cjs");
|
|
|
60
60
|
var import_plan_parser = require("./plan-parser.cjs");
|
|
61
61
|
var import_plan_lint = require("./plan-lint.cjs");
|
|
62
62
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
63
|
+
var import_recovery_store = require("./recovery-store.cjs");
|
|
64
|
+
var import_recovery_output_window = require("./recovery-output-window.cjs");
|
|
65
|
+
var import_recovery_protocol = require("./recovery-protocol.cjs");
|
|
63
66
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
64
67
|
var import_node_crypto = require("node:crypto");
|
|
65
68
|
var import_node_os = __toESM(require("node:os"), 1);
|
|
@@ -161,6 +164,63 @@ const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
|
161
164
|
const activePtys = /* @__PURE__ */ new Map();
|
|
162
165
|
const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
|
|
163
166
|
let currentWorkerSocket = null;
|
|
167
|
+
const workerRuntimeId = crypto.randomUUID();
|
|
168
|
+
const workerOutputWindow = new import_recovery_output_window.WorkerOutputWindow();
|
|
169
|
+
let recoveryStore;
|
|
170
|
+
let workerReconnectAttempt = 0;
|
|
171
|
+
let workerRecoveryId;
|
|
172
|
+
let workerExecutionLeaseDeadline = 0;
|
|
173
|
+
let workerExecutionLeaseTimer;
|
|
174
|
+
let workerLeaseExpired = false;
|
|
175
|
+
const infrastructureStoppedRunIds = /* @__PURE__ */ new Set();
|
|
176
|
+
function isWorkerCommunicationFailure(message) {
|
|
177
|
+
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
|
+
message
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
async function retryWorkerServerRead(read) {
|
|
182
|
+
let deadline;
|
|
183
|
+
let retry = 0;
|
|
184
|
+
while (true) {
|
|
185
|
+
try {
|
|
186
|
+
return await read();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (!recoveryStore || !isWorkerCommunicationFailure(error instanceof Error ? error.message : String(error))) throw error;
|
|
189
|
+
deadline ??= Date.now() + import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS;
|
|
190
|
+
if (workerLeaseExpired || Date.now() >= deadline) throw new WorkerServerUnavailableError("Worker communication recovery expired");
|
|
191
|
+
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
192
|
+
await Bun.sleep((0, import_supervisor.workerReconnectDelayMs)(Math.random, retry++));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function renewWorkerExecutionLease(leaseMs) {
|
|
197
|
+
if (!Number.isFinite(leaseMs) || leaseMs <= 0) return;
|
|
198
|
+
workerLeaseExpired = false;
|
|
199
|
+
workerExecutionLeaseDeadline = Date.now() + Math.min(leaseMs, import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS);
|
|
200
|
+
if (workerExecutionLeaseTimer) clearTimeout(workerExecutionLeaseTimer);
|
|
201
|
+
workerExecutionLeaseTimer = setTimeout(
|
|
202
|
+
() => {
|
|
203
|
+
if (Date.now() < workerExecutionLeaseDeadline) return;
|
|
204
|
+
workerLeaseExpired = true;
|
|
205
|
+
for (const operation of recoveryStore?.operations() ?? []) {
|
|
206
|
+
if (operation.sessionId && operation.state !== "completed") recoveryStore?.cancelSession(operation.sessionId);
|
|
207
|
+
}
|
|
208
|
+
for (const [runId, active] of activeProcesses) {
|
|
209
|
+
if (!active.sessionId) continue;
|
|
210
|
+
infrastructureStoppedRunIds.add(runId);
|
|
211
|
+
recoveryStore?.unknownRun(runId);
|
|
212
|
+
cancelledProcessRuns.add(runId);
|
|
213
|
+
closeProcessStdin(active);
|
|
214
|
+
void (0, import_process_tree.terminateProcessTree)(active.process).catch(
|
|
215
|
+
(error) => process.stderr.write(`[r5d-worker] execution lease termination failed: ${error}
|
|
216
|
+
`)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
Math.min(leaseMs, import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS)
|
|
221
|
+
);
|
|
222
|
+
workerExecutionLeaseTimer.unref();
|
|
223
|
+
}
|
|
164
224
|
let workerAdmissionGeneration = 0;
|
|
165
225
|
const workspaceMutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
|
|
166
226
|
let workspaceSyncQueue = Promise.resolve();
|
|
@@ -619,40 +679,46 @@ function parseRemoteFileManifest(value, collectionKey, label) {
|
|
|
619
679
|
});
|
|
620
680
|
}
|
|
621
681
|
async function fetchRemoteFileManifest(input) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
682
|
+
return retryWorkerServerRead(async () => {
|
|
683
|
+
const response = await fetch(input.url, {
|
|
684
|
+
signal: AbortSignal.timeout(3e4),
|
|
685
|
+
headers: {
|
|
686
|
+
Authorization: `Bearer ${input.token}`
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
const detail = (await readResponseText(response)).trim();
|
|
691
|
+
throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
625
692
|
}
|
|
693
|
+
return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
|
|
626
694
|
});
|
|
627
|
-
if (!response.ok) {
|
|
628
|
-
const detail = (await readResponseText(response)).trim();
|
|
629
|
-
throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
630
|
-
}
|
|
631
|
-
return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
|
|
632
695
|
}
|
|
633
696
|
async function downloadRemoteSyncedFile(input) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
697
|
+
return retryWorkerServerRead(async () => {
|
|
698
|
+
const targetPath = import_node_path.default.join(input.targetDir, input.file.filename);
|
|
699
|
+
assertInsideRoot(input.targetDir, targetPath, input.label);
|
|
700
|
+
const response = await fetch(input.url, {
|
|
701
|
+
signal: AbortSignal.timeout(3e4),
|
|
702
|
+
headers: {
|
|
703
|
+
Authorization: `Bearer ${input.token}`
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
const detail = (await readResponseText(response)).trim();
|
|
708
|
+
throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
709
|
+
}
|
|
710
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
711
|
+
const receivedHash = (0, import_node_crypto.createHash)("sha256").update(buffer).digest("hex");
|
|
712
|
+
if (receivedHash !== input.file.sha256) {
|
|
713
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
|
|
639
714
|
}
|
|
715
|
+
if (buffer.length !== input.file.size) {
|
|
716
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
|
|
717
|
+
}
|
|
718
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
719
|
+
import_node_fs.default.writeFileSync(tempPath, buffer);
|
|
720
|
+
import_node_fs.default.renameSync(tempPath, targetPath);
|
|
640
721
|
});
|
|
641
|
-
if (!response.ok) {
|
|
642
|
-
const detail = (await readResponseText(response)).trim();
|
|
643
|
-
throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
644
|
-
}
|
|
645
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
646
|
-
const receivedHash = (0, import_node_crypto.createHash)("sha256").update(buffer).digest("hex");
|
|
647
|
-
if (receivedHash !== input.file.sha256) {
|
|
648
|
-
throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
|
|
649
|
-
}
|
|
650
|
-
if (buffer.length !== input.file.size) {
|
|
651
|
-
throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
|
|
652
|
-
}
|
|
653
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
654
|
-
import_node_fs.default.writeFileSync(tempPath, buffer);
|
|
655
|
-
import_node_fs.default.renameSync(tempPath, targetPath);
|
|
656
722
|
}
|
|
657
723
|
async function syncRemoteFileSet(input) {
|
|
658
724
|
import_node_fs.default.mkdirSync(input.targetDir, { recursive: true });
|
|
@@ -779,6 +845,7 @@ async function verifyR5dctlAuth(baseUrl, token, workerLabel) {
|
|
|
779
845
|
let response;
|
|
780
846
|
try {
|
|
781
847
|
response = await fetch(statusUrl, {
|
|
848
|
+
signal: AbortSignal.timeout(3e4),
|
|
782
849
|
headers: {
|
|
783
850
|
Authorization: `Bearer ${token}`
|
|
784
851
|
}
|
|
@@ -2942,23 +3009,46 @@ function closeProcessStdin(active) {
|
|
|
2942
3009
|
}
|
|
2943
3010
|
active.stdin = void 0;
|
|
2944
3011
|
}
|
|
2945
|
-
function
|
|
3012
|
+
function sendReplayWorkerMessage(ws, message) {
|
|
2946
3013
|
const target = currentWorkerSocket ?? ws;
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3014
|
+
if (target.readyState !== WebSocket.OPEN) return false;
|
|
3015
|
+
const send = (payload) => {
|
|
3016
|
+
try {
|
|
3017
|
+
target.send(JSON.stringify(payload));
|
|
3018
|
+
return true;
|
|
3019
|
+
} catch {
|
|
3020
|
+
return false;
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
3023
|
+
return message.type === "exec_output" ? workerOutputWindow.send(message, send, target.bufferedAmount) : send(message);
|
|
3024
|
+
}
|
|
3025
|
+
function sendWorkerMessage(ws, message) {
|
|
3026
|
+
if (recoveryStore && "requestId" in message && typeof message.requestId === "string" && recoveryStore.isUnknown(message.requestId))
|
|
3027
|
+
return;
|
|
3028
|
+
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;
|
|
3029
|
+
if (recoveryStore && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
|
|
3030
|
+
recoveryStore.unknown(message.requestId);
|
|
3031
|
+
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
3032
|
+
return;
|
|
2952
3033
|
}
|
|
3034
|
+
const recorded = recoveryStore?.record(message) ?? message;
|
|
3035
|
+
if ((message.type === "exec_exit" || message.type === "exec_error") && infrastructureStoppedRunIds.has(message.runId)) {
|
|
3036
|
+
recoveryStore?.unknownRun(message.runId);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
if (message.type === "exec_output" && workerRecoveryId !== void 0) return;
|
|
3040
|
+
sendReplayWorkerMessage(ws, recorded);
|
|
3041
|
+
}
|
|
3042
|
+
function sendSerializedWorkerMessage(ws, serialized) {
|
|
3043
|
+
sendWorkerMessage(ws, JSON.parse(serialized));
|
|
2953
3044
|
}
|
|
2954
3045
|
function sendWorkerMessageFromCurrentSource(ws, message) {
|
|
2955
|
-
if (
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
} catch (error) {
|
|
2959
|
-
process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
|
|
2960
|
-
`);
|
|
3046
|
+
if ("requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_configured") {
|
|
3047
|
+
sendWorkerMessage(ws, message);
|
|
3048
|
+
return;
|
|
2961
3049
|
}
|
|
3050
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
3051
|
+
sendWorkerMessage(ws, message);
|
|
2962
3052
|
}
|
|
2963
3053
|
function buildActiveProcessReports() {
|
|
2964
3054
|
return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
|
|
@@ -3478,13 +3568,15 @@ async function startWorker(options, projectRuntime = {
|
|
|
3478
3568
|
reconciledProjectConfigFingerprints: /* @__PURE__ */ new Map(),
|
|
3479
3569
|
gitIdentity: null
|
|
3480
3570
|
}) {
|
|
3481
|
-
advanceWorkerAdmissionGeneration();
|
|
3482
3571
|
const config = readConfig(options.configPath);
|
|
3483
3572
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
3484
3573
|
initializeCredentialStoreDirectoryOnce();
|
|
3485
3574
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
3486
3575
|
validateLabel(label);
|
|
3487
3576
|
const rootDir = import_node_path.default.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
3577
|
+
recoveryStore ??= new import_recovery_store.WorkerRecoveryStore(
|
|
3578
|
+
import_node_path.default.join(rootDir, ".worker-recovery", (0, import_node_crypto.createHash)("sha256").update(`${baseUrl}\0${label}`).digest("hex"), "protocol.sqlite")
|
|
3579
|
+
);
|
|
3488
3580
|
const projectsRoot = import_node_path.default.join(rootDir, "projects");
|
|
3489
3581
|
const syncRoot = import_node_path.default.join(rootDir, "sync");
|
|
3490
3582
|
const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
|
|
@@ -3505,7 +3597,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
3505
3597
|
`);
|
|
3506
3598
|
runGit(["--version"]);
|
|
3507
3599
|
await verifyR5dctlAuth(baseUrl, token, label);
|
|
3508
|
-
const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3600
|
+
const initializedWorkspaceState = projectRuntime.initializedWorkspaceState ?? await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3509
3601
|
import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
|
|
3510
3602
|
import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
|
|
3511
3603
|
import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
|
|
@@ -3516,8 +3608,9 @@ async function startWorker(options, projectRuntime = {
|
|
|
3516
3608
|
});
|
|
3517
3609
|
return { store, state: store.read() };
|
|
3518
3610
|
});
|
|
3611
|
+
projectRuntime.initializedWorkspaceState = initializedWorkspaceState;
|
|
3519
3612
|
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3520
|
-
let projectWorkspaceState = initializedWorkspaceState.
|
|
3613
|
+
let projectWorkspaceState = initializedWorkspaceState.store.read();
|
|
3521
3614
|
const { projectConfigById, readyProjectIds, reconciledProjectConfigFingerprints } = projectRuntime;
|
|
3522
3615
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
3523
3616
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
@@ -3557,11 +3650,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
3557
3650
|
let workspaceConfigurationReceiptGeneration = 0;
|
|
3558
3651
|
let workspaceAutomaticTimer;
|
|
3559
3652
|
let workspacePeriodicTimer;
|
|
3560
|
-
let pendingAutomaticTrigger;
|
|
3653
|
+
let pendingAutomaticTrigger = projectRuntime.pendingAutomaticTrigger;
|
|
3561
3654
|
const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
|
|
3562
3655
|
const workspaceProjectionLedger = new import_workspace_projection_ledger.WorkspaceProjectionLedger();
|
|
3563
3656
|
let automaticSyncInFlight = false;
|
|
3564
3657
|
let terminalReplayTimer;
|
|
3658
|
+
let outputReplayTimer;
|
|
3565
3659
|
let workspaceSyncRequestsInFlight = 0;
|
|
3566
3660
|
let lastWorkspaceSyncMirrorObservationMs = 0;
|
|
3567
3661
|
let lastInventoriedMissingCheckouts;
|
|
@@ -4653,7 +4747,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4653
4747
|
});
|
|
4654
4748
|
const workspaceSyncAdmissionSnapshotsMatch = (left, right) => left.generation === right.generation && left.activeIncidentId === right.activeIncidentId && left.deferredIncidentId === right.deferredIncidentId;
|
|
4655
4749
|
const assertWorkspaceSyncStillAdmitted = (admitted, trigger) => {
|
|
4656
|
-
if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) ||
|
|
4750
|
+
if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, trigger)) {
|
|
4657
4751
|
throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
|
|
4658
4752
|
}
|
|
4659
4753
|
};
|
|
@@ -4707,6 +4801,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4707
4801
|
bouncePending: unsafeWorkspaceSyncBouncePending,
|
|
4708
4802
|
invalidateExecution: () => {
|
|
4709
4803
|
workspaceConfigured = false;
|
|
4804
|
+
projectRuntime.configuration = void 0;
|
|
4710
4805
|
advanceWorkerAdmissionGeneration();
|
|
4711
4806
|
},
|
|
4712
4807
|
scheduleRecoveryBounce: () => {
|
|
@@ -4817,7 +4912,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4817
4912
|
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
4818
4913
|
throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
|
|
4819
4914
|
}
|
|
4820
|
-
ws
|
|
4915
|
+
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "workspace_sync_started", attemptId }));
|
|
4821
4916
|
}
|
|
4822
4917
|
let queueMs = 0;
|
|
4823
4918
|
let prepareMs = 0;
|
|
@@ -4869,6 +4964,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4869
4964
|
};
|
|
4870
4965
|
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
4871
4966
|
pendingAutomaticTrigger = trigger;
|
|
4967
|
+
projectRuntime.pendingAutomaticTrigger = trigger;
|
|
4872
4968
|
const pendingBranchDeferral = () => (0, import_workspace_automatic_sync_policy.pendingCreatedBranchAutomaticSyncDeferral)(
|
|
4873
4969
|
projectWorkspaceState.locallyPendingCreatedBranches,
|
|
4874
4970
|
activeWorkspaceMutationTargets(),
|
|
@@ -4891,6 +4987,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
4891
4987
|
return;
|
|
4892
4988
|
}
|
|
4893
4989
|
pendingAutomaticTrigger = void 0;
|
|
4990
|
+
projectRuntime.pendingAutomaticTrigger = void 0;
|
|
4894
4991
|
automaticSyncInFlight = true;
|
|
4895
4992
|
void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
|
|
4896
4993
|
if (result.outcome === "failed") {
|
|
@@ -4914,9 +5011,14 @@ async function startWorker(options, projectRuntime = {
|
|
|
4914
5011
|
workspaceAutomaticTimer.unref();
|
|
4915
5012
|
};
|
|
4916
5013
|
const markWorkspaceDirty = (trigger, force = false, target) => {
|
|
5014
|
+
if (projectRuntime.markWorkspaceDirty && projectRuntime.markWorkspaceDirty !== markWorkspaceDirty) {
|
|
5015
|
+
projectRuntime.markWorkspaceDirty(trigger, force, target);
|
|
5016
|
+
return;
|
|
5017
|
+
}
|
|
4917
5018
|
if (target) recordVisibleWorkspaceMutation(target);
|
|
4918
5019
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
4919
5020
|
};
|
|
5021
|
+
projectRuntime.markWorkspaceDirty = markWorkspaceDirty;
|
|
4920
5022
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
4921
5023
|
const resolveMessageTarget = (target) => {
|
|
4922
5024
|
if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
|
|
@@ -5434,6 +5536,10 @@ async function startWorker(options, projectRuntime = {
|
|
|
5434
5536
|
Authorization: `Bearer ${token}`
|
|
5435
5537
|
}
|
|
5436
5538
|
});
|
|
5539
|
+
const connectionAttemptTimer = setTimeout(() => {
|
|
5540
|
+
if (ws.readyState === WebSocket.CONNECTING) ws.terminate();
|
|
5541
|
+
}, 3e4);
|
|
5542
|
+
connectionAttemptTimer.unref();
|
|
5437
5543
|
let lastServerControlMessageAtMs = performance.now();
|
|
5438
5544
|
let serverHeartbeatProbeStartedAtMs;
|
|
5439
5545
|
let lastServerHeartbeatSweepAtMs = performance.now();
|
|
@@ -5529,7 +5635,9 @@ async function startWorker(options, projectRuntime = {
|
|
|
5529
5635
|
process.on("SIGTERM", handleGracefulShutdown);
|
|
5530
5636
|
process.on("SIGINT", handleGracefulShutdown);
|
|
5531
5637
|
ws.addEventListener("open", () => {
|
|
5638
|
+
clearTimeout(connectionAttemptTimer);
|
|
5532
5639
|
currentWorkerSocket = ws;
|
|
5640
|
+
workerRecoveryId = "connecting";
|
|
5533
5641
|
noteServerControlActivity();
|
|
5534
5642
|
lastServerHeartbeatSweepAtMs = performance.now();
|
|
5535
5643
|
serverHeartbeatWatchdog = setInterval(() => {
|
|
@@ -5573,6 +5681,9 @@ async function startWorker(options, projectRuntime = {
|
|
|
5573
5681
|
});
|
|
5574
5682
|
const hello = {
|
|
5575
5683
|
type: "hello",
|
|
5684
|
+
resumableProtocol: import_recovery_protocol.WORKER_RESUMABLE_PROTOCOL,
|
|
5685
|
+
runtimeId: workerRuntimeId,
|
|
5686
|
+
ledgerId: recoveryStore.ledgerId,
|
|
5576
5687
|
startupReady: startupRecoveryComplete,
|
|
5577
5688
|
hostInfo: {
|
|
5578
5689
|
hostname: (0, import_node_os.hostname)(),
|
|
@@ -5597,15 +5708,17 @@ async function startWorker(options, projectRuntime = {
|
|
|
5597
5708
|
root: rootDir
|
|
5598
5709
|
}
|
|
5599
5710
|
};
|
|
5600
|
-
ws
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
terminalReplayTimer = setInterval(() => {
|
|
5606
|
-
for (const terminal of pendingProcessTerminals.values()) {
|
|
5607
|
-
sendWorkerMessage(ws, terminal);
|
|
5711
|
+
sendSerializedWorkerMessage(ws, JSON.stringify(hello));
|
|
5712
|
+
outputReplayTimer = setInterval(() => {
|
|
5713
|
+
if (workerRecoveryId !== void 0 || currentWorkerSocket !== ws) return;
|
|
5714
|
+
for (const output of recoveryStore.output()) {
|
|
5715
|
+
if (!sendReplayWorkerMessage(ws, output)) break;
|
|
5608
5716
|
}
|
|
5717
|
+
}, 250);
|
|
5718
|
+
outputReplayTimer.unref();
|
|
5719
|
+
terminalReplayTimer = setInterval(() => {
|
|
5720
|
+
if (workerRecoveryId !== void 0) return;
|
|
5721
|
+
for (const terminal of recoveryStore?.terminals() ?? []) sendReplayWorkerMessage(ws, terminal);
|
|
5609
5722
|
}, 5e3);
|
|
5610
5723
|
terminalReplayTimer.unref();
|
|
5611
5724
|
process.stdout.write("[r5d-worker] connected\n");
|
|
@@ -5613,18 +5726,103 @@ async function startWorker(options, projectRuntime = {
|
|
|
5613
5726
|
ws.addEventListener("message", (event) => {
|
|
5614
5727
|
noteServerControlActivity();
|
|
5615
5728
|
void (async () => {
|
|
5729
|
+
if (currentWorkerSocket !== ws) return;
|
|
5616
5730
|
const message = JSON.parse(String(event.data));
|
|
5731
|
+
if (message.type === "recovery_request") {
|
|
5732
|
+
workerRecoveryId = message.recoveryId;
|
|
5733
|
+
sendActiveProcessReport(ws);
|
|
5734
|
+
sendWorkerMessage(ws, {
|
|
5735
|
+
type: "recovery_state",
|
|
5736
|
+
recoveryId: message.recoveryId,
|
|
5737
|
+
runtimeId: workerRuntimeId,
|
|
5738
|
+
ledgerId: recoveryStore.ledgerId,
|
|
5739
|
+
operations: recoveryStore.operations()
|
|
5740
|
+
});
|
|
5741
|
+
const cursors = new Map(recoveryStore.outputCursors().map((cursor) => [cursor.runId, cursor]));
|
|
5742
|
+
for (const cursor of message.outputOffsets) cursors.set(cursor.runId, cursor);
|
|
5743
|
+
workerOutputWindow.reset([...cursors.values()]);
|
|
5744
|
+
for (const response of recoveryStore.responses(message.requests)) sendReplayWorkerMessage(ws, response);
|
|
5745
|
+
for (const output of recoveryStore.output(message.outputOffsets)) {
|
|
5746
|
+
while (true) {
|
|
5747
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId || ws.readyState !== WebSocket.OPEN || workerLeaseExpired)
|
|
5748
|
+
return;
|
|
5749
|
+
if (sendReplayWorkerMessage(ws, output)) break;
|
|
5750
|
+
await Bun.sleep(10);
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
if (currentWorkerSocket !== ws || workerRecoveryId !== message.recoveryId) return;
|
|
5754
|
+
for (const terminal of recoveryStore.terminals()) sendReplayWorkerMessage(ws, terminal);
|
|
5755
|
+
sendWorkerMessage(ws, { type: "recovery_complete", recoveryId: message.recoveryId });
|
|
5756
|
+
return;
|
|
5757
|
+
}
|
|
5758
|
+
if (message.type === "recovery_ready") {
|
|
5759
|
+
if (workerRecoveryId !== message.recoveryId) return;
|
|
5760
|
+
workerRecoveryId = void 0;
|
|
5761
|
+
workerReconnectAttempt = 0;
|
|
5762
|
+
renewWorkerExecutionLease(message.leaseMs);
|
|
5763
|
+
return;
|
|
5764
|
+
}
|
|
5765
|
+
if (message.type === "worker_execution_lease") {
|
|
5766
|
+
renewWorkerExecutionLease(message.leaseMs);
|
|
5767
|
+
return;
|
|
5768
|
+
}
|
|
5769
|
+
if (message.type === "operation_result_ack") {
|
|
5770
|
+
recoveryStore.acknowledgeResult(message.requestId);
|
|
5771
|
+
return;
|
|
5772
|
+
}
|
|
5773
|
+
if (message.type === "exec_output_ack") {
|
|
5774
|
+
recoveryStore.acknowledgeOutput(message.runId, message.stream, message.nextOffset);
|
|
5775
|
+
workerOutputWindow.acknowledge(message.runId, message.stream, message.nextOffset);
|
|
5776
|
+
return;
|
|
5777
|
+
}
|
|
5778
|
+
if (message.type === "cancel_session") {
|
|
5779
|
+
recoveryStore.cancelSession(message.sessionId);
|
|
5780
|
+
await Promise.all(
|
|
5781
|
+
[...activeProcesses].filter(([, active]) => active.sessionId === message.sessionId).map(async ([runId, active]) => {
|
|
5782
|
+
cancelledProcessRuns.add(runId);
|
|
5783
|
+
closeProcessStdin(active);
|
|
5784
|
+
await (0, import_process_tree.terminateProcessTree)(active.process);
|
|
5785
|
+
})
|
|
5786
|
+
);
|
|
5787
|
+
sendWorkerMessage(ws, { type: "cancel_session_ack", cancellationId: message.cancellationId });
|
|
5788
|
+
return;
|
|
5789
|
+
}
|
|
5617
5790
|
if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
|
|
5618
5791
|
if (!startupRecoveryComplete && message.type !== "ping") return;
|
|
5792
|
+
if (currentWorkerSocket !== ws) return;
|
|
5793
|
+
const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
|
|
5794
|
+
if (operationRequestId) {
|
|
5795
|
+
const admission = recoveryStore.admit({ ...message, requestId: operationRequestId });
|
|
5796
|
+
if (admission === "conflict") {
|
|
5797
|
+
ws.close(1008, "Operation identity conflict");
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
sendWorkerMessage(ws, { type: "operation_received", requestId: operationRequestId });
|
|
5801
|
+
if (admission !== "new") {
|
|
5802
|
+
const response = recoveryStore.response(operationRequestId);
|
|
5803
|
+
if (response && admission !== "unknown") sendReplayWorkerMessage(ws, response);
|
|
5804
|
+
return;
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5619
5807
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
5620
|
-
const
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5808
|
+
const admissionCredentials = configuredCredentialGenerationFingerprint;
|
|
5809
|
+
const assertMessageAdmission = () => {
|
|
5810
|
+
if (operationRequestId) {
|
|
5811
|
+
if (workerLeaseExpired || recoveryStore.isRequestCancelled(operationRequestId) || admissionCredentials !== configuredCredentialGenerationFingerprint || shutdownAfterClose || !workspaceConfigured) {
|
|
5812
|
+
recoveryStore.unknown(operationRequestId);
|
|
5813
|
+
throw new StaleWorkerAdmissionError();
|
|
5814
|
+
}
|
|
5815
|
+
return;
|
|
5816
|
+
}
|
|
5817
|
+
assertWorkerChildAdmission({
|
|
5818
|
+
capturedGeneration: messageAdmissionGeneration,
|
|
5819
|
+
currentGeneration: workerAdmissionGeneration,
|
|
5820
|
+
sourceSocketIsCurrent: currentWorkerSocket === ws,
|
|
5821
|
+
sourceSocketOpen: ws.readyState === WebSocket.OPEN,
|
|
5822
|
+
workspaceConfigured,
|
|
5823
|
+
runtimeHealthy: !shutdownAfterClose
|
|
5824
|
+
});
|
|
5825
|
+
};
|
|
5628
5826
|
if (message.type === "connected") {
|
|
5629
5827
|
return;
|
|
5630
5828
|
}
|
|
@@ -5640,11 +5838,15 @@ async function startWorker(options, projectRuntime = {
|
|
|
5640
5838
|
if (message.type === "workspace_config") {
|
|
5641
5839
|
const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
|
|
5642
5840
|
const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
|
|
5643
|
-
|
|
5644
|
-
const
|
|
5645
|
-
|
|
5646
|
-
if (
|
|
5647
|
-
|
|
5841
|
+
const digest = (0, import_recovery_store.workerOperationDigest)(message);
|
|
5842
|
+
const cached = projectRuntime.configuration;
|
|
5843
|
+
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;
|
|
5844
|
+
if (!canReuseConfiguration) {
|
|
5845
|
+
advanceWorkerAdmissionGeneration();
|
|
5846
|
+
projectRuntime.configuration = void 0;
|
|
5847
|
+
}
|
|
5848
|
+
const configured = canReuseConfiguration ? cached.configured : await configureWorkerWorkspace(message, receiptGeneration);
|
|
5849
|
+
const sendConfigured = () => sendWorkerMessageFromCurrentSource(ws, {
|
|
5648
5850
|
type: "workspace_configured",
|
|
5649
5851
|
requestId: message.requestId,
|
|
5650
5852
|
result: configured.result,
|
|
@@ -5652,6 +5854,34 @@ async function startWorker(options, projectRuntime = {
|
|
|
5652
5854
|
aheadOfOriginBranches: configured.aheadOfOriginBranches,
|
|
5653
5855
|
...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
|
|
5654
5856
|
});
|
|
5857
|
+
if (receiptGeneration !== workspaceConfigurationReceiptGeneration || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
5858
|
+
sendConfigured();
|
|
5859
|
+
return;
|
|
5860
|
+
}
|
|
5861
|
+
if (canReuseConfiguration && configured.result.outcome !== "failed") {
|
|
5862
|
+
workspaceRemoteUrl = cached.workspaceSync.remoteUrl;
|
|
5863
|
+
workspaceCredentialHelper = cached.workspaceSync.credentialHelper;
|
|
5864
|
+
workspaceCredentialUsername = cached.workspaceSync.credentialUsername;
|
|
5865
|
+
workspaceGitIdentity = cached.workspaceSync.gitIdentity;
|
|
5866
|
+
workspaceConfigured = true;
|
|
5867
|
+
activeWorkspaceIncidentId = cached.incidentId;
|
|
5868
|
+
deferredWorkspaceConfiguration = cached.deferred;
|
|
5869
|
+
}
|
|
5870
|
+
projectRuntime.configuration = configured.result.outcome !== "failed" && workspaceRemoteUrl && workspaceCredentialHelper && workspaceCredentialUsername && workspaceGitIdentity ? {
|
|
5871
|
+
digest,
|
|
5872
|
+
workspaceSync: {
|
|
5873
|
+
remoteUrl: workspaceRemoteUrl,
|
|
5874
|
+
credentialHelper: workspaceCredentialHelper,
|
|
5875
|
+
credentialUsername: workspaceCredentialUsername,
|
|
5876
|
+
gitIdentity: workspaceGitIdentity
|
|
5877
|
+
},
|
|
5878
|
+
configured,
|
|
5879
|
+
incidentId: activeWorkspaceIncidentId,
|
|
5880
|
+
deferred: deferredWorkspaceConfiguration
|
|
5881
|
+
} : void 0;
|
|
5882
|
+
unsafeWorkspaceSyncBouncePending = false;
|
|
5883
|
+
if (configured.result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
|
|
5884
|
+
sendConfigured();
|
|
5655
5885
|
sendLifecycleProgress(
|
|
5656
5886
|
configured.result.outcome === "failed" ? {
|
|
5657
5887
|
phase: "failed",
|
|
@@ -5687,16 +5917,31 @@ async function startWorker(options, projectRuntime = {
|
|
|
5687
5917
|
return;
|
|
5688
5918
|
}
|
|
5689
5919
|
if (message.type === "sync_workspace") {
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5920
|
+
let retry = 0;
|
|
5921
|
+
let recoveryDeadline;
|
|
5922
|
+
while (true) {
|
|
5923
|
+
const result = await runWorkspaceSync({
|
|
5924
|
+
requestId: message.requestId,
|
|
5925
|
+
attemptId: message.attemptId,
|
|
5926
|
+
trigger: message.trigger,
|
|
5927
|
+
confirmedLargeDiff: message.confirmedLargeDiff,
|
|
5928
|
+
confirmationReason: message.confirmationReason,
|
|
5929
|
+
resetToCanonical: message.resetToCanonical,
|
|
5930
|
+
requiredAncestorHeads: message.requiredAncestorHeads,
|
|
5931
|
+
sendResult: false
|
|
5932
|
+
});
|
|
5933
|
+
if (result.outcome !== "failed" || !isWorkerCommunicationFailure(result.error ?? "")) {
|
|
5934
|
+
sendWorkspaceSyncResult(message.requestId, result);
|
|
5935
|
+
return;
|
|
5936
|
+
}
|
|
5937
|
+
recoveryDeadline ??= Date.now() + import_recovery_protocol.WORKER_RECOVERY_TIMEOUT_MS;
|
|
5938
|
+
if (workerLeaseExpired || Date.now() >= recoveryDeadline) {
|
|
5939
|
+
recoveryStore.unknown(message.requestId);
|
|
5940
|
+
return;
|
|
5941
|
+
}
|
|
5942
|
+
currentWorkerSocket?.close(1012, "Communication recovery");
|
|
5943
|
+
await Bun.sleep((0, import_supervisor.workerReconnectDelayMs)(Math.random, retry++));
|
|
5944
|
+
}
|
|
5700
5945
|
}
|
|
5701
5946
|
if (message.type === "create_project_branch") {
|
|
5702
5947
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
@@ -5962,6 +6207,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5962
6207
|
}
|
|
5963
6208
|
if (message.type === "exec_terminal_ack") {
|
|
5964
6209
|
pendingProcessTerminals.delete(message.runId);
|
|
6210
|
+
recoveryStore.acknowledgeTerminal(message.runId);
|
|
5965
6211
|
return;
|
|
5966
6212
|
}
|
|
5967
6213
|
if (message.type === "port_forward_connect") {
|
|
@@ -6013,7 +6259,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6013
6259
|
return;
|
|
6014
6260
|
}
|
|
6015
6261
|
if (message.type === "ping") {
|
|
6016
|
-
ws
|
|
6262
|
+
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
|
|
6017
6263
|
return;
|
|
6018
6264
|
}
|
|
6019
6265
|
if (message.type === "cancel") {
|
|
@@ -6033,7 +6279,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6033
6279
|
cancelledProcessRuns.add(message.runId);
|
|
6034
6280
|
cancelMessage = "Cancellation queued before command start";
|
|
6035
6281
|
}
|
|
6036
|
-
|
|
6282
|
+
sendSerializedWorkerMessage(
|
|
6283
|
+
ws,
|
|
6037
6284
|
JSON.stringify({
|
|
6038
6285
|
type: "cancel_result",
|
|
6039
6286
|
requestId: message.requestId,
|
|
@@ -6045,7 +6292,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6045
6292
|
return;
|
|
6046
6293
|
}
|
|
6047
6294
|
if (message.type === "exec_stdin") {
|
|
6048
|
-
const sendAck = (payload) =>
|
|
6295
|
+
const sendAck = (payload) => sendSerializedWorkerMessage(
|
|
6296
|
+
ws,
|
|
6049
6297
|
JSON.stringify({
|
|
6050
6298
|
type: "operation_result",
|
|
6051
6299
|
requestId: message.requestId,
|
|
@@ -6168,16 +6416,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
6168
6416
|
ptyId: message.ptyId,
|
|
6169
6417
|
error: `Shell input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
|
|
6170
6418
|
});
|
|
6419
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6171
6420
|
return;
|
|
6172
6421
|
}
|
|
6173
6422
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6174
6423
|
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
|
|
6175
6424
|
}
|
|
6176
6425
|
writePty(ws, message);
|
|
6426
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6177
6427
|
return;
|
|
6178
6428
|
}
|
|
6179
6429
|
if (message.type === "pty_resize") {
|
|
6180
6430
|
resizePty(message);
|
|
6431
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6181
6432
|
return;
|
|
6182
6433
|
}
|
|
6183
6434
|
if (message.type === "pty_close") {
|
|
@@ -6186,6 +6437,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6186
6437
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6187
6438
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
6188
6439
|
}
|
|
6440
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6189
6441
|
return;
|
|
6190
6442
|
}
|
|
6191
6443
|
if (message.type === "exec") {
|
|
@@ -6267,7 +6519,11 @@ async function startWorker(options, projectRuntime = {
|
|
|
6267
6519
|
} finally {
|
|
6268
6520
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6269
6521
|
}
|
|
6270
|
-
|
|
6522
|
+
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6523
|
+
recoveryStore.unknown(message.requestId);
|
|
6524
|
+
return;
|
|
6525
|
+
}
|
|
6526
|
+
sendSerializedWorkerMessage(ws, JSON.stringify(result));
|
|
6271
6527
|
if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
|
|
6272
6528
|
markWorkspaceDirty(
|
|
6273
6529
|
{ type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
|
|
@@ -6390,7 +6646,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6390
6646
|
assertAdmission: assertMessageAdmission
|
|
6391
6647
|
});
|
|
6392
6648
|
});
|
|
6393
|
-
|
|
6649
|
+
sendSerializedWorkerMessage(
|
|
6650
|
+
ws,
|
|
6394
6651
|
JSON.stringify({
|
|
6395
6652
|
type: "operation_result",
|
|
6396
6653
|
requestId: message.requestId,
|
|
@@ -6407,7 +6664,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6407
6664
|
markWorkspaceDirty(dirtyTrigger, false, message.target);
|
|
6408
6665
|
}
|
|
6409
6666
|
} catch (error) {
|
|
6410
|
-
|
|
6667
|
+
sendSerializedWorkerMessage(
|
|
6668
|
+
ws,
|
|
6411
6669
|
JSON.stringify({
|
|
6412
6670
|
type: "operation_result",
|
|
6413
6671
|
requestId: message.requestId,
|
|
@@ -6426,7 +6684,8 @@ async function startWorker(options, projectRuntime = {
|
|
|
6426
6684
|
}
|
|
6427
6685
|
const unhandledMessage = message;
|
|
6428
6686
|
if (typeof unhandledMessage.requestId === "string") {
|
|
6429
|
-
|
|
6687
|
+
sendSerializedWorkerMessage(
|
|
6688
|
+
ws,
|
|
6430
6689
|
JSON.stringify({
|
|
6431
6690
|
type: "operation_result",
|
|
6432
6691
|
requestId: unhandledMessage.requestId,
|
|
@@ -6440,6 +6699,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
6440
6699
|
});
|
|
6441
6700
|
});
|
|
6442
6701
|
ws.addEventListener("close", (event) => {
|
|
6702
|
+
clearTimeout(connectionAttemptTimer);
|
|
6443
6703
|
const processEvents = process;
|
|
6444
6704
|
processEvents.off("SIGTERM", handleGracefulShutdown);
|
|
6445
6705
|
processEvents.off("SIGINT", handleGracefulShutdown);
|
|
@@ -6456,15 +6716,20 @@ async function startWorker(options, projectRuntime = {
|
|
|
6456
6716
|
clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
|
|
6457
6717
|
deferredWorkspaceConfigurationRefreshTimer = void 0;
|
|
6458
6718
|
}
|
|
6719
|
+
if (outputReplayTimer) {
|
|
6720
|
+
clearInterval(outputReplayTimer);
|
|
6721
|
+
outputReplayTimer = void 0;
|
|
6722
|
+
}
|
|
6459
6723
|
if (terminalReplayTimer) {
|
|
6460
6724
|
clearInterval(terminalReplayTimer);
|
|
6461
6725
|
terminalReplayTimer = void 0;
|
|
6462
6726
|
}
|
|
6463
|
-
if (currentWorkerSocket === ws) {
|
|
6464
|
-
|
|
6465
|
-
|
|
6727
|
+
if (currentWorkerSocket === ws && projectRuntime.configuration) {
|
|
6728
|
+
projectRuntime.configuration.incidentId = activeWorkspaceIncidentId;
|
|
6729
|
+
projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
|
|
6466
6730
|
}
|
|
6467
|
-
|
|
6731
|
+
if (currentWorkerSocket === ws) currentWorkerSocket = null;
|
|
6732
|
+
workerRecoveryId = "reconnecting";
|
|
6468
6733
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6469
6734
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
6470
6735
|
`);
|
|
@@ -6474,50 +6739,19 @@ async function startWorker(options, projectRuntime = {
|
|
|
6474
6739
|
if (shutdownAfterClose) {
|
|
6475
6740
|
process.exit(0);
|
|
6476
6741
|
}
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
if (!startupRecoveryComplete) {
|
|
6481
|
-
process.exit(1);
|
|
6482
|
-
return;
|
|
6483
|
-
}
|
|
6484
|
-
const reconnectDelayMs = (0, import_supervisor.workerReconnectDelayMs)();
|
|
6485
|
-
process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
|
|
6742
|
+
const reconnect = () => {
|
|
6743
|
+
const delay = (0, import_supervisor.workerReconnectDelayMs)(Math.random, workerReconnectAttempt++);
|
|
6744
|
+
process.stderr.write(`[r5d-worker] reconnecting in ${delay}ms
|
|
6486
6745
|
`);
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6490
|
-
`);
|
|
6491
|
-
process.exit(error instanceof WorkerServerUnavailableError ? import_supervisor.WORKER_RECONNECT_EXIT_CODE : 1);
|
|
6492
|
-
});
|
|
6493
|
-
}, reconnectDelayMs);
|
|
6494
|
-
});
|
|
6495
|
-
return;
|
|
6496
|
-
}
|
|
6497
|
-
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
6498
|
-
const scheduleReconnect = (reconnect2) => {
|
|
6499
|
-
const reconnectDelayMs = (0, import_supervisor.workerReconnectDelayMs)();
|
|
6500
|
-
process.stderr.write(
|
|
6501
|
-
`[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
|
|
6502
|
-
`
|
|
6503
|
-
);
|
|
6504
|
-
setTimeout(reconnect2, reconnectDelayMs);
|
|
6505
|
-
};
|
|
6506
|
-
const reconnect = () => {
|
|
6507
|
-
void startWorker(options, projectRuntime).catch((error) => {
|
|
6746
|
+
setTimeout(() => {
|
|
6747
|
+
void startupRecoveryPromise.then(() => startWorker(options, projectRuntime)).catch((error) => {
|
|
6508
6748
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6509
6749
|
`);
|
|
6510
|
-
|
|
6511
|
-
scheduleReconnect(reconnect);
|
|
6512
|
-
return;
|
|
6513
|
-
}
|
|
6514
|
-
process.exit(error instanceof WorkerServerUnavailableError ? import_supervisor.WORKER_RECONNECT_EXIT_CODE : 1);
|
|
6750
|
+
reconnect();
|
|
6515
6751
|
});
|
|
6516
|
-
};
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
}
|
|
6520
|
-
process.exit(import_supervisor.WORKER_RECONNECT_EXIT_CODE);
|
|
6752
|
+
}, delay);
|
|
6753
|
+
};
|
|
6754
|
+
reconnect();
|
|
6521
6755
|
});
|
|
6522
6756
|
ws.addEventListener("error", (event) => {
|
|
6523
6757
|
const message = "message" in event && typeof event.message === "string" && event.message.trim() ? `: ${event.message.trim()}` : "";
|