@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/mjs/main.mjs CHANGED
@@ -13,13 +13,19 @@ import {
13
13
  } from "./heartbeat.mjs";
14
14
  import { terminateProcessTree } from "./process-tree.mjs";
15
15
  import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
16
- import { applyWorkspaceIncidentUpdate, releasePendingWorkspaceHead } from "./workspace-incident-state.mjs";
16
+ import { applyWorkspaceIncidentUpdate, releasePendingWorkspaceHead, WorkspaceIncidentOrderingFence } from "./workspace-incident-state.mjs";
17
17
  import { acquireWorkspaceCommandMutation, runWorkspaceCommand } from "./workspace-command-sync-policy.mjs";
18
18
  import {
19
- RecentWorkspaceSyncProofCache,
20
- readShadowWorkspaceCanonicalHead,
21
- workspaceSyncScopeKey
22
- } from "./workspace-sync-fast-path.mjs";
19
+ WORKSPACE_PUBLICATION_QUIET_MS,
20
+ WORKSPACE_IDLE_SAFETY_SCAN_MS,
21
+ ExplicitWorkspacePublicationQueue,
22
+ WorkspaceManifestRevisionFence,
23
+ processWorkspaceConvergenceState,
24
+ waitForWorkspacePublicationOnShutdown,
25
+ workspacePublicationIncidentAction,
26
+ workspacePublicationTelemetry
27
+ } from "./workspace-convergence.mjs";
28
+ import { inspectWorkspaceManifestFilesystem } from "./workspace-manifest-admission.mjs";
23
29
  import {
24
30
  isRetryableWorkerServerStatus,
25
31
  superviseWorkerRuntime,
@@ -30,15 +36,11 @@ import {
30
36
  } from "./supervisor.mjs";
31
37
  import { managedProjectRoot, migrateLegacyProjectRoots, validateManagedBranchName } from "./managed-paths.mjs";
32
38
  import {
33
- WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
34
39
  WorkspaceSyncSingleFlight,
35
- calculateWorkspaceDiffFingerprint,
40
+ convergeWorkspaceHead,
36
41
  synchronizeWorkspace,
37
42
  workspaceProjectsForSync
38
43
  } from "./workspace-sync.mjs";
39
- function workspaceSyncFastPathTelemetry(decision) {
40
- return decision.hit ? { hit: true, reason: decision.reason, proofAgeMs: decision.proofAgeMs } : { hit: false, reason: decision.reason };
41
- }
42
44
  class WorkerServerUnavailableError extends Error {
43
45
  name = "WorkerServerUnavailableError";
44
46
  }
@@ -775,25 +777,13 @@ function fallbackInternalRemoteUrl(baseUrl, projectId) {
775
777
  }
776
778
  function visibleCheckoutRemoteFor(input) {
777
779
  const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
778
- const isCanonicalManifestBranch = Boolean(canonicalCheckout);
779
- const usesCanonicalRemote = isCanonicalManifestBranch || !input.manifest?.repoHttpUrl;
780
- if (usesCanonicalRemote) {
781
- return {
782
- remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
783
- authHeader: `Authorization: Bearer ${input.token}`,
784
- persistAuth: false,
785
- reconcileExistingOrigin: isCanonicalManifestBranch,
786
- requireRemoteBranch: isCanonicalManifestBranch,
787
- requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
788
- };
789
- }
790
780
  return {
791
- remoteUrl: input.manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
792
- authHeader: input.manifest?.repoAuthHeader ?? null,
793
- persistAuth: true,
794
- reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl),
795
- requireRemoteBranch: false,
796
- requiredBaseCommit: null
781
+ remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
782
+ authHeader: `Authorization: Bearer ${input.token}`,
783
+ persistAuth: false,
784
+ reconcileExistingOrigin: true,
785
+ requireRemoteBranch: true,
786
+ requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
797
787
  };
798
788
  }
799
789
  function gitExtraHeaderUrlForRemote(remoteUrl) {
@@ -1014,9 +1004,11 @@ function checkoutVisibleBranch(input) {
1014
1004
  }
1015
1005
  function migrateExistingCheckoutToRequiredRemote(input) {
1016
1006
  if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
1017
- if (hasWorktreeChanges(input.branchPath)) {
1007
+ const dirtyWorkingTree = hasWorktreeChanges(input.branchPath);
1008
+ if (dirtyWorkingTree && input.requiredBaseCommit) {
1018
1009
  throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
1019
1010
  }
1011
+ const preserveWorkingTree = dirtyWorkingTree;
1020
1012
  runGit(
1021
1013
  visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1022
1014
  {
@@ -1031,11 +1023,12 @@ function migrateExistingCheckoutToRequiredRemote(input) {
1031
1023
  if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
1032
1024
  throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1033
1025
  }
1026
+ ensureOriginRemote(input.branchPath, input.remoteUrl);
1027
+ if (preserveWorkingTree) return;
1034
1028
  if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
1035
1029
  const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1036
1030
  runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1037
1031
  }
1038
- ensureOriginRemote(input.branchPath, input.remoteUrl);
1039
1032
  runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1040
1033
  cwd: input.branchPath
1041
1034
  });
@@ -1128,7 +1121,7 @@ function ensureVisibleGitCheckout(input) {
1128
1121
  configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1129
1122
  return branchPath;
1130
1123
  }
1131
- function ensureBranchWorkspace(input) {
1124
+ function ensureBranchWorktree(input) {
1132
1125
  const defaultBranch = input.manifest?.defaultBranch || "main";
1133
1126
  const remote = visibleCheckoutRemoteFor(input);
1134
1127
  return {
@@ -1161,7 +1154,7 @@ function resolveWorkerSessionTarget(input) {
1161
1154
  }
1162
1155
  const manifest = input.manifestByProjectId.get(input.target.projectId);
1163
1156
  const projectRoot = projectRootFor(input.projectsRoot, input.target.projectId, input.manifestByProjectId);
1164
- const workspace = ensureBranchWorkspace({
1157
+ const worktree = ensureBranchWorktree({
1165
1158
  projectId: input.target.projectId,
1166
1159
  baseUrl: input.baseUrl,
1167
1160
  token: input.token,
@@ -1170,7 +1163,7 @@ function resolveWorkerSessionTarget(input) {
1170
1163
  branchName: input.target.branchName,
1171
1164
  manifest
1172
1165
  });
1173
- return { target: input.target, rootPath: workspace.branchPath, manifest };
1166
+ return { target: input.target, rootPath: worktree.branchPath, manifest };
1174
1167
  }
1175
1168
  function workspaceSyncCheckoutTargets(input) {
1176
1169
  const selected = /* @__PURE__ */ new Map();
@@ -2268,6 +2261,7 @@ async function openPty(input) {
2268
2261
  onExit: (event) => {
2269
2262
  input.releaseWorkspaceMutation?.();
2270
2263
  activePtys.delete(input.message.ptyId);
2264
+ input.onTerminal?.();
2271
2265
  sendWorkerMessage(input.ws, {
2272
2266
  type: "pty_exit",
2273
2267
  ptyId: input.message.ptyId,
@@ -2278,6 +2272,7 @@ async function openPty(input) {
2278
2272
  onError: (error) => {
2279
2273
  input.releaseWorkspaceMutation?.();
2280
2274
  activePtys.delete(input.message.ptyId);
2275
+ input.onTerminal?.();
2281
2276
  sendWorkerMessage(input.ws, {
2282
2277
  type: "pty_error",
2283
2278
  requestId: input.message.requestId,
@@ -2369,15 +2364,25 @@ async function startWorker(options) {
2369
2364
  const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2370
2365
  let workspaceRemoteUrl = null;
2371
2366
  let activeWorkspaceIncidentId = null;
2372
- let previousPeriodicFingerprint = null;
2373
- let periodicWorkspaceScan;
2374
- let periodicWorkspaceScanInFlight = false;
2367
+ const workspaceIncidentOrdering = new WorkspaceIncidentOrderingFence();
2368
+ const workspaceConvergence = processWorkspaceConvergenceState(path.join(syncRoot, "workspace-convergence.json"));
2369
+ const workspaceManifestRevision = new WorkspaceManifestRevisionFence();
2370
+ let workspaceReady = false;
2371
+ let workspacePublicationTimer;
2372
+ let workspaceSafetyScan;
2373
+ let workspaceManifestHydrationTimer;
2374
+ let workspaceManifestHydrationInFlight = false;
2375
+ let workspaceSafetyScanInFlight = false;
2376
+ let workspaceHeadConvergenceInFlight = false;
2377
+ let pendingWorkspaceHead;
2378
+ let pendingPublication;
2379
+ const explicitWorkspacePublications = new ExplicitWorkspacePublicationQueue();
2375
2380
  let terminalReplayTimer;
2376
2381
  let workspaceSyncRequestsInFlight = 0;
2377
2382
  let sessionArtifactSyncRequestsInFlight = 0;
2378
2383
  let cliUpdateInProgress = false;
2379
2384
  let reloadAfterClose = false;
2380
- const recentWorkspaceSyncProof = new RecentWorkspaceSyncProofCache();
2385
+ let shutdownAfterClose = false;
2381
2386
  const workspaceSyncInput = (trigger, overrides = {}) => {
2382
2387
  if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2383
2388
  const projects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
@@ -2403,21 +2408,160 @@ async function startWorker(options) {
2403
2408
  );
2404
2409
  }
2405
2410
  };
2406
- const requestWorkspaceSync = (trigger, fingerprint) => {
2411
+ const sendWorkspaceReady = () => {
2412
+ const manifestRevision = workspaceManifestRevision.readyRevision();
2413
+ if (!workspaceReady || !manifestRevision) return;
2407
2414
  try {
2408
2415
  ws.send(
2409
2416
  JSON.stringify({
2410
- type: "workspace_sync_requested",
2411
- observationId: crypto.randomUUID(),
2417
+ type: "workspace_ready",
2418
+ manifestRevision,
2419
+ pendingCheckouts: [...pendingManifestCheckouts.values()].sort(
2420
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
2421
+ ),
2422
+ ...workspaceConvergence.snapshot()
2423
+ })
2424
+ );
2425
+ } catch (error) {
2426
+ process.stderr.write(`[r5d-worker] failed to send workspace readiness: ${error instanceof Error ? error.message : String(error)}
2427
+ `);
2428
+ }
2429
+ };
2430
+ const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
2431
+ const armWorkspacePublication = (trigger, options2 = { delayMs: WORKSPACE_PUBLICATION_QUIET_MS }) => {
2432
+ workspaceConvergence.schedule();
2433
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2434
+ sendWorkspaceReady();
2435
+ if (options2.requestId && !explicitWorkspacePublications.schedule({ requestId: options2.requestId, trigger }, pendingPublication?.requestId)) {
2436
+ return;
2437
+ }
2438
+ if (!options2.requestId && explicitWorkspacePublications.hasScheduled()) return;
2439
+ if (workspacePublicationTimer) clearTimeout(workspacePublicationTimer);
2440
+ workspacePublicationTimer = setTimeout(
2441
+ () => {
2442
+ workspacePublicationTimer = void 0;
2443
+ if (options2.requestId) explicitWorkspacePublications.consumeScheduled(options2.requestId);
2444
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2445
+ if (!workspaceReady || !workspaceRemoteUrl) {
2446
+ armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2447
+ return;
2448
+ }
2449
+ if (activeWorkspaceIncidentId) {
2450
+ workspaceConvergence.block();
2451
+ sendWorkspaceReady();
2452
+ if (workspacePublicationIncidentAction(options2.requestId) === "defer_automatic") return;
2453
+ if (pendingPublication) {
2454
+ explicitWorkspacePublications.enqueue({ requestId: options2.requestId, trigger }, pendingPublication.requestId);
2455
+ return;
2456
+ }
2457
+ const snapshot2 = workspaceConvergence.snapshot();
2458
+ pendingPublication = {
2459
+ requestId: options2.requestId,
2460
+ trigger,
2461
+ dirtyGeneration: snapshot2.dirtyGeneration,
2462
+ explicit: true,
2463
+ workInFlight: false,
2464
+ requestedAt: Date.now()
2465
+ };
2466
+ try {
2467
+ ws.send(
2468
+ JSON.stringify({
2469
+ type: "workspace_publication_request",
2470
+ requestId: options2.requestId,
2471
+ trigger,
2472
+ dirtyGeneration: snapshot2.dirtyGeneration,
2473
+ localHead: snapshot2.localHead,
2474
+ desiredCanonicalHead: snapshot2.desiredCanonicalHead
2475
+ })
2476
+ );
2477
+ } catch (error) {
2478
+ pendingPublication = void 0;
2479
+ workspaceConvergence.defer();
2480
+ process.stderr.write(
2481
+ `[r5d-worker] failed to request blocked workspace publication authority: ${error instanceof Error ? error.message : String(error)}
2482
+ `
2483
+ );
2484
+ armWorkspacePublication(trigger, { delayMs: 1e3, requestId: options2.requestId });
2485
+ }
2486
+ return;
2487
+ }
2488
+ if (pendingPublication) {
2489
+ armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2490
+ return;
2491
+ }
2492
+ const snapshot = workspaceConvergence.snapshot();
2493
+ const requestId = options2.requestId ?? crypto.randomUUID();
2494
+ pendingPublication = {
2495
+ requestId,
2412
2496
  trigger,
2413
- ...fingerprint ? { fingerprint } : {}
2497
+ dirtyGeneration: snapshot.dirtyGeneration,
2498
+ explicit: options2.requestId !== void 0,
2499
+ workInFlight: false,
2500
+ requestedAt: Date.now()
2501
+ };
2502
+ try {
2503
+ ws.send(
2504
+ JSON.stringify({
2505
+ type: "workspace_publication_request",
2506
+ requestId,
2507
+ trigger,
2508
+ dirtyGeneration: snapshot.dirtyGeneration,
2509
+ localHead: snapshot.localHead,
2510
+ desiredCanonicalHead: snapshot.desiredCanonicalHead
2511
+ })
2512
+ );
2513
+ } catch (error) {
2514
+ pendingPublication = void 0;
2515
+ workspaceConvergence.defer();
2516
+ process.stderr.write(
2517
+ `[r5d-worker] failed to request workspace publication: ${error instanceof Error ? error.message : String(error)}
2518
+ `
2519
+ );
2520
+ armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2521
+ }
2522
+ },
2523
+ Math.max(0, options2.delayMs)
2524
+ );
2525
+ workspacePublicationTimer.unref();
2526
+ };
2527
+ const markWorkspaceDirty = (trigger, force = false) => {
2528
+ workspaceConvergence.markDirty();
2529
+ armWorkspacePublication(trigger, { delayMs: force ? 0 : WORKSPACE_PUBLICATION_QUIET_MS });
2530
+ };
2531
+ const convergeAvailableWorkspaceHead = async () => {
2532
+ if (pendingWorkspaceHead === void 0 || !workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || workspaceHeadConvergenceInFlight) {
2533
+ return;
2534
+ }
2535
+ const advertisedHead = pendingWorkspaceHead;
2536
+ pendingWorkspaceHead = void 0;
2537
+ workspaceConvergence.observeCanonicalHead(advertisedHead);
2538
+ workspaceHeadConvergenceInFlight = true;
2539
+ try {
2540
+ const snapshot = workspaceConvergence.snapshot();
2541
+ const hydrateVisible = !pendingPublication && snapshot.dirtyGeneration === snapshot.publishedGeneration;
2542
+ const converged = await workspaceSyncSingleFlight.runExclusive(
2543
+ () => convergeWorkspaceHead(workspaceSyncInput({ type: "inbound_head", detail: advertisedHead ?? "unborn" }), {
2544
+ hydrateVisible
2414
2545
  })
2415
2546
  );
2547
+ workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
2548
+ if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
2549
+ if (hydrateVisible && converged.localChanges) {
2550
+ markWorkspaceDirty({ type: "periodic", detail: "inbound overlap retained for next publication" });
2551
+ }
2552
+ sendWorkspaceReady();
2416
2553
  } catch (error) {
2554
+ pendingWorkspaceHead = advertisedHead;
2417
2555
  process.stderr.write(
2418
- `[r5d-worker] failed to send workspace sync observation on its current connection: ${error instanceof Error ? error.message : String(error)}
2556
+ `[r5d-worker] failed to fetch available workspace head: ${error instanceof Error ? error.message : String(error)}
2419
2557
  `
2420
2558
  );
2559
+ } finally {
2560
+ workspaceHeadConvergenceInFlight = false;
2561
+ heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
2562
+ if (pendingWorkspaceHead !== void 0) {
2563
+ setTimeout(() => void convergeAvailableWorkspaceHead(), 1e3).unref();
2564
+ }
2421
2565
  }
2422
2566
  };
2423
2567
  const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
@@ -2433,7 +2577,7 @@ async function startWorker(options) {
2433
2577
  workspaceShadowRoot,
2434
2578
  manifestByProjectId
2435
2579
  });
2436
- const ensureVisibleWorkspaceCheckouts = (targets, options2 = {}) => {
2580
+ const ensureVisibleWorktrees = (targets, options2 = {}) => {
2437
2581
  const created = [];
2438
2582
  for (const target of targets) {
2439
2583
  const manifest = manifestByProjectId.get(target.projectId);
@@ -2462,20 +2606,62 @@ async function startWorker(options) {
2462
2606
  reconcileExistingOrigin: remote.reconcileExistingOrigin,
2463
2607
  requireRemoteBranch: remote.requireRemoteBranch,
2464
2608
  requiredBaseCommit: remote.requiredBaseCommit,
2465
- allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requireRemoteBranch),
2609
+ allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requiredBaseCommit),
2466
2610
  clearPersistentAuth: !remote.persistAuth
2467
2611
  });
2468
2612
  if (!existed) created.push(target);
2469
2613
  }
2470
2614
  return created;
2471
2615
  };
2472
- const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}, authoritativeCanonicalHead, allowRecentNoChangeFastPath) => {
2616
+ const schedulePendingManifestCheckoutHydration = (delayMs = 0) => {
2617
+ if (workspaceManifestHydrationTimer || workspaceManifestHydrationInFlight || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2618
+ return;
2619
+ }
2620
+ workspaceManifestHydrationTimer = setTimeout(
2621
+ () => {
2622
+ workspaceManifestHydrationTimer = void 0;
2623
+ if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2624
+ if (pendingManifestCheckouts.size > 0 && currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) {
2625
+ schedulePendingManifestCheckoutHydration(1e3);
2626
+ }
2627
+ return;
2628
+ }
2629
+ workspaceManifestHydrationInFlight = true;
2630
+ void workspaceSyncSingleFlight.runExclusive(async () => {
2631
+ if (!workspaceReady || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2632
+ return;
2633
+ }
2634
+ const targets = [...pendingManifestCheckouts.values()];
2635
+ ensureVisibleWorktrees(targets, { strictCanonicalRevalidation: true });
2636
+ for (const target of targets) {
2637
+ const manifest = manifestByProjectId.get(target.projectId);
2638
+ if (!manifest || !manifest.branches.includes(target.branchName)) continue;
2639
+ const branchPath = path.join(projectRootFor(projectsRoot, target.projectId, manifestByProjectId), target.branchName);
2640
+ if (hasNormalVisibleGitDir(branchPath)) {
2641
+ pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
2642
+ }
2643
+ }
2644
+ }).then(() => sendWorkspaceReady()).catch((error) => {
2645
+ process.stderr.write(
2646
+ `[r5d-worker] pending manifest checkout hydration failed: ${error instanceof Error ? error.message : String(error)}
2647
+ `
2648
+ );
2649
+ }).finally(() => {
2650
+ workspaceManifestHydrationInFlight = false;
2651
+ heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
2652
+ if (pendingManifestCheckouts.size > 0) schedulePendingManifestCheckoutHydration(1e3);
2653
+ });
2654
+ },
2655
+ Math.max(0, delayMs)
2656
+ );
2657
+ workspaceManifestHydrationTimer.unref();
2658
+ };
2659
+ const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}) => {
2473
2660
  const requestedAt = Date.now();
2474
2661
  let queueEnteredAt = requestedAt;
2475
2662
  let prepareStartedAt = requestedAt;
2476
2663
  let synchronizeStartedAt = requestedAt;
2477
2664
  let synchronizeFinishedAt = requestedAt;
2478
- let fastPathDecision = { hit: false, reason: "no_recent_proof" };
2479
2665
  workspaceSyncRequestsInFlight += 1;
2480
2666
  try {
2481
2667
  let pendingTargets = [];
@@ -2484,48 +2670,12 @@ async function startWorker(options) {
2484
2670
  const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2485
2671
  const syncProjects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
2486
2672
  if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2487
- const scopeKey = workspaceSyncScopeKey(workspaceRemoteUrl, syncProjects);
2488
2673
  const allowedCheckoutKeys = new Set(
2489
2674
  syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
2490
2675
  );
2491
2676
  const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
2492
2677
  (target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2493
2678
  );
2494
- const workerBusy = activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress;
2495
- fastPathDecision = recentWorkspaceSyncProof.evaluate({
2496
- trigger,
2497
- allowRecentNoChangeFastPath,
2498
- canonicalHead: authoritativeCanonicalHead,
2499
- scopeKey,
2500
- nowMs: Date.now(),
2501
- skipVisibleMirror: overrides.skipVisibleMirror,
2502
- resetToCanonical: overrides.resetToCanonical,
2503
- incidentActive: activeWorkspaceIncidentId !== null,
2504
- workerBusy,
2505
- pendingCheckouts: scopedPendingTargets.length > 0 || Boolean(overrides.newVisibleCheckouts?.length)
2506
- });
2507
- if (fastPathDecision.hit) {
2508
- prepareStartedAt = queueEnteredAt;
2509
- synchronizeStartedAt = queueEnteredAt;
2510
- synchronizeFinishedAt = queueEnteredAt;
2511
- return {
2512
- type: "workspace_sync",
2513
- attemptId: overrides.attemptId ?? crypto.randomUUID(),
2514
- workerLabel: label,
2515
- trigger,
2516
- outcome: "no_change",
2517
- startingHead: fastPathDecision.canonicalHead,
2518
- expectedHead: fastPathDecision.canonicalHead,
2519
- publishedHead: fastPathDecision.canonicalHead,
2520
- rebaseCount: 0,
2521
- diffSizeBytes: 0,
2522
- gitStatus: "",
2523
- affectedProjects: [],
2524
- affectedPaths: [],
2525
- discardedPaths: [],
2526
- localChangesDiscarded: false
2527
- };
2528
- }
2529
2679
  prepareStartedAt = Date.now();
2530
2680
  let checkoutTargets = [];
2531
2681
  if (shouldEnsureCheckouts) {
@@ -2537,7 +2687,7 @@ async function startWorker(options) {
2537
2687
  });
2538
2688
  }
2539
2689
  pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
2540
- const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets, {
2690
+ const createdCheckouts = ensureVisibleWorktrees(checkoutTargets, {
2541
2691
  strictCanonicalRevalidation: true
2542
2692
  });
2543
2693
  const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
@@ -2551,24 +2701,12 @@ async function startWorker(options) {
2551
2701
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2552
2702
  });
2553
2703
  synchronizeStartedAt = Date.now();
2554
- recentWorkspaceSyncProof.invalidate();
2555
- const observationGeneration = recentWorkspaceSyncProof.beginObservation();
2556
- const workerWasBusyDuringObservation = workerBusy;
2557
2704
  let synchronized;
2558
2705
  try {
2559
2706
  synchronized = await synchronizeWorkspace(input);
2560
2707
  } finally {
2561
2708
  synchronizeFinishedAt = Date.now();
2562
2709
  }
2563
- if ((synchronized.outcome === "no_change" || synchronized.outcome === "updated") && pendingTargets.length === 0) {
2564
- recentWorkspaceSyncProof.recordStable({
2565
- observationGeneration,
2566
- canonicalHead: synchronized.publishedHead ?? null,
2567
- scopeKey,
2568
- observedAtMs: synchronizeFinishedAt,
2569
- workerBusy: workerWasBusyDuringObservation || activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress || activeWorkspaceIncidentId !== null
2570
- });
2571
- }
2572
2710
  return synchronized;
2573
2711
  });
2574
2712
  const completedAt = Date.now();
@@ -2578,8 +2716,7 @@ async function startWorker(options) {
2578
2716
  totalMs: completedAt - requestedAt,
2579
2717
  queueMs: queueEnteredAt - requestedAt,
2580
2718
  prepareMs: synchronizeStartedAt - prepareStartedAt,
2581
- synchronizeMs: synchronizeFinishedAt - synchronizeStartedAt,
2582
- fastPath: workspaceSyncFastPathTelemetry(fastPathDecision)
2719
+ synchronizeMs: synchronizeFinishedAt - synchronizeStartedAt
2583
2720
  }
2584
2721
  };
2585
2722
  if (resultWithTelemetry.outcome === "no_change" || resultWithTelemetry.outcome === "published" || resultWithTelemetry.outcome === "updated" || resultWithTelemetry.outcome === "conflict_reset") {
@@ -2588,7 +2725,6 @@ async function startWorker(options) {
2588
2725
  if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
2589
2726
  }
2590
2727
  }
2591
- previousPeriodicFingerprint = null;
2592
2728
  sendWorkspaceSyncResult(authority, resultWithTelemetry);
2593
2729
  return resultWithTelemetry;
2594
2730
  } catch (error) {
@@ -2611,12 +2747,10 @@ async function startWorker(options) {
2611
2747
  totalMs: completedAt - requestedAt,
2612
2748
  queueMs: queueEnteredAt - requestedAt,
2613
2749
  prepareMs: Math.max(0, synchronizeStartedAt - prepareStartedAt),
2614
- synchronizeMs: Math.max(0, synchronizeFinishedAt - synchronizeStartedAt),
2615
- fastPath: workspaceSyncFastPathTelemetry(fastPathDecision)
2750
+ synchronizeMs: Math.max(0, synchronizeFinishedAt - synchronizeStartedAt)
2616
2751
  },
2617
2752
  error: error instanceof Error ? error.message : String(error)
2618
2753
  };
2619
- previousPeriodicFingerprint = null;
2620
2754
  sendWorkspaceSyncResult(authority, result);
2621
2755
  return result;
2622
2756
  } finally {
@@ -2632,6 +2766,24 @@ async function startWorker(options) {
2632
2766
  let heartbeatBusyGrace = null;
2633
2767
  let heartbeatTimedOut = false;
2634
2768
  let heartbeatWatchdog;
2769
+ const handleGracefulShutdown = () => {
2770
+ if (shutdownAfterClose) return;
2771
+ shutdownAfterClose = true;
2772
+ void waitForWorkspacePublicationOnShutdown({
2773
+ snapshot: () => workspaceConvergence.snapshot(),
2774
+ forcePublication: () => armWorkspacePublication({ type: "manual", detail: "graceful worker shutdown" }, { delayMs: 0 })
2775
+ }).then((outcome) => {
2776
+ process.stdout.write(`[r5d-worker] graceful workspace publication ${outcome}
2777
+ `);
2778
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
2779
+ ws.close(1e3, "Worker shutting down");
2780
+ } else {
2781
+ process.exit(0);
2782
+ }
2783
+ });
2784
+ };
2785
+ process.on("SIGTERM", handleGracefulShutdown);
2786
+ process.on("SIGINT", handleGracefulShutdown);
2635
2787
  const stopHeartbeatWatchdog = () => {
2636
2788
  if (heartbeatWatchdog) {
2637
2789
  clearInterval(heartbeatWatchdog);
@@ -2662,7 +2814,8 @@ async function startWorker(options) {
2662
2814
  updateClis: true,
2663
2815
  canonicalResolverCheckout: true,
2664
2816
  browserPortForwarding: true,
2665
- globalWorkspaceSyncLeaseV1: true
2817
+ globalWorkspaceSyncLeaseV1: true,
2818
+ eventualWorkspaceSyncV1: true
2666
2819
  },
2667
2820
  projectRoot: projectsRoot,
2668
2821
  artifactRoot,
@@ -2689,14 +2842,19 @@ async function startWorker(options) {
2689
2842
  return;
2690
2843
  }
2691
2844
  if (message.type === "workspace_manifest") {
2692
- recentWorkspaceSyncProof.invalidate();
2693
- await workspaceSyncSingleFlight.runMutation(() => {
2845
+ const revisionToken = workspaceManifestRevision.begin(message.revision);
2846
+ workspaceReady = false;
2847
+ const manifestFilesystem = inspectWorkspaceManifestFilesystem({
2848
+ projectsRoot,
2849
+ workspaceShadowRoot,
2850
+ projects: message.projects
2851
+ });
2852
+ const completed = await workspaceSyncSingleFlight.runExclusive(async () => {
2853
+ if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2854
+ const firstBootstrap = manifestFilesystem.firstBootstrap;
2694
2855
  configureGitHubAuth(message.githubCredential);
2695
2856
  visibleGitIdentity = message.gitIdentity;
2696
2857
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2697
- const previousCheckoutKeys = new Set(
2698
- allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2699
- );
2700
2858
  const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
2701
2859
  manifestByProjectId.clear();
2702
2860
  for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
@@ -2709,80 +2867,77 @@ async function startWorker(options) {
2709
2867
  }
2710
2868
  for (const checkout of currentCheckouts) {
2711
2869
  const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2712
- if (previousCheckoutKeys.has(key)) continue;
2713
2870
  const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2714
2871
  if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2715
2872
  }
2716
- previousPeriodicFingerprint = null;
2873
+ if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2874
+ const created = firstBootstrap || migratedProjectIds.length > 0 ? ensureVisibleWorktrees(currentCheckouts, { strictCanonicalRevalidation: true }) : [];
2875
+ if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2876
+ const bootstrapped = await convergeWorkspaceHead(workspaceSyncInput({ type: "connect" }, { newVisibleCheckouts: created }), {
2877
+ // An existing checkout becomes routable after a fetch. Its
2878
+ // visible tree converges asynchronously; first bootstrap alone
2879
+ // must materialize the canonical snapshot before readiness.
2880
+ // Re-check the incident immediately before convergence so a
2881
+ // concurrently delivered fence forces fetch-only behavior.
2882
+ hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
2883
+ });
2884
+ if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2885
+ workspaceConvergence.setLocalHead(bootstrapped.localHead);
2886
+ workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
2887
+ for (const checkout of currentCheckouts) {
2888
+ const manifest = manifestByProjectId.get(checkout.projectId);
2889
+ if (!manifest) continue;
2890
+ const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2891
+ if (hasNormalVisibleGitDir(branchPath)) {
2892
+ pendingManifestCheckouts.delete(manifestCheckoutKey(checkout.projectId, checkout.branchName));
2893
+ }
2894
+ }
2717
2895
  process.stdout.write(
2718
2896
  `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2719
2897
  `
2720
2898
  );
2899
+ return true;
2721
2900
  });
2722
- if (!periodicWorkspaceScan) {
2723
- const emptyFingerprint = createHash("sha256").update("").digest("hex");
2724
- periodicWorkspaceScan = setInterval(() => {
2725
- if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2726
- periodicWorkspaceScanInFlight = true;
2901
+ if (!completed || !workspaceManifestRevision.complete(revisionToken)) return;
2902
+ workspaceReady = true;
2903
+ sendWorkspaceReady();
2904
+ schedulePendingManifestCheckoutHydration();
2905
+ if (!workspaceSafetyScan) {
2906
+ workspaceSafetyScan = setInterval(() => {
2907
+ if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || workspaceSafetyScanInFlight) {
2908
+ return;
2909
+ }
2910
+ workspaceSafetyScanInFlight = true;
2727
2911
  void (async () => {
2728
- const genericCheckoutKeys = new Set(
2729
- workspaceProjectsForSync([...manifestByProjectId.values()], { type: "periodic" }).flatMap(
2730
- (project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName))
2731
- )
2912
+ const before = workspaceConvergence.snapshot();
2913
+ const hydrateVisible = before.dirtyGeneration === before.publishedGeneration;
2914
+ const converged = await workspaceSyncSingleFlight.runExclusive(
2915
+ () => convergeWorkspaceHead(workspaceSyncInput({ type: "periodic", detail: "idle convergence scan" }), {
2916
+ hydrateVisible
2917
+ })
2732
2918
  );
2733
- const hasPendingGenericCheckout = [...pendingManifestCheckouts.values()].some(
2734
- (target) => genericCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2735
- );
2736
- if (hasPendingGenericCheckout) {
2737
- recentWorkspaceSyncProof.invalidate();
2738
- previousPeriodicFingerprint = null;
2739
- requestWorkspaceSync({
2740
- type: "periodic",
2741
- detail: "workspace manifest additions"
2742
- });
2743
- return;
2919
+ workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
2920
+ if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
2921
+ if (converged.localChanges && hydrateVisible) {
2922
+ markWorkspaceDirty({ type: "periodic", detail: "idle safety scan" });
2744
2923
  }
2745
- const observation = await workspaceSyncSingleFlight.runExclusive(() => {
2746
- const observationGeneration = recentWorkspaceSyncProof.beginObservation();
2747
- const input = workspaceSyncInput({ type: "periodic" });
2748
- const workerBusy = activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress;
2749
- return {
2750
- observationGeneration,
2751
- scopeKey: workspaceSyncScopeKey(input.remoteUrl, input.projects),
2752
- fingerprint: calculateWorkspaceDiffFingerprint(input),
2753
- canonicalHead: readShadowWorkspaceCanonicalHead(workspaceShadowRoot),
2754
- workerBusy
2755
- };
2756
- });
2757
- if (observation.fingerprint === emptyFingerprint) {
2758
- recentWorkspaceSyncProof.recordStable({
2759
- observationGeneration: observation.observationGeneration,
2760
- canonicalHead: observation.canonicalHead,
2761
- scopeKey: observation.scopeKey,
2762
- observedAtMs: Date.now(),
2763
- workerBusy: observation.workerBusy || activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress
2764
- });
2765
- previousPeriodicFingerprint = null;
2766
- return;
2767
- }
2768
- recentWorkspaceSyncProof.invalidate();
2769
- if (previousPeriodicFingerprint !== observation.fingerprint) {
2770
- previousPeriodicFingerprint = observation.fingerprint;
2771
- return;
2772
- }
2773
- previousPeriodicFingerprint = null;
2774
- requestWorkspaceSync({ type: "periodic" }, observation.fingerprint);
2924
+ sendWorkspaceReady();
2775
2925
  })().catch((error) => {
2776
2926
  process.stderr.write(
2777
- `[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
2927
+ `[r5d-worker] idle workspace safety scan failed: ${error instanceof Error ? error.message : String(error)}
2778
2928
  `
2779
2929
  );
2780
2930
  }).finally(() => {
2781
- periodicWorkspaceScanInFlight = false;
2931
+ workspaceSafetyScanInFlight = false;
2782
2932
  heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
2783
2933
  });
2784
- }, WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
2785
- periodicWorkspaceScan.unref();
2934
+ }, WORKSPACE_IDLE_SAFETY_SCAN_MS);
2935
+ workspaceSafetyScan.unref();
2936
+ }
2937
+ pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
2938
+ void convergeAvailableWorkspaceHead();
2939
+ if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
2940
+ armWorkspacePublication({ type: "periodic", detail: "resume dirty generation after reconnect" }, { delayMs: 0 });
2786
2941
  }
2787
2942
  return;
2788
2943
  }
@@ -2802,12 +2957,181 @@ async function startWorker(options) {
2802
2957
  confirmationReason: message.confirmationReason,
2803
2958
  resetToCanonical: message.resetToCanonical,
2804
2959
  skipVisibleMirror: message.skipVisibleMirror
2805
- },
2806
- message.canonicalHead,
2807
- message.allowRecentNoChangeFastPath
2960
+ }
2808
2961
  );
2809
2962
  return;
2810
2963
  }
2964
+ if (message.type === "workspace_head_available") {
2965
+ workspaceConvergence.observeCanonicalHead(message.canonicalHead);
2966
+ pendingWorkspaceHead = message.canonicalHead;
2967
+ sendWorkspaceReady();
2968
+ void convergeAvailableWorkspaceHead();
2969
+ return;
2970
+ }
2971
+ if (message.type === "workspace_publication_now") {
2972
+ armWorkspacePublication(message.trigger, { delayMs: 0, requestId: message.requestId });
2973
+ return;
2974
+ }
2975
+ if (message.type === "workspace_publication_authority") {
2976
+ const publication = pendingPublication;
2977
+ if (!publication || publication.requestId !== message.requestId) {
2978
+ process.stderr.write(`[r5d-worker] ignoring publication authority for unknown request ${message.requestId}
2979
+ `);
2980
+ return;
2981
+ }
2982
+ if (message.status !== "granted") {
2983
+ pendingPublication = void 0;
2984
+ workspaceConvergence.observeCanonicalHead(message.canonicalHead);
2985
+ const applyBlockedResponse = message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId);
2986
+ if (applyBlockedResponse) {
2987
+ workspaceConvergence.block();
2988
+ if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
2989
+ } else {
2990
+ workspaceConvergence.resetTransientPublicationState();
2991
+ workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
2992
+ if (message.status === "busy") {
2993
+ armWorkspacePublication(publication.trigger, {
2994
+ delayMs: message.retryAfterMs ?? 1e3,
2995
+ requestId: publication.requestId
2996
+ });
2997
+ } else if (!activeWorkspaceIncidentId) {
2998
+ const explicit = explicitWorkspacePublications.next();
2999
+ if (explicit) {
3000
+ armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
3001
+ } else if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3002
+ armWorkspacePublication(publication.trigger, { delayMs: 0 });
3003
+ }
3004
+ }
3005
+ }
3006
+ sendWorkspaceReady();
3007
+ return;
3008
+ }
3009
+ publication.attemptId = message.attemptId;
3010
+ publication.authorityToken = message.authorityToken;
3011
+ publication.workInFlight = true;
3012
+ pendingPublication = publication;
3013
+ if (currentWorkerSocket !== ws) {
3014
+ publication.workInFlight = false;
3015
+ workspaceConvergence.resetTransientPublicationState();
3016
+ return;
3017
+ }
3018
+ workspaceConvergence.beginPreparing();
3019
+ sendWorkspaceReady();
3020
+ let prepareStartedAt = Date.now();
3021
+ let synchronizeStartedAt = prepareStartedAt;
3022
+ let synchronizeStarted = false;
3023
+ let result;
3024
+ try {
3025
+ result = await workspaceSyncSingleFlight.runExclusive(async () => {
3026
+ prepareStartedAt = Date.now();
3027
+ const pendingTargets = [...pendingManifestCheckouts.values()];
3028
+ const createdCheckouts = ensureVisibleWorktrees(pendingTargets, { strictCanonicalRevalidation: true });
3029
+ synchronizeStartedAt = Date.now();
3030
+ synchronizeStarted = true;
3031
+ const prepared = await synchronizeWorkspace(
3032
+ workspaceSyncInput(publication.trigger, {
3033
+ attemptId: message.attemptId,
3034
+ quarantineRef: message.quarantineRef,
3035
+ expectedCanonicalHead: message.expectedHead,
3036
+ newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
3037
+ })
3038
+ );
3039
+ for (const target of pendingTargets) {
3040
+ pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
3041
+ }
3042
+ return prepared.outcome === "updated" ? { ...prepared, outcome: "no_change" } : prepared;
3043
+ });
3044
+ } catch (error) {
3045
+ if (!synchronizeStarted) synchronizeStartedAt = Date.now();
3046
+ result = {
3047
+ type: "workspace_sync",
3048
+ attemptId: message.attemptId,
3049
+ workerLabel: label,
3050
+ trigger: publication.trigger,
3051
+ outcome: "failed",
3052
+ startingHead: message.expectedHead,
3053
+ expectedHead: message.expectedHead,
3054
+ rebaseCount: 0,
3055
+ diffSizeBytes: 0,
3056
+ gitStatus: "",
3057
+ affectedProjects: [],
3058
+ affectedPaths: [],
3059
+ discardedPaths: [],
3060
+ localChangesDiscarded: false,
3061
+ error: error instanceof Error ? error.message : String(error)
3062
+ };
3063
+ }
3064
+ const finishedAt = Date.now();
3065
+ result = {
3066
+ ...result,
3067
+ dirtyGeneration: publication.dirtyGeneration,
3068
+ telemetry: workspacePublicationTelemetry({
3069
+ requestedAt: publication.requestedAt,
3070
+ prepareStartedAt,
3071
+ synchronizeStartedAt,
3072
+ finishedAt
3073
+ })
3074
+ };
3075
+ workspaceConvergence.beginSubmitting();
3076
+ sendWorkspaceReady();
3077
+ try {
3078
+ ws.send(
3079
+ JSON.stringify({
3080
+ type: "workspace_publication_result",
3081
+ requestId: publication.requestId,
3082
+ attemptId: message.attemptId,
3083
+ authorityToken: message.authorityToken,
3084
+ dirtyGeneration: publication.dirtyGeneration,
3085
+ result
3086
+ })
3087
+ );
3088
+ } catch (error) {
3089
+ workspaceConvergence.resetTransientPublicationState();
3090
+ throw error;
3091
+ } finally {
3092
+ publication.workInFlight = false;
3093
+ if (currentWorkerSocket !== ws) workspaceConvergence.resetTransientPublicationState();
3094
+ }
3095
+ return;
3096
+ }
3097
+ if (message.type === "workspace_publication_completed") {
3098
+ const publication = pendingPublication;
3099
+ if (!publication || publication.requestId !== message.requestId || publication.attemptId !== message.attemptId) {
3100
+ process.stderr.write(`[r5d-worker] ignoring completion for unknown publication ${message.requestId}
3101
+ `);
3102
+ return;
3103
+ }
3104
+ pendingPublication = void 0;
3105
+ workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3106
+ if (message.status === "published" || message.status === "no_change") {
3107
+ workspaceConvergence.complete({
3108
+ generation: Math.min(message.publishedGeneration ?? publication.dirtyGeneration, publication.dirtyGeneration),
3109
+ canonicalHead: message.canonicalHead,
3110
+ published: true
3111
+ });
3112
+ } else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
3113
+ workspaceConvergence.block();
3114
+ if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3115
+ } else {
3116
+ workspaceConvergence.resetTransientPublicationState();
3117
+ workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3118
+ }
3119
+ sendWorkspaceReady();
3120
+ const explicit = message.status === "retry" && publication.explicit ? { requestId: publication.requestId, trigger: publication.trigger } : explicitWorkspacePublications.next();
3121
+ if (explicit) {
3122
+ armWorkspacePublication(explicit.trigger, {
3123
+ delayMs: message.retryAfterMs ?? 0,
3124
+ requestId: explicit.requestId
3125
+ });
3126
+ } else if (message.status === "retry" || message.status === "failed" || workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3127
+ armWorkspacePublication(publication.trigger, { delayMs: message.retryAfterMs ?? 1e3 });
3128
+ }
3129
+ if (message.canonicalHead !== workspaceConvergence.snapshot().localHead) {
3130
+ pendingWorkspaceHead = message.canonicalHead;
3131
+ void convergeAvailableWorkspaceHead();
3132
+ }
3133
+ return;
3134
+ }
2811
3135
  if (message.type === "sync_session_artifacts") {
2812
3136
  sessionArtifactSyncRequestsInFlight += 1;
2813
3137
  try {
@@ -2835,18 +3159,27 @@ async function startWorker(options) {
2835
3159
  return;
2836
3160
  }
2837
3161
  if (message.type === "workspace_incident_updated") {
2838
- recentWorkspaceSyncProof.invalidate();
3162
+ workspaceIncidentOrdering.observe(message);
2839
3163
  const previousIncidentId = activeWorkspaceIncidentId;
2840
3164
  activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
2841
- previousPeriodicFingerprint = null;
2842
3165
  const releasedHead = releasePendingWorkspaceHead(
2843
3166
  previousIncidentId,
2844
3167
  activeWorkspaceIncidentId,
2845
3168
  message.status === "resolved" || message.status === "confirmed" || message.status === "reset" ? message.incidentId : null
2846
3169
  );
2847
- if (releasedHead.syncHead) {
2848
- requestWorkspaceSync({ type: "inbound_head", detail: releasedHead.syncHead });
3170
+ workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3171
+ if (releasedHead.fetchAuthoritativeHead) {
3172
+ pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
3173
+ void convergeAvailableWorkspaceHead();
2849
3174
  }
3175
+ const explicit = !activeWorkspaceIncidentId ? explicitWorkspacePublications.next() : void 0;
3176
+ if (explicit) {
3177
+ armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
3178
+ } else if (!activeWorkspaceIncidentId && workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3179
+ armWorkspacePublication({ type: "periodic", detail: "resume after workspace incident" }, { delayMs: 0 });
3180
+ }
3181
+ if (!activeWorkspaceIncidentId) schedulePendingManifestCheckoutHydration();
3182
+ sendWorkspaceReady();
2850
3183
  return;
2851
3184
  }
2852
3185
  if (message.type === "exec_terminal_ack") {
@@ -2882,7 +3215,6 @@ async function startWorker(options) {
2882
3215
  return;
2883
3216
  }
2884
3217
  cliUpdateInProgress = true;
2885
- recentWorkspaceSyncProof.invalidate();
2886
3218
  try {
2887
3219
  const result = await installCliUpdate(message);
2888
3220
  sendWorkerMessage(ws, {
@@ -2945,9 +3277,8 @@ async function startWorker(options) {
2945
3277
  });
2946
3278
  return;
2947
3279
  }
2948
- recentWorkspaceSyncProof.invalidate();
2949
3280
  const releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
2950
- let leaseTransferred = false;
3281
+ let mutationLeaseTransferred = false;
2951
3282
  try {
2952
3283
  const resolvedTarget = resolveMessageTarget(message.target);
2953
3284
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
@@ -2957,9 +3288,14 @@ async function startWorker(options) {
2957
3288
  message,
2958
3289
  resolvedTarget,
2959
3290
  planRoot,
2960
- ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {}
3291
+ ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
3292
+ ...targetMayMutateVisibleWorkspace(message.target) ? {
3293
+ onTerminal: () => {
3294
+ markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true);
3295
+ }
3296
+ } : {}
2961
3297
  });
2962
- leaseTransferred = releaseWorkspaceMutation !== void 0;
3298
+ mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
2963
3299
  } catch (error) {
2964
3300
  sendWorkerMessage(ws, {
2965
3301
  type: "pty_error",
@@ -2968,12 +3304,15 @@ async function startWorker(options) {
2968
3304
  error: error instanceof Error ? error.message : String(error)
2969
3305
  });
2970
3306
  } finally {
2971
- if (!leaseTransferred) releaseWorkspaceMutation?.();
3307
+ if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
2972
3308
  }
2973
3309
  return;
2974
3310
  }
2975
3311
  if (message.type === "pty_input") {
2976
- recentWorkspaceSyncProof.invalidate();
3312
+ const activePty = activePtys.get(message.ptyId);
3313
+ if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
3314
+ markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` });
3315
+ }
2977
3316
  writePty(ws, message);
2978
3317
  return;
2979
3318
  }
@@ -2982,8 +3321,11 @@ async function startWorker(options) {
2982
3321
  return;
2983
3322
  }
2984
3323
  if (message.type === "pty_close") {
2985
- recentWorkspaceSyncProof.invalidate();
3324
+ const activePty = activePtys.get(message.ptyId);
2986
3325
  closePty(message);
3326
+ if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
3327
+ armWorkspacePublication({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, { delayMs: 0 });
3328
+ }
2987
3329
  return;
2988
3330
  }
2989
3331
  if (message.type === "exec") {
@@ -2997,7 +3339,6 @@ async function startWorker(options) {
2997
3339
  });
2998
3340
  return;
2999
3341
  }
3000
- recentWorkspaceSyncProof.invalidate();
3001
3342
  let result;
3002
3343
  try {
3003
3344
  const runCommand = async () => {
@@ -3018,6 +3359,12 @@ async function startWorker(options) {
3018
3359
  };
3019
3360
  }
3020
3361
  ws.send(JSON.stringify(result));
3362
+ if (targetMayMutateVisibleWorkspace(message.target)) {
3363
+ markWorkspaceDirty(
3364
+ { type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
3365
+ true
3366
+ );
3367
+ }
3021
3368
  return;
3022
3369
  }
3023
3370
  if (message.type === "exec_start") {
@@ -3030,25 +3377,38 @@ async function startWorker(options) {
3030
3377
  });
3031
3378
  return;
3032
3379
  }
3033
- recentWorkspaceSyncProof.invalidate();
3034
3380
  sendWorkerMessage(ws, {
3035
3381
  type: "exec_accepted",
3036
3382
  requestId: message.requestId,
3037
3383
  runId: message.runId
3038
3384
  });
3039
3385
  const runCommand = async () => {
3040
- const resolvedTarget = resolveMessageTarget(message.target);
3041
- process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
3386
+ try {
3387
+ const resolvedTarget = resolveMessageTarget(message.target);
3388
+ process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
3042
3389
  `);
3043
- await executeStreamingCommand({
3044
- ws,
3045
- message,
3046
- resolvedTarget,
3047
- baseUrl,
3048
- token,
3049
- artifactRoot,
3050
- planRoot
3051
- });
3390
+ await executeStreamingCommand({
3391
+ ws,
3392
+ message,
3393
+ resolvedTarget,
3394
+ baseUrl,
3395
+ token,
3396
+ artifactRoot,
3397
+ planRoot
3398
+ });
3399
+ } finally {
3400
+ if (targetMayMutateVisibleWorkspace(message.target)) {
3401
+ markWorkspaceDirty(
3402
+ {
3403
+ type: "process_terminal",
3404
+ sessionId: message.sessionId,
3405
+ processRunId: message.runId,
3406
+ detail: "process completed"
3407
+ },
3408
+ true
3409
+ );
3410
+ }
3411
+ }
3052
3412
  };
3053
3413
  const execution = runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
3054
3414
  void execution.catch((error) => {
@@ -3062,9 +3422,8 @@ async function startWorker(options) {
3062
3422
  return;
3063
3423
  }
3064
3424
  if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
3065
- if (message.type === "write" || message.type === "edit") recentWorkspaceSyncProof.invalidate();
3066
3425
  try {
3067
- const result = await workspaceSyncSingleFlight.runMutation(async () => {
3426
+ const result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
3068
3427
  const resolvedTarget = resolveMessageTarget(message.target);
3069
3428
  return await executeOperation({
3070
3429
  message,
@@ -3082,6 +3441,14 @@ async function startWorker(options) {
3082
3441
  result
3083
3442
  })
3084
3443
  );
3444
+ if ((message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target)) {
3445
+ markWorkspaceDirty({
3446
+ type: message.type,
3447
+ sessionId: message.sessionId,
3448
+ toolCallId: message.requestId,
3449
+ ...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
3450
+ });
3451
+ }
3085
3452
  } catch (error) {
3086
3453
  ws.send(
3087
3454
  JSON.stringify({
@@ -3111,11 +3478,23 @@ async function startWorker(options) {
3111
3478
  });
3112
3479
  });
3113
3480
  ws.addEventListener("close", (event) => {
3114
- recentWorkspaceSyncProof.invalidate();
3481
+ process.off("SIGTERM", handleGracefulShutdown);
3482
+ process.off("SIGINT", handleGracefulShutdown);
3115
3483
  stopHeartbeatWatchdog();
3116
- if (periodicWorkspaceScan) {
3117
- clearInterval(periodicWorkspaceScan);
3118
- periodicWorkspaceScan = void 0;
3484
+ if (workspacePublicationTimer) {
3485
+ clearTimeout(workspacePublicationTimer);
3486
+ workspacePublicationTimer = void 0;
3487
+ }
3488
+ if (!pendingPublication?.workInFlight) {
3489
+ workspaceConvergence.resetTransientPublicationState();
3490
+ }
3491
+ if (workspaceSafetyScan) {
3492
+ clearInterval(workspaceSafetyScan);
3493
+ workspaceSafetyScan = void 0;
3494
+ }
3495
+ if (workspaceManifestHydrationTimer) {
3496
+ clearTimeout(workspaceManifestHydrationTimer);
3497
+ workspaceManifestHydrationTimer = void 0;
3119
3498
  }
3120
3499
  if (terminalReplayTimer) {
3121
3500
  clearInterval(terminalReplayTimer);
@@ -3131,6 +3510,9 @@ async function startWorker(options) {
3131
3510
  if (reloadAfterClose) {
3132
3511
  process.exit(WORKER_RELOAD_EXIT_CODE);
3133
3512
  }
3513
+ if (shutdownAfterClose) {
3514
+ process.exit(0);
3515
+ }
3134
3516
  if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
3135
3517
  process.stderr.write(
3136
3518
  `[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms