@ricsam/r5d-worker 0.0.57 → 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.
- package/dist/cjs/main.cjs +133 -12
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-sync-fast-path.cjs +108 -0
- package/dist/cjs/workspace-sync.cjs +124 -53
- package/dist/mjs/main.mjs +137 -12
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-sync-fast-path.mjs +81 -0
- package/dist/mjs/workspace-sync.mjs +124 -53
- package/dist/types/workspace-sync-fast-path.d.ts +69 -0
- package/dist/types/workspace-sync.d.ts +13 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
|
|
1852
|
-
|
|
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
|
-
|
|
1859
|
-
|
|
1860
|
-
candidateHead: candidateHead ?? void 0,
|
|
1934
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
1935
|
+
baseRevision: localHead,
|
|
1936
|
+
expectedHead: currentRemoteHead,
|
|
1861
1937
|
publishedHead: currentRemoteHead,
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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
|
-
|
|
1880
|
-
|
|
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
|
-
|
|
1891
|
-
|
|
1892
|
-
candidateHead: candidateHead ?? void 0,
|
|
1965
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
1966
|
+
baseRevision: fastForwardedFromHead,
|
|
1967
|
+
expectedHead: currentRemoteHead,
|
|
1893
1968
|
publishedHead: currentRemoteHead ?? void 0,
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
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
|
-
|
|
1920
|
-
outcome: "failed",
|
|
1991
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
1992
|
+
baseRevision: candidateHead,
|
|
1921
1993
|
expectedHead,
|
|
1922
|
-
candidateHead: candidateHead ?? void 0,
|
|
1923
1994
|
rebaseCount,
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation
|
|
1927
|
-
|
|
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
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
1957
|
-
outcome: "failed",
|
|
2027
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
2028
|
+
baseRevision: candidateHeadBeforeRebase,
|
|
1958
2029
|
expectedHead,
|
|
1959
|
-
candidateHead,
|
|
1960
2030
|
rebaseCount,
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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>;
|