@ricsam/r5d-worker 0.0.58 → 0.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/main.cjs CHANGED
@@ -65,14 +65,12 @@ var import_process_tree = require("./process-tree.cjs");
65
65
  var import_port_forward_client = require("./port-forward-client.cjs");
66
66
  var import_workspace_incident_state = require("./workspace-incident-state.cjs");
67
67
  var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
68
- var import_workspace_sync_fast_path = require("./workspace-sync-fast-path.cjs");
68
+ var import_workspace_convergence = require("./workspace-convergence.cjs");
69
+ var import_workspace_manifest_admission = require("./workspace-manifest-admission.cjs");
69
70
  var import_supervisor = require("./supervisor.cjs");
70
71
  var import_managed_paths = require("./managed-paths.cjs");
71
72
  var import_workspace_sync = require("./workspace-sync.cjs");
72
73
  const import_meta = {};
73
- function workspaceSyncFastPathTelemetry(decision) {
74
- return decision.hit ? { hit: true, reason: decision.reason, proofAgeMs: decision.proofAgeMs } : { hit: false, reason: decision.reason };
75
- }
76
74
  class WorkerServerUnavailableError extends Error {
77
75
  name = "WorkerServerUnavailableError";
78
76
  }
@@ -809,25 +807,13 @@ function fallbackInternalRemoteUrl(baseUrl, projectId) {
809
807
  }
810
808
  function visibleCheckoutRemoteFor(input) {
811
809
  const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
812
- const isCanonicalManifestBranch = Boolean(canonicalCheckout);
813
- const usesCanonicalRemote = isCanonicalManifestBranch || !input.manifest?.repoHttpUrl;
814
- if (usesCanonicalRemote) {
815
- return {
816
- remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
817
- authHeader: `Authorization: Bearer ${input.token}`,
818
- persistAuth: false,
819
- reconcileExistingOrigin: isCanonicalManifestBranch,
820
- requireRemoteBranch: isCanonicalManifestBranch,
821
- requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
822
- };
823
- }
824
810
  return {
825
- remoteUrl: input.manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
826
- authHeader: input.manifest?.repoAuthHeader ?? null,
827
- persistAuth: true,
828
- reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl),
829
- requireRemoteBranch: false,
830
- requiredBaseCommit: null
811
+ remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
812
+ authHeader: `Authorization: Bearer ${input.token}`,
813
+ persistAuth: false,
814
+ reconcileExistingOrigin: true,
815
+ requireRemoteBranch: true,
816
+ requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
831
817
  };
832
818
  }
833
819
  function gitExtraHeaderUrlForRemote(remoteUrl) {
@@ -1048,9 +1034,11 @@ function checkoutVisibleBranch(input) {
1048
1034
  }
1049
1035
  function migrateExistingCheckoutToRequiredRemote(input) {
1050
1036
  if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
1051
- if (hasWorktreeChanges(input.branchPath)) {
1037
+ const dirtyWorkingTree = hasWorktreeChanges(input.branchPath);
1038
+ if (dirtyWorkingTree && input.requiredBaseCommit) {
1052
1039
  throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
1053
1040
  }
1041
+ const preserveWorkingTree = dirtyWorkingTree;
1054
1042
  runGit(
1055
1043
  visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1056
1044
  {
@@ -1065,11 +1053,12 @@ function migrateExistingCheckoutToRequiredRemote(input) {
1065
1053
  if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
1066
1054
  throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1067
1055
  }
1056
+ ensureOriginRemote(input.branchPath, input.remoteUrl);
1057
+ if (preserveWorkingTree) return;
1068
1058
  if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
1069
1059
  const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1070
1060
  runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1071
1061
  }
1072
- ensureOriginRemote(input.branchPath, input.remoteUrl);
1073
1062
  runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1074
1063
  cwd: input.branchPath
1075
1064
  });
@@ -1162,7 +1151,7 @@ function ensureVisibleGitCheckout(input) {
1162
1151
  (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1163
1152
  return branchPath;
1164
1153
  }
1165
- function ensureBranchWorkspace(input) {
1154
+ function ensureBranchWorktree(input) {
1166
1155
  const defaultBranch = input.manifest?.defaultBranch || "main";
1167
1156
  const remote = visibleCheckoutRemoteFor(input);
1168
1157
  return {
@@ -1195,7 +1184,7 @@ function resolveWorkerSessionTarget(input) {
1195
1184
  }
1196
1185
  const manifest = input.manifestByProjectId.get(input.target.projectId);
1197
1186
  const projectRoot = projectRootFor(input.projectsRoot, input.target.projectId, input.manifestByProjectId);
1198
- const workspace = ensureBranchWorkspace({
1187
+ const worktree = ensureBranchWorktree({
1199
1188
  projectId: input.target.projectId,
1200
1189
  baseUrl: input.baseUrl,
1201
1190
  token: input.token,
@@ -1204,7 +1193,7 @@ function resolveWorkerSessionTarget(input) {
1204
1193
  branchName: input.target.branchName,
1205
1194
  manifest
1206
1195
  });
1207
- return { target: input.target, rootPath: workspace.branchPath, manifest };
1196
+ return { target: input.target, rootPath: worktree.branchPath, manifest };
1208
1197
  }
1209
1198
  function workspaceSyncCheckoutTargets(input) {
1210
1199
  const selected = /* @__PURE__ */ new Map();
@@ -2302,6 +2291,7 @@ async function openPty(input) {
2302
2291
  onExit: (event) => {
2303
2292
  input.releaseWorkspaceMutation?.();
2304
2293
  activePtys.delete(input.message.ptyId);
2294
+ input.onTerminal?.();
2305
2295
  sendWorkerMessage(input.ws, {
2306
2296
  type: "pty_exit",
2307
2297
  ptyId: input.message.ptyId,
@@ -2312,6 +2302,7 @@ async function openPty(input) {
2312
2302
  onError: (error) => {
2313
2303
  input.releaseWorkspaceMutation?.();
2314
2304
  activePtys.delete(input.message.ptyId);
2305
+ input.onTerminal?.();
2315
2306
  sendWorkerMessage(input.ws, {
2316
2307
  type: "pty_error",
2317
2308
  requestId: input.message.requestId,
@@ -2403,15 +2394,25 @@ async function startWorker(options) {
2403
2394
  const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2404
2395
  let workspaceRemoteUrl = null;
2405
2396
  let activeWorkspaceIncidentId = null;
2406
- let previousPeriodicFingerprint = null;
2407
- let periodicWorkspaceScan;
2408
- let periodicWorkspaceScanInFlight = false;
2397
+ const workspaceIncidentOrdering = new import_workspace_incident_state.WorkspaceIncidentOrderingFence();
2398
+ const workspaceConvergence = (0, import_workspace_convergence.processWorkspaceConvergenceState)(import_node_path.default.join(syncRoot, "workspace-convergence.json"));
2399
+ const workspaceManifestRevision = new import_workspace_convergence.WorkspaceManifestRevisionFence();
2400
+ let workspaceReady = false;
2401
+ let workspacePublicationTimer;
2402
+ let workspaceSafetyScan;
2403
+ let workspaceManifestHydrationTimer;
2404
+ let workspaceManifestHydrationInFlight = false;
2405
+ let workspaceSafetyScanInFlight = false;
2406
+ let workspaceHeadConvergenceInFlight = false;
2407
+ let pendingWorkspaceHead;
2408
+ let pendingPublication;
2409
+ const explicitWorkspacePublications = new import_workspace_convergence.ExplicitWorkspacePublicationQueue();
2409
2410
  let terminalReplayTimer;
2410
2411
  let workspaceSyncRequestsInFlight = 0;
2411
2412
  let sessionArtifactSyncRequestsInFlight = 0;
2412
2413
  let cliUpdateInProgress = false;
2413
2414
  let reloadAfterClose = false;
2414
- const recentWorkspaceSyncProof = new import_workspace_sync_fast_path.RecentWorkspaceSyncProofCache();
2415
+ let shutdownAfterClose = false;
2415
2416
  const workspaceSyncInput = (trigger, overrides = {}) => {
2416
2417
  if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2417
2418
  const projects = (0, import_workspace_sync.workspaceProjectsForSync)([...manifestByProjectId.values()], trigger);
@@ -2437,21 +2438,160 @@ async function startWorker(options) {
2437
2438
  );
2438
2439
  }
2439
2440
  };
2440
- const requestWorkspaceSync = (trigger, fingerprint) => {
2441
+ const sendWorkspaceReady = () => {
2442
+ const manifestRevision = workspaceManifestRevision.readyRevision();
2443
+ if (!workspaceReady || !manifestRevision) return;
2441
2444
  try {
2442
2445
  ws.send(
2443
2446
  JSON.stringify({
2444
- type: "workspace_sync_requested",
2445
- observationId: crypto.randomUUID(),
2447
+ type: "workspace_ready",
2448
+ manifestRevision,
2449
+ pendingCheckouts: [...pendingManifestCheckouts.values()].sort(
2450
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
2451
+ ),
2452
+ ...workspaceConvergence.snapshot()
2453
+ })
2454
+ );
2455
+ } catch (error) {
2456
+ process.stderr.write(`[r5d-worker] failed to send workspace readiness: ${error instanceof Error ? error.message : String(error)}
2457
+ `);
2458
+ }
2459
+ };
2460
+ const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
2461
+ const armWorkspacePublication = (trigger, options2 = { delayMs: import_workspace_convergence.WORKSPACE_PUBLICATION_QUIET_MS }) => {
2462
+ workspaceConvergence.schedule();
2463
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2464
+ sendWorkspaceReady();
2465
+ if (options2.requestId && !explicitWorkspacePublications.schedule({ requestId: options2.requestId, trigger }, pendingPublication?.requestId)) {
2466
+ return;
2467
+ }
2468
+ if (!options2.requestId && explicitWorkspacePublications.hasScheduled()) return;
2469
+ if (workspacePublicationTimer) clearTimeout(workspacePublicationTimer);
2470
+ workspacePublicationTimer = setTimeout(
2471
+ () => {
2472
+ workspacePublicationTimer = void 0;
2473
+ if (options2.requestId) explicitWorkspacePublications.consumeScheduled(options2.requestId);
2474
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2475
+ if (!workspaceReady || !workspaceRemoteUrl) {
2476
+ armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2477
+ return;
2478
+ }
2479
+ if (activeWorkspaceIncidentId) {
2480
+ workspaceConvergence.block();
2481
+ sendWorkspaceReady();
2482
+ if ((0, import_workspace_convergence.workspacePublicationIncidentAction)(options2.requestId) === "defer_automatic") return;
2483
+ if (pendingPublication) {
2484
+ explicitWorkspacePublications.enqueue({ requestId: options2.requestId, trigger }, pendingPublication.requestId);
2485
+ return;
2486
+ }
2487
+ const snapshot2 = workspaceConvergence.snapshot();
2488
+ pendingPublication = {
2489
+ requestId: options2.requestId,
2490
+ trigger,
2491
+ dirtyGeneration: snapshot2.dirtyGeneration,
2492
+ explicit: true,
2493
+ workInFlight: false,
2494
+ requestedAt: Date.now()
2495
+ };
2496
+ try {
2497
+ ws.send(
2498
+ JSON.stringify({
2499
+ type: "workspace_publication_request",
2500
+ requestId: options2.requestId,
2501
+ trigger,
2502
+ dirtyGeneration: snapshot2.dirtyGeneration,
2503
+ localHead: snapshot2.localHead,
2504
+ desiredCanonicalHead: snapshot2.desiredCanonicalHead
2505
+ })
2506
+ );
2507
+ } catch (error) {
2508
+ pendingPublication = void 0;
2509
+ workspaceConvergence.defer();
2510
+ process.stderr.write(
2511
+ `[r5d-worker] failed to request blocked workspace publication authority: ${error instanceof Error ? error.message : String(error)}
2512
+ `
2513
+ );
2514
+ armWorkspacePublication(trigger, { delayMs: 1e3, requestId: options2.requestId });
2515
+ }
2516
+ return;
2517
+ }
2518
+ if (pendingPublication) {
2519
+ armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2520
+ return;
2521
+ }
2522
+ const snapshot = workspaceConvergence.snapshot();
2523
+ const requestId = options2.requestId ?? crypto.randomUUID();
2524
+ pendingPublication = {
2525
+ requestId,
2446
2526
  trigger,
2447
- ...fingerprint ? { fingerprint } : {}
2527
+ dirtyGeneration: snapshot.dirtyGeneration,
2528
+ explicit: options2.requestId !== void 0,
2529
+ workInFlight: false,
2530
+ requestedAt: Date.now()
2531
+ };
2532
+ try {
2533
+ ws.send(
2534
+ JSON.stringify({
2535
+ type: "workspace_publication_request",
2536
+ requestId,
2537
+ trigger,
2538
+ dirtyGeneration: snapshot.dirtyGeneration,
2539
+ localHead: snapshot.localHead,
2540
+ desiredCanonicalHead: snapshot.desiredCanonicalHead
2541
+ })
2542
+ );
2543
+ } catch (error) {
2544
+ pendingPublication = void 0;
2545
+ workspaceConvergence.defer();
2546
+ process.stderr.write(
2547
+ `[r5d-worker] failed to request workspace publication: ${error instanceof Error ? error.message : String(error)}
2548
+ `
2549
+ );
2550
+ armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2551
+ }
2552
+ },
2553
+ Math.max(0, options2.delayMs)
2554
+ );
2555
+ workspacePublicationTimer.unref();
2556
+ };
2557
+ const markWorkspaceDirty = (trigger, force = false) => {
2558
+ workspaceConvergence.markDirty();
2559
+ armWorkspacePublication(trigger, { delayMs: force ? 0 : import_workspace_convergence.WORKSPACE_PUBLICATION_QUIET_MS });
2560
+ };
2561
+ const convergeAvailableWorkspaceHead = async () => {
2562
+ if (pendingWorkspaceHead === void 0 || !workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || workspaceHeadConvergenceInFlight) {
2563
+ return;
2564
+ }
2565
+ const advertisedHead = pendingWorkspaceHead;
2566
+ pendingWorkspaceHead = void 0;
2567
+ workspaceConvergence.observeCanonicalHead(advertisedHead);
2568
+ workspaceHeadConvergenceInFlight = true;
2569
+ try {
2570
+ const snapshot = workspaceConvergence.snapshot();
2571
+ const hydrateVisible = !pendingPublication && snapshot.dirtyGeneration === snapshot.publishedGeneration;
2572
+ const converged = await workspaceSyncSingleFlight.runExclusive(
2573
+ () => (0, import_workspace_sync.convergeWorkspaceHead)(workspaceSyncInput({ type: "inbound_head", detail: advertisedHead ?? "unborn" }), {
2574
+ hydrateVisible
2448
2575
  })
2449
2576
  );
2577
+ workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
2578
+ if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
2579
+ if (hydrateVisible && converged.localChanges) {
2580
+ markWorkspaceDirty({ type: "periodic", detail: "inbound overlap retained for next publication" });
2581
+ }
2582
+ sendWorkspaceReady();
2450
2583
  } catch (error) {
2584
+ pendingWorkspaceHead = advertisedHead;
2451
2585
  process.stderr.write(
2452
- `[r5d-worker] failed to send workspace sync observation on its current connection: ${error instanceof Error ? error.message : String(error)}
2586
+ `[r5d-worker] failed to fetch available workspace head: ${error instanceof Error ? error.message : String(error)}
2453
2587
  `
2454
2588
  );
2589
+ } finally {
2590
+ workspaceHeadConvergenceInFlight = false;
2591
+ heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
2592
+ if (pendingWorkspaceHead !== void 0) {
2593
+ setTimeout(() => void convergeAvailableWorkspaceHead(), 1e3).unref();
2594
+ }
2455
2595
  }
2456
2596
  };
2457
2597
  const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
@@ -2467,7 +2607,7 @@ async function startWorker(options) {
2467
2607
  workspaceShadowRoot,
2468
2608
  manifestByProjectId
2469
2609
  });
2470
- const ensureVisibleWorkspaceCheckouts = (targets, options2 = {}) => {
2610
+ const ensureVisibleWorktrees = (targets, options2 = {}) => {
2471
2611
  const created = [];
2472
2612
  for (const target of targets) {
2473
2613
  const manifest = manifestByProjectId.get(target.projectId);
@@ -2496,20 +2636,62 @@ async function startWorker(options) {
2496
2636
  reconcileExistingOrigin: remote.reconcileExistingOrigin,
2497
2637
  requireRemoteBranch: remote.requireRemoteBranch,
2498
2638
  requiredBaseCommit: remote.requiredBaseCommit,
2499
- allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requireRemoteBranch),
2639
+ allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requiredBaseCommit),
2500
2640
  clearPersistentAuth: !remote.persistAuth
2501
2641
  });
2502
2642
  if (!existed) created.push(target);
2503
2643
  }
2504
2644
  return created;
2505
2645
  };
2506
- const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}, authoritativeCanonicalHead, allowRecentNoChangeFastPath) => {
2646
+ const schedulePendingManifestCheckoutHydration = (delayMs = 0) => {
2647
+ if (workspaceManifestHydrationTimer || workspaceManifestHydrationInFlight || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2648
+ return;
2649
+ }
2650
+ workspaceManifestHydrationTimer = setTimeout(
2651
+ () => {
2652
+ workspaceManifestHydrationTimer = void 0;
2653
+ if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2654
+ if (pendingManifestCheckouts.size > 0 && currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) {
2655
+ schedulePendingManifestCheckoutHydration(1e3);
2656
+ }
2657
+ return;
2658
+ }
2659
+ workspaceManifestHydrationInFlight = true;
2660
+ void workspaceSyncSingleFlight.runExclusive(async () => {
2661
+ if (!workspaceReady || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2662
+ return;
2663
+ }
2664
+ const targets = [...pendingManifestCheckouts.values()];
2665
+ ensureVisibleWorktrees(targets, { strictCanonicalRevalidation: true });
2666
+ for (const target of targets) {
2667
+ const manifest = manifestByProjectId.get(target.projectId);
2668
+ if (!manifest || !manifest.branches.includes(target.branchName)) continue;
2669
+ const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, target.projectId, manifestByProjectId), target.branchName);
2670
+ if (hasNormalVisibleGitDir(branchPath)) {
2671
+ pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
2672
+ }
2673
+ }
2674
+ }).then(() => sendWorkspaceReady()).catch((error) => {
2675
+ process.stderr.write(
2676
+ `[r5d-worker] pending manifest checkout hydration failed: ${error instanceof Error ? error.message : String(error)}
2677
+ `
2678
+ );
2679
+ }).finally(() => {
2680
+ workspaceManifestHydrationInFlight = false;
2681
+ heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
2682
+ if (pendingManifestCheckouts.size > 0) schedulePendingManifestCheckoutHydration(1e3);
2683
+ });
2684
+ },
2685
+ Math.max(0, delayMs)
2686
+ );
2687
+ workspaceManifestHydrationTimer.unref();
2688
+ };
2689
+ const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}) => {
2507
2690
  const requestedAt = Date.now();
2508
2691
  let queueEnteredAt = requestedAt;
2509
2692
  let prepareStartedAt = requestedAt;
2510
2693
  let synchronizeStartedAt = requestedAt;
2511
2694
  let synchronizeFinishedAt = requestedAt;
2512
- let fastPathDecision = { hit: false, reason: "no_recent_proof" };
2513
2695
  workspaceSyncRequestsInFlight += 1;
2514
2696
  try {
2515
2697
  let pendingTargets = [];
@@ -2518,48 +2700,12 @@ async function startWorker(options) {
2518
2700
  const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2519
2701
  const syncProjects = (0, import_workspace_sync.workspaceProjectsForSync)([...manifestByProjectId.values()], trigger);
2520
2702
  if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2521
- const scopeKey = (0, import_workspace_sync_fast_path.workspaceSyncScopeKey)(workspaceRemoteUrl, syncProjects);
2522
2703
  const allowedCheckoutKeys = new Set(
2523
2704
  syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
2524
2705
  );
2525
2706
  const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
2526
2707
  (target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2527
2708
  );
2528
- const workerBusy = activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress;
2529
- fastPathDecision = recentWorkspaceSyncProof.evaluate({
2530
- trigger,
2531
- allowRecentNoChangeFastPath,
2532
- canonicalHead: authoritativeCanonicalHead,
2533
- scopeKey,
2534
- nowMs: Date.now(),
2535
- skipVisibleMirror: overrides.skipVisibleMirror,
2536
- resetToCanonical: overrides.resetToCanonical,
2537
- incidentActive: activeWorkspaceIncidentId !== null,
2538
- workerBusy,
2539
- pendingCheckouts: scopedPendingTargets.length > 0 || Boolean(overrides.newVisibleCheckouts?.length)
2540
- });
2541
- if (fastPathDecision.hit) {
2542
- prepareStartedAt = queueEnteredAt;
2543
- synchronizeStartedAt = queueEnteredAt;
2544
- synchronizeFinishedAt = queueEnteredAt;
2545
- return {
2546
- type: "workspace_sync",
2547
- attemptId: overrides.attemptId ?? crypto.randomUUID(),
2548
- workerLabel: label,
2549
- trigger,
2550
- outcome: "no_change",
2551
- startingHead: fastPathDecision.canonicalHead,
2552
- expectedHead: fastPathDecision.canonicalHead,
2553
- publishedHead: fastPathDecision.canonicalHead,
2554
- rebaseCount: 0,
2555
- diffSizeBytes: 0,
2556
- gitStatus: "",
2557
- affectedProjects: [],
2558
- affectedPaths: [],
2559
- discardedPaths: [],
2560
- localChangesDiscarded: false
2561
- };
2562
- }
2563
2709
  prepareStartedAt = Date.now();
2564
2710
  let checkoutTargets = [];
2565
2711
  if (shouldEnsureCheckouts) {
@@ -2571,7 +2717,7 @@ async function startWorker(options) {
2571
2717
  });
2572
2718
  }
2573
2719
  pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
2574
- const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets, {
2720
+ const createdCheckouts = ensureVisibleWorktrees(checkoutTargets, {
2575
2721
  strictCanonicalRevalidation: true
2576
2722
  });
2577
2723
  const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
@@ -2585,24 +2731,12 @@ async function startWorker(options) {
2585
2731
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2586
2732
  });
2587
2733
  synchronizeStartedAt = Date.now();
2588
- recentWorkspaceSyncProof.invalidate();
2589
- const observationGeneration = recentWorkspaceSyncProof.beginObservation();
2590
- const workerWasBusyDuringObservation = workerBusy;
2591
2734
  let synchronized;
2592
2735
  try {
2593
2736
  synchronized = await (0, import_workspace_sync.synchronizeWorkspace)(input);
2594
2737
  } finally {
2595
2738
  synchronizeFinishedAt = Date.now();
2596
2739
  }
2597
- if ((synchronized.outcome === "no_change" || synchronized.outcome === "updated") && pendingTargets.length === 0) {
2598
- recentWorkspaceSyncProof.recordStable({
2599
- observationGeneration,
2600
- canonicalHead: synchronized.publishedHead ?? null,
2601
- scopeKey,
2602
- observedAtMs: synchronizeFinishedAt,
2603
- workerBusy: workerWasBusyDuringObservation || activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress || activeWorkspaceIncidentId !== null
2604
- });
2605
- }
2606
2740
  return synchronized;
2607
2741
  });
2608
2742
  const completedAt = Date.now();
@@ -2612,8 +2746,7 @@ async function startWorker(options) {
2612
2746
  totalMs: completedAt - requestedAt,
2613
2747
  queueMs: queueEnteredAt - requestedAt,
2614
2748
  prepareMs: synchronizeStartedAt - prepareStartedAt,
2615
- synchronizeMs: synchronizeFinishedAt - synchronizeStartedAt,
2616
- fastPath: workspaceSyncFastPathTelemetry(fastPathDecision)
2749
+ synchronizeMs: synchronizeFinishedAt - synchronizeStartedAt
2617
2750
  }
2618
2751
  };
2619
2752
  if (resultWithTelemetry.outcome === "no_change" || resultWithTelemetry.outcome === "published" || resultWithTelemetry.outcome === "updated" || resultWithTelemetry.outcome === "conflict_reset") {
@@ -2622,7 +2755,6 @@ async function startWorker(options) {
2622
2755
  if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
2623
2756
  }
2624
2757
  }
2625
- previousPeriodicFingerprint = null;
2626
2758
  sendWorkspaceSyncResult(authority, resultWithTelemetry);
2627
2759
  return resultWithTelemetry;
2628
2760
  } catch (error) {
@@ -2645,12 +2777,10 @@ async function startWorker(options) {
2645
2777
  totalMs: completedAt - requestedAt,
2646
2778
  queueMs: queueEnteredAt - requestedAt,
2647
2779
  prepareMs: Math.max(0, synchronizeStartedAt - prepareStartedAt),
2648
- synchronizeMs: Math.max(0, synchronizeFinishedAt - synchronizeStartedAt),
2649
- fastPath: workspaceSyncFastPathTelemetry(fastPathDecision)
2780
+ synchronizeMs: Math.max(0, synchronizeFinishedAt - synchronizeStartedAt)
2650
2781
  },
2651
2782
  error: error instanceof Error ? error.message : String(error)
2652
2783
  };
2653
- previousPeriodicFingerprint = null;
2654
2784
  sendWorkspaceSyncResult(authority, result);
2655
2785
  return result;
2656
2786
  } finally {
@@ -2666,6 +2796,24 @@ async function startWorker(options) {
2666
2796
  let heartbeatBusyGrace = null;
2667
2797
  let heartbeatTimedOut = false;
2668
2798
  let heartbeatWatchdog;
2799
+ const handleGracefulShutdown = () => {
2800
+ if (shutdownAfterClose) return;
2801
+ shutdownAfterClose = true;
2802
+ void (0, import_workspace_convergence.waitForWorkspacePublicationOnShutdown)({
2803
+ snapshot: () => workspaceConvergence.snapshot(),
2804
+ forcePublication: () => armWorkspacePublication({ type: "manual", detail: "graceful worker shutdown" }, { delayMs: 0 })
2805
+ }).then((outcome) => {
2806
+ process.stdout.write(`[r5d-worker] graceful workspace publication ${outcome}
2807
+ `);
2808
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
2809
+ ws.close(1e3, "Worker shutting down");
2810
+ } else {
2811
+ process.exit(0);
2812
+ }
2813
+ });
2814
+ };
2815
+ process.on("SIGTERM", handleGracefulShutdown);
2816
+ process.on("SIGINT", handleGracefulShutdown);
2669
2817
  const stopHeartbeatWatchdog = () => {
2670
2818
  if (heartbeatWatchdog) {
2671
2819
  clearInterval(heartbeatWatchdog);
@@ -2696,7 +2844,8 @@ async function startWorker(options) {
2696
2844
  updateClis: true,
2697
2845
  canonicalResolverCheckout: true,
2698
2846
  browserPortForwarding: true,
2699
- globalWorkspaceSyncLeaseV1: true
2847
+ globalWorkspaceSyncLeaseV1: true,
2848
+ eventualWorkspaceSyncV1: true
2700
2849
  },
2701
2850
  projectRoot: projectsRoot,
2702
2851
  artifactRoot,
@@ -2723,14 +2872,19 @@ async function startWorker(options) {
2723
2872
  return;
2724
2873
  }
2725
2874
  if (message.type === "workspace_manifest") {
2726
- recentWorkspaceSyncProof.invalidate();
2727
- await workspaceSyncSingleFlight.runMutation(() => {
2875
+ const revisionToken = workspaceManifestRevision.begin(message.revision);
2876
+ workspaceReady = false;
2877
+ const manifestFilesystem = (0, import_workspace_manifest_admission.inspectWorkspaceManifestFilesystem)({
2878
+ projectsRoot,
2879
+ workspaceShadowRoot,
2880
+ projects: message.projects
2881
+ });
2882
+ const completed = await workspaceSyncSingleFlight.runExclusive(async () => {
2883
+ if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2884
+ const firstBootstrap = manifestFilesystem.firstBootstrap;
2728
2885
  configureGitHubAuth(message.githubCredential);
2729
2886
  visibleGitIdentity = message.gitIdentity;
2730
2887
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2731
- const previousCheckoutKeys = new Set(
2732
- allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2733
- );
2734
2888
  const migratedProjectIds = (0, import_managed_paths.migrateLegacyProjectRoots)(projectsRoot, message.projects);
2735
2889
  manifestByProjectId.clear();
2736
2890
  for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
@@ -2743,80 +2897,77 @@ async function startWorker(options) {
2743
2897
  }
2744
2898
  for (const checkout of currentCheckouts) {
2745
2899
  const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2746
- if (previousCheckoutKeys.has(key)) continue;
2747
2900
  const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2748
2901
  if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2749
2902
  }
2750
- previousPeriodicFingerprint = null;
2903
+ if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2904
+ const created = firstBootstrap || migratedProjectIds.length > 0 ? ensureVisibleWorktrees(currentCheckouts, { strictCanonicalRevalidation: true }) : [];
2905
+ if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2906
+ const bootstrapped = await (0, import_workspace_sync.convergeWorkspaceHead)(workspaceSyncInput({ type: "connect" }, { newVisibleCheckouts: created }), {
2907
+ // An existing checkout becomes routable after a fetch. Its
2908
+ // visible tree converges asynchronously; first bootstrap alone
2909
+ // must materialize the canonical snapshot before readiness.
2910
+ // Re-check the incident immediately before convergence so a
2911
+ // concurrently delivered fence forces fetch-only behavior.
2912
+ hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
2913
+ });
2914
+ if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2915
+ workspaceConvergence.setLocalHead(bootstrapped.localHead);
2916
+ workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
2917
+ for (const checkout of currentCheckouts) {
2918
+ const manifest = manifestByProjectId.get(checkout.projectId);
2919
+ if (!manifest) continue;
2920
+ const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2921
+ if (hasNormalVisibleGitDir(branchPath)) {
2922
+ pendingManifestCheckouts.delete(manifestCheckoutKey(checkout.projectId, checkout.branchName));
2923
+ }
2924
+ }
2751
2925
  process.stdout.write(
2752
2926
  `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2753
2927
  `
2754
2928
  );
2929
+ return true;
2755
2930
  });
2756
- if (!periodicWorkspaceScan) {
2757
- const emptyFingerprint = (0, import_node_crypto.createHash)("sha256").update("").digest("hex");
2758
- periodicWorkspaceScan = setInterval(() => {
2759
- if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2760
- periodicWorkspaceScanInFlight = true;
2931
+ if (!completed || !workspaceManifestRevision.complete(revisionToken)) return;
2932
+ workspaceReady = true;
2933
+ sendWorkspaceReady();
2934
+ schedulePendingManifestCheckoutHydration();
2935
+ if (!workspaceSafetyScan) {
2936
+ workspaceSafetyScan = setInterval(() => {
2937
+ if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || workspaceSafetyScanInFlight) {
2938
+ return;
2939
+ }
2940
+ workspaceSafetyScanInFlight = true;
2761
2941
  void (async () => {
2762
- const genericCheckoutKeys = new Set(
2763
- (0, import_workspace_sync.workspaceProjectsForSync)([...manifestByProjectId.values()], { type: "periodic" }).flatMap(
2764
- (project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName))
2765
- )
2942
+ const before = workspaceConvergence.snapshot();
2943
+ const hydrateVisible = before.dirtyGeneration === before.publishedGeneration;
2944
+ const converged = await workspaceSyncSingleFlight.runExclusive(
2945
+ () => (0, import_workspace_sync.convergeWorkspaceHead)(workspaceSyncInput({ type: "periodic", detail: "idle convergence scan" }), {
2946
+ hydrateVisible
2947
+ })
2766
2948
  );
2767
- const hasPendingGenericCheckout = [...pendingManifestCheckouts.values()].some(
2768
- (target) => genericCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2769
- );
2770
- if (hasPendingGenericCheckout) {
2771
- recentWorkspaceSyncProof.invalidate();
2772
- previousPeriodicFingerprint = null;
2773
- requestWorkspaceSync({
2774
- type: "periodic",
2775
- detail: "workspace manifest additions"
2776
- });
2777
- return;
2949
+ workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
2950
+ if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
2951
+ if (converged.localChanges && hydrateVisible) {
2952
+ markWorkspaceDirty({ type: "periodic", detail: "idle safety scan" });
2778
2953
  }
2779
- const observation = await workspaceSyncSingleFlight.runExclusive(() => {
2780
- const observationGeneration = recentWorkspaceSyncProof.beginObservation();
2781
- const input = workspaceSyncInput({ type: "periodic" });
2782
- const workerBusy = activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress;
2783
- return {
2784
- observationGeneration,
2785
- scopeKey: (0, import_workspace_sync_fast_path.workspaceSyncScopeKey)(input.remoteUrl, input.projects),
2786
- fingerprint: (0, import_workspace_sync.calculateWorkspaceDiffFingerprint)(input),
2787
- canonicalHead: (0, import_workspace_sync_fast_path.readShadowWorkspaceCanonicalHead)(workspaceShadowRoot),
2788
- workerBusy
2789
- };
2790
- });
2791
- if (observation.fingerprint === emptyFingerprint) {
2792
- recentWorkspaceSyncProof.recordStable({
2793
- observationGeneration: observation.observationGeneration,
2794
- canonicalHead: observation.canonicalHead,
2795
- scopeKey: observation.scopeKey,
2796
- observedAtMs: Date.now(),
2797
- workerBusy: observation.workerBusy || activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress
2798
- });
2799
- previousPeriodicFingerprint = null;
2800
- return;
2801
- }
2802
- recentWorkspaceSyncProof.invalidate();
2803
- if (previousPeriodicFingerprint !== observation.fingerprint) {
2804
- previousPeriodicFingerprint = observation.fingerprint;
2805
- return;
2806
- }
2807
- previousPeriodicFingerprint = null;
2808
- requestWorkspaceSync({ type: "periodic" }, observation.fingerprint);
2954
+ sendWorkspaceReady();
2809
2955
  })().catch((error) => {
2810
2956
  process.stderr.write(
2811
- `[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
2957
+ `[r5d-worker] idle workspace safety scan failed: ${error instanceof Error ? error.message : String(error)}
2812
2958
  `
2813
2959
  );
2814
2960
  }).finally(() => {
2815
- periodicWorkspaceScanInFlight = false;
2961
+ workspaceSafetyScanInFlight = false;
2816
2962
  heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
2817
2963
  });
2818
- }, import_workspace_sync.WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
2819
- periodicWorkspaceScan.unref();
2964
+ }, import_workspace_convergence.WORKSPACE_IDLE_SAFETY_SCAN_MS);
2965
+ workspaceSafetyScan.unref();
2966
+ }
2967
+ pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
2968
+ void convergeAvailableWorkspaceHead();
2969
+ if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
2970
+ armWorkspacePublication({ type: "periodic", detail: "resume dirty generation after reconnect" }, { delayMs: 0 });
2820
2971
  }
2821
2972
  return;
2822
2973
  }
@@ -2836,12 +2987,181 @@ async function startWorker(options) {
2836
2987
  confirmationReason: message.confirmationReason,
2837
2988
  resetToCanonical: message.resetToCanonical,
2838
2989
  skipVisibleMirror: message.skipVisibleMirror
2839
- },
2840
- message.canonicalHead,
2841
- message.allowRecentNoChangeFastPath
2990
+ }
2842
2991
  );
2843
2992
  return;
2844
2993
  }
2994
+ if (message.type === "workspace_head_available") {
2995
+ workspaceConvergence.observeCanonicalHead(message.canonicalHead);
2996
+ pendingWorkspaceHead = message.canonicalHead;
2997
+ sendWorkspaceReady();
2998
+ void convergeAvailableWorkspaceHead();
2999
+ return;
3000
+ }
3001
+ if (message.type === "workspace_publication_now") {
3002
+ armWorkspacePublication(message.trigger, { delayMs: 0, requestId: message.requestId });
3003
+ return;
3004
+ }
3005
+ if (message.type === "workspace_publication_authority") {
3006
+ const publication = pendingPublication;
3007
+ if (!publication || publication.requestId !== message.requestId) {
3008
+ process.stderr.write(`[r5d-worker] ignoring publication authority for unknown request ${message.requestId}
3009
+ `);
3010
+ return;
3011
+ }
3012
+ if (message.status !== "granted") {
3013
+ pendingPublication = void 0;
3014
+ workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3015
+ const applyBlockedResponse = message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId);
3016
+ if (applyBlockedResponse) {
3017
+ workspaceConvergence.block();
3018
+ if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3019
+ } else {
3020
+ workspaceConvergence.resetTransientPublicationState();
3021
+ workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3022
+ if (message.status === "busy") {
3023
+ armWorkspacePublication(publication.trigger, {
3024
+ delayMs: message.retryAfterMs ?? 1e3,
3025
+ requestId: publication.requestId
3026
+ });
3027
+ } else if (!activeWorkspaceIncidentId) {
3028
+ const explicit = explicitWorkspacePublications.next();
3029
+ if (explicit) {
3030
+ armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
3031
+ } else if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3032
+ armWorkspacePublication(publication.trigger, { delayMs: 0 });
3033
+ }
3034
+ }
3035
+ }
3036
+ sendWorkspaceReady();
3037
+ return;
3038
+ }
3039
+ publication.attemptId = message.attemptId;
3040
+ publication.authorityToken = message.authorityToken;
3041
+ publication.workInFlight = true;
3042
+ pendingPublication = publication;
3043
+ if (currentWorkerSocket !== ws) {
3044
+ publication.workInFlight = false;
3045
+ workspaceConvergence.resetTransientPublicationState();
3046
+ return;
3047
+ }
3048
+ workspaceConvergence.beginPreparing();
3049
+ sendWorkspaceReady();
3050
+ let prepareStartedAt = Date.now();
3051
+ let synchronizeStartedAt = prepareStartedAt;
3052
+ let synchronizeStarted = false;
3053
+ let result;
3054
+ try {
3055
+ result = await workspaceSyncSingleFlight.runExclusive(async () => {
3056
+ prepareStartedAt = Date.now();
3057
+ const pendingTargets = [...pendingManifestCheckouts.values()];
3058
+ const createdCheckouts = ensureVisibleWorktrees(pendingTargets, { strictCanonicalRevalidation: true });
3059
+ synchronizeStartedAt = Date.now();
3060
+ synchronizeStarted = true;
3061
+ const prepared = await (0, import_workspace_sync.synchronizeWorkspace)(
3062
+ workspaceSyncInput(publication.trigger, {
3063
+ attemptId: message.attemptId,
3064
+ quarantineRef: message.quarantineRef,
3065
+ expectedCanonicalHead: message.expectedHead,
3066
+ newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
3067
+ })
3068
+ );
3069
+ for (const target of pendingTargets) {
3070
+ pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
3071
+ }
3072
+ return prepared.outcome === "updated" ? { ...prepared, outcome: "no_change" } : prepared;
3073
+ });
3074
+ } catch (error) {
3075
+ if (!synchronizeStarted) synchronizeStartedAt = Date.now();
3076
+ result = {
3077
+ type: "workspace_sync",
3078
+ attemptId: message.attemptId,
3079
+ workerLabel: label,
3080
+ trigger: publication.trigger,
3081
+ outcome: "failed",
3082
+ startingHead: message.expectedHead,
3083
+ expectedHead: message.expectedHead,
3084
+ rebaseCount: 0,
3085
+ diffSizeBytes: 0,
3086
+ gitStatus: "",
3087
+ affectedProjects: [],
3088
+ affectedPaths: [],
3089
+ discardedPaths: [],
3090
+ localChangesDiscarded: false,
3091
+ error: error instanceof Error ? error.message : String(error)
3092
+ };
3093
+ }
3094
+ const finishedAt = Date.now();
3095
+ result = {
3096
+ ...result,
3097
+ dirtyGeneration: publication.dirtyGeneration,
3098
+ telemetry: (0, import_workspace_convergence.workspacePublicationTelemetry)({
3099
+ requestedAt: publication.requestedAt,
3100
+ prepareStartedAt,
3101
+ synchronizeStartedAt,
3102
+ finishedAt
3103
+ })
3104
+ };
3105
+ workspaceConvergence.beginSubmitting();
3106
+ sendWorkspaceReady();
3107
+ try {
3108
+ ws.send(
3109
+ JSON.stringify({
3110
+ type: "workspace_publication_result",
3111
+ requestId: publication.requestId,
3112
+ attemptId: message.attemptId,
3113
+ authorityToken: message.authorityToken,
3114
+ dirtyGeneration: publication.dirtyGeneration,
3115
+ result
3116
+ })
3117
+ );
3118
+ } catch (error) {
3119
+ workspaceConvergence.resetTransientPublicationState();
3120
+ throw error;
3121
+ } finally {
3122
+ publication.workInFlight = false;
3123
+ if (currentWorkerSocket !== ws) workspaceConvergence.resetTransientPublicationState();
3124
+ }
3125
+ return;
3126
+ }
3127
+ if (message.type === "workspace_publication_completed") {
3128
+ const publication = pendingPublication;
3129
+ if (!publication || publication.requestId !== message.requestId || publication.attemptId !== message.attemptId) {
3130
+ process.stderr.write(`[r5d-worker] ignoring completion for unknown publication ${message.requestId}
3131
+ `);
3132
+ return;
3133
+ }
3134
+ pendingPublication = void 0;
3135
+ workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3136
+ if (message.status === "published" || message.status === "no_change") {
3137
+ workspaceConvergence.complete({
3138
+ generation: Math.min(message.publishedGeneration ?? publication.dirtyGeneration, publication.dirtyGeneration),
3139
+ canonicalHead: message.canonicalHead,
3140
+ published: true
3141
+ });
3142
+ } else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
3143
+ workspaceConvergence.block();
3144
+ if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3145
+ } else {
3146
+ workspaceConvergence.resetTransientPublicationState();
3147
+ workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3148
+ }
3149
+ sendWorkspaceReady();
3150
+ const explicit = message.status === "retry" && publication.explicit ? { requestId: publication.requestId, trigger: publication.trigger } : explicitWorkspacePublications.next();
3151
+ if (explicit) {
3152
+ armWorkspacePublication(explicit.trigger, {
3153
+ delayMs: message.retryAfterMs ?? 0,
3154
+ requestId: explicit.requestId
3155
+ });
3156
+ } else if (message.status === "retry" || message.status === "failed" || workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3157
+ armWorkspacePublication(publication.trigger, { delayMs: message.retryAfterMs ?? 1e3 });
3158
+ }
3159
+ if (message.canonicalHead !== workspaceConvergence.snapshot().localHead) {
3160
+ pendingWorkspaceHead = message.canonicalHead;
3161
+ void convergeAvailableWorkspaceHead();
3162
+ }
3163
+ return;
3164
+ }
2845
3165
  if (message.type === "sync_session_artifacts") {
2846
3166
  sessionArtifactSyncRequestsInFlight += 1;
2847
3167
  try {
@@ -2869,18 +3189,27 @@ async function startWorker(options) {
2869
3189
  return;
2870
3190
  }
2871
3191
  if (message.type === "workspace_incident_updated") {
2872
- recentWorkspaceSyncProof.invalidate();
3192
+ workspaceIncidentOrdering.observe(message);
2873
3193
  const previousIncidentId = activeWorkspaceIncidentId;
2874
3194
  activeWorkspaceIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
2875
- previousPeriodicFingerprint = null;
2876
3195
  const releasedHead = (0, import_workspace_incident_state.releasePendingWorkspaceHead)(
2877
3196
  previousIncidentId,
2878
3197
  activeWorkspaceIncidentId,
2879
3198
  message.status === "resolved" || message.status === "confirmed" || message.status === "reset" ? message.incidentId : null
2880
3199
  );
2881
- if (releasedHead.syncHead) {
2882
- requestWorkspaceSync({ type: "inbound_head", detail: releasedHead.syncHead });
3200
+ workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3201
+ if (releasedHead.fetchAuthoritativeHead) {
3202
+ pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
3203
+ void convergeAvailableWorkspaceHead();
2883
3204
  }
3205
+ const explicit = !activeWorkspaceIncidentId ? explicitWorkspacePublications.next() : void 0;
3206
+ if (explicit) {
3207
+ armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
3208
+ } else if (!activeWorkspaceIncidentId && workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3209
+ armWorkspacePublication({ type: "periodic", detail: "resume after workspace incident" }, { delayMs: 0 });
3210
+ }
3211
+ if (!activeWorkspaceIncidentId) schedulePendingManifestCheckoutHydration();
3212
+ sendWorkspaceReady();
2884
3213
  return;
2885
3214
  }
2886
3215
  if (message.type === "exec_terminal_ack") {
@@ -2916,7 +3245,6 @@ async function startWorker(options) {
2916
3245
  return;
2917
3246
  }
2918
3247
  cliUpdateInProgress = true;
2919
- recentWorkspaceSyncProof.invalidate();
2920
3248
  try {
2921
3249
  const result = await (0, import_cli_update.installCliUpdate)(message);
2922
3250
  sendWorkerMessage(ws, {
@@ -2979,9 +3307,8 @@ async function startWorker(options) {
2979
3307
  });
2980
3308
  return;
2981
3309
  }
2982
- recentWorkspaceSyncProof.invalidate();
2983
3310
  const releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
2984
- let leaseTransferred = false;
3311
+ let mutationLeaseTransferred = false;
2985
3312
  try {
2986
3313
  const resolvedTarget = resolveMessageTarget(message.target);
2987
3314
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
@@ -2991,9 +3318,14 @@ async function startWorker(options) {
2991
3318
  message,
2992
3319
  resolvedTarget,
2993
3320
  planRoot,
2994
- ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {}
3321
+ ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
3322
+ ...targetMayMutateVisibleWorkspace(message.target) ? {
3323
+ onTerminal: () => {
3324
+ markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true);
3325
+ }
3326
+ } : {}
2995
3327
  });
2996
- leaseTransferred = releaseWorkspaceMutation !== void 0;
3328
+ mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
2997
3329
  } catch (error) {
2998
3330
  sendWorkerMessage(ws, {
2999
3331
  type: "pty_error",
@@ -3002,12 +3334,15 @@ async function startWorker(options) {
3002
3334
  error: error instanceof Error ? error.message : String(error)
3003
3335
  });
3004
3336
  } finally {
3005
- if (!leaseTransferred) releaseWorkspaceMutation?.();
3337
+ if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
3006
3338
  }
3007
3339
  return;
3008
3340
  }
3009
3341
  if (message.type === "pty_input") {
3010
- recentWorkspaceSyncProof.invalidate();
3342
+ const activePty = activePtys.get(message.ptyId);
3343
+ if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
3344
+ markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` });
3345
+ }
3011
3346
  writePty(ws, message);
3012
3347
  return;
3013
3348
  }
@@ -3016,8 +3351,11 @@ async function startWorker(options) {
3016
3351
  return;
3017
3352
  }
3018
3353
  if (message.type === "pty_close") {
3019
- recentWorkspaceSyncProof.invalidate();
3354
+ const activePty = activePtys.get(message.ptyId);
3020
3355
  closePty(message);
3356
+ if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
3357
+ armWorkspacePublication({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, { delayMs: 0 });
3358
+ }
3021
3359
  return;
3022
3360
  }
3023
3361
  if (message.type === "exec") {
@@ -3031,7 +3369,6 @@ async function startWorker(options) {
3031
3369
  });
3032
3370
  return;
3033
3371
  }
3034
- recentWorkspaceSyncProof.invalidate();
3035
3372
  let result;
3036
3373
  try {
3037
3374
  const runCommand = async () => {
@@ -3052,6 +3389,12 @@ async function startWorker(options) {
3052
3389
  };
3053
3390
  }
3054
3391
  ws.send(JSON.stringify(result));
3392
+ if (targetMayMutateVisibleWorkspace(message.target)) {
3393
+ markWorkspaceDirty(
3394
+ { type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
3395
+ true
3396
+ );
3397
+ }
3055
3398
  return;
3056
3399
  }
3057
3400
  if (message.type === "exec_start") {
@@ -3064,25 +3407,38 @@ async function startWorker(options) {
3064
3407
  });
3065
3408
  return;
3066
3409
  }
3067
- recentWorkspaceSyncProof.invalidate();
3068
3410
  sendWorkerMessage(ws, {
3069
3411
  type: "exec_accepted",
3070
3412
  requestId: message.requestId,
3071
3413
  runId: message.runId
3072
3414
  });
3073
3415
  const runCommand = async () => {
3074
- const resolvedTarget = resolveMessageTarget(message.target);
3075
- process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
3416
+ try {
3417
+ const resolvedTarget = resolveMessageTarget(message.target);
3418
+ process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
3076
3419
  `);
3077
- await executeStreamingCommand({
3078
- ws,
3079
- message,
3080
- resolvedTarget,
3081
- baseUrl,
3082
- token,
3083
- artifactRoot,
3084
- planRoot
3085
- });
3420
+ await executeStreamingCommand({
3421
+ ws,
3422
+ message,
3423
+ resolvedTarget,
3424
+ baseUrl,
3425
+ token,
3426
+ artifactRoot,
3427
+ planRoot
3428
+ });
3429
+ } finally {
3430
+ if (targetMayMutateVisibleWorkspace(message.target)) {
3431
+ markWorkspaceDirty(
3432
+ {
3433
+ type: "process_terminal",
3434
+ sessionId: message.sessionId,
3435
+ processRunId: message.runId,
3436
+ detail: "process completed"
3437
+ },
3438
+ true
3439
+ );
3440
+ }
3441
+ }
3086
3442
  };
3087
3443
  const execution = (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
3088
3444
  void execution.catch((error) => {
@@ -3096,9 +3452,8 @@ async function startWorker(options) {
3096
3452
  return;
3097
3453
  }
3098
3454
  if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
3099
- if (message.type === "write" || message.type === "edit") recentWorkspaceSyncProof.invalidate();
3100
3455
  try {
3101
- const result = await workspaceSyncSingleFlight.runMutation(async () => {
3456
+ const result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
3102
3457
  const resolvedTarget = resolveMessageTarget(message.target);
3103
3458
  return await executeOperation({
3104
3459
  message,
@@ -3116,6 +3471,14 @@ async function startWorker(options) {
3116
3471
  result
3117
3472
  })
3118
3473
  );
3474
+ if ((message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target)) {
3475
+ markWorkspaceDirty({
3476
+ type: message.type,
3477
+ sessionId: message.sessionId,
3478
+ toolCallId: message.requestId,
3479
+ ...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
3480
+ });
3481
+ }
3119
3482
  } catch (error) {
3120
3483
  ws.send(
3121
3484
  JSON.stringify({
@@ -3145,11 +3508,23 @@ async function startWorker(options) {
3145
3508
  });
3146
3509
  });
3147
3510
  ws.addEventListener("close", (event) => {
3148
- recentWorkspaceSyncProof.invalidate();
3511
+ process.off("SIGTERM", handleGracefulShutdown);
3512
+ process.off("SIGINT", handleGracefulShutdown);
3149
3513
  stopHeartbeatWatchdog();
3150
- if (periodicWorkspaceScan) {
3151
- clearInterval(periodicWorkspaceScan);
3152
- periodicWorkspaceScan = void 0;
3514
+ if (workspacePublicationTimer) {
3515
+ clearTimeout(workspacePublicationTimer);
3516
+ workspacePublicationTimer = void 0;
3517
+ }
3518
+ if (!pendingPublication?.workInFlight) {
3519
+ workspaceConvergence.resetTransientPublicationState();
3520
+ }
3521
+ if (workspaceSafetyScan) {
3522
+ clearInterval(workspaceSafetyScan);
3523
+ workspaceSafetyScan = void 0;
3524
+ }
3525
+ if (workspaceManifestHydrationTimer) {
3526
+ clearTimeout(workspaceManifestHydrationTimer);
3527
+ workspaceManifestHydrationTimer = void 0;
3153
3528
  }
3154
3529
  if (terminalReplayTimer) {
3155
3530
  clearInterval(terminalReplayTimer);
@@ -3165,6 +3540,9 @@ async function startWorker(options) {
3165
3540
  if (reloadAfterClose) {
3166
3541
  process.exit(import_supervisor.WORKER_RELOAD_EXIT_CODE);
3167
3542
  }
3543
+ if (shutdownAfterClose) {
3544
+ process.exit(0);
3545
+ }
3168
3546
  if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
3169
3547
  process.stderr.write(
3170
3548
  `[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${import_supervisor.WORKER_RECONNECT_DELAY_MS}ms