@ricsam/r5d-worker 0.0.57 → 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.57",
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
+ };