@ricsam/r5d-worker 0.0.51 → 0.0.54

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.
@@ -20,7 +20,9 @@ var heartbeat_exports = {};
20
20
  __export(heartbeat_exports, {
21
21
  WORKER_HEARTBEAT_INTERVAL_MS: () => WORKER_HEARTBEAT_INTERVAL_MS,
22
22
  WORKER_HEARTBEAT_TIMEOUT_MS: () => WORKER_HEARTBEAT_TIMEOUT_MS,
23
- hasWorkerHeartbeatTimedOut: () => hasWorkerHeartbeatTimedOut
23
+ grantWorkerHeartbeatBusyGrace: () => grantWorkerHeartbeatBusyGrace,
24
+ hasWorkerHeartbeatTimedOut: () => hasWorkerHeartbeatTimedOut,
25
+ hasWorkerHeartbeatTimedOutWithBusyGrace: () => hasWorkerHeartbeatTimedOutWithBusyGrace
24
26
  });
25
27
  module.exports = __toCommonJS(heartbeat_exports);
26
28
  const WORKER_HEARTBEAT_INTERVAL_MS = 15e3;
@@ -28,9 +30,20 @@ const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
28
30
  function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
29
31
  return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
30
32
  }
33
+ function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt, currentGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS, graceMs = WORKER_HEARTBEAT_INTERVAL_MS) {
34
+ if (!hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs)) return currentGrace;
35
+ if (currentGrace?.heartbeatAt === lastHeartbeatAt) return currentGrace;
36
+ return { heartbeatAt: lastHeartbeatAt, expiresAt: now + graceMs };
37
+ }
38
+ function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt, busyGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
39
+ if (lastHeartbeatAt !== null && busyGrace?.heartbeatAt === lastHeartbeatAt && now < busyGrace.expiresAt) return false;
40
+ return hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs);
41
+ }
31
42
  // Annotate the CommonJS export names for ESM import in node:
32
43
  0 && (module.exports = {
33
44
  WORKER_HEARTBEAT_INTERVAL_MS,
34
45
  WORKER_HEARTBEAT_TIMEOUT_MS,
35
- hasWorkerHeartbeatTimedOut
46
+ grantWorkerHeartbeatBusyGrace,
47
+ hasWorkerHeartbeatTimedOut,
48
+ hasWorkerHeartbeatTimedOutWithBusyGrace
36
49
  });
package/dist/cjs/main.cjs CHANGED
@@ -63,6 +63,7 @@ var import_git_identity = require("./git-identity.cjs");
63
63
  var import_heartbeat = require("./heartbeat.cjs");
64
64
  var import_process_tree = require("./process-tree.cjs");
65
65
  var import_port_forward_client = require("./port-forward-client.cjs");
66
+ var import_workspace_incident_state = require("./workspace-incident-state.cjs");
66
67
  var import_supervisor = require("./supervisor.cjs");
67
68
  var import_managed_paths = require("./managed-paths.cjs");
68
69
  var import_workspace_sync = require("./workspace-sync.cjs");
@@ -81,6 +82,7 @@ const pendingProcessTerminals = /* @__PURE__ */ new Map();
81
82
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
82
83
  const activePtys = /* @__PURE__ */ new Map();
83
84
  let currentWorkerSocket = null;
85
+ const workspaceSyncSingleFlight = new import_workspace_sync.WorkspaceSyncSingleFlight();
84
86
  let githubCredential = null;
85
87
  let visibleGitIdentity = null;
86
88
  function defaultConfigPath() {
@@ -2252,75 +2254,72 @@ function resolveHostShell(command, platform = process.platform) {
2252
2254
  return { file, args: command === void 0 ? ["-l"] : ["-lc", command] };
2253
2255
  }
2254
2256
  async function openPty(input) {
2255
- try {
2256
- const planProcessEnv = !(input.resolvedTarget.target.type === "workspace" && input.resolvedTarget.target.rootProfile === "canonical_sync") ? preparePlanEnvForShell({
2257
- target: input.resolvedTarget.target,
2258
- planRoot: input.planRoot,
2259
- activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2260
- }) : {};
2261
- const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
2262
- const shell = resolveHostShell(input.message.command);
2263
- const ptyProcess = createNodePtyBridge({
2264
- file: shell.file,
2265
- args: shell.args,
2266
- ptyOptions: {
2267
- name: "xterm-256color",
2268
- cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
2269
- rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
2270
- cwd: input.resolvedTarget.rootPath,
2271
- env: {
2272
- ...process.env,
2273
- ...githubProcessEnv(),
2274
- ...input.message.env ?? {},
2275
- ...planProcessEnv,
2276
- ...targetProcessEnv
2277
- }
2278
- },
2279
- onOpened: () => {
2280
- sendWorkerMessage(input.ws, {
2281
- type: "pty_opened",
2282
- requestId: input.message.requestId,
2283
- ptyId: input.message.ptyId
2284
- });
2285
- },
2286
- onOutput: (data) => {
2287
- sendWorkerMessage(input.ws, {
2288
- type: "pty_output",
2289
- ptyId: input.message.ptyId,
2290
- data
2291
- });
2292
- },
2293
- onExit: (event) => {
2294
- activePtys.delete(input.message.ptyId);
2295
- sendWorkerMessage(input.ws, {
2296
- type: "pty_exit",
2297
- ptyId: input.message.ptyId,
2298
- exitCode: event.exitCode,
2299
- signal: event.signal
2300
- });
2301
- },
2302
- onError: (error) => {
2303
- activePtys.delete(input.message.ptyId);
2304
- sendWorkerMessage(input.ws, {
2305
- type: "pty_error",
2306
- requestId: input.message.requestId,
2307
- ptyId: input.message.ptyId,
2308
- error: error.message
2309
- });
2310
- }
2311
- });
2312
- activePtys.set(input.message.ptyId, {
2313
- ...ptyProcess,
2314
- target: input.resolvedTarget.target
2315
- });
2316
- } catch (error) {
2317
- sendWorkerMessage(input.ws, {
2318
- type: "pty_error",
2319
- requestId: input.message.requestId,
2320
- ptyId: input.message.ptyId,
2321
- error: error instanceof Error ? error.message : String(error)
2322
- });
2257
+ if (activePtys.has(input.message.ptyId)) {
2258
+ throw new Error(`Shell ${input.message.ptyId} is already open`);
2323
2259
  }
2260
+ const planProcessEnv = !(input.resolvedTarget.target.type === "workspace" && input.resolvedTarget.target.rootProfile === "canonical_sync") ? preparePlanEnvForShell({
2261
+ target: input.resolvedTarget.target,
2262
+ planRoot: input.planRoot,
2263
+ activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2264
+ }) : {};
2265
+ const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
2266
+ const shell = resolveHostShell(input.message.command);
2267
+ const ptyProcess = createNodePtyBridge({
2268
+ file: shell.file,
2269
+ args: shell.args,
2270
+ ptyOptions: {
2271
+ name: "xterm-256color",
2272
+ cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
2273
+ rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
2274
+ cwd: input.resolvedTarget.rootPath,
2275
+ env: {
2276
+ ...process.env,
2277
+ ...githubProcessEnv(),
2278
+ ...input.message.env ?? {},
2279
+ ...planProcessEnv,
2280
+ ...targetProcessEnv
2281
+ }
2282
+ },
2283
+ onOpened: () => {
2284
+ sendWorkerMessage(input.ws, {
2285
+ type: "pty_opened",
2286
+ requestId: input.message.requestId,
2287
+ ptyId: input.message.ptyId
2288
+ });
2289
+ },
2290
+ onOutput: (data) => {
2291
+ sendWorkerMessage(input.ws, {
2292
+ type: "pty_output",
2293
+ ptyId: input.message.ptyId,
2294
+ data
2295
+ });
2296
+ },
2297
+ onExit: (event) => {
2298
+ input.releaseWorkspaceMutation();
2299
+ activePtys.delete(input.message.ptyId);
2300
+ sendWorkerMessage(input.ws, {
2301
+ type: "pty_exit",
2302
+ ptyId: input.message.ptyId,
2303
+ exitCode: event.exitCode,
2304
+ signal: event.signal
2305
+ });
2306
+ },
2307
+ onError: (error) => {
2308
+ input.releaseWorkspaceMutation();
2309
+ activePtys.delete(input.message.ptyId);
2310
+ sendWorkerMessage(input.ws, {
2311
+ type: "pty_error",
2312
+ requestId: input.message.requestId,
2313
+ ptyId: input.message.ptyId,
2314
+ error: error.message
2315
+ });
2316
+ }
2317
+ });
2318
+ activePtys.set(input.message.ptyId, {
2319
+ ...ptyProcess,
2320
+ target: input.resolvedTarget.target,
2321
+ releaseWorkspaceMutation: input.releaseWorkspaceMutation
2322
+ });
2324
2323
  }
2325
2324
  function writePty(ws, message) {
2326
2325
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2397,7 +2396,6 @@ async function startWorker(options) {
2397
2396
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2398
2397
  const manifestByProjectId = /* @__PURE__ */ new Map();
2399
2398
  const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2400
- const workspaceSyncSingleFlight = new import_workspace_sync.WorkspaceSyncSingleFlight();
2401
2399
  let workspaceRemoteUrl = null;
2402
2400
  let activeWorkspaceIncidentId = null;
2403
2401
  let previousPeriodicFingerprint = null;
@@ -2423,8 +2421,32 @@ async function startWorker(options) {
2423
2421
  ...overrides
2424
2422
  };
2425
2423
  };
2426
- const sendWorkspaceSyncResult = (requestId, result) => {
2427
- sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2424
+ const sendWorkspaceSyncResult = (authority, result) => {
2425
+ try {
2426
+ ws.send(JSON.stringify({ type: "workspace_sync_result", ...authority, result }));
2427
+ } catch (error) {
2428
+ process.stderr.write(
2429
+ `[r5d-worker] failed to send workspace_sync_result on its issuing connection: ${error instanceof Error ? error.message : String(error)}
2430
+ `
2431
+ );
2432
+ }
2433
+ };
2434
+ const requestWorkspaceSync = (trigger, fingerprint) => {
2435
+ try {
2436
+ ws.send(
2437
+ JSON.stringify({
2438
+ type: "workspace_sync_requested",
2439
+ observationId: crypto.randomUUID(),
2440
+ trigger,
2441
+ ...fingerprint ? { fingerprint } : {}
2442
+ })
2443
+ );
2444
+ } catch (error) {
2445
+ process.stderr.write(
2446
+ `[r5d-worker] failed to send workspace sync observation on its current connection: ${error instanceof Error ? error.message : String(error)}
2447
+ `
2448
+ );
2449
+ }
2428
2450
  };
2429
2451
  const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2430
2452
  const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
@@ -2475,7 +2497,7 @@ async function startWorker(options) {
2475
2497
  }
2476
2498
  return created;
2477
2499
  };
2478
- const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2500
+ const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}) => {
2479
2501
  workspaceSyncRequestsInFlight += 1;
2480
2502
  try {
2481
2503
  let pendingTargets = [];
@@ -2519,7 +2541,7 @@ async function startWorker(options) {
2519
2541
  }
2520
2542
  }
2521
2543
  previousPeriodicFingerprint = null;
2522
- sendWorkspaceSyncResult(requestId, result);
2544
+ sendWorkspaceSyncResult(authority, result);
2523
2545
  return result;
2524
2546
  } catch (error) {
2525
2547
  const result = {
@@ -2539,7 +2561,7 @@ async function startWorker(options) {
2539
2561
  error: error instanceof Error ? error.message : String(error)
2540
2562
  };
2541
2563
  previousPeriodicFingerprint = null;
2542
- sendWorkspaceSyncResult(requestId, result);
2564
+ sendWorkspaceSyncResult(authority, result);
2543
2565
  return result;
2544
2566
  } finally {
2545
2567
  workspaceSyncRequestsInFlight -= 1;
@@ -2551,6 +2573,7 @@ async function startWorker(options) {
2551
2573
  }
2552
2574
  });
2553
2575
  let lastServerHeartbeatAt = null;
2576
+ let heartbeatBusyGrace = null;
2554
2577
  let heartbeatTimedOut = false;
2555
2578
  let heartbeatWatchdog;
2556
2579
  const stopHeartbeatWatchdog = () => {
@@ -2562,7 +2585,7 @@ async function startWorker(options) {
2562
2585
  ws.addEventListener("open", () => {
2563
2586
  currentWorkerSocket = ws;
2564
2587
  heartbeatWatchdog = setInterval(() => {
2565
- if (heartbeatTimedOut || !(0, import_heartbeat.hasWorkerHeartbeatTimedOut)(lastServerHeartbeatAt)) {
2588
+ if (heartbeatTimedOut || !(0, import_heartbeat.hasWorkerHeartbeatTimedOutWithBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace)) {
2566
2589
  return;
2567
2590
  }
2568
2591
  heartbeatTimedOut = true;
@@ -2582,7 +2605,8 @@ async function startWorker(options) {
2582
2605
  capabilities: {
2583
2606
  updateClis: true,
2584
2607
  canonicalResolverCheckout: true,
2585
- browserPortForwarding: true
2608
+ browserPortForwarding: true,
2609
+ globalWorkspaceSyncLeaseV1: true
2586
2610
  },
2587
2611
  projectRoot: projectsRoot,
2588
2612
  artifactRoot,
@@ -2609,33 +2633,35 @@ async function startWorker(options) {
2609
2633
  return;
2610
2634
  }
2611
2635
  if (message.type === "workspace_manifest") {
2612
- configureGitHubAuth(message.githubCredential);
2613
- visibleGitIdentity = message.gitIdentity;
2614
- workspaceRemoteUrl = message.workspaceRemoteUrl;
2615
- const previousCheckoutKeys = new Set(
2616
- allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2617
- );
2618
- const migratedProjectIds = (0, import_managed_paths.migrateLegacyProjectRoots)(projectsRoot, message.projects);
2619
- manifestByProjectId.clear();
2620
- for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2621
- const currentCheckouts = allManifestCheckouts();
2622
- const currentCheckoutKeys = new Set(
2623
- currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2624
- );
2625
- for (const key of pendingManifestCheckouts.keys()) {
2626
- if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2627
- }
2628
- for (const checkout of currentCheckouts) {
2629
- const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2630
- if (previousCheckoutKeys.has(key)) continue;
2631
- const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2632
- if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2633
- }
2634
- previousPeriodicFingerprint = null;
2635
- process.stdout.write(
2636
- `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2636
+ await workspaceSyncSingleFlight.runMutation(() => {
2637
+ configureGitHubAuth(message.githubCredential);
2638
+ visibleGitIdentity = message.gitIdentity;
2639
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2640
+ const previousCheckoutKeys = new Set(
2641
+ allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2642
+ );
2643
+ const migratedProjectIds = (0, import_managed_paths.migrateLegacyProjectRoots)(projectsRoot, message.projects);
2644
+ manifestByProjectId.clear();
2645
+ for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2646
+ const currentCheckouts = allManifestCheckouts();
2647
+ const currentCheckoutKeys = new Set(
2648
+ currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2649
+ );
2650
+ for (const key of pendingManifestCheckouts.keys()) {
2651
+ if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2652
+ }
2653
+ for (const checkout of currentCheckouts) {
2654
+ const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2655
+ if (previousCheckoutKeys.has(key)) continue;
2656
+ const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2657
+ if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2658
+ }
2659
+ previousPeriodicFingerprint = null;
2660
+ process.stdout.write(
2661
+ `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2637
2662
  `
2638
- );
2663
+ );
2664
+ });
2639
2665
  if (!periodicWorkspaceScan) {
2640
2666
  const emptyFingerprint = (0, import_node_crypto.createHash)("sha256").update("").digest("hex");
2641
2667
  periodicWorkspaceScan = setInterval(() => {
@@ -2652,7 +2678,7 @@ async function startWorker(options) {
2652
2678
  );
2653
2679
  if (hasPendingGenericCheckout) {
2654
2680
  previousPeriodicFingerprint = null;
2655
- await runRequestedWorkspaceSync(crypto.randomUUID(), {
2681
+ requestWorkspaceSync({
2656
2682
  type: "periodic",
2657
2683
  detail: "workspace manifest additions"
2658
2684
  });
@@ -2668,7 +2694,7 @@ async function startWorker(options) {
2668
2694
  return;
2669
2695
  }
2670
2696
  previousPeriodicFingerprint = null;
2671
- await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "periodic" });
2697
+ requestWorkspaceSync({ type: "periodic" }, fingerprint);
2672
2698
  })().catch((error) => {
2673
2699
  process.stderr.write(
2674
2700
  `[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
@@ -2676,6 +2702,7 @@ async function startWorker(options) {
2676
2702
  );
2677
2703
  }).finally(() => {
2678
2704
  periodicWorkspaceScanInFlight = false;
2705
+ heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
2679
2706
  });
2680
2707
  }, import_workspace_sync.WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
2681
2708
  periodicWorkspaceScan.unref();
@@ -2683,23 +2710,23 @@ async function startWorker(options) {
2683
2710
  return;
2684
2711
  }
2685
2712
  if (message.type === "sync_workspace") {
2686
- await runRequestedWorkspaceSync(message.requestId, message.trigger, {
2687
- attemptId: message.attemptId,
2688
- confirmedLargeDiff: message.confirmedLargeDiff,
2689
- confirmationReason: message.confirmationReason,
2690
- resetToCanonical: message.resetToCanonical,
2691
- skipVisibleMirror: message.skipVisibleMirror
2692
- });
2693
- return;
2694
- }
2695
- if (message.type === "workspace_head_updated") {
2696
- if (!activeWorkspaceIncidentId) {
2697
- await runRequestedWorkspaceSync(
2698
- crypto.randomUUID(),
2699
- { type: "inbound_head", detail: message.commitHash },
2700
- { skipVisibleMirror: true }
2701
- );
2702
- }
2713
+ await runRequestedWorkspaceSync(
2714
+ {
2715
+ requestId: message.requestId,
2716
+ intentId: message.intentId,
2717
+ leaseEpoch: message.leaseEpoch,
2718
+ leaseToken: message.leaseToken
2719
+ },
2720
+ message.trigger,
2721
+ {
2722
+ attemptId: message.attemptId,
2723
+ quarantineRef: message.quarantineRef,
2724
+ confirmedLargeDiff: message.confirmedLargeDiff,
2725
+ confirmationReason: message.confirmationReason,
2726
+ resetToCanonical: message.resetToCanonical,
2727
+ skipVisibleMirror: message.skipVisibleMirror
2728
+ }
2729
+ );
2703
2730
  return;
2704
2731
  }
2705
2732
  if (message.type === "sync_session_artifacts") {
@@ -2729,8 +2756,17 @@ async function startWorker(options) {
2729
2756
  return;
2730
2757
  }
2731
2758
  if (message.type === "workspace_incident_updated") {
2732
- activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
2759
+ const previousIncidentId = activeWorkspaceIncidentId;
2760
+ activeWorkspaceIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
2733
2761
  previousPeriodicFingerprint = null;
2762
+ const releasedHead = (0, import_workspace_incident_state.releasePendingWorkspaceHead)(
2763
+ previousIncidentId,
2764
+ activeWorkspaceIncidentId,
2765
+ message.status === "resolved" || message.status === "confirmed" || message.status === "reset" ? message.incidentId : null
2766
+ );
2767
+ if (releasedHead.syncHead) {
2768
+ requestWorkspaceSync({ type: "inbound_head", detail: releasedHead.syncHead });
2769
+ }
2734
2770
  return;
2735
2771
  }
2736
2772
  if (message.type === "exec_terminal_ack") {
@@ -2787,6 +2823,7 @@ async function startWorker(options) {
2787
2823
  }
2788
2824
  if (message.type === "ping") {
2789
2825
  lastServerHeartbeatAt = Date.now();
2826
+ heartbeatBusyGrace = null;
2790
2827
  ws.send(JSON.stringify({ type: "pong" }));
2791
2828
  return;
2792
2829
  }
@@ -2827,11 +2864,14 @@ async function startWorker(options) {
2827
2864
  });
2828
2865
  return;
2829
2866
  }
2867
+ const releaseWorkspaceMutation = await workspaceSyncSingleFlight.acquireMutation();
2868
+ let leaseTransferred = false;
2830
2869
  try {
2831
2870
  const resolvedTarget = resolveMessageTarget(message.target);
2832
2871
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
2833
2872
  `);
2834
- await openPty({ ws, message, resolvedTarget, planRoot });
2873
+ await openPty({ ws, message, resolvedTarget, planRoot, releaseWorkspaceMutation });
2874
+ leaseTransferred = true;
2835
2875
  } catch (error) {
2836
2876
  sendWorkerMessage(ws, {
2837
2877
  type: "pty_error",
@@ -2839,6 +2879,8 @@ async function startWorker(options) {
2839
2879
  ptyId: message.ptyId,
2840
2880
  error: error instanceof Error ? error.message : String(error)
2841
2881
  });
2882
+ } finally {
2883
+ if (!leaseTransferred) releaseWorkspaceMutation();
2842
2884
  }
2843
2885
  return;
2844
2886
  }
@@ -2867,10 +2909,12 @@ async function startWorker(options) {
2867
2909
  }
2868
2910
  let result;
2869
2911
  try {
2870
- const resolvedTarget = resolveMessageTarget(message.target);
2871
- process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
2912
+ result = await workspaceSyncSingleFlight.runMutation(async () => {
2913
+ const resolvedTarget = resolveMessageTarget(message.target);
2914
+ process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
2872
2915
  `);
2873
- result = await executeCommand({ message, resolvedTarget, baseUrl, token, artifactRoot, planRoot });
2916
+ return await executeCommand({ message, resolvedTarget, baseUrl, token, artifactRoot, planRoot });
2917
+ });
2874
2918
  } catch (error) {
2875
2919
  result = {
2876
2920
  type: "exec_result",
@@ -2899,11 +2943,11 @@ async function startWorker(options) {
2899
2943
  requestId: message.requestId,
2900
2944
  runId: message.runId
2901
2945
  });
2902
- try {
2946
+ void workspaceSyncSingleFlight.runMutation(async () => {
2903
2947
  const resolvedTarget = resolveMessageTarget(message.target);
2904
2948
  process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
2905
2949
  `);
2906
- void executeStreamingCommand({
2950
+ await executeStreamingCommand({
2907
2951
  ws,
2908
2952
  message,
2909
2953
  resolvedTarget,
@@ -2911,34 +2955,29 @@ async function startWorker(options) {
2911
2955
  token,
2912
2956
  artifactRoot,
2913
2957
  planRoot
2914
- }).catch((error) => {
2915
- sendWorkerMessage(ws, {
2916
- type: "exec_start_error",
2917
- requestId: message.requestId,
2918
- runId: message.runId,
2919
- error: error instanceof Error ? error.message : String(error)
2920
- });
2921
2958
  });
2922
- } catch (error) {
2959
+ }).catch((error) => {
2923
2960
  sendWorkerMessage(ws, {
2924
2961
  type: "exec_start_error",
2925
2962
  requestId: message.requestId,
2926
2963
  runId: message.runId,
2927
2964
  error: error instanceof Error ? error.message : String(error)
2928
2965
  });
2929
- }
2966
+ });
2930
2967
  return;
2931
2968
  }
2932
2969
  if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
2933
2970
  try {
2934
- const resolvedTarget = resolveMessageTarget(message.target);
2935
- const result = await executeOperation({
2936
- message,
2937
- resolvedTarget,
2938
- baseUrl,
2939
- token,
2940
- artifactRoot,
2941
- planRoot
2971
+ const result = await workspaceSyncSingleFlight.runMutation(async () => {
2972
+ const resolvedTarget = resolveMessageTarget(message.target);
2973
+ return await executeOperation({
2974
+ message,
2975
+ resolvedTarget,
2976
+ baseUrl,
2977
+ token,
2978
+ artifactRoot,
2979
+ planRoot
2980
+ });
2942
2981
  });
2943
2982
  ws.send(
2944
2983
  JSON.stringify({
@@ -2971,6 +3010,8 @@ async function startWorker(options) {
2971
3010
  })().catch((error) => {
2972
3011
  process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
2973
3012
  `);
3013
+ }).finally(() => {
3014
+ heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
2974
3015
  });
2975
3016
  });
2976
3017
  ws.addEventListener("close", (event) => {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.51",
3
+ "version": "0.0.54",
4
4
  "type": "commonjs"
5
5
  }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var workspace_incident_state_exports = {};
20
+ __export(workspace_incident_state_exports, {
21
+ applyWorkspaceIncidentUpdate: () => applyWorkspaceIncidentUpdate,
22
+ releasePendingWorkspaceHead: () => releasePendingWorkspaceHead
23
+ });
24
+ module.exports = __toCommonJS(workspace_incident_state_exports);
25
+ function applyWorkspaceIncidentUpdate(currentIncidentId, update) {
26
+ if (update.status === "remediating" || update.status === "waiting_for_worker") return update.incidentId;
27
+ if (currentIncidentId && update.incidentId && update.incidentId !== currentIncidentId) return currentIncidentId;
28
+ return null;
29
+ }
30
+ function releasePendingWorkspaceHead(previousIncidentId, currentIncidentId, terminalIncidentId) {
31
+ if (!previousIncidentId || currentIncidentId) return { syncHead: null };
32
+ if (terminalIncidentId === previousIncidentId) return { syncHead: "authoritative" };
33
+ return { syncHead: null };
34
+ }
35
+ // Annotate the CommonJS export names for ESM import in node:
36
+ 0 && (module.exports = {
37
+ applyWorkspaceIncidentUpdate,
38
+ releasePendingWorkspaceHead
39
+ });