@ricsam/r5d-worker 0.0.97 → 0.0.100

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.
@@ -6,7 +6,15 @@ import {
6
6
  mergeWorkspaceProjectionMount,
7
7
  workspaceMergeProjectionSupport
8
8
  } from "./workspace-merge-projection.mjs";
9
- import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
9
+ import {
10
+ captureWorkingTreeMirrorPlan,
11
+ fsyncWorkingTreePaths,
12
+ inspectWorkingTree,
13
+ mirrorWorkingTree,
14
+ normalizeWorkingTreeRelativePath,
15
+ planWorkingTreeMirror,
16
+ restoreWorkingTreeMirrorScope
17
+ } from "./working-tree-mirror.mjs";
10
18
  import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
11
19
  const WORKSPACE_GIT_BRANCH = "main";
12
20
  const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
@@ -909,7 +917,7 @@ function parseHydrationTransactionManifest(transactionPath) {
909
917
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
910
918
  }
911
919
  const candidate = parsed;
912
- if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
920
+ if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 || !Array.isArray(candidate.mounts)) {
913
921
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
914
922
  }
915
923
  const parseReceipt = (rawReceipt) => {
@@ -922,6 +930,25 @@ function parseHydrationTransactionManifest(transactionPath) {
922
930
  };
923
931
  const targetReceipt = parseReceipt(candidate.targetReceipt);
924
932
  const receiptBefore = parseReceipt(candidate.receiptBefore);
933
+ const parseScope = (rawScope, existed) => {
934
+ if (rawScope === void 0) return void 0;
935
+ if (candidate.version !== 4 || !existed || !rawScope || typeof rawScope !== "object") {
936
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
937
+ }
938
+ const parsePaths = (rawPaths) => {
939
+ if (!Array.isArray(rawPaths)) throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
940
+ const seen = /* @__PURE__ */ new Set();
941
+ return rawPaths.map((rawPath) => {
942
+ if (typeof rawPath !== "string" || normalizeWorkingTreeRelativePath(rawPath) !== rawPath || seen.has(rawPath)) {
943
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
944
+ }
945
+ seen.add(rawPath);
946
+ return rawPath;
947
+ });
948
+ };
949
+ const { absent, trees } = rawScope;
950
+ return { absent: parsePaths(absent), trees: parsePaths(trees) };
951
+ };
925
952
  const seenIds = /* @__PURE__ */ new Set();
926
953
  const seenSnapshots = /* @__PURE__ */ new Set();
927
954
  const mounts = candidate.mounts.map((rawMount) => {
@@ -938,10 +965,20 @@ function parseHydrationTransactionManifest(transactionPath) {
938
965
  }
939
966
  seenIds.add(mount.id);
940
967
  seenSnapshots.add(mount.snapshotName);
941
- return mount;
968
+ const scope = parseScope(rawMount.scope, mount.existed);
969
+ return {
970
+ id: mount.id,
971
+ incarnationKey: mount.incarnationKey,
972
+ sourcePath: mount.sourcePath,
973
+ durabilityRootPath: mount.durabilityRootPath,
974
+ workspaceRelativePath: mount.workspaceRelativePath,
975
+ existed: mount.existed,
976
+ snapshotName: mount.snapshotName,
977
+ ...scope ? { scope } : {}
978
+ };
942
979
  });
943
980
  return {
944
- version: 3,
981
+ version: candidate.version === 4 ? 4 : 3,
945
982
  targetReceipt,
946
983
  receiptBefore,
947
984
  mounts
@@ -963,6 +1000,7 @@ function hydrationTransactionCommitted(transactionPath) {
963
1000
  }
964
1001
  function restoreHydrationTransaction(workspacePath, manifest) {
965
1002
  const transactionPath = hydrationTransactionPath(workspacePath);
1003
+ const durabilityScopes = /* @__PURE__ */ new Map();
966
1004
  for (const mount of manifest.mounts) {
967
1005
  assertManagedDirectoryPath({
968
1006
  trustedRoot: mount.durabilityRootPath,
@@ -978,6 +1016,11 @@ function restoreHydrationTransaction(workspacePath, manifest) {
978
1016
  if (!snapshotStatus.isDirectory() || snapshotStatus.isSymbolicLink()) {
979
1017
  throw new Error(`Workspace hydration snapshot is not a regular directory: ${snapshotPath}`);
980
1018
  }
1019
+ if (mount.scope) {
1020
+ const restored = restoreWorkingTreeMirrorScope({ snapshotRoot: snapshotPath, targetRoot: mount.sourcePath, scope: mount.scope });
1021
+ durabilityScopes.set(mount.id, restored.paths);
1022
+ continue;
1023
+ }
981
1024
  mirrorWorkingTree({ sourceRoot: snapshotPath, targetRoot: mount.sourcePath, sourceMode: "all", deletionMode: "all" });
982
1025
  }
983
1026
  fsyncHydratedWorkspaceMounts(
@@ -986,7 +1029,8 @@ function restoreHydrationTransaction(workspacePath, manifest) {
986
1029
  hydrationIncarnationKey: mount.incarnationKey,
987
1030
  sourceMode: "all",
988
1031
  hydrateDeletionMode: "all"
989
- }))
1032
+ })),
1033
+ durabilityScopes
990
1034
  );
991
1035
  }
992
1036
  function recoverPendingHydrationTransaction(workspacePath) {
@@ -1030,57 +1074,80 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1030
1074
  const transactionPath = hydrationTransactionPath(workspacePath);
1031
1075
  const stagingPath = path.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
1032
1076
  fs.mkdirSync(stagingPath, { mode: 448 });
1077
+ const durabilityScopes = /* @__PURE__ */ new Map();
1033
1078
  try {
1034
1079
  const snapshotRoot = path.join(stagingPath, "mounts");
1035
1080
  fs.mkdirSync(snapshotRoot, { mode: 448 });
1036
- const manifest = {
1037
- version: 3,
1038
- targetReceipt,
1039
- receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
1040
- mounts: mounts.map((mount, index) => {
1041
- const sourcePath = path.resolve(mount.sourcePath);
1042
- const durabilityRootPath = path.resolve(mount.durabilityRootPath);
1081
+ const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
1082
+ const manifestMounts = mounts.map((mount, index) => {
1083
+ const sourcePath = path.resolve(mount.sourcePath);
1084
+ const durabilityRootPath = path.resolve(mount.durabilityRootPath);
1085
+ assertManagedDirectoryPath({
1086
+ trustedRoot: durabilityRootPath,
1087
+ candidate: sourcePath,
1088
+ label: `Workspace hydration snapshot mount ${mount.id}`
1089
+ });
1090
+ const sourceStatus = lstatIfExists(sourcePath);
1091
+ if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
1092
+ throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
1093
+ }
1094
+ const snapshotName = String(index);
1095
+ const snapshotPath = path.join(snapshotRoot, snapshotName);
1096
+ fs.mkdirSync(snapshotPath, { mode: 448 });
1097
+ let scope;
1098
+ if (sourceStatus && mount.hydrateDeletionMode === "git") {
1099
+ const outerPath = workspaceMountOuterPath(workspacePath, mount);
1043
1100
  assertManagedDirectoryPath({
1044
- trustedRoot: durabilityRootPath,
1045
- candidate: sourcePath,
1046
- label: `Workspace hydration snapshot mount ${mount.id}`
1101
+ trustedRoot: workspacePath,
1102
+ candidate: outerPath,
1103
+ label: `Workspace mount ${mount.id} outer source`
1047
1104
  });
1048
- const sourceStatus = lstatIfExists(sourcePath);
1049
- if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
1050
- throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
1051
- }
1052
- const snapshotName = String(index);
1053
- const snapshotPath = path.join(snapshotRoot, snapshotName);
1054
- fs.mkdirSync(snapshotPath, { mode: 448 });
1055
- if (sourceStatus) {
1056
- mirrorWorkingTree({
1057
- sourceRoot: sourcePath,
1058
- targetRoot: snapshotPath,
1059
- sourceMode: "all",
1060
- deletionMode: "all",
1061
- // This tree is still private and the source authority is untouched.
1062
- // The single recursive barrier below makes the complete snapshot
1063
- // durable before its directory entry is published or hydration starts.
1064
- durability: "deferred_private_staging"
1065
- });
1066
- }
1067
- return {
1068
- id: mount.id,
1069
- incarnationKey: mount.hydrationIncarnationKey,
1070
- sourcePath,
1071
- durabilityRootPath,
1072
- workspaceRelativePath: mount.workspaceRelativePath,
1073
- existed: Boolean(sourceStatus),
1074
- snapshotName
1075
- };
1076
- })
1105
+ const plan = planWorkingTreeMirror({ sourceRoot: outerPath, targetRoot: sourcePath, sourceMode: "all", deletionMode: "git" });
1106
+ scope = captureWorkingTreeMirrorPlan({
1107
+ plan,
1108
+ targetRoot: sourcePath,
1109
+ snapshotRoot: snapshotPath,
1110
+ // Same private-staging deferral as the complete snapshot below.
1111
+ durability: "deferred_private_staging"
1112
+ });
1113
+ durabilityScopes.set(mount.id, [...plan.desired.keys()]);
1114
+ } else if (sourceStatus) {
1115
+ mirrorWorkingTree({
1116
+ sourceRoot: sourcePath,
1117
+ targetRoot: snapshotPath,
1118
+ sourceMode: "all",
1119
+ deletionMode: "all",
1120
+ // This tree is still private and the source authority is untouched.
1121
+ // The single recursive barrier below makes the complete snapshot
1122
+ // durable before its directory entry is published or hydration starts.
1123
+ durability: "deferred_private_staging"
1124
+ });
1125
+ }
1126
+ return {
1127
+ id: mount.id,
1128
+ incarnationKey: mount.hydrationIncarnationKey,
1129
+ sourcePath,
1130
+ durabilityRootPath,
1131
+ workspaceRelativePath: mount.workspaceRelativePath,
1132
+ existed: Boolean(sourceStatus),
1133
+ snapshotName,
1134
+ ...scope ? { scope } : {}
1135
+ };
1136
+ });
1137
+ const manifest = {
1138
+ // Older workers reject version 4, so a scoped snapshot can never be
1139
+ // replayed as a complete copy after a downgrade.
1140
+ version: manifestMounts.some((mount) => mount.scope) ? 4 : 3,
1141
+ targetReceipt,
1142
+ receiptBefore,
1143
+ mounts: manifestMounts
1077
1144
  };
1078
1145
  writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
1079
1146
  `);
1080
1147
  fsyncTree(stagingPath);
1081
1148
  fs.renameSync(stagingPath, transactionPath);
1082
1149
  fsyncDirectory(stateDirectory);
1083
- return manifest;
1150
+ return { manifest, durabilityScopes };
1084
1151
  } catch (error) {
1085
1152
  fs.rmSync(stagingPath, { recursive: true, force: true });
1086
1153
  fsyncDirectory(stateDirectory);
@@ -1092,7 +1159,11 @@ function markHydrationTransactionDurable(workspacePath) {
1092
1159
  writePrivateFileDurably(path.join(transactionPath, "committed"), "committed\n");
1093
1160
  fsyncDirectory(transactionPath);
1094
1161
  }
1095
- function fsyncHydratedWorkspaceMount(mount) {
1162
+ function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
1163
+ if (mount.hydrateDeletionMode !== "git" || !sourceStatus.isDirectory() || !lstatIfExists(path.join(sourcePath, ".git"))) return null;
1164
+ return [...inspectWorkingTree(sourcePath, "git").keys()];
1165
+ }
1166
+ function fsyncHydratedWorkspaceMount(mount, scope) {
1096
1167
  assertManagedDirectoryPath({
1097
1168
  trustedRoot: mount.durabilityRootPath,
1098
1169
  candidate: mount.sourcePath,
@@ -1117,7 +1188,17 @@ function fsyncHydratedWorkspaceMount(mount) {
1117
1188
  fs.closeSync(descriptor);
1118
1189
  }
1119
1190
  };
1120
- if (fs.existsSync(mount.sourcePath)) syncEntry(mount.sourcePath, true);
1191
+ const sourcePath = path.resolve(mount.sourcePath);
1192
+ const sourceStatus = lstatIfExists(sourcePath);
1193
+ if (sourceStatus) {
1194
+ const paths = scope ?? gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus);
1195
+ if (paths) {
1196
+ if (!sourceStatus.isDirectory()) throw new Error(`Workspace mount ${mount.id} source is not a regular directory: ${sourcePath}`);
1197
+ fsyncWorkingTreePaths(sourcePath, paths);
1198
+ } else {
1199
+ syncEntry(sourcePath, true);
1200
+ }
1201
+ }
1121
1202
  const durabilityRoot = path.resolve(mount.durabilityRootPath);
1122
1203
  let boundaryParent = path.dirname(path.resolve(mount.sourcePath));
1123
1204
  while (true) {
@@ -1136,8 +1217,8 @@ function fsyncHydratedWorkspaceMount(mount) {
1136
1217
  boundaryParent = nextParent;
1137
1218
  }
1138
1219
  }
1139
- function fsyncHydratedWorkspaceMounts(mounts) {
1140
- for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
1220
+ function fsyncHydratedWorkspaceMounts(mounts, scopes = /* @__PURE__ */ new Map()) {
1221
+ for (const mount of mounts) fsyncHydratedWorkspaceMount(mount, scopes.get(mount.id));
1141
1222
  }
1142
1223
  function workspaceIsShallow(workspacePath) {
1143
1224
  return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
@@ -1386,6 +1467,9 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
1386
1467
  }
1387
1468
  }
1388
1469
  }
1470
+ function workspaceMountOuterPath(workspacePath, mount) {
1471
+ return path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1472
+ }
1389
1473
  function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1390
1474
  const hydratedMountIds = [];
1391
1475
  const skippedMountIds = [];
@@ -1394,7 +1478,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1394
1478
  skippedMountIds.push(mount.id);
1395
1479
  continue;
1396
1480
  }
1397
- const sourceRoot = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1481
+ const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
1398
1482
  assertManagedDirectoryPath({
1399
1483
  trustedRoot: workspacePath,
1400
1484
  candidate: sourceRoot,
@@ -1480,56 +1564,67 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1480
1564
  const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1481
1565
  const requestedIds = new Set(input.mounts.map(({ id }) => id));
1482
1566
  const requiredIds = new Set(requiredMounts.map(({ id }) => id));
1483
- const hydrationMounts2 = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1484
- const advancedDurabilityMounts2 = durabilityMounts.filter(
1567
+ const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1568
+ const advancedDurabilityMounts = durabilityMounts.filter(
1485
1569
  ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1486
1570
  );
1487
1571
  const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1488
1572
  const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
1489
1573
  return {
1490
1574
  busyMountIds,
1491
- hydrationMounts: hydrationMounts2,
1492
- advancedDurabilityMounts: advancedDurabilityMounts2,
1575
+ hydrationMounts,
1576
+ advancedDurabilityMounts,
1493
1577
  skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
1494
- targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts2, ...carriedMounts], receiptMounts) : null
1578
+ targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts, ...carriedMounts], receiptMounts) : null
1495
1579
  };
1496
1580
  };
1497
1581
  const noopCandidate = computeSelection();
1498
1582
  if (noopCandidate.hydrationMounts.length === 0 && noopCandidate.advancedDurabilityMounts.length === 0 && noopCandidate.targetReceipt && lstatIfExists(hydrationTransactionPath(workspacePath)) === null && workspaceHydrationReceiptsEqual(receiptBeforeTransaction, noopCandidate.targetReceipt) && hydratedWorkspaceReceiptFileIsV3(workspacePath) && await workspaceBasisRefsAreCurrentAsync(workspacePath, noopCandidate.targetReceipt)) {
1499
1583
  return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
1500
1584
  }
1501
- const selection = computeSelection();
1502
- const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
1503
- const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1585
+ const hooks = input.hydrationHooks;
1586
+ const context = {
1587
+ transactionId: crypto.randomUUID(),
1588
+ workspacePath,
1589
+ mountIds: [...new Set([...noopCandidate.hydrationMounts, ...noopCandidate.advancedDurabilityMounts].map(({ id }) => id))].sort()
1590
+ };
1591
+ if (hooks) await hooks.beforeTransaction(context);
1504
1592
  try {
1505
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1506
- const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1507
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1508
- throw new Error("Workspace hydration did not include every required mount");
1509
- }
1510
- fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
1511
- markHydrationTransactionDurable(workspacePath);
1512
- if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1513
- removeHydrationTransaction(workspacePath);
1514
- return {
1515
- hydratedMountIds: hydration.hydratedMountIds,
1516
- skippedMountIds: selection.skippedMountIds
1517
- };
1518
- } catch (error) {
1519
- const transactionPath = hydrationTransactionPath(workspacePath);
1520
- if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1521
- throw error;
1522
- }
1593
+ const selection = computeSelection();
1594
+ const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
1595
+ const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1523
1596
  try {
1524
- restoreHydrationTransaction(workspacePath, manifest);
1597
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1598
+ const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1599
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1600
+ throw new Error("Workspace hydration did not include every required mount");
1601
+ }
1602
+ fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
1603
+ markHydrationTransactionDurable(workspacePath);
1604
+ if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1525
1605
  removeHydrationTransaction(workspacePath);
1526
- } catch (restoreError) {
1527
- throw new AggregateError(
1528
- [error, restoreError],
1529
- `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1530
- );
1606
+ return {
1607
+ hydratedMountIds: hydration.hydratedMountIds,
1608
+ skippedMountIds: selection.skippedMountIds
1609
+ };
1610
+ } catch (error) {
1611
+ const transactionPath = hydrationTransactionPath(workspacePath);
1612
+ if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1613
+ throw error;
1614
+ }
1615
+ try {
1616
+ restoreHydrationTransaction(workspacePath, manifest);
1617
+ removeHydrationTransaction(workspacePath);
1618
+ } catch (restoreError) {
1619
+ throw new AggregateError(
1620
+ [error, restoreError],
1621
+ `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1622
+ );
1623
+ }
1624
+ throw error;
1531
1625
  }
1532
- throw error;
1626
+ } finally {
1627
+ hooks?.afterTransaction(context);
1533
1628
  }
1534
1629
  }
1535
1630
  async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
@@ -1541,7 +1636,8 @@ async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1541
1636
  return hydrateWorkspaceGitMountsTransactionally({
1542
1637
  workspacePath,
1543
1638
  mounts,
1544
- ignoreBusy: options.ignoreBusy
1639
+ ignoreBusy: options.ignoreBusy,
1640
+ hydrationHooks: options.hydrationHooks
1545
1641
  });
1546
1642
  }
1547
1643
  async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
@@ -1581,7 +1677,8 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
1581
1677
  receiptMounts: configuredBasisMounts,
1582
1678
  requiredMounts: hydrationTargets,
1583
1679
  ignoreBusy: options.ignoreBusy,
1584
- recordCurrentHead: true
1680
+ recordCurrentHead: true,
1681
+ hydrationHooks: options.hydrationHooks
1585
1682
  });
1586
1683
  void hydration;
1587
1684
  }
@@ -1632,7 +1729,8 @@ async function resetWorkspaceGit(input) {
1632
1729
  durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
1633
1730
  receiptMounts: input.mounts,
1634
1731
  requiredMounts: requiredLiveMounts,
1635
- recordCurrentHead: true
1732
+ recordCurrentHead: true,
1733
+ hydrationHooks: input.hydrationHooks
1636
1734
  });
1637
1735
  const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
1638
1736
  const resetHead = revParse(workspacePath, "HEAD");
@@ -1887,7 +1985,8 @@ async function synchronizeWorkspaceGit(input) {
1887
1985
  await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1888
1986
  ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
1889
1987
  preserveResolutionInProgress,
1890
- preserveStaleBases: true
1988
+ preserveStaleBases: true,
1989
+ hydrationHooks: input.hydrationHooks
1891
1990
  });
1892
1991
  const selected = activeMounts(input.mounts);
1893
1992
  const projectionMutationTokens = new Map(
@@ -2013,7 +2112,8 @@ async function synchronizeWorkspaceGit(input) {
2013
2112
  carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
2014
2113
  receiptMounts: completionReceiptMounts,
2015
2114
  requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2016
- recordCurrentHead: true
2115
+ recordCurrentHead: true,
2116
+ hydrationHooks: input.hydrationHooks
2017
2117
  });
2018
2118
  for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
2019
2119
  refreshCycleSkippedMounts(completionReceiptMounts);
@@ -2,6 +2,7 @@
2
2
  import { Database } from "bun:sqlite";
3
3
  import type { WorkerGitIdentity } from "./git-identity";
4
4
  import { configureGitHubRegistryAuthFiles, type PreparedPrivateAuthFileGeneration } from "./registry-auth";
5
+ import { type WorkspaceHydrationTransactionHooks } from "./workspace-git-sync";
5
6
  type WorkerProjectConfig = {
6
7
  projectId: string;
7
8
  checkoutPathSegments: [namespace: string, project: string];
@@ -23,6 +24,11 @@ type WorkerProjectConfig = {
23
24
  baseCommitHash: string;
24
25
  }>;
25
26
  };
27
+ type WorkerAheadOfOriginBranch = {
28
+ projectId: string;
29
+ branchName: string;
30
+ ahead: number;
31
+ };
26
32
  type WorkerLifecycleProgress = {
27
33
  phase: "starting" | "recovering" | "configuring" | "syncing" | "ready" | "failed";
28
34
  operationId: string;
@@ -39,9 +45,28 @@ declare function workerServerHeartbeatAction(input: {
39
45
  timeoutMs: number;
40
46
  watchdogSweepDelayed: boolean;
41
47
  }): "wait" | "start_probe" | "terminate";
48
+ /**
49
+ * A hydration transaction blocks this event loop until it ends, so no pong,
50
+ * progress, or result can leave the process while it runs; on a slow disk
51
+ * that is hours. Declare a hydration heartbeat lease first: the server keeps
52
+ * the control socket open and defers the affected request deadlines while the
53
+ * lease is active instead of reading the silence as a dead worker, which is
54
+ * what turned one slow hydration into a mandatory canonical reset (incident
55
+ * 3bccab03). Bun writes the frame to the socket synchronously; the extra
56
+ * event-loop turn lets control messages that were already queued run before
57
+ * the busy observation that precedes the transaction body.
58
+ */
59
+ declare function createWorkspaceHydrationLeaseHooks(send: (message: Extract<WorkerClientMessage, {
60
+ type: "heartbeat_lease";
61
+ }>) => void, options?: {
62
+ yieldToEventLoop?: () => Promise<void>;
63
+ log?: (line: string) => void;
64
+ now?: () => number;
65
+ }): WorkspaceHydrationTransactionHooks;
42
66
  export declare const workerLifecycleTestHarness: {
43
67
  boundedWorkerLifecycleProgress: typeof boundedWorkerLifecycleProgress;
44
68
  workerServerHeartbeatAction: typeof workerServerHeartbeatAction;
69
+ createWorkspaceHydrationLeaseHooks: typeof createWorkspaceHydrationLeaseHooks;
45
70
  };
46
71
  type WorkspaceSyncTrigger = {
47
72
  type: "connect" | "inbound_head" | "write" | "edit" | "shell_inline" | "process_terminal" | "process_cancel" | "periodic" | "manual" | "remediation" | "remediation_confirm" | "remediation_reset";
@@ -103,6 +128,144 @@ export type WorkerSessionTarget = {
103
128
  ownerUserId: string;
104
129
  rootProfile: "visible_projects" | "canonical_sync";
105
130
  };
131
+ type WorkerClientMessage = {
132
+ type: "hello";
133
+ hostInfo?: Record<string, unknown>;
134
+ startupReady?: boolean;
135
+ } | {
136
+ type: "worker_progress";
137
+ progress: WorkerLifecycleProgress;
138
+ } | {
139
+ type: "heartbeat_lease";
140
+ operationId: string;
141
+ active: boolean;
142
+ /** A hydration transaction may block the worker for hours; the server bounds it separately from preparation leases. */
143
+ kind?: "hydration";
144
+ } | {
145
+ type: "startup_ready";
146
+ } | {
147
+ type: "active_processes";
148
+ processes: Array<{
149
+ runId: string;
150
+ target: WorkerSessionTarget;
151
+ sessionId: string;
152
+ platform: string;
153
+ arch: string;
154
+ mode: "foreground" | "detached";
155
+ pid?: number;
156
+ processGroupId?: number;
157
+ credentialId?: string;
158
+ argv: string[];
159
+ command: string;
160
+ cwd?: string;
161
+ startedAt: string;
162
+ interactive?: boolean;
163
+ workspaceEffect?: "none";
164
+ }>;
165
+ } | {
166
+ type: "pty_opened";
167
+ requestId: string;
168
+ ptyId: string;
169
+ } | {
170
+ type: "pty_output";
171
+ ptyId: string;
172
+ data: string;
173
+ } | {
174
+ type: "pty_exit";
175
+ ptyId: string;
176
+ exitCode: number;
177
+ signal?: number | string;
178
+ } | {
179
+ type: "pty_error";
180
+ ptyId: string;
181
+ requestId?: string;
182
+ error: string;
183
+ } | {
184
+ type: "operation_result";
185
+ requestId: string;
186
+ result?: WorkerOperationResult;
187
+ error?: string;
188
+ } | {
189
+ type: "exec_result";
190
+ requestId: string;
191
+ stdout: string;
192
+ stderr: string;
193
+ exitCode: number;
194
+ durationMs?: number;
195
+ error?: string;
196
+ } | {
197
+ type: "exec_accepted";
198
+ requestId: string;
199
+ runId: string;
200
+ } | {
201
+ type: "exec_started";
202
+ requestId: string;
203
+ runId: string;
204
+ cwd: string;
205
+ pid?: number;
206
+ processGroupId?: number;
207
+ } | {
208
+ type: "exec_start_error";
209
+ requestId: string;
210
+ runId: string;
211
+ error: string;
212
+ } | {
213
+ type: "exec_output";
214
+ runId: string;
215
+ stream: "stdout" | "stderr";
216
+ data: string;
217
+ } | {
218
+ type: "exec_exit";
219
+ runId: string;
220
+ exitCode: number;
221
+ durationMs?: number;
222
+ timedOut?: boolean;
223
+ workspaceEffect?: "none";
224
+ canonicalWorkspaceHead?: string;
225
+ } | {
226
+ type: "exec_error";
227
+ runId: string;
228
+ error: string;
229
+ durationMs?: number;
230
+ workspaceEffect?: "none";
231
+ canonicalWorkspaceHead?: string;
232
+ } | {
233
+ type: "cancel_result";
234
+ requestId: string;
235
+ runId: string;
236
+ cancelled: boolean;
237
+ message?: string;
238
+ } | {
239
+ type: "pong";
240
+ } | {
241
+ type: "workspace_configured";
242
+ requestId: string;
243
+ result: WorkspaceSyncResult;
244
+ pendingCheckouts: Array<{
245
+ projectId: string;
246
+ branchName: string;
247
+ }>;
248
+ aheadOfOriginBranches: WorkerAheadOfOriginBranch[];
249
+ deferredWorkspaceSyncForIncidentId?: string;
250
+ } | {
251
+ type: "workspace_sync_started";
252
+ attemptId: string;
253
+ } | {
254
+ type: "workspace_sync_result";
255
+ requestId?: string;
256
+ result: WorkspaceSyncResult;
257
+ } | {
258
+ type: "session_artifacts_sync_result";
259
+ requestId: string;
260
+ sessionId: string;
261
+ error?: string;
262
+ } | {
263
+ type: "update_clis_result";
264
+ requestId: string;
265
+ installedWorkerVersion?: string;
266
+ installedR5dctlVersion?: string;
267
+ error?: string;
268
+ };
106
269
  type WorkerServerMessage = {
107
270
  type: "connected";
108
271
  label: string;
@@ -371,6 +534,23 @@ type WorkerViewFileBytesResult = {
371
534
  width: number;
372
535
  height: number;
373
536
  };
537
+ type WorkerExecStdinAck = {
538
+ type: "exec_stdin";
539
+ runId: string;
540
+ bytesWritten: number;
541
+ eof: boolean;
542
+ };
543
+ type WorkerCreateProjectBranchResult = {
544
+ type: "create_project_branch";
545
+ branchId: string;
546
+ branchName: string;
547
+ baseCommitHash: string;
548
+ };
549
+ type WorkerDeleteProjectBranchResult = {
550
+ type: "delete_project_branch";
551
+ branchId: string;
552
+ branchName: string;
553
+ };
374
554
  type WorkerCodeEntry = {
375
555
  name: string;
376
556
  type: "file" | "directory";
@@ -389,6 +569,7 @@ type WorkerCodeReadResult = {
389
569
  mtime: string;
390
570
  base64: string;
391
571
  };
572
+ type WorkerOperationResult = WorkerReadFileResult | WorkerWriteFileResult | WorkerEditFileResult | WorkerGrepResult | WorkerFindResult | WorkerLsResult | WorkerViewFileBytesResult | WorkerExecStdinAck | WorkerCreateProjectBranchResult | WorkerDeleteProjectBranchResult | WorkerCodeListResult | WorkerCodeReadResult;
392
573
  declare class WorkspaceSyncAdmissionChangedError extends Error {
393
574
  readonly name = "WorkspaceSyncAdmissionChangedError";
394
575
  }