@ricsam/r5d-worker 0.0.80 → 0.0.82

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.
@@ -46,6 +46,7 @@ module.exports = __toCommonJS(workspace_git_sync_exports);
46
46
  var import_node_fs = __toESM(require("node:fs"), 1);
47
47
  var import_node_path = __toESM(require("node:path"), 1);
48
48
  var import_git_process_environment = require("./git-process-environment.cjs");
49
+ var import_workspace_merge_projection = require("./workspace-merge-projection.cjs");
49
50
  var import_working_tree_mirror = require("./working-tree-mirror.cjs");
50
51
  var import_workspace_mount_boundary = require("./workspace-mount-boundary.cjs");
51
52
  const WORKSPACE_GIT_BRANCH = "main";
@@ -140,22 +141,52 @@ function compareWorkspaceHydrationMountBasis(left, right) {
140
141
  return left.id.localeCompare(right.id);
141
142
  }
142
143
  function workspaceHydrationReceipt(head, mounts) {
143
- const bases = mounts.map(workspaceHydrationMountBasis).sort(compareWorkspaceHydrationMountBasis);
144
+ const bases = mounts.map((mount) => ({ ...workspaceHydrationMountBasis(mount), head })).sort(compareWorkspaceHydrationMountBasis);
144
145
  const ids = /* @__PURE__ */ new Set();
145
146
  for (const basis of bases) {
146
147
  if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
147
148
  ids.add(basis.id);
148
149
  }
149
- return { version: 2, head, mounts: bases };
150
+ return { version: 3, mounts: bases };
151
+ }
152
+ function receiptWithMountsAtHead(previous, head, mounts, allConfiguredMounts) {
153
+ const advanceIds = new Set(mounts.map(({ id }) => id));
154
+ const previousById = new Map(previous?.mounts.map((entry) => [entry.id, entry]) ?? []);
155
+ const entries = allConfiguredMounts.flatMap((mount) => {
156
+ const basis = workspaceHydrationMountBasis(mount);
157
+ if (advanceIds.has(mount.id)) return [{ ...basis, head }];
158
+ const carried = previousById.get(mount.id);
159
+ if (!carried) return [];
160
+ return [carried];
161
+ });
162
+ entries.sort(compareWorkspaceHydrationMountBasis);
163
+ const ids = /* @__PURE__ */ new Set();
164
+ for (const entry of entries) {
165
+ if (ids.has(entry.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${entry.id}`);
166
+ ids.add(entry.id);
167
+ }
168
+ return { version: 3, mounts: entries };
150
169
  }
151
170
  function workspaceHydrationReceiptsEqual(left, right) {
152
- return JSON.stringify(left) === JSON.stringify(right);
171
+ if (JSON.stringify(left) !== JSON.stringify(right)) return false;
172
+ const effectiveLegacyHead = (receipt) => receipt?.mounts.length === 0 ? receipt.legacyHead ?? null : null;
173
+ return effectiveLegacyHead(left) === effectiveLegacyHead(right);
153
174
  }
154
175
  function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
155
- if (!receipt || receipt.head !== head) return false;
176
+ if (!receipt) return false;
156
177
  const expected = workspaceHydrationMountBasis(mount);
157
178
  const actual = receipt.mounts.find(({ id }) => id === expected.id);
158
- return Boolean(actual && JSON.stringify(actual) === JSON.stringify(expected));
179
+ if (!actual || actual.head !== head) return false;
180
+ const { head: _head, ...actualBasis } = actual;
181
+ return JSON.stringify(actualBasis) === JSON.stringify(expected);
182
+ }
183
+ function workspaceHydrationReceiptMountBasisHead(receipt, mount) {
184
+ if (!receipt) return null;
185
+ const expected = workspaceHydrationMountBasis(mount);
186
+ const actual = receipt.mounts.find(({ id }) => id === expected.id);
187
+ if (!actual) return null;
188
+ const { head, ...actualBasis } = actual;
189
+ return JSON.stringify(actualBasis) === JSON.stringify(expected) ? head : null;
159
190
  }
160
191
  function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
161
192
  return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
@@ -176,7 +207,9 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
176
207
  if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
177
208
  throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
178
209
  }
179
- return { version: 2, head: legacyHead, mounts: [] };
210
+ const receipt2 = { version: 3, mounts: [] };
211
+ Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
212
+ return receipt2;
180
213
  }
181
214
  let parsed;
182
215
  try {
@@ -188,7 +221,8 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
188
221
  throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
189
222
  }
190
223
  const candidate = parsed;
191
- if (candidate.version !== 2 || typeof candidate.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head)) {
224
+ const liftedV2Head = candidate.version === 2 && typeof candidate.head === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head) ? candidate.head : null;
225
+ if (candidate.version !== 3 && liftedV2Head === null) {
192
226
  throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
193
227
  }
194
228
  if (!Array.isArray(candidate.mounts)) {
@@ -202,23 +236,51 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
202
236
  const mount = rawMount;
203
237
  const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
204
238
  const durabilityRelative = typeof mount.sourcePath === "string" && typeof mount.durabilityRootPath === "string" ? import_node_path.default.relative(mount.durabilityRootPath, mount.sourcePath) : "..";
205
- if (typeof mount.id !== "string" || !mount.id || seenIds.has(mount.id) || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || import_node_path.default.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || import_node_path.default.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(durabilityRelative) || typeof mount.workspaceRelativePath !== "string" || mount.workspaceRelativePath !== normalizedRelativePath || !normalizedRelativePath || normalizedRelativePath === "." || normalizedRelativePath.split("/").some((segment) => segment === ".." || segment === ".git") || mount.sourceMode !== "all" && mount.sourceMode !== "git" || mount.hydrateDeletionMode !== "all" && mount.hydrateDeletionMode !== "git" || typeof mount.preserveLocalOnInitialOuterAbsence !== "boolean" || typeof mount.preserveLocalOnHydrationBasisChange !== "boolean" || typeof mount.deleteWhenSourceMissing !== "boolean") {
239
+ if (typeof mount.id !== "string" || !mount.id || seenIds.has(mount.id) || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || import_node_path.default.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || import_node_path.default.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(durabilityRelative) || typeof mount.workspaceRelativePath !== "string" || mount.workspaceRelativePath !== normalizedRelativePath || !normalizedRelativePath || normalizedRelativePath === "." || normalizedRelativePath.split("/").some((segment) => segment === ".." || segment === ".git") || mount.sourceMode !== "all" && mount.sourceMode !== "git" || mount.hydrateDeletionMode !== "all" && mount.hydrateDeletionMode !== "git" || typeof mount.preserveLocalOnInitialOuterAbsence !== "boolean" || typeof mount.preserveLocalOnHydrationBasisChange !== "boolean" || typeof mount.deleteWhenSourceMissing !== "boolean" || candidate.version === 3 && (typeof mount.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head))) {
206
240
  throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
207
241
  }
208
242
  seenIds.add(mount.id);
209
- return mount;
243
+ return {
244
+ id: mount.id,
245
+ incarnationKey: mount.incarnationKey,
246
+ sourcePath: mount.sourcePath,
247
+ durabilityRootPath: mount.durabilityRootPath,
248
+ workspaceRelativePath: mount.workspaceRelativePath,
249
+ sourceMode: mount.sourceMode,
250
+ hydrateDeletionMode: mount.hydrateDeletionMode,
251
+ preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence,
252
+ preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange,
253
+ deleteWhenSourceMissing: mount.deleteWhenSourceMissing,
254
+ head: candidate.version === 3 ? mount.head : liftedV2Head
255
+ };
210
256
  });
257
+ const sortedMounts = mounts.sort(compareWorkspaceHydrationMountBasis);
258
+ if (liftedV2Head) {
259
+ const legacyReceipt = {
260
+ version: 2,
261
+ head: liftedV2Head,
262
+ mounts: sortedMounts.map(({ head: _head, ...basis }) => basis)
263
+ };
264
+ if (content !== `${JSON.stringify(legacyReceipt)}
265
+ `) {
266
+ throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
267
+ }
268
+ }
211
269
  const receipt = {
212
- version: 2,
213
- head: candidate.head,
214
- mounts: mounts.sort(compareWorkspaceHydrationMountBasis)
270
+ version: 3,
271
+ mounts: sortedMounts
215
272
  };
216
- if (content !== `${JSON.stringify(receipt)}
273
+ if (liftedV2Head) Object.defineProperty(receipt, "legacyHead", { value: liftedV2Head, enumerable: false });
274
+ if (candidate.version === 3 && content !== `${JSON.stringify(receipt)}
217
275
  `) {
218
276
  throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
219
277
  }
220
- if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
221
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
278
+ const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
279
+ if (liftedV2Head) receiptHeads.add(liftedV2Head);
280
+ for (const head of receiptHeads) {
281
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`])) {
282
+ throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
283
+ }
222
284
  }
223
285
  return receipt;
224
286
  }
@@ -232,22 +294,92 @@ function readHydratedWorkspaceReceipt(workspacePath) {
232
294
  const content = import_node_fs.default.readFileSync(receiptPath, "utf8");
233
295
  return parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
234
296
  }
297
+ function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
298
+ const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
299
+ const status = lstatIfExists(receiptPath);
300
+ if (!status?.isFile() || status.isSymbolicLink()) return false;
301
+ try {
302
+ return JSON.parse(import_node_fs.default.readFileSync(receiptPath, "utf8")).version === 3;
303
+ } catch {
304
+ return false;
305
+ }
306
+ }
235
307
  function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
236
308
  const resolvedWorkspacePath = import_node_path.default.resolve(workspacePath);
237
309
  if (!import_node_fs.default.existsSync(import_node_path.default.join(resolvedWorkspacePath, ".git"))) return true;
238
310
  if (mounts) validateMounts(resolvedWorkspacePath, mounts);
239
311
  const head = revParse(resolvedWorkspacePath, "HEAD");
240
312
  if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
241
- if (head === null) return true;
242
313
  const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
243
- return mounts ? workspaceHydrationReceiptMatchesMounts(receipt, head, configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts)) : receipt?.head === head;
314
+ if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
315
+ if (head === null) return true;
316
+ if (!receipt) return false;
317
+ return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
318
+ (mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean(mount.busy?.() && workspaceHydrationReceiptMountBasisHead(receipt, mount))
319
+ ) : Boolean(receipt && receipt.mounts.every((mount) => mount.head === head));
320
+ }
321
+ const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
322
+ const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
323
+ function workspaceBasisRefName(mountId) {
324
+ const sanitized = Buffer.from(mountId, "utf8").toString("base64url");
325
+ if (!sanitized) throw new Error(`Cannot derive a workspace basis ref for mount id: ${mountId}`);
326
+ return `${WORKSPACE_GIT_BASIS_REF_PREFIX}${sanitized}`;
327
+ }
328
+ function listWorkspaceRefs(workspacePath, prefix) {
329
+ return git(workspacePath, ["for-each-ref", "--format=%(refname)", prefix], `list ${prefix} refs`).split("\n").filter(Boolean).sort();
330
+ }
331
+ function desiredWorkspaceBasisRefs(receipt) {
332
+ const desired = /* @__PURE__ */ new Map();
333
+ for (const mount of receipt.mounts) {
334
+ const ref = workspaceBasisRefName(mount.id);
335
+ if (desired.has(ref)) throw new Error(`Workspace hydration mount ids collide at basis ref ${ref}`);
336
+ desired.set(ref, mount.head);
337
+ }
338
+ return desired;
339
+ }
340
+ function workspaceBasisRefsMatch(workspacePath, receipt) {
341
+ const desired = desiredWorkspaceBasisRefs(receipt);
342
+ const actualRefs = listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX);
343
+ if (actualRefs.length !== desired.size) return false;
344
+ return actualRefs.every((ref) => desired.get(ref) === revParse(workspacePath, ref));
345
+ }
346
+ function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
347
+ return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
348
+ }
349
+ function repairWorkspaceBasisRefs(workspacePath, receipt) {
350
+ const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
351
+ if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
352
+ }
353
+ function updateWorkspaceBasisRefs(workspacePath, receipt) {
354
+ const desired = desiredWorkspaceBasisRefs(receipt);
355
+ const transaction = ["start"];
356
+ for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
357
+ for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
358
+ if (!desired.has(ref)) transaction.push(`delete ${ref}`);
359
+ }
360
+ transaction.push("prepare", "commit", "");
361
+ const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
362
+ cwd: workspacePath,
363
+ stdin: Buffer.from(transaction.join("\n")),
364
+ stdout: "pipe",
365
+ stderr: "pipe",
366
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
367
+ });
368
+ if (updated.exitCode !== 0) {
369
+ throw new Error(`Synchronize workspace hydration basis refs: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
370
+ }
371
+ for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) {
372
+ git(workspacePath, ["update-ref", "-d", ref], "remove staged workspace hydration basis ref");
373
+ }
244
374
  }
245
375
  function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
246
- if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(receipt.head)) {
247
- throw new Error(`Cannot record invalid fully hydrated workspace object id: ${receipt.head}`);
248
- }
249
- if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
250
- throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${receipt.head}`);
376
+ for (const mount of receipt.mounts) {
377
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
378
+ throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
379
+ }
380
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${mount.head}^{commit}`])) {
381
+ throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${mount.head}`);
382
+ }
251
383
  }
252
384
  readHydratedWorkspaceReceipt(workspacePath);
253
385
  const gitDirectory = import_node_path.default.join(workspacePath, ".git");
@@ -267,6 +399,12 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
267
399
  throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
268
400
  }
269
401
  const temporaryPath = import_node_path.default.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
402
+ const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
403
+ desiredWorkspaceBasisRefs(receipt);
404
+ for (const mount of receipt.mounts) {
405
+ const stagedRef = `${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)}`;
406
+ git(workspacePath, ["update-ref", stagedRef, mount.head], "stage workspace hydration basis ref");
407
+ }
270
408
  let descriptor;
271
409
  try {
272
410
  descriptor = import_node_fs.default.openSync(temporaryPath, import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL, 384);
@@ -277,6 +415,7 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
277
415
  descriptor = void 0;
278
416
  import_node_fs.default.renameSync(temporaryPath, receiptPath);
279
417
  fsyncDirectory(receiptDirectory);
418
+ updateWorkspaceBasisRefs(workspacePath, receipt);
280
419
  } catch (error) {
281
420
  if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
282
421
  import_node_fs.default.rmSync(temporaryPath, { force: true });
@@ -404,6 +543,16 @@ function workspaceRebaseInProgress(workspacePath) {
404
543
  const gitDirectory = import_node_path.default.join(workspacePath, ".git");
405
544
  return import_node_fs.default.existsSync(import_node_path.default.join(gitDirectory, "rebase-merge")) || import_node_fs.default.existsSync(import_node_path.default.join(gitDirectory, "rebase-apply"));
406
545
  }
546
+ function workspaceResolutionInProgress(workspacePath) {
547
+ return workspaceRebaseInProgress(workspacePath) || import_node_fs.default.existsSync(import_node_path.default.join(workspacePath, ".git", "MERGE_HEAD"));
548
+ }
549
+ function workspaceHasUnmergedEntries(workspacePath) {
550
+ const result = gitResult(workspacePath, ["ls-files", "-u", "-z"]);
551
+ if (result.exitCode !== 0) {
552
+ throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
553
+ }
554
+ return result.stdout.length > 0;
555
+ }
407
556
  function clearWorkspaceCheckoutTree(workspacePath) {
408
557
  for (const name of import_node_fs.default.readdirSync(workspacePath)) {
409
558
  if (name === ".git") continue;
@@ -411,15 +560,18 @@ function clearWorkspaceCheckoutTree(workspacePath) {
411
560
  }
412
561
  fsyncDirectory(workspacePath);
413
562
  }
414
- function recoverWorkspaceCheckoutDurability(workspacePath) {
563
+ function recoverWorkspaceCheckoutDurability(workspacePath, preserveResolutionInProgress = false) {
415
564
  workspacePath = import_node_path.default.resolve(workspacePath);
416
565
  const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
417
566
  const observedHead = revParse(workspacePath, "HEAD");
418
567
  if (record?.state === "durable" && record.head === observedHead) return;
568
+ if (preserveResolutionInProgress && workspaceResolutionInProgress(workspacePath)) {
569
+ throw new Error("Workspace resolution is in progress; finish or abort it before synchronizing");
570
+ }
419
571
  let targetHead = record?.state === "transition" ? record.head : observedHead;
420
572
  if (record?.state === "transition" && observedHead && observedHead !== record.head) {
421
573
  const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
422
- if (hydrationReceipt?.head === observedHead) {
574
+ if (hydrationReceipt?.legacyHead === observedHead || hydrationReceipt?.mounts.some((mount) => mount.head === observedHead)) {
423
575
  targetHead = observedHead;
424
576
  }
425
577
  }
@@ -501,7 +653,7 @@ function parseHydrationTransactionManifest(transactionPath) {
501
653
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
502
654
  }
503
655
  const candidate = parsed;
504
- if (candidate.version !== 2 || !Array.isArray(candidate.mounts)) {
656
+ if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
505
657
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
506
658
  }
507
659
  const parseReceipt = (rawReceipt) => {
@@ -533,7 +685,7 @@ function parseHydrationTransactionManifest(transactionPath) {
533
685
  return mount;
534
686
  });
535
687
  return {
536
- version: 2,
688
+ version: 3,
537
689
  targetReceipt,
538
690
  receiptBefore,
539
691
  mounts
@@ -626,7 +778,7 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
626
778
  const snapshotRoot = import_node_path.default.join(stagingPath, "mounts");
627
779
  import_node_fs.default.mkdirSync(snapshotRoot, { mode: 448 });
628
780
  const manifest = {
629
- version: 2,
781
+ version: 3,
630
782
  targetReceipt,
631
783
  receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
632
784
  mounts: mounts.map((mount, index) => {
@@ -805,7 +957,11 @@ function configureWorkspaceRepository(input) {
805
957
  if (!name || !email) throw new Error("Workspace Git identity must include name and email");
806
958
  git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
807
959
  git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
808
- git(input.workspacePath, ["config", "--local", "core.fsync", "committed"], "configure durable workspace commits");
960
+ git(
961
+ input.workspacePath,
962
+ ["config", "--local", "core.fsync", "committed,reference"],
963
+ "configure durable workspace commits and references"
964
+ );
809
965
  git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
810
966
  }
811
967
  function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
@@ -869,7 +1025,7 @@ function ensureWorkspaceGitClone(input) {
869
1025
  }
870
1026
  }
871
1027
  configureWorkspaceRepository({ ...input, workspacePath });
872
- recoverWorkspaceCheckoutDurability(workspacePath);
1028
+ recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
873
1029
  const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
874
1030
  const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
875
1031
  if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
@@ -1019,31 +1175,40 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1019
1175
  }
1020
1176
  function hydrateWorkspaceGitMountsTransactionally(input) {
1021
1177
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1022
- const mountIds = input.mounts.map(({ id }) => id).sort();
1023
- const requiredIds = (input.requiredMounts ?? input.mounts).map(({ id }) => id).sort();
1024
- if (mountIds.length !== requiredIds.length || mountIds.some((id, index) => id !== requiredIds[index])) {
1025
- return { hydratedMountIds: [], skippedMountIds: requiredIds };
1026
- }
1027
- const busyMountIds = input.ignoreBusy ? [] : (input.durabilityMounts ?? input.mounts).filter((mount) => mount.busy?.()).map(({ id }) => id).sort();
1028
- if (busyMountIds.length > 0) {
1029
- return { hydratedMountIds: [], skippedMountIds: busyMountIds };
1030
- }
1178
+ const durabilityMounts = input.durabilityMounts ?? input.mounts;
1179
+ const receiptMounts = input.receiptMounts ?? durabilityMounts;
1180
+ const requiredMounts = input.requiredMounts ?? input.mounts;
1181
+ const candidateMounts = [
1182
+ ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
1183
+ ];
1184
+ const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1185
+ const requestedIds = new Set(input.mounts.map(({ id }) => id));
1186
+ const requiredIds = new Set(requiredMounts.map(({ id }) => id));
1187
+ const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1188
+ const advancedDurabilityMounts = durabilityMounts.filter(
1189
+ ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1190
+ );
1191
+ const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1031
1192
  if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
1032
1193
  throw new Error("Workspace checkout is not durably materialized at its current HEAD");
1033
1194
  }
1034
1195
  const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
1035
- const targetReceipt = targetHead ? workspaceHydrationReceipt(targetHead, input.receiptMounts ?? input.durabilityMounts ?? input.mounts) : null;
1036
- const manifest = beginHydrationTransaction(workspacePath, input.mounts, targetReceipt);
1196
+ const targetReceipt = targetHead ? receiptWithMountsAtHead(readHydratedWorkspaceReceipt(workspacePath), targetHead, advancedDurabilityMounts, receiptMounts) : null;
1197
+ const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1037
1198
  try {
1038
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, input.mounts, { ignoreBusy: true });
1039
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredIds[index])) {
1199
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1200
+ const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1201
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1040
1202
  throw new Error("Workspace hydration did not include every required mount");
1041
1203
  }
1042
- fsyncHydratedWorkspaceMounts(input.durabilityMounts ?? input.mounts);
1204
+ fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
1043
1205
  markHydrationTransactionDurable(workspacePath);
1044
1206
  if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1045
1207
  removeHydrationTransaction(workspacePath);
1046
- return hydration;
1208
+ return {
1209
+ hydratedMountIds: hydration.hydratedMountIds,
1210
+ skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort()
1211
+ };
1047
1212
  } catch (error) {
1048
1213
  const transactionPath = hydrationTransactionPath(workspacePath);
1049
1214
  if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
@@ -1076,44 +1241,44 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1076
1241
  function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1077
1242
  workspacePath = import_node_path.default.resolve(workspacePath);
1078
1243
  validateMounts(workspacePath, mounts);
1079
- recoverWorkspaceCheckoutDurability(workspacePath);
1244
+ recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
1080
1245
  recoverPendingHydrationTransaction(workspacePath);
1081
1246
  const currentHead = revParse(workspacePath, "HEAD");
1082
- if (!currentHead) return;
1083
1247
  const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
1248
+ repairWorkspaceBasisRefs(workspacePath, currentReceipt);
1249
+ if (!currentHead) return;
1084
1250
  const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
1085
1251
  const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
1086
1252
  const uncoveredBasisMounts = configuredBasisMounts.filter(
1087
1253
  (mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
1088
1254
  );
1089
1255
  if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
1090
- const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1091
- if (busyMountIds.length > 0) {
1092
- throw new Error(
1093
- `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${busyMountIds.join(", ")}`
1094
- );
1095
- }
1096
- const hydrationTargets = uncoveredBasisMounts.filter(
1097
- (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1256
+ const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1257
+ const recoveryMounts = uncoveredBasisMounts.filter(
1258
+ (mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
1259
+ );
1260
+ const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
1261
+ const hydrationTargets = idleBasislessMounts.filter(
1262
+ (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1098
1263
  );
1099
1264
  const hydration = hydrateWorkspaceGitMountsTransactionally({
1100
1265
  workspacePath,
1101
1266
  mounts: hydrationTargets,
1102
- durabilityMounts: uncoveredBasisMounts,
1267
+ durabilityMounts: idleBasislessMounts,
1103
1268
  receiptMounts: configuredBasisMounts,
1104
1269
  requiredMounts: hydrationTargets,
1105
1270
  ignoreBusy: options.ignoreBusy,
1106
1271
  recordCurrentHead: true
1107
1272
  });
1108
- if (hydration.skippedMountIds.length > 0) {
1109
- throw new Error(
1110
- `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
1111
- );
1112
- }
1273
+ void hydration;
1113
1274
  }
1114
1275
  function resetWorkspaceGit(input) {
1115
1276
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1116
1277
  validateMounts(workspacePath, input.mounts);
1278
+ const busyResetMountIds = input.mounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1279
+ if (busyResetMountIds.length > 0) {
1280
+ throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
1281
+ }
1117
1282
  const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1118
1283
  recoverPendingHydrationTransaction(workspacePath);
1119
1284
  const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
@@ -1300,69 +1465,128 @@ async function synchronizeWorkspaceGit(input) {
1300
1465
  const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
1301
1466
  const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
1302
1467
  validateMounts(workspacePath, input.mounts);
1303
- const busyLiveMountIds = input.skipMountMirror ? [] : input.mounts.filter((mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()).map(({ id }) => id).sort();
1304
- if (busyLiveMountIds.length > 0) {
1305
- const cloneExists = import_node_fs.default.existsSync(import_node_path.default.join(workspacePath, ".git"));
1306
- const localHead = cloneExists ? revParse(workspacePath, "HEAD") : null;
1307
- const remoteHead2 = cloneExists ? revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`) : null;
1308
- return {
1309
- outcome: "no_change",
1310
- startingHead: localHead,
1311
- localHead,
1312
- remoteHead: remoteHead2,
1313
- publishedHead: remoteHead2,
1314
- rebaseCount: 0,
1315
- diffSizeBytes: 0,
1316
- affectedPaths: [],
1317
- activeMountIds: [],
1318
- skippedMountIds: input.mounts.map(({ id }) => id).sort()
1319
- };
1320
- }
1321
- const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1468
+ const preserveResolutionInProgress = input.skipMountMirror === true;
1469
+ const initial = ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
1322
1470
  const startingHead = initial.localHead;
1323
1471
  const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
1324
1472
  const deferredMountIds = /* @__PURE__ */ new Set();
1325
- if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead && receiptBeforeInitialHydration?.head === initial.localHead) {
1473
+ if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
1326
1474
  for (const mount of input.mounts) {
1327
- if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && !workspaceHydrationReceiptCoversMount(receiptBeforeInitialHydration, initial.localHead, mount)) {
1475
+ if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && workspaceHydrationReceiptMountBasisHead(receiptBeforeInitialHydration, mount) === null) {
1328
1476
  deferredMountIds.add(mount.id);
1329
1477
  }
1330
1478
  }
1331
1479
  }
1332
1480
  recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1333
- ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
1481
+ ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
1482
+ preserveResolutionInProgress,
1483
+ preserveStaleBases: true
1334
1484
  });
1335
- const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => import_node_fs.default.existsSync(sourcePath));
1336
1485
  const selected = activeMounts(input.mounts);
1337
- const projectedActiveMounts = selected.active.filter(({ id }) => !deferredMountIds.has(id));
1338
- const projectedTombstones = selected.tombstones.filter(({ id }) => !deferredMountIds.has(id));
1486
+ const projectionMutationTokens = new Map(
1487
+ input.mounts.flatMap((mount) => mount.mutationToken ? [[mount.id, mount.mutationToken()]] : [])
1488
+ );
1489
+ const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
1490
+ const receiptRequiredLiveMounts = selected.active;
1339
1491
  const projectionBasisHead = revParse(workspacePath, "HEAD");
1340
1492
  const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
1341
- const projectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts).filter(
1342
- (mount) => !deferredMountIds.has(mount.id) && Boolean(projectionBasisHead && workspaceHydrationReceiptCoversMount(projectionBasisReceipt, projectionBasisHead, mount))
1343
- );
1493
+ const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1494
+ const fastProjectionMounts = [];
1495
+ const mergedProjectionTrees = /* @__PURE__ */ new Map();
1496
+ const mergedProjectionMounts = [];
1497
+ const projectionSkippedIds = /* @__PURE__ */ new Set();
1498
+ let projectionWarning;
1499
+ const refreshCycleSkippedMounts = (mounts = input.mounts) => {
1500
+ for (const mount of mounts) {
1501
+ const projectionToken = projectionMutationTokens.get(mount.id);
1502
+ if (projectionToken !== void 0 && mount.mutationToken?.() !== projectionToken) cycleSkippedMountIds.add(mount.id);
1503
+ if (mount.busy?.()) cycleSkippedMountIds.add(mount.id);
1504
+ }
1505
+ };
1506
+ const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
1507
+ refreshCycleSkippedMounts();
1508
+ const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
1509
+ return {
1510
+ activeMountIds: [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort(),
1511
+ skippedMountIds: [...skippedIds].sort()
1512
+ };
1513
+ };
1514
+ if (!input.skipMountMirror) {
1515
+ const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
1516
+ const mergeSupport = mergeProjectionEnabled ? (0, import_workspace_merge_projection.workspaceMergeProjectionSupport)() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
1517
+ for (const mount of [...selected.active, ...selected.tombstones]) {
1518
+ if (deferredMountIds.has(mount.id)) continue;
1519
+ const basisHead = workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount);
1520
+ if (projectionBasisHead === null || basisHead === projectionBasisHead) {
1521
+ fastProjectionMounts.push(mount);
1522
+ continue;
1523
+ }
1524
+ if (basisHead === null) {
1525
+ deferredMountIds.add(mount.id);
1526
+ continue;
1527
+ }
1528
+ if (!mergeSupport.supported) {
1529
+ projectionSkippedIds.add(mount.id);
1530
+ projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
1531
+ continue;
1532
+ }
1533
+ const merged = (0, import_workspace_merge_projection.mergeWorkspaceProjectionMount)({
1534
+ workspacePath,
1535
+ mount: {
1536
+ id: mount.id,
1537
+ sourcePath: mount.sourcePath,
1538
+ workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
1539
+ sourceMode: mount.sourceMode
1540
+ },
1541
+ basisHead,
1542
+ currentHead: projectionBasisHead,
1543
+ attemptId
1544
+ });
1545
+ if (merged.kind === "conflict") {
1546
+ const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
1547
+ return {
1548
+ outcome: "conflict_blocked",
1549
+ startingHead,
1550
+ localHead: merged.oursCommit,
1551
+ remoteHead: initial.remoteHead,
1552
+ publishedHead: initial.remoteHead,
1553
+ rebaseCount: 0,
1554
+ diffSizeBytes: 0,
1555
+ affectedPaths: merged.conflictPaths,
1556
+ activeMountIds: [],
1557
+ skippedMountIds: input.mounts.map(({ id }) => id).sort(),
1558
+ conflictPaths: merged.conflictPaths,
1559
+ conflictSnapshotRefs: refs,
1560
+ conflictKind: "projection_merge",
1561
+ error: merged.error
1562
+ };
1563
+ }
1564
+ mergedProjectionTrees.set(mount.id, merged.resultTree);
1565
+ mergedProjectionMounts.push(mount);
1566
+ }
1567
+ }
1568
+ if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
1569
+ `);
1344
1570
  const completedMountSelection = (requireCurrentHeadReceipt = false) => {
1345
1571
  const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
1346
1572
  const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1347
- if (currentHeadBeforeHydration && workspaceHydrationReceiptMatchesMounts(
1348
- readHydratedWorkspaceReceipt(workspacePath),
1349
- currentHeadBeforeHydration,
1350
- completionReceiptMounts
1351
- )) {
1352
- const lateBusyIds = selected.active.filter((mount) => mount.busy?.()).map(({ id }) => id);
1353
- const lateBusy = new Set(lateBusyIds);
1354
- return {
1355
- activeMountIds: [...selected.active.filter(({ id }) => !lateBusy.has(id)), ...selected.tombstones].map(({ id }) => id).sort(),
1356
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...lateBusyIds])].sort()
1357
- };
1358
- }
1359
1573
  const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
1574
+ refreshCycleSkippedMounts(completionReceiptMounts);
1360
1575
  const mountIsAlreadyCovered = (mount) => Boolean(
1361
1576
  currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
1362
1577
  );
1363
- const uncoveredCompletionMounts = selected.active.filter((mount) => !mountIsAlreadyCovered(mount));
1364
- const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1365
- const uncoveredCompletionBasisMounts = completionReceiptMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1578
+ if (currentHeadBeforeHydration && completionReceiptMounts.every((mount) => cycleSkippedMountIds.has(mount.id) || mountIsAlreadyCovered(mount))) {
1579
+ return classifyMounts([...selected.active, ...selected.tombstones]);
1580
+ }
1581
+ const uncoveredCompletionMounts = selected.active.filter(
1582
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1583
+ );
1584
+ const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter(
1585
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1586
+ );
1587
+ const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
1588
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1589
+ );
1366
1590
  const hydration = hydrateWorkspaceGitMountsTransactionally({
1367
1591
  workspacePath,
1368
1592
  mounts: uncoveredCompletionMounts,
@@ -1371,22 +1595,32 @@ async function synchronizeWorkspaceGit(input) {
1371
1595
  requiredMounts: uncoveredRequiredLiveMounts,
1372
1596
  recordCurrentHead: true
1373
1597
  });
1598
+ for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
1599
+ refreshCycleSkippedMounts(completionReceiptMounts);
1374
1600
  if (requireCurrentHeadReceipt) {
1375
1601
  const currentHead = revParse(workspacePath, "HEAD");
1376
- if (currentHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), currentHead, completionReceiptMounts)) {
1602
+ const receipt = readHydratedWorkspaceReceipt(workspacePath);
1603
+ const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
1604
+ const idleMounts = completionReceiptMounts.filter(
1605
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mount.busy?.() && !projectionSkippedIds.has(mount.id)
1606
+ );
1607
+ if (currentHead && !idleMounts.every((mount) => workspaceHydrationReceiptCoversMount(receipt, currentHead, mount))) {
1377
1608
  throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
1378
1609
  }
1379
1610
  }
1380
- return {
1381
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1382
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1383
- };
1611
+ return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
1384
1612
  };
1385
1613
  let newlyStagedPaths = [];
1386
1614
  const stageAndCommitWorkspace = () => {
1615
+ if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
1616
+ throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
1617
+ }
1387
1618
  git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
1388
1619
  newlyStagedPaths = stagedPaths(workspacePath);
1389
1620
  if (newlyStagedPaths.length > 0) {
1621
+ if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
1622
+ throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
1623
+ }
1390
1624
  git(
1391
1625
  workspacePath,
1392
1626
  [
@@ -1401,11 +1635,22 @@ async function synchronizeWorkspaceGit(input) {
1401
1635
  if (!input.skipMountMirror) {
1402
1636
  beginWorkspaceCheckoutTransition(workspacePath);
1403
1637
  try {
1404
- mirrorMountsToWorkspace(workspacePath, projectedActiveMounts);
1405
- removeWorkspaceMounts(workspacePath, projectedTombstones);
1638
+ const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
1639
+ const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
1640
+ const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
1641
+ mirrorMountsToWorkspace(workspacePath, fastActiveMounts);
1642
+ removeWorkspaceMounts(workspacePath, fastTombstones);
1643
+ for (const mount of mergedProjectionMounts) {
1644
+ (0, import_workspace_merge_projection.materializeWorkspaceProjectionTree)({
1645
+ workspacePath,
1646
+ workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
1647
+ resultTree: mergedProjectionTrees.get(mount.id)
1648
+ });
1649
+ }
1650
+ const restoreFromHeadIds = /* @__PURE__ */ new Set([...deferredMountIds, ...selected.skipped.map(({ id }) => id)]);
1406
1651
  restoreDeferredWorkspaceMountsFromHead(
1407
1652
  workspacePath,
1408
- input.mounts.filter(({ id }) => deferredMountIds.has(id))
1653
+ input.mounts.filter(({ id }) => restoreFromHeadIds.has(id))
1409
1654
  );
1410
1655
  stageAndCommitWorkspace();
1411
1656
  fsyncWorkspaceCheckoutTree(workspacePath);
@@ -1419,9 +1664,15 @@ async function synchronizeWorkspaceGit(input) {
1419
1664
  if (!input.skipMountMirror) {
1420
1665
  const projectedHead = revParse(workspacePath, "HEAD");
1421
1666
  if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
1422
- const targetProjectionReceipt = workspaceHydrationReceipt(projectedHead, projectionReceiptMounts);
1423
- if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), targetProjectionReceipt)) {
1424
- fsyncHydratedWorkspaceMounts([...projectedActiveMounts, ...projectedTombstones]);
1667
+ const targetProjectionReceipt = receiptWithMountsAtHead(
1668
+ projectionBasisReceipt,
1669
+ projectedHead,
1670
+ fastProjectionMounts,
1671
+ allProjectionReceiptMounts
1672
+ );
1673
+ const currentProjectionReceipt = readHydratedWorkspaceReceipt(workspacePath);
1674
+ if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !workspaceBasisRefsAreCurrent(workspacePath, targetProjectionReceipt)) {
1675
+ fsyncHydratedWorkspaceMounts(fastProjectionMounts);
1425
1676
  updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
1426
1677
  }
1427
1678
  completeWorkspaceCheckoutTransition(workspacePath);
@@ -1433,6 +1684,7 @@ async function synchronizeWorkspaceGit(input) {
1433
1684
  const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
1434
1685
  if (reconciled.kind === "conflict") {
1435
1686
  const localHead2 = revParse(workspacePath, "HEAD");
1687
+ const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
1436
1688
  return {
1437
1689
  outcome: "conflict_blocked",
1438
1690
  startingHead,
@@ -1442,10 +1694,10 @@ async function synchronizeWorkspaceGit(input) {
1442
1694
  rebaseCount: rebaseCount + 1,
1443
1695
  diffSizeBytes: 0,
1444
1696
  affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
1445
- activeMountIds: [...projectedActiveMounts, ...projectedTombstones].map(({ id }) => id).sort(),
1446
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
1697
+ ...classifiedMounts,
1447
1698
  conflictPaths: reconciled.conflictPaths,
1448
1699
  conflictSnapshotRefs: reconciled.refs,
1700
+ conflictKind: "integration_rebase",
1449
1701
  error: reconciled.error
1450
1702
  };
1451
1703
  }
@@ -1454,6 +1706,7 @@ async function synchronizeWorkspaceGit(input) {
1454
1706
  if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
1455
1707
  const localHead = revParse(workspacePath, "HEAD");
1456
1708
  if (!localHead) {
1709
+ const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
1457
1710
  return {
1458
1711
  outcome: "no_change",
1459
1712
  startingHead,
@@ -1463,13 +1716,14 @@ async function synchronizeWorkspaceGit(input) {
1463
1716
  rebaseCount,
1464
1717
  diffSizeBytes: 0,
1465
1718
  affectedPaths: [],
1466
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1467
- skippedMountIds: selected.skipped.map(({ id }) => id).sort()
1719
+ ...classifiedMounts,
1720
+ ...projectionWarning ? { error: projectionWarning } : {}
1468
1721
  };
1469
1722
  }
1470
1723
  const paths = changedPaths(workspacePath, remoteHead, localHead);
1471
1724
  const size = paths.length > 0 ? await diffSizeBytes(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
1472
1725
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
1726
+ const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
1473
1727
  return {
1474
1728
  outcome: "large_diff_blocked",
1475
1729
  startingHead,
@@ -1479,8 +1733,7 @@ async function synchronizeWorkspaceGit(input) {
1479
1733
  rebaseCount,
1480
1734
  diffSizeBytes: size,
1481
1735
  affectedPaths: paths,
1482
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1483
- skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
1736
+ ...classifiedMounts,
1484
1737
  error: `Workspace diff exceeds the ${maxDiffBytes}-byte automatic publication limit`
1485
1738
  };
1486
1739
  }
@@ -1499,7 +1752,8 @@ async function synchronizeWorkspaceGit(input) {
1499
1752
  rebaseCount,
1500
1753
  diffSizeBytes: size,
1501
1754
  affectedPaths: paths,
1502
- ...completedMounts
1755
+ ...completedMounts,
1756
+ ...projectionWarning ? { error: projectionWarning } : {}
1503
1757
  };
1504
1758
  }
1505
1759
  const pushArgs = [
@@ -1526,7 +1780,8 @@ async function synchronizeWorkspaceGit(input) {
1526
1780
  rebaseCount,
1527
1781
  diffSizeBytes: size,
1528
1782
  affectedPaths: paths,
1529
- ...completedMounts
1783
+ ...completedMounts,
1784
+ ...projectionWarning ? { error: projectionWarning } : {}
1530
1785
  };
1531
1786
  }
1532
1787
  if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}