@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/cjs/main.cjs CHANGED
@@ -72,6 +72,8 @@ var import_workspace_incident_state = require("./workspace-incident-state.cjs");
72
72
  var import_workspace_automatic_sync_policy = require("./workspace-automatic-sync-policy.cjs");
73
73
  var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
74
74
  var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
75
+ var import_workspace_projection_ledger = require("./workspace-projection-ledger.cjs");
76
+ var import_workspace_sync_coalescer = require("./workspace-sync-coalescer.cjs");
75
77
  var import_workspace_path_move = require("./workspace-path-move.cjs");
76
78
  var import_workspace_project_config_policy = require("./workspace-project-config-policy.cjs");
77
79
  var import_workspace_preserve_only_policy = require("./workspace-preserve-only-policy.cjs");
@@ -175,6 +177,12 @@ const workspaceSyncSingleFlight = {
175
177
  acquireMutation() {
176
178
  return workspaceMutationGate.acquireMutation();
177
179
  },
180
+ runBranchMutation(operation) {
181
+ return workspaceMutationGate.runBranchMutation(operation);
182
+ },
183
+ branchMutationQueuePosition() {
184
+ return workspaceMutationGate.branchMutationQueuePosition();
185
+ },
178
186
  afterCurrent() {
179
187
  return workspaceSyncQueue;
180
188
  }
@@ -305,6 +313,7 @@ const workerPtyTestHarness = {
305
313
  removeEnvFiles: removePtyEnvFiles,
306
314
  resolveEnvFileReferences: resolvePtyEnvFileReferences,
307
315
  commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
316
+ targetProcessEnv,
308
317
  canonicalSyncTerminalHead,
309
318
  ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
310
319
  parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
@@ -312,21 +321,12 @@ const workerPtyTestHarness = {
312
321
  function defaultConfigPath() {
313
322
  return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
314
323
  }
315
- function defaultProjectsRoot() {
316
- return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "projects");
317
- }
318
- function defaultSyncRoot() {
319
- return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "sync");
320
- }
321
- function defaultArtifactRoot() {
322
- return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "artifacts");
323
- }
324
- function defaultPlanRoot() {
325
- return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "plans");
324
+ function defaultRootDir() {
325
+ return import_node_path.default.join(import_node_os.default.homedir(), ".r5d");
326
326
  }
327
327
  function printHelp() {
328
328
  process.stdout.write(`Usage:
329
- r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
329
+ r5d-worker start --label <label> [--root <dir>] [--base-url <url>] [--token <token>] [--api-key <key>]
330
330
  r5d-worker --version
331
331
 
332
332
  Options:
@@ -335,10 +335,7 @@ Options:
335
335
  --token <token> r5d worker token
336
336
  --api-key <key> r5d API key
337
337
  --config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
338
- --project-root <dir> Override projects checkout root (default ~/.r5d/projects)
339
- --sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
340
- --artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
341
- --plan-root <dir> Override synced plan root (default from R5D_PLANS_ROOT or ~/.r5d/plans)
338
+ --root <dir> Override worker data root (default from R5D_ROOT or ~/.r5d)
342
339
  -v, --version Show r5d-worker version
343
340
  -h, --help Show this help
344
341
  `);
@@ -453,24 +450,9 @@ function parseArgs(argv) {
453
450
  options.label = label;
454
451
  continue;
455
452
  }
456
- const projectRoot = readOption("--project-root");
457
- if (projectRoot !== void 0) {
458
- options.projectRoot = projectRoot;
459
- continue;
460
- }
461
- const syncRoot = readOption("--sync-root");
462
- if (syncRoot !== void 0) {
463
- options.syncRoot = syncRoot;
464
- continue;
465
- }
466
- const artifactRoot = readOption("--artifact-root");
467
- if (artifactRoot !== void 0) {
468
- options.artifactRoot = artifactRoot;
469
- continue;
470
- }
471
- const planRoot = readOption("--plan-root");
472
- if (planRoot !== void 0) {
473
- options.planRoot = planRoot;
453
+ const rootDir = readOption("--root");
454
+ if (rootDir !== void 0) {
455
+ options.rootDir = rootDir;
474
456
  continue;
475
457
  }
476
458
  rest.push(arg);
@@ -515,11 +497,9 @@ async function readResponseText(response) {
515
497
  return "";
516
498
  }
517
499
  }
518
- const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
519
- const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
520
- const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
521
- const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
522
- const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
500
+ const R5D_ROOT_REF = "$R5D_ROOT";
501
+ const R5D_ROOT_ENV = "R5D_ROOT";
502
+ const BUILT_IN_TOOL_PATH_REFS = [R5D_ROOT_REF];
523
503
  function parseBuiltInToolPath(inputPath) {
524
504
  const normalized = inputPath.replace(/\\/g, "/");
525
505
  const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
@@ -535,6 +515,26 @@ function parseBuiltInToolPath(inputPath) {
535
515
  relativePath: match[3] ?? ""
536
516
  };
537
517
  }
518
+ function expandRootPathSegments(relativePath, builtInPaths) {
519
+ const values = {
520
+ R5D_SESSION_ID: builtInPaths.sessionId,
521
+ R5D_PROJECT_ID: builtInPaths.projectId,
522
+ R5D_BRANCH_NAME: builtInPaths.branchName,
523
+ R5D_ACTIVE_PLAN_ID: builtInPaths.activePlanId
524
+ };
525
+ let expandedPath = relativePath;
526
+ for (const [variableName, value] of Object.entries(values)) {
527
+ const variablePattern = new RegExp(`\\$(?:\\{${variableName}\\}|${variableName})(?=\\/|\\.|$)`, "g");
528
+ if (!variablePattern.test(expandedPath)) continue;
529
+ if (!value) throw new Error(`$${variableName} is unavailable for this worker path`);
530
+ expandedPath = expandedPath.replace(variablePattern, value);
531
+ }
532
+ const unsupportedVariable = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/.exec(expandedPath);
533
+ if (unsupportedVariable) {
534
+ throw new Error(`Unsupported ${R5D_ROOT_REF} path variable "$${unsupportedVariable[1] ?? unsupportedVariable[2]}"`);
535
+ }
536
+ return expandedPath.split("/").filter(Boolean);
537
+ }
538
538
  function validateArtifactSessionId(sessionId) {
539
539
  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)) {
540
540
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -550,14 +550,6 @@ function sessionArtifactDir(artifactRoot, sessionId) {
550
550
  validateArtifactSessionId(sessionId);
551
551
  return import_node_path.default.join(artifactRoot, sessionId);
552
552
  }
553
- function artifactEnv(artifactRoot, sessionId) {
554
- if (!sessionId) {
555
- return {};
556
- }
557
- return {
558
- R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
559
- };
560
- }
561
553
  function projectPlanDir(planRoot, projectId, branchName) {
562
554
  validateProjectId(projectId);
563
555
  validateBranchName(branchName);
@@ -570,21 +562,18 @@ function targetPlanDir(planRoot, target) {
570
562
  }
571
563
  return import_node_path.default.join(planRoot, "workspace");
572
564
  }
573
- function planEnv(planRoot, target, activePlanId) {
574
- const plansDir = targetPlanDir(planRoot, target);
575
- const env = {
576
- [R5D_PLANS_DIR_ENV]: plansDir
577
- };
565
+ function planEnv(activePlanId) {
566
+ const env = {};
578
567
  if (activePlanId) {
579
568
  validatePlanId(activePlanId);
580
569
  env.R5D_ACTIVE_PLAN_ID = activePlanId;
581
- env.R5D_ACTIVE_PLAN_FILE = import_node_path.default.join(plansDir, `${activePlanId}.plan.md`);
582
570
  }
583
571
  return env;
584
572
  }
585
573
  function isArtifactEnvPath(filePath) {
586
574
  try {
587
- return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
575
+ const parsed = parseBuiltInToolPath(filePath);
576
+ return parsed?.ref === R5D_ROOT_REF && parsed.relativePath.split("/").filter(Boolean)[0] === "artifacts";
588
577
  } catch {
589
578
  return false;
590
579
  }
@@ -726,48 +715,46 @@ async function prepareBuiltInToolPaths(input) {
726
715
  if (!parsed) {
727
716
  return void 0;
728
717
  }
729
- if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
718
+ const builtInPaths = {
719
+ rootDir: input.rootDir,
720
+ sessionId: input.sessionId,
721
+ activePlanId: input.activePlanId,
722
+ ...input.target.type === "project" ? { projectId: input.target.projectId, branchName: input.target.branchName } : {}
723
+ };
724
+ const rootPathSegments = expandRootPathSegments(parsed.relativePath, builtInPaths);
725
+ if (rootPathSegments[0] === "artifacts") {
730
726
  if (input.access === "write") {
731
727
  throw new Error(
732
- `${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
728
+ `${R5D_ROOT_REF}/artifacts attachments are read-only. Copy them into a project path before editing or creating files.`
733
729
  );
734
730
  }
735
- if (!input.sessionId) {
736
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
737
- }
738
- return {
739
- artifactsDir: await syncSessionArtifacts({
740
- baseUrl: input.baseUrl,
741
- token: input.token,
742
- sessionId: input.sessionId,
743
- artifactRoot: input.artifactRoot
744
- })
745
- };
746
- }
747
- const plansDir = targetPlanDir(input.planRoot, input.target);
748
- import_node_fs.default.mkdirSync(plansDir, { recursive: true });
749
- if (parsed.ref === R5D_PLANS_DIR_REF) {
750
- return { plansDir };
731
+ const referencedSessionId = rootPathSegments[1];
732
+ if (!referencedSessionId) return builtInPaths;
733
+ validateArtifactSessionId(referencedSessionId);
734
+ await syncSessionArtifacts({
735
+ baseUrl: input.baseUrl,
736
+ token: input.token,
737
+ sessionId: referencedSessionId,
738
+ artifactRoot: import_node_path.default.join(input.rootDir, "artifacts")
739
+ });
751
740
  }
752
- if (!input.activePlanId) {
753
- throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
741
+ if (rootPathSegments[0] === "plans") {
742
+ import_node_fs.default.mkdirSync(targetPlanDir(import_node_path.default.join(input.rootDir, "plans"), input.target), { recursive: true });
754
743
  }
755
- validatePlanId(input.activePlanId);
756
- return {
757
- plansDir,
758
- activePlanFile: import_node_path.default.join(plansDir, `${input.activePlanId}.plan.md`)
759
- };
744
+ return builtInPaths;
760
745
  }
761
746
  async function prepareArtifactEnvForShell(input) {
747
+ const env = { [R5D_ROOT_ENV]: input.rootDir };
762
748
  if (!input.sessionId) {
763
- return {};
749
+ return env;
764
750
  }
751
+ const artifactRoot = import_node_path.default.join(input.rootDir, "artifacts");
765
752
  try {
766
753
  await syncSessionArtifacts({
767
754
  baseUrl: input.baseUrl,
768
755
  token: input.token,
769
756
  sessionId: input.sessionId,
770
- artifactRoot: input.artifactRoot
757
+ artifactRoot
771
758
  });
772
759
  } catch (error) {
773
760
  process.stderr.write(
@@ -775,16 +762,14 @@ async function prepareArtifactEnvForShell(input) {
775
762
  `
776
763
  );
777
764
  }
778
- const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
765
+ const artifactsDir = sessionArtifactDir(artifactRoot, input.sessionId);
779
766
  import_node_fs.default.mkdirSync(artifactsDir, { recursive: true });
780
- return {
781
- R5D_ARTIFACTS_DIR: artifactsDir
782
- };
767
+ return env;
783
768
  }
784
769
  function preparePlanEnvForShell(input) {
785
- const plansDir = targetPlanDir(input.planRoot, input.target);
770
+ const plansDir = targetPlanDir(import_node_path.default.join(input.rootDir, "plans"), input.target);
786
771
  import_node_fs.default.mkdirSync(plansDir, { recursive: true });
787
- return planEnv(input.planRoot, input.target, input.activePlanId);
772
+ return planEnv(input.activePlanId);
788
773
  }
789
774
  async function verifyR5dctlAuth(baseUrl, token, workerLabel) {
790
775
  const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
@@ -952,30 +937,17 @@ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
952
937
  if (parsed.relativePath.split("/").includes("..")) {
953
938
  throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
954
939
  }
955
- let rootPath;
956
- let absolutePath;
957
- if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
958
- rootPath = builtInPaths?.artifactsDir;
959
- } else if (parsed.ref === R5D_PLANS_DIR_REF) {
960
- rootPath = builtInPaths?.plansDir;
961
- } else {
962
- if (parsed.relativePath) {
963
- throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
964
- }
965
- rootPath = builtInPaths?.plansDir;
966
- absolutePath = builtInPaths?.activePlanFile;
967
- }
968
- if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
969
- const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
970
- throw new Error(`${parsed.ref} paths require ${requirement}`);
971
- }
972
- const normalizedPath = parsed.relativePath ? import_node_path.default.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
940
+ const rootPath = builtInPaths?.rootDir;
941
+ if (!rootPath) throw new Error(`${R5D_ROOT_REF} paths require an available worker root`);
942
+ const expandedPath = expandRootPathSegments(parsed.relativePath, builtInPaths);
943
+ if (expandedPath.includes("..")) throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
944
+ const normalizedPath = expandedPath.length > 0 ? import_node_path.default.posix.normalize(expandedPath.join("/")).replace(/^\/+|\/+$/g, "") : "";
973
945
  const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
974
- absolutePath ??= normalizedRelativePath ? import_node_path.default.resolve(rootPath, ...normalizedRelativePath.split("/")) : import_node_path.default.resolve(rootPath);
946
+ const absolutePath = normalizedRelativePath ? import_node_path.default.resolve(rootPath, ...normalizedRelativePath.split("/")) : import_node_path.default.resolve(rootPath);
975
947
  assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
976
948
  return {
977
949
  absolutePath,
978
- displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
950
+ displayPath: parsed.relativePath ? `${parsed.ref}/${parsed.relativePath}` : parsed.ref,
979
951
  repoRelativePath: null,
980
952
  scope: "virtual",
981
953
  virtualRootPath: import_node_path.default.resolve(rootPath)
@@ -2219,8 +2191,7 @@ async function executeReadFileOperation(input) {
2219
2191
  token: input.token,
2220
2192
  sessionId: input.message.sessionId,
2221
2193
  activePlanId: input.message.activePlanId,
2222
- artifactRoot: input.artifactRoot,
2223
- planRoot: input.planRoot,
2194
+ rootDir: input.rootDir,
2224
2195
  access: "read"
2225
2196
  });
2226
2197
  return readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
@@ -2233,8 +2204,7 @@ async function executeWriteFileOperation(input) {
2233
2204
  token: input.token,
2234
2205
  sessionId: input.message.sessionId,
2235
2206
  activePlanId: input.message.activePlanId,
2236
- artifactRoot: input.artifactRoot,
2237
- planRoot: input.planRoot,
2207
+ rootDir: input.rootDir,
2238
2208
  access: "write"
2239
2209
  });
2240
2210
  input.assertAdmission();
@@ -2252,8 +2222,7 @@ async function executeEditFileOperation(input) {
2252
2222
  token: input.token,
2253
2223
  sessionId: input.message.sessionId,
2254
2224
  activePlanId: input.message.activePlanId,
2255
- artifactRoot: input.artifactRoot,
2256
- planRoot: input.planRoot,
2225
+ rootDir: input.rootDir,
2257
2226
  access: "write"
2258
2227
  });
2259
2228
  input.assertAdmission();
@@ -2380,8 +2349,7 @@ async function executeGrepOperation(input) {
2380
2349
  token: input.token,
2381
2350
  sessionId: input.message.sessionId,
2382
2351
  activePlanId: input.message.activePlanId,
2383
- artifactRoot: input.artifactRoot,
2384
- planRoot: input.planRoot,
2352
+ rootDir: input.rootDir,
2385
2353
  access: "read"
2386
2354
  });
2387
2355
  return grepWorkerFiles(
@@ -2439,8 +2407,7 @@ async function executeFindOperation(input) {
2439
2407
  token: input.token,
2440
2408
  sessionId: input.message.sessionId,
2441
2409
  activePlanId: input.message.activePlanId,
2442
- artifactRoot: input.artifactRoot,
2443
- planRoot: input.planRoot,
2410
+ rootDir: input.rootDir,
2444
2411
  access: "read"
2445
2412
  });
2446
2413
  return findWorkerFiles(
@@ -2481,8 +2448,7 @@ async function executeLsOperation(input) {
2481
2448
  token: input.token,
2482
2449
  sessionId: input.message.sessionId,
2483
2450
  activePlanId: input.message.activePlanId,
2484
- artifactRoot: input.artifactRoot,
2485
- planRoot: input.planRoot,
2451
+ rootDir: input.rootDir,
2486
2452
  access: "read"
2487
2453
  });
2488
2454
  return listWorkerDirectory(input.resolvedTarget.rootPath, input.message.path, input.message.limit, builtInPaths);
@@ -2588,8 +2554,7 @@ async function executeViewFileBytesOperation(input) {
2588
2554
  token: input.token,
2589
2555
  sessionId: input.message.sessionId,
2590
2556
  activePlanId: input.message.activePlanId,
2591
- artifactRoot: input.artifactRoot,
2592
- planRoot: input.planRoot,
2557
+ rootDir: input.rootDir,
2593
2558
  access: "read"
2594
2559
  });
2595
2560
  return readWorkerImageFile(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
@@ -2629,27 +2594,30 @@ function targetIdentityEnv(target) {
2629
2594
  R5D_BRANCH_NAME: ""
2630
2595
  };
2631
2596
  }
2597
+ function targetProcessEnv(rootDir, target) {
2598
+ return { [R5D_ROOT_ENV]: rootDir, ...targetIdentityEnv(target) };
2599
+ }
2632
2600
  async function prepareShellEnvForTarget(input) {
2633
2601
  const artifactProcessEnv = await prepareArtifactEnvForShell({
2634
2602
  baseUrl: input.baseUrl,
2635
2603
  token: input.token,
2636
2604
  sessionId: input.sessionId,
2637
- artifactRoot: input.artifactRoot
2605
+ rootDir: input.rootDir
2638
2606
  });
2639
2607
  if (input.target.type === "workspace" && input.target.rootProfile === "canonical_sync") {
2640
2608
  return {
2641
2609
  ...artifactProcessEnv,
2642
- ...targetIdentityEnv(input.target)
2610
+ ...targetProcessEnv(input.rootDir, input.target)
2643
2611
  };
2644
2612
  }
2645
2613
  return {
2646
2614
  ...artifactProcessEnv,
2647
2615
  ...preparePlanEnvForShell({
2648
2616
  target: input.target,
2649
- planRoot: input.planRoot,
2617
+ rootDir: input.rootDir,
2650
2618
  activePlanId: input.activePlanId
2651
2619
  }),
2652
- ...targetIdentityEnv(input.target)
2620
+ ...targetProcessEnv(input.rootDir, input.target)
2653
2621
  };
2654
2622
  }
2655
2623
  async function executeCommand(input) {
@@ -2664,13 +2632,12 @@ async function executeCommand(input) {
2664
2632
  };
2665
2633
  input.setPreparationHeartbeatLease(true);
2666
2634
  try {
2667
- const targetProcessEnv = await prepareShellEnvForTarget({
2635
+ const targetProcessEnv2 = await prepareShellEnvForTarget({
2668
2636
  target: input.resolvedTarget.target,
2669
2637
  baseUrl: input.baseUrl,
2670
2638
  token: input.token,
2671
2639
  sessionId: input.message.sessionId,
2672
- artifactRoot: input.artifactRoot,
2673
- planRoot: input.planRoot,
2640
+ rootDir: input.rootDir,
2674
2641
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2675
2642
  });
2676
2643
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
@@ -2680,7 +2647,7 @@ async function executeCommand(input) {
2680
2647
  stdout: "pipe",
2681
2648
  stderr: "pipe",
2682
2649
  detached: true,
2683
- env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
2650
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2])
2684
2651
  });
2685
2652
  spawnedProcess = subprocess;
2686
2653
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
@@ -2769,13 +2736,12 @@ async function executeStreamingCommand(input) {
2769
2736
  });
2770
2737
  return;
2771
2738
  }
2772
- const targetProcessEnv = await prepareShellEnvForTarget({
2739
+ const targetProcessEnv2 = await prepareShellEnvForTarget({
2773
2740
  target: input.resolvedTarget.target,
2774
2741
  baseUrl: input.baseUrl,
2775
2742
  token: input.token,
2776
2743
  sessionId: input.message.sessionId,
2777
- artifactRoot: input.artifactRoot,
2778
- planRoot: input.planRoot,
2744
+ rootDir: input.rootDir,
2779
2745
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2780
2746
  });
2781
2747
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
@@ -2789,7 +2755,7 @@ async function executeStreamingCommand(input) {
2789
2755
  stdout: "pipe",
2790
2756
  stderr: "pipe",
2791
2757
  detached: true,
2792
- env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
2758
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2])
2793
2759
  });
2794
2760
  spawnedProcess = subprocess;
2795
2761
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
@@ -3253,10 +3219,10 @@ async function openPty(input) {
3253
3219
  }
3254
3220
  const planProcessEnv = !(input.resolvedTarget.target.type === "workspace" && input.resolvedTarget.target.rootProfile === "canonical_sync") ? preparePlanEnvForShell({
3255
3221
  target: input.resolvedTarget.target,
3256
- planRoot: input.planRoot,
3222
+ rootDir: input.rootDir,
3257
3223
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
3258
3224
  }) : {};
3259
- const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
3225
+ const targetEnv = targetProcessEnv(input.rootDir, input.resolvedTarget.target);
3260
3226
  const shell = resolveHostShell(input.message.command);
3261
3227
  const outputCoalescer = (0, import_pty_output_coalescer.createPtyOutputCoalescer)({
3262
3228
  send: (data) => {
@@ -3288,7 +3254,7 @@ async function openPty(input) {
3288
3254
  githubProcessEnv(),
3289
3255
  resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
3290
3256
  planProcessEnv,
3291
- targetProcessEnv,
3257
+ targetEnv,
3292
3258
  shell.env ?? {}
3293
3259
  ])
3294
3260
  },
@@ -3461,12 +3427,13 @@ async function startWorker(options) {
3461
3427
  initializeCredentialStoreDirectoryOnce();
3462
3428
  const label = requireValue(options.label, "Missing required --label <label>");
3463
3429
  validateLabel(label);
3464
- const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
3465
- const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
3430
+ const rootDir = import_node_path.default.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
3431
+ const projectsRoot = import_node_path.default.join(rootDir, "projects");
3432
+ const syncRoot = import_node_path.default.join(rootDir, "sync");
3466
3433
  const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
3467
3434
  const projectWorktreeSnapshotsRoot = import_node_path.default.join(syncRoot, "project-worktree-snapshots");
3468
- const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
3469
- const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
3435
+ const artifactRoot = import_node_path.default.join(rootDir, "artifacts");
3436
+ const planRoot = import_node_path.default.join(rootDir, "plans");
3470
3437
  (0, import_managed_paths.assertDisjointManagedRoots)([
3471
3438
  { label: "projects root", rootPath: projectsRoot },
3472
3439
  { label: "sync root", rootPath: syncRoot },
@@ -3475,13 +3442,7 @@ async function startWorker(options) {
3475
3442
  ]);
3476
3443
  process.stdout.write(`[r5d-worker] label: ${label}
3477
3444
  `);
3478
- process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
3479
- `);
3480
- process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
3481
- `);
3482
- process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
3483
- `);
3484
- process.stdout.write(`[r5d-worker] plan root: ${planRoot}
3445
+ process.stdout.write(`[r5d-worker] root: ${rootDir}
3485
3446
  `);
3486
3447
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
3487
3448
  `);
@@ -3506,9 +3467,29 @@ async function startWorker(options) {
3506
3467
  const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
3507
3468
  const lastObservedProjectHeads = /* @__PURE__ */ new Map();
3508
3469
  const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3470
+ const advertisedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
3471
+ let advertisedWorkspaceHead;
3509
3472
  const fetchedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3510
- const lastProjectMirrorFetchAtMs = /* @__PURE__ */ new Map();
3473
+ const fetchedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
3511
3474
  const projectMirrorFetchSkippingEnabled = process.env.R5D_PROJECT_MIRROR_FETCH_SKIP !== "0";
3475
+ const forgetLocalProjectMirrorRefsTokens = (projectId, branchName) => {
3476
+ fetchedProjectMirrorRefsTokens.delete(projectId);
3477
+ if (branchName === void 0) fetchedProjectMirrorMountTokens.delete(projectId);
3478
+ else fetchedProjectMirrorMountTokens.get(projectId)?.delete(branchName);
3479
+ };
3480
+ const recordLocalProjectMirrorRefsTokens = async (project) => {
3481
+ try {
3482
+ const tokens = await (0, import_project_worktrees.computeLocalProjectMirrorRefsTokens)({
3483
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3484
+ primaryBranchName: primaryProjectBranch(project),
3485
+ mounts: project.branches.map(({ branchId, branchName }) => ({ branchId, branchName }))
3486
+ });
3487
+ fetchedProjectMirrorRefsTokens.set(project.projectId, tokens.project);
3488
+ fetchedProjectMirrorMountTokens.set(project.projectId, tokens.mounts);
3489
+ } catch {
3490
+ forgetLocalProjectMirrorRefsTokens(project.projectId);
3491
+ }
3492
+ };
3512
3493
  const pendingMirrorDeletes = /* @__PURE__ */ new Map();
3513
3494
  let workspaceRemoteUrl = null;
3514
3495
  let workspaceCredentialHelper = null;
@@ -3523,10 +3504,12 @@ async function startWorker(options) {
3523
3504
  let workspacePeriodicTimer;
3524
3505
  let pendingAutomaticTrigger;
3525
3506
  const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
3507
+ const workspaceProjectionLedger = new import_workspace_projection_ledger.WorkspaceProjectionLedger();
3526
3508
  let automaticSyncInFlight = false;
3527
3509
  let terminalReplayTimer;
3528
3510
  let workspaceSyncRequestsInFlight = 0;
3529
3511
  let lastWorkspaceSyncMirrorObservationMs = 0;
3512
+ let lastInventoriedMissingCheckouts;
3530
3513
  let sessionArtifactSyncRequestsInFlight = 0;
3531
3514
  let cliUpdateInProgress = false;
3532
3515
  let reloadAfterClose = false;
@@ -3584,8 +3567,15 @@ async function startWorker(options) {
3584
3567
  const primaryBranchName = primaryProjectBranch(project);
3585
3568
  const connection = projectConnection(project);
3586
3569
  const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
3587
- if (requireLocalBranch || hasProjectWorktree(sourcePath)) {
3588
- (0, import_project_worktrees.deleteLinkedProjectBranch)({ projectRoot, primaryBranchName, branchName });
3570
+ const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryBranchName);
3571
+ if (requireLocalBranch || import_node_fs.default.existsSync(sourcePath) && hasProjectWorktree(primaryPath)) {
3572
+ const deleted = (0, import_project_worktrees.deleteLinkedProjectBranch)({ projectRoot, primaryBranchName, branchName });
3573
+ if (deleted.movedAsidePath) {
3574
+ process.stderr.write(
3575
+ `[r5d-worker] set aside ${sourcePath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
3576
+ `
3577
+ );
3578
+ }
3589
3579
  }
3590
3580
  const planSourcePath = import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"));
3591
3581
  import_node_fs.default.rmSync(planSourcePath, { recursive: true, force: true });
@@ -3658,11 +3648,17 @@ async function startWorker(options) {
3658
3648
  if (branchName === primaryBranchName) {
3659
3649
  throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
3660
3650
  }
3661
- (0, import_project_worktrees.deleteLinkedProjectBranch)({
3651
+ const deleted = (0, import_project_worktrees.deleteLinkedProjectBranch)({
3662
3652
  projectRoot: configuredProjectRoot(projectsRoot, project),
3663
3653
  primaryBranchName,
3664
3654
  branchName
3665
3655
  });
3656
+ if (deleted.movedAsidePath) {
3657
+ process.stderr.write(
3658
+ `[r5d-worker] set aside ${branchPath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
3659
+ `
3660
+ );
3661
+ }
3666
3662
  import_node_fs.default.rmSync(import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
3667
3663
  readyProjectIds.delete(project.projectId);
3668
3664
  reconciledProjectConfigFingerprints.delete(project.projectId);
@@ -3831,6 +3827,40 @@ async function startWorker(options) {
3831
3827
  });
3832
3828
  };
3833
3829
  const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3830
+ const inventoryConfiguredProjectCheckouts = () => {
3831
+ const inventory = { projects: [], missingCheckouts: [] };
3832
+ for (const project of projectConfigById.values()) {
3833
+ if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) {
3834
+ inventory.projects.push(project);
3835
+ continue;
3836
+ }
3837
+ const checkouts = (0, import_project_worktrees.inventoryProjectCheckouts)({
3838
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3839
+ primaryBranchName: primaryProjectBranch(project),
3840
+ branchNames: project.branches.map(({ branchName }) => branchName)
3841
+ });
3842
+ if (checkouts.missingCheckouts.length === 0) {
3843
+ inventory.projects.push(project);
3844
+ continue;
3845
+ }
3846
+ for (const missing of checkouts.missingCheckouts) inventory.missingCheckouts.push({ projectId: project.projectId, ...missing });
3847
+ const linked = new Set(checkouts.linkedBranchNames);
3848
+ inventory.projects.push({ ...project, branches: project.branches.filter(({ branchName }) => linked.has(branchName)) });
3849
+ }
3850
+ inventory.missingCheckouts.sort(
3851
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
3852
+ );
3853
+ return inventory;
3854
+ };
3855
+ const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
3856
+ const logMissingCheckouts = (inventory) => {
3857
+ if (inventory.missingCheckouts.length === 0) return;
3858
+ const described = inventory.missingCheckouts.map(({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`).join(", ");
3859
+ process.stderr.write(
3860
+ `[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
3861
+ `
3862
+ );
3863
+ };
3834
3864
  const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
3835
3865
  const mounts = [];
3836
3866
  const creatorLocalIncarnations = new Map(
@@ -3892,7 +3922,7 @@ async function startWorker(options) {
3892
3922
  workspaceRelativePath: "workspace-plans",
3893
3923
  sourceMode: "all",
3894
3924
  hydrateDeletionMode: "all",
3895
- // Workspace-level agents receive this directory through R5D_PLANS_DIR.
3925
+ // Workspace-level agents reach this directory through $R5D_ROOT/plans.
3896
3926
  // Keep both projection and hydration away from it while any visible-
3897
3927
  // projects process, PTY, or built-in write/edit is active or reserved.
3898
3928
  busy: () => {
@@ -3930,7 +3960,10 @@ async function startWorker(options) {
3930
3960
  deleteWhenSourceMissing: true
3931
3961
  });
3932
3962
  }
3933
- return mounts;
3963
+ return mounts.map((mount) => {
3964
+ const lastProjectedMutationToken = workspaceProjectionLedger.lastProjectedToken(mount);
3965
+ return lastProjectedMutationToken === void 0 ? mount : { ...mount, lastProjectedMutationToken };
3966
+ });
3934
3967
  };
3935
3968
  const workspaceLocalHead = async () => {
3936
3969
  if (!import_node_fs.default.existsSync(import_node_path.default.join(workspaceShadowRoot, ".git"))) return null;
@@ -4106,9 +4139,9 @@ async function startWorker(options) {
4106
4139
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
4107
4140
  );
4108
4141
  };
4109
- const captureProjectHeadsForWorkspacePublication = async () => {
4142
+ const captureProjectHeadsForWorkspacePublication = async (projects = projectConfigById.values()) => {
4110
4143
  const heads = /* @__PURE__ */ new Map();
4111
- for (const project of projectConfigById.values()) {
4144
+ for (const project of projects) {
4112
4145
  if (!readyProjectIds.has(project.projectId)) continue;
4113
4146
  for (const branch of project.branches) {
4114
4147
  const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
@@ -4118,9 +4151,9 @@ async function startWorker(options) {
4118
4151
  }
4119
4152
  return heads;
4120
4153
  };
4121
- const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4154
+ const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set(), projects = projectConfigById.values()) => {
4122
4155
  const active = new Set(activeMountIds);
4123
- for (const project of projectConfigById.values()) {
4156
+ for (const project of projects) {
4124
4157
  if (!readyProjectIds.has(project.projectId) || !(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) continue;
4125
4158
  const changed = /* @__PURE__ */ new Map();
4126
4159
  for (const branch of project.branches) {
@@ -4155,17 +4188,7 @@ async function startWorker(options) {
4155
4188
  for (const result of results) {
4156
4189
  lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
4157
4190
  }
4158
- try {
4159
- fetchedProjectMirrorRefsTokens.set(
4160
- project.projectId,
4161
- await (0, import_project_worktrees.computeLocalProjectMirrorRefsToken)({
4162
- projectRoot: configuredProjectRoot(projectsRoot, project),
4163
- primaryBranchName: primaryProjectBranch(project)
4164
- })
4165
- );
4166
- } catch {
4167
- fetchedProjectMirrorRefsTokens.delete(project.projectId);
4168
- }
4191
+ await recordLocalProjectMirrorRefsTokens(project);
4169
4192
  }
4170
4193
  const publishedTombstoneIds = [
4171
4194
  ...new Set(
@@ -4191,7 +4214,7 @@ async function startWorker(options) {
4191
4214
  credentialHelper: deletion.credentialHelper,
4192
4215
  credentialUsername: deletion.credentialUsername
4193
4216
  });
4194
- fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4217
+ forgetLocalProjectMirrorRefsTokens(deletion.projectId, deletion.branchName);
4195
4218
  }
4196
4219
  if (deletion.tombstoneId && !deletion.projectDeleted) {
4197
4220
  projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
@@ -4203,8 +4226,8 @@ async function startWorker(options) {
4203
4226
  if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
4204
4227
  removeProjectCredentialStore(deletion.projectId);
4205
4228
  advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
4206
- fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4207
- lastProjectMirrorFetchAtMs.delete(deletion.projectId);
4229
+ advertisedProjectMirrorMountTokens.delete(deletion.projectId);
4230
+ forgetLocalProjectMirrorRefsTokens(deletion.projectId);
4208
4231
  }
4209
4232
  lastObservedProjectHeads.delete(key);
4210
4233
  }
@@ -4213,71 +4236,63 @@ async function startWorker(options) {
4213
4236
  const connection = projectConnection(project);
4214
4237
  const projectRoot = configuredProjectRoot(projectsRoot, project);
4215
4238
  const primaryBranchName = primaryProjectBranch(project);
4216
- if (options2.refreshRemote) {
4217
- const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
4218
- const observations = await (0, import_project_worktrees.observeProjectMirrorHeads)({
4219
- projectRoot,
4220
- primaryBranchName,
4221
- branchNames: project.branches.map(({ branchName }) => branchName),
4222
- mirrorUrl: connection.mirrorUrl,
4223
- credentialHelper: connection.credentialHelper,
4224
- credentialUsername: connection.mirrorCredentialUsername,
4225
- allowNonFastForward: options2.allowNonFastForward,
4226
- branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4227
- });
4228
- if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
4229
- else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
4230
- lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
4231
- return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
4232
- }
4233
- return {
4234
- projectId: project.projectId,
4239
+ const observations = await (0, import_project_worktrees.observeProjectMirrorHeads)({
4235
4240
  projectRoot,
4236
4241
  primaryBranchName,
4237
- observations: await (0, import_project_worktrees.observeProjectMirrorHeads)({
4238
- projectRoot,
4239
- primaryBranchName,
4240
- branchNames: project.branches.map(({ branchName }) => branchName),
4241
- mirrorUrl: connection.mirrorUrl,
4242
- credentialHelper: connection.credentialHelper,
4243
- credentialUsername: connection.mirrorCredentialUsername,
4244
- allowNonFastForward: options2.allowNonFastForward,
4245
- branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
4246
- refreshRemote: false
4247
- })
4248
- };
4242
+ branchNames: project.branches.map(({ branchName }) => branchName),
4243
+ mirrorUrl: connection.mirrorUrl,
4244
+ credentialHelper: connection.credentialHelper,
4245
+ credentialUsername: connection.mirrorCredentialUsername,
4246
+ allowNonFastForward: options2.allowNonFastForward,
4247
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
4248
+ refreshRemote: options2.fetch.scope !== "none",
4249
+ ...options2.fetch.scope === "mounts" ? { fetchOnlyBranches: new Set(options2.fetch.mounts.map(({ branchName }) => branchName)) } : {}
4250
+ });
4251
+ if (options2.fetch.scope !== "none") await recordLocalProjectMirrorRefsTokens(project);
4252
+ return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
4249
4253
  };
4250
4254
  const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
4251
4255
  const observationStartedAt = Date.now();
4252
- const eligibleProjects = [...projectConfigById.values()].filter(
4256
+ const eligibleProjects = [...input.projects ?? projectConfigById.values()].filter(
4253
4257
  (project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
4254
4258
  );
4255
4259
  try {
4256
4260
  if (input.failClosed) {
4257
4261
  const bundles2 = [];
4258
4262
  for (const project of eligibleProjects) {
4259
- bundles2.push(await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, refreshRemote: true }));
4263
+ bundles2.push(
4264
+ await observeSingleProjectMirrorBundle(project, {
4265
+ allowNonFastForward: input.allowNonFastForward,
4266
+ fetch: { scope: "project", reason: "fail_closed" }
4267
+ })
4268
+ );
4260
4269
  }
4261
4270
  return bundles2;
4262
4271
  }
4263
- let fetchedCount = 0;
4272
+ let wholeMirrorFetches = 0;
4273
+ let scopedMirrorFetches = 0;
4274
+ let fetchedMounts = 0;
4275
+ let eligibleMounts = 0;
4264
4276
  const bundles = await mapWithConcurrencyLimit(
4265
4277
  eligibleProjects,
4266
4278
  PROJECT_MIRROR_OBSERVE_CONCURRENCY,
4267
4279
  async (project) => {
4268
4280
  try {
4269
- const decision = (0, import_project_mirror_fetch_policy.projectMirrorFetchDecision)({
4270
- advertisedToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
4271
- fetchedToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
4272
- lastFetchAtMs: lastProjectMirrorFetchAtMs.get(project.projectId),
4273
- nowMs: Date.now(),
4281
+ const fetch2 = (0, import_project_mirror_fetch_policy.planProjectMirrorObservationFetch)({
4282
+ branchNames: project.branches.map(({ branchName }) => branchName),
4283
+ advertisedProjectToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
4284
+ advertisedMountTokens: advertisedProjectMirrorMountTokens.get(project.projectId),
4285
+ fetchedProjectToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
4286
+ fetchedMountTokens: fetchedProjectMirrorMountTokens.get(project.projectId) ?? /* @__PURE__ */ new Map(),
4274
4287
  skippingEnabled: projectMirrorFetchSkippingEnabled
4275
4288
  });
4276
- if (decision.fetch) fetchedCount += 1;
4277
- return await observeSingleProjectMirrorBundle(project, {
4278
- allowNonFastForward: input.allowNonFastForward,
4279
- refreshRemote: decision.fetch
4280
- });
4289
+ eligibleMounts += project.branches.length;
4290
+ if (fetch2.scope === "project") wholeMirrorFetches += 1;
4291
+ if (fetch2.scope === "mounts") {
4292
+ scopedMirrorFetches += 1;
4293
+ fetchedMounts += fetch2.mounts.length;
4294
+ }
4295
+ return await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, fetch: fetch2 });
4281
4296
  } catch (error) {
4282
4297
  process.stderr.write(
4283
4298
  `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
@@ -4287,9 +4302,9 @@ async function startWorker(options) {
4287
4302
  }
4288
4303
  }
4289
4304
  );
4290
- if (fetchedCount > 0) {
4305
+ if (wholeMirrorFetches + scopedMirrorFetches > 0) {
4291
4306
  process.stderr.write(
4292
- `[r5d-worker] project mirror observation fetched ${fetchedCount} of ${eligibleProjects.length} mirror(s)
4307
+ `[r5d-worker] project mirror observation fetched ${wholeMirrorFetches + scopedMirrorFetches} of ${eligibleProjects.length} mirror(s) (${wholeMirrorFetches} whole, ${scopedMirrorFetches} scoped to ${fetchedMounts} of ${eligibleMounts} mount(s))
4293
4308
  `
4294
4309
  );
4295
4310
  }
@@ -4348,7 +4363,7 @@ async function startWorker(options) {
4348
4363
  }
4349
4364
  }
4350
4365
  };
4351
- const mapWorkspaceGitResult = async (attemptId, trigger, result) => {
4366
+ const mapWorkspaceGitResult = async (attemptId, trigger, result, missingCheckouts) => {
4352
4367
  const activeProjectBranchPublications = (0, import_workspace_publication_evidence.activeProjectBranchPublicationEvidence)([...projectConfigById.values()], result.activeMountIds);
4353
4368
  return {
4354
4369
  type: "workspace_sync",
@@ -4366,6 +4381,7 @@ async function startWorker(options) {
4366
4381
  affectedPaths: result.affectedPaths,
4367
4382
  activeMountIds: result.activeMountIds,
4368
4383
  skippedMountIds: result.skippedMountIds,
4384
+ ...missingCheckouts ? { missingCheckouts } : {},
4369
4385
  activeProjectBranchPublications,
4370
4386
  discardedPaths: [],
4371
4387
  localChangesDiscarded: false,
@@ -4393,6 +4409,7 @@ async function startWorker(options) {
4393
4409
  affectedPaths: [],
4394
4410
  discardedPaths: [],
4395
4411
  localChangesDiscarded: false,
4412
+ ...lastInventoriedMissingCheckouts ? { missingCheckouts: lastInventoriedMissingCheckouts } : {},
4396
4413
  error: error instanceof Error ? error.message : String(error)
4397
4414
  };
4398
4415
  };
@@ -4401,8 +4418,13 @@ async function startWorker(options) {
4401
4418
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4402
4419
  throw new Error("Worker workspace configuration has not been received");
4403
4420
  }
4404
- const configuredMounts = buildWorkspaceMounts();
4421
+ const checkoutInventory = inventoryConfiguredProjectCheckouts();
4422
+ logMissingCheckouts(checkoutInventory);
4423
+ const missingCheckouts = reportedMissingCheckouts(checkoutInventory);
4424
+ lastInventoriedMissingCheckouts = missingCheckouts;
4425
+ const configuredMounts = buildWorkspaceMounts(checkoutInventory.projects);
4405
4426
  if (input.resetToCanonical) {
4427
+ workspaceProjectionLedger.reset();
4406
4428
  const resetHasBusyLiveMount = configuredMounts.some(
4407
4429
  (mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
4408
4430
  );
@@ -4411,7 +4433,8 @@ async function startWorker(options) {
4411
4433
  }
4412
4434
  const observedProjectHeads2 = await observeProjectHeadsBeforeOuterWorkspace({
4413
4435
  allowNonFastForward: true,
4414
- failClosed: true
4436
+ failClosed: true,
4437
+ projects: checkoutInventory.projects
4415
4438
  });
4416
4439
  const reset = await (0, import_workspace_git_sync.resetWorkspaceGit)({
4417
4440
  workspacePath: workspaceShadowRoot,
@@ -4439,13 +4462,15 @@ async function startWorker(options) {
4439
4462
  affectedPaths: reset.discardedPaths,
4440
4463
  activeMountIds: reset.activeMountIds,
4441
4464
  skippedMountIds: reset.skippedMountIds,
4465
+ missingCheckouts,
4442
4466
  discardedPaths: reset.discardedPaths,
4443
4467
  localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
4444
4468
  };
4445
4469
  }
4446
4470
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4447
4471
  const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
4448
- const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4472
+ if (outerRemediation) workspaceProjectionLedger.reset();
4473
+ const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false, projects: checkoutInventory.projects });
4449
4474
  const expectedMirrorHeads = /* @__PURE__ */ new Map();
4450
4475
  for (const bundle of observedProjectHeads) {
4451
4476
  for (const observation of bundle.observations) {
@@ -4457,7 +4482,7 @@ async function startWorker(options) {
4457
4482
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
4458
4483
  )
4459
4484
  );
4460
- const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication();
4485
+ const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication(checkoutInventory.projects);
4461
4486
  const result = await (0, import_workspace_git_sync.synchronizeWorkspaceGit)({
4462
4487
  attemptId: input.attemptId,
4463
4488
  workerLabel: label,
@@ -4471,9 +4496,20 @@ async function startWorker(options) {
4471
4496
  allowLargeDiff: input.confirmedLargeDiff,
4472
4497
  skipMountMirror: outerRemediation,
4473
4498
  requiredAncestorHeads: input.requiredAncestorHeads,
4499
+ // Same gate as the mirror fetches: while the server's read of the hidden
4500
+ // workspace head equals the head this clone last fetched or pushed, the
4501
+ // cycle's workspace head fetches would transfer nothing.
4502
+ advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
4474
4503
  assertStillAdmitted: input.assertStillAdmitted,
4475
4504
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4476
- await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4505
+ await pushChangedProjectHeads(
4506
+ activeMountIds,
4507
+ publishedHead,
4508
+ publicationHeads,
4509
+ expectedMirrorHeads,
4510
+ inboundMoveMountIds,
4511
+ checkoutInventory.projects
4512
+ );
4477
4513
  },
4478
4514
  hydrationHooks: workspaceHydrationTransactionHooks
4479
4515
  });
@@ -4492,7 +4528,11 @@ async function startWorker(options) {
4492
4528
  result.localHead ?? result.remoteHead
4493
4529
  );
4494
4530
  }
4495
- return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
4531
+ const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result, missingCheckouts);
4532
+ if (!outerRemediation) {
4533
+ workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
4534
+ }
4535
+ return mapped;
4496
4536
  };
4497
4537
  const sendWorkspaceSyncResult = (requestId, result) => {
4498
4538
  try {
@@ -4530,17 +4570,143 @@ async function startWorker(options) {
4530
4570
  }, unsafeWorkspaceSyncRecoveryBounceDelayMs(attempt));
4531
4571
  timer.unref();
4532
4572
  };
4573
+ const captureWorkspaceSyncAdmission = () => ({
4574
+ generation: workerAdmissionGeneration,
4575
+ activeIncidentId: activeWorkspaceIncidentId,
4576
+ deferredIncidentId: deferredWorkspaceConfiguration?.incidentId ?? null
4577
+ });
4578
+ const workspaceSyncAdmissionSnapshotsMatch = (left, right) => left.generation === right.generation && left.activeIncidentId === right.activeIncidentId && left.deferredIncidentId === right.deferredIncidentId;
4579
+ const assertWorkspaceSyncStillAdmitted = (admitted, trigger) => {
4580
+ if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, trigger)) {
4581
+ throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
4582
+ }
4583
+ };
4584
+ const executeWorkspaceSyncCycle = async (input) => {
4585
+ const attemptId = input.attemptId;
4586
+ lastWorkspaceSyncMirrorObservationMs = 0;
4587
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4588
+ return await failedWorkspaceSyncResult(
4589
+ attemptId,
4590
+ input.trigger,
4591
+ new Error(
4592
+ `Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
4593
+ )
4594
+ );
4595
+ }
4596
+ try {
4597
+ input.assertStillAdmitted();
4598
+ const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
4599
+ return await performWorkspaceSync({
4600
+ attemptId,
4601
+ trigger: input.trigger,
4602
+ confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
4603
+ confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
4604
+ resetToCanonical: input.resetToCanonical,
4605
+ requiredAncestorHeads: input.requiredAncestorHeads,
4606
+ assertStillAdmitted: input.assertStillAdmitted
4607
+ });
4608
+ } catch (error) {
4609
+ workspaceProjectionLedger.reset();
4610
+ const errorMessage = error instanceof Error ? error.message : String(error);
4611
+ const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
4612
+ error,
4613
+ resetToCanonical: input.resetToCanonical === true,
4614
+ inspectRecoverableHydration: () => (0, import_workspace_git_sync.workspaceGitHydrationIsRecoverable)(workspaceShadowRoot, buildWorkspaceMounts())
4615
+ });
4616
+ if (hydrationRecoverable) {
4617
+ if (error instanceof WorkspaceSyncAdmissionChangedError) {
4618
+ process.stderr.write(
4619
+ `[r5d-worker] workspace synchronization was cancelled with every mount at a recorded basis; the next cycle converges it: ${errorMessage}
4620
+ `
4621
+ );
4622
+ }
4623
+ return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
4624
+ }
4625
+ const fenced = fenceUnsafeWorkspaceSyncFailure({
4626
+ hydrationRecoverable,
4627
+ resetToCanonical: input.resetToCanonical === true,
4628
+ recoveryAttempts: unsafeWorkspaceSyncRecoveryAttempts,
4629
+ maxRecoveryAttempts: WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
4630
+ hasRetainableWork: activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0,
4631
+ bouncePending: unsafeWorkspaceSyncBouncePending,
4632
+ invalidateExecution: () => {
4633
+ workspaceConfigured = false;
4634
+ advanceWorkerAdmissionGeneration();
4635
+ },
4636
+ scheduleRecoveryBounce: () => {
4637
+ unsafeWorkspaceSyncRecoveryAttempts += 1;
4638
+ scheduleUnsafeWorkspaceSyncRecoveryBounce(errorMessage);
4639
+ },
4640
+ fatalExit: (exitCode) => {
4641
+ process.stderr.write(
4642
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4643
+ `
4644
+ );
4645
+ process.exit(exitCode);
4646
+ },
4647
+ exitCode: 1
4648
+ });
4649
+ if (fenced === "recovery_pending") {
4650
+ process.stderr.write(
4651
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis while in-process recovery is already pending: ${errorMessage}
4652
+ `
4653
+ );
4654
+ }
4655
+ return {
4656
+ ...await failedWorkspaceSyncResult(attemptId, input.trigger, error),
4657
+ failureReason: "unsafe_hydration",
4658
+ recoveryAttempt: unsafeWorkspaceSyncRecoveryAttempts
4659
+ };
4660
+ }
4661
+ };
4662
+ const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set(["remediation", "remediation_confirm", "remediation_reset"]);
4663
+ const isOrdinaryWorkspaceSyncRequest = (input) => input.confirmedLargeDiff !== true && input.resetToCanonical !== true && !(input.requiredAncestorHeads && input.requiredAncestorHeads.length > 0) && !REMEDIATION_TRIGGER_TYPES.has(input.trigger.type);
4664
+ const LOCAL_ACTIVITY_TRIGGER_TYPES = /* @__PURE__ */ new Set([
4665
+ "write",
4666
+ "edit",
4667
+ "shell_inline",
4668
+ "process_terminal",
4669
+ "process_cancel"
4670
+ ]);
4671
+ const answerOrdinaryWorkspaceSyncFromLastCycle = (requests) => {
4672
+ if (!workspaceConfigured) return null;
4673
+ return workspaceProjectionLedger.answerFromLastCycle({
4674
+ // A workspace-rooted command carries no project target because it may
4675
+ // have touched any checkout.
4676
+ targetMountIds: requests.map(
4677
+ ({ trigger }) => LOCAL_ACTIVITY_TRIGGER_TYPES.has(trigger.type) && trigger.projectId && trigger.branchName ? projectMountId(trigger.projectId, trigger.branchName) : null
4678
+ ),
4679
+ mounts: buildWorkspaceMounts(),
4680
+ pendingDeletions: pendingMirrorDeletes.size > 0 || projectWorkspaceState.pendingTreeDeletions.length > 0 || projectWorkspaceState.pendingMirrorRefDeletions.length > 0
4681
+ });
4682
+ };
4683
+ const ordinaryWorkspaceSyncCoalescer = new import_workspace_sync_coalescer.WorkspaceSyncCoalescer({
4684
+ runExclusive: (operation) => workspaceSyncSingleFlight.runExclusive(operation),
4685
+ runBatch: async (requests) => {
4686
+ const startedAtMs = Date.now();
4687
+ const admission = captureWorkspaceSyncAdmission();
4688
+ const staleAttemptIds = /* @__PURE__ */ new Set();
4689
+ const admitted = [];
4690
+ for (const request of requests) {
4691
+ if (workspaceSyncAdmissionSnapshotsMatch(request.admission, admission)) admitted.push(request);
4692
+ else staleAttemptIds.add(request.attemptId);
4693
+ }
4694
+ const leader = admitted[0];
4695
+ if (!leader) return { result: null, ranCycle: false, startedAtMs, finishedAtMs: Date.now(), prepareMs: 0, staleAttemptIds };
4696
+ const answered = answerOrdinaryWorkspaceSyncFromLastCycle(admitted);
4697
+ if (answered) return { result: answered, ranCycle: false, startedAtMs, finishedAtMs: Date.now(), prepareMs: 0, staleAttemptIds };
4698
+ const result = await executeWorkspaceSyncCycle({
4699
+ attemptId: leader.attemptId,
4700
+ trigger: leader.trigger,
4701
+ assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, leader.trigger)
4702
+ });
4703
+ return { result, ranCycle: true, startedAtMs, finishedAtMs: Date.now(), prepareMs: lastWorkspaceSyncMirrorObservationMs, staleAttemptIds };
4704
+ }
4705
+ });
4533
4706
  const runWorkspaceSync = async (input) => {
4534
4707
  const attemptId = input.attemptId ?? crypto.randomUUID();
4535
4708
  const requestedAt = Date.now();
4536
- const admittedGeneration = workerAdmissionGeneration;
4537
- const admittedActiveIncidentId = activeWorkspaceIncidentId;
4538
- const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
4539
- const assertStillAdmitted = () => {
4540
- if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4541
- throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
4542
- }
4543
- };
4709
+ const admission = captureWorkspaceSyncAdmission();
4544
4710
  if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4545
4711
  const result2 = {
4546
4712
  ...await failedWorkspaceSyncResult(
@@ -4566,104 +4732,47 @@ async function startWorker(options) {
4566
4732
  }
4567
4733
  ws.send(JSON.stringify({ type: "workspace_sync_started", attemptId }));
4568
4734
  }
4569
- let queueEnteredAt = requestedAt;
4570
- let syncStartedAt = requestedAt;
4735
+ let queueMs = 0;
4736
+ let prepareMs = 0;
4737
+ let synchronizeMs = 0;
4571
4738
  workspaceSyncRequestsInFlight += 1;
4572
4739
  let result;
4573
4740
  try {
4574
- result = await workspaceSyncSingleFlight.runExclusive(async () => {
4575
- queueEnteredAt = Date.now();
4576
- syncStartedAt = Date.now();
4577
- lastWorkspaceSyncMirrorObservationMs = 0;
4578
- if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4579
- return await failedWorkspaceSyncResult(
4580
- attemptId,
4581
- input.trigger,
4582
- new Error(
4583
- `Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
4584
- )
4585
- );
4586
- }
4587
- try {
4588
- assertStillAdmitted();
4589
- const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
4590
- return await performWorkspaceSync({
4741
+ if (isOrdinaryWorkspaceSyncRequest(input)) {
4742
+ const settlement = await ordinaryWorkspaceSyncCoalescer.request({ attemptId, trigger: input.trigger, admission });
4743
+ queueMs = settlement.startedAtMs - requestedAt;
4744
+ prepareMs = settlement.prepareMs;
4745
+ synchronizeMs = settlement.finishedAtMs - settlement.startedAtMs;
4746
+ result = settlement.staleAttemptIds.has(attemptId) || settlement.result === null ? await failedWorkspaceSyncResult(
4747
+ attemptId,
4748
+ input.trigger,
4749
+ new Error("Workspace synchronization admission changed while the request was queued")
4750
+ ) : { ...settlement.result, attemptId, trigger: input.trigger };
4751
+ } else {
4752
+ result = await workspaceSyncSingleFlight.runExclusive(async () => {
4753
+ const startedAt = Date.now();
4754
+ queueMs = startedAt - requestedAt;
4755
+ const cycleResult = await executeWorkspaceSyncCycle({
4591
4756
  attemptId,
4592
4757
  trigger: input.trigger,
4593
- confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
4594
- confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
4758
+ confirmedLargeDiff: input.confirmedLargeDiff,
4759
+ confirmationReason: input.confirmationReason,
4595
4760
  resetToCanonical: input.resetToCanonical,
4596
4761
  requiredAncestorHeads: input.requiredAncestorHeads,
4597
- assertStillAdmitted
4762
+ assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, input.trigger)
4598
4763
  });
4599
- } catch (error) {
4600
- const errorMessage = error instanceof Error ? error.message : String(error);
4601
- const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
4602
- error,
4603
- resetToCanonical: input.resetToCanonical === true,
4604
- inspectRecoverableHydration: () => (0, import_workspace_git_sync.workspaceGitHydrationIsRecoverable)(workspaceShadowRoot, buildWorkspaceMounts())
4605
- });
4606
- if (hydrationRecoverable) {
4607
- if (error instanceof WorkspaceSyncAdmissionChangedError) {
4608
- process.stderr.write(
4609
- `[r5d-worker] workspace synchronization was cancelled with every mount at a recorded basis; the next cycle converges it: ${errorMessage}
4610
- `
4611
- );
4612
- }
4613
- return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
4614
- }
4615
- const fenced = fenceUnsafeWorkspaceSyncFailure({
4616
- hydrationRecoverable,
4617
- resetToCanonical: input.resetToCanonical === true,
4618
- recoveryAttempts: unsafeWorkspaceSyncRecoveryAttempts,
4619
- maxRecoveryAttempts: WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
4620
- hasRetainableWork: activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0,
4621
- bouncePending: unsafeWorkspaceSyncBouncePending,
4622
- invalidateExecution: () => {
4623
- workspaceConfigured = false;
4624
- advanceWorkerAdmissionGeneration();
4625
- },
4626
- scheduleRecoveryBounce: () => {
4627
- unsafeWorkspaceSyncRecoveryAttempts += 1;
4628
- scheduleUnsafeWorkspaceSyncRecoveryBounce(errorMessage);
4629
- },
4630
- fatalExit: (exitCode) => {
4631
- process.stderr.write(
4632
- `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4633
- `
4634
- );
4635
- process.exit(exitCode);
4636
- },
4637
- exitCode: 1
4638
- });
4639
- if (fenced === "recovery_pending") {
4640
- process.stderr.write(
4641
- `[r5d-worker] workspace synchronization failed without a recoverable hydration basis while in-process recovery is already pending: ${errorMessage}
4642
- `
4643
- );
4644
- }
4645
- return {
4646
- ...await failedWorkspaceSyncResult(attemptId, input.trigger, error),
4647
- failureReason: "unsafe_hydration",
4648
- recoveryAttempt: unsafeWorkspaceSyncRecoveryAttempts
4649
- };
4650
- }
4651
- });
4764
+ prepareMs = lastWorkspaceSyncMirrorObservationMs;
4765
+ synchronizeMs = Date.now() - startedAt;
4766
+ return cycleResult;
4767
+ });
4768
+ }
4652
4769
  } finally {
4653
4770
  workspaceSyncRequestsInFlight -= 1;
4654
4771
  }
4655
4772
  if (result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
4656
- const finishedAt = Date.now();
4657
4773
  result = {
4658
4774
  ...result,
4659
- telemetry: {
4660
- totalMs: finishedAt - requestedAt,
4661
- queueMs: queueEnteredAt - requestedAt,
4662
- // Duration of the pre-outer mirror-head observation phase; near zero
4663
- // on a cycle whose token-gated observation skipped every fetch.
4664
- prepareMs: lastWorkspaceSyncMirrorObservationMs,
4665
- synchronizeMs: finishedAt - syncStartedAt
4666
- }
4775
+ telemetry: { totalMs: Date.now() - requestedAt, queueMs, prepareMs, synchronizeMs }
4667
4776
  };
4668
4777
  if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result);
4669
4778
  sendLifecycleProgress(
@@ -4933,6 +5042,7 @@ async function startWorker(options) {
4933
5042
  );
4934
5043
  }
4935
5044
  workspaceConfigured = false;
5045
+ workspaceProjectionLedger.reset();
4936
5046
  const preserveOnlyBranches = message.projects.flatMap(
4937
5047
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4938
5048
  );
@@ -4942,6 +5052,16 @@ async function startWorker(options) {
4942
5052
  preserveOnlyBranches
4943
5053
  });
4944
5054
  }
5055
+ for (const project of message.projects) {
5056
+ for (const branch of project.branches) {
5057
+ const pending2 = projectWorkspaceState.locallyPendingCreatedBranches.find(
5058
+ (candidate) => candidate.projectId === project.projectId && candidate.branchName === branch.branchName
5059
+ );
5060
+ if (pending2 && pending2.branchId === branch.branchId) {
5061
+ projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
5062
+ }
5063
+ }
5064
+ }
4945
5065
  const stillPendingCreatedBranches = new Map(
4946
5066
  projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4947
5067
  (0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(branch.projectId, branch.branchName),
@@ -5383,11 +5503,10 @@ async function startWorker(options) {
5383
5503
  workspaceConfigResetToCanonicalV1: true,
5384
5504
  projectBranchDeletionFastAckV1: true,
5385
5505
  projectMirrorLeaseV1: true,
5386
- projectMirrorRefsTokensV1: true
5506
+ projectMirrorRefsTokensV1: true,
5507
+ projectBranchWorkingTreeModeV1: true
5387
5508
  },
5388
- projectRoot: projectsRoot,
5389
- artifactRoot,
5390
- planRoot
5509
+ root: rootDir
5391
5510
  }
5392
5511
  };
5393
5512
  ws.send(JSON.stringify(hello));
@@ -5422,14 +5541,12 @@ async function startWorker(options) {
5422
5541
  return;
5423
5542
  }
5424
5543
  if (message.type === "project_mirror_refs_tokens") {
5544
+ const advertised = (0, import_project_mirror_fetch_policy.parseProjectMirrorRefsTokensMessage)(message);
5425
5545
  advertisedProjectMirrorRefsTokens.clear();
5426
- if (message.tokens && typeof message.tokens === "object") {
5427
- for (const [projectId, token2] of Object.entries(message.tokens)) {
5428
- if (typeof token2 === "string" && /^[0-9a-f]{64}$/.test(token2)) {
5429
- advertisedProjectMirrorRefsTokens.set(projectId, token2);
5430
- }
5431
- }
5432
- }
5546
+ for (const [projectId, token2] of advertised.projectTokens) advertisedProjectMirrorRefsTokens.set(projectId, token2);
5547
+ advertisedProjectMirrorMountTokens.clear();
5548
+ for (const [projectId, tokens] of advertised.mountTokens) advertisedProjectMirrorMountTokens.set(projectId, tokens);
5549
+ advertisedWorkspaceHead = advertised.workspaceHead;
5433
5550
  return;
5434
5551
  }
5435
5552
  if (message.type === "workspace_config") {
@@ -5502,13 +5619,21 @@ async function startWorker(options) {
5502
5619
  });
5503
5620
  return;
5504
5621
  }
5622
+ sendWorkerMessage(ws, {
5623
+ type: "operation_queued",
5624
+ requestId: message.requestId,
5625
+ queuePosition: workspaceSyncSingleFlight.branchMutationQueuePosition()
5626
+ });
5505
5627
  try {
5628
+ if (message.workingTree !== "carry" && message.workingTree !== "clean") {
5629
+ throw new Error('Project branch creation requires workingTree "carry" or "clean"');
5630
+ }
5506
5631
  const pendingBranch = {
5507
5632
  branchId: message.branchId,
5508
5633
  projectId: message.projectId,
5509
5634
  branchName: message.targetBranch
5510
5635
  };
5511
- const created = await workspaceSyncSingleFlight.runMutation(() => {
5636
+ const created = await workspaceSyncSingleFlight.runBranchMutation(() => {
5512
5637
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5513
5638
  throw new Error(
5514
5639
  `Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
@@ -5549,6 +5674,7 @@ async function startWorker(options) {
5549
5674
  primaryBranchName: primaryProjectBranch(project),
5550
5675
  sourceBranchName: message.sourceBranch,
5551
5676
  branchName: message.targetBranch,
5677
+ workingTree: message.workingTree,
5552
5678
  pendingRetry: pendingWasRecorded
5553
5679
  });
5554
5680
  gitBranchCreated = true;
@@ -5605,9 +5731,14 @@ async function startWorker(options) {
5605
5731
  });
5606
5732
  return;
5607
5733
  }
5734
+ sendWorkerMessage(ws, {
5735
+ type: "operation_queued",
5736
+ requestId: message.requestId,
5737
+ queuePosition: workspaceSyncSingleFlight.branchMutationQueuePosition()
5738
+ });
5608
5739
  try {
5609
5740
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
5610
- const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
5741
+ const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(() => {
5611
5742
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5612
5743
  throw new Error(
5613
5744
  `Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
@@ -5638,6 +5769,9 @@ async function startWorker(options) {
5638
5769
  projectId: message.projectId,
5639
5770
  branchName: message.branchName
5640
5771
  });
5772
+ if (pendingLocalBranch) {
5773
+ projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
5774
+ }
5641
5775
  pendingCreatedBranchPublicationNotBefore.delete((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(message.projectId, message.branchName));
5642
5776
  if (pendingMirrorDeletes.has(deletionKey)) return true;
5643
5777
  stageProjectBranchDeletion(project, message.branchName, true);
@@ -5915,7 +6049,7 @@ async function startWorker(options) {
5915
6049
  ws,
5916
6050
  message,
5917
6051
  resolvedTarget,
5918
- planRoot,
6052
+ rootDir,
5919
6053
  assertAdmission: assertMessageAdmission,
5920
6054
  ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
5921
6055
  ...targetMayMutateVisibleWorkspace(message.target) ? {
@@ -6013,8 +6147,7 @@ async function startWorker(options) {
6013
6147
  resolvedTarget,
6014
6148
  baseUrl,
6015
6149
  token,
6016
- artifactRoot,
6017
- planRoot,
6150
+ rootDir,
6018
6151
  setPreparationHeartbeatLease: (active) => {
6019
6152
  sendWorkerMessageFromCurrentSource(ws, {
6020
6153
  type: "heartbeat_lease",
@@ -6083,8 +6216,7 @@ async function startWorker(options) {
6083
6216
  resolvedTarget,
6084
6217
  baseUrl,
6085
6218
  token,
6086
- artifactRoot,
6087
- planRoot,
6219
+ rootDir,
6088
6220
  setPreparationHeartbeatLease: (active) => {
6089
6221
  sendWorkerMessageFromCurrentSource(ws, {
6090
6222
  type: "heartbeat_lease",
@@ -6166,8 +6298,7 @@ async function startWorker(options) {
6166
6298
  resolvedTarget,
6167
6299
  baseUrl,
6168
6300
  token,
6169
- artifactRoot,
6170
- planRoot,
6301
+ rootDir,
6171
6302
  assertAdmission: assertMessageAdmission
6172
6303
  });
6173
6304
  });