@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.
- package/README.md +1 -1
- package/dist/cjs/main.cjs +569 -191
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-convergence.cjs +256 -0
- package/dist/cjs/workspace-incident-state.cjs +32 -3
- package/dist/cjs/workspace-manifest-admission.cjs +64 -0
- package/dist/cjs/workspace-sync.cjs +129 -16
- package/dist/mjs/main.mjs +579 -197
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-convergence.mjs +213 -0
- package/dist/mjs/workspace-incident-state.mjs +31 -3
- package/dist/mjs/workspace-manifest-admission.mjs +30 -0
- package/dist/mjs/workspace-sync.mjs +126 -15
- package/dist/types/workspace-convergence.d.ts +82 -0
- package/dist/types/workspace-incident-state.d.ts +15 -1
- package/dist/types/workspace-manifest-admission.d.ts +24 -0
- package/dist/types/workspace-sync.d.ts +23 -6
- package/package.json +1 -1
- package/dist/cjs/workspace-sync-fast-path.cjs +0 -108
- package/dist/mjs/workspace-sync-fast-path.mjs +0 -81
- package/dist/types/workspace-sync-fast-path.d.ts +0 -69
package/dist/mjs/package.json
CHANGED
|
@@ -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 {
|
|
8
|
-
|
|
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
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|