@hypercarrier/rarebit 0.1.0-alpha.1

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.
Files changed (50) hide show
  1. package/AGENTS.md +14 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/RUNBOOK.md +36 -0
  6. package/SECURITY.md +11 -0
  7. package/VISUAL-LANGUAGE.md +89 -0
  8. package/bin/rarebit.mjs +20 -0
  9. package/package.json +96 -0
  10. package/schemas/rarebit.schema.json +618 -0
  11. package/scripts/e2e-recall-pi083.mjs +199 -0
  12. package/scripts/e2e-summary-cache.mjs +257 -0
  13. package/scripts/verify-package.mjs +81 -0
  14. package/src/automatic-summary-policy.mjs +210 -0
  15. package/src/extension.mjs +516 -0
  16. package/src/herdr-activity.mjs +111 -0
  17. package/src/index.mjs +13 -0
  18. package/src/lifecycle.mjs +322 -0
  19. package/src/rarebit-activity.mjs +48 -0
  20. package/src/rarebit-artifact-state.mjs +839 -0
  21. package/src/rarebit-cli.mjs +305 -0
  22. package/src/rarebit-command.mjs +281 -0
  23. package/src/rarebit-core.mjs +525 -0
  24. package/src/rarebit-model.mjs +226 -0
  25. package/src/rarebit-recall.mjs +182 -0
  26. package/src/rarebit-service.mjs +579 -0
  27. package/src/rarebit-session.mjs +227 -0
  28. package/src/rarebit-settings.mjs +79 -0
  29. package/src/rarebit-store.mjs +771 -0
  30. package/src/rarebit-visual-language.mjs +110 -0
  31. package/src/types.d.ts +601 -0
  32. package/test/automatic-summary-policy.test.mjs +476 -0
  33. package/test/extension-activity.test.mjs +73 -0
  34. package/test/fixtures/branch-mixed.json +117 -0
  35. package/test/fixtures/conformance-corpus.v1.json +306 -0
  36. package/test/fixtures/truncated-mixed.jsonl +2 -0
  37. package/test/herdr-activity.test.mjs +93 -0
  38. package/test/lifecycle-contract-independent.test.mjs +627 -0
  39. package/test/rarebit-activity.test.mjs +82 -0
  40. package/test/rarebit-artifact-state.test.mjs +440 -0
  41. package/test/rarebit-cli.test.mjs +222 -0
  42. package/test/rarebit-command.test.mjs +127 -0
  43. package/test/rarebit-core.test.mjs +461 -0
  44. package/test/rarebit-cross-consumer-conformance.test.mjs +150 -0
  45. package/test/rarebit-independent-contract.test.mjs +317 -0
  46. package/test/rarebit-recall.test.mjs +434 -0
  47. package/test/rarebit-sidecar-protocol.test.mjs +538 -0
  48. package/test/rarebit-visual-language.test.mjs +75 -0
  49. package/test/summary-attention-independent.test.mjs +172 -0
  50. package/test/types-history.ts +13 -0
@@ -0,0 +1,771 @@
1
+ import { chmod, lstat, mkdir, open, readFile, unlink } from "node:fs/promises";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import {
6
+ projectRarebitArtifactState,
7
+ validateRarebitArtifactReceipt,
8
+ validateRarebitTitleReceipt,
9
+ } from "./rarebit-artifact-state.mjs";
10
+ import { readRarebitSession } from "./rarebit-session.mjs";
11
+
12
+ export const DEFAULT_RAREBIT_ROOT = join(homedir(), ".pi", "agent", "rarebit");
13
+ export const DEFAULT_RAREBIT_SESSION_ROOT = join(
14
+ homedir(),
15
+ ".pi",
16
+ "agent",
17
+ "sessions",
18
+ );
19
+ export const DEFAULT_RAREBIT_LEASE_MS = 10 * 60_000;
20
+ export const RAREBIT_SIDECAR_PROTOCOL_VERSION = 1;
21
+
22
+ const MAX_TAIL_BYTES = 64 * 1024;
23
+ const MAX_RECEIPT_BYTES = 48 * 1024;
24
+ const MAX_HEAD_BYTES = 8 * 1024;
25
+ const MAX_HISTORY_BYTES = 1024 * 1024;
26
+ const MAX_HISTORY_RECORDS = 1_000;
27
+ const COMMIT_LOCK_RETRY_MS = 5;
28
+ const COMMIT_LOCK_ATTEMPTS = 2_000;
29
+
30
+ function sha256Bytes(value) {
31
+ return createHash("sha256").update(value).digest("hex");
32
+ }
33
+
34
+ function mirroredPath(
35
+ sessionFile,
36
+ root,
37
+ outputRoot,
38
+ { allowExternalSession = false } = {},
39
+ ) {
40
+ if (typeof sessionFile !== "string" || !sessionFile.trim())
41
+ throw new Error("A persisted Pi Session file is required");
42
+ const source = resolve(sessionFile);
43
+ const relativePath = relative(resolve(root), source);
44
+ const outsideRoot =
45
+ !relativePath ||
46
+ relativePath === ".." ||
47
+ relativePath.startsWith(`..${sep}`) ||
48
+ relativePath.includes(`${sep}..${sep}`);
49
+ if (outsideRoot && !allowExternalSession)
50
+ throw new Error("Pi Session file is outside the configured Session root");
51
+ if (outsideRoot)
52
+ return join(
53
+ resolve(outputRoot),
54
+ "external",
55
+ `${sha256Bytes(source)}.jsonl`,
56
+ );
57
+ return join(resolve(outputRoot), relativePath);
58
+ }
59
+
60
+ export function rarebitMaterializationPath(
61
+ sessionFile,
62
+ {
63
+ sessionRoot = DEFAULT_RAREBIT_SESSION_ROOT,
64
+ rarebitRoot = DEFAULT_RAREBIT_ROOT,
65
+ allowExternalSession = false,
66
+ } = {},
67
+ ) {
68
+ return mirroredPath(
69
+ sessionFile,
70
+ sessionRoot,
71
+ join(rarebitRoot, "materializations-v4"),
72
+ { allowExternalSession },
73
+ );
74
+ }
75
+
76
+ async function createPrivateDirectory(path) {
77
+ await mkdir(path, { recursive: true, mode: 0o700 });
78
+ await chmod(path, 0o700);
79
+ }
80
+
81
+ async function privateRegularFileInfo(path) {
82
+ const info = await lstat(path);
83
+ if (info.isSymbolicLink() || !info.isFile())
84
+ throw new Error("Rarebit materialization path must be a regular file");
85
+ return info;
86
+ }
87
+
88
+ function validHead(head) {
89
+ return (
90
+ head?.type === "rarebit_head" &&
91
+ head.protocolVersion === RAREBIT_SIDECAR_PROTOCOL_VERSION &&
92
+ typeof head.sessionId === "string" &&
93
+ head.sessionId.length > 0 &&
94
+ Number.isSafeInteger(head.receiptOffset) &&
95
+ head.receiptOffset >= 0 &&
96
+ Number.isSafeInteger(head.receiptLength) &&
97
+ head.receiptLength > 0 &&
98
+ head.receiptLength <= MAX_RECEIPT_BYTES &&
99
+ /^[a-f0-9]{64}$/.test(head.receiptHash)
100
+ );
101
+ }
102
+
103
+ async function readTail(path) {
104
+ const info = await privateRegularFileInfo(path);
105
+ const length = Math.min(info.size, MAX_TAIL_BYTES);
106
+ const handle = await open(path, "r");
107
+ try {
108
+ const buffer = Buffer.alloc(length);
109
+ const { bytesRead } = await handle.read(
110
+ buffer,
111
+ 0,
112
+ length,
113
+ info.size - length,
114
+ );
115
+ return {
116
+ size: info.size,
117
+ offset: info.size - bytesRead,
118
+ buffer: buffer.subarray(0, bytesRead),
119
+ };
120
+ } finally {
121
+ await handle.close();
122
+ }
123
+ }
124
+
125
+ function completeTailLines(tail) {
126
+ let buffer = tail.buffer;
127
+ let offset = tail.offset;
128
+ if (offset > 0) {
129
+ const firstNewline = buffer.indexOf(10);
130
+ if (firstNewline < 0)
131
+ return { lines: [], tornTail: true, discardedPrefix: true };
132
+ buffer = buffer.subarray(firstNewline + 1);
133
+ offset += firstNewline + 1;
134
+ }
135
+ const tornTail = buffer.length > 0 && buffer.at(-1) !== 10;
136
+ const completeLength = tornTail ? buffer.lastIndexOf(10) + 1 : buffer.length;
137
+ const complete = buffer.subarray(0, completeLength);
138
+ const lines = [];
139
+ let cursor = 0;
140
+ while (cursor < complete.length) {
141
+ const newline = complete.indexOf(10, cursor);
142
+ if (newline < 0) break;
143
+ const bytes = complete.subarray(cursor, newline);
144
+ if (bytes.length > 0) {
145
+ const line = bytes.toString("utf8");
146
+ let record = null;
147
+ try {
148
+ record = JSON.parse(line);
149
+ } catch {
150
+ // Retain the raw complete line so an invalid final head fails closed.
151
+ }
152
+ lines.push({ offset: offset + cursor, line, record });
153
+ }
154
+ cursor = newline + 1;
155
+ }
156
+ return { lines, tornTail, discardedPrefix: tail.offset > 0 };
157
+ }
158
+
159
+ async function readRecordReference(
160
+ path,
161
+ reference,
162
+ { requireSummary = false } = {},
163
+ ) {
164
+ if (
165
+ !Number.isSafeInteger(reference?.receiptOffset) ||
166
+ reference.receiptOffset < 0 ||
167
+ !Number.isSafeInteger(reference?.receiptLength) ||
168
+ reference.receiptLength < 1 ||
169
+ reference.receiptLength > MAX_RECEIPT_BYTES ||
170
+ !/^[a-f0-9]{64}$/.test(reference?.receiptHash ?? "")
171
+ )
172
+ return { error: "sidecar_head_invalid" };
173
+ const info = await privateRegularFileInfo(path);
174
+ if (reference.receiptOffset + reference.receiptLength > info.size)
175
+ return { error: "sidecar_head_invalid" };
176
+ const handle = await open(path, "r");
177
+ try {
178
+ const buffer = Buffer.alloc(reference.receiptLength);
179
+ const { bytesRead } = await handle.read(
180
+ buffer,
181
+ 0,
182
+ buffer.length,
183
+ reference.receiptOffset,
184
+ );
185
+ if (bytesRead !== reference.receiptLength)
186
+ return { error: "sidecar_head_invalid" };
187
+ const bytes = buffer.subarray(0, bytesRead);
188
+ if (sha256Bytes(bytes) !== reference.receiptHash || bytes.at(-1) !== 10)
189
+ return { error: "sidecar_head_invalid" };
190
+ let record;
191
+ try {
192
+ record = JSON.parse(bytes.subarray(0, -1).toString("utf8"));
193
+ } catch {
194
+ return { error: "sidecar_head_invalid" };
195
+ }
196
+ if (
197
+ requireSummary &&
198
+ (record?.type !== "rarebit_summary" ||
199
+ record?.sessionId !== reference.sessionId ||
200
+ !validateRarebitArtifactReceipt(record).valid)
201
+ )
202
+ return { error: "sidecar_head_invalid" };
203
+ if (
204
+ !requireSummary &&
205
+ !(
206
+ (record?.type === "rarebit_summary" &&
207
+ validateRarebitArtifactReceipt(record).valid) ||
208
+ (record?.type === "rarebit_title" &&
209
+ validateRarebitTitleReceipt(record).valid)
210
+ )
211
+ )
212
+ return { error: "sidecar_head_invalid" };
213
+ return { record };
214
+ } finally {
215
+ await handle.close();
216
+ }
217
+ }
218
+
219
+ async function nativeObservation(sessionFile, options) {
220
+ try {
221
+ const session = await readRarebitSession(sessionFile, {
222
+ sessionRoot: options.sessionRoot,
223
+ });
224
+ return {
225
+ availability: "available",
226
+ sessionId: session.session.id,
227
+ selection: {
228
+ ...session.selection,
229
+ selectorVersion: session.selection.manifest.selectorVersion,
230
+ },
231
+ };
232
+ } catch (error) {
233
+ return {
234
+ availability: error?.code === "ENOENT" ? "missing" : "unreadable",
235
+ };
236
+ }
237
+ }
238
+
239
+ /** Bounded normal reader: one tail window plus one referenced receipt range. */
240
+ export async function readRarebitCurrent({
241
+ sessionFile,
242
+ native = null,
243
+ expectation,
244
+ deadlineExpired,
245
+ ...options
246
+ } = {}) {
247
+ const path = rarebitMaterializationPath(sessionFile, options);
248
+ const resolvedNative =
249
+ native ?? (await nativeObservation(sessionFile, options));
250
+ const withState = (result) => ({
251
+ ...result,
252
+ artifactState: projectRarebitArtifactState({
253
+ native: resolvedNative,
254
+ materialization: {
255
+ availability: result.availability,
256
+ records: result.records,
257
+ },
258
+ expectation,
259
+ deadlineExpired,
260
+ }),
261
+ });
262
+ try {
263
+ const tail = await readTail(path);
264
+ const parsed = completeTailLines(tail);
265
+ const heads = parsed.lines.filter(({ record }) => validHead(record));
266
+ const last = heads.at(-1) ?? null;
267
+ const lastValidHeadOffset = last?.offset ?? -1;
268
+ const invalidHeadAfterCurrent = parsed.lines.some(
269
+ ({ offset, line, record }) =>
270
+ offset > lastValidHeadOffset &&
271
+ (record?.type === "rarebit_head" ||
272
+ (!record && line.includes('"rarebit_head"'))) &&
273
+ !validHead(record),
274
+ );
275
+ if (invalidHeadAfterCurrent)
276
+ return withState({
277
+ availability: "unreadable",
278
+ path,
279
+ receipt: null,
280
+ records: [],
281
+ head: last?.record ?? null,
282
+ diagnostics: { tornTail: false, reason: "sidecar_head_invalid" },
283
+ });
284
+
285
+ if (!last)
286
+ return withState({
287
+ availability: "missing",
288
+ path,
289
+ receipt: null,
290
+ records: [],
291
+ head: null,
292
+ diagnostics: {
293
+ tornTail: parsed.tornTail,
294
+ reason: parsed.lines.length
295
+ ? "sidecar_head_missing"
296
+ : "sidecar_empty",
297
+ },
298
+ });
299
+
300
+ const checked = await readRecordReference(path, last.record, {
301
+ requireSummary: true,
302
+ });
303
+ if (checked.error)
304
+ return withState({
305
+ availability: "unreadable",
306
+ path,
307
+ receipt: null,
308
+ records: [],
309
+ head: last.record,
310
+ diagnostics: {
311
+ tornTail: parsed.tornTail,
312
+ reason: checked.error,
313
+ },
314
+ });
315
+
316
+ const recordsAfterHead = parsed.lines.filter(
317
+ ({ offset }) => offset > last.offset,
318
+ ).length;
319
+ return withState({
320
+ availability: "available",
321
+ path,
322
+ receipt: checked.record,
323
+ records: [checked.record],
324
+ head: last.record,
325
+ diagnostics: {
326
+ tornTail: parsed.tornTail,
327
+ tailOffset: tail.offset,
328
+ ...(recordsAfterHead > 0 ? { reason: "sidecar_uncommitted_tail" } : {}),
329
+ },
330
+ });
331
+ } catch (error) {
332
+ if (error?.code === "ENOENT")
333
+ return withState({
334
+ availability: "missing",
335
+ path,
336
+ receipt: null,
337
+ records: [],
338
+ head: null,
339
+ diagnostics: { tornTail: false, reason: "sidecar_missing" },
340
+ });
341
+ return withState({
342
+ availability: "unreadable",
343
+ path,
344
+ receipt: null,
345
+ records: [],
346
+ head: null,
347
+ diagnostics: { tornTail: false, reason: "sidecar_unreadable" },
348
+ });
349
+ }
350
+ }
351
+
352
+ /** Explicit bounded audit path, resumable at the returned absolute offset. */
353
+ export async function readRarebitHistory({
354
+ sessionFile,
355
+ fromOffset = 0,
356
+ limit = 100,
357
+ ...options
358
+ } = {}) {
359
+ if (
360
+ !Number.isSafeInteger(fromOffset) ||
361
+ fromOffset < 0 ||
362
+ !Number.isInteger(limit) ||
363
+ limit < 1 ||
364
+ limit > MAX_HISTORY_RECORDS
365
+ )
366
+ throw new TypeError("History offset and limit are outside protocol bounds");
367
+ const path = rarebitMaterializationPath(sessionFile, options);
368
+ let handle;
369
+ try {
370
+ const info = await privateRegularFileInfo(path);
371
+ if (fromOffset >= info.size)
372
+ return {
373
+ availability: "available",
374
+ path,
375
+ records: [],
376
+ nextOffset: null,
377
+ };
378
+ handle = await open(path, "r");
379
+ const byteCount = Math.min(MAX_HISTORY_BYTES, info.size - fromOffset);
380
+ const buffer = Buffer.alloc(byteCount);
381
+ const { bytesRead } = await handle.read(buffer, 0, byteCount, fromOffset);
382
+ const bytes = buffer.subarray(0, bytesRead);
383
+ let cursor = 0;
384
+ if (fromOffset > 0) {
385
+ const previous = Buffer.alloc(1);
386
+ await handle.read(previous, 0, 1, fromOffset - 1);
387
+ if (previous[0] !== 10) {
388
+ const newline = bytes.indexOf(10);
389
+ if (newline < 0)
390
+ return {
391
+ availability: "available",
392
+ path,
393
+ records: [],
394
+ nextOffset:
395
+ fromOffset + bytes.length < info.size
396
+ ? fromOffset + bytes.length
397
+ : null,
398
+ };
399
+ cursor = newline + 1;
400
+ }
401
+ }
402
+
403
+ const records = [];
404
+ while (cursor < bytes.length && records.length < limit) {
405
+ const newline = bytes.indexOf(10, cursor);
406
+ if (newline < 0) break;
407
+ const lineOffset = fromOffset + cursor;
408
+ const line = bytes.subarray(cursor, newline).toString("utf8");
409
+ cursor = newline + 1;
410
+ if (!line) continue;
411
+ try {
412
+ const record = JSON.parse(line);
413
+ if (
414
+ (record.type === "rarebit_summary" &&
415
+ validateRarebitArtifactReceipt(record).valid) ||
416
+ (record.type === "rarebit_title" &&
417
+ validateRarebitTitleReceipt(record).valid)
418
+ )
419
+ records.push({ offset: lineOffset, record });
420
+ } catch {
421
+ // History is evidence-oriented but returns only complete JSON records.
422
+ }
423
+ }
424
+ const absoluteCursor = fromOffset + cursor;
425
+ let nextOffset = null;
426
+ if (absoluteCursor < info.size) {
427
+ if (cursor === 0 && bytes.length > 0)
428
+ nextOffset = fromOffset + bytes.length;
429
+ else nextOffset = absoluteCursor;
430
+ }
431
+ return { availability: "available", path, records, nextOffset };
432
+ } catch (error) {
433
+ if (error?.code === "ENOENT")
434
+ return { availability: "missing", path, records: [], nextOffset: null };
435
+ return {
436
+ availability: "unreadable",
437
+ path,
438
+ records: [],
439
+ nextOffset: null,
440
+ };
441
+ } finally {
442
+ await handle?.close();
443
+ }
444
+ }
445
+
446
+ async function readJobClaim(path) {
447
+ try {
448
+ return JSON.parse(await readFile(path, "utf8"));
449
+ } catch (error) {
450
+ if (error?.code === "ENOENT") return null;
451
+ throw error;
452
+ }
453
+ }
454
+
455
+ async function writeJsonFile(path, value, flags = "w") {
456
+ const handle = await open(path, flags, 0o600);
457
+ try {
458
+ await handle.writeFile(`${JSON.stringify(value)}\n`);
459
+ await handle.sync();
460
+ } finally {
461
+ await handle.close();
462
+ }
463
+ }
464
+
465
+ async function ownsReservation(reservation) {
466
+ const claim = await readJobClaim(reservation.claimPath);
467
+ return (
468
+ claim?.state === "active" && claim.leaseToken === reservation.leaseToken
469
+ );
470
+ }
471
+
472
+ async function acquireCommitLock(path) {
473
+ const lockPath = `${path}.commit-lock`;
474
+ const token = randomBytes(24).toString("hex");
475
+ for (let attempt = 0; attempt < COMMIT_LOCK_ATTEMPTS; attempt += 1) {
476
+ try {
477
+ await writeJsonFile(
478
+ lockPath,
479
+ { token, claimedAt: new Date().toISOString() },
480
+ "wx",
481
+ );
482
+ return { lockPath, token };
483
+ } catch (error) {
484
+ if (error?.code !== "EEXIST") throw error;
485
+ await new Promise((resolveWait) =>
486
+ setTimeout(resolveWait, COMMIT_LOCK_RETRY_MS),
487
+ );
488
+ }
489
+ }
490
+ throw new Error(
491
+ "Rarebit Session commit lock is held; explicit repair is required if its owner crashed",
492
+ );
493
+ }
494
+
495
+ async function releaseCommitLock(lock) {
496
+ if (!lock) return;
497
+ const current = await readJobClaim(lock.lockPath);
498
+ if (current?.token === lock.token)
499
+ await unlink(lock.lockPath).catch((error) => {
500
+ if (error?.code !== "ENOENT") throw error;
501
+ });
502
+ }
503
+
504
+ function activeClaim(jobId, leaseToken, claimedAt, leaseMs) {
505
+ return {
506
+ jobId,
507
+ state: "active",
508
+ leaseToken,
509
+ claimedAt: new Date(claimedAt).toISOString(),
510
+ leaseMs,
511
+ };
512
+ }
513
+
514
+ function claimExpired(claim, now) {
515
+ const claimedAt = Date.parse(claim?.claimedAt);
516
+ return (
517
+ claim?.state === "active" &&
518
+ Number.isFinite(claimedAt) &&
519
+ Number.isFinite(claim.leaseMs) &&
520
+ now - claimedAt > claim.leaseMs
521
+ );
522
+ }
523
+
524
+ async function settledDuplicate(claim, materializationPath) {
525
+ if (claim?.state !== "settled" || !claim.receiptRef) return null;
526
+ const checked = await readRecordReference(
527
+ materializationPath,
528
+ claim.receiptRef,
529
+ );
530
+ if (checked.error)
531
+ throw new Error("Rarebit settled job receipt pointer is invalid");
532
+ return checked.record;
533
+ }
534
+
535
+ export async function reserveRarebitJob({
536
+ jobId,
537
+ sessionFile,
538
+ native = null,
539
+ rarebitRoot = DEFAULT_RAREBIT_ROOT,
540
+ sessionRoot = DEFAULT_RAREBIT_SESSION_ROOT,
541
+ leaseMs = DEFAULT_RAREBIT_LEASE_MS,
542
+ allowExternalSession = false,
543
+ now = Date.now(),
544
+ hooks = null,
545
+ } = {}) {
546
+ if (typeof jobId !== "string" || !/^[a-f0-9]{64}$/.test(jobId))
547
+ throw new TypeError("Rarebit jobId must be a SHA-256 hex string");
548
+ if (!Number.isFinite(leaseMs) || leaseMs < 1)
549
+ throw new TypeError("Rarebit leaseMs must be a positive finite number");
550
+ const materializationPath = rarebitMaterializationPath(sessionFile, {
551
+ rarebitRoot,
552
+ sessionRoot,
553
+ allowExternalSession,
554
+ });
555
+ const jobsDir = join(resolve(rarebitRoot), "jobs-v4");
556
+ await createPrivateDirectory(jobsDir);
557
+ await createPrivateDirectory(dirname(materializationPath));
558
+ const claimPath = join(jobsDir, `${jobId}.json`);
559
+ const leaseToken = randomBytes(24).toString("hex");
560
+ const reservation = {
561
+ acquired: true,
562
+ jobId,
563
+ claimPath,
564
+ leaseToken,
565
+ materializationPath,
566
+ sessionFile,
567
+ native,
568
+ rarebitRoot,
569
+ sessionRoot,
570
+ allowExternalSession,
571
+ hooks,
572
+ };
573
+ try {
574
+ await writeJsonFile(
575
+ claimPath,
576
+ activeClaim(jobId, leaseToken, now, leaseMs),
577
+ "wx",
578
+ );
579
+ return reservation;
580
+ } catch (error) {
581
+ if (error?.code !== "EEXIST") throw error;
582
+ }
583
+
584
+ let claim = await readJobClaim(claimPath);
585
+ const duplicate = await settledDuplicate(claim, materializationPath);
586
+ if (duplicate)
587
+ return {
588
+ acquired: false,
589
+ duplicate: true,
590
+ jobId,
591
+ materializationPath,
592
+ record: { ...duplicate, path: materializationPath },
593
+ sessionFile,
594
+ rarebitRoot,
595
+ sessionRoot,
596
+ allowExternalSession,
597
+ };
598
+ if (!claimExpired(claim, now))
599
+ return {
600
+ acquired: false,
601
+ inFlight: true,
602
+ jobId,
603
+ materializationPath,
604
+ sessionFile,
605
+ rarebitRoot,
606
+ sessionRoot,
607
+ allowExternalSession,
608
+ };
609
+
610
+ const lock = await acquireCommitLock(materializationPath);
611
+ try {
612
+ claim = await readJobClaim(claimPath);
613
+ const settled = await settledDuplicate(claim, materializationPath);
614
+ if (settled)
615
+ return {
616
+ acquired: false,
617
+ duplicate: true,
618
+ jobId,
619
+ materializationPath,
620
+ record: { ...settled, path: materializationPath },
621
+ sessionFile,
622
+ rarebitRoot,
623
+ sessionRoot,
624
+ allowExternalSession,
625
+ };
626
+ if (!claimExpired(claim, now))
627
+ return {
628
+ acquired: false,
629
+ inFlight: true,
630
+ jobId,
631
+ materializationPath,
632
+ sessionFile,
633
+ rarebitRoot,
634
+ sessionRoot,
635
+ allowExternalSession,
636
+ };
637
+ await writeJsonFile(
638
+ claimPath,
639
+ activeClaim(jobId, leaseToken, now, leaseMs),
640
+ );
641
+ return reservation;
642
+ } finally {
643
+ await releaseCommitLock(lock);
644
+ }
645
+ }
646
+
647
+ export async function settleRarebitJob(reservation, record) {
648
+ if (!reservation?.acquired)
649
+ throw new Error("An acquired Rarebit reservation is required");
650
+ const path = reservation.materializationPath;
651
+ await createPrivateDirectory(dirname(path));
652
+ const settled = { ...record, jobId: reservation.jobId };
653
+ if (settled.type !== "rarebit_summary" && settled.type !== "rarebit_title")
654
+ throw new TypeError("Rarebit derivation receipt type is unsupported");
655
+ if (
656
+ settled.type === "rarebit_summary" &&
657
+ !validateRarebitArtifactReceipt(settled).valid
658
+ )
659
+ throw new TypeError(
660
+ `Rarebit Summary receipt does not match protocol v4 (${settled.status}; keys: ${Object.keys(settled).sort().join(",")})`,
661
+ );
662
+ if (
663
+ settled.type === "rarebit_title" &&
664
+ !validateRarebitTitleReceipt(settled).valid
665
+ )
666
+ throw new TypeError(
667
+ `Rarebit Title receipt does not match protocol v4 (${settled.status}; keys: ${Object.keys(settled).sort().join(",")})`,
668
+ );
669
+ const receiptLine = Buffer.from(`${JSON.stringify(settled)}\n`);
670
+ if (receiptLine.length > MAX_RECEIPT_BYTES)
671
+ throw new RangeError("Rarebit receipt exceeds protocol byte limit");
672
+
673
+ let lock;
674
+ try {
675
+ lock = await acquireCommitLock(path);
676
+ if (!(await ownsReservation(reservation)))
677
+ throw new Error("Rarebit reservation lease was fenced by a newer worker");
678
+ await reservation.hooks?.afterLeaseValidated?.();
679
+
680
+ const existing = await lstat(path).catch((error) =>
681
+ error?.code === "ENOENT" ? null : Promise.reject(error),
682
+ );
683
+ if (existing && (existing.isSymbolicLink() || !existing.isFile()))
684
+ throw new Error("Rarebit materialization path must be a regular file");
685
+
686
+ const native = await nativeObservation(reservation.sessionFile, {
687
+ sessionRoot: reservation.sessionRoot,
688
+ });
689
+ const current = await readRarebitCurrent({
690
+ sessionFile: reservation.sessionFile,
691
+ native,
692
+ rarebitRoot: reservation.rarebitRoot,
693
+ sessionRoot: reservation.sessionRoot,
694
+ allowExternalSession: reservation.allowExternalSession,
695
+ });
696
+ const candidateState = projectRarebitArtifactState({
697
+ native,
698
+ materialization: {
699
+ availability: "available",
700
+ records: [current.receipt, settled].filter(Boolean),
701
+ },
702
+ });
703
+ const semanticCurrent =
704
+ native.availability === "available" &&
705
+ settled.type === "rarebit_summary" &&
706
+ candidateState.receiptRef?.jobId === settled.jobId;
707
+
708
+ let head = current.head;
709
+ const handle = await open(path, "a+", 0o600);
710
+ let receiptOffset;
711
+ try {
712
+ ({ size: receiptOffset } = await handle.stat());
713
+ await handle.write(receiptLine);
714
+ if (semanticCurrent) {
715
+ head = {
716
+ type: "rarebit_head",
717
+ protocolVersion: RAREBIT_SIDECAR_PROTOCOL_VERSION,
718
+ sessionId: settled.sessionId,
719
+ receiptOffset,
720
+ receiptLength: receiptLine.length,
721
+ receiptHash: sha256Bytes(receiptLine),
722
+ };
723
+ }
724
+ if (head) {
725
+ const headLine = Buffer.from(`${JSON.stringify(head)}\n`);
726
+ if (headLine.length > MAX_HEAD_BYTES)
727
+ throw new RangeError("Rarebit head exceeds protocol byte limit");
728
+ await handle.write(headLine);
729
+ }
730
+ await handle.sync();
731
+ } finally {
732
+ await handle.close();
733
+ }
734
+ await chmod(path, 0o600);
735
+
736
+ if (!(await ownsReservation(reservation)))
737
+ throw new Error("Rarebit reservation lease was fenced by a newer worker");
738
+ await writeJsonFile(reservation.claimPath, {
739
+ jobId: reservation.jobId,
740
+ state: "settled",
741
+ leaseToken: reservation.leaseToken,
742
+ receiptRef: {
743
+ receiptOffset,
744
+ receiptLength: receiptLine.length,
745
+ receiptHash: sha256Bytes(receiptLine),
746
+ },
747
+ });
748
+ } catch (error) {
749
+ if (lock && (await ownsReservation(reservation)))
750
+ await unlink(reservation.claimPath).catch((unlinkError) => {
751
+ if (unlinkError?.code !== "ENOENT") throw unlinkError;
752
+ });
753
+ throw error;
754
+ } finally {
755
+ await releaseCommitLock(lock);
756
+ }
757
+ return { ...settled, path };
758
+ }
759
+
760
+ export async function releaseRarebitJob(reservation) {
761
+ if (!reservation?.claimPath || !reservation.materializationPath) return;
762
+ const lock = await acquireCommitLock(reservation.materializationPath);
763
+ try {
764
+ if (await ownsReservation(reservation))
765
+ await unlink(reservation.claimPath).catch((error) => {
766
+ if (error?.code !== "ENOENT") throw error;
767
+ });
768
+ } finally {
769
+ await releaseCommitLock(lock);
770
+ }
771
+ }