@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/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 { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
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
- try {
2214
- const planProcessEnv = !(input.resolvedTarget.target.type === "workspace" && input.resolvedTarget.target.rootProfile === "canonical_sync") ? preparePlanEnvForShell({
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 = (requestId, result) => {
2385
- sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
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 (requestId, trigger, overrides = {}) => {
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(requestId, result);
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(requestId, result);
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 || !hasWorkerHeartbeatTimedOut(lastServerHeartbeatAt)) {
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
- configureGitHubAuth(message.githubCredential);
2571
- visibleGitIdentity = message.gitIdentity;
2572
- workspaceRemoteUrl = message.workspaceRemoteUrl;
2573
- const previousCheckoutKeys = new Set(
2574
- allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2575
- );
2576
- const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
2577
- manifestByProjectId.clear();
2578
- for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2579
- const currentCheckouts = allManifestCheckouts();
2580
- const currentCheckoutKeys = new Set(
2581
- currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2582
- );
2583
- for (const key of pendingManifestCheckouts.keys()) {
2584
- if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2585
- }
2586
- for (const checkout of currentCheckouts) {
2587
- const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2588
- if (previousCheckoutKeys.has(key)) continue;
2589
- const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2590
- if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2591
- }
2592
- previousPeriodicFingerprint = null;
2593
- process.stdout.write(
2594
- `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
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
- await runRequestedWorkspaceSync(crypto.randomUUID(), {
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
- await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "periodic" });
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(message.requestId, message.trigger, {
2645
- attemptId: message.attemptId,
2646
- confirmedLargeDiff: message.confirmedLargeDiff,
2647
- confirmationReason: message.confirmationReason,
2648
- resetToCanonical: message.resetToCanonical,
2649
- skipVisibleMirror: message.skipVisibleMirror
2650
- });
2651
- return;
2652
- }
2653
- if (message.type === "workspace_head_updated") {
2654
- if (!activeWorkspaceIncidentId) {
2655
- await runRequestedWorkspaceSync(
2656
- crypto.randomUUID(),
2657
- { type: "inbound_head", detail: message.commitHash },
2658
- { skipVisibleMirror: true }
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
- activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
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
- const resolvedTarget = resolveMessageTarget(message.target);
2829
- process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
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
- result = await executeCommand({ message, resolvedTarget, baseUrl, token, artifactRoot, planRoot });
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
- try {
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
- void executeStreamingCommand({
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
- } catch (error) {
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 resolvedTarget = resolveMessageTarget(message.target);
2893
- const result = await executeOperation({
2894
- message,
2895
- resolvedTarget,
2896
- baseUrl,
2897
- token,
2898
- artifactRoot,
2899
- planRoot
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) => {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.53",
3
+ "version": "0.0.55",
4
4
  "type": "module"
5
5
  }
@@ -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
+ };