@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.
@@ -1,6 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
4
+ import {
5
+ materializeWorkspaceProjectionTree,
6
+ mergeWorkspaceProjectionMount,
7
+ workspaceMergeProjectionSupport
8
+ } from "./workspace-merge-projection.mjs";
4
9
  import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
5
10
  import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
6
11
  const WORKSPACE_GIT_BRANCH = "main";
@@ -95,22 +100,52 @@ function compareWorkspaceHydrationMountBasis(left, right) {
95
100
  return left.id.localeCompare(right.id);
96
101
  }
97
102
  function workspaceHydrationReceipt(head, mounts) {
98
- const bases = mounts.map(workspaceHydrationMountBasis).sort(compareWorkspaceHydrationMountBasis);
103
+ const bases = mounts.map((mount) => ({ ...workspaceHydrationMountBasis(mount), head })).sort(compareWorkspaceHydrationMountBasis);
99
104
  const ids = /* @__PURE__ */ new Set();
100
105
  for (const basis of bases) {
101
106
  if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
102
107
  ids.add(basis.id);
103
108
  }
104
- return { version: 2, head, mounts: bases };
109
+ return { version: 3, mounts: bases };
110
+ }
111
+ function receiptWithMountsAtHead(previous, head, mounts, allConfiguredMounts) {
112
+ const advanceIds = new Set(mounts.map(({ id }) => id));
113
+ const previousById = new Map(previous?.mounts.map((entry) => [entry.id, entry]) ?? []);
114
+ const entries = allConfiguredMounts.flatMap((mount) => {
115
+ const basis = workspaceHydrationMountBasis(mount);
116
+ if (advanceIds.has(mount.id)) return [{ ...basis, head }];
117
+ const carried = previousById.get(mount.id);
118
+ if (!carried) return [];
119
+ return [carried];
120
+ });
121
+ entries.sort(compareWorkspaceHydrationMountBasis);
122
+ const ids = /* @__PURE__ */ new Set();
123
+ for (const entry of entries) {
124
+ if (ids.has(entry.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${entry.id}`);
125
+ ids.add(entry.id);
126
+ }
127
+ return { version: 3, mounts: entries };
105
128
  }
106
129
  function workspaceHydrationReceiptsEqual(left, right) {
107
- return JSON.stringify(left) === JSON.stringify(right);
130
+ if (JSON.stringify(left) !== JSON.stringify(right)) return false;
131
+ const effectiveLegacyHead = (receipt) => receipt?.mounts.length === 0 ? receipt.legacyHead ?? null : null;
132
+ return effectiveLegacyHead(left) === effectiveLegacyHead(right);
108
133
  }
109
134
  function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
110
- if (!receipt || receipt.head !== head) return false;
135
+ if (!receipt) return false;
111
136
  const expected = workspaceHydrationMountBasis(mount);
112
137
  const actual = receipt.mounts.find(({ id }) => id === expected.id);
113
- return Boolean(actual && JSON.stringify(actual) === JSON.stringify(expected));
138
+ if (!actual || actual.head !== head) return false;
139
+ const { head: _head, ...actualBasis } = actual;
140
+ return JSON.stringify(actualBasis) === JSON.stringify(expected);
141
+ }
142
+ function workspaceHydrationReceiptMountBasisHead(receipt, mount) {
143
+ if (!receipt) return null;
144
+ const expected = workspaceHydrationMountBasis(mount);
145
+ const actual = receipt.mounts.find(({ id }) => id === expected.id);
146
+ if (!actual) return null;
147
+ const { head, ...actualBasis } = actual;
148
+ return JSON.stringify(actualBasis) === JSON.stringify(expected) ? head : null;
114
149
  }
115
150
  function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
116
151
  return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
@@ -131,7 +166,9 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
131
166
  if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
132
167
  throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
133
168
  }
134
- return { version: 2, head: legacyHead, mounts: [] };
169
+ const receipt2 = { version: 3, mounts: [] };
170
+ Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
171
+ return receipt2;
135
172
  }
136
173
  let parsed;
137
174
  try {
@@ -143,7 +180,8 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
143
180
  throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
144
181
  }
145
182
  const candidate = parsed;
146
- if (candidate.version !== 2 || typeof candidate.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head)) {
183
+ const liftedV2Head = candidate.version === 2 && typeof candidate.head === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head) ? candidate.head : null;
184
+ if (candidate.version !== 3 && liftedV2Head === null) {
147
185
  throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
148
186
  }
149
187
  if (!Array.isArray(candidate.mounts)) {
@@ -157,23 +195,51 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
157
195
  const mount = rawMount;
158
196
  const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
159
197
  const durabilityRelative = typeof mount.sourcePath === "string" && typeof mount.durabilityRootPath === "string" ? path.relative(mount.durabilityRootPath, mount.sourcePath) : "..";
160
- 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" || path.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || path.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.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") {
198
+ 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" || path.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || path.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.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))) {
161
199
  throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
162
200
  }
163
201
  seenIds.add(mount.id);
164
- return mount;
202
+ return {
203
+ id: mount.id,
204
+ incarnationKey: mount.incarnationKey,
205
+ sourcePath: mount.sourcePath,
206
+ durabilityRootPath: mount.durabilityRootPath,
207
+ workspaceRelativePath: mount.workspaceRelativePath,
208
+ sourceMode: mount.sourceMode,
209
+ hydrateDeletionMode: mount.hydrateDeletionMode,
210
+ preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence,
211
+ preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange,
212
+ deleteWhenSourceMissing: mount.deleteWhenSourceMissing,
213
+ head: candidate.version === 3 ? mount.head : liftedV2Head
214
+ };
165
215
  });
216
+ const sortedMounts = mounts.sort(compareWorkspaceHydrationMountBasis);
217
+ if (liftedV2Head) {
218
+ const legacyReceipt = {
219
+ version: 2,
220
+ head: liftedV2Head,
221
+ mounts: sortedMounts.map(({ head: _head, ...basis }) => basis)
222
+ };
223
+ if (content !== `${JSON.stringify(legacyReceipt)}
224
+ `) {
225
+ throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
226
+ }
227
+ }
166
228
  const receipt = {
167
- version: 2,
168
- head: candidate.head,
169
- mounts: mounts.sort(compareWorkspaceHydrationMountBasis)
229
+ version: 3,
230
+ mounts: sortedMounts
170
231
  };
171
- if (content !== `${JSON.stringify(receipt)}
232
+ if (liftedV2Head) Object.defineProperty(receipt, "legacyHead", { value: liftedV2Head, enumerable: false });
233
+ if (candidate.version === 3 && content !== `${JSON.stringify(receipt)}
172
234
  `) {
173
235
  throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
174
236
  }
175
- if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
176
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
237
+ const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
238
+ if (liftedV2Head) receiptHeads.add(liftedV2Head);
239
+ for (const head of receiptHeads) {
240
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`])) {
241
+ throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
242
+ }
177
243
  }
178
244
  return receipt;
179
245
  }
@@ -187,22 +253,92 @@ function readHydratedWorkspaceReceipt(workspacePath) {
187
253
  const content = fs.readFileSync(receiptPath, "utf8");
188
254
  return parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
189
255
  }
256
+ function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
257
+ const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
258
+ const status = lstatIfExists(receiptPath);
259
+ if (!status?.isFile() || status.isSymbolicLink()) return false;
260
+ try {
261
+ return JSON.parse(fs.readFileSync(receiptPath, "utf8")).version === 3;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
190
266
  function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
191
267
  const resolvedWorkspacePath = path.resolve(workspacePath);
192
268
  if (!fs.existsSync(path.join(resolvedWorkspacePath, ".git"))) return true;
193
269
  if (mounts) validateMounts(resolvedWorkspacePath, mounts);
194
270
  const head = revParse(resolvedWorkspacePath, "HEAD");
195
271
  if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
196
- if (head === null) return true;
197
272
  const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
198
- return mounts ? workspaceHydrationReceiptMatchesMounts(receipt, head, configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts)) : receipt?.head === head;
273
+ if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
274
+ if (head === null) return true;
275
+ if (!receipt) return false;
276
+ return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
277
+ (mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean(mount.busy?.() && workspaceHydrationReceiptMountBasisHead(receipt, mount))
278
+ ) : Boolean(receipt && receipt.mounts.every((mount) => mount.head === head));
279
+ }
280
+ const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
281
+ const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
282
+ function workspaceBasisRefName(mountId) {
283
+ const sanitized = Buffer.from(mountId, "utf8").toString("base64url");
284
+ if (!sanitized) throw new Error(`Cannot derive a workspace basis ref for mount id: ${mountId}`);
285
+ return `${WORKSPACE_GIT_BASIS_REF_PREFIX}${sanitized}`;
286
+ }
287
+ function listWorkspaceRefs(workspacePath, prefix) {
288
+ return git(workspacePath, ["for-each-ref", "--format=%(refname)", prefix], `list ${prefix} refs`).split("\n").filter(Boolean).sort();
289
+ }
290
+ function desiredWorkspaceBasisRefs(receipt) {
291
+ const desired = /* @__PURE__ */ new Map();
292
+ for (const mount of receipt.mounts) {
293
+ const ref = workspaceBasisRefName(mount.id);
294
+ if (desired.has(ref)) throw new Error(`Workspace hydration mount ids collide at basis ref ${ref}`);
295
+ desired.set(ref, mount.head);
296
+ }
297
+ return desired;
298
+ }
299
+ function workspaceBasisRefsMatch(workspacePath, receipt) {
300
+ const desired = desiredWorkspaceBasisRefs(receipt);
301
+ const actualRefs = listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX);
302
+ if (actualRefs.length !== desired.size) return false;
303
+ return actualRefs.every((ref) => desired.get(ref) === revParse(workspacePath, ref));
304
+ }
305
+ function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
306
+ return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
307
+ }
308
+ function repairWorkspaceBasisRefs(workspacePath, receipt) {
309
+ const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
310
+ if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
311
+ }
312
+ function updateWorkspaceBasisRefs(workspacePath, receipt) {
313
+ const desired = desiredWorkspaceBasisRefs(receipt);
314
+ const transaction = ["start"];
315
+ for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
316
+ for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
317
+ if (!desired.has(ref)) transaction.push(`delete ${ref}`);
318
+ }
319
+ transaction.push("prepare", "commit", "");
320
+ const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
321
+ cwd: workspacePath,
322
+ stdin: Buffer.from(transaction.join("\n")),
323
+ stdout: "pipe",
324
+ stderr: "pipe",
325
+ env: workerGitProcessEnvironment()
326
+ });
327
+ if (updated.exitCode !== 0) {
328
+ throw new Error(`Synchronize workspace hydration basis refs: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
329
+ }
330
+ for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) {
331
+ git(workspacePath, ["update-ref", "-d", ref], "remove staged workspace hydration basis ref");
332
+ }
199
333
  }
200
334
  function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
201
- if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(receipt.head)) {
202
- throw new Error(`Cannot record invalid fully hydrated workspace object id: ${receipt.head}`);
203
- }
204
- if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
205
- throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${receipt.head}`);
335
+ for (const mount of receipt.mounts) {
336
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
337
+ throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
338
+ }
339
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${mount.head}^{commit}`])) {
340
+ throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${mount.head}`);
341
+ }
206
342
  }
207
343
  readHydratedWorkspaceReceipt(workspacePath);
208
344
  const gitDirectory = path.join(workspacePath, ".git");
@@ -222,6 +358,12 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
222
358
  throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
223
359
  }
224
360
  const temporaryPath = path.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
361
+ const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
362
+ desiredWorkspaceBasisRefs(receipt);
363
+ for (const mount of receipt.mounts) {
364
+ const stagedRef = `${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)}`;
365
+ git(workspacePath, ["update-ref", stagedRef, mount.head], "stage workspace hydration basis ref");
366
+ }
225
367
  let descriptor;
226
368
  try {
227
369
  descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
@@ -232,6 +374,7 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
232
374
  descriptor = void 0;
233
375
  fs.renameSync(temporaryPath, receiptPath);
234
376
  fsyncDirectory(receiptDirectory);
377
+ updateWorkspaceBasisRefs(workspacePath, receipt);
235
378
  } catch (error) {
236
379
  if (descriptor !== void 0) fs.closeSync(descriptor);
237
380
  fs.rmSync(temporaryPath, { force: true });
@@ -359,6 +502,16 @@ function workspaceRebaseInProgress(workspacePath) {
359
502
  const gitDirectory = path.join(workspacePath, ".git");
360
503
  return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
361
504
  }
505
+ function workspaceResolutionInProgress(workspacePath) {
506
+ return workspaceRebaseInProgress(workspacePath) || fs.existsSync(path.join(workspacePath, ".git", "MERGE_HEAD"));
507
+ }
508
+ function workspaceHasUnmergedEntries(workspacePath) {
509
+ const result = gitResult(workspacePath, ["ls-files", "-u", "-z"]);
510
+ if (result.exitCode !== 0) {
511
+ throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
512
+ }
513
+ return result.stdout.length > 0;
514
+ }
362
515
  function clearWorkspaceCheckoutTree(workspacePath) {
363
516
  for (const name of fs.readdirSync(workspacePath)) {
364
517
  if (name === ".git") continue;
@@ -366,15 +519,18 @@ function clearWorkspaceCheckoutTree(workspacePath) {
366
519
  }
367
520
  fsyncDirectory(workspacePath);
368
521
  }
369
- function recoverWorkspaceCheckoutDurability(workspacePath) {
522
+ function recoverWorkspaceCheckoutDurability(workspacePath, preserveResolutionInProgress = false) {
370
523
  workspacePath = path.resolve(workspacePath);
371
524
  const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
372
525
  const observedHead = revParse(workspacePath, "HEAD");
373
526
  if (record?.state === "durable" && record.head === observedHead) return;
527
+ if (preserveResolutionInProgress && workspaceResolutionInProgress(workspacePath)) {
528
+ throw new Error("Workspace resolution is in progress; finish or abort it before synchronizing");
529
+ }
374
530
  let targetHead = record?.state === "transition" ? record.head : observedHead;
375
531
  if (record?.state === "transition" && observedHead && observedHead !== record.head) {
376
532
  const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
377
- if (hydrationReceipt?.head === observedHead) {
533
+ if (hydrationReceipt?.legacyHead === observedHead || hydrationReceipt?.mounts.some((mount) => mount.head === observedHead)) {
378
534
  targetHead = observedHead;
379
535
  }
380
536
  }
@@ -456,7 +612,7 @@ function parseHydrationTransactionManifest(transactionPath) {
456
612
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
457
613
  }
458
614
  const candidate = parsed;
459
- if (candidate.version !== 2 || !Array.isArray(candidate.mounts)) {
615
+ if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
460
616
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
461
617
  }
462
618
  const parseReceipt = (rawReceipt) => {
@@ -488,7 +644,7 @@ function parseHydrationTransactionManifest(transactionPath) {
488
644
  return mount;
489
645
  });
490
646
  return {
491
- version: 2,
647
+ version: 3,
492
648
  targetReceipt,
493
649
  receiptBefore,
494
650
  mounts
@@ -581,7 +737,7 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
581
737
  const snapshotRoot = path.join(stagingPath, "mounts");
582
738
  fs.mkdirSync(snapshotRoot, { mode: 448 });
583
739
  const manifest = {
584
- version: 2,
740
+ version: 3,
585
741
  targetReceipt,
586
742
  receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
587
743
  mounts: mounts.map((mount, index) => {
@@ -760,7 +916,11 @@ function configureWorkspaceRepository(input) {
760
916
  if (!name || !email) throw new Error("Workspace Git identity must include name and email");
761
917
  git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
762
918
  git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
763
- git(input.workspacePath, ["config", "--local", "core.fsync", "committed"], "configure durable workspace commits");
919
+ git(
920
+ input.workspacePath,
921
+ ["config", "--local", "core.fsync", "committed,reference"],
922
+ "configure durable workspace commits and references"
923
+ );
764
924
  git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
765
925
  }
766
926
  function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
@@ -824,7 +984,7 @@ function ensureWorkspaceGitClone(input) {
824
984
  }
825
985
  }
826
986
  configureWorkspaceRepository({ ...input, workspacePath });
827
- recoverWorkspaceCheckoutDurability(workspacePath);
987
+ recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
828
988
  const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
829
989
  const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
830
990
  if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
@@ -974,31 +1134,40 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
974
1134
  }
975
1135
  function hydrateWorkspaceGitMountsTransactionally(input) {
976
1136
  const workspacePath = path.resolve(input.workspacePath);
977
- const mountIds = input.mounts.map(({ id }) => id).sort();
978
- const requiredIds = (input.requiredMounts ?? input.mounts).map(({ id }) => id).sort();
979
- if (mountIds.length !== requiredIds.length || mountIds.some((id, index) => id !== requiredIds[index])) {
980
- return { hydratedMountIds: [], skippedMountIds: requiredIds };
981
- }
982
- const busyMountIds = input.ignoreBusy ? [] : (input.durabilityMounts ?? input.mounts).filter((mount) => mount.busy?.()).map(({ id }) => id).sort();
983
- if (busyMountIds.length > 0) {
984
- return { hydratedMountIds: [], skippedMountIds: busyMountIds };
985
- }
1137
+ const durabilityMounts = input.durabilityMounts ?? input.mounts;
1138
+ const receiptMounts = input.receiptMounts ?? durabilityMounts;
1139
+ const requiredMounts = input.requiredMounts ?? input.mounts;
1140
+ const candidateMounts = [
1141
+ ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
1142
+ ];
1143
+ const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1144
+ const requestedIds = new Set(input.mounts.map(({ id }) => id));
1145
+ const requiredIds = new Set(requiredMounts.map(({ id }) => id));
1146
+ const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1147
+ const advancedDurabilityMounts = durabilityMounts.filter(
1148
+ ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1149
+ );
1150
+ const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
986
1151
  if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
987
1152
  throw new Error("Workspace checkout is not durably materialized at its current HEAD");
988
1153
  }
989
1154
  const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
990
- const targetReceipt = targetHead ? workspaceHydrationReceipt(targetHead, input.receiptMounts ?? input.durabilityMounts ?? input.mounts) : null;
991
- const manifest = beginHydrationTransaction(workspacePath, input.mounts, targetReceipt);
1155
+ const targetReceipt = targetHead ? receiptWithMountsAtHead(readHydratedWorkspaceReceipt(workspacePath), targetHead, advancedDurabilityMounts, receiptMounts) : null;
1156
+ const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
992
1157
  try {
993
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, input.mounts, { ignoreBusy: true });
994
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredIds[index])) {
1158
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1159
+ const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1160
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
995
1161
  throw new Error("Workspace hydration did not include every required mount");
996
1162
  }
997
- fsyncHydratedWorkspaceMounts(input.durabilityMounts ?? input.mounts);
1163
+ fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
998
1164
  markHydrationTransactionDurable(workspacePath);
999
1165
  if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1000
1166
  removeHydrationTransaction(workspacePath);
1001
- return hydration;
1167
+ return {
1168
+ hydratedMountIds: hydration.hydratedMountIds,
1169
+ skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort()
1170
+ };
1002
1171
  } catch (error) {
1003
1172
  const transactionPath = hydrationTransactionPath(workspacePath);
1004
1173
  if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
@@ -1031,44 +1200,44 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1031
1200
  function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1032
1201
  workspacePath = path.resolve(workspacePath);
1033
1202
  validateMounts(workspacePath, mounts);
1034
- recoverWorkspaceCheckoutDurability(workspacePath);
1203
+ recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
1035
1204
  recoverPendingHydrationTransaction(workspacePath);
1036
1205
  const currentHead = revParse(workspacePath, "HEAD");
1037
- if (!currentHead) return;
1038
1206
  const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
1207
+ repairWorkspaceBasisRefs(workspacePath, currentReceipt);
1208
+ if (!currentHead) return;
1039
1209
  const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
1040
1210
  const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
1041
1211
  const uncoveredBasisMounts = configuredBasisMounts.filter(
1042
1212
  (mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
1043
1213
  );
1044
1214
  if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
1045
- const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1046
- if (busyMountIds.length > 0) {
1047
- throw new Error(
1048
- `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${busyMountIds.join(", ")}`
1049
- );
1050
- }
1051
- const hydrationTargets = uncoveredBasisMounts.filter(
1052
- (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1215
+ const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1216
+ const recoveryMounts = uncoveredBasisMounts.filter(
1217
+ (mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
1218
+ );
1219
+ const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
1220
+ const hydrationTargets = idleBasislessMounts.filter(
1221
+ (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1053
1222
  );
1054
1223
  const hydration = hydrateWorkspaceGitMountsTransactionally({
1055
1224
  workspacePath,
1056
1225
  mounts: hydrationTargets,
1057
- durabilityMounts: uncoveredBasisMounts,
1226
+ durabilityMounts: idleBasislessMounts,
1058
1227
  receiptMounts: configuredBasisMounts,
1059
1228
  requiredMounts: hydrationTargets,
1060
1229
  ignoreBusy: options.ignoreBusy,
1061
1230
  recordCurrentHead: true
1062
1231
  });
1063
- if (hydration.skippedMountIds.length > 0) {
1064
- throw new Error(
1065
- `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
1066
- );
1067
- }
1232
+ void hydration;
1068
1233
  }
1069
1234
  function resetWorkspaceGit(input) {
1070
1235
  const workspacePath = path.resolve(input.workspacePath);
1071
1236
  validateMounts(workspacePath, input.mounts);
1237
+ const busyResetMountIds = input.mounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1238
+ if (busyResetMountIds.length > 0) {
1239
+ throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
1240
+ }
1072
1241
  const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1073
1242
  recoverPendingHydrationTransaction(workspacePath);
1074
1243
  const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
@@ -1255,69 +1424,128 @@ async function synchronizeWorkspaceGit(input) {
1255
1424
  const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
1256
1425
  const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
1257
1426
  validateMounts(workspacePath, input.mounts);
1258
- const busyLiveMountIds = input.skipMountMirror ? [] : input.mounts.filter((mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()).map(({ id }) => id).sort();
1259
- if (busyLiveMountIds.length > 0) {
1260
- const cloneExists = fs.existsSync(path.join(workspacePath, ".git"));
1261
- const localHead = cloneExists ? revParse(workspacePath, "HEAD") : null;
1262
- const remoteHead2 = cloneExists ? revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`) : null;
1263
- return {
1264
- outcome: "no_change",
1265
- startingHead: localHead,
1266
- localHead,
1267
- remoteHead: remoteHead2,
1268
- publishedHead: remoteHead2,
1269
- rebaseCount: 0,
1270
- diffSizeBytes: 0,
1271
- affectedPaths: [],
1272
- activeMountIds: [],
1273
- skippedMountIds: input.mounts.map(({ id }) => id).sort()
1274
- };
1275
- }
1276
- const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1427
+ const preserveResolutionInProgress = input.skipMountMirror === true;
1428
+ const initial = ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
1277
1429
  const startingHead = initial.localHead;
1278
1430
  const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
1279
1431
  const deferredMountIds = /* @__PURE__ */ new Set();
1280
- if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead && receiptBeforeInitialHydration?.head === initial.localHead) {
1432
+ if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
1281
1433
  for (const mount of input.mounts) {
1282
- if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && !workspaceHydrationReceiptCoversMount(receiptBeforeInitialHydration, initial.localHead, mount)) {
1434
+ if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && workspaceHydrationReceiptMountBasisHead(receiptBeforeInitialHydration, mount) === null) {
1283
1435
  deferredMountIds.add(mount.id);
1284
1436
  }
1285
1437
  }
1286
1438
  }
1287
1439
  recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1288
- ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
1440
+ ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
1441
+ preserveResolutionInProgress,
1442
+ preserveStaleBases: true
1289
1443
  });
1290
- const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
1291
1444
  const selected = activeMounts(input.mounts);
1292
- const projectedActiveMounts = selected.active.filter(({ id }) => !deferredMountIds.has(id));
1293
- const projectedTombstones = selected.tombstones.filter(({ id }) => !deferredMountIds.has(id));
1445
+ const projectionMutationTokens = new Map(
1446
+ input.mounts.flatMap((mount) => mount.mutationToken ? [[mount.id, mount.mutationToken()]] : [])
1447
+ );
1448
+ const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
1449
+ const receiptRequiredLiveMounts = selected.active;
1294
1450
  const projectionBasisHead = revParse(workspacePath, "HEAD");
1295
1451
  const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
1296
- const projectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts).filter(
1297
- (mount) => !deferredMountIds.has(mount.id) && Boolean(projectionBasisHead && workspaceHydrationReceiptCoversMount(projectionBasisReceipt, projectionBasisHead, mount))
1298
- );
1452
+ const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1453
+ const fastProjectionMounts = [];
1454
+ const mergedProjectionTrees = /* @__PURE__ */ new Map();
1455
+ const mergedProjectionMounts = [];
1456
+ const projectionSkippedIds = /* @__PURE__ */ new Set();
1457
+ let projectionWarning;
1458
+ const refreshCycleSkippedMounts = (mounts = input.mounts) => {
1459
+ for (const mount of mounts) {
1460
+ const projectionToken = projectionMutationTokens.get(mount.id);
1461
+ if (projectionToken !== void 0 && mount.mutationToken?.() !== projectionToken) cycleSkippedMountIds.add(mount.id);
1462
+ if (mount.busy?.()) cycleSkippedMountIds.add(mount.id);
1463
+ }
1464
+ };
1465
+ const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
1466
+ refreshCycleSkippedMounts();
1467
+ const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
1468
+ return {
1469
+ activeMountIds: [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort(),
1470
+ skippedMountIds: [...skippedIds].sort()
1471
+ };
1472
+ };
1473
+ if (!input.skipMountMirror) {
1474
+ const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
1475
+ const mergeSupport = mergeProjectionEnabled ? workspaceMergeProjectionSupport() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
1476
+ for (const mount of [...selected.active, ...selected.tombstones]) {
1477
+ if (deferredMountIds.has(mount.id)) continue;
1478
+ const basisHead = workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount);
1479
+ if (projectionBasisHead === null || basisHead === projectionBasisHead) {
1480
+ fastProjectionMounts.push(mount);
1481
+ continue;
1482
+ }
1483
+ if (basisHead === null) {
1484
+ deferredMountIds.add(mount.id);
1485
+ continue;
1486
+ }
1487
+ if (!mergeSupport.supported) {
1488
+ projectionSkippedIds.add(mount.id);
1489
+ projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
1490
+ continue;
1491
+ }
1492
+ const merged = mergeWorkspaceProjectionMount({
1493
+ workspacePath,
1494
+ mount: {
1495
+ id: mount.id,
1496
+ sourcePath: mount.sourcePath,
1497
+ workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
1498
+ sourceMode: mount.sourceMode
1499
+ },
1500
+ basisHead,
1501
+ currentHead: projectionBasisHead,
1502
+ attemptId
1503
+ });
1504
+ if (merged.kind === "conflict") {
1505
+ const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
1506
+ return {
1507
+ outcome: "conflict_blocked",
1508
+ startingHead,
1509
+ localHead: merged.oursCommit,
1510
+ remoteHead: initial.remoteHead,
1511
+ publishedHead: initial.remoteHead,
1512
+ rebaseCount: 0,
1513
+ diffSizeBytes: 0,
1514
+ affectedPaths: merged.conflictPaths,
1515
+ activeMountIds: [],
1516
+ skippedMountIds: input.mounts.map(({ id }) => id).sort(),
1517
+ conflictPaths: merged.conflictPaths,
1518
+ conflictSnapshotRefs: refs,
1519
+ conflictKind: "projection_merge",
1520
+ error: merged.error
1521
+ };
1522
+ }
1523
+ mergedProjectionTrees.set(mount.id, merged.resultTree);
1524
+ mergedProjectionMounts.push(mount);
1525
+ }
1526
+ }
1527
+ if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
1528
+ `);
1299
1529
  const completedMountSelection = (requireCurrentHeadReceipt = false) => {
1300
1530
  const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
1301
1531
  const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1302
- if (currentHeadBeforeHydration && workspaceHydrationReceiptMatchesMounts(
1303
- readHydratedWorkspaceReceipt(workspacePath),
1304
- currentHeadBeforeHydration,
1305
- completionReceiptMounts
1306
- )) {
1307
- const lateBusyIds = selected.active.filter((mount) => mount.busy?.()).map(({ id }) => id);
1308
- const lateBusy = new Set(lateBusyIds);
1309
- return {
1310
- activeMountIds: [...selected.active.filter(({ id }) => !lateBusy.has(id)), ...selected.tombstones].map(({ id }) => id).sort(),
1311
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...lateBusyIds])].sort()
1312
- };
1313
- }
1314
1532
  const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
1533
+ refreshCycleSkippedMounts(completionReceiptMounts);
1315
1534
  const mountIsAlreadyCovered = (mount) => Boolean(
1316
1535
  currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
1317
1536
  );
1318
- const uncoveredCompletionMounts = selected.active.filter((mount) => !mountIsAlreadyCovered(mount));
1319
- const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1320
- const uncoveredCompletionBasisMounts = completionReceiptMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1537
+ if (currentHeadBeforeHydration && completionReceiptMounts.every((mount) => cycleSkippedMountIds.has(mount.id) || mountIsAlreadyCovered(mount))) {
1538
+ return classifyMounts([...selected.active, ...selected.tombstones]);
1539
+ }
1540
+ const uncoveredCompletionMounts = selected.active.filter(
1541
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1542
+ );
1543
+ const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter(
1544
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1545
+ );
1546
+ const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
1547
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1548
+ );
1321
1549
  const hydration = hydrateWorkspaceGitMountsTransactionally({
1322
1550
  workspacePath,
1323
1551
  mounts: uncoveredCompletionMounts,
@@ -1326,22 +1554,32 @@ async function synchronizeWorkspaceGit(input) {
1326
1554
  requiredMounts: uncoveredRequiredLiveMounts,
1327
1555
  recordCurrentHead: true
1328
1556
  });
1557
+ for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
1558
+ refreshCycleSkippedMounts(completionReceiptMounts);
1329
1559
  if (requireCurrentHeadReceipt) {
1330
1560
  const currentHead = revParse(workspacePath, "HEAD");
1331
- if (currentHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), currentHead, completionReceiptMounts)) {
1561
+ const receipt = readHydratedWorkspaceReceipt(workspacePath);
1562
+ const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
1563
+ const idleMounts = completionReceiptMounts.filter(
1564
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mount.busy?.() && !projectionSkippedIds.has(mount.id)
1565
+ );
1566
+ if (currentHead && !idleMounts.every((mount) => workspaceHydrationReceiptCoversMount(receipt, currentHead, mount))) {
1332
1567
  throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
1333
1568
  }
1334
1569
  }
1335
- return {
1336
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1337
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1338
- };
1570
+ return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
1339
1571
  };
1340
1572
  let newlyStagedPaths = [];
1341
1573
  const stageAndCommitWorkspace = () => {
1574
+ if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
1575
+ throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
1576
+ }
1342
1577
  git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
1343
1578
  newlyStagedPaths = stagedPaths(workspacePath);
1344
1579
  if (newlyStagedPaths.length > 0) {
1580
+ if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
1581
+ throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
1582
+ }
1345
1583
  git(
1346
1584
  workspacePath,
1347
1585
  [
@@ -1356,11 +1594,22 @@ async function synchronizeWorkspaceGit(input) {
1356
1594
  if (!input.skipMountMirror) {
1357
1595
  beginWorkspaceCheckoutTransition(workspacePath);
1358
1596
  try {
1359
- mirrorMountsToWorkspace(workspacePath, projectedActiveMounts);
1360
- removeWorkspaceMounts(workspacePath, projectedTombstones);
1597
+ const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
1598
+ const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
1599
+ const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
1600
+ mirrorMountsToWorkspace(workspacePath, fastActiveMounts);
1601
+ removeWorkspaceMounts(workspacePath, fastTombstones);
1602
+ for (const mount of mergedProjectionMounts) {
1603
+ materializeWorkspaceProjectionTree({
1604
+ workspacePath,
1605
+ workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
1606
+ resultTree: mergedProjectionTrees.get(mount.id)
1607
+ });
1608
+ }
1609
+ const restoreFromHeadIds = /* @__PURE__ */ new Set([...deferredMountIds, ...selected.skipped.map(({ id }) => id)]);
1361
1610
  restoreDeferredWorkspaceMountsFromHead(
1362
1611
  workspacePath,
1363
- input.mounts.filter(({ id }) => deferredMountIds.has(id))
1612
+ input.mounts.filter(({ id }) => restoreFromHeadIds.has(id))
1364
1613
  );
1365
1614
  stageAndCommitWorkspace();
1366
1615
  fsyncWorkspaceCheckoutTree(workspacePath);
@@ -1374,9 +1623,15 @@ async function synchronizeWorkspaceGit(input) {
1374
1623
  if (!input.skipMountMirror) {
1375
1624
  const projectedHead = revParse(workspacePath, "HEAD");
1376
1625
  if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
1377
- const targetProjectionReceipt = workspaceHydrationReceipt(projectedHead, projectionReceiptMounts);
1378
- if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), targetProjectionReceipt)) {
1379
- fsyncHydratedWorkspaceMounts([...projectedActiveMounts, ...projectedTombstones]);
1626
+ const targetProjectionReceipt = receiptWithMountsAtHead(
1627
+ projectionBasisReceipt,
1628
+ projectedHead,
1629
+ fastProjectionMounts,
1630
+ allProjectionReceiptMounts
1631
+ );
1632
+ const currentProjectionReceipt = readHydratedWorkspaceReceipt(workspacePath);
1633
+ if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !workspaceBasisRefsAreCurrent(workspacePath, targetProjectionReceipt)) {
1634
+ fsyncHydratedWorkspaceMounts(fastProjectionMounts);
1380
1635
  updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
1381
1636
  }
1382
1637
  completeWorkspaceCheckoutTransition(workspacePath);
@@ -1388,6 +1643,7 @@ async function synchronizeWorkspaceGit(input) {
1388
1643
  const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
1389
1644
  if (reconciled.kind === "conflict") {
1390
1645
  const localHead2 = revParse(workspacePath, "HEAD");
1646
+ const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
1391
1647
  return {
1392
1648
  outcome: "conflict_blocked",
1393
1649
  startingHead,
@@ -1397,10 +1653,10 @@ async function synchronizeWorkspaceGit(input) {
1397
1653
  rebaseCount: rebaseCount + 1,
1398
1654
  diffSizeBytes: 0,
1399
1655
  affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
1400
- activeMountIds: [...projectedActiveMounts, ...projectedTombstones].map(({ id }) => id).sort(),
1401
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
1656
+ ...classifiedMounts,
1402
1657
  conflictPaths: reconciled.conflictPaths,
1403
1658
  conflictSnapshotRefs: reconciled.refs,
1659
+ conflictKind: "integration_rebase",
1404
1660
  error: reconciled.error
1405
1661
  };
1406
1662
  }
@@ -1409,6 +1665,7 @@ async function synchronizeWorkspaceGit(input) {
1409
1665
  if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
1410
1666
  const localHead = revParse(workspacePath, "HEAD");
1411
1667
  if (!localHead) {
1668
+ const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
1412
1669
  return {
1413
1670
  outcome: "no_change",
1414
1671
  startingHead,
@@ -1418,13 +1675,14 @@ async function synchronizeWorkspaceGit(input) {
1418
1675
  rebaseCount,
1419
1676
  diffSizeBytes: 0,
1420
1677
  affectedPaths: [],
1421
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1422
- skippedMountIds: selected.skipped.map(({ id }) => id).sort()
1678
+ ...classifiedMounts,
1679
+ ...projectionWarning ? { error: projectionWarning } : {}
1423
1680
  };
1424
1681
  }
1425
1682
  const paths = changedPaths(workspacePath, remoteHead, localHead);
1426
1683
  const size = paths.length > 0 ? await diffSizeBytes(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
1427
1684
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
1685
+ const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
1428
1686
  return {
1429
1687
  outcome: "large_diff_blocked",
1430
1688
  startingHead,
@@ -1434,8 +1692,7 @@ async function synchronizeWorkspaceGit(input) {
1434
1692
  rebaseCount,
1435
1693
  diffSizeBytes: size,
1436
1694
  affectedPaths: paths,
1437
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1438
- skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
1695
+ ...classifiedMounts,
1439
1696
  error: `Workspace diff exceeds the ${maxDiffBytes}-byte automatic publication limit`
1440
1697
  };
1441
1698
  }
@@ -1454,7 +1711,8 @@ async function synchronizeWorkspaceGit(input) {
1454
1711
  rebaseCount,
1455
1712
  diffSizeBytes: size,
1456
1713
  affectedPaths: paths,
1457
- ...completedMounts
1714
+ ...completedMounts,
1715
+ ...projectionWarning ? { error: projectionWarning } : {}
1458
1716
  };
1459
1717
  }
1460
1718
  const pushArgs = [
@@ -1481,7 +1739,8 @@ async function synchronizeWorkspaceGit(input) {
1481
1739
  rebaseCount,
1482
1740
  diffSizeBytes: size,
1483
1741
  affectedPaths: paths,
1484
- ...completedMounts
1742
+ ...completedMounts,
1743
+ ...projectionWarning ? { error: projectionWarning } : {}
1485
1744
  };
1486
1745
  }
1487
1746
  if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}