@ricsam/r5d-worker 0.0.58 → 0.0.60

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,213 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const WORKSPACE_PUBLICATION_QUIET_MS = 5e3;
4
+ const WORKSPACE_IDLE_SAFETY_SCAN_MS = 6e4;
5
+ const WORKSPACE_MANIFEST_REVISION_PATTERN = /^[0-9a-f]{64}$/;
6
+ class WorkspaceManifestRevisionFence {
7
+ sequence = 0;
8
+ requestedRevision = null;
9
+ completedRevision = null;
10
+ begin(revision) {
11
+ if (!WORKSPACE_MANIFEST_REVISION_PATTERN.test(revision)) {
12
+ throw new Error("Workspace manifest revision must be a lowercase SHA-256 hash");
13
+ }
14
+ this.sequence += 1;
15
+ this.requestedRevision = revision;
16
+ this.completedRevision = null;
17
+ return { sequence: this.sequence, revision };
18
+ }
19
+ isCurrent(token) {
20
+ return token.sequence === this.sequence && token.revision === this.requestedRevision;
21
+ }
22
+ complete(token) {
23
+ if (!this.isCurrent(token)) return false;
24
+ this.completedRevision = token.revision;
25
+ return true;
26
+ }
27
+ readyRevision() {
28
+ return this.completedRevision === this.requestedRevision ? this.completedRevision : null;
29
+ }
30
+ }
31
+ function workspacePublicationTelemetry(input) {
32
+ return {
33
+ totalMs: Math.max(0, input.finishedAt - input.requestedAt),
34
+ queueMs: Math.max(0, input.prepareStartedAt - input.requestedAt),
35
+ prepareMs: Math.max(0, input.synchronizeStartedAt - input.prepareStartedAt),
36
+ synchronizeMs: Math.max(0, input.finishedAt - input.synchronizeStartedAt)
37
+ };
38
+ }
39
+ function workspacePublicationIncidentAction(requestId) {
40
+ return requestId ? "request_terminal_authority" : "defer_automatic";
41
+ }
42
+ class ExplicitWorkspacePublicationQueue {
43
+ scheduled;
44
+ queued = [];
45
+ schedule(publication, pendingRequestId) {
46
+ if (pendingRequestId === publication.requestId || this.scheduled?.requestId === publication.requestId || this.queued.some((queued) => queued.requestId === publication.requestId)) {
47
+ return false;
48
+ }
49
+ if (pendingRequestId || this.scheduled) {
50
+ this.queued.push(publication);
51
+ return false;
52
+ }
53
+ this.scheduled = publication;
54
+ return true;
55
+ }
56
+ hasScheduled() {
57
+ return this.scheduled !== void 0;
58
+ }
59
+ consumeScheduled(requestId) {
60
+ if (this.scheduled?.requestId === requestId) this.scheduled = void 0;
61
+ }
62
+ enqueue(publication, pendingRequestId) {
63
+ if (pendingRequestId === publication.requestId || this.scheduled?.requestId === publication.requestId || this.queued.some((queued) => queued.requestId === publication.requestId)) {
64
+ return;
65
+ }
66
+ this.queued.push(publication);
67
+ }
68
+ next() {
69
+ return this.queued.shift();
70
+ }
71
+ }
72
+ const EMPTY_STATE = {
73
+ localHead: null,
74
+ desiredCanonicalHead: null,
75
+ dirtyGeneration: 0,
76
+ publishedGeneration: 0
77
+ };
78
+ function validHead(value) {
79
+ return value === null || typeof value === "string" && /^[0-9a-f]{40,64}$/.test(value);
80
+ }
81
+ function validGeneration(value) {
82
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
83
+ }
84
+ function readWorkspaceConvergenceState(statePath) {
85
+ try {
86
+ const parsed = JSON.parse(fs.readFileSync(statePath, "utf8"));
87
+ if (!validHead(parsed.localHead) || !validHead(parsed.desiredCanonicalHead) || !validGeneration(parsed.dirtyGeneration) || !validGeneration(parsed.publishedGeneration) || parsed.publishedGeneration > parsed.dirtyGeneration) {
88
+ return { ...EMPTY_STATE };
89
+ }
90
+ return {
91
+ localHead: parsed.localHead,
92
+ desiredCanonicalHead: parsed.desiredCanonicalHead,
93
+ dirtyGeneration: parsed.dirtyGeneration,
94
+ publishedGeneration: parsed.publishedGeneration
95
+ };
96
+ } catch {
97
+ return { ...EMPTY_STATE };
98
+ }
99
+ }
100
+ class WorkspaceConvergenceState {
101
+ constructor(statePath) {
102
+ this.statePath = statePath;
103
+ this.state = readWorkspaceConvergenceState(statePath);
104
+ this.publicationState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
105
+ }
106
+ statePath;
107
+ state;
108
+ publicationState;
109
+ snapshot() {
110
+ return { ...this.state, publicationState: this.publicationState };
111
+ }
112
+ setLocalHead(localHead) {
113
+ if (!validHead(localHead)) throw new Error("Invalid local workspace head");
114
+ this.state.localHead = localHead;
115
+ this.persist();
116
+ }
117
+ observeCanonicalHead(canonicalHead) {
118
+ if (!validHead(canonicalHead)) throw new Error("Invalid canonical workspace head");
119
+ if (this.state.desiredCanonicalHead === canonicalHead) return false;
120
+ this.state.desiredCanonicalHead = canonicalHead;
121
+ this.persist();
122
+ return true;
123
+ }
124
+ markDirty() {
125
+ this.state.dirtyGeneration += 1;
126
+ if (this.publicationState === "idle") this.publicationState = "scheduled";
127
+ this.persist();
128
+ return this.state.dirtyGeneration;
129
+ }
130
+ schedule() {
131
+ if (this.publicationState === "idle") this.publicationState = "scheduled";
132
+ }
133
+ beginPreparing() {
134
+ if (this.publicationState === "preparing" || this.publicationState === "submitting") {
135
+ throw new Error("A workspace publication is already in flight");
136
+ }
137
+ this.publicationState = "preparing";
138
+ }
139
+ beginSubmitting() {
140
+ if (this.publicationState !== "preparing") throw new Error("Workspace publication authority was not prepared");
141
+ this.publicationState = "submitting";
142
+ }
143
+ defer() {
144
+ this.publicationState = "scheduled";
145
+ }
146
+ block() {
147
+ this.publicationState = "blocked";
148
+ }
149
+ releaseBlock() {
150
+ this.publicationState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
151
+ }
152
+ observeIncidentState(blocked) {
153
+ if (this.publicationState === "preparing" || this.publicationState === "submitting") return;
154
+ if (blocked) this.block();
155
+ else this.releaseBlock();
156
+ }
157
+ resetTransientPublicationState() {
158
+ this.publicationState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
159
+ }
160
+ complete(input) {
161
+ if (!validGeneration(input.generation) || input.generation > this.state.dirtyGeneration) {
162
+ throw new Error("Invalid completed workspace generation");
163
+ }
164
+ if (!validHead(input.canonicalHead)) throw new Error("Invalid completed canonical workspace head");
165
+ if (input.published) {
166
+ this.state.publishedGeneration = Math.max(this.state.publishedGeneration, input.generation);
167
+ this.state.localHead = input.canonicalHead;
168
+ this.state.desiredCanonicalHead = input.canonicalHead;
169
+ }
170
+ this.publicationState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
171
+ this.persist();
172
+ }
173
+ persist() {
174
+ fs.mkdirSync(path.dirname(this.statePath), { recursive: true });
175
+ const temporaryPath = `${this.statePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
176
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(this.state)}
177
+ `, { mode: 384 });
178
+ fs.renameSync(temporaryPath, this.statePath);
179
+ }
180
+ }
181
+ const PROCESS_WORKSPACE_CONVERGENCE_STATES = /* @__PURE__ */ new Map();
182
+ function processWorkspaceConvergenceState(statePath) {
183
+ const key = path.resolve(statePath);
184
+ const existing = PROCESS_WORKSPACE_CONVERGENCE_STATES.get(key);
185
+ if (existing) return existing;
186
+ const created = new WorkspaceConvergenceState(key);
187
+ PROCESS_WORKSPACE_CONVERGENCE_STATES.set(key, created);
188
+ return created;
189
+ }
190
+ async function waitForWorkspacePublicationOnShutdown(input) {
191
+ input.forcePublication();
192
+ const deadline = Date.now() + (input.timeoutMs ?? 1e4);
193
+ const pollMs = input.pollMs ?? 50;
194
+ while (Date.now() < deadline) {
195
+ const snapshot = input.snapshot();
196
+ if (snapshot.publicationState === "blocked") return "blocked";
197
+ if (snapshot.publicationState === "idle" && snapshot.publishedGeneration >= snapshot.dirtyGeneration) return "settled";
198
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
199
+ }
200
+ return "timed_out";
201
+ }
202
+ export {
203
+ ExplicitWorkspacePublicationQueue,
204
+ WORKSPACE_IDLE_SAFETY_SCAN_MS,
205
+ WORKSPACE_PUBLICATION_QUIET_MS,
206
+ WorkspaceConvergenceState,
207
+ WorkspaceManifestRevisionFence,
208
+ processWorkspaceConvergenceState,
209
+ readWorkspaceConvergenceState,
210
+ waitForWorkspacePublicationOnShutdown,
211
+ workspacePublicationIncidentAction,
212
+ workspacePublicationTelemetry
213
+ };
@@ -1,14 +1,42 @@
1
+ const DEFAULT_TERMINAL_INCIDENT_HISTORY_LIMIT = 32;
2
+ function isTerminalWorkspaceIncidentUpdate(update) {
3
+ return update.incidentId !== null && (update.status === "resolved" || update.status === "confirmed" || update.status === "reset");
4
+ }
5
+ class WorkspaceIncidentOrderingFence {
6
+ constructor(historyLimit = DEFAULT_TERMINAL_INCIDENT_HISTORY_LIMIT) {
7
+ this.historyLimit = historyLimit;
8
+ if (!Number.isSafeInteger(historyLimit) || historyLimit < 1) {
9
+ throw new Error("Workspace incident history limit must be a positive integer");
10
+ }
11
+ }
12
+ historyLimit;
13
+ terminalIncidentIds = /* @__PURE__ */ new Set();
14
+ terminalIncidentOrder = [];
15
+ observe(update) {
16
+ if (!isTerminalWorkspaceIncidentUpdate(update) || this.terminalIncidentIds.has(update.incidentId)) return;
17
+ this.terminalIncidentIds.add(update.incidentId);
18
+ this.terminalIncidentOrder.push(update.incidentId);
19
+ while (this.terminalIncidentOrder.length > this.historyLimit) {
20
+ const expired = this.terminalIncidentOrder.shift();
21
+ if (expired) this.terminalIncidentIds.delete(expired);
22
+ }
23
+ }
24
+ permitsBlockedResponse(incidentId) {
25
+ if (!incidentId) return false;
26
+ return !this.terminalIncidentIds.has(incidentId);
27
+ }
28
+ }
1
29
  function applyWorkspaceIncidentUpdate(currentIncidentId, update) {
2
30
  if (update.status === "remediating" || update.status === "waiting_for_worker") return update.incidentId;
3
31
  if (currentIncidentId && update.incidentId && update.incidentId !== currentIncidentId) return currentIncidentId;
4
32
  return null;
5
33
  }
6
34
  function releasePendingWorkspaceHead(previousIncidentId, currentIncidentId, terminalIncidentId) {
7
- if (!previousIncidentId || currentIncidentId) return { syncHead: null };
8
- if (terminalIncidentId === previousIncidentId) return { syncHead: "authoritative" };
9
- return { syncHead: null };
35
+ if (!previousIncidentId || currentIncidentId) return { fetchAuthoritativeHead: false };
36
+ return { fetchAuthoritativeHead: terminalIncidentId === previousIncidentId };
10
37
  }
11
38
  export {
39
+ WorkspaceIncidentOrderingFence,
12
40
  applyWorkspaceIncidentUpdate,
13
41
  releasePendingWorkspaceHead
14
42
  };
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { legacyProjectSlug, managedProjectRoot } from "./managed-paths.mjs";
4
+ function hasNormalGitDirectory(checkoutPath) {
5
+ const gitPath = path.join(checkoutPath, ".git");
6
+ return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
7
+ }
8
+ function inspectWorkspaceManifestFilesystem(input) {
9
+ const firstBootstrap = !hasNormalGitDirectory(input.workspaceShadowRoot);
10
+ const missingCheckouts = [];
11
+ const migratedProjectIds = [];
12
+ for (const project of input.projects) {
13
+ const projectRoot = managedProjectRoot(input.projectsRoot, project.checkoutPathSegments);
14
+ const legacyRoot = path.join(input.projectsRoot, legacyProjectSlug(project.projectPath, project.projectId));
15
+ if (fs.existsSync(legacyRoot)) migratedProjectIds.push(project.projectId);
16
+ for (const branchName of project.branches) {
17
+ if (!hasNormalGitDirectory(path.join(projectRoot, branchName))) {
18
+ missingCheckouts.push({ projectId: project.projectId, branchName });
19
+ }
20
+ }
21
+ }
22
+ return {
23
+ firstBootstrap,
24
+ missingCheckouts,
25
+ migratedProjectIds
26
+ };
27
+ }
28
+ export {
29
+ inspectWorkspaceManifestFilesystem
30
+ };
@@ -5,8 +5,8 @@ import { managedBranchPath } from "./managed-paths.mjs";
5
5
  import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
6
6
  const WORKSPACE_BRANCH = "main";
7
7
  const WORKSPACE_INTENT_REF_PREFIX = "refs/r5d/workspace-intents/";
8
+ const WORKSPACE_PUBLICATION_REF_PREFIX = "refs/r5d/workspace-publications/";
8
9
  const MAX_WORKSPACE_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
9
- const WORKSPACE_PERIODIC_SCAN_INTERVAL_MS = 5e3;
10
10
  const MAX_REPORTED_WORKSPACE_PATHS = 2e3;
11
11
  const MAX_REPORTED_WORKSPACE_STATUS_BYTES = 256 * 1024;
12
12
  const MAX_MIRROR_COMPARISON_CACHE_ENTRIES = 2e5;
@@ -1705,9 +1705,10 @@ function commitMessage(input) {
1705
1705
  });
1706
1706
  }
1707
1707
  function assertWorkspaceQuarantineRef(quarantineRef) {
1708
- const suffix = quarantineRef.slice(WORKSPACE_INTENT_REF_PREFIX.length);
1709
- if (!quarantineRef.startsWith(WORKSPACE_INTENT_REF_PREFIX) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(suffix)) {
1710
- throw new Error(`Workspace candidate ref must be below ${WORKSPACE_INTENT_REF_PREFIX}`);
1708
+ const prefix = quarantineRef.startsWith(WORKSPACE_PUBLICATION_REF_PREFIX) ? WORKSPACE_PUBLICATION_REF_PREFIX : WORKSPACE_INTENT_REF_PREFIX;
1709
+ const suffix = quarantineRef.slice(prefix.length);
1710
+ if (!quarantineRef.startsWith(prefix) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(suffix)) {
1711
+ throw new Error(`Workspace candidate ref must be below ${WORKSPACE_PUBLICATION_REF_PREFIX} or ${WORKSPACE_INTENT_REF_PREFIX}`);
1711
1712
  }
1712
1713
  const result = Bun.spawnSync(["git", "check-ref-format", quarantineRef], {
1713
1714
  stdout: "pipe",
@@ -1827,6 +1828,36 @@ function preserveWorkspaceConflictCandidate(input, observed, options) {
1827
1828
  };
1828
1829
  }
1829
1830
  }
1831
+ function preserveLargeDiffCandidate(input, observed, error) {
1832
+ try {
1833
+ if (stagedPaths(input).length > 0) {
1834
+ runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit blocked workspace candidate");
1835
+ }
1836
+ const candidateHead = revParse(input, "HEAD");
1837
+ if (!candidateHead) throw new Error("the blocked workspace candidate does not have a commit");
1838
+ assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1839
+ const upload = uploadWorkspaceCandidate(input, candidateHead);
1840
+ if (upload.exitCode !== 0) {
1841
+ throw new Error(upload.stderr || upload.stdout || "blocked workspace candidate upload failed");
1842
+ }
1843
+ return {
1844
+ ...observed,
1845
+ outcome: "large_diff_blocked",
1846
+ expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
1847
+ candidateHead,
1848
+ quarantineRef: input.quarantineRef,
1849
+ error,
1850
+ gitStatus: gitStatus(input)
1851
+ };
1852
+ } catch (candidateError) {
1853
+ return {
1854
+ ...observed,
1855
+ outcome: "failed",
1856
+ error: `Workspace safety guard blocked publication but could not preserve a durable candidate: ${candidateError instanceof Error ? candidateError.message : String(candidateError)}`,
1857
+ gitStatus: gitStatus(input)
1858
+ };
1859
+ }
1860
+ }
1830
1861
  async function synchronizeWorkspace(rawInput) {
1831
1862
  let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
1832
1863
  try {
@@ -1838,6 +1869,15 @@ async function synchronizeWorkspace(rawInput) {
1838
1869
  let hydrationPreimages = input.skipVisibleMirror ? hydrationPreimagesFromShadowRevision(input, shadowWasCreated ? null : revParse(input, "HEAD")) : /* @__PURE__ */ new Map();
1839
1870
  const remoteHeadAtStart = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1840
1871
  const result = baseResult(input, remoteHeadAtStart);
1872
+ if (input.expectedCanonicalHead !== void 0 && remoteHeadAtStart !== input.expectedCanonicalHead) {
1873
+ return {
1874
+ ...result,
1875
+ outcome: "failed",
1876
+ expectedHead: input.expectedCanonicalHead,
1877
+ publishedHead: remoteHeadAtStart ?? void 0,
1878
+ error: `canonical_head_advanced: expected ${input.expectedCanonicalHead ?? "unborn"}, fetched ${remoteHeadAtStart ?? "unborn"}`
1879
+ };
1880
+ }
1841
1881
  const inboundConflictsAtStart = input.skipVisibleMirror && !isCanonicalRemediationSync && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1842
1882
  if (input.resetToCanonical) {
1843
1883
  if (input.trigger.canonicalCheckoutOnly) {
@@ -1886,18 +1926,14 @@ async function synchronizeWorkspace(rawInput) {
1886
1926
  const destructiveReductions = destructiveCheckoutReductions(input, baseRevision);
1887
1927
  if (destructiveReductions.length > 0 && !input.confirmedLargeDiff) {
1888
1928
  const summary = destructiveReductions.map(({ root, trackedBefore, trackedAfter }) => `${root} (${trackedBefore} -> ${trackedAfter} tracked files)`).join(", ");
1889
- return {
1890
- ...observed,
1891
- outcome: "large_diff_blocked",
1892
- error: `Workspace safety guard blocked a destructive checkout reduction: ${summary}`
1893
- };
1929
+ return preserveLargeDiffCandidate(input, observed, `Workspace safety guard blocked a destructive checkout reduction: ${summary}`);
1894
1930
  }
1895
1931
  if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff && !input.trigger.canonicalCheckoutOnly) {
1896
- return {
1897
- ...observed,
1898
- outcome: "large_diff_blocked",
1899
- error: `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
1900
- };
1932
+ return preserveLargeDiffCandidate(
1933
+ input,
1934
+ observed,
1935
+ `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
1936
+ );
1901
1937
  }
1902
1938
  if (stagedWorkingPaths.length > 0) {
1903
1939
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
@@ -2062,6 +2098,79 @@ async function synchronizeWorkspace(rawInput) {
2062
2098
  };
2063
2099
  }
2064
2100
  }
2101
+ async function convergeWorkspaceHead(rawInput, options) {
2102
+ const input = {
2103
+ ...rawInput,
2104
+ projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2105
+ };
2106
+ if (input.trigger.canonicalCheckoutOnly) {
2107
+ throw new Error("Ordinary inbound convergence cannot target a canonical resolver checkout");
2108
+ }
2109
+ ensureShadowWorkspace(input);
2110
+ const fetchedLocalHead = revParse(input, "HEAD");
2111
+ const fetchedCanonicalHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2112
+ if (!options.hydrateVisible) {
2113
+ return {
2114
+ localHead: fetchedLocalHead,
2115
+ canonicalHead: fetchedCanonicalHead,
2116
+ localChanges: false,
2117
+ hydrated: false,
2118
+ hydrationConflicts: []
2119
+ };
2120
+ }
2121
+ resetUncommittedShadowSnapshot(input);
2122
+ restoreUnscopedCanonicalCheckoutSubtrees(input);
2123
+ const localHead = fetchedLocalHead;
2124
+ const canonicalHead = fetchedCanonicalHead;
2125
+ const projection = mirrorVisibleWorkspaceToShadow(input);
2126
+ runGit(
2127
+ input,
2128
+ input.shadowRoot,
2129
+ ["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2130
+ "stage inbound workspace observation"
2131
+ );
2132
+ forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
2133
+ const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write inbound workspace observation tree");
2134
+ const headTree = localHead ? revParse(input, `${localHead}^{tree}`) : null;
2135
+ const localChanges = stagedTree !== headTree;
2136
+ if (!options.hydrateVisible || localChanges || !canonicalHead || localHead === canonicalHead) {
2137
+ resetUncommittedShadowSnapshot(input);
2138
+ return { localHead, canonicalHead, localChanges, hydrated: false, hydrationConflicts: [] };
2139
+ }
2140
+ const preimages = projection.visiblePreimages ?? hydrationPreimagesFromShadowRevision(input, localHead);
2141
+ const hydrationConflicts = localHead ? hydrationConflictPaths(input, preimages, localHead, canonicalHead) : [];
2142
+ if (hydrationConflicts.length > 0) {
2143
+ resetUncommittedShadowSnapshot(input);
2144
+ return { localHead, canonicalHead, localChanges: true, hydrated: false, hydrationConflicts };
2145
+ }
2146
+ runGit(input, input.shadowRoot, ["reset", "--hard", canonicalHead], "fast-forward workspace to available canonical head");
2147
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean fast-forwarded workspace");
2148
+ mirrorShadowWorkspaceToVisible(input, projection.opaqueRoots, preimages);
2149
+ return { localHead: canonicalHead, canonicalHead, localChanges: false, hydrated: true, hydrationConflicts: [] };
2150
+ }
2151
+ function calculateWorkspaceLocalDiffFingerprint(rawInput) {
2152
+ const input = {
2153
+ ...rawInput,
2154
+ projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2155
+ };
2156
+ if (!fs.existsSync(path.join(input.shadowRoot, ".git"))) {
2157
+ throw new Error("Cannot inspect local workspace changes before first bootstrap");
2158
+ }
2159
+ resetUncommittedShadowSnapshot(input);
2160
+ restoreUnscopedCanonicalCheckoutSubtrees(input);
2161
+ const projection = mirrorVisibleWorkspaceToShadow(input);
2162
+ runGit(
2163
+ input,
2164
+ input.shadowRoot,
2165
+ ["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2166
+ "stage local workspace fingerprint"
2167
+ );
2168
+ forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
2169
+ const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write local workspace fingerprint tree");
2170
+ const headTree = revParse(input, "HEAD^{tree}");
2171
+ resetUncommittedShadowSnapshot(input);
2172
+ return stagedTree === headTree ? createHash("sha256").update("").digest("hex") : createHash("sha256").update(stagedTree).digest("hex");
2173
+ }
2065
2174
  function calculateWorkspaceDiffFingerprint(rawInput) {
2066
2175
  const input = {
2067
2176
  ...rawInput,
@@ -2133,11 +2242,13 @@ export {
2133
2242
  WORKSPACE_BRANCH,
2134
2243
  WORKSPACE_INTENT_REF_PREFIX,
2135
2244
  WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
2136
- WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
2245
+ WORKSPACE_PUBLICATION_REF_PREFIX,
2137
2246
  WorkspaceSyncSingleFlight,
2138
2247
  assertCanonicalCheckoutIndexMatchesWorktree,
2139
2248
  assertWorkspaceQuarantineRef,
2140
2249
  calculateWorkspaceDiffFingerprint,
2250
+ calculateWorkspaceLocalDiffFingerprint,
2251
+ convergeWorkspaceHead,
2141
2252
  encodeWorkspaceBranch,
2142
2253
  mirrorShadowWorkspaceToVisible,
2143
2254
  mirrorVisibleWorkspaceToShadow,
@@ -0,0 +1,82 @@
1
+ export declare const WORKSPACE_PUBLICATION_QUIET_MS = 5000;
2
+ export declare const WORKSPACE_IDLE_SAFETY_SCAN_MS = 60000;
3
+ export type WorkspacePublicationState = "idle" | "scheduled" | "preparing" | "submitting" | "blocked";
4
+ export type WorkspaceConvergenceSnapshot = {
5
+ localHead: string | null;
6
+ desiredCanonicalHead: string | null;
7
+ dirtyGeneration: number;
8
+ publishedGeneration: number;
9
+ publicationState: WorkspacePublicationState;
10
+ };
11
+ export type ExplicitWorkspacePublication<TTrigger> = {
12
+ requestId: string;
13
+ trigger: TTrigger;
14
+ };
15
+ export type WorkspaceManifestRevisionToken = {
16
+ sequence: number;
17
+ revision: string;
18
+ };
19
+ export declare class WorkspaceManifestRevisionFence {
20
+ private sequence;
21
+ private requestedRevision;
22
+ private completedRevision;
23
+ begin(revision: string): WorkspaceManifestRevisionToken;
24
+ isCurrent(token: WorkspaceManifestRevisionToken): boolean;
25
+ complete(token: WorkspaceManifestRevisionToken): boolean;
26
+ readyRevision(): string | null;
27
+ }
28
+ export declare function workspacePublicationTelemetry(input: {
29
+ requestedAt: number;
30
+ prepareStartedAt: number;
31
+ synchronizeStartedAt: number;
32
+ finishedAt: number;
33
+ }): {
34
+ totalMs: number;
35
+ queueMs: number;
36
+ prepareMs: number;
37
+ synchronizeMs: number;
38
+ };
39
+ export declare function workspacePublicationIncidentAction(requestId: string | undefined): "defer_automatic" | "request_terminal_authority";
40
+ export declare class ExplicitWorkspacePublicationQueue<TTrigger> {
41
+ private scheduled;
42
+ private readonly queued;
43
+ schedule(publication: ExplicitWorkspacePublication<TTrigger>, pendingRequestId?: string): boolean;
44
+ hasScheduled(): boolean;
45
+ consumeScheduled(requestId: string): void;
46
+ enqueue(publication: ExplicitWorkspacePublication<TTrigger>, pendingRequestId?: string): void;
47
+ next(): ExplicitWorkspacePublication<TTrigger> | undefined;
48
+ }
49
+ type PersistedWorkspaceConvergenceState = Omit<WorkspaceConvergenceSnapshot, "publicationState">;
50
+ export declare function readWorkspaceConvergenceState(statePath: string): PersistedWorkspaceConvergenceState;
51
+ export declare class WorkspaceConvergenceState {
52
+ private readonly statePath;
53
+ private state;
54
+ private publicationState;
55
+ constructor(statePath: string);
56
+ snapshot(): WorkspaceConvergenceSnapshot;
57
+ setLocalHead(localHead: string | null): void;
58
+ observeCanonicalHead(canonicalHead: string | null): boolean;
59
+ markDirty(): number;
60
+ schedule(): void;
61
+ beginPreparing(): void;
62
+ beginSubmitting(): void;
63
+ defer(): void;
64
+ block(): void;
65
+ releaseBlock(): void;
66
+ observeIncidentState(blocked: boolean): void;
67
+ resetTransientPublicationState(): void;
68
+ complete(input: {
69
+ generation: number;
70
+ canonicalHead: string | null;
71
+ published: boolean;
72
+ }): void;
73
+ private persist;
74
+ }
75
+ export declare function processWorkspaceConvergenceState(statePath: string): WorkspaceConvergenceState;
76
+ export declare function waitForWorkspacePublicationOnShutdown(input: {
77
+ snapshot: () => WorkspaceConvergenceSnapshot;
78
+ forcePublication: () => void;
79
+ timeoutMs?: number;
80
+ pollMs?: number;
81
+ }): Promise<"settled" | "blocked" | "timed_out">;
82
+ export {};
@@ -2,7 +2,21 @@ export type WorkspaceIncidentUpdate = {
2
2
  incidentId: string | null;
3
3
  status: "remediating" | "waiting_for_worker" | "resolved" | "confirmed" | "reset" | null;
4
4
  };
5
+ /**
6
+ * WebSocket writes are ordered, but independent server handlers can decide an
7
+ * incident state under the user lock and write their messages after releasing
8
+ * it. Remember terminal incident IDs so a delayed publication response cannot
9
+ * resurrect a barrier that this connection has already observed as terminal.
10
+ */
11
+ export declare class WorkspaceIncidentOrderingFence {
12
+ private readonly historyLimit;
13
+ private readonly terminalIncidentIds;
14
+ private readonly terminalIncidentOrder;
15
+ constructor(historyLimit?: number);
16
+ observe(update: WorkspaceIncidentUpdate): void;
17
+ permitsBlockedResponse(incidentId: string | null | undefined): boolean;
18
+ }
5
19
  export declare function applyWorkspaceIncidentUpdate(currentIncidentId: string | null, update: WorkspaceIncidentUpdate): string | null;
6
20
  export declare function releasePendingWorkspaceHead(previousIncidentId: string | null, currentIncidentId: string | null, terminalIncidentId?: string | null): {
7
- syncHead: string | null;
21
+ fetchAuthoritativeHead: boolean;
8
22
  };
@@ -0,0 +1,24 @@
1
+ export type WorkspaceManifestCheckout = {
2
+ projectId: string;
3
+ projectPath: string;
4
+ checkoutPathSegments: [namespace: string, project: string];
5
+ branches: string[];
6
+ };
7
+ export type WorkspaceManifestFilesystemState = {
8
+ firstBootstrap: boolean;
9
+ missingCheckouts: Array<{
10
+ projectId: string;
11
+ branchName: string;
12
+ }>;
13
+ migratedProjectIds: string[];
14
+ };
15
+ /**
16
+ * Inspects manifest-related filesystem state without reserving visible paths.
17
+ * Missing checkouts are target-specific pending work and hydrate
18
+ * opportunistically without delaying unrelated targets or running commands.
19
+ */
20
+ export declare function inspectWorkspaceManifestFilesystem(input: {
21
+ projectsRoot: string;
22
+ workspaceShadowRoot: string;
23
+ projects: WorkspaceManifestCheckout[];
24
+ }): WorkspaceManifestFilesystemState;