@ricsam/r5d-worker 0.0.94 → 0.0.96

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.
@@ -1483,13 +1483,38 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1483
1483
  }
1484
1484
  return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
1485
1485
  }
1486
+ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
1487
+ const candidates = mounts.flatMap((mount) => {
1488
+ const basisHead = workspaceHydrationReceiptMountBasisHead(receipt, mount);
1489
+ return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
1490
+ });
1491
+ if (candidates.length === 0) return /* @__PURE__ */ new Set();
1492
+ const objects = await resolveGitObjectIdsAsync(
1493
+ workspacePath,
1494
+ candidates.flatMap(({ mount, basisHead }) => [
1495
+ workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
1496
+ workspaceTreeObjectName(head, mount.workspaceRelativePath)
1497
+ ])
1498
+ );
1499
+ const unchanged = /* @__PURE__ */ new Set();
1500
+ for (const { mount, basisHead } of candidates) {
1501
+ const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
1502
+ const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
1503
+ if (before === after) unchanged.add(mount.id);
1504
+ }
1505
+ return unchanged;
1506
+ }
1486
1507
  async function hydrateWorkspaceGitMountsTransactionally(input) {
1487
1508
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1488
1509
  const durabilityMounts = input.durabilityMounts ?? input.mounts;
1489
1510
  const receiptMounts = input.receiptMounts ?? durabilityMounts;
1490
1511
  const requiredMounts = input.requiredMounts ?? input.mounts;
1512
+ const carryMounts = input.carryMounts ?? [];
1513
+ if (carryMounts.length > 0 && !input.recordCurrentHead) {
1514
+ throw new Error("Workspace hydration can only carry unchanged mounts while recording the current head");
1515
+ }
1491
1516
  const candidateMounts = [
1492
- ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
1517
+ ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts, ...carryMounts].map((mount) => [mount.id, mount])).values()
1493
1518
  ];
1494
1519
  if (input.recordCurrentHead && !await workspaceCheckoutDurabilityIsCurrentAsync(workspacePath)) {
1495
1520
  throw new Error("Workspace checkout is not durably materialized at its current HEAD");
@@ -1505,12 +1530,13 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1505
1530
  ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1506
1531
  );
1507
1532
  const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1533
+ const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
1508
1534
  return {
1509
1535
  busyMountIds,
1510
1536
  hydrationMounts: hydrationMounts2,
1511
1537
  advancedDurabilityMounts: advancedDurabilityMounts2,
1512
1538
  skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
1513
- targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, advancedDurabilityMounts2, receiptMounts) : null
1539
+ targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts2, ...carriedMounts], receiptMounts) : null
1514
1540
  };
1515
1541
  };
1516
1542
  const noopCandidate = computeSelection();
@@ -1686,14 +1712,26 @@ async function changedPaths(workspacePath, baseRevision, headRevision) {
1686
1712
  const base = baseRevision ?? emptyTreeHash(workspacePath);
1687
1713
  return (await gitAsync(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths")).split("\0").filter(Boolean).sort();
1688
1714
  }
1689
- async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
1715
+ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, pathspecs = []) {
1690
1716
  const base = baseRevision ?? emptyTreeHash(workspacePath);
1691
- const subprocess = Bun.spawn(["git", ...NON_RECURSIVE_GIT_CONFIG, "diff", "--binary", "--no-ext-diff", base, headRevision], {
1692
- cwd: workspacePath,
1693
- stdout: "pipe",
1694
- stderr: "pipe",
1695
- env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1696
- });
1717
+ const subprocess = Bun.spawn(
1718
+ [
1719
+ "git",
1720
+ ...NON_RECURSIVE_GIT_CONFIG,
1721
+ "diff",
1722
+ "--binary",
1723
+ "--no-ext-diff",
1724
+ base,
1725
+ headRevision,
1726
+ ...pathspecs.length > 0 ? ["--", ...pathspecs] : []
1727
+ ],
1728
+ {
1729
+ cwd: workspacePath,
1730
+ stdout: "pipe",
1731
+ stderr: "pipe",
1732
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1733
+ }
1734
+ );
1697
1735
  const stderrPromise = new Response(subprocess.stderr).text();
1698
1736
  const reader = subprocess.stdout.getReader();
1699
1737
  let total = 0;
@@ -1713,6 +1751,73 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
1713
1751
  if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
1714
1752
  return total;
1715
1753
  }
1754
+ async function resolveGitObjectIdsAsync(workspacePath, names) {
1755
+ const resolved = /* @__PURE__ */ new Map();
1756
+ const unique = [...new Set(names)];
1757
+ if (unique.length === 0) return resolved;
1758
+ const subprocess = Bun.spawn(gitCommandArgs(["cat-file", "--batch-check"]), {
1759
+ cwd: workspacePath,
1760
+ stdin: Buffer.from(`${unique.join("\n")}
1761
+ `),
1762
+ stdout: "pipe",
1763
+ stderr: "pipe",
1764
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1765
+ });
1766
+ const [stdout, stderr, exitCode] = await Promise.all([
1767
+ new Response(subprocess.stdout).text(),
1768
+ new Response(subprocess.stderr).text(),
1769
+ subprocess.exited
1770
+ ]);
1771
+ if (exitCode !== 0) throw new Error(`Resolve workspace objects: ${stderr.trim() || `git exited ${exitCode}`}`);
1772
+ const lines = stdout.split("\n").filter((line) => line.length > 0);
1773
+ if (lines.length !== unique.length) {
1774
+ throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1775
+ }
1776
+ unique.forEach((name, index) => {
1777
+ const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
1778
+ resolved.set(name, match ? match[1] : null);
1779
+ });
1780
+ return resolved;
1781
+ }
1782
+ function workspaceTreeObjectName(revision, workspaceRelativePath) {
1783
+ return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
1784
+ }
1785
+ async function measureInitialPublicationDiffSize(input) {
1786
+ const candidates = input.mounts.filter(
1787
+ (mount) => typeof mount.initialPublicationSourceRelativePath === "string"
1788
+ );
1789
+ if (candidates.length === 0) return null;
1790
+ const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
1791
+ const objects = await resolveGitObjectIdsAsync(
1792
+ input.workspacePath,
1793
+ candidates.flatMap(
1794
+ (mount) => revisions.flatMap((revision) => [
1795
+ workspaceTreeObjectName(revision, mount.workspaceRelativePath),
1796
+ workspaceTreeObjectName(revision, mount.initialPublicationSourceRelativePath)
1797
+ ])
1798
+ )
1799
+ );
1800
+ const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
1801
+ const initialPublications = candidates.flatMap((mount) => {
1802
+ const published = lookup(input.localHead, mount.workspaceRelativePath);
1803
+ if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
1804
+ const source = lookup(input.localHead, mount.initialPublicationSourceRelativePath) ?? lookup(input.remoteHead, mount.initialPublicationSourceRelativePath);
1805
+ return [{ mount, published, source }];
1806
+ });
1807
+ if (initialPublications.length === 0) return null;
1808
+ let size = await input.measure(
1809
+ input.workspacePath,
1810
+ input.remoteHead,
1811
+ input.localHead,
1812
+ input.limit,
1813
+ initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
1814
+ );
1815
+ for (const { published, source } of initialPublications) {
1816
+ if (size > input.limit) break;
1817
+ size += await input.measure(input.workspacePath, source, published, input.limit - size);
1818
+ }
1819
+ return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
1820
+ }
1716
1821
  function conflictPaths(workspacePath) {
1717
1822
  const result = gitResult(workspacePath, ["diff", "--name-only", "--diff-filter=U", "-z"]);
1718
1823
  return result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
@@ -1940,12 +2045,19 @@ async function synchronizeWorkspaceGit(input) {
1940
2045
  const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
1941
2046
  (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1942
2047
  );
2048
+ const unchangedMountIds = currentHeadBeforeHydration ? await selectMountsWithUnchangedOuterSubtree(
2049
+ workspacePath,
2050
+ currentReceiptBeforeHydration,
2051
+ currentHeadBeforeHydration,
2052
+ uncoveredCompletionMounts
2053
+ ) : /* @__PURE__ */ new Set();
1943
2054
  const hydration = await hydrateWorkspaceGitMountsTransactionally({
1944
2055
  workspacePath,
1945
- mounts: uncoveredCompletionMounts,
1946
- durabilityMounts: uncoveredCompletionBasisMounts,
2056
+ mounts: uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2057
+ durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2058
+ carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
1947
2059
  receiptMounts: completionReceiptMounts,
1948
- requiredMounts: uncoveredRequiredLiveMounts,
2060
+ requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
1949
2061
  recordCurrentHead: true
1950
2062
  });
1951
2063
  for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
@@ -2095,8 +2207,25 @@ async function synchronizeWorkspaceGit(input) {
2095
2207
  };
2096
2208
  }
2097
2209
  const paths = await changedPaths(workspacePath, remoteHead, localHead);
2098
- const size = paths.length > 0 ? await (input.measureDiffSize ?? diffSizeBytes)(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2210
+ const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
2211
+ let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2099
2212
  input.assertStillAdmitted?.();
2213
+ let initialPublicationMountIds;
2214
+ if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2215
+ const initialPublication = await measureInitialPublicationDiffSize({
2216
+ workspacePath,
2217
+ remoteHead,
2218
+ localHead,
2219
+ mounts: input.mounts,
2220
+ limit: maxDiffBytes,
2221
+ measure: measureDiffSize
2222
+ });
2223
+ input.assertStillAdmitted?.();
2224
+ if (initialPublication && initialPublication.size <= maxDiffBytes) {
2225
+ size = initialPublication.size;
2226
+ initialPublicationMountIds = initialPublication.mountIds;
2227
+ }
2228
+ }
2100
2229
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2101
2230
  const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
2102
2231
  return {
@@ -2141,7 +2270,9 @@ async function synchronizeWorkspaceGit(input) {
2141
2270
  "push",
2142
2271
  "--no-recurse-submodules"
2143
2272
  ];
2144
- if (input.allowLargeDiff) pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2273
+ if (input.allowLargeDiff || initialPublicationMountIds) {
2274
+ pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2275
+ }
2145
2276
  pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
2146
2277
  const push = await gitResultAsync(workspacePath, pushArgs);
2147
2278
  if (push.exitCode === 0) {
@@ -2163,6 +2294,7 @@ async function synchronizeWorkspaceGit(input) {
2163
2294
  diffSizeBytes: size,
2164
2295
  affectedPaths: paths,
2165
2296
  ...completedMounts,
2297
+ ...initialPublicationMountIds ? { initialPublicationMountIds } : {},
2166
2298
  ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
2167
2299
  ...projectionWarning ? { error: projectionWarning } : {}
2168
2300
  };
package/dist/mjs/main.mjs CHANGED
@@ -58,6 +58,7 @@ import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchNa
58
58
  import {
59
59
  applyObservedProjectMirrorHeads,
60
60
  assertProjectMirrorHeadsPushed,
61
+ computeLocalProjectMirrorRefsToken,
61
62
  createOrRetryLinkedProjectBranch,
62
63
  deleteLinkedProjectBranch,
63
64
  deleteProjectMirrorBranch,
@@ -70,6 +71,7 @@ import {
70
71
  pushProjectMirrorHeads,
71
72
  removeProjectWorktrees
72
73
  } from "./project-worktrees.mjs";
74
+ import { projectMirrorFetchDecision } from "./project-mirror-fetch-policy.mjs";
73
75
  import {
74
76
  PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
75
77
  recoverProjectSnapshotsInChild,
@@ -119,6 +121,7 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
119
121
  const MAX_LINE_LENGTH = 2e3;
120
122
  const WORKSPACE_GIT_QUIET_MS = 5e3;
121
123
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
124
+ const PROJECT_MIRROR_OBSERVE_CONCURRENCY = 6;
122
125
  const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
123
126
  const PTY_INPUT_BUSY_GRACE_MS = 3e3;
124
127
  const PTY_FOREGROUND_POLL_MS = 1e3;
@@ -1009,6 +1012,21 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
1009
1012
  function canonicalProjectRemoteUrl(baseUrl, projectId) {
1010
1013
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
1011
1014
  }
1015
+ async function mapWithConcurrencyLimit(items, limit, task) {
1016
+ const results = new Array(items.length);
1017
+ let nextIndex = 0;
1018
+ await Promise.all(
1019
+ Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
1020
+ while (true) {
1021
+ const index = nextIndex;
1022
+ nextIndex += 1;
1023
+ if (index >= items.length) return;
1024
+ results[index] = await task(items[index], index);
1025
+ }
1026
+ })
1027
+ );
1028
+ return results;
1029
+ }
1012
1030
  function shellQuote(value) {
1013
1031
  return `'${value.replace(/'/g, "'\\''")}'`;
1014
1032
  }
@@ -3468,6 +3486,10 @@ async function startWorker(options) {
3468
3486
  const readyProjectIds = /* @__PURE__ */ new Set();
3469
3487
  const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
3470
3488
  const lastObservedProjectHeads = /* @__PURE__ */ new Map();
3489
+ const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3490
+ const fetchedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3491
+ const lastProjectMirrorFetchAtMs = /* @__PURE__ */ new Map();
3492
+ const projectMirrorFetchSkippingEnabled = process.env.R5D_PROJECT_MIRROR_FETCH_SKIP !== "0";
3471
3493
  const pendingMirrorDeletes = /* @__PURE__ */ new Map();
3472
3494
  let workspaceRemoteUrl = null;
3473
3495
  let workspaceCredentialHelper = null;
@@ -3485,6 +3507,7 @@ async function startWorker(options) {
3485
3507
  let automaticSyncInFlight = false;
3486
3508
  let terminalReplayTimer;
3487
3509
  let workspaceSyncRequestsInFlight = 0;
3510
+ let lastWorkspaceSyncMirrorObservationMs = 0;
3488
3511
  let sessionArtifactSyncRequestsInFlight = 0;
3489
3512
  let cliUpdateInProgress = false;
3490
3513
  let reloadAfterClose = false;
@@ -3808,6 +3831,7 @@ async function startWorker(options) {
3808
3831
  const branchKey = projectBranchKey(project.projectId, branch.branchName);
3809
3832
  const creatorLocalIncarnation = creatorLocalIncarnations.get(branchKey) === branch.branchId;
3810
3833
  const preservesCheckoutPathMove = checkoutPathMoveBranches.has(branchKey);
3834
+ const initialPublicationSourceBranch = creatorLocalIncarnation ? branch.sourceBranchName : null;
3811
3835
  mounts.push({
3812
3836
  id: projectMountId(project.projectId, branch.branchName),
3813
3837
  hydrationIncarnationKey: branch.branchId,
@@ -3817,6 +3841,7 @@ async function startWorker(options) {
3817
3841
  sourceMode: "git",
3818
3842
  hydrateDeletionMode: "git",
3819
3843
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3844
+ ...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspaceProjectRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
3820
3845
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3821
3846
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3822
3847
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
@@ -3831,6 +3856,7 @@ async function startWorker(options) {
3831
3856
  sourceMode: "all",
3832
3857
  hydrateDeletionMode: "all",
3833
3858
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3859
+ ...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspacePlanRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
3834
3860
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3835
3861
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3836
3862
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
@@ -4110,6 +4136,17 @@ async function startWorker(options) {
4110
4136
  for (const result of results) {
4111
4137
  lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
4112
4138
  }
4139
+ try {
4140
+ fetchedProjectMirrorRefsTokens.set(
4141
+ project.projectId,
4142
+ await computeLocalProjectMirrorRefsToken({
4143
+ projectRoot: configuredProjectRoot(projectsRoot, project),
4144
+ primaryBranchName: primaryProjectBranch(project)
4145
+ })
4146
+ );
4147
+ } catch {
4148
+ fetchedProjectMirrorRefsTokens.delete(project.projectId);
4149
+ }
4113
4150
  }
4114
4151
  const publishedTombstoneIds = [
4115
4152
  ...new Set(
@@ -4135,6 +4172,7 @@ async function startWorker(options) {
4135
4172
  credentialHelper: deletion.credentialHelper,
4136
4173
  credentialUsername: deletion.credentialUsername
4137
4174
  });
4175
+ fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4138
4176
  }
4139
4177
  if (deletion.tombstoneId && !deletion.projectDeleted) {
4140
4178
  projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
@@ -4145,42 +4183,101 @@ async function startWorker(options) {
4145
4183
  pendingMirrorDeletes.delete(key);
4146
4184
  if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
4147
4185
  removeProjectCredentialStore(deletion.projectId);
4186
+ advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
4187
+ fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4188
+ lastProjectMirrorFetchAtMs.delete(deletion.projectId);
4148
4189
  }
4149
4190
  lastObservedProjectHeads.delete(key);
4150
4191
  }
4151
4192
  };
4193
+ const observeSingleProjectMirrorBundle = async (project, options2) => {
4194
+ const connection = projectConnection(project);
4195
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
4196
+ const primaryBranchName = primaryProjectBranch(project);
4197
+ if (options2.refreshRemote) {
4198
+ const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
4199
+ const observations = await observeProjectMirrorHeads({
4200
+ projectRoot,
4201
+ primaryBranchName,
4202
+ branchNames: project.branches.map(({ branchName }) => branchName),
4203
+ mirrorUrl: connection.mirrorUrl,
4204
+ credentialHelper: connection.credentialHelper,
4205
+ credentialUsername: connection.mirrorCredentialUsername,
4206
+ allowNonFastForward: options2.allowNonFastForward,
4207
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4208
+ });
4209
+ if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
4210
+ else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
4211
+ lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
4212
+ return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
4213
+ }
4214
+ return {
4215
+ projectId: project.projectId,
4216
+ projectRoot,
4217
+ primaryBranchName,
4218
+ observations: await observeProjectMirrorHeads({
4219
+ projectRoot,
4220
+ primaryBranchName,
4221
+ branchNames: project.branches.map(({ branchName }) => branchName),
4222
+ mirrorUrl: connection.mirrorUrl,
4223
+ credentialHelper: connection.credentialHelper,
4224
+ credentialUsername: connection.mirrorCredentialUsername,
4225
+ allowNonFastForward: options2.allowNonFastForward,
4226
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
4227
+ refreshRemote: false
4228
+ })
4229
+ };
4230
+ };
4152
4231
  const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
4153
- const bundles = [];
4154
- for (const project of projectConfigById.values()) {
4155
- if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
4156
- try {
4157
- const connection = projectConnection(project);
4158
- const projectRoot = configuredProjectRoot(projectsRoot, project);
4159
- const primaryBranchName = primaryProjectBranch(project);
4160
- bundles.push({
4161
- projectId: project.projectId,
4162
- projectRoot,
4163
- primaryBranchName,
4164
- observations: await observeProjectMirrorHeads({
4165
- projectRoot,
4166
- primaryBranchName,
4167
- branchNames: project.branches.map(({ branchName }) => branchName),
4168
- mirrorUrl: connection.mirrorUrl,
4169
- credentialHelper: connection.credentialHelper,
4170
- credentialUsername: connection.mirrorCredentialUsername,
4171
- allowNonFastForward: input.allowNonFastForward,
4172
- branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4173
- })
4174
- });
4175
- } catch (error) {
4176
- if (input.failClosed) throw error;
4232
+ const observationStartedAt = Date.now();
4233
+ const eligibleProjects = [...projectConfigById.values()].filter(
4234
+ (project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
4235
+ );
4236
+ try {
4237
+ if (input.failClosed) {
4238
+ const bundles2 = [];
4239
+ for (const project of eligibleProjects) {
4240
+ bundles2.push(await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, refreshRemote: true }));
4241
+ }
4242
+ return bundles2;
4243
+ }
4244
+ let fetchedCount = 0;
4245
+ const bundles = await mapWithConcurrencyLimit(
4246
+ eligibleProjects,
4247
+ PROJECT_MIRROR_OBSERVE_CONCURRENCY,
4248
+ async (project) => {
4249
+ try {
4250
+ const decision = projectMirrorFetchDecision({
4251
+ advertisedToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
4252
+ fetchedToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
4253
+ lastFetchAtMs: lastProjectMirrorFetchAtMs.get(project.projectId),
4254
+ nowMs: Date.now(),
4255
+ skippingEnabled: projectMirrorFetchSkippingEnabled
4256
+ });
4257
+ if (decision.fetch) fetchedCount += 1;
4258
+ return await observeSingleProjectMirrorBundle(project, {
4259
+ allowNonFastForward: input.allowNonFastForward,
4260
+ refreshRemote: decision.fetch
4261
+ });
4262
+ } catch (error) {
4263
+ process.stderr.write(
4264
+ `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
4265
+ `
4266
+ );
4267
+ return null;
4268
+ }
4269
+ }
4270
+ );
4271
+ if (fetchedCount > 0) {
4177
4272
  process.stderr.write(
4178
- `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
4273
+ `[r5d-worker] project mirror observation fetched ${fetchedCount} of ${eligibleProjects.length} mirror(s)
4179
4274
  `
4180
4275
  );
4181
4276
  }
4277
+ return bundles.filter((bundle) => bundle !== null);
4278
+ } finally {
4279
+ lastWorkspaceSyncMirrorObservationMs = Date.now() - observationStartedAt;
4182
4280
  }
4183
- return bundles;
4184
4281
  };
4185
4282
  const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
4186
4283
  const active = new Set(activeMountIds);
@@ -4359,6 +4456,12 @@ async function startWorker(options) {
4359
4456
  }
4360
4457
  });
4361
4458
  input.assertStillAdmitted?.();
4459
+ if (result.initialPublicationMountIds && result.initialPublicationMountIds.length > 0) {
4460
+ process.stderr.write(
4461
+ `[r5d-worker] published first checkout copies above the automatic diff-size limit, measured against their source subtrees at ${result.diffSizeBytes} bytes: ${result.initialPublicationMountIds.join(", ")}
4462
+ `
4463
+ );
4464
+ }
4362
4465
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4363
4466
  await applyObservedProjectHeadsAfterInboundWorkspace(
4364
4467
  observedProjectHeads,
@@ -4449,6 +4552,7 @@ async function startWorker(options) {
4449
4552
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
4450
4553
  queueEnteredAt = Date.now();
4451
4554
  syncStartedAt = Date.now();
4555
+ lastWorkspaceSyncMirrorObservationMs = 0;
4452
4556
  if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4453
4557
  return await failedWorkspaceSyncResult(
4454
4558
  attemptId,
@@ -4533,7 +4637,9 @@ async function startWorker(options) {
4533
4637
  telemetry: {
4534
4638
  totalMs: finishedAt - requestedAt,
4535
4639
  queueMs: queueEnteredAt - requestedAt,
4536
- prepareMs: 0,
4640
+ // Duration of the pre-outer mirror-head observation phase; near zero
4641
+ // on a cycle whose token-gated observation skipped every fetch.
4642
+ prepareMs: lastWorkspaceSyncMirrorObservationMs,
4537
4643
  synchronizeMs: finishedAt - syncStartedAt
4538
4644
  }
4539
4645
  };
@@ -5251,7 +5357,8 @@ async function startWorker(options) {
5251
5357
  workspaceIncidentConfigDeferralV1: true,
5252
5358
  workspaceConfigResetToCanonicalV1: true,
5253
5359
  projectBranchDeletionFastAckV1: true,
5254
- projectMirrorLeaseV1: true
5360
+ projectMirrorLeaseV1: true,
5361
+ projectMirrorRefsTokensV1: true
5255
5362
  },
5256
5363
  projectRoot: projectsRoot,
5257
5364
  artifactRoot,
@@ -5289,6 +5396,17 @@ async function startWorker(options) {
5289
5396
  if (message.type === "connected") {
5290
5397
  return;
5291
5398
  }
5399
+ if (message.type === "project_mirror_refs_tokens") {
5400
+ advertisedProjectMirrorRefsTokens.clear();
5401
+ if (message.tokens && typeof message.tokens === "object") {
5402
+ for (const [projectId, token2] of Object.entries(message.tokens)) {
5403
+ if (typeof token2 === "string" && /^[0-9a-f]{64}$/.test(token2)) {
5404
+ advertisedProjectMirrorRefsTokens.set(projectId, token2);
5405
+ }
5406
+ }
5407
+ }
5408
+ return;
5409
+ }
5292
5410
  if (message.type === "workspace_config") {
5293
5411
  const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
5294
5412
  const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.94",
3
+ "version": "0.0.96",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,16 @@
1
+ const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
2
+ function projectMirrorFetchDecision(input) {
3
+ if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
4
+ if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
5
+ if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
6
+ if (input.advertisedToken !== input.fetchedToken) return { fetch: true, reason: "token_changed" };
7
+ const backstopMs = input.backstopMs ?? PROJECT_MIRROR_FETCH_BACKSTOP_MS;
8
+ if (input.lastFetchAtMs === void 0 || input.nowMs - input.lastFetchAtMs >= backstopMs) {
9
+ return { fetch: true, reason: "backstop_elapsed" };
10
+ }
11
+ return { fetch: false, reason: "token_match" };
12
+ }
13
+ export {
14
+ PROJECT_MIRROR_FETCH_BACKSTOP_MS,
15
+ projectMirrorFetchDecision
16
+ };
@@ -0,0 +1,24 @@
1
+ import { createHash } from "node:crypto";
2
+ const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
3
+ const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
4
+ function hashProjectMirrorRefs(refs) {
5
+ const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
6
+ return createHash("sha256").update(lines.join("\n")).digest("hex");
7
+ }
8
+ function canonicalizeLocalProjectMirrorRefs(refs) {
9
+ const canonical = [];
10
+ for (const { refname, objectName } of refs) {
11
+ if (refname.startsWith(PROJECT_MIRROR_REF_PREFIX)) {
12
+ canonical.push({ refname: `refs/heads/${refname.slice(PROJECT_MIRROR_REF_PREFIX.length)}`, objectName });
13
+ } else if (refname.startsWith(PROJECT_SERVER_SYNCED_REF_PREFIX)) {
14
+ canonical.push({ refname, objectName });
15
+ }
16
+ }
17
+ return canonical;
18
+ }
19
+ export {
20
+ PROJECT_MIRROR_REF_PREFIX,
21
+ PROJECT_SERVER_SYNCED_REF_PREFIX,
22
+ canonicalizeLocalProjectMirrorRefs,
23
+ hashProjectMirrorRefs
24
+ };