@ricsam/r5d-worker 0.0.132 → 0.0.133

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.
Files changed (47) hide show
  1. package/dist/cjs/atomic-rename.cjs +303 -0
  2. package/dist/cjs/git-blob-hash.cjs +41 -0
  3. package/dist/cjs/main.cjs +187 -38
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/three-way-merge.cjs +346 -0
  6. package/dist/cjs/working-tree-mirror.cjs +1049 -64
  7. package/dist/cjs/workspace-command-sync-policy.cjs +8 -4
  8. package/dist/cjs/workspace-command-targets.cjs +63 -0
  9. package/dist/cjs/workspace-filesystem-job-types.cjs +11 -1
  10. package/dist/cjs/workspace-filesystem-jobs.cjs +2 -0
  11. package/dist/cjs/workspace-git-sync.cjs +846 -61
  12. package/dist/cjs/workspace-hydration-ledger.cjs +66 -0
  13. package/dist/cjs/workspace-hydration-merge.cjs +433 -0
  14. package/dist/cjs/workspace-hydration-recovery-state.cjs +53 -0
  15. package/dist/cjs/workspace-merge-projection.cjs +81 -10
  16. package/dist/cjs/workspace-project-config-policy.cjs +19 -12
  17. package/dist/mjs/atomic-rename.mjs +261 -0
  18. package/dist/mjs/git-blob-hash.mjs +16 -0
  19. package/dist/mjs/main.mjs +196 -39
  20. package/dist/mjs/package.json +1 -1
  21. package/dist/mjs/three-way-merge.mjs +318 -0
  22. package/dist/mjs/working-tree-mirror.mjs +1035 -64
  23. package/dist/mjs/workspace-command-sync-policy.mjs +8 -4
  24. package/dist/mjs/workspace-command-targets.mjs +37 -0
  25. package/dist/mjs/workspace-filesystem-job-types.mjs +11 -1
  26. package/dist/mjs/workspace-filesystem-jobs.mjs +4 -0
  27. package/dist/mjs/workspace-git-sync.mjs +854 -62
  28. package/dist/mjs/workspace-hydration-ledger.mjs +42 -0
  29. package/dist/mjs/workspace-hydration-merge.mjs +399 -0
  30. package/dist/mjs/workspace-hydration-recovery-state.mjs +29 -0
  31. package/dist/mjs/workspace-merge-projection.mjs +85 -11
  32. package/dist/mjs/workspace-project-config-policy.mjs +16 -10
  33. package/dist/types/atomic-rename.d.ts +78 -0
  34. package/dist/types/git-blob-hash.d.ts +10 -0
  35. package/dist/types/main.d.ts +21 -2
  36. package/dist/types/three-way-merge.d.ts +77 -0
  37. package/dist/types/working-tree-mirror.d.ts +270 -7
  38. package/dist/types/workspace-command-sync-policy.d.ts +12 -6
  39. package/dist/types/workspace-command-targets.d.ts +37 -0
  40. package/dist/types/workspace-filesystem-job-types.d.ts +46 -4
  41. package/dist/types/workspace-git-sync.d.ts +125 -3
  42. package/dist/types/workspace-hydration-ledger.d.ts +43 -0
  43. package/dist/types/workspace-hydration-merge.d.ts +95 -0
  44. package/dist/types/workspace-hydration-recovery-state.d.ts +10 -0
  45. package/dist/types/workspace-merge-projection.d.ts +19 -1
  46. package/dist/types/workspace-project-config-policy.d.ts +17 -3
  47. package/package.json +2 -2
@@ -33,7 +33,10 @@ __export(workspace_git_sync_exports, {
33
33
  WORKSPACE_GIT_CHECKOUT_DURABILITY: () => WORKSPACE_GIT_CHECKOUT_DURABILITY,
34
34
  WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION: () => WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
35
35
  WORKSPACE_GIT_HYDRATED_RECEIPT: () => WORKSPACE_GIT_HYDRATED_RECEIPT,
36
+ WORKSPACE_GIT_HYDRATION_RECOVERIES: () => WORKSPACE_GIT_HYDRATION_RECOVERIES,
36
37
  WORKSPACE_GIT_HYDRATION_TRANSACTION: () => WORKSPACE_GIT_HYDRATION_TRANSACTION,
38
+ WORKSPACE_MERGE_HYDRATION_SWITCH: () => WORKSPACE_MERGE_HYDRATION_SWITCH,
39
+ WorkspaceHydrationRecoveryRequiredError: () => WorkspaceHydrationRecoveryRequiredError,
37
40
  WorkspaceRemediationAncestryError: () => WorkspaceRemediationAncestryError,
38
41
  configureExistingWorkspaceGitForRemediation: () => configureExistingWorkspaceGitForRemediation,
39
42
  ensureWorkspaceGitClone: () => ensureWorkspaceGitClone,
@@ -43,7 +46,9 @@ __export(workspace_git_sync_exports, {
43
46
  runCheckoutDurabilityRecoveryJob: () => runCheckoutDurabilityRecoveryJob,
44
47
  runCheckoutTransitionBeginJob: () => runCheckoutTransitionBeginJob,
45
48
  runCheckoutTransitionCompleteJob: () => runCheckoutTransitionCompleteJob,
49
+ runHydrationPreserveInterruptedJob: () => runHydrationPreserveInterruptedJob,
46
50
  runHydrationRawJob: () => runHydrationRawJob,
51
+ runHydrationRecoveryInspectJob: () => runHydrationRecoveryInspectJob,
47
52
  runHydrationRecoveryJob: () => runHydrationRecoveryJob,
48
53
  runHydrationTransactionJob: () => runHydrationTransactionJob,
49
54
  runOuterCheckoutFsyncJob: () => runOuterCheckoutFsyncJob,
@@ -63,7 +68,10 @@ var import_node_path = __toESM(require("node:path"), 1);
63
68
  var import_git_process_environment = require("./git-process-environment.cjs");
64
69
  var import_workspace_merge_projection = require("./workspace-merge-projection.cjs");
65
70
  var import_workspace_filesystem_executor = require("./workspace-filesystem-executor.cjs");
71
+ var import_git_blob_hash = require("./git-blob-hash.cjs");
66
72
  var import_working_tree_mirror = require("./working-tree-mirror.cjs");
73
+ var import_workspace_hydration_merge = require("./workspace-hydration-merge.cjs");
74
+ var import_workspace_hydration_ledger = require("./workspace-hydration-ledger.cjs");
67
75
  var import_workspace_mount_boundary = require("./workspace-mount-boundary.cjs");
68
76
  const WORKSPACE_GIT_BRANCH = "main";
69
77
  const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
@@ -71,7 +79,36 @@ const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v
71
79
  const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
72
80
  const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
73
81
  const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
82
+ const WORKSPACE_GIT_HYDRATION_RECOVERIES = "r5d/workspace-hydration-recoveries";
83
+ const WORKSPACE_GIT_HYDRATION_PRESERVED_BASES = "r5d/workspace-hydration-preserved-bases";
74
84
  const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
85
+ const WORKSPACE_MERGE_HYDRATION_SWITCH = "R5D_MERGE_HYDRATION";
86
+ function workspaceMergeHydrationEnabled() {
87
+ return process.env[WORKSPACE_MERGE_HYDRATION_SWITCH] !== "0";
88
+ }
89
+ const hydrationPreBlobLedgers = /* @__PURE__ */ new Map();
90
+ function hydrationPreBlobLedger(workspacePath) {
91
+ const key = import_node_path.default.resolve(workspacePath);
92
+ let ledger = hydrationPreBlobLedgers.get(key);
93
+ if (!ledger) {
94
+ ledger = new import_workspace_hydration_ledger.WorkspaceHydrationPreBlobLedger();
95
+ hydrationPreBlobLedgers.set(key, ledger);
96
+ }
97
+ return ledger;
98
+ }
99
+ class WorkspaceHydrationRecoveryRequiredError extends Error {
100
+ constructor(hydrationRecovery, options) {
101
+ super(
102
+ `Interrupted guarded workspace hydration ${hydrationRecovery.transactionId} requires explicit preservation recovery; visible files and the prior receipt are preserved, and snapshots remain at ${hydrationRecovery.transactionPath} (mounts: ${hydrationRecovery.mounts.map(({ id }) => id).join(", ")})${hydrationRecovery.mounts.some(({ retainedPaths }) => retainedPaths?.length) ? `; displaced entries retained at ${hydrationRecovery.mounts.flatMap(({ retainedPaths }) => retainedPaths ?? []).join(", ")}` : ""}`,
103
+ options
104
+ );
105
+ this.hydrationRecovery = hydrationRecovery;
106
+ this.name = "WorkspaceHydrationRecoveryRequiredError";
107
+ }
108
+ hydrationRecovery;
109
+ code = "hydration_recovery_required";
110
+ }
111
+ const HYDRATION_TRANSACTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
75
112
  function workspaceGitMountData(mount) {
76
113
  return {
77
114
  id: mount.id,
@@ -927,9 +964,8 @@ async function runWorkspaceCheckoutTransition(workspacePath, mutate) {
927
964
  await throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
928
965
  }
929
966
  }
930
- function parseHydrationTransactionManifest(transactionPath) {
967
+ function parseHydrationTransactionManifest(transactionPath, workspacePath = import_node_path.default.resolve(transactionPath, "..", "..", ".."), options = {}) {
931
968
  const manifestPath = import_node_path.default.join(transactionPath, "manifest.json");
932
- const workspacePath = import_node_path.default.resolve(transactionPath, "..", "..", "..");
933
969
  const status = import_node_fs.default.lstatSync(manifestPath);
934
970
  if (!status.isFile() || status.isSymbolicLink()) {
935
971
  throw new Error(`Workspace hydration transaction manifest is not a regular file: ${manifestPath}`);
@@ -944,22 +980,26 @@ function parseHydrationTransactionManifest(transactionPath) {
944
980
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
945
981
  }
946
982
  const candidate = parsed;
947
- if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 || !Array.isArray(candidate.mounts)) {
983
+ if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 && candidate.version !== 5 || !Array.isArray(candidate.mounts) || candidate.preservedMounts !== void 0 && (candidate.version !== 5 || !Array.isArray(candidate.preservedMounts))) {
948
984
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
949
985
  }
986
+ if (candidate.version === 5 && (typeof candidate.transactionId !== "string" || !HYDRATION_TRANSACTION_ID.test(candidate.transactionId))) {
987
+ throw new Error(`Workspace hydration transaction identity is invalid: ${manifestPath}`);
988
+ }
950
989
  const parseReceipt = (rawReceipt) => {
951
990
  if (rawReceipt === null) return null;
952
991
  if (!rawReceipt || typeof rawReceipt !== "object") {
953
992
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
954
993
  }
955
- return parseWorkspaceHydrationReceiptContent(workspacePath, manifestPath, `${JSON.stringify(rawReceipt)}
956
- `);
994
+ const content = `${JSON.stringify(rawReceipt)}
995
+ `;
996
+ return options.verifyReceiptCommits === false ? parseWorkspaceHydrationReceiptContentUnverified(manifestPath, content).receipt : parseWorkspaceHydrationReceiptContent(workspacePath, manifestPath, content);
957
997
  };
958
998
  const targetReceipt = parseReceipt(candidate.targetReceipt);
959
999
  const receiptBefore = parseReceipt(candidate.receiptBefore);
960
1000
  const parseScope = (rawScope, existed) => {
961
1001
  if (rawScope === void 0) return void 0;
962
- if (candidate.version !== 4 || !existed || !rawScope || typeof rawScope !== "object") {
1002
+ if (candidate.version !== 4 && candidate.version !== 5 || !existed || !rawScope || typeof rawScope !== "object") {
963
1003
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
964
1004
  }
965
1005
  const parsePaths = (rawPaths) => {
@@ -978,7 +1018,7 @@ function parseHydrationTransactionManifest(transactionPath) {
978
1018
  };
979
1019
  const seenIds = /* @__PURE__ */ new Set();
980
1020
  const seenSnapshots = /* @__PURE__ */ new Set();
981
- const mounts = candidate.mounts.map((rawMount) => {
1021
+ const parsedMounts = [...candidate.mounts, ...candidate.preservedMounts ?? []].map((rawMount) => {
982
1022
  if (!rawMount || typeof rawMount !== "object") {
983
1023
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
984
1024
  }
@@ -993,6 +1033,9 @@ function parseHydrationTransactionManifest(transactionPath) {
993
1033
  seenIds.add(mount.id);
994
1034
  seenSnapshots.add(mount.snapshotName);
995
1035
  const scope = parseScope(rawMount.scope, mount.existed);
1036
+ if (candidate.version === 5 && mount.recovery !== "snapshot" && mount.recovery !== "guarded" || candidate.version !== 5 && mount.recovery !== void 0 || mount.recovery === "guarded" && (!mount.existed || !scope)) {
1037
+ throw new Error(`Workspace hydration recovery policy is invalid: ${manifestPath}`);
1038
+ }
996
1039
  return {
997
1040
  id: mount.id,
998
1041
  incarnationKey: mount.incarnationKey,
@@ -1001,16 +1044,22 @@ function parseHydrationTransactionManifest(transactionPath) {
1001
1044
  workspaceRelativePath: mount.workspaceRelativePath,
1002
1045
  existed: mount.existed,
1003
1046
  snapshotName: mount.snapshotName,
1004
- ...scope ? { scope } : {}
1047
+ ...scope ? { scope } : {},
1048
+ ...candidate.version === 5 ? { recovery: mount.recovery } : {}
1005
1049
  };
1006
1050
  });
1007
1051
  return {
1008
- version: candidate.version === 4 ? 4 : 3,
1052
+ version: candidate.version === 5 ? 5 : candidate.version === 4 ? 4 : 3,
1053
+ ...candidate.version === 5 ? { transactionId: candidate.transactionId } : {},
1009
1054
  targetReceipt,
1010
1055
  receiptBefore,
1011
- mounts
1056
+ mounts: parsedMounts.slice(0, candidate.mounts.length),
1057
+ ...candidate.preservedMounts !== void 0 ? { preservedMounts: parsedMounts.slice(candidate.mounts.length) } : {}
1012
1058
  };
1013
1059
  }
1060
+ function hydrationEvidenceMounts(manifest) {
1061
+ return [...manifest.mounts, ...manifest.preservedMounts ?? []];
1062
+ }
1014
1063
  function removeHydrationTransaction(workspacePath) {
1015
1064
  const transactionPath = hydrationTransactionPath(workspacePath);
1016
1065
  import_node_fs.default.rmSync(transactionPath, { recursive: true, force: true });
@@ -1025,7 +1074,303 @@ function hydrationTransactionCommitted(transactionPath) {
1025
1074
  }
1026
1075
  return true;
1027
1076
  }
1077
+ function hydrationRetainedEvidence(mount) {
1078
+ if (!lstatIfExists(mount.sourcePath)) return null;
1079
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1080
+ trustedRoot: mount.durabilityRootPath,
1081
+ candidate: mount.sourcePath,
1082
+ label: `Hydration evidence mount ${mount.id}`
1083
+ });
1084
+ const dotGit = import_node_path.default.join(mount.sourcePath, ".git");
1085
+ if (lstatIfExists(dotGit)?.isSymbolicLink()) throw new Error(`Hydration evidence Git directory is a symlink: ${dotGit}`);
1086
+ const directory = (0, import_working_tree_mirror.workingTreeDisplacedRetentionDirectory)(mount.sourcePath);
1087
+ if (!directory) return null;
1088
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1089
+ trustedRoot: mount.durabilityRootPath,
1090
+ candidate: directory,
1091
+ label: `Hydration retained evidence ${mount.id}`
1092
+ });
1093
+ if (!lstatIfExists(directory)) return null;
1094
+ return {
1095
+ directory,
1096
+ paths: import_node_fs.default.readdirSync(directory).sort().map((name) => import_node_path.default.join(directory, name))
1097
+ };
1098
+ }
1099
+ function fsyncHydrationRetainedEvidence(mount) {
1100
+ const evidence = hydrationRetainedEvidence(mount);
1101
+ if (!evidence || evidence.paths.length === 0) return;
1102
+ fsyncTree(evidence.directory);
1103
+ for (let directory = import_node_path.default.dirname(evidence.directory); ; directory = import_node_path.default.dirname(directory)) {
1104
+ fsyncDirectory(directory);
1105
+ if (directory === import_node_path.default.resolve(mount.durabilityRootPath)) break;
1106
+ }
1107
+ }
1108
+ function readHydrationArchivedEvidence(transactionPath, manifest) {
1109
+ const recordPath = import_node_path.default.join(transactionPath, "retained-evidence.json");
1110
+ const status = lstatIfExists(recordPath);
1111
+ if (!status) return [];
1112
+ if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Invalid hydration retained-evidence record: ${recordPath}`);
1113
+ const record = JSON.parse(import_node_fs.default.readFileSync(recordPath, "utf8"));
1114
+ if (record.version !== 1 || !Array.isArray(record.entries)) throw new Error(`Invalid hydration retained-evidence record: ${recordPath}`);
1115
+ const seen = /* @__PURE__ */ new Set();
1116
+ return record.entries.map((raw) => {
1117
+ if (!raw || typeof raw !== "object") throw new Error(`Invalid hydration retained-evidence entry: ${recordPath}`);
1118
+ const entry = raw;
1119
+ const mount = hydrationEvidenceMounts(manifest).find(({ id }) => id === entry.mountId);
1120
+ const parts = typeof entry.archiveRelativePath === "string" ? entry.archiveRelativePath.split("/") : [];
1121
+ const sourceRelative = typeof entry.sourcePath === "string" && mount ? import_node_path.default.relative(mount.durabilityRootPath, entry.sourcePath) : "";
1122
+ if (!mount || typeof entry.sourcePath !== "string" || import_node_path.default.resolve(entry.sourcePath) !== entry.sourcePath || !sourceRelative || sourceRelative === ".." || sourceRelative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(sourceRelative) || parts.length !== 4 || parts[0] !== "displaced" || !HYDRATION_TRANSACTION_ID.test(parts[1]) || parts[2] !== mount.snapshotName || !parts[3] || parts[3] === "." || parts[3] === ".." || parts[3].includes("\\") || seen.has(entry.archiveRelativePath)) {
1123
+ throw new Error(`Invalid hydration retained-evidence entry: ${recordPath}`);
1124
+ }
1125
+ const archivedPath = import_node_path.default.join(transactionPath, ...parts);
1126
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1127
+ trustedRoot: transactionPath,
1128
+ candidate: import_node_path.default.dirname(archivedPath),
1129
+ label: "Archived hydration evidence"
1130
+ });
1131
+ if (!lstatIfExists(archivedPath)) throw new Error(`Archived hydration evidence is missing: ${archivedPath}`);
1132
+ seen.add(entry.archiveRelativePath);
1133
+ return { mountId: entry.mountId, sourcePath: entry.sourcePath, archiveRelativePath: entry.archiveRelativePath };
1134
+ });
1135
+ }
1136
+ function captureHydrationRetainedEvidence(transactionPath, manifest) {
1137
+ const entries = readHydrationArchivedEvidence(transactionPath, manifest);
1138
+ const captureId = crypto.randomUUID();
1139
+ let captured = false;
1140
+ for (const mount of hydrationEvidenceMounts(manifest)) {
1141
+ const evidence = hydrationRetainedEvidence(mount);
1142
+ if (!evidence || evidence.paths.length === 0) continue;
1143
+ fsyncHydrationRetainedEvidence(mount);
1144
+ const relativeDirectory = `displaced/${captureId}/${mount.snapshotName}`;
1145
+ const destinationDirectory = import_node_path.default.join(transactionPath, relativeDirectory);
1146
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({ trustedRoot: transactionPath, candidate: destinationDirectory, label: "Hydration evidence capture" });
1147
+ import_node_fs.default.mkdirSync(destinationDirectory, { recursive: true, mode: 448 });
1148
+ for (const sourcePath of evidence.paths) {
1149
+ const archiveRelativePath = `${relativeDirectory}/${import_node_path.default.basename(sourcePath)}`;
1150
+ import_node_fs.default.cpSync(sourcePath, import_node_path.default.join(transactionPath, archiveRelativePath), {
1151
+ recursive: true,
1152
+ dereference: false,
1153
+ verbatimSymlinks: true,
1154
+ errorOnExist: true,
1155
+ force: false,
1156
+ preserveTimestamps: true
1157
+ });
1158
+ entries.push({ mountId: mount.id, sourcePath, archiveRelativePath });
1159
+ captured = true;
1160
+ }
1161
+ }
1162
+ if (captured) {
1163
+ fsyncTree(import_node_path.default.join(transactionPath, "displaced"));
1164
+ const temporaryPath = import_node_path.default.join(transactionPath, `.retained-evidence.${crypto.randomUUID()}.tmp`);
1165
+ writePrivateFileDurably(temporaryPath, `${JSON.stringify({ version: 1, entries })}
1166
+ `);
1167
+ import_node_fs.default.renameSync(temporaryPath, import_node_path.default.join(transactionPath, "retained-evidence.json"));
1168
+ fsyncDirectory(transactionPath);
1169
+ }
1170
+ return entries;
1171
+ }
1172
+ function hydrationRecoveryRequiredError(workspacePath, manifest, cause) {
1173
+ if (manifest.version !== 5 || !manifest.transactionId) throw new Error("Guarded hydration lacks a durable transaction identity");
1174
+ return new WorkspaceHydrationRecoveryRequiredError(
1175
+ {
1176
+ transactionId: manifest.transactionId,
1177
+ transactionPath: hydrationTransactionPath(workspacePath),
1178
+ mounts: hydrationEvidenceMounts(manifest).map(({ id, incarnationKey, sourcePath, durabilityRootPath, workspaceRelativePath }) => {
1179
+ const retainedPaths = hydrationRetainedEvidence({ id, sourcePath, durabilityRootPath })?.paths;
1180
+ return {
1181
+ id,
1182
+ incarnationKey,
1183
+ sourcePath,
1184
+ durabilityRootPath,
1185
+ workspaceRelativePath,
1186
+ ...retainedPaths?.length ? { retainedPaths } : {}
1187
+ };
1188
+ }),
1189
+ receiptBefore: manifest.receiptBefore
1190
+ },
1191
+ cause === void 0 ? void 0 : { cause }
1192
+ );
1193
+ }
1194
+ function runHydrationRecoveryInspectJob(input) {
1195
+ const workspacePath = import_node_path.default.resolve(input.workspacePath);
1196
+ const workspaceStatus = lstatIfExists(workspacePath);
1197
+ if (!workspaceStatus) return null;
1198
+ if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink())
1199
+ throw new Error(`Invalid workspace hydration root: ${workspacePath}`);
1200
+ const transactionPath = hydrationTransactionPath(workspacePath);
1201
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1202
+ trustedRoot: workspacePath,
1203
+ candidate: transactionPath,
1204
+ label: "Workspace hydration transaction inspection"
1205
+ });
1206
+ const status = lstatIfExists(transactionPath);
1207
+ if (!status) return null;
1208
+ if (!status.isDirectory() || status.isSymbolicLink()) throw new Error(`Invalid workspace hydration transaction: ${transactionPath}`);
1209
+ const manifest = parseHydrationTransactionManifest(transactionPath);
1210
+ if (hydrationTransactionCommitted(transactionPath) || !manifest.mounts.some(({ recovery }) => recovery === "guarded")) return null;
1211
+ return hydrationRecoveryRequiredError(workspacePath, manifest).hydrationRecovery;
1212
+ }
1213
+ function readPreservedHydrationBases(workspacePath) {
1214
+ const recordPath = import_node_path.default.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
1215
+ const status = lstatIfExists(recordPath);
1216
+ if (!status) return null;
1217
+ if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Preserved hydration bases are not a regular file: ${recordPath}`);
1218
+ return parseWorkspaceHydrationReceiptContentUnverified(recordPath, import_node_fs.default.readFileSync(recordPath, "utf8")).receipt;
1219
+ }
1220
+ async function readPreservedHydrationBasesAsync(workspacePath) {
1221
+ const recordPath = import_node_path.default.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
1222
+ let status;
1223
+ try {
1224
+ status = await import_node_fs.default.promises.lstat(recordPath);
1225
+ } catch (error) {
1226
+ if (error.code === "ENOENT") return null;
1227
+ throw error;
1228
+ }
1229
+ if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Preserved hydration bases are not a regular file: ${recordPath}`);
1230
+ return parseWorkspaceHydrationReceiptContentUnverified(recordPath, await import_node_fs.default.promises.readFile(recordPath, "utf8")).receipt;
1231
+ }
1232
+ function preserveHydrationBases(workspacePath, manifest) {
1233
+ const current = readHydratedWorkspaceReceipt(workspacePath);
1234
+ if (!workspaceHydrationReceiptsEqual(current, manifest.receiptBefore)) {
1235
+ throw new Error(`Hydration receipt changed before preservation of transaction ${manifest.transactionId}`);
1236
+ }
1237
+ const previous = readPreservedHydrationBases(workspacePath);
1238
+ const affectedIds = new Set(hydrationEvidenceMounts(manifest).map(({ id }) => id));
1239
+ const mounts = (current?.mounts ?? []).filter(
1240
+ (entry) => affectedIds.has(entry.id) || previous?.mounts.some(
1241
+ (preserved) => workspaceHydrationReceiptsEqual({ version: 3, mounts: [entry] }, { version: 3, mounts: [preserved] })
1242
+ )
1243
+ );
1244
+ const recordPath = import_node_path.default.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
1245
+ const temporaryPath = `${recordPath}.${crypto.randomUUID()}.tmp`;
1246
+ writePrivateFileDurably(temporaryPath, `${JSON.stringify({ version: 3, mounts })}
1247
+ `);
1248
+ import_node_fs.default.renameSync(temporaryPath, recordPath);
1249
+ fsyncDirectory(import_node_path.default.dirname(recordPath));
1250
+ }
1251
+ function runHydrationPreserveInterruptedJob(input) {
1252
+ const workspacePath = import_node_path.default.resolve(input.workspacePath);
1253
+ if (!HYDRATION_TRANSACTION_ID.test(input.expectedTransactionId)) throw new Error("Invalid hydration recovery transaction identity");
1254
+ const stateDirectory = workspacePrivateStateDirectory(workspacePath);
1255
+ const transactionPath = hydrationTransactionPath(workspacePath);
1256
+ const archivesPath = import_node_path.default.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_RECOVERIES.split("/"));
1257
+ const archivePath = import_node_path.default.join(archivesPath, input.expectedTransactionId);
1258
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({ trustedRoot: stateDirectory, candidate: archivesPath, label: "Workspace hydration archives" });
1259
+ const pending = lstatIfExists(transactionPath);
1260
+ if (!pending) {
1261
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({ trustedRoot: stateDirectory, candidate: archivePath, label: "Workspace hydration archive" });
1262
+ if (!lstatIfExists(archivePath))
1263
+ throw new Error(`No pending or preserved hydration transaction matches ${input.expectedTransactionId}`);
1264
+ const archived = parseHydrationTransactionManifest(archivePath, workspacePath, { verifyReceiptCommits: false });
1265
+ const markerPath2 = import_node_path.default.join(archivePath, "preserved.json");
1266
+ const markerStatus = import_node_fs.default.lstatSync(markerPath2);
1267
+ if (!markerStatus.isFile() || markerStatus.isSymbolicLink()) throw new Error(`Invalid hydration preservation record: ${markerPath2}`);
1268
+ const marker = JSON.parse(import_node_fs.default.readFileSync(markerPath2, "utf8"));
1269
+ if (archived.version !== 5 || archived.transactionId !== input.expectedTransactionId || marker.version !== 1 || marker.transactionId !== archived.transactionId) {
1270
+ throw new Error(`Hydration archive identity does not match ${input.expectedTransactionId}`);
1271
+ }
1272
+ const retainedEntries2 = readHydrationArchivedEvidence(archivePath, archived).map(({ mountId, sourcePath, archiveRelativePath }) => ({
1273
+ mountId,
1274
+ sourcePath,
1275
+ archivePath: import_node_path.default.join(archivePath, archiveRelativePath)
1276
+ }));
1277
+ fsyncDirectory(archivePath);
1278
+ fsyncDirectory(archivesPath);
1279
+ fsyncDirectory(stateDirectory);
1280
+ return {
1281
+ transactionId: archived.transactionId,
1282
+ archivePath,
1283
+ receiptBefore: archived.receiptBefore,
1284
+ preservedMountIds: hydrationEvidenceMounts(archived).map(({ id }) => id).sort(),
1285
+ alreadyPreserved: true,
1286
+ ...retainedEntries2.length > 0 ? { retainedEntries: retainedEntries2 } : {}
1287
+ };
1288
+ }
1289
+ if (!pending.isDirectory() || pending.isSymbolicLink()) throw new Error(`Invalid workspace hydration transaction: ${transactionPath}`);
1290
+ const manifest = parseHydrationTransactionManifest(transactionPath);
1291
+ if (manifest.version !== 5 || manifest.transactionId !== input.expectedTransactionId) {
1292
+ throw new Error(
1293
+ `Stale hydration recovery transaction identity: expected ${input.expectedTransactionId}, pending ${manifest.transactionId ?? "legacy transaction"}`
1294
+ );
1295
+ }
1296
+ if (hydrationTransactionCommitted(transactionPath))
1297
+ throw new Error(`Hydration transaction ${manifest.transactionId} is committed; ordinary recovery must finish its receipt`);
1298
+ if (!manifest.mounts.some(({ recovery }) => recovery === "guarded"))
1299
+ throw new Error(`Hydration transaction ${manifest.transactionId} has no interrupted guarded mounts`);
1300
+ if (lstatIfExists(archivePath))
1301
+ throw new Error(`Hydration archive already exists while transaction ${manifest.transactionId} is pending`);
1302
+ if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), manifest.receiptBefore)) {
1303
+ throw new Error(`Hydration receipt changed before preservation of transaction ${manifest.transactionId}`);
1304
+ }
1305
+ for (const mount of hydrationEvidenceMounts(manifest)) {
1306
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1307
+ trustedRoot: mount.durabilityRootPath,
1308
+ candidate: mount.sourcePath,
1309
+ label: `Hydration preservation mount ${mount.id}`
1310
+ });
1311
+ const source = lstatIfExists(mount.sourcePath);
1312
+ if (mount.existed && !source) throw new Error(`Hydration preservation mount ${mount.id} is missing: ${mount.sourcePath}`);
1313
+ const snapshotPath = import_node_path.default.join(transactionPath, "mounts", mount.snapshotName);
1314
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1315
+ trustedRoot: transactionPath,
1316
+ candidate: snapshotPath,
1317
+ label: `Hydration preservation snapshot ${mount.id}`
1318
+ });
1319
+ if (!lstatIfExists(snapshotPath)) throw new Error(`Hydration preservation snapshot ${mount.id} is missing`);
1320
+ const basis = manifest.receiptBefore?.mounts.find((entry) => entry.id === mount.id);
1321
+ if (!basis || basis.incarnationKey !== mount.incarnationKey || basis.sourcePath !== mount.sourcePath || basis.durabilityRootPath !== mount.durabilityRootPath || basis.workspaceRelativePath !== mount.workspaceRelativePath) {
1322
+ throw new Error(
1323
+ `Hydration preservation mount ${mount.id} has no matching prior receipt basis; manual salvage is required. The transaction and all snapshots remain at ${transactionPath}`
1324
+ );
1325
+ }
1326
+ }
1327
+ for (const mount of hydrationEvidenceMounts(manifest)) {
1328
+ fsyncHydratedWorkspaceMount({ ...mount, hydrationIncarnationKey: mount.incarnationKey, sourceMode: "all", hydrateDeletionMode: "all" });
1329
+ }
1330
+ const retainedEntries = captureHydrationRetainedEvidence(transactionPath, manifest).map(
1331
+ ({ mountId, sourcePath, archiveRelativePath }) => ({
1332
+ mountId,
1333
+ sourcePath,
1334
+ archivePath: import_node_path.default.join(archivePath, archiveRelativePath)
1335
+ })
1336
+ );
1337
+ fsyncTree(transactionPath);
1338
+ preserveHydrationBases(workspacePath, manifest);
1339
+ repairWorkspaceBasisRefs(workspacePath, manifest.receiptBefore);
1340
+ const markerPath = import_node_path.default.join(transactionPath, "preserved.json");
1341
+ const markerContent = `${JSON.stringify({ version: 1, transactionId: manifest.transactionId })}
1342
+ `;
1343
+ const existingMarker = lstatIfExists(markerPath);
1344
+ if (existingMarker) {
1345
+ if (!existingMarker.isFile() || existingMarker.isSymbolicLink() || import_node_fs.default.readFileSync(markerPath, "utf8") !== markerContent) {
1346
+ throw new Error(`Invalid hydration preservation record: ${markerPath}`);
1347
+ }
1348
+ } else {
1349
+ const markerTemporaryPath = import_node_path.default.join(transactionPath, `.preserved.${crypto.randomUUID()}.tmp`);
1350
+ writePrivateFileDurably(markerTemporaryPath, markerContent);
1351
+ import_node_fs.default.renameSync(markerTemporaryPath, markerPath);
1352
+ }
1353
+ fsyncDirectory(transactionPath);
1354
+ if (!lstatIfExists(archivesPath)) {
1355
+ import_node_fs.default.mkdirSync(archivesPath, { mode: 448 });
1356
+ fsyncDirectory(stateDirectory);
1357
+ }
1358
+ import_node_fs.default.renameSync(transactionPath, archivePath);
1359
+ fsyncDirectory(archivesPath);
1360
+ fsyncDirectory(stateDirectory);
1361
+ return {
1362
+ transactionId: manifest.transactionId,
1363
+ archivePath,
1364
+ receiptBefore: manifest.receiptBefore,
1365
+ preservedMountIds: hydrationEvidenceMounts(manifest).map(({ id }) => id).sort(),
1366
+ alreadyPreserved: false,
1367
+ ...retainedEntries.length > 0 ? { retainedEntries } : {}
1368
+ };
1369
+ }
1028
1370
  function restoreHydrationTransaction(workspacePath, manifest) {
1371
+ if (manifest.mounts.some(({ recovery }) => recovery === "guarded")) {
1372
+ throw hydrationRecoveryRequiredError(workspacePath, manifest);
1373
+ }
1029
1374
  const transactionPath = hydrationTransactionPath(workspacePath);
1030
1375
  const durabilityScopes = /* @__PURE__ */ new Map();
1031
1376
  for (const mount of manifest.mounts) {
@@ -1088,25 +1433,132 @@ function recoverPendingHydrationTransaction(workspacePath) {
1088
1433
  return;
1089
1434
  }
1090
1435
  const receipt = readHydratedWorkspaceReceipt(workspacePath);
1091
- if (manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
1436
+ if (manifest.version !== 5 && manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
1092
1437
  removeHydrationTransaction(workspacePath);
1093
1438
  return;
1094
1439
  }
1095
1440
  restoreHydrationTransaction(workspacePath, manifest);
1096
1441
  removeHydrationTransaction(workspacePath);
1097
1442
  }
1098
- function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1443
+ function gitOutputSync(cwd, args, action, input) {
1444
+ const result = Bun.spawnSync(gitCommandArgs(args), {
1445
+ cwd,
1446
+ ...input ? { stdin: input } : {},
1447
+ stdout: "pipe",
1448
+ stderr: "pipe",
1449
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1450
+ });
1451
+ if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
1452
+ return Buffer.from(result.stdout);
1453
+ }
1454
+ function workspaceSubtreeChanges(workspacePath, projectedCommit, head, workspaceRelativePath) {
1455
+ const normalized = normalizedWorkspaceMountPath(workspaceRelativePath);
1456
+ const names = [`${projectedCommit}:${normalized}`, `${head}:${normalized}`];
1457
+ const resolved = resolveGitObjectsSync(workspacePath, names);
1458
+ const emptyTree = emptyTreeHash(workspacePath);
1459
+ const subtree = (name) => {
1460
+ const object = resolved.get(name);
1461
+ return object?.type === "tree" ? object.objectId : emptyTree;
1462
+ };
1463
+ const base = subtree(names[0]);
1464
+ const theirs = subtree(names[1]);
1465
+ if (base === theirs) return (0, import_workspace_hydration_merge.parseWorkspaceSubtreeChanges)(Buffer.alloc(0));
1466
+ return (0, import_workspace_hydration_merge.parseWorkspaceSubtreeChanges)(
1467
+ gitOutputSync(
1468
+ workspacePath,
1469
+ ["diff-tree", "-r", "-z", "--no-renames", "--raw", base, theirs],
1470
+ "compare projected and hydration subtrees"
1471
+ )
1472
+ );
1473
+ }
1474
+ function resolveGitObjectsSync(workspacePath, names) {
1475
+ const resolved = /* @__PURE__ */ new Map();
1476
+ const unique = [...new Set(names)];
1477
+ if (unique.length === 0) return resolved;
1478
+ const output = gitOutputSync(
1479
+ workspacePath,
1480
+ ["cat-file", "--batch-check"],
1481
+ "resolve workspace objects",
1482
+ Buffer.from(`${unique.join("\n")}
1483
+ `)
1484
+ );
1485
+ const lines = output.toString().split("\n").filter((line) => line.length > 0);
1486
+ if (lines.length !== unique.length) {
1487
+ throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1488
+ }
1489
+ unique.forEach((name, index) => {
1490
+ const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
1491
+ resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
1492
+ });
1493
+ return resolved;
1494
+ }
1495
+ function readWorkspaceBlobs(workspacePath, objectIds) {
1496
+ const blobs = /* @__PURE__ */ new Map();
1497
+ const unique = [...new Set(objectIds)];
1498
+ if (unique.length === 0) return blobs;
1499
+ const output = gitOutputSync(
1500
+ workspacePath,
1501
+ ["cat-file", "--batch"],
1502
+ "read projected workspace blobs",
1503
+ Buffer.from(`${unique.join("\n")}
1504
+ `)
1505
+ );
1506
+ let offset = 0;
1507
+ while (offset < output.length) {
1508
+ const newline = output.indexOf(10, offset);
1509
+ if (newline < 0) break;
1510
+ const header = output.subarray(offset, newline).toString();
1511
+ offset = newline + 1;
1512
+ const match = /^([0-9a-f]{40,64}) (\S+) (\d+)$/u.exec(header);
1513
+ if (!match) {
1514
+ if (/ missing$/u.test(header)) continue;
1515
+ throw new Error(`Unexpected cat-file record while reading projected blobs: ${header}`);
1516
+ }
1517
+ const size = Number(match[3]);
1518
+ if (match[2] === "blob") blobs.set(match[1], Buffer.from(output.subarray(offset, offset + size)));
1519
+ offset += size + 1;
1520
+ }
1521
+ return blobs;
1522
+ }
1523
+ function revertReceiptMounts(target, before, mountIds) {
1524
+ if (!target || mountIds.size === 0) return target;
1525
+ const beforeById = new Map(before?.mounts.map((entry) => [entry.id, entry]) ?? []);
1526
+ const mounts = target.mounts.flatMap((entry) => {
1527
+ if (!mountIds.has(entry.id)) return [entry];
1528
+ const previous = beforeById.get(entry.id);
1529
+ return previous ? [previous] : [];
1530
+ }).sort(compareWorkspaceHydrationMountBasis);
1531
+ return { version: 3, mounts };
1532
+ }
1533
+ function rewriteHydrationTransactionManifest(workspacePath, manifest) {
1534
+ const transactionPath = hydrationTransactionPath(workspacePath);
1535
+ const temporaryPath = import_node_path.default.join(transactionPath, `.manifest.${process.pid}.${crypto.randomUUID()}.tmp`);
1536
+ try {
1537
+ writePrivateFileDurably(temporaryPath, `${JSON.stringify(manifest)}
1538
+ `);
1539
+ import_node_fs.default.renameSync(temporaryPath, import_node_path.default.join(transactionPath, "manifest.json"));
1540
+ fsyncDirectory(transactionPath);
1541
+ } catch (error) {
1542
+ import_node_fs.default.rmSync(temporaryPath, { force: true });
1543
+ throw error;
1544
+ }
1545
+ }
1546
+ function beginHydrationTransaction(workspacePath, mounts, targetReceipt, mergeBases) {
1099
1547
  recoverPendingHydrationTransaction(workspacePath);
1100
1548
  const stateDirectory = workspacePrivateStateDirectory(workspacePath);
1101
1549
  const transactionPath = hydrationTransactionPath(workspacePath);
1102
1550
  const stagingPath = import_node_path.default.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
1103
1551
  import_node_fs.default.mkdirSync(stagingPath, { mode: 448 });
1104
1552
  const durabilityScopes = /* @__PURE__ */ new Map();
1553
+ const mergePlans = /* @__PURE__ */ new Map();
1554
+ const skippedMounts = [];
1105
1555
  try {
1106
1556
  const snapshotRoot = import_node_path.default.join(stagingPath, "mounts");
1107
1557
  import_node_fs.default.mkdirSync(snapshotRoot, { mode: 448 });
1108
1558
  const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
1109
- const manifestMounts = mounts.map((mount, index) => {
1559
+ const head = mergeBases && Object.keys(mergeBases).length > 0 ? revParse(workspacePath, "HEAD") : null;
1560
+ const manifestMounts = [];
1561
+ for (const mount of mounts) {
1110
1562
  const sourcePath = import_node_path.default.resolve(mount.sourcePath);
1111
1563
  const durabilityRootPath = import_node_path.default.resolve(mount.durabilityRootPath);
1112
1564
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
@@ -1118,45 +1570,89 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1118
1570
  if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
1119
1571
  throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
1120
1572
  }
1121
- const snapshotName = String(index);
1573
+ const snapshotName = String(manifestMounts.length);
1122
1574
  const snapshotPath = import_node_path.default.join(snapshotRoot, snapshotName);
1123
- import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
1124
1575
  let scope;
1125
- if (sourceStatus && mount.hydrateDeletionMode === "git") {
1576
+ const basis = mergeBases?.[mount.id];
1577
+ if (sourceStatus && mount.hydrateDeletionMode === "git" && basis && head) {
1126
1578
  const outerPath = workspaceMountOuterPath(workspacePath, mount);
1127
1579
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1128
1580
  trustedRoot: workspacePath,
1129
1581
  candidate: outerPath,
1130
1582
  label: `Workspace mount ${mount.id} outer source`
1131
1583
  });
1132
- const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({
1584
+ const prepared = (0, import_working_tree_mirror.prepareWorkingTreeMirror)({
1133
1585
  sourceRoot: outerPath,
1134
1586
  targetRoot: sourcePath,
1135
1587
  sourceMode: "all",
1136
1588
  deletionMode: "git",
1137
1589
  sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
1138
1590
  });
1591
+ const decision = (0, import_workspace_hydration_merge.decideWorkspaceHydrationMerge)({
1592
+ prepared,
1593
+ changes: workspaceSubtreeChanges(workspacePath, basis.projectedCommit, head, mount.workspaceRelativePath),
1594
+ projectedFiles: basis.projectedFiles,
1595
+ ...basis.readAtMs === void 0 ? {} : { readAtMs: basis.readAtMs },
1596
+ readBaseBlobs: (objectIds) => readWorkspaceBlobs(workspacePath, objectIds),
1597
+ readBinaryPaths: (relativePaths) => (0, import_workspace_hydration_merge.readHydrationBinaryPaths)({ checkoutRoot: prepared.targetRoot, outerRoot: prepared.sourceRoot, relativePaths })
1598
+ });
1599
+ if (decision.kind !== "apply") {
1600
+ skippedMounts.push({
1601
+ mountId: mount.id,
1602
+ reason: decision.kind === "conflict" ? "hydration_merge_conflict" : "hydration_target_changed",
1603
+ paths: decision.paths
1604
+ });
1605
+ continue;
1606
+ }
1607
+ import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
1608
+ const plan = (0, import_working_tree_mirror.planPreparedWorkingTreeMirror)(prepared, { selection: decision.selection, overrides: decision.overrides });
1139
1609
  scope = (0, import_working_tree_mirror.captureWorkingTreeMirrorPlan)({
1140
1610
  plan,
1141
1611
  targetRoot: sourcePath,
1142
1612
  snapshotRoot: snapshotPath,
1143
- // Same private-staging deferral as the complete snapshot below.
1144
1613
  durability: "deferred_private_staging"
1145
1614
  });
1146
- durabilityScopes.set(mount.id, [...plan.desired.keys()]);
1147
- } else if (sourceStatus) {
1148
- (0, import_working_tree_mirror.mirrorWorkingTree)({
1149
- sourceRoot: sourcePath,
1150
- targetRoot: snapshotPath,
1615
+ durabilityScopes.set(mount.id, [.../* @__PURE__ */ new Set([...plan.desired.keys(), ...decision.selection.remove])]);
1616
+ mergePlans.set(mount.id, { prepared, decision, plan, snapshotPath: import_node_path.default.join(transactionPath, "mounts", snapshotName), basis });
1617
+ } else if (sourceStatus && mount.hydrateDeletionMode === "git") {
1618
+ import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
1619
+ const outerPath = workspaceMountOuterPath(workspacePath, mount);
1620
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1621
+ trustedRoot: workspacePath,
1622
+ candidate: outerPath,
1623
+ label: `Workspace mount ${mount.id} outer source`
1624
+ });
1625
+ const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({
1626
+ sourceRoot: outerPath,
1627
+ targetRoot: sourcePath,
1151
1628
  sourceMode: "all",
1152
- deletionMode: "all",
1153
- // This tree is still private and the source authority is untouched.
1154
- // The single recursive barrier below makes the complete snapshot
1155
- // durable before its directory entry is published or hydration starts.
1629
+ deletionMode: "git",
1630
+ sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
1631
+ });
1632
+ scope = (0, import_working_tree_mirror.captureWorkingTreeMirrorPlan)({
1633
+ plan,
1634
+ targetRoot: sourcePath,
1635
+ snapshotRoot: snapshotPath,
1636
+ // Same private-staging deferral as the complete snapshot below.
1156
1637
  durability: "deferred_private_staging"
1157
1638
  });
1639
+ durabilityScopes.set(mount.id, [...plan.desired.keys()]);
1640
+ } else {
1641
+ import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
1642
+ if (sourceStatus) {
1643
+ (0, import_working_tree_mirror.mirrorWorkingTree)({
1644
+ sourceRoot: sourcePath,
1645
+ targetRoot: snapshotPath,
1646
+ sourceMode: "all",
1647
+ deletionMode: "all",
1648
+ // This tree is still private and the source authority is untouched.
1649
+ // The single recursive barrier below makes the complete snapshot
1650
+ // durable before its directory entry is published or hydration starts.
1651
+ durability: "deferred_private_staging"
1652
+ });
1653
+ }
1158
1654
  }
1159
- return {
1655
+ manifestMounts.push({
1160
1656
  id: mount.id,
1161
1657
  incarnationKey: mount.hydrationIncarnationKey,
1162
1658
  sourcePath,
@@ -1164,23 +1660,27 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1164
1660
  workspaceRelativePath: mount.workspaceRelativePath,
1165
1661
  existed: Boolean(sourceStatus),
1166
1662
  snapshotName,
1167
- ...scope ? { scope } : {}
1168
- };
1169
- });
1663
+ ...scope ? { scope } : {},
1664
+ recovery: mergePlans.has(mount.id) ? "guarded" : "snapshot"
1665
+ });
1666
+ }
1667
+ const effectiveTargetReceipt = revertReceiptMounts(targetReceipt, receiptBefore, new Set(skippedMounts.map(({ mountId }) => mountId)));
1668
+ const guarded = mergePlans.size > 0;
1170
1669
  const manifest = {
1171
- // Older workers reject version 4, so a scoped snapshot can never be
1172
- // replayed as a complete copy after a downgrade.
1173
- version: manifestMounts.some((mount) => mount.scope) ? 4 : 3,
1174
- targetReceipt,
1670
+ // Older workers reject guarded transactions rather than replaying them
1671
+ // through an unguarded snapshot restore after a downgrade.
1672
+ version: guarded ? 5 : manifestMounts.some((mount) => mount.scope) ? 4 : 3,
1673
+ ...guarded ? { transactionId: crypto.randomUUID() } : {},
1674
+ targetReceipt: effectiveTargetReceipt,
1175
1675
  receiptBefore,
1176
- mounts: manifestMounts
1676
+ mounts: guarded ? manifestMounts : manifestMounts.map(({ recovery: _recovery, ...mount }) => mount)
1177
1677
  };
1178
1678
  writePrivateFileDurably(import_node_path.default.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
1179
1679
  `);
1180
1680
  fsyncTree(stagingPath);
1181
1681
  import_node_fs.default.renameSync(stagingPath, transactionPath);
1182
1682
  fsyncDirectory(stateDirectory);
1183
- return { manifest, durabilityScopes };
1683
+ return { manifest, durabilityScopes, mergePlans, skippedMounts, targetReceipt: effectiveTargetReceipt };
1184
1684
  } catch (error) {
1185
1685
  import_node_fs.default.rmSync(stagingPath, { recursive: true, force: true });
1186
1686
  fsyncDirectory(stateDirectory);
@@ -1189,7 +1689,9 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1189
1689
  }
1190
1690
  function markHydrationTransactionDurable(workspacePath) {
1191
1691
  const transactionPath = hydrationTransactionPath(workspacePath);
1192
- writePrivateFileDurably(import_node_path.default.join(transactionPath, "committed"), "committed\n");
1692
+ const temporaryPath = import_node_path.default.join(transactionPath, `.committed.${crypto.randomUUID()}.tmp`);
1693
+ writePrivateFileDurably(temporaryPath, "committed\n");
1694
+ import_node_fs.default.renameSync(temporaryPath, import_node_path.default.join(transactionPath, "committed"));
1193
1695
  fsyncDirectory(transactionPath);
1194
1696
  }
1195
1697
  function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
@@ -1341,6 +1843,22 @@ async function configureWorkspaceRepository(input) {
1341
1843
  "configure durable workspace commits and references"
1342
1844
  );
1343
1845
  await gitAsync(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
1846
+ const attributesPath = import_node_path.default.join(input.workspacePath, ".git", "info", "attributes");
1847
+ const attributes = [
1848
+ "# r5d workspace clone: checkout bytes are carried verbatim; no conversion or filter may change what projection reads or hydration writes.",
1849
+ "* -text -eol -filter -ident -working-tree-encoding",
1850
+ ""
1851
+ ].join("\n");
1852
+ let currentAttributes = null;
1853
+ try {
1854
+ currentAttributes = await import_node_fs.default.promises.readFile(attributesPath, "utf8");
1855
+ } catch (error) {
1856
+ if (error.code !== "ENOENT") throw error;
1857
+ }
1858
+ if (currentAttributes !== attributes) {
1859
+ await import_node_fs.default.promises.mkdir(import_node_path.default.dirname(attributesPath), { recursive: true });
1860
+ await import_node_fs.default.promises.writeFile(attributesPath, attributes, "utf8");
1861
+ }
1344
1862
  }
1345
1863
  async function configureExistingWorkspaceGitForRemediation(input) {
1346
1864
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
@@ -1472,7 +1990,7 @@ function activeMounts(mounts) {
1472
1990
  }
1473
1991
  return { active, tombstones, skipped };
1474
1992
  }
1475
- function mirrorMountsToWorkspace(workspacePath, mounts) {
1993
+ function mirrorMountsToWorkspace(workspacePath, mounts, projectedFiles) {
1476
1994
  const projected = [];
1477
1995
  for (const mount of mounts) {
1478
1996
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
@@ -1489,7 +2007,8 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
1489
2007
  sourceRoot: mount.sourcePath,
1490
2008
  targetRoot: import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1491
2009
  sourceMode: mount.sourceMode,
1492
- deletionMode: "all"
2010
+ deletionMode: "all",
2011
+ ...projectedFiles ? { projected: projectedFiles } : {}
1493
2012
  });
1494
2013
  projected.push({ mount, gitlinks: mirrored.gitlinks });
1495
2014
  }
@@ -1600,11 +2119,60 @@ function outerSubtreeGitlinks(workspacePath, mount) {
1600
2119
  function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1601
2120
  const hydratedMountIds = [];
1602
2121
  const skippedMountIds = [];
2122
+ const mergeSkips = [];
2123
+ const rollbackScopes = /* @__PURE__ */ new Map();
2124
+ const preBlobs = {};
1603
2125
  for (const mount of mounts) {
1604
2126
  if (!options.ignoreBusy && mount.busy?.()) {
1605
2127
  skippedMountIds.push(mount.id);
1606
2128
  continue;
1607
2129
  }
2130
+ const mergePlan = options.mergePlans?.get(mount.id);
2131
+ if (mergePlan) {
2132
+ const journal = { entries: [] };
2133
+ const runtimeJournal = { journal, snapshotPath: mergePlan.snapshotPath };
2134
+ options.journals?.set(mount.id, runtimeJournal);
2135
+ try {
2136
+ (0, import_working_tree_mirror.applyWorkingTreeMirror)(mergePlan.prepared, {
2137
+ selection: mergePlan.decision.selection,
2138
+ guards: {
2139
+ expectations: mergePlan.decision.expectations,
2140
+ overrides: mergePlan.decision.overrides,
2141
+ preBlobs: mergePlan.decision.preBlobs,
2142
+ journal
2143
+ }
2144
+ });
2145
+ } catch (error) {
2146
+ if (!(error instanceof import_working_tree_mirror.WorkingTreeTargetChangedError)) throw error;
2147
+ const rolledBack = (0, import_working_tree_mirror.rollbackWorkingTreeMirrorJournal)({
2148
+ journal,
2149
+ targetRoot: mount.sourcePath,
2150
+ snapshotRoot: mergePlan.snapshotPath
2151
+ });
2152
+ runtimeJournal.rolledBack = rolledBack;
2153
+ const retainedPaths = [
2154
+ ...new Set([...error.retained, ...rolledBack.retained ?? []].map(({ path: retainedPath }) => retainedPath))
2155
+ ].sort();
2156
+ if (retainedPaths.length > 0) {
2157
+ process.stderr.write(
2158
+ `[r5d-worker] hydration preserved displaced writer entries for mount ${mount.id}: ${retainedPaths.join(", ")}
2159
+ `
2160
+ );
2161
+ }
2162
+ options.onMergeRollback?.(mount, rolledBack);
2163
+ mergeSkips.push({
2164
+ mountId: mount.id,
2165
+ reason: "hydration_target_changed",
2166
+ paths: [.../* @__PURE__ */ new Set([error.relativePath, ...rolledBack.abandoned])].sort(),
2167
+ ...retainedPaths.length > 0 ? { retainedPaths } : {}
2168
+ });
2169
+ rollbackScopes.set(mount.id, rolledBack.paths);
2170
+ continue;
2171
+ }
2172
+ hydratedMountIds.push(mount.id);
2173
+ preBlobs[mount.id] = Object.fromEntries(mergePlan.decision.preBlobs);
2174
+ continue;
2175
+ }
1608
2176
  const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
1609
2177
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1610
2178
  trustedRoot: workspacePath,
@@ -1650,7 +2218,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1650
2218
  });
1651
2219
  hydratedMountIds.push(mount.id);
1652
2220
  }
1653
- return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
2221
+ return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort(), mergeSkips, rollbackScopes, preBlobs };
1654
2222
  }
1655
2223
  async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
1656
2224
  const candidates = mounts.flatMap((mount) => {
@@ -1710,7 +2278,7 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1710
2278
  };
1711
2279
  const noopCandidate = computeSelection();
1712
2280
  if (noopCandidate.hydrationMounts.length === 0 && noopCandidate.advancedDurabilityMounts.length === 0 && noopCandidate.targetReceipt && lstatIfExists(hydrationTransactionPath(workspacePath)) === null && workspaceHydrationReceiptsEqual(receiptBeforeTransaction, noopCandidate.targetReceipt) && await hydratedWorkspaceReceiptFileIsV3(workspacePath) && await workspaceBasisRefsAreCurrentAsync(workspacePath, noopCandidate.targetReceipt)) {
1713
- return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
2281
+ return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds, mergeSkips: [] };
1714
2282
  }
1715
2283
  const hooks = input.hydrationHooks;
1716
2284
  const context = {
@@ -1725,13 +2293,30 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1725
2293
  const heldMountIds = [...new Set([...hydrationMounts, ...advancedDurabilityMounts].map(({ id }) => id))];
1726
2294
  const releaseHold = hooks?.holdMounts(heldMountIds, `hydration transaction ${context.transactionId}`);
1727
2295
  try {
2296
+ const mergeBases = {};
2297
+ for (const mount of hydrationMounts) {
2298
+ const basis = input.mergeBases?.get(mount.id);
2299
+ if (basis) mergeBases[mount.id] = basis;
2300
+ }
1728
2301
  const hydration = await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("hydration_transaction", {
1729
2302
  workspacePath,
1730
2303
  hydrationMounts: hydrationMounts.map(workspaceGitMountData),
1731
2304
  advancedDurabilityMounts: advancedDurabilityMounts.map(workspaceGitMountData),
1732
- targetReceipt
2305
+ targetReceipt,
2306
+ ...Object.keys(mergeBases).length > 0 ? { mergeBases } : {}
1733
2307
  });
1734
- return { hydratedMountIds: hydration.hydratedMountIds, skippedMountIds: selection.skippedMountIds };
2308
+ const ledger = hydrationPreBlobLedger(workspacePath);
2309
+ for (const mount of hydrationMounts) {
2310
+ if (!hydration.hydratedMountIds.includes(mount.id)) continue;
2311
+ const preBlobs = mergeBases[mount.id] ? hydration.hydratedPreBlobs[mount.id] : void 0;
2312
+ if (preBlobs) ledger.record(mount, preBlobs);
2313
+ else ledger.clearMount(mount);
2314
+ }
2315
+ return {
2316
+ hydratedMountIds: hydration.hydratedMountIds,
2317
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selection.skippedMountIds, ...hydration.skippedMounts.map(({ mountId }) => mountId)])].sort(),
2318
+ mergeSkips: hydration.skippedMounts
2319
+ };
1735
2320
  } finally {
1736
2321
  releaseHold?.();
1737
2322
  }
@@ -1741,28 +2326,90 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1741
2326
  }
1742
2327
  function runHydrationTransactionJob(input) {
1743
2328
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1744
- const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = input;
1745
- const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
2329
+ const { hydrationMounts, advancedDurabilityMounts } = input;
2330
+ const begun = beginHydrationTransaction(workspacePath, hydrationMounts, input.targetReceipt, input.mergeBases);
2331
+ const { durabilityScopes, mergePlans, skippedMounts } = begun;
2332
+ let manifest = begun.manifest;
2333
+ let targetReceipt = begun.targetReceipt;
2334
+ const skippedIds = new Set(skippedMounts.map(({ mountId }) => mountId));
2335
+ const journals = /* @__PURE__ */ new Map();
2336
+ const excludeRolledBackMount = (mount, rollback) => {
2337
+ const settledMount = manifest.mounts.find(({ id }) => id === mount.id);
2338
+ if (!settledMount) return;
2339
+ fsyncHydratedWorkspaceMount(mount, rollback.paths);
2340
+ fsyncHydrationRetainedEvidence(mount);
2341
+ if (rollback.retained?.length) {
2342
+ process.stderr.write(
2343
+ `[r5d-worker] hydration rollback retained writer entries for mount ${mount.id}: ${rollback.retained.map(({ path: retainedPath }) => retainedPath).join(", ")}
2344
+ `
2345
+ );
2346
+ }
2347
+ const nextReceipt = revertReceiptMounts(targetReceipt, manifest.receiptBefore, /* @__PURE__ */ new Set([mount.id]));
2348
+ const nextManifest = {
2349
+ ...manifest,
2350
+ targetReceipt: nextReceipt,
2351
+ mounts: manifest.mounts.filter(({ id }) => id !== mount.id),
2352
+ preservedMounts: [...manifest.preservedMounts ?? [], settledMount]
2353
+ };
2354
+ rewriteHydrationTransactionManifest(workspacePath, nextManifest);
2355
+ manifest = nextManifest;
2356
+ targetReceipt = nextReceipt;
2357
+ };
1746
2358
  try {
1747
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1748
- const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
2359
+ const activeMounts2 = hydrationMounts.filter(({ id }) => !skippedIds.has(id));
2360
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, activeMounts2, {
2361
+ ignoreBusy: true,
2362
+ mergePlans,
2363
+ journals,
2364
+ onMergeRollback: excludeRolledBackMount
2365
+ });
2366
+ if (hydration.mergeSkips.length > 0) {
2367
+ for (const skip of hydration.mergeSkips) {
2368
+ skippedMounts.push(skip);
2369
+ skippedIds.add(skip.mountId);
2370
+ }
2371
+ }
2372
+ const expectedHydratedIds = activeMounts2.map(({ id }) => id).filter((id) => !skippedIds.has(id)).sort();
1749
2373
  if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1750
2374
  throw new Error("Workspace hydration did not include every required mount");
1751
2375
  }
1752
- fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
2376
+ fsyncHydratedWorkspaceMounts(
2377
+ advancedDurabilityMounts.filter(({ id }) => !skippedIds.has(id)),
2378
+ durabilityScopes
2379
+ );
1753
2380
  markHydrationTransactionDurable(workspacePath);
1754
2381
  if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1755
2382
  removeHydrationTransaction(workspacePath);
1756
- return { hydratedMountIds: hydration.hydratedMountIds };
2383
+ return { hydratedMountIds: hydration.hydratedMountIds, skippedMounts, hydratedPreBlobs: hydration.preBlobs };
1757
2384
  } catch (error) {
1758
2385
  const transactionPath = hydrationTransactionPath(workspacePath);
1759
2386
  if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1760
2387
  throw error;
1761
2388
  }
1762
2389
  try {
2390
+ for (const [mountId, mergePlan] of mergePlans) {
2391
+ const mount = hydrationMounts.find(({ id }) => id === mountId);
2392
+ if (!mount || !manifest.mounts.some(({ id }) => id === mountId)) continue;
2393
+ const runtimeJournal = journals.get(mountId);
2394
+ const rolledBack = runtimeJournal ? runtimeJournal.rolledBack ?? (0, import_working_tree_mirror.rollbackWorkingTreeMirrorJournal)({
2395
+ journal: runtimeJournal.journal,
2396
+ targetRoot: mount.sourcePath,
2397
+ snapshotRoot: mergePlan.snapshotPath
2398
+ }) : { paths: [], abandoned: [] };
2399
+ if (runtimeJournal) runtimeJournal.rolledBack = rolledBack;
2400
+ excludeRolledBackMount(mount, rolledBack);
2401
+ }
1763
2402
  restoreHydrationTransaction(workspacePath, manifest);
1764
2403
  removeHydrationTransaction(workspacePath);
1765
2404
  } catch (restoreError) {
2405
+ const pendingManifest = parseHydrationTransactionManifest(transactionPath);
2406
+ if (pendingManifest.mounts.some(({ recovery }) => recovery === "guarded")) {
2407
+ throw hydrationRecoveryRequiredError(
2408
+ workspacePath,
2409
+ pendingManifest,
2410
+ new AggregateError([error, restoreError], "Guarded hydration rollback did not settle")
2411
+ );
2412
+ }
1766
2413
  throw new AggregateError(
1767
2414
  [error, restoreError],
1768
2415
  `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
@@ -1824,6 +2471,7 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
1824
2471
  });
1825
2472
  const currentHead = await revParseAsync(workspacePath, "HEAD");
1826
2473
  const currentReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
2474
+ const preservedBases = await readPreservedHydrationBasesAsync(workspacePath);
1827
2475
  if (!currentHead) return;
1828
2476
  const outerHeadContainsMount = (mount) => tryGitAsync(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
1829
2477
  const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
@@ -1833,7 +2481,7 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
1833
2481
  if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
1834
2482
  const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1835
2483
  const recoveryMounts = uncoveredBasisMounts.filter(
1836
- (mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
2484
+ (mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id) && workspaceHydrationReceiptMountBasisHead(preservedBases, mount) !== workspaceHydrationReceiptMountBasisHead(currentReceipt, mount)
1837
2485
  );
1838
2486
  const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
1839
2487
  const hydrationTargets = [];
@@ -1863,6 +2511,7 @@ async function resetWorkspaceGit(input) {
1863
2511
  if (busyResetMountIds.length > 0) {
1864
2512
  throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
1865
2513
  }
2514
+ hydrationPreBlobLedger(workspacePath).clear();
1866
2515
  const initial = await ensureWorkspaceGitClone({ ...input, workspacePath });
1867
2516
  await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("hydration_recovery", { workspacePath, preserveResolutionInProgress: false });
1868
2517
  const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
@@ -2206,6 +2855,24 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2206
2855
  const mergedProjectionMounts = [];
2207
2856
  const projectionSkippedIds = /* @__PURE__ */ new Set();
2208
2857
  let projectionWarning;
2858
+ const projectedMountFiles = /* @__PURE__ */ new Map();
2859
+ const mergedProjectionBases = /* @__PURE__ */ new Map();
2860
+ const projectedMountBases = /* @__PURE__ */ new Map();
2861
+ const mountSkipReasons = {};
2862
+ const staleRewriteLedger = hydrationPreBlobLedger(workspacePath);
2863
+ const staleRewriteBlobsFor = (mount) => {
2864
+ const preBlobs = staleRewriteLedger.preBlobs(mount);
2865
+ return preBlobs.size > 0 ? Object.fromEntries(preBlobs) : void 0;
2866
+ };
2867
+ const recordStaleRewrite = (mount, paths) => {
2868
+ projectionSkippedIds.add(mount.id);
2869
+ mountSkipReasons[mount.id] = { reason: "stale_rewrite_detected", paths: [...paths] };
2870
+ const holders = mount.describeHolders?.();
2871
+ process.stderr.write(
2872
+ `[r5d-worker] stale rewrite detected in mount ${mount.id}: ${(0, import_workspace_hydration_merge.describeHydrationSkipPaths)(mount.sourcePath, paths)} hold exactly the bytes the last hydration replaced; the mount stays pinned at its recorded basis until they change${holders ? `; holders: ${holders}` : ""}
2873
+ `
2874
+ );
2875
+ };
2209
2876
  const refreshCycleSkippedMounts = (mounts = input.mounts) => {
2210
2877
  for (const mount of mounts) {
2211
2878
  const projectionToken = projectionMutationTokens.get(mount.id);
@@ -2228,7 +2895,8 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2228
2895
  const token = projectionMutationTokens.get(id);
2229
2896
  return token === void 0 ? [] : [[id, token]];
2230
2897
  })
2231
- )
2898
+ ),
2899
+ ...Object.keys(mountSkipReasons).length > 0 ? { mountSkipReasons: { ...mountSkipReasons } } : {}
2232
2900
  };
2233
2901
  };
2234
2902
  const mountProjectionIsCurrent = (mount) => mount.lastProjectedMutationToken !== void 0 && !mount.deleteWhenSourceMissing && mount.preserveLocalOnHydrationBasisChange !== true && projectionBasisHead !== null && workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount) === projectionBasisHead && projectionMutationTokens.get(mount.id) === mount.lastProjectedMutationToken && mount.mutationToken?.() === mount.lastProjectedMutationToken;
@@ -2258,6 +2926,7 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2258
2926
  const releaseMergeHold = input.hydrationHooks?.holdMounts([mount.id], `merge projection of mount ${mount.id}`);
2259
2927
  let merged;
2260
2928
  try {
2929
+ const staleRewriteBlobs = staleRewriteBlobsFor(mount);
2261
2930
  merged = await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("projection_merge", {
2262
2931
  workspacePath,
2263
2932
  mount: {
@@ -2268,11 +2937,22 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2268
2937
  },
2269
2938
  basisHead,
2270
2939
  currentHead: projectionBasisHead,
2271
- attemptId
2940
+ attemptId,
2941
+ ...staleRewriteBlobs ? { staleRewriteBlobs } : {}
2272
2942
  });
2273
2943
  } finally {
2274
2944
  releaseMergeHold?.();
2275
2945
  }
2946
+ if (merged.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, merged.staleRewriteCleared);
2947
+ if (merged.kind === "stale_rewrite") {
2948
+ recordStaleRewrite(mount, merged.paths);
2949
+ continue;
2950
+ }
2951
+ mergedProjectionBases.set(mount.id, {
2952
+ projectedCommit: merged.oursCommit,
2953
+ projectedFiles: merged.projectedFiles,
2954
+ readAtMs: merged.readAtMs
2955
+ });
2276
2956
  if (merged.kind === "conflict") {
2277
2957
  const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
2278
2958
  return {
@@ -2325,17 +3005,44 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2325
3005
  currentHeadBeforeHydration,
2326
3006
  uncoveredCompletionMounts
2327
3007
  ) : /* @__PURE__ */ new Set();
3008
+ const hydrationMounts = uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id));
3009
+ const mergeBases = /* @__PURE__ */ new Map();
3010
+ if (workspaceMergeHydrationEnabled()) {
3011
+ for (const mount of hydrationMounts) {
3012
+ const basis = projectedMountBases.get(mount.id);
3013
+ if (basis) mergeBases.set(mount.id, basis);
3014
+ }
3015
+ }
2328
3016
  const hydration = await hydrateWorkspaceGitMountsTransactionally({
2329
3017
  workspacePath,
2330
- mounts: uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id)),
3018
+ mounts: hydrationMounts,
2331
3019
  durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2332
3020
  carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
2333
3021
  receiptMounts: completionReceiptMounts,
2334
3022
  requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2335
3023
  recordCurrentHead: true,
2336
- hydrationHooks: input.hydrationHooks
3024
+ hydrationHooks: input.hydrationHooks,
3025
+ ...mergeBases.size > 0 ? { mergeBases } : {}
2337
3026
  });
2338
3027
  for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
3028
+ if (currentHeadBeforeHydration) {
3029
+ for (const id of hydration.hydratedMountIds) {
3030
+ if (mergeBases.has(id)) projectedMountBases.set(id, { projectedCommit: currentHeadBeforeHydration, projectedFiles: null });
3031
+ }
3032
+ }
3033
+ for (const skip of hydration.mergeSkips) {
3034
+ const mount = hydrationMounts.find(({ id }) => id === skip.mountId);
3035
+ const basis = mount ? workspaceHydrationReceiptMountBasisHead(currentReceiptBeforeHydration, mount) : null;
3036
+ mountSkipReasons[skip.mountId] = {
3037
+ reason: skip.reason,
3038
+ paths: skip.paths,
3039
+ ...skip.retainedPaths?.length ? { retainedPaths: skip.retainedPaths } : {}
3040
+ };
3041
+ process.stderr.write(
3042
+ `[r5d-worker] merge hydration pinned mount ${skip.mountId} at ${basis ?? "its recorded basis"} (${skip.reason}): ${skip.reason === "hydration_merge_conflict" ? "writes that landed after the projection read conflict with the inbound change at" : "the checkout changed while it was being hydrated at"} ${(0, import_workspace_hydration_merge.describeHydrationSkipPaths)(mount?.sourcePath ?? skip.mountId, skip.paths)}; the next projection merges it from that basis${skip.retainedPaths?.length ? `; displaced writer entries preserved at ${skip.retainedPaths.join(", ")}` : ""}
3043
+ `
3044
+ );
3045
+ }
2339
3046
  refreshCycleSkippedMounts(completionReceiptMounts);
2340
3047
  if (requireCurrentHeadReceipt) {
2341
3048
  const currentHead = await revParseAsync(workspacePath, "HEAD");
@@ -2376,10 +3083,11 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2376
3083
  };
2377
3084
  const projectedFastMounts = [];
2378
3085
  const currentProjectionMounts = [];
3086
+ const selectedActiveIdSet = new Set(selected.active.map(({ id }) => id));
2379
3087
  if (!input.skipMountMirror) {
2380
3088
  await beginWorkspaceCheckoutTransition(workspacePath);
2381
3089
  try {
2382
- const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
3090
+ const selectedActiveIds = selectedActiveIdSet;
2383
3091
  const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
2384
3092
  const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
2385
3093
  for (const mount of fastActiveMounts) {
@@ -2392,15 +3100,27 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2392
3100
  continue;
2393
3101
  }
2394
3102
  const releaseMirrorHold = input.hydrationHooks?.holdMounts([mount.id], `projection of mount ${mount.id}`);
3103
+ let staleRewritePaths;
2395
3104
  try {
3105
+ const staleRewriteBlobs = staleRewriteBlobsFor(mount);
2396
3106
  const mirrored = await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("projection_mirror", {
2397
3107
  workspacePath,
2398
- mount: workspaceGitMountData(mount)
3108
+ mount: workspaceGitMountData(mount),
3109
+ ...staleRewriteBlobs ? { staleRewriteBlobs } : {}
2399
3110
  });
2400
- projectedGitlinks.push({ mount, gitlinks: mirrored.gitlinks });
3111
+ staleRewritePaths = mirrored.staleRewritePaths;
3112
+ if (mirrored.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, mirrored.staleRewriteCleared);
3113
+ if (staleRewritePaths.length === 0) {
3114
+ projectedGitlinks.push({ mount, gitlinks: mirrored.gitlinks });
3115
+ projectedMountFiles.set(mount.id, { files: mirrored.projectedFiles, readAtMs: mirrored.readAtMs });
3116
+ }
2401
3117
  } finally {
2402
3118
  releaseMirrorHold?.();
2403
3119
  }
3120
+ if (staleRewritePaths.length > 0) {
3121
+ recordStaleRewrite(mount, staleRewritePaths);
3122
+ continue;
3123
+ }
2404
3124
  projectedFastMounts.push(mount);
2405
3125
  }
2406
3126
  await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("projection_outer_apply", {
@@ -2427,6 +3147,18 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2427
3147
  if (!input.skipMountMirror) {
2428
3148
  const projectedHead = await revParseAsync(workspacePath, "HEAD");
2429
3149
  if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
3150
+ for (const mount of projectedFastMounts) {
3151
+ if (!selectedActiveIdSet.has(mount.id)) continue;
3152
+ const read = projectedMountFiles.get(mount.id);
3153
+ projectedMountBases.set(mount.id, {
3154
+ projectedCommit: projectedHead,
3155
+ projectedFiles: read?.files ?? null,
3156
+ ...read ? { readAtMs: read.readAtMs } : {}
3157
+ });
3158
+ }
3159
+ for (const mount of currentProjectionMounts)
3160
+ projectedMountBases.set(mount.id, { projectedCommit: projectedHead, projectedFiles: null });
3161
+ for (const [mountId, basis] of mergedProjectionBases) projectedMountBases.set(mountId, basis);
2430
3162
  const targetProjectionReceipt = receiptWithMountsAtHead(
2431
3163
  projectionBasisReceipt,
2432
3164
  projectedHead,
@@ -2606,9 +3338,52 @@ ${push.stdout}`)) {
2606
3338
  }
2607
3339
  throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
2608
3340
  }
3341
+ function detectStaleRewrites(workspacePath, mount, staleRewriteBlobs) {
3342
+ const outerRoot = workspaceMountOuterPath(workspacePath, mount);
3343
+ const stale = [];
3344
+ const cleared = [];
3345
+ for (const [relativePath, blob] of Object.entries(staleRewriteBlobs)) {
3346
+ const algorithm = (0, import_git_blob_hash.gitObjectHashAlgorithmFor)(blob);
3347
+ if (!algorithm) continue;
3348
+ const source = (0, import_working_tree_mirror.inspectWorkingTreePath)(mount.sourcePath, relativePath);
3349
+ if (source.kind !== "entry" || !source.stat.isFile()) {
3350
+ cleared.push(relativePath);
3351
+ continue;
3352
+ }
3353
+ const content = import_node_fs.default.readFileSync(source.absolutePath);
3354
+ const outer = (0, import_working_tree_mirror.inspectWorkingTreePath)(outerRoot, relativePath);
3355
+ const outerEqual = outer.kind === "entry" && outer.stat.isFile() && import_node_fs.default.readFileSync(outer.absolutePath).equals(content);
3356
+ if ((0, import_git_blob_hash.gitBlobHash)(content, algorithm) !== blob) {
3357
+ if (!outerEqual) cleared.push(relativePath);
3358
+ continue;
3359
+ }
3360
+ if (outerEqual) {
3361
+ cleared.push(relativePath);
3362
+ continue;
3363
+ }
3364
+ stale.push(relativePath);
3365
+ }
3366
+ return { stale: stale.sort(), cleared: cleared.sort() };
3367
+ }
2609
3368
  function runProjectionMirrorJob(input) {
2610
- const projected = mirrorMountsToWorkspace(import_node_path.default.resolve(input.workspacePath), [input.mount]);
2611
- return { gitlinks: projected[0]?.gitlinks ?? [] };
3369
+ const workspacePath = import_node_path.default.resolve(input.workspacePath);
3370
+ let staleRewriteCleared = [];
3371
+ if (input.staleRewriteBlobs) {
3372
+ const detected = detectStaleRewrites(workspacePath, input.mount, input.staleRewriteBlobs);
3373
+ staleRewriteCleared = detected.cleared;
3374
+ if (detected.stale.length > 0) {
3375
+ return { gitlinks: [], projectedFiles: {}, readAtMs: Date.now(), staleRewritePaths: detected.stale, staleRewriteCleared };
3376
+ }
3377
+ }
3378
+ const projectedFiles = /* @__PURE__ */ new Map();
3379
+ const projected = mirrorMountsToWorkspace(workspacePath, [input.mount], projectedFiles);
3380
+ return {
3381
+ gitlinks: projected[0]?.gitlinks ?? [],
3382
+ projectedFiles: Object.fromEntries(projectedFiles),
3383
+ readAtMs: Date.now(),
3384
+ staleRewritePaths: [],
3385
+ staleRewriteCleared
3386
+ };
2612
3387
  }
2613
3388
  function runProjectionOuterApplyJob(input) {
2614
3389
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
@@ -2626,11 +3401,16 @@ function runProjectionReceiptJob(input) {
2626
3401
  return null;
2627
3402
  }
2628
3403
  const workspaceGitSyncTestHarness = {
3404
+ beginHydrationTransaction,
2629
3405
  commandArgs: gitCommandArgs,
2630
3406
  workspaceCloneCommandArgs,
2631
3407
  configureWorkspaceRepository,
2632
3408
  mirrorMountsToWorkspace,
2633
- hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts
3409
+ hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts,
3410
+ hydrationPreBlobLedger,
3411
+ resetHydrationPreBlobLedgers() {
3412
+ hydrationPreBlobLedgers.clear();
3413
+ }
2634
3414
  };
2635
3415
  // Annotate the CommonJS export names for ESM import in node:
2636
3416
  0 && (module.exports = {
@@ -2639,7 +3419,10 @@ const workspaceGitSyncTestHarness = {
2639
3419
  WORKSPACE_GIT_CHECKOUT_DURABILITY,
2640
3420
  WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
2641
3421
  WORKSPACE_GIT_HYDRATED_RECEIPT,
3422
+ WORKSPACE_GIT_HYDRATION_RECOVERIES,
2642
3423
  WORKSPACE_GIT_HYDRATION_TRANSACTION,
3424
+ WORKSPACE_MERGE_HYDRATION_SWITCH,
3425
+ WorkspaceHydrationRecoveryRequiredError,
2643
3426
  WorkspaceRemediationAncestryError,
2644
3427
  configureExistingWorkspaceGitForRemediation,
2645
3428
  ensureWorkspaceGitClone,
@@ -2649,7 +3432,9 @@ const workspaceGitSyncTestHarness = {
2649
3432
  runCheckoutDurabilityRecoveryJob,
2650
3433
  runCheckoutTransitionBeginJob,
2651
3434
  runCheckoutTransitionCompleteJob,
3435
+ runHydrationPreserveInterruptedJob,
2652
3436
  runHydrationRawJob,
3437
+ runHydrationRecoveryInspectJob,
2653
3438
  runHydrationRecoveryJob,
2654
3439
  runHydrationTransactionJob,
2655
3440
  runOuterCheckoutFsyncJob,