@ricsam/r5d-worker 0.0.56 → 0.0.58

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.
@@ -0,0 +1,81 @@
1
+ import { createHash } from "node:crypto";
2
+ const RECENT_WORKSPACE_SYNC_PROOF_MAX_AGE_MS = 15e3;
3
+ function stableProjectShape(project) {
4
+ return {
5
+ projectId: project.projectId,
6
+ checkoutPathSegments: [...project.checkoutPathSegments],
7
+ projectPath: project.projectPath,
8
+ repoHttpUrl: project.repoHttpUrl,
9
+ defaultBranch: project.defaultBranch,
10
+ branches: [...project.branches].sort(),
11
+ canonicalCheckouts: [...project.canonicalCheckouts].sort(
12
+ (left, right) => left.branchName.localeCompare(right.branchName) || left.scaffoldCommitHash.localeCompare(right.scaffoldCommitHash)
13
+ )
14
+ };
15
+ }
16
+ function workspaceSyncScopeKey(remoteUrl, projects) {
17
+ const stableProjects = [...projects].map(stableProjectShape).sort((left, right) => left.projectId.localeCompare(right.projectId) || left.projectPath.localeCompare(right.projectPath));
18
+ return createHash("sha256").update(JSON.stringify({ remoteUrl, projects: stableProjects })).digest("hex");
19
+ }
20
+ function readShadowWorkspaceCanonicalHead(shadowRoot) {
21
+ const result = Bun.spawnSync(["git", "rev-parse", "--verify", "HEAD"], {
22
+ cwd: shadowRoot,
23
+ stdout: "pipe",
24
+ stderr: "ignore"
25
+ });
26
+ if (result.exitCode !== 0) return null;
27
+ const head = result.stdout.toString().trim();
28
+ return /^[0-9a-f]{40,64}$/.test(head) ? head : null;
29
+ }
30
+ class RecentWorkspaceSyncProofCache {
31
+ constructor(maxAgeMs = RECENT_WORKSPACE_SYNC_PROOF_MAX_AGE_MS) {
32
+ this.maxAgeMs = maxAgeMs;
33
+ }
34
+ maxAgeMs;
35
+ generation = 0;
36
+ proof = null;
37
+ beginObservation() {
38
+ return this.generation;
39
+ }
40
+ invalidate() {
41
+ this.generation += 1;
42
+ }
43
+ recordStable(input) {
44
+ if (input.observationGeneration !== this.generation || input.workerBusy || !input.canonicalHead || !/^[0-9a-f]{40,64}$/.test(input.canonicalHead)) {
45
+ return false;
46
+ }
47
+ this.proof = {
48
+ canonicalHead: input.canonicalHead,
49
+ scopeKey: input.scopeKey,
50
+ observedAtMs: input.observedAtMs,
51
+ generation: this.generation
52
+ };
53
+ return true;
54
+ }
55
+ evaluate(input) {
56
+ if (!input.allowRecentNoChangeFastPath || input.trigger.type !== "inbound_head") {
57
+ return { hit: false, reason: "unsupported_trigger" };
58
+ }
59
+ if (input.trigger.canonicalCheckoutOnly) return { hit: false, reason: "canonical_checkout" };
60
+ if (input.skipVisibleMirror) return { hit: false, reason: "visible_mirror_skipped" };
61
+ if (input.resetToCanonical) return { hit: false, reason: "reset_requested" };
62
+ if (input.incidentActive) return { hit: false, reason: "incident_active" };
63
+ if (input.workerBusy) return { hit: false, reason: "worker_busy" };
64
+ if (input.pendingCheckouts) return { hit: false, reason: "pending_checkouts" };
65
+ if (!input.canonicalHead) return { hit: false, reason: "canonical_head_missing" };
66
+ const proof = this.proof;
67
+ if (!proof) return { hit: false, reason: "no_recent_proof" };
68
+ if (proof.generation !== this.generation) return { hit: false, reason: "proof_invalidated" };
69
+ const proofAgeMs = Math.max(0, input.nowMs - proof.observedAtMs);
70
+ if (proofAgeMs > this.maxAgeMs) return { hit: false, reason: "proof_expired" };
71
+ if (proof.scopeKey !== input.scopeKey) return { hit: false, reason: "scope_changed" };
72
+ if (proof.canonicalHead !== input.canonicalHead) return { hit: false, reason: "canonical_head_changed" };
73
+ return { hit: true, reason: "recent_stable_no_change", canonicalHead: proof.canonicalHead, proofAgeMs };
74
+ }
75
+ }
76
+ export {
77
+ RECENT_WORKSPACE_SYNC_PROOF_MAX_AGE_MS,
78
+ RecentWorkspaceSyncProofCache,
79
+ readShadowWorkspaceCanonicalHead,
80
+ workspaceSyncScopeKey
81
+ };
@@ -911,7 +911,7 @@ function hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot
911
911
  expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
912
912
  return expectedCheckoutSnapshot !== null;
913
913
  };
914
- let currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
914
+ const currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
915
915
  const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
916
916
  (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
917
917
  );
@@ -1533,6 +1533,33 @@ function resetUncommittedShadowSnapshot(input) {
1533
1533
  }
1534
1534
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
1535
1535
  }
1536
+ function activeWorkspaceRebase(input) {
1537
+ for (const name of ["rebase-merge", "rebase-apply"]) {
1538
+ const resolved = runGitResult(input, input.shadowRoot, ["rev-parse", "--git-path", name]);
1539
+ if (resolved.exitCode !== 0 || !resolved.stdout) continue;
1540
+ const gitPath = path.isAbsolute(resolved.stdout) ? resolved.stdout : path.resolve(input.shadowRoot, resolved.stdout);
1541
+ if (fs.existsSync(gitPath)) return name;
1542
+ }
1543
+ return null;
1544
+ }
1545
+ function remediationRebaseInProgressResult(input, result) {
1546
+ if (input.trigger.type !== "remediation" && input.trigger.type !== "remediation_confirm") return null;
1547
+ const rebaseKind = activeWorkspaceRebase(input);
1548
+ if (!rebaseKind) return null;
1549
+ const conflictPaths = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]).stdout.split("\0").filter(Boolean).sort();
1550
+ const status = reportedStatus(gitStatus(input));
1551
+ return {
1552
+ ...result,
1553
+ outcome: "conflict_blocked",
1554
+ expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
1555
+ candidateHead: revParse(input, "ORIG_HEAD") ?? revParse(input, "HEAD") ?? void 0,
1556
+ rebaseCount: 1,
1557
+ gitStatus: status,
1558
+ affectedProjects: affectedProjects(conflictPaths),
1559
+ affectedPaths: reportedPaths(conflictPaths),
1560
+ error: `Workspace conflict remediation has an in-progress ${rebaseKind}; the index and worktree were preserved until the resolver completes or aborts it.`
1561
+ };
1562
+ }
1536
1563
  function fastForwardCleanShadowForNewCheckouts(input) {
1537
1564
  if (!input.newVisibleCheckouts?.length) return null;
1538
1565
  runGit(input, input.shadowRoot, ["add", "-A"], "stage existing workspace changes before checkout hydration");
@@ -1752,17 +1779,66 @@ function convergeRedundantInboundCandidate(input, result) {
1752
1779
  affectedPaths: []
1753
1780
  };
1754
1781
  }
1782
+ function preserveWorkspaceConflictCandidate(input, observed, options) {
1783
+ try {
1784
+ if (!options.baseRevision) throw new Error("the preserved workspace does not have an old-base commit");
1785
+ runGit(input, input.shadowRoot, ["reset", "--hard", options.baseRevision], "restore old-base workspace conflict candidate");
1786
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace conflict candidate");
1787
+ if (!options.preserveShadowCandidate) {
1788
+ const projection = mirrorVisibleWorkspaceToShadow(input);
1789
+ restoreUnscopedCanonicalCheckoutSubtrees(input);
1790
+ const stagePathspecs = projection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
1791
+ runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage preserved workspace conflict candidate");
1792
+ forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
1793
+ if (stagedPaths(input).length > 0) {
1794
+ runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit preserved workspace conflict candidate");
1795
+ }
1796
+ }
1797
+ const candidateHead = revParse(input, "HEAD");
1798
+ if (!candidateHead) throw new Error("the preserved workspace conflict candidate does not have a commit");
1799
+ assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1800
+ const upload = uploadWorkspaceCandidate(input, candidateHead);
1801
+ if (upload.exitCode !== 0) {
1802
+ throw new Error(upload.stderr || upload.stdout || "workspace conflict candidate upload failed");
1803
+ }
1804
+ const conflictPaths = [...new Set(options.conflictPaths)].sort();
1805
+ return {
1806
+ ...observed,
1807
+ outcome: "conflict_blocked",
1808
+ expectedHead: options.expectedHead,
1809
+ candidateHead,
1810
+ quarantineRef: input.quarantineRef,
1811
+ ...options.publishedHead ? { publishedHead: options.publishedHead } : {},
1812
+ rebaseCount: options.rebaseCount ?? 0,
1813
+ affectedProjects: affectedProjects(conflictPaths),
1814
+ affectedPaths: reportedPaths(conflictPaths),
1815
+ error: options.error,
1816
+ gitStatus: gitStatus(input)
1817
+ };
1818
+ } catch (error) {
1819
+ return {
1820
+ ...observed,
1821
+ outcome: "failed",
1822
+ rebaseCount: options.rebaseCount ?? 0,
1823
+ affectedProjects: affectedProjects(options.conflictPaths),
1824
+ affectedPaths: reportedPaths(options.conflictPaths),
1825
+ error: `Workspace synchronization found a conflict but could not preserve a durable candidate: ${error instanceof Error ? error.message : String(error)}`,
1826
+ gitStatus: gitStatus(input)
1827
+ };
1828
+ }
1829
+ }
1755
1830
  async function synchronizeWorkspace(rawInput) {
1756
1831
  let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
1757
1832
  try {
1758
1833
  input = { ...input, projects: workspaceProjectsForSync(input.projects, input.trigger) };
1834
+ const isCanonicalRemediationSync = input.skipVisibleMirror === true && (input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm");
1759
1835
  let mirroredGitlinkProjection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
1760
1836
  let fastForwardedFromHead = null;
1761
1837
  const shadowWasCreated = ensureShadowWorkspace(input);
1762
1838
  let hydrationPreimages = input.skipVisibleMirror ? hydrationPreimagesFromShadowRevision(input, shadowWasCreated ? null : revParse(input, "HEAD")) : /* @__PURE__ */ new Map();
1763
1839
  const remoteHeadAtStart = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1764
1840
  const result = baseResult(input, remoteHeadAtStart);
1765
- const inboundConflictsAtStart = input.skipVisibleMirror && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1841
+ const inboundConflictsAtStart = input.skipVisibleMirror && !isCanonicalRemediationSync && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1766
1842
  if (input.resetToCanonical) {
1767
1843
  if (input.trigger.canonicalCheckoutOnly) {
1768
1844
  throw new Error("A canonical-checkout-only synchronization cannot reset the authoritative resolver checkout");
@@ -1776,6 +1852,8 @@ async function synchronizeWorkspace(rawInput) {
1776
1852
  gitStatus: gitStatus(input)
1777
1853
  };
1778
1854
  }
1855
+ const inProgressRemediation = remediationRebaseInProgressResult(input, result);
1856
+ if (inProgressRemediation) return inProgressRemediation;
1779
1857
  if (!input.skipVisibleMirror) resetUncommittedShadowSnapshot(input);
1780
1858
  restoreUnscopedCanonicalCheckoutSubtrees(input);
1781
1859
  if (!input.skipVisibleMirror) {
@@ -1831,15 +1909,12 @@ async function synchronizeWorkspace(rawInput) {
1831
1909
  const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
1832
1910
  if (hasUnpushedCommit && inboundConflictsAtStart.length > 0) {
1833
1911
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflictsAtStart])].sort();
1834
- return {
1835
- ...observed,
1836
- outcome: "failed",
1837
- candidateHead: candidateHead ?? void 0,
1838
- affectedProjects: affectedProjects(conflictPaths),
1839
- affectedPaths: reportedPaths(conflictPaths),
1840
- error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation.",
1841
- gitStatus: gitStatus(input)
1842
- };
1912
+ return preserveWorkspaceConflictCandidate(input, observed, {
1913
+ baseRevision: candidateHead,
1914
+ expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
1915
+ conflictPaths,
1916
+ error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation."
1917
+ });
1843
1918
  }
1844
1919
  if (!hasUnpushedCommit) {
1845
1920
  const localHead = revParse(input, "HEAD");
@@ -1848,22 +1923,21 @@ async function synchronizeWorkspace(rawInput) {
1848
1923
  runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "update workspace from canonical state");
1849
1924
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean updated workspace");
1850
1925
  assertPublishedCanonicalCheckoutTree(input, currentRemoteHead, sampledCanonicalCheckoutTreeHash);
1851
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1852
- const inboundConflicts = input.trigger.canonicalCheckoutOnly ? [] : localHead ? hydrationConflictPaths(input, hydrationPreimages, localHead, currentRemoteHead) : visibleHydrationConflictPaths(input, hydrationPreimages, currentRemoteHead);
1926
+ if (!isCanonicalRemediationSync) {
1927
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1928
+ }
1929
+ const inboundConflicts = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : localHead ? hydrationConflictPaths(input, hydrationPreimages, localHead, currentRemoteHead) : visibleHydrationConflictPaths(input, hydrationPreimages, currentRemoteHead);
1853
1930
  if (inboundConflicts.length > 0 && localHead) {
1854
1931
  runGit(input, input.shadowRoot, ["reset", "--hard", localHead], "preserve old workspace base after inbound conflict");
1855
1932
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved old workspace base");
1856
1933
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflicts])].sort();
1857
- return {
1858
- ...observed,
1859
- outcome: "failed",
1860
- candidateHead: candidateHead ?? void 0,
1934
+ return preserveWorkspaceConflictCandidate(input, observed, {
1935
+ baseRevision: localHead,
1936
+ expectedHead: currentRemoteHead,
1861
1937
  publishedHead: currentRemoteHead,
1862
- affectedProjects: affectedProjects(conflictPaths),
1863
- affectedPaths: reportedPaths(conflictPaths),
1864
- error: "Inbound workspace update overlaps local visible changes; disjoint updates were hydrated and the old base was preserved for reconciliation.",
1865
- gitStatus: gitStatus(input)
1866
- };
1938
+ conflictPaths,
1939
+ error: "Inbound workspace update overlaps local visible changes; disjoint updates were hydrated and the old base was preserved for reconciliation."
1940
+ });
1867
1941
  }
1868
1942
  return {
1869
1943
  ...observed,
@@ -1876,8 +1950,10 @@ async function synchronizeWorkspace(rawInput) {
1876
1950
  const authoritativeHead = currentRemoteHead ?? localHead;
1877
1951
  if (authoritativeHead) {
1878
1952
  assertPublishedCanonicalCheckoutTree(input, authoritativeHead, sampledCanonicalCheckoutTreeHash);
1879
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1880
- const hydrationConflicts2 = input.trigger.canonicalCheckoutOnly ? [] : fastForwardedFromHead ? hydrationConflictPaths(input, hydrationPreimages, fastForwardedFromHead, authoritativeHead) : visibleHydrationConflictPaths(input, hydrationPreimages, authoritativeHead);
1953
+ if (!isCanonicalRemediationSync) {
1954
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1955
+ }
1956
+ const hydrationConflicts2 = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : fastForwardedFromHead ? hydrationConflictPaths(input, hydrationPreimages, fastForwardedFromHead, authoritativeHead) : visibleHydrationConflictPaths(input, hydrationPreimages, authoritativeHead);
1881
1957
  if (hydrationConflicts2.length > 0 && fastForwardedFromHead) {
1882
1958
  runGit(
1883
1959
  input,
@@ -1886,16 +1962,13 @@ async function synchronizeWorkspace(rawInput) {
1886
1962
  "preserve old workspace base after new-checkout fast-forward conflict"
1887
1963
  );
1888
1964
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved new-checkout workspace base");
1889
- return {
1890
- ...observed,
1891
- outcome: "failed",
1892
- candidateHead: candidateHead ?? void 0,
1965
+ return preserveWorkspaceConflictCandidate(input, observed, {
1966
+ baseRevision: fastForwardedFromHead,
1967
+ expectedHead: currentRemoteHead,
1893
1968
  publishedHead: currentRemoteHead ?? void 0,
1894
- affectedProjects: affectedProjects(hydrationConflicts2),
1895
- affectedPaths: reportedPaths(hydrationConflicts2),
1896
- error: "Workspace fast-forward for a new checkout overlaps local visible changes; compatible updates were hydrated and the old base was preserved for reconciliation.",
1897
- gitStatus: gitStatus(input)
1898
- };
1969
+ conflictPaths: hydrationConflicts2,
1970
+ error: "Workspace fast-forward for a new checkout overlaps local visible changes; compatible updates were hydrated and the old base was preserved for reconciliation."
1971
+ });
1899
1972
  }
1900
1973
  }
1901
1974
  return {
@@ -1915,17 +1988,14 @@ async function synchronizeWorkspace(rawInput) {
1915
1988
  const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
1916
1989
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort();
1917
1990
  runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
1918
- return {
1919
- ...observed,
1920
- outcome: "failed",
1991
+ return preserveWorkspaceConflictCandidate(input, observed, {
1992
+ baseRevision: candidateHead,
1921
1993
  expectedHead,
1922
- candidateHead: candidateHead ?? void 0,
1923
1994
  rebaseCount,
1924
- affectedProjects: affectedProjects(conflictPaths),
1925
- affectedPaths: reportedPaths(conflictPaths),
1926
- error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`,
1927
- gitStatus: gitStatus(input)
1928
- };
1995
+ preserveShadowCandidate: isCanonicalRemediationSync,
1996
+ conflictPaths,
1997
+ error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`
1998
+ });
1929
1999
  }
1930
2000
  candidateHead = revParse(input, "HEAD");
1931
2001
  assertPublishedCanonicalCheckoutTree(input, "HEAD", sampledCanonicalCheckoutTreeHash);
@@ -1941,8 +2011,10 @@ async function synchronizeWorkspace(rawInput) {
1941
2011
  };
1942
2012
  }
1943
2013
  assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
1944
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1945
- const hydrationConflicts = input.trigger.canonicalCheckoutOnly ? [] : candidateHeadBeforeRebase ? hydrationConflictPaths(input, hydrationPreimages, candidateHeadBeforeRebase, "HEAD") : visibleHydrationConflictPaths(input, hydrationPreimages, "HEAD");
2014
+ if (!isCanonicalRemediationSync) {
2015
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
2016
+ }
2017
+ const hydrationConflicts = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : candidateHeadBeforeRebase ? hydrationConflictPaths(input, hydrationPreimages, candidateHeadBeforeRebase, "HEAD") : visibleHydrationConflictPaths(input, hydrationPreimages, "HEAD");
1946
2018
  if (hydrationConflicts.length > 0 && candidateHeadBeforeRebase) {
1947
2019
  runGit(
1948
2020
  input,
@@ -1952,17 +2024,13 @@ async function synchronizeWorkspace(rawInput) {
1952
2024
  );
1953
2025
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace candidate");
1954
2026
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...hydrationConflicts])].sort();
1955
- return {
1956
- ...observed,
1957
- outcome: "failed",
2027
+ return preserveWorkspaceConflictCandidate(input, observed, {
2028
+ baseRevision: candidateHeadBeforeRebase,
1958
2029
  expectedHead,
1959
- candidateHead,
1960
2030
  rebaseCount,
1961
- affectedProjects: affectedProjects(conflictPaths),
1962
- affectedPaths: reportedPaths(conflictPaths),
1963
- error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved.",
1964
- gitStatus: gitStatus(input)
1965
- };
2031
+ conflictPaths,
2032
+ error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved."
2033
+ });
1966
2034
  }
1967
2035
  const upload = uploadWorkspaceCandidate(input, candidateHead);
1968
2036
  if (upload.exitCode !== 0) {
@@ -2047,6 +2115,9 @@ class WorkspaceSyncSingleFlight {
2047
2115
  fingerprintPrepared(prepare) {
2048
2116
  return this.enqueue(() => calculateWorkspaceDiffFingerprint(prepare()));
2049
2117
  }
2118
+ runExclusive(operation) {
2119
+ return this.enqueue(operation);
2120
+ }
2050
2121
  runMutation(operation) {
2051
2122
  return this.mutationGate.runMutation(operation);
2052
2123
  }
@@ -0,0 +1,69 @@
1
+ export declare const RECENT_WORKSPACE_SYNC_PROOF_MAX_AGE_MS = 15000;
2
+ export type WorkspaceSyncFastPathMissReason = "unsupported_trigger" | "canonical_checkout" | "visible_mirror_skipped" | "reset_requested" | "incident_active" | "worker_busy" | "pending_checkouts" | "canonical_head_missing" | "no_recent_proof" | "proof_invalidated" | "proof_expired" | "scope_changed" | "canonical_head_changed";
3
+ export type WorkspaceSyncFastPathDecision = {
4
+ hit: true;
5
+ reason: "recent_stable_no_change";
6
+ canonicalHead: string;
7
+ proofAgeMs: number;
8
+ } | {
9
+ hit: false;
10
+ reason: WorkspaceSyncFastPathMissReason;
11
+ };
12
+ type WorkspaceSyncScopeProject = {
13
+ projectId: string;
14
+ checkoutPathSegments: readonly string[];
15
+ projectPath: string;
16
+ repoHttpUrl: string | null;
17
+ defaultBranch: string;
18
+ branches: readonly string[];
19
+ canonicalCheckouts: ReadonlyArray<{
20
+ branchName: string;
21
+ scaffoldCommitHash: string;
22
+ }>;
23
+ };
24
+ type WorkspaceSyncFastPathInput = {
25
+ trigger: {
26
+ type: string;
27
+ canonicalCheckoutOnly?: boolean;
28
+ };
29
+ allowRecentNoChangeFastPath: boolean;
30
+ canonicalHead?: string | null;
31
+ scopeKey: string;
32
+ nowMs: number;
33
+ skipVisibleMirror?: boolean;
34
+ resetToCanonical?: boolean;
35
+ incidentActive: boolean;
36
+ workerBusy: boolean;
37
+ pendingCheckouts: boolean;
38
+ };
39
+ /**
40
+ * Fingerprints the inputs that determine which visible paths participate in a
41
+ * generic workspace scan. Authentication headers are deliberately excluded.
42
+ */
43
+ export declare function workspaceSyncScopeKey(remoteUrl: string, projects: readonly WorkspaceSyncScopeProject[]): string;
44
+ export declare function readShadowWorkspaceCanonicalHead(shadowRoot: string): string | null;
45
+ /**
46
+ * A short-lived proof cache for the duplicate scan between the worker's
47
+ * periodic stable-fingerprint pass and an immediately following agent start.
48
+ *
49
+ * The proof is process/connection local. Callers invalidate it before every
50
+ * known local mutation and retain an observation generation while a scan is
51
+ * running, so a mutation racing the scan cannot publish a usable proof.
52
+ */
53
+ export declare class RecentWorkspaceSyncProofCache {
54
+ private readonly maxAgeMs;
55
+ private generation;
56
+ private proof;
57
+ constructor(maxAgeMs?: number);
58
+ beginObservation(): number;
59
+ invalidate(): void;
60
+ recordStable(input: {
61
+ observationGeneration: number;
62
+ canonicalHead: string | null;
63
+ scopeKey: string;
64
+ observedAtMs: number;
65
+ workerBusy: boolean;
66
+ }): boolean;
67
+ evaluate(input: WorkspaceSyncFastPathInput): WorkspaceSyncFastPathDecision;
68
+ }
69
+ export {};
@@ -32,7 +32,7 @@ export type WorkspaceSyncTrigger = {
32
32
  canonicalCheckoutOnly?: boolean;
33
33
  detail?: string;
34
34
  };
35
- export type WorkspaceSyncOutcome = "no_change" | "candidate_ready" | "published" | "updated" | "conflict_reset" | "large_diff_blocked" | "reset" | "failed";
35
+ export type WorkspaceSyncOutcome = "no_change" | "candidate_ready" | "published" | "updated" | "conflict_reset" | "large_diff_blocked" | "conflict_blocked" | "reset" | "failed";
36
36
  export type WorkspaceSyncResult = {
37
37
  type: "workspace_sync";
38
38
  attemptId: string;
@@ -52,6 +52,17 @@ export type WorkspaceSyncResult = {
52
52
  discardedPaths: string[];
53
53
  localChangesDiscarded: boolean;
54
54
  sampledCanonicalCheckoutTreeHash?: string;
55
+ telemetry?: {
56
+ totalMs: number;
57
+ queueMs: number;
58
+ prepareMs: number;
59
+ synchronizeMs: number;
60
+ fastPath: {
61
+ hit: boolean;
62
+ reason: string;
63
+ proofAgeMs?: number;
64
+ };
65
+ };
55
66
  error?: string;
56
67
  };
57
68
  export type WorkspaceSyncInput = {
@@ -122,6 +133,7 @@ export declare class WorkspaceSyncSingleFlight {
122
133
  runPrepared(prepare: () => WorkspaceSyncInput): Promise<WorkspaceSyncResult>;
123
134
  fingerprint(input: WorkspaceSyncInput): Promise<string>;
124
135
  fingerprintPrepared(prepare: () => WorkspaceSyncInput): Promise<string>;
136
+ runExclusive<T>(operation: () => Promise<T> | T): Promise<T>;
125
137
  runMutation<T>(operation: () => Promise<T> | T): Promise<T>;
126
138
  acquireMutation(): Promise<WorkspaceMutationLease>;
127
139
  afterCurrent(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",