@ricsam/r5d-worker 0.0.58 → 0.0.59

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.59",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,275 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const WORKSPACE_CHECKPOINT_QUIET_MS = 5e3;
4
+ const WORKSPACE_IDLE_SAFETY_SCAN_MS = 6e4;
5
+ const WORKSPACE_MANIFEST_REVISION_PATTERN = /^[0-9a-f]{64}$/;
6
+ class WorkspaceFilesystemWriterGate {
7
+ activeWriters = 0;
8
+ checkpointActive = false;
9
+ waitingWriters = [];
10
+ waitingCheckpoints = [];
11
+ acquireWriter() {
12
+ if (!this.checkpointActive && this.waitingCheckpoints.length === 0) {
13
+ this.activeWriters += 1;
14
+ return Promise.resolve(this.writerRelease());
15
+ }
16
+ return new Promise((resolve) => this.waitingWriters.push(resolve));
17
+ }
18
+ tryAcquireCheckpoint() {
19
+ if (this.checkpointActive || this.activeWriters > 0 || this.waitingCheckpoints.length > 0) return null;
20
+ this.checkpointActive = true;
21
+ return this.checkpointRelease();
22
+ }
23
+ acquireCheckpoint() {
24
+ const immediate = this.tryAcquireCheckpoint();
25
+ if (immediate) return Promise.resolve(immediate);
26
+ return new Promise((resolve) => this.waitingCheckpoints.push(resolve));
27
+ }
28
+ checkpointRelease() {
29
+ let released = false;
30
+ return () => {
31
+ if (released) return;
32
+ released = true;
33
+ this.checkpointActive = false;
34
+ this.drain();
35
+ };
36
+ }
37
+ writerRelease() {
38
+ let released = false;
39
+ return () => {
40
+ if (released) return;
41
+ released = true;
42
+ this.activeWriters -= 1;
43
+ this.drain();
44
+ };
45
+ }
46
+ drain() {
47
+ if (this.checkpointActive || this.activeWriters > 0) return;
48
+ const checkpoint = this.waitingCheckpoints.shift();
49
+ if (checkpoint) {
50
+ this.checkpointActive = true;
51
+ checkpoint(this.checkpointRelease());
52
+ return;
53
+ }
54
+ while (this.waitingWriters.length > 0) {
55
+ const resolve = this.waitingWriters.shift();
56
+ if (!resolve) continue;
57
+ this.activeWriters += 1;
58
+ resolve(this.writerRelease());
59
+ }
60
+ }
61
+ }
62
+ const PROCESS_WORKSPACE_FILESYSTEM_WRITERS = new WorkspaceFilesystemWriterGate();
63
+ function processWorkspaceFilesystemWriterGate() {
64
+ return PROCESS_WORKSPACE_FILESYSTEM_WRITERS;
65
+ }
66
+ class WorkspaceManifestRevisionFence {
67
+ sequence = 0;
68
+ requestedRevision = null;
69
+ completedRevision = null;
70
+ begin(revision) {
71
+ if (!WORKSPACE_MANIFEST_REVISION_PATTERN.test(revision)) {
72
+ throw new Error("Workspace manifest revision must be a lowercase SHA-256 hash");
73
+ }
74
+ this.sequence += 1;
75
+ this.requestedRevision = revision;
76
+ this.completedRevision = null;
77
+ return { sequence: this.sequence, revision };
78
+ }
79
+ isCurrent(token) {
80
+ return token.sequence === this.sequence && token.revision === this.requestedRevision;
81
+ }
82
+ complete(token) {
83
+ if (!this.isCurrent(token)) return false;
84
+ this.completedRevision = token.revision;
85
+ return true;
86
+ }
87
+ readyRevision() {
88
+ return this.completedRevision === this.requestedRevision ? this.completedRevision : null;
89
+ }
90
+ }
91
+ function workspaceCheckpointTelemetry(input) {
92
+ return {
93
+ totalMs: Math.max(0, input.finishedAt - input.requestedAt),
94
+ queueMs: Math.max(0, input.prepareStartedAt - input.requestedAt),
95
+ prepareMs: Math.max(0, input.synchronizeStartedAt - input.prepareStartedAt),
96
+ synchronizeMs: Math.max(0, input.finishedAt - input.synchronizeStartedAt)
97
+ };
98
+ }
99
+ function workspaceCheckpointIncidentAction(requestId) {
100
+ return requestId ? "request_terminal_authority" : "defer_automatic";
101
+ }
102
+ class ExplicitWorkspaceCheckpointQueue {
103
+ scheduled;
104
+ queued = [];
105
+ schedule(checkpoint, pendingRequestId) {
106
+ if (pendingRequestId === checkpoint.requestId || this.scheduled?.requestId === checkpoint.requestId || this.queued.some((queued) => queued.requestId === checkpoint.requestId)) {
107
+ return false;
108
+ }
109
+ if (pendingRequestId || this.scheduled) {
110
+ this.queued.push(checkpoint);
111
+ return false;
112
+ }
113
+ this.scheduled = checkpoint;
114
+ return true;
115
+ }
116
+ hasScheduled() {
117
+ return this.scheduled !== void 0;
118
+ }
119
+ consumeScheduled(requestId) {
120
+ if (this.scheduled?.requestId === requestId) this.scheduled = void 0;
121
+ }
122
+ enqueue(checkpoint, pendingRequestId) {
123
+ if (pendingRequestId === checkpoint.requestId || this.scheduled?.requestId === checkpoint.requestId || this.queued.some((queued) => queued.requestId === checkpoint.requestId)) {
124
+ return;
125
+ }
126
+ this.queued.push(checkpoint);
127
+ }
128
+ next() {
129
+ return this.queued.shift();
130
+ }
131
+ }
132
+ const EMPTY_STATE = {
133
+ localHead: null,
134
+ desiredCanonicalHead: null,
135
+ dirtyGeneration: 0,
136
+ publishedGeneration: 0
137
+ };
138
+ function validHead(value) {
139
+ return value === null || typeof value === "string" && /^[0-9a-f]{40,64}$/.test(value);
140
+ }
141
+ function validGeneration(value) {
142
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
143
+ }
144
+ function readWorkspaceConvergenceState(statePath) {
145
+ try {
146
+ const parsed = JSON.parse(fs.readFileSync(statePath, "utf8"));
147
+ if (!validHead(parsed.localHead) || !validHead(parsed.desiredCanonicalHead) || !validGeneration(parsed.dirtyGeneration) || !validGeneration(parsed.publishedGeneration) || parsed.publishedGeneration > parsed.dirtyGeneration) {
148
+ return { ...EMPTY_STATE };
149
+ }
150
+ return {
151
+ localHead: parsed.localHead,
152
+ desiredCanonicalHead: parsed.desiredCanonicalHead,
153
+ dirtyGeneration: parsed.dirtyGeneration,
154
+ publishedGeneration: parsed.publishedGeneration
155
+ };
156
+ } catch {
157
+ return { ...EMPTY_STATE };
158
+ }
159
+ }
160
+ class WorkspaceConvergenceState {
161
+ constructor(statePath) {
162
+ this.statePath = statePath;
163
+ this.state = readWorkspaceConvergenceState(statePath);
164
+ this.checkpointState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
165
+ }
166
+ statePath;
167
+ state;
168
+ checkpointState;
169
+ snapshot() {
170
+ return { ...this.state, checkpointState: this.checkpointState };
171
+ }
172
+ setLocalHead(localHead) {
173
+ if (!validHead(localHead)) throw new Error("Invalid local workspace head");
174
+ this.state.localHead = localHead;
175
+ this.persist();
176
+ }
177
+ observeCanonicalHead(canonicalHead) {
178
+ if (!validHead(canonicalHead)) throw new Error("Invalid canonical workspace head");
179
+ if (this.state.desiredCanonicalHead === canonicalHead) return false;
180
+ this.state.desiredCanonicalHead = canonicalHead;
181
+ this.persist();
182
+ return true;
183
+ }
184
+ markDirty() {
185
+ this.state.dirtyGeneration += 1;
186
+ if (this.checkpointState === "idle") this.checkpointState = "scheduled";
187
+ this.persist();
188
+ return this.state.dirtyGeneration;
189
+ }
190
+ schedule() {
191
+ if (this.checkpointState === "idle") this.checkpointState = "scheduled";
192
+ }
193
+ beginPreparing() {
194
+ if (this.checkpointState === "preparing" || this.checkpointState === "submitting") {
195
+ throw new Error("A workspace checkpoint is already in flight");
196
+ }
197
+ this.checkpointState = "preparing";
198
+ }
199
+ beginSubmitting() {
200
+ if (this.checkpointState !== "preparing") throw new Error("Workspace checkpoint authority was not prepared");
201
+ this.checkpointState = "submitting";
202
+ }
203
+ defer() {
204
+ this.checkpointState = "scheduled";
205
+ }
206
+ block() {
207
+ this.checkpointState = "blocked";
208
+ }
209
+ releaseBlock() {
210
+ this.checkpointState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
211
+ }
212
+ observeIncidentState(blocked) {
213
+ if (this.checkpointState === "preparing" || this.checkpointState === "submitting") return;
214
+ if (blocked) this.block();
215
+ else this.releaseBlock();
216
+ }
217
+ resetTransientCheckpointState() {
218
+ this.checkpointState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
219
+ }
220
+ complete(input) {
221
+ if (!validGeneration(input.generation) || input.generation > this.state.dirtyGeneration) {
222
+ throw new Error("Invalid completed workspace generation");
223
+ }
224
+ if (!validHead(input.canonicalHead)) throw new Error("Invalid completed canonical workspace head");
225
+ if (input.published) {
226
+ this.state.publishedGeneration = Math.max(this.state.publishedGeneration, input.generation);
227
+ this.state.localHead = input.canonicalHead;
228
+ this.state.desiredCanonicalHead = input.canonicalHead;
229
+ }
230
+ this.checkpointState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
231
+ this.persist();
232
+ }
233
+ persist() {
234
+ fs.mkdirSync(path.dirname(this.statePath), { recursive: true });
235
+ const temporaryPath = `${this.statePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
236
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(this.state)}
237
+ `, { mode: 384 });
238
+ fs.renameSync(temporaryPath, this.statePath);
239
+ }
240
+ }
241
+ const PROCESS_WORKSPACE_CONVERGENCE_STATES = /* @__PURE__ */ new Map();
242
+ function processWorkspaceConvergenceState(statePath) {
243
+ const key = path.resolve(statePath);
244
+ const existing = PROCESS_WORKSPACE_CONVERGENCE_STATES.get(key);
245
+ if (existing) return existing;
246
+ const created = new WorkspaceConvergenceState(key);
247
+ PROCESS_WORKSPACE_CONVERGENCE_STATES.set(key, created);
248
+ return created;
249
+ }
250
+ async function waitForWorkspaceCheckpointOnShutdown(input) {
251
+ input.forceCheckpoint();
252
+ const deadline = Date.now() + (input.timeoutMs ?? 1e4);
253
+ const pollMs = input.pollMs ?? 50;
254
+ while (Date.now() < deadline) {
255
+ const snapshot = input.snapshot();
256
+ if (snapshot.checkpointState === "blocked") return "blocked";
257
+ if (snapshot.checkpointState === "idle" && snapshot.publishedGeneration >= snapshot.dirtyGeneration) return "settled";
258
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
259
+ }
260
+ return "timed_out";
261
+ }
262
+ export {
263
+ ExplicitWorkspaceCheckpointQueue,
264
+ WORKSPACE_CHECKPOINT_QUIET_MS,
265
+ WORKSPACE_IDLE_SAFETY_SCAN_MS,
266
+ WorkspaceConvergenceState,
267
+ WorkspaceFilesystemWriterGate,
268
+ WorkspaceManifestRevisionFence,
269
+ processWorkspaceConvergenceState,
270
+ processWorkspaceFilesystemWriterGate,
271
+ readWorkspaceConvergenceState,
272
+ waitForWorkspaceCheckpointOnShutdown,
273
+ workspaceCheckpointIncidentAction,
274
+ workspaceCheckpointTelemetry
275
+ };
@@ -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,35 @@
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 inspectWorkspaceManifestAdmission(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
+ requiresVisibleLease: firstBootstrap || migratedProjectIds.length > 0
27
+ };
28
+ }
29
+ async function acquireWorkspaceManifestVisibleLease(gate, admission) {
30
+ return admission.requiresVisibleLease ? await gate.acquireCheckpoint() : void 0;
31
+ }
32
+ export {
33
+ acquireWorkspaceManifestVisibleLease,
34
+ inspectWorkspaceManifestAdmission
35
+ };
@@ -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,12 @@ 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(
1712
+ `Workspace candidate ref must be below ${WORKSPACE_PUBLICATION_REF_PREFIX} or ${WORKSPACE_INTENT_REF_PREFIX}`
1713
+ );
1711
1714
  }
1712
1715
  const result = Bun.spawnSync(["git", "check-ref-format", quarantineRef], {
1713
1716
  stdout: "pipe",
@@ -1827,6 +1830,36 @@ function preserveWorkspaceConflictCandidate(input, observed, options) {
1827
1830
  };
1828
1831
  }
1829
1832
  }
1833
+ function preserveLargeDiffCandidate(input, observed, error) {
1834
+ try {
1835
+ if (stagedPaths(input).length > 0) {
1836
+ runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit blocked workspace candidate");
1837
+ }
1838
+ const candidateHead = revParse(input, "HEAD");
1839
+ if (!candidateHead) throw new Error("the blocked workspace candidate does not have a commit");
1840
+ assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1841
+ const upload = uploadWorkspaceCandidate(input, candidateHead);
1842
+ if (upload.exitCode !== 0) {
1843
+ throw new Error(upload.stderr || upload.stdout || "blocked workspace candidate upload failed");
1844
+ }
1845
+ return {
1846
+ ...observed,
1847
+ outcome: "large_diff_blocked",
1848
+ expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
1849
+ candidateHead,
1850
+ quarantineRef: input.quarantineRef,
1851
+ error,
1852
+ gitStatus: gitStatus(input)
1853
+ };
1854
+ } catch (candidateError) {
1855
+ return {
1856
+ ...observed,
1857
+ outcome: "failed",
1858
+ error: `Workspace safety guard blocked publication but could not preserve a durable candidate: ${candidateError instanceof Error ? candidateError.message : String(candidateError)}`,
1859
+ gitStatus: gitStatus(input)
1860
+ };
1861
+ }
1862
+ }
1830
1863
  async function synchronizeWorkspace(rawInput) {
1831
1864
  let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
1832
1865
  try {
@@ -1838,6 +1871,15 @@ async function synchronizeWorkspace(rawInput) {
1838
1871
  let hydrationPreimages = input.skipVisibleMirror ? hydrationPreimagesFromShadowRevision(input, shadowWasCreated ? null : revParse(input, "HEAD")) : /* @__PURE__ */ new Map();
1839
1872
  const remoteHeadAtStart = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1840
1873
  const result = baseResult(input, remoteHeadAtStart);
1874
+ if (input.expectedCanonicalHead !== void 0 && remoteHeadAtStart !== input.expectedCanonicalHead) {
1875
+ return {
1876
+ ...result,
1877
+ outcome: "failed",
1878
+ expectedHead: input.expectedCanonicalHead,
1879
+ publishedHead: remoteHeadAtStart ?? void 0,
1880
+ error: `canonical_head_advanced: expected ${input.expectedCanonicalHead ?? "unborn"}, fetched ${remoteHeadAtStart ?? "unborn"}`
1881
+ };
1882
+ }
1841
1883
  const inboundConflictsAtStart = input.skipVisibleMirror && !isCanonicalRemediationSync && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1842
1884
  if (input.resetToCanonical) {
1843
1885
  if (input.trigger.canonicalCheckoutOnly) {
@@ -1886,18 +1928,18 @@ async function synchronizeWorkspace(rawInput) {
1886
1928
  const destructiveReductions = destructiveCheckoutReductions(input, baseRevision);
1887
1929
  if (destructiveReductions.length > 0 && !input.confirmedLargeDiff) {
1888
1930
  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
- };
1931
+ return preserveLargeDiffCandidate(
1932
+ input,
1933
+ observed,
1934
+ `Workspace safety guard blocked a destructive checkout reduction: ${summary}`
1935
+ );
1894
1936
  }
1895
1937
  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
- };
1938
+ return preserveLargeDiffCandidate(
1939
+ input,
1940
+ observed,
1941
+ `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
1942
+ );
1901
1943
  }
1902
1944
  if (stagedWorkingPaths.length > 0) {
1903
1945
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
@@ -2062,6 +2104,79 @@ async function synchronizeWorkspace(rawInput) {
2062
2104
  };
2063
2105
  }
2064
2106
  }
2107
+ async function convergeWorkspaceHead(rawInput, options) {
2108
+ const input = {
2109
+ ...rawInput,
2110
+ projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2111
+ };
2112
+ if (input.trigger.canonicalCheckoutOnly) {
2113
+ throw new Error("Ordinary inbound convergence cannot target a canonical resolver checkout");
2114
+ }
2115
+ ensureShadowWorkspace(input);
2116
+ const fetchedLocalHead = revParse(input, "HEAD");
2117
+ const fetchedCanonicalHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2118
+ if (!options.hydrateVisible) {
2119
+ return {
2120
+ localHead: fetchedLocalHead,
2121
+ canonicalHead: fetchedCanonicalHead,
2122
+ localChanges: false,
2123
+ hydrated: false,
2124
+ hydrationConflicts: []
2125
+ };
2126
+ }
2127
+ resetUncommittedShadowSnapshot(input);
2128
+ restoreUnscopedCanonicalCheckoutSubtrees(input);
2129
+ const localHead = fetchedLocalHead;
2130
+ const canonicalHead = fetchedCanonicalHead;
2131
+ const projection = mirrorVisibleWorkspaceToShadow(input);
2132
+ runGit(
2133
+ input,
2134
+ input.shadowRoot,
2135
+ ["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2136
+ "stage inbound workspace observation"
2137
+ );
2138
+ forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
2139
+ const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write inbound workspace observation tree");
2140
+ const headTree = localHead ? revParse(input, `${localHead}^{tree}`) : null;
2141
+ const localChanges = stagedTree !== headTree;
2142
+ if (!options.hydrateVisible || localChanges || !canonicalHead || localHead === canonicalHead) {
2143
+ resetUncommittedShadowSnapshot(input);
2144
+ return { localHead, canonicalHead, localChanges, hydrated: false, hydrationConflicts: [] };
2145
+ }
2146
+ const preimages = projection.visiblePreimages ?? hydrationPreimagesFromShadowRevision(input, localHead);
2147
+ const hydrationConflicts = localHead ? hydrationConflictPaths(input, preimages, localHead, canonicalHead) : [];
2148
+ if (hydrationConflicts.length > 0) {
2149
+ resetUncommittedShadowSnapshot(input);
2150
+ return { localHead, canonicalHead, localChanges: true, hydrated: false, hydrationConflicts };
2151
+ }
2152
+ runGit(input, input.shadowRoot, ["reset", "--hard", canonicalHead], "fast-forward workspace to available canonical head");
2153
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean fast-forwarded workspace");
2154
+ mirrorShadowWorkspaceToVisible(input, projection.opaqueRoots, preimages);
2155
+ return { localHead: canonicalHead, canonicalHead, localChanges: false, hydrated: true, hydrationConflicts: [] };
2156
+ }
2157
+ function calculateWorkspaceLocalDiffFingerprint(rawInput) {
2158
+ const input = {
2159
+ ...rawInput,
2160
+ projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2161
+ };
2162
+ if (!fs.existsSync(path.join(input.shadowRoot, ".git"))) {
2163
+ throw new Error("Cannot inspect local workspace changes before first bootstrap");
2164
+ }
2165
+ resetUncommittedShadowSnapshot(input);
2166
+ restoreUnscopedCanonicalCheckoutSubtrees(input);
2167
+ const projection = mirrorVisibleWorkspaceToShadow(input);
2168
+ runGit(
2169
+ input,
2170
+ input.shadowRoot,
2171
+ ["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2172
+ "stage local workspace fingerprint"
2173
+ );
2174
+ forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
2175
+ const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write local workspace fingerprint tree");
2176
+ const headTree = revParse(input, "HEAD^{tree}");
2177
+ resetUncommittedShadowSnapshot(input);
2178
+ return stagedTree === headTree ? createHash("sha256").update("").digest("hex") : createHash("sha256").update(stagedTree).digest("hex");
2179
+ }
2065
2180
  function calculateWorkspaceDiffFingerprint(rawInput) {
2066
2181
  const input = {
2067
2182
  ...rawInput,
@@ -2133,11 +2248,13 @@ export {
2133
2248
  WORKSPACE_BRANCH,
2134
2249
  WORKSPACE_INTENT_REF_PREFIX,
2135
2250
  WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
2136
- WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
2251
+ WORKSPACE_PUBLICATION_REF_PREFIX,
2137
2252
  WorkspaceSyncSingleFlight,
2138
2253
  assertCanonicalCheckoutIndexMatchesWorktree,
2139
2254
  assertWorkspaceQuarantineRef,
2140
2255
  calculateWorkspaceDiffFingerprint,
2256
+ calculateWorkspaceLocalDiffFingerprint,
2257
+ convergeWorkspaceHead,
2141
2258
  encodeWorkspaceBranch,
2142
2259
  mirrorShadowWorkspaceToVisible,
2143
2260
  mirrorVisibleWorkspaceToShadow,
@@ -0,0 +1,102 @@
1
+ export declare const WORKSPACE_CHECKPOINT_QUIET_MS = 5000;
2
+ export declare const WORKSPACE_IDLE_SAFETY_SCAN_MS = 60000;
3
+ export type WorkspaceCheckpointState = "idle" | "scheduled" | "preparing" | "submitting" | "blocked";
4
+ export type WorkspaceConvergenceSnapshot = {
5
+ localHead: string | null;
6
+ desiredCanonicalHead: string | null;
7
+ dirtyGeneration: number;
8
+ publishedGeneration: number;
9
+ checkpointState: WorkspaceCheckpointState;
10
+ };
11
+ export type ExplicitWorkspaceCheckpoint<TTrigger> = {
12
+ requestId: string;
13
+ trigger: TTrigger;
14
+ };
15
+ export type WorkspaceManifestRevisionToken = {
16
+ sequence: number;
17
+ revision: string;
18
+ };
19
+ export type WorkspaceWriterLease = () => void;
20
+ /**
21
+ * Makes checkpoint admission atomic with command startup. Once a checkpoint
22
+ * lease is admitted, later writers wait; once a writer lease is admitted, a
23
+ * checkpoint must defer. Writer acquisition increments synchronously before
24
+ * its promise resolves, covering asynchronous environment setup and spawn.
25
+ */
26
+ export declare class WorkspaceFilesystemWriterGate {
27
+ private activeWriters;
28
+ private checkpointActive;
29
+ private readonly waitingWriters;
30
+ private readonly waitingCheckpoints;
31
+ acquireWriter(): Promise<WorkspaceWriterLease>;
32
+ tryAcquireCheckpoint(): WorkspaceWriterLease | null;
33
+ acquireCheckpoint(): Promise<WorkspaceWriterLease>;
34
+ private checkpointRelease;
35
+ private writerRelease;
36
+ private drain;
37
+ }
38
+ export declare function processWorkspaceFilesystemWriterGate(): WorkspaceFilesystemWriterGate;
39
+ export declare class WorkspaceManifestRevisionFence {
40
+ private sequence;
41
+ private requestedRevision;
42
+ private completedRevision;
43
+ begin(revision: string): WorkspaceManifestRevisionToken;
44
+ isCurrent(token: WorkspaceManifestRevisionToken): boolean;
45
+ complete(token: WorkspaceManifestRevisionToken): boolean;
46
+ readyRevision(): string | null;
47
+ }
48
+ export declare function workspaceCheckpointTelemetry(input: {
49
+ requestedAt: number;
50
+ prepareStartedAt: number;
51
+ synchronizeStartedAt: number;
52
+ finishedAt: number;
53
+ }): {
54
+ totalMs: number;
55
+ queueMs: number;
56
+ prepareMs: number;
57
+ synchronizeMs: number;
58
+ };
59
+ export declare function workspaceCheckpointIncidentAction(requestId: string | undefined): "defer_automatic" | "request_terminal_authority";
60
+ export declare class ExplicitWorkspaceCheckpointQueue<TTrigger> {
61
+ private scheduled;
62
+ private readonly queued;
63
+ schedule(checkpoint: ExplicitWorkspaceCheckpoint<TTrigger>, pendingRequestId?: string): boolean;
64
+ hasScheduled(): boolean;
65
+ consumeScheduled(requestId: string): void;
66
+ enqueue(checkpoint: ExplicitWorkspaceCheckpoint<TTrigger>, pendingRequestId?: string): void;
67
+ next(): ExplicitWorkspaceCheckpoint<TTrigger> | undefined;
68
+ }
69
+ type PersistedWorkspaceConvergenceState = Omit<WorkspaceConvergenceSnapshot, "checkpointState">;
70
+ export declare function readWorkspaceConvergenceState(statePath: string): PersistedWorkspaceConvergenceState;
71
+ export declare class WorkspaceConvergenceState {
72
+ private readonly statePath;
73
+ private state;
74
+ private checkpointState;
75
+ constructor(statePath: string);
76
+ snapshot(): WorkspaceConvergenceSnapshot;
77
+ setLocalHead(localHead: string | null): void;
78
+ observeCanonicalHead(canonicalHead: string | null): boolean;
79
+ markDirty(): number;
80
+ schedule(): void;
81
+ beginPreparing(): void;
82
+ beginSubmitting(): void;
83
+ defer(): void;
84
+ block(): void;
85
+ releaseBlock(): void;
86
+ observeIncidentState(blocked: boolean): void;
87
+ resetTransientCheckpointState(): void;
88
+ complete(input: {
89
+ generation: number;
90
+ canonicalHead: string | null;
91
+ published: boolean;
92
+ }): void;
93
+ private persist;
94
+ }
95
+ export declare function processWorkspaceConvergenceState(statePath: string): WorkspaceConvergenceState;
96
+ export declare function waitForWorkspaceCheckpointOnShutdown(input: {
97
+ snapshot: () => WorkspaceConvergenceSnapshot;
98
+ forceCheckpoint: () => void;
99
+ timeoutMs?: number;
100
+ pollMs?: number;
101
+ }): Promise<"settled" | "blocked" | "timed_out">;
102
+ export {};