@ricsam/r5d-worker 0.0.53 → 0.0.55
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/heartbeat.cjs +15 -2
- package/dist/cjs/main.cjs +186 -145
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-incident-state.cjs +39 -0
- package/dist/cjs/workspace-mutation-gate.cjs +92 -0
- package/dist/cjs/workspace-sync.cjs +73 -41
- package/dist/mjs/heartbeat.mjs +12 -1
- package/dist/mjs/main.mjs +191 -146
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-incident-state.mjs +14 -0
- package/dist/mjs/workspace-mutation-gate.mjs +68 -0
- package/dist/mjs/workspace-sync.mjs +71 -41
- package/dist/types/heartbeat.d.ts +7 -0
- package/dist/types/workspace-incident-state.d.ts +8 -0
- package/dist/types/workspace-mutation-gate.d.ts +19 -0
- package/dist/types/workspace-sync.d.ts +14 -1
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -6,9 +6,14 @@ import os, { hostname } from "node:os";
|
|
|
6
6
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
7
7
|
import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
|
|
8
8
|
import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
grantWorkerHeartbeatBusyGrace,
|
|
11
|
+
hasWorkerHeartbeatTimedOutWithBusyGrace,
|
|
12
|
+
WORKER_HEARTBEAT_INTERVAL_MS
|
|
13
|
+
} from "./heartbeat.mjs";
|
|
10
14
|
import { terminateProcessTree } from "./process-tree.mjs";
|
|
11
15
|
import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
|
|
16
|
+
import { applyWorkspaceIncidentUpdate, releasePendingWorkspaceHead } from "./workspace-incident-state.mjs";
|
|
12
17
|
import {
|
|
13
18
|
isRetryableWorkerServerStatus,
|
|
14
19
|
superviseWorkerRuntime,
|
|
@@ -39,6 +44,7 @@ const pendingProcessTerminals = /* @__PURE__ */ new Map();
|
|
|
39
44
|
const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
40
45
|
const activePtys = /* @__PURE__ */ new Map();
|
|
41
46
|
let currentWorkerSocket = null;
|
|
47
|
+
const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
|
|
42
48
|
let githubCredential = null;
|
|
43
49
|
let visibleGitIdentity = null;
|
|
44
50
|
function defaultConfigPath() {
|
|
@@ -2210,75 +2216,72 @@ function resolveHostShell(command, platform = process.platform) {
|
|
|
2210
2216
|
return { file, args: command === void 0 ? ["-l"] : ["-lc", command] };
|
|
2211
2217
|
}
|
|
2212
2218
|
async function openPty(input) {
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
target: input.resolvedTarget.target,
|
|
2216
|
-
planRoot: input.planRoot,
|
|
2217
|
-
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
2218
|
-
}) : {};
|
|
2219
|
-
const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
|
|
2220
|
-
const shell = resolveHostShell(input.message.command);
|
|
2221
|
-
const ptyProcess = createNodePtyBridge({
|
|
2222
|
-
file: shell.file,
|
|
2223
|
-
args: shell.args,
|
|
2224
|
-
ptyOptions: {
|
|
2225
|
-
name: "xterm-256color",
|
|
2226
|
-
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
2227
|
-
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
2228
|
-
cwd: input.resolvedTarget.rootPath,
|
|
2229
|
-
env: {
|
|
2230
|
-
...process.env,
|
|
2231
|
-
...githubProcessEnv(),
|
|
2232
|
-
...input.message.env ?? {},
|
|
2233
|
-
...planProcessEnv,
|
|
2234
|
-
...targetProcessEnv
|
|
2235
|
-
}
|
|
2236
|
-
},
|
|
2237
|
-
onOpened: () => {
|
|
2238
|
-
sendWorkerMessage(input.ws, {
|
|
2239
|
-
type: "pty_opened",
|
|
2240
|
-
requestId: input.message.requestId,
|
|
2241
|
-
ptyId: input.message.ptyId
|
|
2242
|
-
});
|
|
2243
|
-
},
|
|
2244
|
-
onOutput: (data) => {
|
|
2245
|
-
sendWorkerMessage(input.ws, {
|
|
2246
|
-
type: "pty_output",
|
|
2247
|
-
ptyId: input.message.ptyId,
|
|
2248
|
-
data
|
|
2249
|
-
});
|
|
2250
|
-
},
|
|
2251
|
-
onExit: (event) => {
|
|
2252
|
-
activePtys.delete(input.message.ptyId);
|
|
2253
|
-
sendWorkerMessage(input.ws, {
|
|
2254
|
-
type: "pty_exit",
|
|
2255
|
-
ptyId: input.message.ptyId,
|
|
2256
|
-
exitCode: event.exitCode,
|
|
2257
|
-
signal: event.signal
|
|
2258
|
-
});
|
|
2259
|
-
},
|
|
2260
|
-
onError: (error) => {
|
|
2261
|
-
activePtys.delete(input.message.ptyId);
|
|
2262
|
-
sendWorkerMessage(input.ws, {
|
|
2263
|
-
type: "pty_error",
|
|
2264
|
-
requestId: input.message.requestId,
|
|
2265
|
-
ptyId: input.message.ptyId,
|
|
2266
|
-
error: error.message
|
|
2267
|
-
});
|
|
2268
|
-
}
|
|
2269
|
-
});
|
|
2270
|
-
activePtys.set(input.message.ptyId, {
|
|
2271
|
-
...ptyProcess,
|
|
2272
|
-
target: input.resolvedTarget.target
|
|
2273
|
-
});
|
|
2274
|
-
} catch (error) {
|
|
2275
|
-
sendWorkerMessage(input.ws, {
|
|
2276
|
-
type: "pty_error",
|
|
2277
|
-
requestId: input.message.requestId,
|
|
2278
|
-
ptyId: input.message.ptyId,
|
|
2279
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2280
|
-
});
|
|
2219
|
+
if (activePtys.has(input.message.ptyId)) {
|
|
2220
|
+
throw new Error(`Shell ${input.message.ptyId} is already open`);
|
|
2281
2221
|
}
|
|
2222
|
+
const planProcessEnv = !(input.resolvedTarget.target.type === "workspace" && input.resolvedTarget.target.rootProfile === "canonical_sync") ? preparePlanEnvForShell({
|
|
2223
|
+
target: input.resolvedTarget.target,
|
|
2224
|
+
planRoot: input.planRoot,
|
|
2225
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
2226
|
+
}) : {};
|
|
2227
|
+
const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
|
|
2228
|
+
const shell = resolveHostShell(input.message.command);
|
|
2229
|
+
const ptyProcess = createNodePtyBridge({
|
|
2230
|
+
file: shell.file,
|
|
2231
|
+
args: shell.args,
|
|
2232
|
+
ptyOptions: {
|
|
2233
|
+
name: "xterm-256color",
|
|
2234
|
+
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
2235
|
+
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
2236
|
+
cwd: input.resolvedTarget.rootPath,
|
|
2237
|
+
env: {
|
|
2238
|
+
...process.env,
|
|
2239
|
+
...githubProcessEnv(),
|
|
2240
|
+
...input.message.env ?? {},
|
|
2241
|
+
...planProcessEnv,
|
|
2242
|
+
...targetProcessEnv
|
|
2243
|
+
}
|
|
2244
|
+
},
|
|
2245
|
+
onOpened: () => {
|
|
2246
|
+
sendWorkerMessage(input.ws, {
|
|
2247
|
+
type: "pty_opened",
|
|
2248
|
+
requestId: input.message.requestId,
|
|
2249
|
+
ptyId: input.message.ptyId
|
|
2250
|
+
});
|
|
2251
|
+
},
|
|
2252
|
+
onOutput: (data) => {
|
|
2253
|
+
sendWorkerMessage(input.ws, {
|
|
2254
|
+
type: "pty_output",
|
|
2255
|
+
ptyId: input.message.ptyId,
|
|
2256
|
+
data
|
|
2257
|
+
});
|
|
2258
|
+
},
|
|
2259
|
+
onExit: (event) => {
|
|
2260
|
+
input.releaseWorkspaceMutation();
|
|
2261
|
+
activePtys.delete(input.message.ptyId);
|
|
2262
|
+
sendWorkerMessage(input.ws, {
|
|
2263
|
+
type: "pty_exit",
|
|
2264
|
+
ptyId: input.message.ptyId,
|
|
2265
|
+
exitCode: event.exitCode,
|
|
2266
|
+
signal: event.signal
|
|
2267
|
+
});
|
|
2268
|
+
},
|
|
2269
|
+
onError: (error) => {
|
|
2270
|
+
input.releaseWorkspaceMutation();
|
|
2271
|
+
activePtys.delete(input.message.ptyId);
|
|
2272
|
+
sendWorkerMessage(input.ws, {
|
|
2273
|
+
type: "pty_error",
|
|
2274
|
+
requestId: input.message.requestId,
|
|
2275
|
+
ptyId: input.message.ptyId,
|
|
2276
|
+
error: error.message
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
});
|
|
2280
|
+
activePtys.set(input.message.ptyId, {
|
|
2281
|
+
...ptyProcess,
|
|
2282
|
+
target: input.resolvedTarget.target,
|
|
2283
|
+
releaseWorkspaceMutation: input.releaseWorkspaceMutation
|
|
2284
|
+
});
|
|
2282
2285
|
}
|
|
2283
2286
|
function writePty(ws, message) {
|
|
2284
2287
|
const ptyProcess = activePtys.get(message.ptyId);
|
|
@@ -2355,7 +2358,6 @@ async function startWorker(options) {
|
|
|
2355
2358
|
fs.mkdirSync(planRoot, { recursive: true });
|
|
2356
2359
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2357
2360
|
const pendingManifestCheckouts = /* @__PURE__ */ new Map();
|
|
2358
|
-
const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
|
|
2359
2361
|
let workspaceRemoteUrl = null;
|
|
2360
2362
|
let activeWorkspaceIncidentId = null;
|
|
2361
2363
|
let previousPeriodicFingerprint = null;
|
|
@@ -2381,8 +2383,32 @@ async function startWorker(options) {
|
|
|
2381
2383
|
...overrides
|
|
2382
2384
|
};
|
|
2383
2385
|
};
|
|
2384
|
-
const sendWorkspaceSyncResult = (
|
|
2385
|
-
|
|
2386
|
+
const sendWorkspaceSyncResult = (authority, result) => {
|
|
2387
|
+
try {
|
|
2388
|
+
ws.send(JSON.stringify({ type: "workspace_sync_result", ...authority, result }));
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
process.stderr.write(
|
|
2391
|
+
`[r5d-worker] failed to send workspace_sync_result on its issuing connection: ${error instanceof Error ? error.message : String(error)}
|
|
2392
|
+
`
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
const requestWorkspaceSync = (trigger, fingerprint) => {
|
|
2397
|
+
try {
|
|
2398
|
+
ws.send(
|
|
2399
|
+
JSON.stringify({
|
|
2400
|
+
type: "workspace_sync_requested",
|
|
2401
|
+
observationId: crypto.randomUUID(),
|
|
2402
|
+
trigger,
|
|
2403
|
+
...fingerprint ? { fingerprint } : {}
|
|
2404
|
+
})
|
|
2405
|
+
);
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
process.stderr.write(
|
|
2408
|
+
`[r5d-worker] failed to send workspace sync observation on its current connection: ${error instanceof Error ? error.message : String(error)}
|
|
2409
|
+
`
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2386
2412
|
};
|
|
2387
2413
|
const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
2388
2414
|
const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
|
|
@@ -2433,7 +2459,7 @@ async function startWorker(options) {
|
|
|
2433
2459
|
}
|
|
2434
2460
|
return created;
|
|
2435
2461
|
};
|
|
2436
|
-
const runRequestedWorkspaceSync = async (
|
|
2462
|
+
const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}) => {
|
|
2437
2463
|
workspaceSyncRequestsInFlight += 1;
|
|
2438
2464
|
try {
|
|
2439
2465
|
let pendingTargets = [];
|
|
@@ -2477,7 +2503,7 @@ async function startWorker(options) {
|
|
|
2477
2503
|
}
|
|
2478
2504
|
}
|
|
2479
2505
|
previousPeriodicFingerprint = null;
|
|
2480
|
-
sendWorkspaceSyncResult(
|
|
2506
|
+
sendWorkspaceSyncResult(authority, result);
|
|
2481
2507
|
return result;
|
|
2482
2508
|
} catch (error) {
|
|
2483
2509
|
const result = {
|
|
@@ -2497,7 +2523,7 @@ async function startWorker(options) {
|
|
|
2497
2523
|
error: error instanceof Error ? error.message : String(error)
|
|
2498
2524
|
};
|
|
2499
2525
|
previousPeriodicFingerprint = null;
|
|
2500
|
-
sendWorkspaceSyncResult(
|
|
2526
|
+
sendWorkspaceSyncResult(authority, result);
|
|
2501
2527
|
return result;
|
|
2502
2528
|
} finally {
|
|
2503
2529
|
workspaceSyncRequestsInFlight -= 1;
|
|
@@ -2509,6 +2535,7 @@ async function startWorker(options) {
|
|
|
2509
2535
|
}
|
|
2510
2536
|
});
|
|
2511
2537
|
let lastServerHeartbeatAt = null;
|
|
2538
|
+
let heartbeatBusyGrace = null;
|
|
2512
2539
|
let heartbeatTimedOut = false;
|
|
2513
2540
|
let heartbeatWatchdog;
|
|
2514
2541
|
const stopHeartbeatWatchdog = () => {
|
|
@@ -2520,7 +2547,7 @@ async function startWorker(options) {
|
|
|
2520
2547
|
ws.addEventListener("open", () => {
|
|
2521
2548
|
currentWorkerSocket = ws;
|
|
2522
2549
|
heartbeatWatchdog = setInterval(() => {
|
|
2523
|
-
if (heartbeatTimedOut || !
|
|
2550
|
+
if (heartbeatTimedOut || !hasWorkerHeartbeatTimedOutWithBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace)) {
|
|
2524
2551
|
return;
|
|
2525
2552
|
}
|
|
2526
2553
|
heartbeatTimedOut = true;
|
|
@@ -2540,7 +2567,8 @@ async function startWorker(options) {
|
|
|
2540
2567
|
capabilities: {
|
|
2541
2568
|
updateClis: true,
|
|
2542
2569
|
canonicalResolverCheckout: true,
|
|
2543
|
-
browserPortForwarding: true
|
|
2570
|
+
browserPortForwarding: true,
|
|
2571
|
+
globalWorkspaceSyncLeaseV1: true
|
|
2544
2572
|
},
|
|
2545
2573
|
projectRoot: projectsRoot,
|
|
2546
2574
|
artifactRoot,
|
|
@@ -2567,33 +2595,35 @@ async function startWorker(options) {
|
|
|
2567
2595
|
return;
|
|
2568
2596
|
}
|
|
2569
2597
|
if (message.type === "workspace_manifest") {
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
const
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2598
|
+
await workspaceSyncSingleFlight.runMutation(() => {
|
|
2599
|
+
configureGitHubAuth(message.githubCredential);
|
|
2600
|
+
visibleGitIdentity = message.gitIdentity;
|
|
2601
|
+
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
2602
|
+
const previousCheckoutKeys = new Set(
|
|
2603
|
+
allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
|
|
2604
|
+
);
|
|
2605
|
+
const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
|
|
2606
|
+
manifestByProjectId.clear();
|
|
2607
|
+
for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
|
|
2608
|
+
const currentCheckouts = allManifestCheckouts();
|
|
2609
|
+
const currentCheckoutKeys = new Set(
|
|
2610
|
+
currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
|
|
2611
|
+
);
|
|
2612
|
+
for (const key of pendingManifestCheckouts.keys()) {
|
|
2613
|
+
if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
|
|
2614
|
+
}
|
|
2615
|
+
for (const checkout of currentCheckouts) {
|
|
2616
|
+
const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
|
|
2617
|
+
if (previousCheckoutKeys.has(key)) continue;
|
|
2618
|
+
const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
|
|
2619
|
+
if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
|
|
2620
|
+
}
|
|
2621
|
+
previousPeriodicFingerprint = null;
|
|
2622
|
+
process.stdout.write(
|
|
2623
|
+
`[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
|
|
2595
2624
|
`
|
|
2596
|
-
|
|
2625
|
+
);
|
|
2626
|
+
});
|
|
2597
2627
|
if (!periodicWorkspaceScan) {
|
|
2598
2628
|
const emptyFingerprint = createHash("sha256").update("").digest("hex");
|
|
2599
2629
|
periodicWorkspaceScan = setInterval(() => {
|
|
@@ -2610,7 +2640,7 @@ async function startWorker(options) {
|
|
|
2610
2640
|
);
|
|
2611
2641
|
if (hasPendingGenericCheckout) {
|
|
2612
2642
|
previousPeriodicFingerprint = null;
|
|
2613
|
-
|
|
2643
|
+
requestWorkspaceSync({
|
|
2614
2644
|
type: "periodic",
|
|
2615
2645
|
detail: "workspace manifest additions"
|
|
2616
2646
|
});
|
|
@@ -2626,7 +2656,7 @@ async function startWorker(options) {
|
|
|
2626
2656
|
return;
|
|
2627
2657
|
}
|
|
2628
2658
|
previousPeriodicFingerprint = null;
|
|
2629
|
-
|
|
2659
|
+
requestWorkspaceSync({ type: "periodic" }, fingerprint);
|
|
2630
2660
|
})().catch((error) => {
|
|
2631
2661
|
process.stderr.write(
|
|
2632
2662
|
`[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -2634,6 +2664,7 @@ async function startWorker(options) {
|
|
|
2634
2664
|
);
|
|
2635
2665
|
}).finally(() => {
|
|
2636
2666
|
periodicWorkspaceScanInFlight = false;
|
|
2667
|
+
heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
2637
2668
|
});
|
|
2638
2669
|
}, WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
|
|
2639
2670
|
periodicWorkspaceScan.unref();
|
|
@@ -2641,23 +2672,23 @@ async function startWorker(options) {
|
|
|
2641
2672
|
return;
|
|
2642
2673
|
}
|
|
2643
2674
|
if (message.type === "sync_workspace") {
|
|
2644
|
-
await runRequestedWorkspaceSync(
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2675
|
+
await runRequestedWorkspaceSync(
|
|
2676
|
+
{
|
|
2677
|
+
requestId: message.requestId,
|
|
2678
|
+
intentId: message.intentId,
|
|
2679
|
+
leaseEpoch: message.leaseEpoch,
|
|
2680
|
+
leaseToken: message.leaseToken
|
|
2681
|
+
},
|
|
2682
|
+
message.trigger,
|
|
2683
|
+
{
|
|
2684
|
+
attemptId: message.attemptId,
|
|
2685
|
+
quarantineRef: message.quarantineRef,
|
|
2686
|
+
confirmedLargeDiff: message.confirmedLargeDiff,
|
|
2687
|
+
confirmationReason: message.confirmationReason,
|
|
2688
|
+
resetToCanonical: message.resetToCanonical,
|
|
2689
|
+
skipVisibleMirror: message.skipVisibleMirror
|
|
2690
|
+
}
|
|
2691
|
+
);
|
|
2661
2692
|
return;
|
|
2662
2693
|
}
|
|
2663
2694
|
if (message.type === "sync_session_artifacts") {
|
|
@@ -2687,8 +2718,17 @@ async function startWorker(options) {
|
|
|
2687
2718
|
return;
|
|
2688
2719
|
}
|
|
2689
2720
|
if (message.type === "workspace_incident_updated") {
|
|
2690
|
-
|
|
2721
|
+
const previousIncidentId = activeWorkspaceIncidentId;
|
|
2722
|
+
activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
|
|
2691
2723
|
previousPeriodicFingerprint = null;
|
|
2724
|
+
const releasedHead = releasePendingWorkspaceHead(
|
|
2725
|
+
previousIncidentId,
|
|
2726
|
+
activeWorkspaceIncidentId,
|
|
2727
|
+
message.status === "resolved" || message.status === "confirmed" || message.status === "reset" ? message.incidentId : null
|
|
2728
|
+
);
|
|
2729
|
+
if (releasedHead.syncHead) {
|
|
2730
|
+
requestWorkspaceSync({ type: "inbound_head", detail: releasedHead.syncHead });
|
|
2731
|
+
}
|
|
2692
2732
|
return;
|
|
2693
2733
|
}
|
|
2694
2734
|
if (message.type === "exec_terminal_ack") {
|
|
@@ -2745,6 +2785,7 @@ async function startWorker(options) {
|
|
|
2745
2785
|
}
|
|
2746
2786
|
if (message.type === "ping") {
|
|
2747
2787
|
lastServerHeartbeatAt = Date.now();
|
|
2788
|
+
heartbeatBusyGrace = null;
|
|
2748
2789
|
ws.send(JSON.stringify({ type: "pong" }));
|
|
2749
2790
|
return;
|
|
2750
2791
|
}
|
|
@@ -2785,11 +2826,14 @@ async function startWorker(options) {
|
|
|
2785
2826
|
});
|
|
2786
2827
|
return;
|
|
2787
2828
|
}
|
|
2829
|
+
const releaseWorkspaceMutation = await workspaceSyncSingleFlight.acquireMutation();
|
|
2830
|
+
let leaseTransferred = false;
|
|
2788
2831
|
try {
|
|
2789
2832
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
2790
2833
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
|
|
2791
2834
|
`);
|
|
2792
|
-
await openPty({ ws, message, resolvedTarget, planRoot });
|
|
2835
|
+
await openPty({ ws, message, resolvedTarget, planRoot, releaseWorkspaceMutation });
|
|
2836
|
+
leaseTransferred = true;
|
|
2793
2837
|
} catch (error) {
|
|
2794
2838
|
sendWorkerMessage(ws, {
|
|
2795
2839
|
type: "pty_error",
|
|
@@ -2797,6 +2841,8 @@ async function startWorker(options) {
|
|
|
2797
2841
|
ptyId: message.ptyId,
|
|
2798
2842
|
error: error instanceof Error ? error.message : String(error)
|
|
2799
2843
|
});
|
|
2844
|
+
} finally {
|
|
2845
|
+
if (!leaseTransferred) releaseWorkspaceMutation();
|
|
2800
2846
|
}
|
|
2801
2847
|
return;
|
|
2802
2848
|
}
|
|
@@ -2825,10 +2871,12 @@ async function startWorker(options) {
|
|
|
2825
2871
|
}
|
|
2826
2872
|
let result;
|
|
2827
2873
|
try {
|
|
2828
|
-
|
|
2829
|
-
|
|
2874
|
+
result = await workspaceSyncSingleFlight.runMutation(async () => {
|
|
2875
|
+
const resolvedTarget = resolveMessageTarget(message.target);
|
|
2876
|
+
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
2830
2877
|
`);
|
|
2831
|
-
|
|
2878
|
+
return await executeCommand({ message, resolvedTarget, baseUrl, token, artifactRoot, planRoot });
|
|
2879
|
+
});
|
|
2832
2880
|
} catch (error) {
|
|
2833
2881
|
result = {
|
|
2834
2882
|
type: "exec_result",
|
|
@@ -2857,11 +2905,11 @@ async function startWorker(options) {
|
|
|
2857
2905
|
requestId: message.requestId,
|
|
2858
2906
|
runId: message.runId
|
|
2859
2907
|
});
|
|
2860
|
-
|
|
2908
|
+
void workspaceSyncSingleFlight.runMutation(async () => {
|
|
2861
2909
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
2862
2910
|
process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
|
|
2863
2911
|
`);
|
|
2864
|
-
|
|
2912
|
+
await executeStreamingCommand({
|
|
2865
2913
|
ws,
|
|
2866
2914
|
message,
|
|
2867
2915
|
resolvedTarget,
|
|
@@ -2869,34 +2917,29 @@ async function startWorker(options) {
|
|
|
2869
2917
|
token,
|
|
2870
2918
|
artifactRoot,
|
|
2871
2919
|
planRoot
|
|
2872
|
-
}).catch((error) => {
|
|
2873
|
-
sendWorkerMessage(ws, {
|
|
2874
|
-
type: "exec_start_error",
|
|
2875
|
-
requestId: message.requestId,
|
|
2876
|
-
runId: message.runId,
|
|
2877
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2878
|
-
});
|
|
2879
2920
|
});
|
|
2880
|
-
}
|
|
2921
|
+
}).catch((error) => {
|
|
2881
2922
|
sendWorkerMessage(ws, {
|
|
2882
2923
|
type: "exec_start_error",
|
|
2883
2924
|
requestId: message.requestId,
|
|
2884
2925
|
runId: message.runId,
|
|
2885
2926
|
error: error instanceof Error ? error.message : String(error)
|
|
2886
2927
|
});
|
|
2887
|
-
}
|
|
2928
|
+
});
|
|
2888
2929
|
return;
|
|
2889
2930
|
}
|
|
2890
2931
|
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
|
|
2891
2932
|
try {
|
|
2892
|
-
const
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2933
|
+
const result = await workspaceSyncSingleFlight.runMutation(async () => {
|
|
2934
|
+
const resolvedTarget = resolveMessageTarget(message.target);
|
|
2935
|
+
return await executeOperation({
|
|
2936
|
+
message,
|
|
2937
|
+
resolvedTarget,
|
|
2938
|
+
baseUrl,
|
|
2939
|
+
token,
|
|
2940
|
+
artifactRoot,
|
|
2941
|
+
planRoot
|
|
2942
|
+
});
|
|
2900
2943
|
});
|
|
2901
2944
|
ws.send(
|
|
2902
2945
|
JSON.stringify({
|
|
@@ -2929,6 +2972,8 @@ async function startWorker(options) {
|
|
|
2929
2972
|
})().catch((error) => {
|
|
2930
2973
|
process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
|
|
2931
2974
|
`);
|
|
2975
|
+
}).finally(() => {
|
|
2976
|
+
heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
2932
2977
|
});
|
|
2933
2978
|
});
|
|
2934
2979
|
ws.addEventListener("close", (event) => {
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function applyWorkspaceIncidentUpdate(currentIncidentId, update) {
|
|
2
|
+
if (update.status === "remediating" || update.status === "waiting_for_worker") return update.incidentId;
|
|
3
|
+
if (currentIncidentId && update.incidentId && update.incidentId !== currentIncidentId) return currentIncidentId;
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
function releasePendingWorkspaceHead(previousIncidentId, currentIncidentId, terminalIncidentId) {
|
|
7
|
+
if (!previousIncidentId || currentIncidentId) return { syncHead: null };
|
|
8
|
+
if (terminalIncidentId === previousIncidentId) return { syncHead: "authoritative" };
|
|
9
|
+
return { syncHead: null };
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
applyWorkspaceIncidentUpdate,
|
|
13
|
+
releasePendingWorkspaceHead
|
|
14
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
class WorkspaceMutationGate {
|
|
2
|
+
activeMutations = 0;
|
|
3
|
+
syncActive = false;
|
|
4
|
+
waiters = [];
|
|
5
|
+
acquireMutation() {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
this.waiters.push({ kind: "mutation", resolve });
|
|
8
|
+
this.drain();
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
async runMutation(operation) {
|
|
12
|
+
const release = await this.acquireMutation();
|
|
13
|
+
try {
|
|
14
|
+
return await operation();
|
|
15
|
+
} finally {
|
|
16
|
+
release();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async runSync(operation) {
|
|
20
|
+
const release = await new Promise((resolve) => {
|
|
21
|
+
this.waiters.push({ kind: "sync", resolve });
|
|
22
|
+
this.drain();
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
return await operation();
|
|
26
|
+
} finally {
|
|
27
|
+
release();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
drain() {
|
|
31
|
+
if (this.syncActive || this.waiters.length === 0) return;
|
|
32
|
+
const first = this.waiters[0];
|
|
33
|
+
if (first?.kind === "sync") {
|
|
34
|
+
if (this.activeMutations > 0) return;
|
|
35
|
+
this.waiters.shift();
|
|
36
|
+
this.syncActive = true;
|
|
37
|
+
first.resolve(this.releaseSync());
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
while (this.waiters[0]?.kind === "mutation") {
|
|
41
|
+
const waiter = this.waiters.shift();
|
|
42
|
+
if (!waiter || waiter.kind !== "mutation") break;
|
|
43
|
+
this.activeMutations += 1;
|
|
44
|
+
waiter.resolve(this.releaseMutation());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
releaseMutation() {
|
|
48
|
+
let released = false;
|
|
49
|
+
return () => {
|
|
50
|
+
if (released) return;
|
|
51
|
+
released = true;
|
|
52
|
+
this.activeMutations -= 1;
|
|
53
|
+
this.drain();
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
releaseSync() {
|
|
57
|
+
let released = false;
|
|
58
|
+
return () => {
|
|
59
|
+
if (released) return;
|
|
60
|
+
released = true;
|
|
61
|
+
this.syncActive = false;
|
|
62
|
+
this.drain();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
WorkspaceMutationGate
|
|
68
|
+
};
|