@ricsam/r5d-worker 0.0.107 → 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/README.md +1 -1
- package/dist/cjs/main.cjs +431 -145
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/plan-lint.cjs +165 -0
- package/dist/cjs/plan-parser.cjs +127 -0
- package/dist/cjs/plan-paths.cjs +53 -0
- 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 +433 -144
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/plan-lint.mjs +129 -0
- package/dist/mjs/plan-parser.mjs +94 -0
- package/dist/mjs/plan-paths.mjs +25 -0
- 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 +49 -3
- package/dist/types/plan-lint.d.ts +16 -0
- package/dist/types/plan-parser.d.ts +38 -0
- package/dist/types/plan-paths.d.ts +36 -0
- 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 +3 -2
package/dist/mjs/main.mjs
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { getWorkerPlanPath, getWorkerPlansPath, validatePlanId } from "./plan-paths.mjs";
|
|
3
|
+
import { createPlanMarkdown, updateTaskCheckbox } from "./plan-parser.mjs";
|
|
4
|
+
import { lintPlans, formatPlanLintErrors } from "./plan-lint.mjs";
|
|
2
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";
|
|
3
12
|
import path from "node:path";
|
|
4
13
|
import { createHash } from "node:crypto";
|
|
5
14
|
import os, { hostname } from "node:os";
|
|
@@ -169,6 +178,63 @@ const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
|
169
178
|
const activePtys = /* @__PURE__ */ new Map();
|
|
170
179
|
const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
|
|
171
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
|
+
}
|
|
172
238
|
let workerAdmissionGeneration = 0;
|
|
173
239
|
const workspaceMutationGate = new WorkspaceMutationGate();
|
|
174
240
|
let workspaceSyncQueue = Promise.resolve();
|
|
@@ -627,40 +693,46 @@ function parseRemoteFileManifest(value, collectionKey, label) {
|
|
|
627
693
|
});
|
|
628
694
|
}
|
|
629
695
|
async function fetchRemoteFileManifest(input) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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}` : ""}`);
|
|
633
706
|
}
|
|
707
|
+
return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
|
|
634
708
|
});
|
|
635
|
-
if (!response.ok) {
|
|
636
|
-
const detail = (await readResponseText(response)).trim();
|
|
637
|
-
throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
638
|
-
}
|
|
639
|
-
return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
|
|
640
709
|
}
|
|
641
710
|
async function downloadRemoteSyncedFile(input) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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`);
|
|
728
|
+
}
|
|
729
|
+
if (buffer.length !== input.file.size) {
|
|
730
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
|
|
647
731
|
}
|
|
732
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
733
|
+
fs.writeFileSync(tempPath, buffer);
|
|
734
|
+
fs.renameSync(tempPath, targetPath);
|
|
648
735
|
});
|
|
649
|
-
if (!response.ok) {
|
|
650
|
-
const detail = (await readResponseText(response)).trim();
|
|
651
|
-
throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
652
|
-
}
|
|
653
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
654
|
-
const receivedHash = createHash("sha256").update(buffer).digest("hex");
|
|
655
|
-
if (receivedHash !== input.file.sha256) {
|
|
656
|
-
throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
|
|
657
|
-
}
|
|
658
|
-
if (buffer.length !== input.file.size) {
|
|
659
|
-
throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
|
|
660
|
-
}
|
|
661
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
662
|
-
fs.writeFileSync(tempPath, buffer);
|
|
663
|
-
fs.renameSync(tempPath, targetPath);
|
|
664
736
|
}
|
|
665
737
|
async function syncRemoteFileSet(input) {
|
|
666
738
|
fs.mkdirSync(input.targetDir, { recursive: true });
|
|
@@ -787,6 +859,7 @@ async function verifyR5dctlAuth(baseUrl, token, workerLabel) {
|
|
|
787
859
|
let response;
|
|
788
860
|
try {
|
|
789
861
|
response = await fetch(statusUrl, {
|
|
862
|
+
signal: AbortSignal.timeout(3e4),
|
|
790
863
|
headers: {
|
|
791
864
|
Authorization: `Bearer ${token}`
|
|
792
865
|
}
|
|
@@ -845,11 +918,6 @@ function validateProjectId(projectId) {
|
|
|
845
918
|
function validateBranchName(branchName) {
|
|
846
919
|
validateManagedBranchName(branchName);
|
|
847
920
|
}
|
|
848
|
-
function validatePlanId(planId) {
|
|
849
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
|
|
850
|
-
throw new Error(`Invalid plan id from server: ${planId}`);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
921
|
const NON_RECURSIVE_GIT_CONFIG_ARGS = [
|
|
854
922
|
"-c",
|
|
855
923
|
"submodule.recurse=false",
|
|
@@ -1756,6 +1824,7 @@ const workerGitSecurityTestHarness = {
|
|
|
1756
1824
|
workspaceSyncFailureHydrationIsSafe,
|
|
1757
1825
|
assertWorkerChildAdmission,
|
|
1758
1826
|
executeWriteFileOperation,
|
|
1827
|
+
executeOperation,
|
|
1759
1828
|
executeEditFileOperation,
|
|
1760
1829
|
terminateCredentialBearingChildren,
|
|
1761
1830
|
terminateCredentialBearingChildrenWithRetention,
|
|
@@ -2194,6 +2263,55 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
|
2194
2263
|
edits: edits.length
|
|
2195
2264
|
};
|
|
2196
2265
|
}
|
|
2266
|
+
async function executePlanOperation(input) {
|
|
2267
|
+
const { message, resolvedTarget } = input;
|
|
2268
|
+
const filePath = message.type === "lint_plans" ? getWorkerPlansPath(resolvedTarget.target) : getWorkerPlanPath(resolvedTarget.target, message.planId);
|
|
2269
|
+
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
2270
|
+
target: resolvedTarget.target,
|
|
2271
|
+
filePath,
|
|
2272
|
+
baseUrl: input.baseUrl,
|
|
2273
|
+
token: input.token,
|
|
2274
|
+
rootDir: input.rootDir,
|
|
2275
|
+
sessionId: message.sessionId,
|
|
2276
|
+
access: message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing ? "write" : "read"
|
|
2277
|
+
});
|
|
2278
|
+
const resolved = resolveWorkerFilePath(resolvedTarget.rootPath, filePath, builtInPaths);
|
|
2279
|
+
if (message.type === "lint_plans") {
|
|
2280
|
+
const result = await lintPlans(resolved.absolutePath, message.glob);
|
|
2281
|
+
return {
|
|
2282
|
+
type: "lint_plans",
|
|
2283
|
+
content: result.lintedPlanCount === 0 ? message.glob ? `No plan files matched glob "${message.glob}".` : "No plan files found." : formatPlanLintErrors(result.errors) || "No plan lint issues found."
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
return withFileMutationQueue(mutationQueueKey(resolvedTarget.target, resolved), async () => {
|
|
2287
|
+
input.assertAdmission();
|
|
2288
|
+
if (message.type === "read_plan") {
|
|
2289
|
+
let created = false;
|
|
2290
|
+
if (message.createIfMissing) {
|
|
2291
|
+
try {
|
|
2292
|
+
fs.writeFileSync(resolved.absolutePath, createPlanMarkdown(message.planId), { encoding: "utf8", flag: "wx" });
|
|
2293
|
+
created = true;
|
|
2294
|
+
} catch (error) {
|
|
2295
|
+
if (error.code !== "EEXIST") throw error;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
type: "read_plan",
|
|
2300
|
+
created,
|
|
2301
|
+
read: readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
const content = fs.readFileSync(resolved.absolutePath, "utf8");
|
|
2305
|
+
writeWorkerTextFile(resolvedTarget.rootPath, filePath, updateTaskCheckbox(content, message.taskId, message.checked), builtInPaths);
|
|
2306
|
+
return {
|
|
2307
|
+
type: "update_task_status",
|
|
2308
|
+
planId: message.planId,
|
|
2309
|
+
taskId: message.taskId,
|
|
2310
|
+
checked: message.checked,
|
|
2311
|
+
message: `Task ${message.taskId} marked ${message.checked ? "complete" : "incomplete"}.`
|
|
2312
|
+
};
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2197
2315
|
async function executeReadFileOperation(input) {
|
|
2198
2316
|
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
2199
2317
|
target: input.resolvedTarget.target,
|
|
@@ -2572,6 +2690,10 @@ async function executeViewFileBytesOperation(input) {
|
|
|
2572
2690
|
}
|
|
2573
2691
|
async function executeOperation(input) {
|
|
2574
2692
|
switch (input.message.type) {
|
|
2693
|
+
case "read_plan":
|
|
2694
|
+
case "lint_plans":
|
|
2695
|
+
case "update_task_status":
|
|
2696
|
+
return executePlanOperation({ ...input, message: input.message });
|
|
2575
2697
|
case "read":
|
|
2576
2698
|
return executeReadFileOperation({ ...input, message: input.message });
|
|
2577
2699
|
case "write":
|
|
@@ -2901,23 +3023,46 @@ function closeProcessStdin(active) {
|
|
|
2901
3023
|
}
|
|
2902
3024
|
active.stdin = void 0;
|
|
2903
3025
|
}
|
|
2904
|
-
function
|
|
3026
|
+
function sendReplayWorkerMessage(ws, message) {
|
|
2905
3027
|
const target = currentWorkerSocket ?? ws;
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
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;
|
|
2911
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));
|
|
2912
3058
|
}
|
|
2913
3059
|
function sendWorkerMessageFromCurrentSource(ws, message) {
|
|
2914
|
-
if (
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
} catch (error) {
|
|
2918
|
-
process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
|
|
2919
|
-
`);
|
|
3060
|
+
if ("requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_configured") {
|
|
3061
|
+
sendWorkerMessage(ws, message);
|
|
3062
|
+
return;
|
|
2920
3063
|
}
|
|
3064
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
3065
|
+
sendWorkerMessage(ws, message);
|
|
2921
3066
|
}
|
|
2922
3067
|
function buildActiveProcessReports() {
|
|
2923
3068
|
return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
|
|
@@ -3431,14 +3576,21 @@ function websocketUrl(baseUrl, label) {
|
|
|
3431
3576
|
url.searchParams.set("version", getWorkerVersion());
|
|
3432
3577
|
return url.toString();
|
|
3433
3578
|
}
|
|
3434
|
-
async function startWorker(options
|
|
3435
|
-
|
|
3579
|
+
async function startWorker(options, projectRuntime = {
|
|
3580
|
+
projectConfigById: /* @__PURE__ */ new Map(),
|
|
3581
|
+
readyProjectIds: /* @__PURE__ */ new Set(),
|
|
3582
|
+
reconciledProjectConfigFingerprints: /* @__PURE__ */ new Map(),
|
|
3583
|
+
gitIdentity: null
|
|
3584
|
+
}) {
|
|
3436
3585
|
const config = readConfig(options.configPath);
|
|
3437
3586
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
3438
3587
|
initializeCredentialStoreDirectoryOnce();
|
|
3439
3588
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
3440
3589
|
validateLabel(label);
|
|
3441
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
|
+
);
|
|
3442
3594
|
const projectsRoot = path.join(rootDir, "projects");
|
|
3443
3595
|
const syncRoot = path.join(rootDir, "sync");
|
|
3444
3596
|
const workspaceShadowRoot = path.join(syncRoot, "workspace");
|
|
@@ -3459,7 +3611,7 @@ async function startWorker(options) {
|
|
|
3459
3611
|
`);
|
|
3460
3612
|
runGit(["--version"]);
|
|
3461
3613
|
await verifyR5dctlAuth(baseUrl, token, label);
|
|
3462
|
-
const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3614
|
+
const initializedWorkspaceState = projectRuntime.initializedWorkspaceState ?? await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3463
3615
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
3464
3616
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
3465
3617
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
@@ -3470,12 +3622,11 @@ async function startWorker(options) {
|
|
|
3470
3622
|
});
|
|
3471
3623
|
return { store, state: store.read() };
|
|
3472
3624
|
});
|
|
3625
|
+
projectRuntime.initializedWorkspaceState = initializedWorkspaceState;
|
|
3473
3626
|
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3474
|
-
let projectWorkspaceState = initializedWorkspaceState.
|
|
3475
|
-
const projectConfigById
|
|
3627
|
+
let projectWorkspaceState = initializedWorkspaceState.store.read();
|
|
3628
|
+
const { projectConfigById, readyProjectIds, reconciledProjectConfigFingerprints } = projectRuntime;
|
|
3476
3629
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
3477
|
-
const readyProjectIds = /* @__PURE__ */ new Set();
|
|
3478
|
-
const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
|
|
3479
3630
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
3480
3631
|
const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3481
3632
|
const advertisedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
|
|
@@ -3505,7 +3656,7 @@ async function startWorker(options) {
|
|
|
3505
3656
|
let workspaceRemoteUrl = null;
|
|
3506
3657
|
let workspaceCredentialHelper = null;
|
|
3507
3658
|
let workspaceCredentialUsername = null;
|
|
3508
|
-
let workspaceGitIdentity =
|
|
3659
|
+
let workspaceGitIdentity = projectRuntime.gitIdentity;
|
|
3509
3660
|
let workspaceConfigured = false;
|
|
3510
3661
|
let activeWorkspaceIncidentId = null;
|
|
3511
3662
|
let deferredWorkspaceConfiguration = null;
|
|
@@ -3513,11 +3664,12 @@ async function startWorker(options) {
|
|
|
3513
3664
|
let workspaceConfigurationReceiptGeneration = 0;
|
|
3514
3665
|
let workspaceAutomaticTimer;
|
|
3515
3666
|
let workspacePeriodicTimer;
|
|
3516
|
-
let pendingAutomaticTrigger;
|
|
3667
|
+
let pendingAutomaticTrigger = projectRuntime.pendingAutomaticTrigger;
|
|
3517
3668
|
const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
|
|
3518
3669
|
const workspaceProjectionLedger = new WorkspaceProjectionLedger();
|
|
3519
3670
|
let automaticSyncInFlight = false;
|
|
3520
3671
|
let terminalReplayTimer;
|
|
3672
|
+
let outputReplayTimer;
|
|
3521
3673
|
let workspaceSyncRequestsInFlight = 0;
|
|
3522
3674
|
let lastWorkspaceSyncMirrorObservationMs = 0;
|
|
3523
3675
|
let lastInventoriedMissingCheckouts;
|
|
@@ -3866,7 +4018,9 @@ async function startWorker(options) {
|
|
|
3866
4018
|
const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
|
|
3867
4019
|
const logMissingCheckouts = (inventory) => {
|
|
3868
4020
|
if (inventory.missingCheckouts.length === 0) return;
|
|
3869
|
-
const described = inventory.missingCheckouts.map(
|
|
4021
|
+
const described = inventory.missingCheckouts.map(
|
|
4022
|
+
({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`
|
|
4023
|
+
).join(", ");
|
|
3870
4024
|
process.stderr.write(
|
|
3871
4025
|
`[r5d-worker] skipping ${inventory.missingCheckouts.length} configured project checkout(s) this cycle: ${described}; delete the branch through the platform or reconnect this worker to recreate the checkout
|
|
3872
4026
|
`
|
|
@@ -4437,7 +4591,9 @@ async function startWorker(options) {
|
|
|
4437
4591
|
error: error instanceof Error ? error.message : String(error)
|
|
4438
4592
|
};
|
|
4439
4593
|
};
|
|
4440
|
-
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4594
|
+
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4595
|
+
(message) => sendWorkerMessageFromCurrentSource(ws, message)
|
|
4596
|
+
);
|
|
4441
4597
|
const performWorkspaceSync = async (input) => {
|
|
4442
4598
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4443
4599
|
throw new Error("Worker workspace configuration has not been received");
|
|
@@ -4494,7 +4650,11 @@ async function startWorker(options) {
|
|
|
4494
4650
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4495
4651
|
const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
|
|
4496
4652
|
if (outerRemediation) workspaceProjectionLedger.reset();
|
|
4497
|
-
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({
|
|
4653
|
+
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({
|
|
4654
|
+
allowNonFastForward: false,
|
|
4655
|
+
failClosed: false,
|
|
4656
|
+
projects: checkoutInventory.projects
|
|
4657
|
+
});
|
|
4498
4658
|
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4499
4659
|
for (const bundle of observedProjectHeads) {
|
|
4500
4660
|
for (const observation of bundle.observations) {
|
|
@@ -4601,7 +4761,7 @@ async function startWorker(options) {
|
|
|
4601
4761
|
});
|
|
4602
4762
|
const workspaceSyncAdmissionSnapshotsMatch = (left, right) => left.generation === right.generation && left.activeIncidentId === right.activeIncidentId && left.deferredIncidentId === right.deferredIncidentId;
|
|
4603
4763
|
const assertWorkspaceSyncStillAdmitted = (admitted, trigger) => {
|
|
4604
|
-
if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) ||
|
|
4764
|
+
if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, trigger)) {
|
|
4605
4765
|
throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
|
|
4606
4766
|
}
|
|
4607
4767
|
};
|
|
@@ -4655,6 +4815,7 @@ async function startWorker(options) {
|
|
|
4655
4815
|
bouncePending: unsafeWorkspaceSyncBouncePending,
|
|
4656
4816
|
invalidateExecution: () => {
|
|
4657
4817
|
workspaceConfigured = false;
|
|
4818
|
+
projectRuntime.configuration = void 0;
|
|
4658
4819
|
advanceWorkerAdmissionGeneration();
|
|
4659
4820
|
},
|
|
4660
4821
|
scheduleRecoveryBounce: () => {
|
|
@@ -4683,7 +4844,11 @@ async function startWorker(options) {
|
|
|
4683
4844
|
};
|
|
4684
4845
|
}
|
|
4685
4846
|
};
|
|
4686
|
-
const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4847
|
+
const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4848
|
+
"remediation",
|
|
4849
|
+
"remediation_confirm",
|
|
4850
|
+
"remediation_reset"
|
|
4851
|
+
]);
|
|
4687
4852
|
const isOrdinaryWorkspaceSyncRequest = (input) => input.confirmedLargeDiff !== true && input.resetToCanonical !== true && !(input.requiredAncestorHeads && input.requiredAncestorHeads.length > 0) && !REMEDIATION_TRIGGER_TYPES.has(input.trigger.type);
|
|
4688
4853
|
const LOCAL_ACTIVITY_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4689
4854
|
"write",
|
|
@@ -4724,7 +4889,14 @@ async function startWorker(options) {
|
|
|
4724
4889
|
trigger: leader.trigger,
|
|
4725
4890
|
assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, leader.trigger)
|
|
4726
4891
|
});
|
|
4727
|
-
return {
|
|
4892
|
+
return {
|
|
4893
|
+
result,
|
|
4894
|
+
ranCycle: true,
|
|
4895
|
+
startedAtMs,
|
|
4896
|
+
finishedAtMs: Date.now(),
|
|
4897
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
4898
|
+
staleAttemptIds
|
|
4899
|
+
};
|
|
4728
4900
|
}
|
|
4729
4901
|
});
|
|
4730
4902
|
const runWorkspaceSync = async (input) => {
|
|
@@ -4754,7 +4926,7 @@ async function startWorker(options) {
|
|
|
4754
4926
|
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
4755
4927
|
throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
|
|
4756
4928
|
}
|
|
4757
|
-
ws
|
|
4929
|
+
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "workspace_sync_started", attemptId }));
|
|
4758
4930
|
}
|
|
4759
4931
|
let queueMs = 0;
|
|
4760
4932
|
let prepareMs = 0;
|
|
@@ -4806,6 +4978,7 @@ async function startWorker(options) {
|
|
|
4806
4978
|
};
|
|
4807
4979
|
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
4808
4980
|
pendingAutomaticTrigger = trigger;
|
|
4981
|
+
projectRuntime.pendingAutomaticTrigger = trigger;
|
|
4809
4982
|
const pendingBranchDeferral = () => pendingCreatedBranchAutomaticSyncDeferral(
|
|
4810
4983
|
projectWorkspaceState.locallyPendingCreatedBranches,
|
|
4811
4984
|
activeWorkspaceMutationTargets(),
|
|
@@ -4828,6 +5001,7 @@ async function startWorker(options) {
|
|
|
4828
5001
|
return;
|
|
4829
5002
|
}
|
|
4830
5003
|
pendingAutomaticTrigger = void 0;
|
|
5004
|
+
projectRuntime.pendingAutomaticTrigger = void 0;
|
|
4831
5005
|
automaticSyncInFlight = true;
|
|
4832
5006
|
void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
|
|
4833
5007
|
if (result.outcome === "failed") {
|
|
@@ -4851,9 +5025,14 @@ async function startWorker(options) {
|
|
|
4851
5025
|
workspaceAutomaticTimer.unref();
|
|
4852
5026
|
};
|
|
4853
5027
|
const markWorkspaceDirty = (trigger, force = false, target) => {
|
|
5028
|
+
if (projectRuntime.markWorkspaceDirty && projectRuntime.markWorkspaceDirty !== markWorkspaceDirty) {
|
|
5029
|
+
projectRuntime.markWorkspaceDirty(trigger, force, target);
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
4854
5032
|
if (target) recordVisibleWorkspaceMutation(target);
|
|
4855
5033
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
4856
5034
|
};
|
|
5035
|
+
projectRuntime.markWorkspaceDirty = markWorkspaceDirty;
|
|
4857
5036
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
4858
5037
|
const resolveMessageTarget = (target) => {
|
|
4859
5038
|
if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
|
|
@@ -5156,6 +5335,7 @@ async function startWorker(options) {
|
|
|
5156
5335
|
bootstrapCredentialGenerationFingerprintAtProcessStart = null;
|
|
5157
5336
|
visibleGitIdentity = message.gitIdentity;
|
|
5158
5337
|
workspaceGitIdentity = message.gitIdentity;
|
|
5338
|
+
projectRuntime.gitIdentity = message.gitIdentity;
|
|
5159
5339
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
5160
5340
|
workspaceCredentialHelper = nextWorkspaceCredentialHelper;
|
|
5161
5341
|
workspaceCredentialUsername = workerCredentialUsername;
|
|
@@ -5370,6 +5550,10 @@ async function startWorker(options) {
|
|
|
5370
5550
|
Authorization: `Bearer ${token}`
|
|
5371
5551
|
}
|
|
5372
5552
|
});
|
|
5553
|
+
const connectionAttemptTimer = setTimeout(() => {
|
|
5554
|
+
if (ws.readyState === WebSocket.CONNECTING) ws.terminate();
|
|
5555
|
+
}, 3e4);
|
|
5556
|
+
connectionAttemptTimer.unref();
|
|
5373
5557
|
let lastServerControlMessageAtMs = performance.now();
|
|
5374
5558
|
let serverHeartbeatProbeStartedAtMs;
|
|
5375
5559
|
let lastServerHeartbeatSweepAtMs = performance.now();
|
|
@@ -5465,7 +5649,9 @@ async function startWorker(options) {
|
|
|
5465
5649
|
process.on("SIGTERM", handleGracefulShutdown);
|
|
5466
5650
|
process.on("SIGINT", handleGracefulShutdown);
|
|
5467
5651
|
ws.addEventListener("open", () => {
|
|
5652
|
+
clearTimeout(connectionAttemptTimer);
|
|
5468
5653
|
currentWorkerSocket = ws;
|
|
5654
|
+
workerRecoveryId = "connecting";
|
|
5469
5655
|
noteServerControlActivity();
|
|
5470
5656
|
lastServerHeartbeatSweepAtMs = performance.now();
|
|
5471
5657
|
serverHeartbeatWatchdog = setInterval(() => {
|
|
@@ -5509,6 +5695,9 @@ async function startWorker(options) {
|
|
|
5509
5695
|
});
|
|
5510
5696
|
const hello = {
|
|
5511
5697
|
type: "hello",
|
|
5698
|
+
resumableProtocol: WORKER_RESUMABLE_PROTOCOL,
|
|
5699
|
+
runtimeId: workerRuntimeId,
|
|
5700
|
+
ledgerId: recoveryStore.ledgerId,
|
|
5512
5701
|
startupReady: startupRecoveryComplete,
|
|
5513
5702
|
hostInfo: {
|
|
5514
5703
|
hostname: hostname(),
|
|
@@ -5533,15 +5722,17 @@ async function startWorker(options) {
|
|
|
5533
5722
|
root: rootDir
|
|
5534
5723
|
}
|
|
5535
5724
|
};
|
|
5536
|
-
ws
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
terminalReplayTimer = setInterval(() => {
|
|
5542
|
-
for (const terminal of pendingProcessTerminals.values()) {
|
|
5543
|
-
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;
|
|
5544
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);
|
|
5545
5736
|
}, 5e3);
|
|
5546
5737
|
terminalReplayTimer.unref();
|
|
5547
5738
|
process.stdout.write("[r5d-worker] connected\n");
|
|
@@ -5549,18 +5740,103 @@ async function startWorker(options) {
|
|
|
5549
5740
|
ws.addEventListener("message", (event) => {
|
|
5550
5741
|
noteServerControlActivity();
|
|
5551
5742
|
void (async () => {
|
|
5743
|
+
if (currentWorkerSocket !== ws) return;
|
|
5552
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
|
+
}
|
|
5553
5804
|
if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
|
|
5554
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
|
+
}
|
|
5555
5821
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
5556
|
-
const
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
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
|
+
};
|
|
5564
5840
|
if (message.type === "connected") {
|
|
5565
5841
|
return;
|
|
5566
5842
|
}
|
|
@@ -5576,8 +5852,17 @@ async function startWorker(options) {
|
|
|
5576
5852
|
if (message.type === "workspace_config") {
|
|
5577
5853
|
const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
|
|
5578
5854
|
const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
|
|
5579
|
-
|
|
5580
|
-
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;
|
|
5581
5866
|
unsafeWorkspaceSyncBouncePending = false;
|
|
5582
5867
|
if (configured.result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
|
|
5583
5868
|
sendWorkerMessageFromCurrentSource(ws, {
|
|
@@ -5623,16 +5908,31 @@ async function startWorker(options) {
|
|
|
5623
5908
|
return;
|
|
5624
5909
|
}
|
|
5625
5910
|
if (message.type === "sync_workspace") {
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
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
|
+
}
|
|
5636
5936
|
}
|
|
5637
5937
|
if (message.type === "create_project_branch") {
|
|
5638
5938
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
@@ -5898,6 +6198,7 @@ async function startWorker(options) {
|
|
|
5898
6198
|
}
|
|
5899
6199
|
if (message.type === "exec_terminal_ack") {
|
|
5900
6200
|
pendingProcessTerminals.delete(message.runId);
|
|
6201
|
+
recoveryStore.acknowledgeTerminal(message.runId);
|
|
5901
6202
|
return;
|
|
5902
6203
|
}
|
|
5903
6204
|
if (message.type === "port_forward_connect") {
|
|
@@ -5949,7 +6250,7 @@ async function startWorker(options) {
|
|
|
5949
6250
|
return;
|
|
5950
6251
|
}
|
|
5951
6252
|
if (message.type === "ping") {
|
|
5952
|
-
ws
|
|
6253
|
+
sendSerializedWorkerMessage(ws, JSON.stringify({ type: "pong" }));
|
|
5953
6254
|
return;
|
|
5954
6255
|
}
|
|
5955
6256
|
if (message.type === "cancel") {
|
|
@@ -5969,7 +6270,8 @@ async function startWorker(options) {
|
|
|
5969
6270
|
cancelledProcessRuns.add(message.runId);
|
|
5970
6271
|
cancelMessage = "Cancellation queued before command start";
|
|
5971
6272
|
}
|
|
5972
|
-
|
|
6273
|
+
sendSerializedWorkerMessage(
|
|
6274
|
+
ws,
|
|
5973
6275
|
JSON.stringify({
|
|
5974
6276
|
type: "cancel_result",
|
|
5975
6277
|
requestId: message.requestId,
|
|
@@ -5981,7 +6283,8 @@ async function startWorker(options) {
|
|
|
5981
6283
|
return;
|
|
5982
6284
|
}
|
|
5983
6285
|
if (message.type === "exec_stdin") {
|
|
5984
|
-
const sendAck = (payload) =>
|
|
6286
|
+
const sendAck = (payload) => sendSerializedWorkerMessage(
|
|
6287
|
+
ws,
|
|
5985
6288
|
JSON.stringify({
|
|
5986
6289
|
type: "operation_result",
|
|
5987
6290
|
requestId: message.requestId,
|
|
@@ -6104,16 +6407,19 @@ async function startWorker(options) {
|
|
|
6104
6407
|
ptyId: message.ptyId,
|
|
6105
6408
|
error: `Shell input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
|
|
6106
6409
|
});
|
|
6410
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6107
6411
|
return;
|
|
6108
6412
|
}
|
|
6109
6413
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6110
6414
|
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
|
|
6111
6415
|
}
|
|
6112
6416
|
writePty(ws, message);
|
|
6417
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6113
6418
|
return;
|
|
6114
6419
|
}
|
|
6115
6420
|
if (message.type === "pty_resize") {
|
|
6116
6421
|
resizePty(message);
|
|
6422
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6117
6423
|
return;
|
|
6118
6424
|
}
|
|
6119
6425
|
if (message.type === "pty_close") {
|
|
@@ -6122,6 +6428,7 @@ async function startWorker(options) {
|
|
|
6122
6428
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
6123
6429
|
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
6124
6430
|
}
|
|
6431
|
+
sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
|
|
6125
6432
|
return;
|
|
6126
6433
|
}
|
|
6127
6434
|
if (message.type === "exec") {
|
|
@@ -6203,7 +6510,11 @@ async function startWorker(options) {
|
|
|
6203
6510
|
} finally {
|
|
6204
6511
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
6205
6512
|
}
|
|
6206
|
-
|
|
6513
|
+
if (infrastructureStoppedRunIds.has(message.runId)) {
|
|
6514
|
+
recoveryStore.unknown(message.requestId);
|
|
6515
|
+
return;
|
|
6516
|
+
}
|
|
6517
|
+
sendSerializedWorkerMessage(ws, JSON.stringify(result));
|
|
6207
6518
|
if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
|
|
6208
6519
|
markWorkspaceDirty(
|
|
6209
6520
|
{ type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
|
|
@@ -6296,7 +6607,7 @@ async function startWorker(options) {
|
|
|
6296
6607
|
});
|
|
6297
6608
|
return;
|
|
6298
6609
|
}
|
|
6299
|
-
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
|
|
6610
|
+
if (message.type === "read_plan" || message.type === "lint_plans" || message.type === "update_task_status" || message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
|
|
6300
6611
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6301
6612
|
sendWorkerMessage(ws, {
|
|
6302
6613
|
type: "operation_result",
|
|
@@ -6306,7 +6617,7 @@ async function startWorker(options) {
|
|
|
6306
6617
|
return;
|
|
6307
6618
|
}
|
|
6308
6619
|
const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
|
|
6309
|
-
const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
|
|
6620
|
+
const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing) && targetMayMutateVisibleWorkspace(message.target);
|
|
6310
6621
|
if (reservesVisibleWorkspace) {
|
|
6311
6622
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
6312
6623
|
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
@@ -6326,16 +6637,17 @@ async function startWorker(options) {
|
|
|
6326
6637
|
assertAdmission: assertMessageAdmission
|
|
6327
6638
|
});
|
|
6328
6639
|
});
|
|
6329
|
-
|
|
6640
|
+
sendSerializedWorkerMessage(
|
|
6641
|
+
ws,
|
|
6330
6642
|
JSON.stringify({
|
|
6331
6643
|
type: "operation_result",
|
|
6332
6644
|
requestId: message.requestId,
|
|
6333
6645
|
result
|
|
6334
6646
|
})
|
|
6335
6647
|
);
|
|
6336
|
-
if (mutatesVisibleWorkspace) {
|
|
6648
|
+
if (mutatesVisibleWorkspace && (result.type !== "read_plan" || result.created)) {
|
|
6337
6649
|
dirtyTrigger = {
|
|
6338
|
-
type: message.type,
|
|
6650
|
+
type: message.type === "edit" || message.type === "update_task_status" ? "edit" : "write",
|
|
6339
6651
|
sessionId: message.sessionId,
|
|
6340
6652
|
toolCallId: message.requestId,
|
|
6341
6653
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
@@ -6343,7 +6655,8 @@ async function startWorker(options) {
|
|
|
6343
6655
|
markWorkspaceDirty(dirtyTrigger, false, message.target);
|
|
6344
6656
|
}
|
|
6345
6657
|
} catch (error) {
|
|
6346
|
-
|
|
6658
|
+
sendSerializedWorkerMessage(
|
|
6659
|
+
ws,
|
|
6347
6660
|
JSON.stringify({
|
|
6348
6661
|
type: "operation_result",
|
|
6349
6662
|
requestId: message.requestId,
|
|
@@ -6362,7 +6675,8 @@ async function startWorker(options) {
|
|
|
6362
6675
|
}
|
|
6363
6676
|
const unhandledMessage = message;
|
|
6364
6677
|
if (typeof unhandledMessage.requestId === "string") {
|
|
6365
|
-
|
|
6678
|
+
sendSerializedWorkerMessage(
|
|
6679
|
+
ws,
|
|
6366
6680
|
JSON.stringify({
|
|
6367
6681
|
type: "operation_result",
|
|
6368
6682
|
requestId: unhandledMessage.requestId,
|
|
@@ -6376,6 +6690,7 @@ async function startWorker(options) {
|
|
|
6376
6690
|
});
|
|
6377
6691
|
});
|
|
6378
6692
|
ws.addEventListener("close", (event) => {
|
|
6693
|
+
clearTimeout(connectionAttemptTimer);
|
|
6379
6694
|
const processEvents = process;
|
|
6380
6695
|
processEvents.off("SIGTERM", handleGracefulShutdown);
|
|
6381
6696
|
processEvents.off("SIGINT", handleGracefulShutdown);
|
|
@@ -6392,15 +6707,20 @@ async function startWorker(options) {
|
|
|
6392
6707
|
clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
|
|
6393
6708
|
deferredWorkspaceConfigurationRefreshTimer = void 0;
|
|
6394
6709
|
}
|
|
6710
|
+
if (outputReplayTimer) {
|
|
6711
|
+
clearInterval(outputReplayTimer);
|
|
6712
|
+
outputReplayTimer = void 0;
|
|
6713
|
+
}
|
|
6395
6714
|
if (terminalReplayTimer) {
|
|
6396
6715
|
clearInterval(terminalReplayTimer);
|
|
6397
6716
|
terminalReplayTimer = void 0;
|
|
6398
6717
|
}
|
|
6399
|
-
if (
|
|
6400
|
-
|
|
6401
|
-
|
|
6718
|
+
if (projectRuntime.configuration) {
|
|
6719
|
+
projectRuntime.configuration.incidentId = activeWorkspaceIncidentId;
|
|
6720
|
+
projectRuntime.configuration.deferred = deferredWorkspaceConfiguration;
|
|
6402
6721
|
}
|
|
6403
|
-
|
|
6722
|
+
if (currentWorkerSocket === ws) currentWorkerSocket = null;
|
|
6723
|
+
workerRecoveryId = "reconnecting";
|
|
6404
6724
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
6405
6725
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
6406
6726
|
`);
|
|
@@ -6410,50 +6730,19 @@ async function startWorker(options) {
|
|
|
6410
6730
|
if (shutdownAfterClose) {
|
|
6411
6731
|
process.exit(0);
|
|
6412
6732
|
}
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
if (!startupRecoveryComplete) {
|
|
6417
|
-
process.exit(1);
|
|
6418
|
-
return;
|
|
6419
|
-
}
|
|
6420
|
-
const reconnectDelayMs = workerReconnectDelayMs();
|
|
6421
|
-
process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
|
|
6733
|
+
const reconnect = () => {
|
|
6734
|
+
const delay = workerReconnectDelayMs(Math.random, workerReconnectAttempt++);
|
|
6735
|
+
process.stderr.write(`[r5d-worker] reconnecting in ${delay}ms
|
|
6422
6736
|
`);
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6426
|
-
`);
|
|
6427
|
-
process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
|
|
6428
|
-
});
|
|
6429
|
-
}, reconnectDelayMs);
|
|
6430
|
-
});
|
|
6431
|
-
return;
|
|
6432
|
-
}
|
|
6433
|
-
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
6434
|
-
const scheduleReconnect = (reconnect2) => {
|
|
6435
|
-
const reconnectDelayMs = workerReconnectDelayMs();
|
|
6436
|
-
process.stderr.write(
|
|
6437
|
-
`[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
|
|
6438
|
-
`
|
|
6439
|
-
);
|
|
6440
|
-
setTimeout(reconnect2, reconnectDelayMs);
|
|
6441
|
-
};
|
|
6442
|
-
const reconnect = () => {
|
|
6443
|
-
void startWorker(options).catch((error) => {
|
|
6737
|
+
setTimeout(() => {
|
|
6738
|
+
void startupRecoveryPromise.then(() => startWorker(options, projectRuntime)).catch((error) => {
|
|
6444
6739
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6445
6740
|
`);
|
|
6446
|
-
|
|
6447
|
-
scheduleReconnect(reconnect);
|
|
6448
|
-
return;
|
|
6449
|
-
}
|
|
6450
|
-
process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
|
|
6741
|
+
reconnect();
|
|
6451
6742
|
});
|
|
6452
|
-
};
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
}
|
|
6456
|
-
process.exit(WORKER_RECONNECT_EXIT_CODE);
|
|
6743
|
+
}, delay);
|
|
6744
|
+
};
|
|
6745
|
+
reconnect();
|
|
6457
6746
|
});
|
|
6458
6747
|
ws.addEventListener("error", (event) => {
|
|
6459
6748
|
const message = "message" in event && typeof event.message === "string" && event.message.trim() ? `: ${event.message.trim()}` : "";
|