@ricsam/r5d-worker 0.0.101 → 0.0.104

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
@@ -34,6 +34,8 @@ import {
34
34
  runWorkspaceCommand
35
35
  } from "./workspace-command-sync-policy.mjs";
36
36
  import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
37
+ import { WorkspaceProjectionLedger } from "./workspace-projection-ledger.mjs";
38
+ import { WorkspaceSyncCoalescer } from "./workspace-sync-coalescer.mjs";
37
39
  import { checkoutPathMovePublicationComplete, preserveProjectCheckoutPathMove } from "./workspace-path-move.mjs";
38
40
  import { busyProjectConfigurationChangeIds, deferredProjectConfigurationPendingBranches } from "./workspace-project-config-policy.mjs";
39
41
  import { creatorLocalProjectBranchIsAuthorized, workerProjectBranchDisposition } from "./workspace-preserve-only-policy.mjs";
@@ -58,11 +60,12 @@ import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchNa
58
60
  import {
59
61
  applyObservedProjectMirrorHeads,
60
62
  assertProjectMirrorHeadsPushed,
61
- computeLocalProjectMirrorRefsToken,
63
+ computeLocalProjectMirrorRefsTokens,
62
64
  createOrRetryLinkedProjectBranch,
63
65
  deleteLinkedProjectBranch,
64
66
  deleteProjectMirrorBranch,
65
67
  ensureProjectWorktrees,
68
+ inventoryProjectCheckouts,
66
69
  observeProjectMirrorHeads,
67
70
  projectBranchMayExistAfterCreateFailure,
68
71
  projectOriginBranchName,
@@ -71,7 +74,10 @@ import {
71
74
  pushProjectMirrorHeads,
72
75
  removeProjectWorktrees
73
76
  } from "./project-worktrees.mjs";
74
- import { projectMirrorFetchDecision } from "./project-mirror-fetch-policy.mjs";
77
+ import {
78
+ parseProjectMirrorRefsTokensMessage,
79
+ planProjectMirrorObservationFetch
80
+ } from "./project-mirror-fetch-policy.mjs";
75
81
  import {
76
82
  PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
77
83
  recoverProjectSnapshotsInChild,
@@ -182,6 +188,12 @@ const workspaceSyncSingleFlight = {
182
188
  acquireMutation() {
183
189
  return workspaceMutationGate.acquireMutation();
184
190
  },
191
+ runBranchMutation(operation) {
192
+ return workspaceMutationGate.runBranchMutation(operation);
193
+ },
194
+ branchMutationQueuePosition() {
195
+ return workspaceMutationGate.branchMutationQueuePosition();
196
+ },
185
197
  afterCurrent() {
186
198
  return workspaceSyncQueue;
187
199
  }
@@ -312,6 +324,7 @@ const workerPtyTestHarness = {
312
324
  removeEnvFiles: removePtyEnvFiles,
313
325
  resolveEnvFileReferences: resolvePtyEnvFileReferences,
314
326
  commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
327
+ targetProcessEnv,
315
328
  canonicalSyncTerminalHead,
316
329
  ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
317
330
  parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
@@ -319,21 +332,12 @@ const workerPtyTestHarness = {
319
332
  function defaultConfigPath() {
320
333
  return path.join(os.homedir(), ".config", "r5d", "r5dctl", "config.json");
321
334
  }
322
- function defaultProjectsRoot() {
323
- return path.join(os.homedir(), ".r5d", "projects");
324
- }
325
- function defaultSyncRoot() {
326
- return path.join(os.homedir(), ".r5d", "sync");
327
- }
328
- function defaultArtifactRoot() {
329
- return path.join(os.homedir(), ".r5d", "artifacts");
330
- }
331
- function defaultPlanRoot() {
332
- return path.join(os.homedir(), ".r5d", "plans");
335
+ function defaultRootDir() {
336
+ return path.join(os.homedir(), ".r5d");
333
337
  }
334
338
  function printHelp() {
335
339
  process.stdout.write(`Usage:
336
- r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
340
+ r5d-worker start --label <label> [--root <dir>] [--base-url <url>] [--token <token>] [--api-key <key>]
337
341
  r5d-worker --version
338
342
 
339
343
  Options:
@@ -342,10 +346,7 @@ Options:
342
346
  --token <token> r5d worker token
343
347
  --api-key <key> r5d API key
344
348
  --config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
345
- --project-root <dir> Override projects checkout root (default ~/.r5d/projects)
346
- --sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
347
- --artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
348
- --plan-root <dir> Override synced plan root (default from R5D_PLANS_ROOT or ~/.r5d/plans)
349
+ --root <dir> Override worker data root (default from R5D_ROOT or ~/.r5d)
349
350
  -v, --version Show r5d-worker version
350
351
  -h, --help Show this help
351
352
  `);
@@ -460,24 +461,9 @@ function parseArgs(argv) {
460
461
  options.label = label;
461
462
  continue;
462
463
  }
463
- const projectRoot = readOption("--project-root");
464
- if (projectRoot !== void 0) {
465
- options.projectRoot = projectRoot;
466
- continue;
467
- }
468
- const syncRoot = readOption("--sync-root");
469
- if (syncRoot !== void 0) {
470
- options.syncRoot = syncRoot;
471
- continue;
472
- }
473
- const artifactRoot = readOption("--artifact-root");
474
- if (artifactRoot !== void 0) {
475
- options.artifactRoot = artifactRoot;
476
- continue;
477
- }
478
- const planRoot = readOption("--plan-root");
479
- if (planRoot !== void 0) {
480
- options.planRoot = planRoot;
464
+ const rootDir = readOption("--root");
465
+ if (rootDir !== void 0) {
466
+ options.rootDir = rootDir;
481
467
  continue;
482
468
  }
483
469
  rest.push(arg);
@@ -522,11 +508,9 @@ async function readResponseText(response) {
522
508
  return "";
523
509
  }
524
510
  }
525
- const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
526
- const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
527
- const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
528
- const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
529
- const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
511
+ const R5D_ROOT_REF = "$R5D_ROOT";
512
+ const R5D_ROOT_ENV = "R5D_ROOT";
513
+ const BUILT_IN_TOOL_PATH_REFS = [R5D_ROOT_REF];
530
514
  function parseBuiltInToolPath(inputPath) {
531
515
  const normalized = inputPath.replace(/\\/g, "/");
532
516
  const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
@@ -542,6 +526,26 @@ function parseBuiltInToolPath(inputPath) {
542
526
  relativePath: match[3] ?? ""
543
527
  };
544
528
  }
529
+ function expandRootPathSegments(relativePath, builtInPaths) {
530
+ const values = {
531
+ R5D_SESSION_ID: builtInPaths.sessionId,
532
+ R5D_PROJECT_ID: builtInPaths.projectId,
533
+ R5D_BRANCH_NAME: builtInPaths.branchName,
534
+ R5D_ACTIVE_PLAN_ID: builtInPaths.activePlanId
535
+ };
536
+ let expandedPath = relativePath;
537
+ for (const [variableName, value] of Object.entries(values)) {
538
+ const variablePattern = new RegExp(`\\$(?:\\{${variableName}\\}|${variableName})(?=\\/|\\.|$)`, "g");
539
+ if (!variablePattern.test(expandedPath)) continue;
540
+ if (!value) throw new Error(`$${variableName} is unavailable for this worker path`);
541
+ expandedPath = expandedPath.replace(variablePattern, value);
542
+ }
543
+ const unsupportedVariable = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/.exec(expandedPath);
544
+ if (unsupportedVariable) {
545
+ throw new Error(`Unsupported ${R5D_ROOT_REF} path variable "$${unsupportedVariable[1] ?? unsupportedVariable[2]}"`);
546
+ }
547
+ return expandedPath.split("/").filter(Boolean);
548
+ }
545
549
  function validateArtifactSessionId(sessionId) {
546
550
  if (!/^(migration-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
547
551
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -557,14 +561,6 @@ function sessionArtifactDir(artifactRoot, sessionId) {
557
561
  validateArtifactSessionId(sessionId);
558
562
  return path.join(artifactRoot, sessionId);
559
563
  }
560
- function artifactEnv(artifactRoot, sessionId) {
561
- if (!sessionId) {
562
- return {};
563
- }
564
- return {
565
- R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
566
- };
567
- }
568
564
  function projectPlanDir(planRoot, projectId, branchName) {
569
565
  validateProjectId(projectId);
570
566
  validateBranchName(branchName);
@@ -577,21 +573,18 @@ function targetPlanDir(planRoot, target) {
577
573
  }
578
574
  return path.join(planRoot, "workspace");
579
575
  }
580
- function planEnv(planRoot, target, activePlanId) {
581
- const plansDir = targetPlanDir(planRoot, target);
582
- const env = {
583
- [R5D_PLANS_DIR_ENV]: plansDir
584
- };
576
+ function planEnv(activePlanId) {
577
+ const env = {};
585
578
  if (activePlanId) {
586
579
  validatePlanId(activePlanId);
587
580
  env.R5D_ACTIVE_PLAN_ID = activePlanId;
588
- env.R5D_ACTIVE_PLAN_FILE = path.join(plansDir, `${activePlanId}.plan.md`);
589
581
  }
590
582
  return env;
591
583
  }
592
584
  function isArtifactEnvPath(filePath) {
593
585
  try {
594
- return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
586
+ const parsed = parseBuiltInToolPath(filePath);
587
+ return parsed?.ref === R5D_ROOT_REF && parsed.relativePath.split("/").filter(Boolean)[0] === "artifacts";
595
588
  } catch {
596
589
  return false;
597
590
  }
@@ -733,48 +726,46 @@ async function prepareBuiltInToolPaths(input) {
733
726
  if (!parsed) {
734
727
  return void 0;
735
728
  }
736
- if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
729
+ const builtInPaths = {
730
+ rootDir: input.rootDir,
731
+ sessionId: input.sessionId,
732
+ activePlanId: input.activePlanId,
733
+ ...input.target.type === "project" ? { projectId: input.target.projectId, branchName: input.target.branchName } : {}
734
+ };
735
+ const rootPathSegments = expandRootPathSegments(parsed.relativePath, builtInPaths);
736
+ if (rootPathSegments[0] === "artifacts") {
737
737
  if (input.access === "write") {
738
738
  throw new Error(
739
- `${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
739
+ `${R5D_ROOT_REF}/artifacts attachments are read-only. Copy them into a project path before editing or creating files.`
740
740
  );
741
741
  }
742
- if (!input.sessionId) {
743
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
744
- }
745
- return {
746
- artifactsDir: await syncSessionArtifacts({
747
- baseUrl: input.baseUrl,
748
- token: input.token,
749
- sessionId: input.sessionId,
750
- artifactRoot: input.artifactRoot
751
- })
752
- };
753
- }
754
- const plansDir = targetPlanDir(input.planRoot, input.target);
755
- fs.mkdirSync(plansDir, { recursive: true });
756
- if (parsed.ref === R5D_PLANS_DIR_REF) {
757
- return { plansDir };
742
+ const referencedSessionId = rootPathSegments[1];
743
+ if (!referencedSessionId) return builtInPaths;
744
+ validateArtifactSessionId(referencedSessionId);
745
+ await syncSessionArtifacts({
746
+ baseUrl: input.baseUrl,
747
+ token: input.token,
748
+ sessionId: referencedSessionId,
749
+ artifactRoot: path.join(input.rootDir, "artifacts")
750
+ });
758
751
  }
759
- if (!input.activePlanId) {
760
- throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
752
+ if (rootPathSegments[0] === "plans") {
753
+ fs.mkdirSync(targetPlanDir(path.join(input.rootDir, "plans"), input.target), { recursive: true });
761
754
  }
762
- validatePlanId(input.activePlanId);
763
- return {
764
- plansDir,
765
- activePlanFile: path.join(plansDir, `${input.activePlanId}.plan.md`)
766
- };
755
+ return builtInPaths;
767
756
  }
768
757
  async function prepareArtifactEnvForShell(input) {
758
+ const env = { [R5D_ROOT_ENV]: input.rootDir };
769
759
  if (!input.sessionId) {
770
- return {};
760
+ return env;
771
761
  }
762
+ const artifactRoot = path.join(input.rootDir, "artifacts");
772
763
  try {
773
764
  await syncSessionArtifacts({
774
765
  baseUrl: input.baseUrl,
775
766
  token: input.token,
776
767
  sessionId: input.sessionId,
777
- artifactRoot: input.artifactRoot
768
+ artifactRoot
778
769
  });
779
770
  } catch (error) {
780
771
  process.stderr.write(
@@ -782,16 +773,14 @@ async function prepareArtifactEnvForShell(input) {
782
773
  `
783
774
  );
784
775
  }
785
- const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
776
+ const artifactsDir = sessionArtifactDir(artifactRoot, input.sessionId);
786
777
  fs.mkdirSync(artifactsDir, { recursive: true });
787
- return {
788
- R5D_ARTIFACTS_DIR: artifactsDir
789
- };
778
+ return env;
790
779
  }
791
780
  function preparePlanEnvForShell(input) {
792
- const plansDir = targetPlanDir(input.planRoot, input.target);
781
+ const plansDir = targetPlanDir(path.join(input.rootDir, "plans"), input.target);
793
782
  fs.mkdirSync(plansDir, { recursive: true });
794
- return planEnv(input.planRoot, input.target, input.activePlanId);
783
+ return planEnv(input.activePlanId);
795
784
  }
796
785
  async function verifyR5dctlAuth(baseUrl, token, workerLabel) {
797
786
  const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
@@ -959,30 +948,17 @@ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
959
948
  if (parsed.relativePath.split("/").includes("..")) {
960
949
  throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
961
950
  }
962
- let rootPath;
963
- let absolutePath;
964
- if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
965
- rootPath = builtInPaths?.artifactsDir;
966
- } else if (parsed.ref === R5D_PLANS_DIR_REF) {
967
- rootPath = builtInPaths?.plansDir;
968
- } else {
969
- if (parsed.relativePath) {
970
- throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
971
- }
972
- rootPath = builtInPaths?.plansDir;
973
- absolutePath = builtInPaths?.activePlanFile;
974
- }
975
- if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
976
- const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
977
- throw new Error(`${parsed.ref} paths require ${requirement}`);
978
- }
979
- const normalizedPath = parsed.relativePath ? path.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
951
+ const rootPath = builtInPaths?.rootDir;
952
+ if (!rootPath) throw new Error(`${R5D_ROOT_REF} paths require an available worker root`);
953
+ const expandedPath = expandRootPathSegments(parsed.relativePath, builtInPaths);
954
+ if (expandedPath.includes("..")) throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
955
+ const normalizedPath = expandedPath.length > 0 ? path.posix.normalize(expandedPath.join("/")).replace(/^\/+|\/+$/g, "") : "";
980
956
  const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
981
- absolutePath ??= normalizedRelativePath ? path.resolve(rootPath, ...normalizedRelativePath.split("/")) : path.resolve(rootPath);
957
+ const absolutePath = normalizedRelativePath ? path.resolve(rootPath, ...normalizedRelativePath.split("/")) : path.resolve(rootPath);
982
958
  assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
983
959
  return {
984
960
  absolutePath,
985
- displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
961
+ displayPath: parsed.relativePath ? `${parsed.ref}/${parsed.relativePath}` : parsed.ref,
986
962
  repoRelativePath: null,
987
963
  scope: "virtual",
988
964
  virtualRootPath: path.resolve(rootPath)
@@ -2226,8 +2202,7 @@ async function executeReadFileOperation(input) {
2226
2202
  token: input.token,
2227
2203
  sessionId: input.message.sessionId,
2228
2204
  activePlanId: input.message.activePlanId,
2229
- artifactRoot: input.artifactRoot,
2230
- planRoot: input.planRoot,
2205
+ rootDir: input.rootDir,
2231
2206
  access: "read"
2232
2207
  });
2233
2208
  return readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
@@ -2240,8 +2215,7 @@ async function executeWriteFileOperation(input) {
2240
2215
  token: input.token,
2241
2216
  sessionId: input.message.sessionId,
2242
2217
  activePlanId: input.message.activePlanId,
2243
- artifactRoot: input.artifactRoot,
2244
- planRoot: input.planRoot,
2218
+ rootDir: input.rootDir,
2245
2219
  access: "write"
2246
2220
  });
2247
2221
  input.assertAdmission();
@@ -2259,8 +2233,7 @@ async function executeEditFileOperation(input) {
2259
2233
  token: input.token,
2260
2234
  sessionId: input.message.sessionId,
2261
2235
  activePlanId: input.message.activePlanId,
2262
- artifactRoot: input.artifactRoot,
2263
- planRoot: input.planRoot,
2236
+ rootDir: input.rootDir,
2264
2237
  access: "write"
2265
2238
  });
2266
2239
  input.assertAdmission();
@@ -2387,8 +2360,7 @@ async function executeGrepOperation(input) {
2387
2360
  token: input.token,
2388
2361
  sessionId: input.message.sessionId,
2389
2362
  activePlanId: input.message.activePlanId,
2390
- artifactRoot: input.artifactRoot,
2391
- planRoot: input.planRoot,
2363
+ rootDir: input.rootDir,
2392
2364
  access: "read"
2393
2365
  });
2394
2366
  return grepWorkerFiles(
@@ -2446,8 +2418,7 @@ async function executeFindOperation(input) {
2446
2418
  token: input.token,
2447
2419
  sessionId: input.message.sessionId,
2448
2420
  activePlanId: input.message.activePlanId,
2449
- artifactRoot: input.artifactRoot,
2450
- planRoot: input.planRoot,
2421
+ rootDir: input.rootDir,
2451
2422
  access: "read"
2452
2423
  });
2453
2424
  return findWorkerFiles(
@@ -2488,8 +2459,7 @@ async function executeLsOperation(input) {
2488
2459
  token: input.token,
2489
2460
  sessionId: input.message.sessionId,
2490
2461
  activePlanId: input.message.activePlanId,
2491
- artifactRoot: input.artifactRoot,
2492
- planRoot: input.planRoot,
2462
+ rootDir: input.rootDir,
2493
2463
  access: "read"
2494
2464
  });
2495
2465
  return listWorkerDirectory(input.resolvedTarget.rootPath, input.message.path, input.message.limit, builtInPaths);
@@ -2595,8 +2565,7 @@ async function executeViewFileBytesOperation(input) {
2595
2565
  token: input.token,
2596
2566
  sessionId: input.message.sessionId,
2597
2567
  activePlanId: input.message.activePlanId,
2598
- artifactRoot: input.artifactRoot,
2599
- planRoot: input.planRoot,
2568
+ rootDir: input.rootDir,
2600
2569
  access: "read"
2601
2570
  });
2602
2571
  return readWorkerImageFile(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
@@ -2636,27 +2605,30 @@ function targetIdentityEnv(target) {
2636
2605
  R5D_BRANCH_NAME: ""
2637
2606
  };
2638
2607
  }
2608
+ function targetProcessEnv(rootDir, target) {
2609
+ return { [R5D_ROOT_ENV]: rootDir, ...targetIdentityEnv(target) };
2610
+ }
2639
2611
  async function prepareShellEnvForTarget(input) {
2640
2612
  const artifactProcessEnv = await prepareArtifactEnvForShell({
2641
2613
  baseUrl: input.baseUrl,
2642
2614
  token: input.token,
2643
2615
  sessionId: input.sessionId,
2644
- artifactRoot: input.artifactRoot
2616
+ rootDir: input.rootDir
2645
2617
  });
2646
2618
  if (input.target.type === "workspace" && input.target.rootProfile === "canonical_sync") {
2647
2619
  return {
2648
2620
  ...artifactProcessEnv,
2649
- ...targetIdentityEnv(input.target)
2621
+ ...targetProcessEnv(input.rootDir, input.target)
2650
2622
  };
2651
2623
  }
2652
2624
  return {
2653
2625
  ...artifactProcessEnv,
2654
2626
  ...preparePlanEnvForShell({
2655
2627
  target: input.target,
2656
- planRoot: input.planRoot,
2628
+ rootDir: input.rootDir,
2657
2629
  activePlanId: input.activePlanId
2658
2630
  }),
2659
- ...targetIdentityEnv(input.target)
2631
+ ...targetProcessEnv(input.rootDir, input.target)
2660
2632
  };
2661
2633
  }
2662
2634
  async function executeCommand(input) {
@@ -2671,13 +2643,12 @@ async function executeCommand(input) {
2671
2643
  };
2672
2644
  input.setPreparationHeartbeatLease(true);
2673
2645
  try {
2674
- const targetProcessEnv = await prepareShellEnvForTarget({
2646
+ const targetProcessEnv2 = await prepareShellEnvForTarget({
2675
2647
  target: input.resolvedTarget.target,
2676
2648
  baseUrl: input.baseUrl,
2677
2649
  token: input.token,
2678
2650
  sessionId: input.message.sessionId,
2679
- artifactRoot: input.artifactRoot,
2680
- planRoot: input.planRoot,
2651
+ rootDir: input.rootDir,
2681
2652
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2682
2653
  });
2683
2654
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
@@ -2687,7 +2658,7 @@ async function executeCommand(input) {
2687
2658
  stdout: "pipe",
2688
2659
  stderr: "pipe",
2689
2660
  detached: true,
2690
- env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
2661
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2])
2691
2662
  });
2692
2663
  spawnedProcess = subprocess;
2693
2664
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
@@ -2776,13 +2747,12 @@ async function executeStreamingCommand(input) {
2776
2747
  });
2777
2748
  return;
2778
2749
  }
2779
- const targetProcessEnv = await prepareShellEnvForTarget({
2750
+ const targetProcessEnv2 = await prepareShellEnvForTarget({
2780
2751
  target: input.resolvedTarget.target,
2781
2752
  baseUrl: input.baseUrl,
2782
2753
  token: input.token,
2783
2754
  sessionId: input.message.sessionId,
2784
- artifactRoot: input.artifactRoot,
2785
- planRoot: input.planRoot,
2755
+ rootDir: input.rootDir,
2786
2756
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2787
2757
  });
2788
2758
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
@@ -2796,7 +2766,7 @@ async function executeStreamingCommand(input) {
2796
2766
  stdout: "pipe",
2797
2767
  stderr: "pipe",
2798
2768
  detached: true,
2799
- env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
2769
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2])
2800
2770
  });
2801
2771
  spawnedProcess = subprocess;
2802
2772
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
@@ -3260,10 +3230,10 @@ async function openPty(input) {
3260
3230
  }
3261
3231
  const planProcessEnv = !(input.resolvedTarget.target.type === "workspace" && input.resolvedTarget.target.rootProfile === "canonical_sync") ? preparePlanEnvForShell({
3262
3232
  target: input.resolvedTarget.target,
3263
- planRoot: input.planRoot,
3233
+ rootDir: input.rootDir,
3264
3234
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
3265
3235
  }) : {};
3266
- const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
3236
+ const targetEnv = targetProcessEnv(input.rootDir, input.resolvedTarget.target);
3267
3237
  const shell = resolveHostShell(input.message.command);
3268
3238
  const outputCoalescer = createPtyOutputCoalescer({
3269
3239
  send: (data) => {
@@ -3295,7 +3265,7 @@ async function openPty(input) {
3295
3265
  githubProcessEnv(),
3296
3266
  resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
3297
3267
  planProcessEnv,
3298
- targetProcessEnv,
3268
+ targetEnv,
3299
3269
  shell.env ?? {}
3300
3270
  ])
3301
3271
  },
@@ -3468,12 +3438,13 @@ async function startWorker(options) {
3468
3438
  initializeCredentialStoreDirectoryOnce();
3469
3439
  const label = requireValue(options.label, "Missing required --label <label>");
3470
3440
  validateLabel(label);
3471
- const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
3472
- const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
3441
+ const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
3442
+ const projectsRoot = path.join(rootDir, "projects");
3443
+ const syncRoot = path.join(rootDir, "sync");
3473
3444
  const workspaceShadowRoot = path.join(syncRoot, "workspace");
3474
3445
  const projectWorktreeSnapshotsRoot = path.join(syncRoot, "project-worktree-snapshots");
3475
- const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
3476
- const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
3446
+ const artifactRoot = path.join(rootDir, "artifacts");
3447
+ const planRoot = path.join(rootDir, "plans");
3477
3448
  assertDisjointManagedRoots([
3478
3449
  { label: "projects root", rootPath: projectsRoot },
3479
3450
  { label: "sync root", rootPath: syncRoot },
@@ -3482,13 +3453,7 @@ async function startWorker(options) {
3482
3453
  ]);
3483
3454
  process.stdout.write(`[r5d-worker] label: ${label}
3484
3455
  `);
3485
- process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
3486
- `);
3487
- process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
3488
- `);
3489
- process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
3490
- `);
3491
- process.stdout.write(`[r5d-worker] plan root: ${planRoot}
3456
+ process.stdout.write(`[r5d-worker] root: ${rootDir}
3492
3457
  `);
3493
3458
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
3494
3459
  `);
@@ -3513,9 +3478,29 @@ async function startWorker(options) {
3513
3478
  const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
3514
3479
  const lastObservedProjectHeads = /* @__PURE__ */ new Map();
3515
3480
  const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3481
+ const advertisedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
3482
+ let advertisedWorkspaceHead;
3516
3483
  const fetchedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3517
- const lastProjectMirrorFetchAtMs = /* @__PURE__ */ new Map();
3484
+ const fetchedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
3518
3485
  const projectMirrorFetchSkippingEnabled = process.env.R5D_PROJECT_MIRROR_FETCH_SKIP !== "0";
3486
+ const forgetLocalProjectMirrorRefsTokens = (projectId, branchName) => {
3487
+ fetchedProjectMirrorRefsTokens.delete(projectId);
3488
+ if (branchName === void 0) fetchedProjectMirrorMountTokens.delete(projectId);
3489
+ else fetchedProjectMirrorMountTokens.get(projectId)?.delete(branchName);
3490
+ };
3491
+ const recordLocalProjectMirrorRefsTokens = async (project) => {
3492
+ try {
3493
+ const tokens = await computeLocalProjectMirrorRefsTokens({
3494
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3495
+ primaryBranchName: primaryProjectBranch(project),
3496
+ mounts: project.branches.map(({ branchId, branchName }) => ({ branchId, branchName }))
3497
+ });
3498
+ fetchedProjectMirrorRefsTokens.set(project.projectId, tokens.project);
3499
+ fetchedProjectMirrorMountTokens.set(project.projectId, tokens.mounts);
3500
+ } catch {
3501
+ forgetLocalProjectMirrorRefsTokens(project.projectId);
3502
+ }
3503
+ };
3519
3504
  const pendingMirrorDeletes = /* @__PURE__ */ new Map();
3520
3505
  let workspaceRemoteUrl = null;
3521
3506
  let workspaceCredentialHelper = null;
@@ -3530,10 +3515,12 @@ async function startWorker(options) {
3530
3515
  let workspacePeriodicTimer;
3531
3516
  let pendingAutomaticTrigger;
3532
3517
  const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
3518
+ const workspaceProjectionLedger = new WorkspaceProjectionLedger();
3533
3519
  let automaticSyncInFlight = false;
3534
3520
  let terminalReplayTimer;
3535
3521
  let workspaceSyncRequestsInFlight = 0;
3536
3522
  let lastWorkspaceSyncMirrorObservationMs = 0;
3523
+ let lastInventoriedMissingCheckouts;
3537
3524
  let sessionArtifactSyncRequestsInFlight = 0;
3538
3525
  let cliUpdateInProgress = false;
3539
3526
  let reloadAfterClose = false;
@@ -3591,8 +3578,15 @@ async function startWorker(options) {
3591
3578
  const primaryBranchName = primaryProjectBranch(project);
3592
3579
  const connection = projectConnection(project);
3593
3580
  const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
3594
- if (requireLocalBranch || hasProjectWorktree(sourcePath)) {
3595
- deleteLinkedProjectBranch({ projectRoot, primaryBranchName, branchName });
3581
+ const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryBranchName);
3582
+ if (requireLocalBranch || fs.existsSync(sourcePath) && hasProjectWorktree(primaryPath)) {
3583
+ const deleted = deleteLinkedProjectBranch({ projectRoot, primaryBranchName, branchName });
3584
+ if (deleted.movedAsidePath) {
3585
+ process.stderr.write(
3586
+ `[r5d-worker] set aside ${sourcePath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
3587
+ `
3588
+ );
3589
+ }
3596
3590
  }
3597
3591
  const planSourcePath = path.join(planRoot, project.projectId, ...branchName.split("/"));
3598
3592
  fs.rmSync(planSourcePath, { recursive: true, force: true });
@@ -3665,11 +3659,17 @@ async function startWorker(options) {
3665
3659
  if (branchName === primaryBranchName) {
3666
3660
  throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
3667
3661
  }
3668
- deleteLinkedProjectBranch({
3662
+ const deleted = deleteLinkedProjectBranch({
3669
3663
  projectRoot: configuredProjectRoot(projectsRoot, project),
3670
3664
  primaryBranchName,
3671
3665
  branchName
3672
3666
  });
3667
+ if (deleted.movedAsidePath) {
3668
+ process.stderr.write(
3669
+ `[r5d-worker] set aside ${branchPath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
3670
+ `
3671
+ );
3672
+ }
3673
3673
  fs.rmSync(path.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
3674
3674
  readyProjectIds.delete(project.projectId);
3675
3675
  reconciledProjectConfigFingerprints.delete(project.projectId);
@@ -3838,6 +3838,40 @@ async function startWorker(options) {
3838
3838
  });
3839
3839
  };
3840
3840
  const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3841
+ const inventoryConfiguredProjectCheckouts = () => {
3842
+ const inventory = { projects: [], missingCheckouts: [] };
3843
+ for (const project of projectConfigById.values()) {
3844
+ if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) {
3845
+ inventory.projects.push(project);
3846
+ continue;
3847
+ }
3848
+ const checkouts = inventoryProjectCheckouts({
3849
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3850
+ primaryBranchName: primaryProjectBranch(project),
3851
+ branchNames: project.branches.map(({ branchName }) => branchName)
3852
+ });
3853
+ if (checkouts.missingCheckouts.length === 0) {
3854
+ inventory.projects.push(project);
3855
+ continue;
3856
+ }
3857
+ for (const missing of checkouts.missingCheckouts) inventory.missingCheckouts.push({ projectId: project.projectId, ...missing });
3858
+ const linked = new Set(checkouts.linkedBranchNames);
3859
+ inventory.projects.push({ ...project, branches: project.branches.filter(({ branchName }) => linked.has(branchName)) });
3860
+ }
3861
+ inventory.missingCheckouts.sort(
3862
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
3863
+ );
3864
+ return inventory;
3865
+ };
3866
+ const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
3867
+ const logMissingCheckouts = (inventory) => {
3868
+ if (inventory.missingCheckouts.length === 0) return;
3869
+ const described = inventory.missingCheckouts.map(({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`).join(", ");
3870
+ process.stderr.write(
3871
+ `[r5d-worker] skipping ${inventory.missingCheckouts.length} configured project checkout(s) this cycle: ${described}; delete the branch through the platform or reconnect this worker to recreate the checkout
3872
+ `
3873
+ );
3874
+ };
3841
3875
  const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
3842
3876
  const mounts = [];
3843
3877
  const creatorLocalIncarnations = new Map(
@@ -3899,7 +3933,7 @@ async function startWorker(options) {
3899
3933
  workspaceRelativePath: "workspace-plans",
3900
3934
  sourceMode: "all",
3901
3935
  hydrateDeletionMode: "all",
3902
- // Workspace-level agents receive this directory through R5D_PLANS_DIR.
3936
+ // Workspace-level agents reach this directory through $R5D_ROOT/plans.
3903
3937
  // Keep both projection and hydration away from it while any visible-
3904
3938
  // projects process, PTY, or built-in write/edit is active or reserved.
3905
3939
  busy: () => {
@@ -3937,7 +3971,10 @@ async function startWorker(options) {
3937
3971
  deleteWhenSourceMissing: true
3938
3972
  });
3939
3973
  }
3940
- return mounts;
3974
+ return mounts.map((mount) => {
3975
+ const lastProjectedMutationToken = workspaceProjectionLedger.lastProjectedToken(mount);
3976
+ return lastProjectedMutationToken === void 0 ? mount : { ...mount, lastProjectedMutationToken };
3977
+ });
3941
3978
  };
3942
3979
  const workspaceLocalHead = async () => {
3943
3980
  if (!fs.existsSync(path.join(workspaceShadowRoot, ".git"))) return null;
@@ -4113,9 +4150,9 @@ async function startWorker(options) {
4113
4150
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
4114
4151
  );
4115
4152
  };
4116
- const captureProjectHeadsForWorkspacePublication = async () => {
4153
+ const captureProjectHeadsForWorkspacePublication = async (projects = projectConfigById.values()) => {
4117
4154
  const heads = /* @__PURE__ */ new Map();
4118
- for (const project of projectConfigById.values()) {
4155
+ for (const project of projects) {
4119
4156
  if (!readyProjectIds.has(project.projectId)) continue;
4120
4157
  for (const branch of project.branches) {
4121
4158
  const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
@@ -4125,9 +4162,9 @@ async function startWorker(options) {
4125
4162
  }
4126
4163
  return heads;
4127
4164
  };
4128
- const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4165
+ const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set(), projects = projectConfigById.values()) => {
4129
4166
  const active = new Set(activeMountIds);
4130
- for (const project of projectConfigById.values()) {
4167
+ for (const project of projects) {
4131
4168
  if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
4132
4169
  const changed = /* @__PURE__ */ new Map();
4133
4170
  for (const branch of project.branches) {
@@ -4162,17 +4199,7 @@ async function startWorker(options) {
4162
4199
  for (const result of results) {
4163
4200
  lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
4164
4201
  }
4165
- try {
4166
- fetchedProjectMirrorRefsTokens.set(
4167
- project.projectId,
4168
- await computeLocalProjectMirrorRefsToken({
4169
- projectRoot: configuredProjectRoot(projectsRoot, project),
4170
- primaryBranchName: primaryProjectBranch(project)
4171
- })
4172
- );
4173
- } catch {
4174
- fetchedProjectMirrorRefsTokens.delete(project.projectId);
4175
- }
4202
+ await recordLocalProjectMirrorRefsTokens(project);
4176
4203
  }
4177
4204
  const publishedTombstoneIds = [
4178
4205
  ...new Set(
@@ -4198,7 +4225,7 @@ async function startWorker(options) {
4198
4225
  credentialHelper: deletion.credentialHelper,
4199
4226
  credentialUsername: deletion.credentialUsername
4200
4227
  });
4201
- fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4228
+ forgetLocalProjectMirrorRefsTokens(deletion.projectId, deletion.branchName);
4202
4229
  }
4203
4230
  if (deletion.tombstoneId && !deletion.projectDeleted) {
4204
4231
  projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
@@ -4210,8 +4237,8 @@ async function startWorker(options) {
4210
4237
  if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
4211
4238
  removeProjectCredentialStore(deletion.projectId);
4212
4239
  advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
4213
- fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4214
- lastProjectMirrorFetchAtMs.delete(deletion.projectId);
4240
+ advertisedProjectMirrorMountTokens.delete(deletion.projectId);
4241
+ forgetLocalProjectMirrorRefsTokens(deletion.projectId);
4215
4242
  }
4216
4243
  lastObservedProjectHeads.delete(key);
4217
4244
  }
@@ -4220,71 +4247,63 @@ async function startWorker(options) {
4220
4247
  const connection = projectConnection(project);
4221
4248
  const projectRoot = configuredProjectRoot(projectsRoot, project);
4222
4249
  const primaryBranchName = primaryProjectBranch(project);
4223
- if (options2.refreshRemote) {
4224
- const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
4225
- const observations = await observeProjectMirrorHeads({
4226
- projectRoot,
4227
- primaryBranchName,
4228
- branchNames: project.branches.map(({ branchName }) => branchName),
4229
- mirrorUrl: connection.mirrorUrl,
4230
- credentialHelper: connection.credentialHelper,
4231
- credentialUsername: connection.mirrorCredentialUsername,
4232
- allowNonFastForward: options2.allowNonFastForward,
4233
- branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4234
- });
4235
- if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
4236
- else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
4237
- lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
4238
- return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
4239
- }
4240
- return {
4241
- projectId: project.projectId,
4250
+ const observations = await observeProjectMirrorHeads({
4242
4251
  projectRoot,
4243
4252
  primaryBranchName,
4244
- observations: await observeProjectMirrorHeads({
4245
- projectRoot,
4246
- primaryBranchName,
4247
- branchNames: project.branches.map(({ branchName }) => branchName),
4248
- mirrorUrl: connection.mirrorUrl,
4249
- credentialHelper: connection.credentialHelper,
4250
- credentialUsername: connection.mirrorCredentialUsername,
4251
- allowNonFastForward: options2.allowNonFastForward,
4252
- branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
4253
- refreshRemote: false
4254
- })
4255
- };
4253
+ branchNames: project.branches.map(({ branchName }) => branchName),
4254
+ mirrorUrl: connection.mirrorUrl,
4255
+ credentialHelper: connection.credentialHelper,
4256
+ credentialUsername: connection.mirrorCredentialUsername,
4257
+ allowNonFastForward: options2.allowNonFastForward,
4258
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
4259
+ refreshRemote: options2.fetch.scope !== "none",
4260
+ ...options2.fetch.scope === "mounts" ? { fetchOnlyBranches: new Set(options2.fetch.mounts.map(({ branchName }) => branchName)) } : {}
4261
+ });
4262
+ if (options2.fetch.scope !== "none") await recordLocalProjectMirrorRefsTokens(project);
4263
+ return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
4256
4264
  };
4257
4265
  const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
4258
4266
  const observationStartedAt = Date.now();
4259
- const eligibleProjects = [...projectConfigById.values()].filter(
4267
+ const eligibleProjects = [...input.projects ?? projectConfigById.values()].filter(
4260
4268
  (project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
4261
4269
  );
4262
4270
  try {
4263
4271
  if (input.failClosed) {
4264
4272
  const bundles2 = [];
4265
4273
  for (const project of eligibleProjects) {
4266
- bundles2.push(await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, refreshRemote: true }));
4274
+ bundles2.push(
4275
+ await observeSingleProjectMirrorBundle(project, {
4276
+ allowNonFastForward: input.allowNonFastForward,
4277
+ fetch: { scope: "project", reason: "fail_closed" }
4278
+ })
4279
+ );
4267
4280
  }
4268
4281
  return bundles2;
4269
4282
  }
4270
- let fetchedCount = 0;
4283
+ let wholeMirrorFetches = 0;
4284
+ let scopedMirrorFetches = 0;
4285
+ let fetchedMounts = 0;
4286
+ let eligibleMounts = 0;
4271
4287
  const bundles = await mapWithConcurrencyLimit(
4272
4288
  eligibleProjects,
4273
4289
  PROJECT_MIRROR_OBSERVE_CONCURRENCY,
4274
4290
  async (project) => {
4275
4291
  try {
4276
- const decision = projectMirrorFetchDecision({
4277
- advertisedToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
4278
- fetchedToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
4279
- lastFetchAtMs: lastProjectMirrorFetchAtMs.get(project.projectId),
4280
- nowMs: Date.now(),
4292
+ const fetch2 = planProjectMirrorObservationFetch({
4293
+ branchNames: project.branches.map(({ branchName }) => branchName),
4294
+ advertisedProjectToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
4295
+ advertisedMountTokens: advertisedProjectMirrorMountTokens.get(project.projectId),
4296
+ fetchedProjectToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
4297
+ fetchedMountTokens: fetchedProjectMirrorMountTokens.get(project.projectId) ?? /* @__PURE__ */ new Map(),
4281
4298
  skippingEnabled: projectMirrorFetchSkippingEnabled
4282
4299
  });
4283
- if (decision.fetch) fetchedCount += 1;
4284
- return await observeSingleProjectMirrorBundle(project, {
4285
- allowNonFastForward: input.allowNonFastForward,
4286
- refreshRemote: decision.fetch
4287
- });
4300
+ eligibleMounts += project.branches.length;
4301
+ if (fetch2.scope === "project") wholeMirrorFetches += 1;
4302
+ if (fetch2.scope === "mounts") {
4303
+ scopedMirrorFetches += 1;
4304
+ fetchedMounts += fetch2.mounts.length;
4305
+ }
4306
+ return await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, fetch: fetch2 });
4288
4307
  } catch (error) {
4289
4308
  process.stderr.write(
4290
4309
  `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
@@ -4294,9 +4313,9 @@ async function startWorker(options) {
4294
4313
  }
4295
4314
  }
4296
4315
  );
4297
- if (fetchedCount > 0) {
4316
+ if (wholeMirrorFetches + scopedMirrorFetches > 0) {
4298
4317
  process.stderr.write(
4299
- `[r5d-worker] project mirror observation fetched ${fetchedCount} of ${eligibleProjects.length} mirror(s)
4318
+ `[r5d-worker] project mirror observation fetched ${wholeMirrorFetches + scopedMirrorFetches} of ${eligibleProjects.length} mirror(s) (${wholeMirrorFetches} whole, ${scopedMirrorFetches} scoped to ${fetchedMounts} of ${eligibleMounts} mount(s))
4300
4319
  `
4301
4320
  );
4302
4321
  }
@@ -4355,7 +4374,7 @@ async function startWorker(options) {
4355
4374
  }
4356
4375
  }
4357
4376
  };
4358
- const mapWorkspaceGitResult = async (attemptId, trigger, result) => {
4377
+ const mapWorkspaceGitResult = async (attemptId, trigger, result, missingCheckouts) => {
4359
4378
  const activeProjectBranchPublications = activeProjectBranchPublicationEvidence([...projectConfigById.values()], result.activeMountIds);
4360
4379
  return {
4361
4380
  type: "workspace_sync",
@@ -4373,6 +4392,7 @@ async function startWorker(options) {
4373
4392
  affectedPaths: result.affectedPaths,
4374
4393
  activeMountIds: result.activeMountIds,
4375
4394
  skippedMountIds: result.skippedMountIds,
4395
+ ...missingCheckouts ? { missingCheckouts } : {},
4376
4396
  activeProjectBranchPublications,
4377
4397
  discardedPaths: [],
4378
4398
  localChangesDiscarded: false,
@@ -4400,6 +4420,7 @@ async function startWorker(options) {
4400
4420
  affectedPaths: [],
4401
4421
  discardedPaths: [],
4402
4422
  localChangesDiscarded: false,
4423
+ ...lastInventoriedMissingCheckouts ? { missingCheckouts: lastInventoriedMissingCheckouts } : {},
4403
4424
  error: error instanceof Error ? error.message : String(error)
4404
4425
  };
4405
4426
  };
@@ -4408,8 +4429,13 @@ async function startWorker(options) {
4408
4429
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4409
4430
  throw new Error("Worker workspace configuration has not been received");
4410
4431
  }
4411
- const configuredMounts = buildWorkspaceMounts();
4432
+ const checkoutInventory = inventoryConfiguredProjectCheckouts();
4433
+ logMissingCheckouts(checkoutInventory);
4434
+ const missingCheckouts = reportedMissingCheckouts(checkoutInventory);
4435
+ lastInventoriedMissingCheckouts = missingCheckouts;
4436
+ const configuredMounts = buildWorkspaceMounts(checkoutInventory.projects);
4412
4437
  if (input.resetToCanonical) {
4438
+ workspaceProjectionLedger.reset();
4413
4439
  const resetHasBusyLiveMount = configuredMounts.some(
4414
4440
  (mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
4415
4441
  );
@@ -4418,7 +4444,8 @@ async function startWorker(options) {
4418
4444
  }
4419
4445
  const observedProjectHeads2 = await observeProjectHeadsBeforeOuterWorkspace({
4420
4446
  allowNonFastForward: true,
4421
- failClosed: true
4447
+ failClosed: true,
4448
+ projects: checkoutInventory.projects
4422
4449
  });
4423
4450
  const reset = await resetWorkspaceGit({
4424
4451
  workspacePath: workspaceShadowRoot,
@@ -4446,13 +4473,15 @@ async function startWorker(options) {
4446
4473
  affectedPaths: reset.discardedPaths,
4447
4474
  activeMountIds: reset.activeMountIds,
4448
4475
  skippedMountIds: reset.skippedMountIds,
4476
+ missingCheckouts,
4449
4477
  discardedPaths: reset.discardedPaths,
4450
4478
  localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
4451
4479
  };
4452
4480
  }
4453
4481
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4454
4482
  const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
4455
- const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4483
+ if (outerRemediation) workspaceProjectionLedger.reset();
4484
+ const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false, projects: checkoutInventory.projects });
4456
4485
  const expectedMirrorHeads = /* @__PURE__ */ new Map();
4457
4486
  for (const bundle of observedProjectHeads) {
4458
4487
  for (const observation of bundle.observations) {
@@ -4464,7 +4493,7 @@ async function startWorker(options) {
4464
4493
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
4465
4494
  )
4466
4495
  );
4467
- const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication();
4496
+ const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication(checkoutInventory.projects);
4468
4497
  const result = await synchronizeWorkspaceGit({
4469
4498
  attemptId: input.attemptId,
4470
4499
  workerLabel: label,
@@ -4478,9 +4507,20 @@ async function startWorker(options) {
4478
4507
  allowLargeDiff: input.confirmedLargeDiff,
4479
4508
  skipMountMirror: outerRemediation,
4480
4509
  requiredAncestorHeads: input.requiredAncestorHeads,
4510
+ // Same gate as the mirror fetches: while the server's read of the hidden
4511
+ // workspace head equals the head this clone last fetched or pushed, the
4512
+ // cycle's workspace head fetches would transfer nothing.
4513
+ advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
4481
4514
  assertStillAdmitted: input.assertStillAdmitted,
4482
4515
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4483
- await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4516
+ await pushChangedProjectHeads(
4517
+ activeMountIds,
4518
+ publishedHead,
4519
+ publicationHeads,
4520
+ expectedMirrorHeads,
4521
+ inboundMoveMountIds,
4522
+ checkoutInventory.projects
4523
+ );
4484
4524
  },
4485
4525
  hydrationHooks: workspaceHydrationTransactionHooks
4486
4526
  });
@@ -4499,7 +4539,11 @@ async function startWorker(options) {
4499
4539
  result.localHead ?? result.remoteHead
4500
4540
  );
4501
4541
  }
4502
- return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
4542
+ const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result, missingCheckouts);
4543
+ if (!outerRemediation) {
4544
+ workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
4545
+ }
4546
+ return mapped;
4503
4547
  };
4504
4548
  const sendWorkspaceSyncResult = (requestId, result) => {
4505
4549
  try {
@@ -4537,17 +4581,143 @@ async function startWorker(options) {
4537
4581
  }, unsafeWorkspaceSyncRecoveryBounceDelayMs(attempt));
4538
4582
  timer.unref();
4539
4583
  };
4584
+ const captureWorkspaceSyncAdmission = () => ({
4585
+ generation: workerAdmissionGeneration,
4586
+ activeIncidentId: activeWorkspaceIncidentId,
4587
+ deferredIncidentId: deferredWorkspaceConfiguration?.incidentId ?? null
4588
+ });
4589
+ const workspaceSyncAdmissionSnapshotsMatch = (left, right) => left.generation === right.generation && left.activeIncidentId === right.activeIncidentId && left.deferredIncidentId === right.deferredIncidentId;
4590
+ const assertWorkspaceSyncStillAdmitted = (admitted, trigger) => {
4591
+ if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, trigger)) {
4592
+ throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
4593
+ }
4594
+ };
4595
+ const executeWorkspaceSyncCycle = async (input) => {
4596
+ const attemptId = input.attemptId;
4597
+ lastWorkspaceSyncMirrorObservationMs = 0;
4598
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4599
+ return await failedWorkspaceSyncResult(
4600
+ attemptId,
4601
+ input.trigger,
4602
+ new Error(
4603
+ `Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
4604
+ )
4605
+ );
4606
+ }
4607
+ try {
4608
+ input.assertStillAdmitted();
4609
+ const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
4610
+ return await performWorkspaceSync({
4611
+ attemptId,
4612
+ trigger: input.trigger,
4613
+ confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
4614
+ confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
4615
+ resetToCanonical: input.resetToCanonical,
4616
+ requiredAncestorHeads: input.requiredAncestorHeads,
4617
+ assertStillAdmitted: input.assertStillAdmitted
4618
+ });
4619
+ } catch (error) {
4620
+ workspaceProjectionLedger.reset();
4621
+ const errorMessage = error instanceof Error ? error.message : String(error);
4622
+ const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
4623
+ error,
4624
+ resetToCanonical: input.resetToCanonical === true,
4625
+ inspectRecoverableHydration: () => workspaceGitHydrationIsRecoverable(workspaceShadowRoot, buildWorkspaceMounts())
4626
+ });
4627
+ if (hydrationRecoverable) {
4628
+ if (error instanceof WorkspaceSyncAdmissionChangedError) {
4629
+ process.stderr.write(
4630
+ `[r5d-worker] workspace synchronization was cancelled with every mount at a recorded basis; the next cycle converges it: ${errorMessage}
4631
+ `
4632
+ );
4633
+ }
4634
+ return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
4635
+ }
4636
+ const fenced = fenceUnsafeWorkspaceSyncFailure({
4637
+ hydrationRecoverable,
4638
+ resetToCanonical: input.resetToCanonical === true,
4639
+ recoveryAttempts: unsafeWorkspaceSyncRecoveryAttempts,
4640
+ maxRecoveryAttempts: WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
4641
+ hasRetainableWork: activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0,
4642
+ bouncePending: unsafeWorkspaceSyncBouncePending,
4643
+ invalidateExecution: () => {
4644
+ workspaceConfigured = false;
4645
+ advanceWorkerAdmissionGeneration();
4646
+ },
4647
+ scheduleRecoveryBounce: () => {
4648
+ unsafeWorkspaceSyncRecoveryAttempts += 1;
4649
+ scheduleUnsafeWorkspaceSyncRecoveryBounce(errorMessage);
4650
+ },
4651
+ fatalExit: (exitCode) => {
4652
+ process.stderr.write(
4653
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4654
+ `
4655
+ );
4656
+ process.exit(exitCode);
4657
+ },
4658
+ exitCode: 1
4659
+ });
4660
+ if (fenced === "recovery_pending") {
4661
+ process.stderr.write(
4662
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis while in-process recovery is already pending: ${errorMessage}
4663
+ `
4664
+ );
4665
+ }
4666
+ return {
4667
+ ...await failedWorkspaceSyncResult(attemptId, input.trigger, error),
4668
+ failureReason: "unsafe_hydration",
4669
+ recoveryAttempt: unsafeWorkspaceSyncRecoveryAttempts
4670
+ };
4671
+ }
4672
+ };
4673
+ const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set(["remediation", "remediation_confirm", "remediation_reset"]);
4674
+ const isOrdinaryWorkspaceSyncRequest = (input) => input.confirmedLargeDiff !== true && input.resetToCanonical !== true && !(input.requiredAncestorHeads && input.requiredAncestorHeads.length > 0) && !REMEDIATION_TRIGGER_TYPES.has(input.trigger.type);
4675
+ const LOCAL_ACTIVITY_TRIGGER_TYPES = /* @__PURE__ */ new Set([
4676
+ "write",
4677
+ "edit",
4678
+ "shell_inline",
4679
+ "process_terminal",
4680
+ "process_cancel"
4681
+ ]);
4682
+ const answerOrdinaryWorkspaceSyncFromLastCycle = (requests) => {
4683
+ if (!workspaceConfigured) return null;
4684
+ return workspaceProjectionLedger.answerFromLastCycle({
4685
+ // A workspace-rooted command carries no project target because it may
4686
+ // have touched any checkout.
4687
+ targetMountIds: requests.map(
4688
+ ({ trigger }) => LOCAL_ACTIVITY_TRIGGER_TYPES.has(trigger.type) && trigger.projectId && trigger.branchName ? projectMountId(trigger.projectId, trigger.branchName) : null
4689
+ ),
4690
+ mounts: buildWorkspaceMounts(),
4691
+ pendingDeletions: pendingMirrorDeletes.size > 0 || projectWorkspaceState.pendingTreeDeletions.length > 0 || projectWorkspaceState.pendingMirrorRefDeletions.length > 0
4692
+ });
4693
+ };
4694
+ const ordinaryWorkspaceSyncCoalescer = new WorkspaceSyncCoalescer({
4695
+ runExclusive: (operation) => workspaceSyncSingleFlight.runExclusive(operation),
4696
+ runBatch: async (requests) => {
4697
+ const startedAtMs = Date.now();
4698
+ const admission = captureWorkspaceSyncAdmission();
4699
+ const staleAttemptIds = /* @__PURE__ */ new Set();
4700
+ const admitted = [];
4701
+ for (const request of requests) {
4702
+ if (workspaceSyncAdmissionSnapshotsMatch(request.admission, admission)) admitted.push(request);
4703
+ else staleAttemptIds.add(request.attemptId);
4704
+ }
4705
+ const leader = admitted[0];
4706
+ if (!leader) return { result: null, ranCycle: false, startedAtMs, finishedAtMs: Date.now(), prepareMs: 0, staleAttemptIds };
4707
+ const answered = answerOrdinaryWorkspaceSyncFromLastCycle(admitted);
4708
+ if (answered) return { result: answered, ranCycle: false, startedAtMs, finishedAtMs: Date.now(), prepareMs: 0, staleAttemptIds };
4709
+ const result = await executeWorkspaceSyncCycle({
4710
+ attemptId: leader.attemptId,
4711
+ trigger: leader.trigger,
4712
+ assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, leader.trigger)
4713
+ });
4714
+ return { result, ranCycle: true, startedAtMs, finishedAtMs: Date.now(), prepareMs: lastWorkspaceSyncMirrorObservationMs, staleAttemptIds };
4715
+ }
4716
+ });
4540
4717
  const runWorkspaceSync = async (input) => {
4541
4718
  const attemptId = input.attemptId ?? crypto.randomUUID();
4542
4719
  const requestedAt = Date.now();
4543
- const admittedGeneration = workerAdmissionGeneration;
4544
- const admittedActiveIncidentId = activeWorkspaceIncidentId;
4545
- const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
4546
- const assertStillAdmitted = () => {
4547
- if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4548
- throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
4549
- }
4550
- };
4720
+ const admission = captureWorkspaceSyncAdmission();
4551
4721
  if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4552
4722
  const result2 = {
4553
4723
  ...await failedWorkspaceSyncResult(
@@ -4573,104 +4743,47 @@ async function startWorker(options) {
4573
4743
  }
4574
4744
  ws.send(JSON.stringify({ type: "workspace_sync_started", attemptId }));
4575
4745
  }
4576
- let queueEnteredAt = requestedAt;
4577
- let syncStartedAt = requestedAt;
4746
+ let queueMs = 0;
4747
+ let prepareMs = 0;
4748
+ let synchronizeMs = 0;
4578
4749
  workspaceSyncRequestsInFlight += 1;
4579
4750
  let result;
4580
4751
  try {
4581
- result = await workspaceSyncSingleFlight.runExclusive(async () => {
4582
- queueEnteredAt = Date.now();
4583
- syncStartedAt = Date.now();
4584
- lastWorkspaceSyncMirrorObservationMs = 0;
4585
- if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4586
- return await failedWorkspaceSyncResult(
4587
- attemptId,
4588
- input.trigger,
4589
- new Error(
4590
- `Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
4591
- )
4592
- );
4593
- }
4594
- try {
4595
- assertStillAdmitted();
4596
- const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
4597
- return await performWorkspaceSync({
4752
+ if (isOrdinaryWorkspaceSyncRequest(input)) {
4753
+ const settlement = await ordinaryWorkspaceSyncCoalescer.request({ attemptId, trigger: input.trigger, admission });
4754
+ queueMs = settlement.startedAtMs - requestedAt;
4755
+ prepareMs = settlement.prepareMs;
4756
+ synchronizeMs = settlement.finishedAtMs - settlement.startedAtMs;
4757
+ result = settlement.staleAttemptIds.has(attemptId) || settlement.result === null ? await failedWorkspaceSyncResult(
4758
+ attemptId,
4759
+ input.trigger,
4760
+ new Error("Workspace synchronization admission changed while the request was queued")
4761
+ ) : { ...settlement.result, attemptId, trigger: input.trigger };
4762
+ } else {
4763
+ result = await workspaceSyncSingleFlight.runExclusive(async () => {
4764
+ const startedAt = Date.now();
4765
+ queueMs = startedAt - requestedAt;
4766
+ const cycleResult = await executeWorkspaceSyncCycle({
4598
4767
  attemptId,
4599
4768
  trigger: input.trigger,
4600
- confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
4601
- confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
4769
+ confirmedLargeDiff: input.confirmedLargeDiff,
4770
+ confirmationReason: input.confirmationReason,
4602
4771
  resetToCanonical: input.resetToCanonical,
4603
4772
  requiredAncestorHeads: input.requiredAncestorHeads,
4604
- assertStillAdmitted
4773
+ assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, input.trigger)
4605
4774
  });
4606
- } catch (error) {
4607
- const errorMessage = error instanceof Error ? error.message : String(error);
4608
- const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
4609
- error,
4610
- resetToCanonical: input.resetToCanonical === true,
4611
- inspectRecoverableHydration: () => workspaceGitHydrationIsRecoverable(workspaceShadowRoot, buildWorkspaceMounts())
4612
- });
4613
- if (hydrationRecoverable) {
4614
- if (error instanceof WorkspaceSyncAdmissionChangedError) {
4615
- process.stderr.write(
4616
- `[r5d-worker] workspace synchronization was cancelled with every mount at a recorded basis; the next cycle converges it: ${errorMessage}
4617
- `
4618
- );
4619
- }
4620
- return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
4621
- }
4622
- const fenced = fenceUnsafeWorkspaceSyncFailure({
4623
- hydrationRecoverable,
4624
- resetToCanonical: input.resetToCanonical === true,
4625
- recoveryAttempts: unsafeWorkspaceSyncRecoveryAttempts,
4626
- maxRecoveryAttempts: WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
4627
- hasRetainableWork: activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0,
4628
- bouncePending: unsafeWorkspaceSyncBouncePending,
4629
- invalidateExecution: () => {
4630
- workspaceConfigured = false;
4631
- advanceWorkerAdmissionGeneration();
4632
- },
4633
- scheduleRecoveryBounce: () => {
4634
- unsafeWorkspaceSyncRecoveryAttempts += 1;
4635
- scheduleUnsafeWorkspaceSyncRecoveryBounce(errorMessage);
4636
- },
4637
- fatalExit: (exitCode) => {
4638
- process.stderr.write(
4639
- `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4640
- `
4641
- );
4642
- process.exit(exitCode);
4643
- },
4644
- exitCode: 1
4645
- });
4646
- if (fenced === "recovery_pending") {
4647
- process.stderr.write(
4648
- `[r5d-worker] workspace synchronization failed without a recoverable hydration basis while in-process recovery is already pending: ${errorMessage}
4649
- `
4650
- );
4651
- }
4652
- return {
4653
- ...await failedWorkspaceSyncResult(attemptId, input.trigger, error),
4654
- failureReason: "unsafe_hydration",
4655
- recoveryAttempt: unsafeWorkspaceSyncRecoveryAttempts
4656
- };
4657
- }
4658
- });
4775
+ prepareMs = lastWorkspaceSyncMirrorObservationMs;
4776
+ synchronizeMs = Date.now() - startedAt;
4777
+ return cycleResult;
4778
+ });
4779
+ }
4659
4780
  } finally {
4660
4781
  workspaceSyncRequestsInFlight -= 1;
4661
4782
  }
4662
4783
  if (result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
4663
- const finishedAt = Date.now();
4664
4784
  result = {
4665
4785
  ...result,
4666
- telemetry: {
4667
- totalMs: finishedAt - requestedAt,
4668
- queueMs: queueEnteredAt - requestedAt,
4669
- // Duration of the pre-outer mirror-head observation phase; near zero
4670
- // on a cycle whose token-gated observation skipped every fetch.
4671
- prepareMs: lastWorkspaceSyncMirrorObservationMs,
4672
- synchronizeMs: finishedAt - syncStartedAt
4673
- }
4786
+ telemetry: { totalMs: Date.now() - requestedAt, queueMs, prepareMs, synchronizeMs }
4674
4787
  };
4675
4788
  if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result);
4676
4789
  sendLifecycleProgress(
@@ -4940,6 +5053,7 @@ async function startWorker(options) {
4940
5053
  );
4941
5054
  }
4942
5055
  workspaceConfigured = false;
5056
+ workspaceProjectionLedger.reset();
4943
5057
  const preserveOnlyBranches = message.projects.flatMap(
4944
5058
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4945
5059
  );
@@ -4949,6 +5063,16 @@ async function startWorker(options) {
4949
5063
  preserveOnlyBranches
4950
5064
  });
4951
5065
  }
5066
+ for (const project of message.projects) {
5067
+ for (const branch of project.branches) {
5068
+ const pending2 = projectWorkspaceState.locallyPendingCreatedBranches.find(
5069
+ (candidate) => candidate.projectId === project.projectId && candidate.branchName === branch.branchName
5070
+ );
5071
+ if (pending2 && pending2.branchId === branch.branchId) {
5072
+ projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
5073
+ }
5074
+ }
5075
+ }
4952
5076
  const stillPendingCreatedBranches = new Map(
4953
5077
  projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4954
5078
  pendingCreatedBranchKey(branch.projectId, branch.branchName),
@@ -5390,11 +5514,10 @@ async function startWorker(options) {
5390
5514
  workspaceConfigResetToCanonicalV1: true,
5391
5515
  projectBranchDeletionFastAckV1: true,
5392
5516
  projectMirrorLeaseV1: true,
5393
- projectMirrorRefsTokensV1: true
5517
+ projectMirrorRefsTokensV1: true,
5518
+ projectBranchWorkingTreeModeV1: true
5394
5519
  },
5395
- projectRoot: projectsRoot,
5396
- artifactRoot,
5397
- planRoot
5520
+ root: rootDir
5398
5521
  }
5399
5522
  };
5400
5523
  ws.send(JSON.stringify(hello));
@@ -5429,14 +5552,12 @@ async function startWorker(options) {
5429
5552
  return;
5430
5553
  }
5431
5554
  if (message.type === "project_mirror_refs_tokens") {
5555
+ const advertised = parseProjectMirrorRefsTokensMessage(message);
5432
5556
  advertisedProjectMirrorRefsTokens.clear();
5433
- if (message.tokens && typeof message.tokens === "object") {
5434
- for (const [projectId, token2] of Object.entries(message.tokens)) {
5435
- if (typeof token2 === "string" && /^[0-9a-f]{64}$/.test(token2)) {
5436
- advertisedProjectMirrorRefsTokens.set(projectId, token2);
5437
- }
5438
- }
5439
- }
5557
+ for (const [projectId, token2] of advertised.projectTokens) advertisedProjectMirrorRefsTokens.set(projectId, token2);
5558
+ advertisedProjectMirrorMountTokens.clear();
5559
+ for (const [projectId, tokens] of advertised.mountTokens) advertisedProjectMirrorMountTokens.set(projectId, tokens);
5560
+ advertisedWorkspaceHead = advertised.workspaceHead;
5440
5561
  return;
5441
5562
  }
5442
5563
  if (message.type === "workspace_config") {
@@ -5509,13 +5630,21 @@ async function startWorker(options) {
5509
5630
  });
5510
5631
  return;
5511
5632
  }
5633
+ sendWorkerMessage(ws, {
5634
+ type: "operation_queued",
5635
+ requestId: message.requestId,
5636
+ queuePosition: workspaceSyncSingleFlight.branchMutationQueuePosition()
5637
+ });
5512
5638
  try {
5639
+ if (message.workingTree !== "carry" && message.workingTree !== "clean") {
5640
+ throw new Error('Project branch creation requires workingTree "carry" or "clean"');
5641
+ }
5513
5642
  const pendingBranch = {
5514
5643
  branchId: message.branchId,
5515
5644
  projectId: message.projectId,
5516
5645
  branchName: message.targetBranch
5517
5646
  };
5518
- const created = await workspaceSyncSingleFlight.runMutation(() => {
5647
+ const created = await workspaceSyncSingleFlight.runBranchMutation(() => {
5519
5648
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5520
5649
  throw new Error(
5521
5650
  `Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
@@ -5556,6 +5685,7 @@ async function startWorker(options) {
5556
5685
  primaryBranchName: primaryProjectBranch(project),
5557
5686
  sourceBranchName: message.sourceBranch,
5558
5687
  branchName: message.targetBranch,
5688
+ workingTree: message.workingTree,
5559
5689
  pendingRetry: pendingWasRecorded
5560
5690
  });
5561
5691
  gitBranchCreated = true;
@@ -5612,9 +5742,14 @@ async function startWorker(options) {
5612
5742
  });
5613
5743
  return;
5614
5744
  }
5745
+ sendWorkerMessage(ws, {
5746
+ type: "operation_queued",
5747
+ requestId: message.requestId,
5748
+ queuePosition: workspaceSyncSingleFlight.branchMutationQueuePosition()
5749
+ });
5615
5750
  try {
5616
5751
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
5617
- const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
5752
+ const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(() => {
5618
5753
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5619
5754
  throw new Error(
5620
5755
  `Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
@@ -5645,6 +5780,9 @@ async function startWorker(options) {
5645
5780
  projectId: message.projectId,
5646
5781
  branchName: message.branchName
5647
5782
  });
5783
+ if (pendingLocalBranch) {
5784
+ projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
5785
+ }
5648
5786
  pendingCreatedBranchPublicationNotBefore.delete(pendingCreatedBranchKey(message.projectId, message.branchName));
5649
5787
  if (pendingMirrorDeletes.has(deletionKey)) return true;
5650
5788
  stageProjectBranchDeletion(project, message.branchName, true);
@@ -5922,7 +6060,7 @@ async function startWorker(options) {
5922
6060
  ws,
5923
6061
  message,
5924
6062
  resolvedTarget,
5925
- planRoot,
6063
+ rootDir,
5926
6064
  assertAdmission: assertMessageAdmission,
5927
6065
  ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
5928
6066
  ...targetMayMutateVisibleWorkspace(message.target) ? {
@@ -6020,8 +6158,7 @@ async function startWorker(options) {
6020
6158
  resolvedTarget,
6021
6159
  baseUrl,
6022
6160
  token,
6023
- artifactRoot,
6024
- planRoot,
6161
+ rootDir,
6025
6162
  setPreparationHeartbeatLease: (active) => {
6026
6163
  sendWorkerMessageFromCurrentSource(ws, {
6027
6164
  type: "heartbeat_lease",
@@ -6090,8 +6227,7 @@ async function startWorker(options) {
6090
6227
  resolvedTarget,
6091
6228
  baseUrl,
6092
6229
  token,
6093
- artifactRoot,
6094
- planRoot,
6230
+ rootDir,
6095
6231
  setPreparationHeartbeatLease: (active) => {
6096
6232
  sendWorkerMessageFromCurrentSource(ws, {
6097
6233
  type: "heartbeat_lease",
@@ -6173,8 +6309,7 @@ async function startWorker(options) {
6173
6309
  resolvedTarget,
6174
6310
  baseUrl,
6175
6311
  token,
6176
- artifactRoot,
6177
- planRoot,
6312
+ rootDir,
6178
6313
  assertAdmission: assertMessageAdmission
6179
6314
  });
6180
6315
  });