@lore-co/cli 0.1.17 → 0.1.19

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 (46) hide show
  1. package/README.md +99 -17
  2. package/dist/ask.d.ts.map +1 -1
  3. package/dist/ask.js +3 -26
  4. package/dist/ask.js.map +1 -1
  5. package/dist/cli.d.ts +37 -2
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +1071 -80
  8. package/dist/cli.js.map +1 -1
  9. package/dist/context-fallback.d.ts +37 -0
  10. package/dist/context-fallback.d.ts.map +1 -0
  11. package/dist/context-fallback.js +259 -0
  12. package/dist/context-fallback.js.map +1 -0
  13. package/dist/generated-assets.d.ts +4 -4
  14. package/dist/generated-assets.d.ts.map +1 -1
  15. package/dist/generated-assets.js +4 -4
  16. package/dist/generated-assets.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/invocation-health-writer.d.ts +19 -0
  22. package/dist/invocation-health-writer.d.ts.map +1 -0
  23. package/dist/invocation-health-writer.js +131 -0
  24. package/dist/invocation-health-writer.js.map +1 -0
  25. package/dist/reliability-store.d.ts +283 -0
  26. package/dist/reliability-store.d.ts.map +1 -0
  27. package/dist/reliability-store.js +1913 -0
  28. package/dist/reliability-store.js.map +1 -0
  29. package/dist/runtime-version.d.ts +2 -0
  30. package/dist/runtime-version.d.ts.map +1 -0
  31. package/dist/runtime-version.js +5 -0
  32. package/dist/runtime-version.js.map +1 -0
  33. package/dist/runtime.d.ts +9 -1
  34. package/dist/runtime.d.ts.map +1 -1
  35. package/dist/runtime.js +1113 -153
  36. package/dist/runtime.js.map +1 -1
  37. package/dist/self-host.d.ts +15 -2
  38. package/dist/self-host.d.ts.map +1 -1
  39. package/dist/self-host.js +55 -8
  40. package/dist/self-host.js.map +1 -1
  41. package/dist/signed-snapshot.d.ts +59 -0
  42. package/dist/signed-snapshot.d.ts.map +1 -0
  43. package/dist/signed-snapshot.js +303 -0
  44. package/dist/signed-snapshot.js.map +1 -0
  45. package/dist/update.js +3 -3
  46. package/package.json +19 -3
@@ -0,0 +1,1913 @@
1
+ import { constants } from "node:fs";
2
+ import { chmod, lstat, mkdir, open, readdir, rename, rm, rmdir, stat, unlink, } from "node:fs/promises";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { homedir, hostname } from "node:os";
5
+ import { basename, dirname, resolve } from "node:path";
6
+ export const RELIABILITY_STORE_VERSION = 1;
7
+ export const DEFAULT_MAX_RECORD_BYTES = 1024 * 1024;
8
+ export const DEFAULT_MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024;
9
+ export const DEFAULT_MAX_OUTBOX_ITEMS = 100;
10
+ export const DEFAULT_MAX_OUTBOX_BYTES = 64 * 1024 * 1024;
11
+ export const DEFAULT_MAX_CONTEXT_CACHE_ITEMS = 256;
12
+ export const DEFAULT_MAX_CONTEXT_CACHE_BYTES = 64 * 1024 * 1024;
13
+ const MAX_CONTEXT_CACHE_SCAN_FILES = DEFAULT_MAX_CONTEXT_CACHE_ITEMS * 16;
14
+ const DIRECTORY_MODE = 0o700;
15
+ const FILE_MODE = 0o600;
16
+ const OUTBOX_STATES = [
17
+ "ready",
18
+ "in-flight",
19
+ "auth-blocked",
20
+ "dead",
21
+ ];
22
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
23
+ export class ReliabilityStoreError extends Error {
24
+ code;
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.name = "ReliabilityStoreError";
28
+ this.code = code;
29
+ }
30
+ }
31
+ function isErrno(error, code) {
32
+ return (typeof error === "object" &&
33
+ error !== null &&
34
+ "code" in error &&
35
+ error.code === code);
36
+ }
37
+ function storeError(code, message) {
38
+ return new ReliabilityStoreError(code, message);
39
+ }
40
+ function positiveInteger(value, fallback, name) {
41
+ const selected = value ?? fallback;
42
+ if (!Number.isSafeInteger(selected) || selected <= 0) {
43
+ throw storeError("INVALID_INPUT", `${name} must be a positive integer`);
44
+ }
45
+ return selected;
46
+ }
47
+ function dateIso(date, name) {
48
+ if (!Number.isFinite(date.getTime())) {
49
+ throw storeError("INVALID_INPUT", `${name} must be a valid date`);
50
+ }
51
+ return date.toISOString();
52
+ }
53
+ function validIso(value) {
54
+ return (typeof value === "string" &&
55
+ Number.isFinite(Date.parse(value)) &&
56
+ new Date(value).toISOString() === value);
57
+ }
58
+ function contextCacheRecord(value) {
59
+ const record = objectRecord(value);
60
+ if (record.version !== RELIABILITY_STORE_VERSION ||
61
+ typeof record.cacheKey !== "string" ||
62
+ !validIso(record.writtenAt) ||
63
+ record.value === undefined) {
64
+ throw storeError("CORRUPT_RECORD", "Context cache record is invalid");
65
+ }
66
+ return record;
67
+ }
68
+ function boundedString(value, name, maxCharacters) {
69
+ const trimmed = value.trim();
70
+ if (trimmed === "" || Array.from(trimmed).length > maxCharacters) {
71
+ throw storeError("INVALID_INPUT", `${name} must contain 1-${maxCharacters} characters`);
72
+ }
73
+ return trimmed;
74
+ }
75
+ function workspaceSegment(workspaceId) {
76
+ const value = boundedString(workspaceId, "workspaceId", 200);
77
+ if (value === "." ||
78
+ value === ".." ||
79
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(value)) {
80
+ throw storeError("INVALID_INPUT", "workspaceId is not path-safe");
81
+ }
82
+ return value;
83
+ }
84
+ function sha256(value) {
85
+ return createHash("sha256").update(value).digest("hex");
86
+ }
87
+ function truncate(value, maxCharacters = 1_000) {
88
+ return Array.from(value.trim()).slice(0, maxCharacters).join("");
89
+ }
90
+ function normalizeJson(value, state, depth) {
91
+ state.nodes += 1;
92
+ if (state.nodes > 100_000 || depth > 100) {
93
+ throw storeError("SERIALIZATION_LIMIT", "JSON value is too complex");
94
+ }
95
+ if (value === null ||
96
+ typeof value === "boolean" ||
97
+ typeof value === "string") {
98
+ return value;
99
+ }
100
+ if (typeof value === "number") {
101
+ if (!Number.isFinite(value)) {
102
+ throw storeError("INVALID_INPUT", "JSON numbers must be finite");
103
+ }
104
+ return value;
105
+ }
106
+ if (typeof value !== "object") {
107
+ throw storeError("INVALID_INPUT", "Value is not JSON-serializable");
108
+ }
109
+ if (state.seen.has(value)) {
110
+ throw storeError("INVALID_INPUT", "Circular JSON values are not supported");
111
+ }
112
+ state.seen.add(value);
113
+ try {
114
+ if (Array.isArray(value)) {
115
+ return value.map((item) => normalizeJson(item, state, depth + 1));
116
+ }
117
+ const prototype = Object.getPrototypeOf(value);
118
+ if (prototype !== Object.prototype && prototype !== null) {
119
+ throw storeError("INVALID_INPUT", "Only plain JSON objects are supported");
120
+ }
121
+ const normalized = {};
122
+ for (const key of Object.keys(value).sort()) {
123
+ normalized[key] = normalizeJson(value[key], state, depth + 1);
124
+ }
125
+ return normalized;
126
+ }
127
+ finally {
128
+ state.seen.delete(value);
129
+ }
130
+ }
131
+ export function serializeBoundedJson(value, maxBytes = DEFAULT_MAX_RECORD_BYTES) {
132
+ positiveInteger(maxBytes, DEFAULT_MAX_RECORD_BYTES, "maxBytes");
133
+ const normalized = normalizeJson(value, { nodes: 0, seen: new Set() }, 0);
134
+ const serialized = `${JSON.stringify(normalized)}\n`;
135
+ if (Buffer.byteLength(serialized) > maxBytes) {
136
+ throw storeError("SERIALIZATION_LIMIT", `Serialized JSON exceeds ${maxBytes} bytes`);
137
+ }
138
+ return serialized;
139
+ }
140
+ export function parseBoundedJson(serialized, maxBytes = DEFAULT_MAX_RECORD_BYTES) {
141
+ if (Buffer.byteLength(serialized) > maxBytes) {
142
+ throw storeError("SERIALIZATION_LIMIT", `JSON exceeds ${maxBytes} bytes`);
143
+ }
144
+ let parsed;
145
+ try {
146
+ parsed = JSON.parse(serialized);
147
+ }
148
+ catch {
149
+ throw storeError("CORRUPT_RECORD", "Stored JSON is malformed");
150
+ }
151
+ return normalizeJson(parsed, { nodes: 0, seen: new Set() }, 0);
152
+ }
153
+ async function assertDirectory(path) {
154
+ let metadata;
155
+ try {
156
+ metadata = await lstat(path);
157
+ }
158
+ catch (error) {
159
+ if (isErrno(error, "ENOENT")) {
160
+ throw storeError("UNSAFE_PATH", `Directory does not exist: ${path}`);
161
+ }
162
+ throw error;
163
+ }
164
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
165
+ throw storeError("UNSAFE_PATH", `Expected a real directory: ${path}`);
166
+ }
167
+ }
168
+ async function ensurePrivateDirectory(path) {
169
+ await assertDirectory(dirname(path));
170
+ let created = false;
171
+ try {
172
+ await mkdir(path, { mode: DIRECTORY_MODE });
173
+ created = true;
174
+ }
175
+ catch (error) {
176
+ if (!isErrno(error, "EEXIST")) {
177
+ throw error;
178
+ }
179
+ }
180
+ const metadata = await lstat(path);
181
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
182
+ throw storeError("UNSAFE_PATH", `Refusing unsafe directory: ${path}`);
183
+ }
184
+ await chmod(path, DIRECTORY_MODE);
185
+ if (created) {
186
+ await fsyncDirectory(dirname(path));
187
+ }
188
+ }
189
+ async function regularFileMetadata(path, missingOk = false) {
190
+ try {
191
+ const metadata = await lstat(path);
192
+ if (metadata.isSymbolicLink() || !metadata.isFile()) {
193
+ throw storeError("UNSAFE_PATH", `Refusing non-regular file: ${path}`);
194
+ }
195
+ return metadata;
196
+ }
197
+ catch (error) {
198
+ if (missingOk && isErrno(error, "ENOENT")) {
199
+ return null;
200
+ }
201
+ throw error;
202
+ }
203
+ }
204
+ async function privateFileMetadata(path) {
205
+ const metadata = await regularFileMetadata(path);
206
+ if (metadata === null || (metadata.mode & 0o777) !== FILE_MODE) {
207
+ throw storeError("UNSAFE_PATH", `File permissions must be 0600: ${path}`);
208
+ }
209
+ return metadata;
210
+ }
211
+ export async function fsyncDirectory(path) {
212
+ await assertDirectory(path);
213
+ const handle = await open(path, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
214
+ try {
215
+ await handle.sync();
216
+ }
217
+ finally {
218
+ await handle.close();
219
+ }
220
+ }
221
+ export async function atomicWriteFile(path, contents, maxBytes = DEFAULT_MAX_SNAPSHOT_BYTES) {
222
+ const bytes = typeof contents === "string"
223
+ ? Buffer.byteLength(contents)
224
+ : contents.byteLength;
225
+ if (bytes > maxBytes) {
226
+ throw storeError("SERIALIZATION_LIMIT", `File contents exceed ${maxBytes} bytes`);
227
+ }
228
+ const parent = dirname(path);
229
+ await assertDirectory(parent);
230
+ await regularFileMetadata(path, true);
231
+ const temporaryPath = resolve(parent, `.${sha256(path).slice(0, 16)}.${process.pid}.${randomUUID()}.tmp`);
232
+ let handle;
233
+ try {
234
+ handle = await open(temporaryPath, constants.O_CREAT |
235
+ constants.O_EXCL |
236
+ constants.O_WRONLY |
237
+ (constants.O_NOFOLLOW ?? 0), FILE_MODE);
238
+ await handle.writeFile(contents);
239
+ await handle.chmod(FILE_MODE);
240
+ await handle.sync();
241
+ await handle.close();
242
+ handle = undefined;
243
+ await regularFileMetadata(path, true);
244
+ await rename(temporaryPath, path);
245
+ await chmod(path, FILE_MODE);
246
+ await fsyncDirectory(parent);
247
+ }
248
+ catch (error) {
249
+ if (handle !== undefined) {
250
+ await handle.close().catch(() => undefined);
251
+ }
252
+ await unlink(temporaryPath).catch(() => undefined);
253
+ throw error;
254
+ }
255
+ }
256
+ export async function atomicWriteJson(path, value, maxBytes = DEFAULT_MAX_RECORD_BYTES) {
257
+ await atomicWriteFile(path, serializeBoundedJson(value, maxBytes), maxBytes);
258
+ }
259
+ export async function readBoundedFile(path, maxBytes = DEFAULT_MAX_SNAPSHOT_BYTES) {
260
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
261
+ try {
262
+ const metadata = await handle.stat();
263
+ if (!metadata.isFile()) {
264
+ throw storeError("UNSAFE_PATH", `Refusing non-regular file: ${path}`);
265
+ }
266
+ if (metadata.size > maxBytes) {
267
+ throw storeError("SERIALIZATION_LIMIT", `File exceeds ${maxBytes} bytes`);
268
+ }
269
+ return await handle.readFile("utf8");
270
+ }
271
+ finally {
272
+ await handle.close();
273
+ }
274
+ }
275
+ export async function readBoundedJsonFile(path, maxBytes = DEFAULT_MAX_RECORD_BYTES) {
276
+ return parseBoundedJson(await readBoundedFile(path, maxBytes), maxBytes);
277
+ }
278
+ export async function durableUnlink(path, missingOk = false) {
279
+ const metadata = await regularFileMetadata(path, missingOk);
280
+ if (metadata === null) {
281
+ return false;
282
+ }
283
+ await unlink(path);
284
+ await fsyncDirectory(dirname(path));
285
+ return true;
286
+ }
287
+ export async function durableMove(sourcePath, destinationPath) {
288
+ await regularFileMetadata(sourcePath);
289
+ await assertDirectory(dirname(destinationPath));
290
+ if ((await regularFileMetadata(destinationPath, true)) !== null) {
291
+ throw storeError("IDEMPOTENCY_CONFLICT", `Destination already exists: ${destinationPath}`);
292
+ }
293
+ const sourceHandle = await open(sourcePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
294
+ try {
295
+ const metadata = await sourceHandle.stat();
296
+ if (!metadata.isFile()) {
297
+ throw storeError("UNSAFE_PATH", `Refusing non-regular file: ${sourcePath}`);
298
+ }
299
+ await sourceHandle.sync();
300
+ }
301
+ finally {
302
+ await sourceHandle.close();
303
+ }
304
+ await rename(sourcePath, destinationPath);
305
+ await fsyncDirectory(dirname(destinationPath));
306
+ if (dirname(sourcePath) !== dirname(destinationPath)) {
307
+ await fsyncDirectory(dirname(sourcePath));
308
+ }
309
+ }
310
+ async function sleep(milliseconds) {
311
+ await new Promise((resolvePromise) => {
312
+ setTimeout(resolvePromise, milliseconds);
313
+ });
314
+ }
315
+ async function removeOwnedLock(lockPath, ownerPath, token) {
316
+ try {
317
+ const parsed = await readBoundedJsonFile(ownerPath, 4_096);
318
+ if (typeof parsed !== "object" ||
319
+ parsed === null ||
320
+ Array.isArray(parsed) ||
321
+ parsed.token !== token) {
322
+ return;
323
+ }
324
+ await durableUnlink(ownerPath, true);
325
+ await rmdir(lockPath);
326
+ await fsyncDirectory(dirname(lockPath));
327
+ }
328
+ catch (error) {
329
+ if (!isErrno(error, "ENOENT")) {
330
+ throw error;
331
+ }
332
+ }
333
+ }
334
+ async function stealStaleLock(lockPath, staleMs) {
335
+ let metadata;
336
+ try {
337
+ metadata = await lstat(lockPath);
338
+ }
339
+ catch (error) {
340
+ if (isErrno(error, "ENOENT")) {
341
+ return;
342
+ }
343
+ throw error;
344
+ }
345
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
346
+ throw storeError("UNSAFE_PATH", `Refusing unsafe lock: ${lockPath}`);
347
+ }
348
+ let ownerIsDead = false;
349
+ try {
350
+ const owner = objectRecord(await readBoundedJsonFile(resolve(lockPath, "owner.json"), 4_096));
351
+ if (owner.hostname === hostname() &&
352
+ typeof owner.pid === "number" &&
353
+ Number.isSafeInteger(owner.pid) &&
354
+ owner.pid > 0) {
355
+ try {
356
+ process.kill(owner.pid, 0);
357
+ return;
358
+ }
359
+ catch (error) {
360
+ if (!isErrno(error, "ESRCH")) {
361
+ return;
362
+ }
363
+ ownerIsDead = true;
364
+ }
365
+ }
366
+ }
367
+ catch {
368
+ // Old or malformed locks can be reclaimed after the stale threshold.
369
+ }
370
+ if (!ownerIsDead && Date.now() - metadata.mtimeMs <= staleMs) {
371
+ return;
372
+ }
373
+ const stalePath = `${lockPath}.stale.${randomUUID()}`;
374
+ try {
375
+ await rename(lockPath, stalePath);
376
+ }
377
+ catch (error) {
378
+ if (isErrno(error, "ENOENT")) {
379
+ return;
380
+ }
381
+ throw error;
382
+ }
383
+ await fsyncDirectory(dirname(lockPath));
384
+ await rm(stalePath, { recursive: true, force: false });
385
+ await fsyncDirectory(dirname(lockPath));
386
+ }
387
+ export async function withDirectoryLock(lockPath, operation, options = {}) {
388
+ const timeoutMs = positiveInteger(options.timeoutMs, 5_000, "timeoutMs");
389
+ const staleMs = positiveInteger(options.staleMs, 60_000, "staleMs");
390
+ const retryMs = positiveInteger(options.retryMs, 20, "retryMs");
391
+ await assertDirectory(dirname(lockPath));
392
+ const startedAt = Date.now();
393
+ const token = randomUUID();
394
+ const ownerPath = resolve(lockPath, "owner.json");
395
+ for (;;) {
396
+ let created = false;
397
+ try {
398
+ await mkdir(lockPath, { mode: DIRECTORY_MODE });
399
+ created = true;
400
+ await chmod(lockPath, DIRECTORY_MODE);
401
+ await fsyncDirectory(dirname(lockPath));
402
+ await atomicWriteJson(ownerPath, {
403
+ token,
404
+ pid: process.pid,
405
+ hostname: hostname(),
406
+ acquiredAt: new Date().toISOString(),
407
+ }, 4_096);
408
+ break;
409
+ }
410
+ catch (error) {
411
+ if (created) {
412
+ await rm(lockPath, { recursive: true, force: true });
413
+ await fsyncDirectory(dirname(lockPath));
414
+ }
415
+ if (!isErrno(error, "EEXIST")) {
416
+ throw error;
417
+ }
418
+ await stealStaleLock(lockPath, staleMs);
419
+ if (Date.now() - startedAt >= timeoutMs) {
420
+ throw storeError("LOCK_TIMEOUT", `Timed out acquiring ${lockPath}`);
421
+ }
422
+ await sleep(Math.min(retryMs, timeoutMs));
423
+ }
424
+ }
425
+ try {
426
+ return await operation();
427
+ }
428
+ finally {
429
+ await removeOwnedLock(lockPath, ownerPath, token);
430
+ }
431
+ }
432
+ export function computeRetryBackoffMs(attempt, options = {}) {
433
+ if (!Number.isSafeInteger(attempt) || attempt < 1) {
434
+ throw storeError("INVALID_INPUT", "attempt must be a positive integer");
435
+ }
436
+ const baseMs = positiveInteger(options.baseMs, 1_000, "baseMs");
437
+ const maxMs = positiveInteger(options.maxMs, 5 * 60_000, "maxMs");
438
+ const jitterRatio = options.jitterRatio ?? 0.2;
439
+ if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) {
440
+ throw storeError("INVALID_INPUT", "jitterRatio must be between 0 and 1");
441
+ }
442
+ const exponential = Math.min(maxMs, baseMs * 2 ** Math.min(attempt - 1, 30));
443
+ const random = options.random ?? Math.random;
444
+ const jitter = exponential * jitterRatio * (random() * 2 - 1);
445
+ return Math.max(0, Math.min(maxMs, Math.round(exponential + jitter)));
446
+ }
447
+ export function classifyRetryFailure(input) {
448
+ if (typeof input !== "object" || input === null) {
449
+ return "retryable";
450
+ }
451
+ const failure = input;
452
+ if (failure.incompatible === true) {
453
+ return "incompatible";
454
+ }
455
+ if (failure.retryable === true) {
456
+ return "retryable";
457
+ }
458
+ if (failure.retryable === false) {
459
+ return "permanent";
460
+ }
461
+ const httpStatus = typeof failure.httpStatus === "number"
462
+ ? failure.httpStatus
463
+ : typeof failure.status === "number"
464
+ ? failure.status
465
+ : undefined;
466
+ if (httpStatus === 401 || httpStatus === 403) {
467
+ return "authentication";
468
+ }
469
+ if (httpStatus === 408 ||
470
+ httpStatus === 409 ||
471
+ httpStatus === 425 ||
472
+ httpStatus === 429 ||
473
+ (httpStatus !== undefined && httpStatus >= 500)) {
474
+ return "retryable";
475
+ }
476
+ if (httpStatus !== undefined && httpStatus >= 400) {
477
+ return "permanent";
478
+ }
479
+ const code = failure.code;
480
+ if (typeof code === "string" &&
481
+ new Set([
482
+ "ECONNABORTED",
483
+ "ECONNREFUSED",
484
+ "ECONNRESET",
485
+ "EHOSTUNREACH",
486
+ "ENETDOWN",
487
+ "ENETUNREACH",
488
+ "ENOTFOUND",
489
+ "ETIMEDOUT",
490
+ ]).has(code)) {
491
+ return "retryable";
492
+ }
493
+ return "retryable";
494
+ }
495
+ function objectRecord(value) {
496
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
497
+ throw storeError("CORRUPT_RECORD", "Stored record must be an object");
498
+ }
499
+ return value;
500
+ }
501
+ function parseRetry(value) {
502
+ const record = value === undefined ? null : objectRecord(value);
503
+ if (record === null ||
504
+ typeof record.attempts !== "number" ||
505
+ !Number.isSafeInteger(record.attempts) ||
506
+ record.attempts < 0 ||
507
+ !(record.nextAttemptAt === null ||
508
+ validIso(record.nextAttemptAt)) ||
509
+ !(record.lastAttemptAt === null ||
510
+ validIso(record.lastAttemptAt)) ||
511
+ !(record.lastError === null ||
512
+ typeof record.lastError === "string") ||
513
+ !(record.lastFailureClassification === null ||
514
+ record.lastFailureClassification === "retryable" ||
515
+ record.lastFailureClassification === "authentication" ||
516
+ record.lastFailureClassification === "permanent" ||
517
+ record.lastFailureClassification === "incompatible")) {
518
+ throw storeError("CORRUPT_RECORD", "Stored retry metadata is invalid");
519
+ }
520
+ return {
521
+ attempts: record.attempts,
522
+ nextAttemptAt: record.nextAttemptAt,
523
+ lastAttemptAt: record.lastAttemptAt,
524
+ lastError: record.lastError,
525
+ lastFailureClassification: record.lastFailureClassification,
526
+ };
527
+ }
528
+ function parseClaim(value) {
529
+ if (value === undefined) {
530
+ return undefined;
531
+ }
532
+ const record = objectRecord(value);
533
+ if (typeof record.id !== "string" ||
534
+ typeof record.workerId !== "string" ||
535
+ !validIso(record.claimedAt) ||
536
+ !validIso(record.leaseExpiresAt)) {
537
+ throw storeError("CORRUPT_RECORD", "Stored claim is invalid");
538
+ }
539
+ return {
540
+ id: record.id,
541
+ workerId: record.workerId,
542
+ claimedAt: record.claimedAt,
543
+ leaseExpiresAt: record.leaseExpiresAt,
544
+ };
545
+ }
546
+ function parseStoredOutbox(value, state) {
547
+ const record = objectRecord(value);
548
+ if (record.version !== RELIABILITY_STORE_VERSION ||
549
+ typeof record.id !== "string" ||
550
+ typeof record.sequence !== "number" ||
551
+ !Number.isSafeInteger(record.sequence) ||
552
+ record.sequence < 1 ||
553
+ typeof record.kind !== "string" ||
554
+ typeof record.idempotencyKey !== "string" ||
555
+ typeof record.payloadHash !== "string" ||
556
+ !/^[a-f0-9]{64}$/u.test(record.payloadHash) ||
557
+ record.payload === undefined ||
558
+ !validIso(record.enqueuedAt)) {
559
+ throw storeError("CORRUPT_RECORD", "Stored outbox entry is invalid");
560
+ }
561
+ const claim = parseClaim(record.claim);
562
+ return {
563
+ version: RELIABILITY_STORE_VERSION,
564
+ id: record.id,
565
+ sequence: record.sequence,
566
+ state,
567
+ kind: record.kind,
568
+ idempotencyKey: record.idempotencyKey,
569
+ payloadHash: record.payloadHash,
570
+ payload: record.payload,
571
+ enqueuedAt: record.enqueuedAt,
572
+ retry: parseRetry(record.retry),
573
+ ...(state !== "in-flight" || claim === undefined ? {} : { claim }),
574
+ };
575
+ }
576
+ function storedEntry(entry) {
577
+ const { state: _state, ...stored } = entry;
578
+ return stored;
579
+ }
580
+ function sequenceFilename(sequence, id) {
581
+ return `${String(sequence).padStart(16, "0")}-${id}.json`;
582
+ }
583
+ function stateDirectoryName(state) {
584
+ return state;
585
+ }
586
+ function trustKey(value, workspaceId) {
587
+ if (value.workspaceId !== workspaceId ||
588
+ value.kty !== "OKP" ||
589
+ value.crv !== "Ed25519" ||
590
+ typeof value.kid !== "string" ||
591
+ value.kid.trim() === "" ||
592
+ typeof value.x !== "string" ||
593
+ !BASE64URL_PATTERN.test(value.x) ||
594
+ value.x.includes("=") ||
595
+ Buffer.from(value.x, "base64url").byteLength !== 32 ||
596
+ (value.alg !== undefined && value.alg !== "EdDSA") ||
597
+ (value.use !== undefined && value.use !== "sig") ||
598
+ (value.notBefore !== undefined && !validIso(value.notBefore)) ||
599
+ (value.retiredAt !== undefined &&
600
+ value.retiredAt !== null &&
601
+ !validIso(value.retiredAt)) ||
602
+ "d" in value) {
603
+ throw storeError("INVALID_INPUT", "Public trust key is invalid");
604
+ }
605
+ return {
606
+ workspaceId,
607
+ kid: boundedString(value.kid, "kid", 200),
608
+ kty: "OKP",
609
+ crv: "Ed25519",
610
+ x: value.x,
611
+ ...(value.alg === undefined ? {} : { alg: value.alg }),
612
+ ...(value.use === undefined ? {} : { use: value.use }),
613
+ ...(value.notBefore === undefined
614
+ ? {}
615
+ : { notBefore: value.notBefore }),
616
+ ...(value.retiredAt === undefined
617
+ ? {}
618
+ : { retiredAt: value.retiredAt }),
619
+ };
620
+ }
621
+ export class ReliabilityStore {
622
+ workspaceId;
623
+ directory;
624
+ home;
625
+ rootDirectory;
626
+ maxRecordBytes;
627
+ maxSnapshotBytes;
628
+ maxOutboxItems;
629
+ maxOutboxBytes;
630
+ lockOptions;
631
+ now;
632
+ random;
633
+ constructor(workspaceId, options = {}) {
634
+ this.workspaceId = workspaceSegment(workspaceId);
635
+ this.home = resolve(options.home ?? homedir());
636
+ this.rootDirectory = resolve(this.home, ".lore", "store-v1");
637
+ this.directory = resolve(this.rootDirectory, "workspaces", this.workspaceId);
638
+ this.maxRecordBytes = positiveInteger(options.maxRecordBytes, DEFAULT_MAX_RECORD_BYTES, "maxRecordBytes");
639
+ this.maxSnapshotBytes = positiveInteger(options.maxSnapshotBytes, DEFAULT_MAX_SNAPSHOT_BYTES, "maxSnapshotBytes");
640
+ this.maxOutboxItems = positiveInteger(options.maxOutboxItems, DEFAULT_MAX_OUTBOX_ITEMS, "maxOutboxItems");
641
+ this.maxOutboxBytes = positiveInteger(options.maxOutboxBytes, DEFAULT_MAX_OUTBOX_BYTES, "maxOutboxBytes");
642
+ this.lockOptions = {
643
+ timeoutMs: positiveInteger(options.lockTimeoutMs, 5_000, "lockTimeoutMs"),
644
+ staleMs: positiveInteger(options.lockStaleMs, 60_000, "lockStaleMs"),
645
+ retryMs: positiveInteger(options.lockRetryMs, 20, "lockRetryMs"),
646
+ };
647
+ this.now = options.now ?? (() => new Date());
648
+ this.random = options.random ?? Math.random;
649
+ }
650
+ layoutPaths() {
651
+ const lore = resolve(this.home, ".lore");
652
+ const workspaces = resolve(this.rootDirectory, "workspaces");
653
+ return [
654
+ lore,
655
+ this.rootDirectory,
656
+ workspaces,
657
+ this.directory,
658
+ resolve(this.directory, "outbox"),
659
+ ...OUTBOX_STATES.map((state) => resolve(this.directory, "outbox", stateDirectoryName(state))),
660
+ resolve(this.directory, "cache"),
661
+ resolve(this.directory, "cache", "context"),
662
+ resolve(this.directory, "cache", "policy"),
663
+ resolve(this.directory, "state"),
664
+ resolve(this.directory, "trust"),
665
+ resolve(this.directory, "locks"),
666
+ resolve(this.directory, "quarantine"),
667
+ ];
668
+ }
669
+ async initialize() {
670
+ await assertDirectory(this.home);
671
+ for (const path of this.layoutPaths()) {
672
+ await ensurePrivateDirectory(path);
673
+ }
674
+ }
675
+ async assertExistingLayout() {
676
+ await assertDirectory(this.home);
677
+ for (const path of this.layoutPaths()) {
678
+ const metadata = await lstat(path);
679
+ if (metadata.isSymbolicLink() ||
680
+ !metadata.isDirectory() ||
681
+ (metadata.mode & 0o777) !== DIRECTORY_MODE) {
682
+ throw storeError("UNSAFE_PATH", `Store directory permissions must be 0700: ${path}`);
683
+ }
684
+ }
685
+ }
686
+ stateDirectory(state) {
687
+ return resolve(this.directory, "outbox", stateDirectoryName(state));
688
+ }
689
+ async withLock(operation, lockName = "mutation.lock") {
690
+ await this.initialize();
691
+ return withDirectoryLock(resolve(this.directory, "locks", lockName), operation, this.lockOptions);
692
+ }
693
+ stateLockName(key) {
694
+ return `state-${sha256(key)}.lock`;
695
+ }
696
+ async scanOutbox(states = OUTBOX_STATES) {
697
+ const located = [];
698
+ for (const state of states) {
699
+ const directory = this.stateDirectory(state);
700
+ const names = (await readdir(directory))
701
+ .filter((name) => name.endsWith(".json"))
702
+ .sort();
703
+ for (const name of names) {
704
+ const path = resolve(directory, name);
705
+ const metadata = await regularFileMetadata(path);
706
+ if (metadata === null) {
707
+ continue;
708
+ }
709
+ const entry = parseStoredOutbox(await readBoundedJsonFile(path, this.maxRecordBytes), state);
710
+ located.push({ entry, path, bytes: metadata.size });
711
+ }
712
+ }
713
+ return located.sort((left, right) => left.entry.sequence - right.entry.sequence);
714
+ }
715
+ async nextSequence(entries) {
716
+ const counterPath = resolve(this.directory, "outbox", "sequence.json");
717
+ let fromCounter = 1;
718
+ try {
719
+ const record = objectRecord(await readBoundedJsonFile(counterPath, 4_096));
720
+ if (record.version !== RELIABILITY_STORE_VERSION ||
721
+ typeof record.nextSequence !== "number" ||
722
+ !Number.isSafeInteger(record.nextSequence) ||
723
+ record.nextSequence < 1) {
724
+ throw storeError("CORRUPT_RECORD", "Outbox sequence is invalid");
725
+ }
726
+ fromCounter = record.nextSequence;
727
+ }
728
+ catch (error) {
729
+ if (!isErrno(error, "ENOENT")) {
730
+ throw error;
731
+ }
732
+ }
733
+ const highest = entries.length === 0
734
+ ? 0
735
+ : Math.max(...entries.map(({ entry }) => entry.sequence));
736
+ return Math.max(fromCounter, highest + 1);
737
+ }
738
+ async writeEntry(path, entry) {
739
+ await atomicWriteJson(path, storedEntry(entry), this.maxRecordBytes);
740
+ }
741
+ async enqueue(input) {
742
+ const kind = boundedString(input.kind, "kind", 100);
743
+ const idempotencyKey = boundedString(input.idempotencyKey, "idempotencyKey", 500);
744
+ const payload = normalizeJson(input.payload, { nodes: 0, seen: new Set() }, 0);
745
+ const payloadHash = sha256(serializeBoundedJson({ kind, payload }, this.maxRecordBytes));
746
+ const enqueuedAt = dateIso(input.enqueuedAt ?? this.now(), "enqueuedAt");
747
+ return this.withLock(async () => {
748
+ const entries = await this.scanOutbox();
749
+ const existing = entries.find(({ entry }) => entry.idempotencyKey === idempotencyKey);
750
+ if (existing !== undefined) {
751
+ if (existing.entry.payloadHash !== payloadHash ||
752
+ existing.entry.kind !== kind) {
753
+ throw storeError("IDEMPOTENCY_CONFLICT", "Idempotency key was already used with a different payload");
754
+ }
755
+ return { entry: existing.entry, created: false };
756
+ }
757
+ if (entries.length >= this.maxOutboxItems) {
758
+ throw storeError("CAPACITY_EXCEEDED", "Outbox item capacity has been reached");
759
+ }
760
+ const sequence = await this.nextSequence(entries);
761
+ if (!Number.isSafeInteger(sequence)) {
762
+ throw storeError("CAPACITY_EXCEEDED", "Outbox sequence is exhausted");
763
+ }
764
+ const id = randomUUID();
765
+ const entry = {
766
+ version: RELIABILITY_STORE_VERSION,
767
+ id,
768
+ sequence,
769
+ state: "ready",
770
+ kind,
771
+ idempotencyKey,
772
+ payloadHash,
773
+ payload,
774
+ enqueuedAt,
775
+ retry: {
776
+ attempts: 0,
777
+ nextAttemptAt: enqueuedAt,
778
+ lastAttemptAt: null,
779
+ lastError: null,
780
+ lastFailureClassification: null,
781
+ },
782
+ };
783
+ const serialized = serializeBoundedJson(storedEntry(entry), this.maxRecordBytes);
784
+ const usedBytes = entries.reduce((sum, item) => sum + item.bytes, 0);
785
+ if (usedBytes + Buffer.byteLength(serialized) > this.maxOutboxBytes) {
786
+ throw storeError("CAPACITY_EXCEEDED", "Outbox byte capacity has been reached");
787
+ }
788
+ const path = resolve(this.stateDirectory("ready"), sequenceFilename(sequence, id));
789
+ await atomicWriteFile(path, serialized, this.maxRecordBytes);
790
+ const counter = {
791
+ version: RELIABILITY_STORE_VERSION,
792
+ nextSequence: sequence + 1,
793
+ };
794
+ await atomicWriteJson(resolve(this.directory, "outbox", "sequence.json"), counter, 4_096);
795
+ return { entry, created: true };
796
+ });
797
+ }
798
+ async transferPendingTo(destination) {
799
+ if (destination.directory === this.directory) {
800
+ return 0;
801
+ }
802
+ await destination.initialize();
803
+ return this.withLock(async () => {
804
+ await this.recoverStaleClaimsLocked(this.now());
805
+ const pending = await this.scanOutbox(["ready", "auth-blocked"]);
806
+ let transferred = 0;
807
+ for (const located of pending) {
808
+ await destination.enqueue({
809
+ kind: located.entry.kind,
810
+ idempotencyKey: located.entry.idempotencyKey,
811
+ payload: located.entry.payload,
812
+ enqueuedAt: new Date(located.entry.enqueuedAt),
813
+ });
814
+ await durableUnlink(located.path);
815
+ transferred += 1;
816
+ }
817
+ return transferred;
818
+ });
819
+ }
820
+ async recoverStaleClaimsLocked(now) {
821
+ const nowIso = dateIso(now, "now");
822
+ const inFlight = await this.scanOutbox(["in-flight"]);
823
+ let recovered = 0;
824
+ for (const located of inFlight) {
825
+ const claim = located.entry.claim;
826
+ if (claim !== undefined &&
827
+ Date.parse(claim.leaseExpiresAt) > now.getTime()) {
828
+ continue;
829
+ }
830
+ const { claim: _claim, ...withoutClaim } = located.entry;
831
+ const recoveredEntry = {
832
+ ...withoutClaim,
833
+ state: "ready",
834
+ retry: {
835
+ attempts: located.entry.retry.attempts + 1,
836
+ nextAttemptAt: nowIso,
837
+ lastAttemptAt: located.entry.retry.lastAttemptAt,
838
+ lastError: "Recovered stale in-flight claim",
839
+ lastFailureClassification: "retryable",
840
+ },
841
+ };
842
+ const destination = resolve(this.stateDirectory("ready"), basename(located.path));
843
+ await durableMove(located.path, destination);
844
+ await this.writeEntry(destination, recoveredEntry);
845
+ recovered += 1;
846
+ }
847
+ return recovered;
848
+ }
849
+ async recoverStaleClaims(now = this.now()) {
850
+ return this.withLock(() => this.recoverStaleClaimsLocked(now));
851
+ }
852
+ async claimNext(options) {
853
+ const workerId = boundedString(options.workerId, "workerId", 200);
854
+ const kinds = options.kinds === undefined
855
+ ? undefined
856
+ : new Set(options.kinds.map((kind) => boundedString(kind, "outbox kind", 200)));
857
+ const leaseMs = positiveInteger(options.leaseMs, 30_000, "leaseMs");
858
+ const now = options.now ?? this.now();
859
+ const nowIso = dateIso(now, "now");
860
+ return this.withLock(async () => {
861
+ await this.recoverStaleClaimsLocked(now);
862
+ const active = (await this.scanOutbox(["in-flight"])).filter(({ entry }) => kinds === undefined || kinds.has(entry.kind));
863
+ if (active.length > 0) {
864
+ return null;
865
+ }
866
+ const ready = (await this.scanOutbox(["ready"])).filter(({ entry }) => kinds === undefined || kinds.has(entry.kind));
867
+ const first = ready[0];
868
+ if (first === undefined ||
869
+ (first.entry.retry.nextAttemptAt !== null &&
870
+ Date.parse(first.entry.retry.nextAttemptAt) > now.getTime())) {
871
+ return null;
872
+ }
873
+ const claim = {
874
+ id: randomUUID(),
875
+ workerId,
876
+ claimedAt: nowIso,
877
+ leaseExpiresAt: new Date(now.getTime() + leaseMs).toISOString(),
878
+ };
879
+ const claimed = {
880
+ ...first.entry,
881
+ state: "in-flight",
882
+ retry: {
883
+ ...first.entry.retry,
884
+ lastAttemptAt: nowIso,
885
+ },
886
+ claim,
887
+ };
888
+ const destination = resolve(this.stateDirectory("in-flight"), basename(first.path));
889
+ await durableMove(first.path, destination);
890
+ await this.writeEntry(destination, claimed);
891
+ return claimed;
892
+ });
893
+ }
894
+ async claimedEntry(claimId) {
895
+ const claimed = (await this.scanOutbox(["in-flight"])).find(({ entry }) => entry.claim?.id === claimId);
896
+ if (claimed === undefined) {
897
+ throw storeError("CLAIM_CONFLICT", "Outbox claim is no longer active");
898
+ }
899
+ return claimed;
900
+ }
901
+ async acknowledgeClaim(claimId) {
902
+ const id = boundedString(claimId, "claimId", 200);
903
+ await this.withLock(async () => {
904
+ const claimed = await this.claimedEntry(id);
905
+ await durableUnlink(claimed.path);
906
+ });
907
+ }
908
+ async failClaim(input) {
909
+ const claimId = boundedString(input.claimId, "claimId", 200);
910
+ const message = truncate(input.message);
911
+ if (message === "") {
912
+ throw storeError("INVALID_INPUT", "message must not be empty");
913
+ }
914
+ const now = input.now ?? this.now();
915
+ const nowIso = dateIso(now, "now");
916
+ return this.withLock(async () => {
917
+ const located = await this.claimedEntry(claimId);
918
+ const attempts = located.entry.retry.attempts + 1;
919
+ const retryable = input.classification === "retryable";
920
+ const nextState = input.classification === "authentication"
921
+ ? "auth-blocked"
922
+ : retryable
923
+ ? "ready"
924
+ : "dead";
925
+ const retryAfterMs = input.retryAfterMs === undefined
926
+ ? computeRetryBackoffMs(attempts, { random: this.random })
927
+ : positiveInteger(input.retryAfterMs, 1, "retryAfterMs");
928
+ const nextAttemptAt = nextState === "ready"
929
+ ? new Date(now.getTime() + retryAfterMs).toISOString()
930
+ : null;
931
+ const { claim: _claim, ...withoutClaim } = located.entry;
932
+ const failed = {
933
+ ...withoutClaim,
934
+ state: nextState,
935
+ retry: {
936
+ attempts,
937
+ nextAttemptAt,
938
+ lastAttemptAt: nowIso,
939
+ lastError: message,
940
+ lastFailureClassification: input.classification,
941
+ },
942
+ };
943
+ const destination = resolve(this.stateDirectory(nextState), basename(located.path));
944
+ await durableMove(located.path, destination);
945
+ await this.writeEntry(destination, failed);
946
+ return failed;
947
+ });
948
+ }
949
+ async releaseAuthBlocked(now = this.now()) {
950
+ const nowIso = dateIso(now, "now");
951
+ return this.withLock(async () => {
952
+ const blocked = await this.scanOutbox(["auth-blocked"]);
953
+ for (const located of blocked) {
954
+ const released = {
955
+ ...located.entry,
956
+ state: "ready",
957
+ retry: {
958
+ ...located.entry.retry,
959
+ nextAttemptAt: nowIso,
960
+ },
961
+ };
962
+ const destination = resolve(this.stateDirectory("ready"), basename(located.path));
963
+ await durableMove(located.path, destination);
964
+ await this.writeEntry(destination, released);
965
+ }
966
+ return blocked.length;
967
+ });
968
+ }
969
+ async removeDead(sequence) {
970
+ if (!Number.isSafeInteger(sequence) || sequence < 1) {
971
+ throw storeError("INVALID_INPUT", "sequence must be a positive integer");
972
+ }
973
+ return this.withLock(async () => {
974
+ const dead = (await this.scanOutbox(["dead"])).find(({ entry }) => entry.sequence === sequence);
975
+ return dead === undefined ? false : durableUnlink(dead.path);
976
+ });
977
+ }
978
+ async listOutbox(states = OUTBOX_STATES) {
979
+ await this.initialize();
980
+ for (const state of states) {
981
+ if (!OUTBOX_STATES.includes(state)) {
982
+ throw storeError("INVALID_INPUT", `Unknown outbox state: ${state}`);
983
+ }
984
+ }
985
+ return (await this.scanOutbox(states)).map(({ entry }) => entry);
986
+ }
987
+ cachePath(kind, cacheKey) {
988
+ const key = boundedString(cacheKey, "cacheKey", 1_000);
989
+ return resolve(this.directory, "cache", kind, `${sha256(key)}.${kind === "context" ? "json" : "jws.json"}`);
990
+ }
991
+ async pruneContextSnapshotsLocked() {
992
+ const directory = resolve(this.directory, "cache", "context");
993
+ const located = [];
994
+ for (const name of (await readdir(directory))
995
+ .filter((value) => value.endsWith(".json"))
996
+ .sort()
997
+ .slice(0, MAX_CONTEXT_CACHE_SCAN_FILES)) {
998
+ const path = resolve(directory, name);
999
+ try {
1000
+ const metadata = await regularFileMetadata(path);
1001
+ if (metadata === null) {
1002
+ continue;
1003
+ }
1004
+ const record = contextCacheRecord(await readBoundedJsonFile(path, this.maxSnapshotBytes));
1005
+ if (this.cachePath("context", record.cacheKey) !== path) {
1006
+ continue;
1007
+ }
1008
+ located.push({
1009
+ path,
1010
+ bytes: metadata.size,
1011
+ writtenAt: record.writtenAt,
1012
+ cacheKey: record.cacheKey,
1013
+ });
1014
+ }
1015
+ catch {
1016
+ // Corrupt records remain visible to doctor and are ignored for pruning.
1017
+ }
1018
+ }
1019
+ located.sort((left, right) => Date.parse(right.writtenAt) - Date.parse(left.writtenAt) ||
1020
+ (left.cacheKey < right.cacheKey
1021
+ ? -1
1022
+ : left.cacheKey > right.cacheKey
1023
+ ? 1
1024
+ : 0));
1025
+ let retainedBytes = 0;
1026
+ for (const [index, item] of located.entries()) {
1027
+ retainedBytes += item.bytes;
1028
+ if (index >= DEFAULT_MAX_CONTEXT_CACHE_ITEMS ||
1029
+ retainedBytes > DEFAULT_MAX_CONTEXT_CACHE_BYTES) {
1030
+ await durableUnlink(item.path, true);
1031
+ }
1032
+ }
1033
+ }
1034
+ statePath(stateKey) {
1035
+ const key = boundedString(stateKey, "stateKey", 1_000);
1036
+ return resolve(this.directory, "state", `${sha256(key)}.json`);
1037
+ }
1038
+ async readStateValue(key) {
1039
+ try {
1040
+ const record = objectRecord(await readBoundedJsonFile(this.statePath(key), this.maxRecordBytes));
1041
+ if (record.version !== RELIABILITY_STORE_VERSION ||
1042
+ record.cacheKey !== key ||
1043
+ !validIso(record.writtenAt) ||
1044
+ record.value === undefined) {
1045
+ throw storeError("CORRUPT_RECORD", "Runtime state record is invalid");
1046
+ }
1047
+ return record.value;
1048
+ }
1049
+ catch (error) {
1050
+ if (isErrno(error, "ENOENT")) {
1051
+ return null;
1052
+ }
1053
+ throw error;
1054
+ }
1055
+ }
1056
+ async writeState(stateKey, value, writtenAt = this.now()) {
1057
+ const key = boundedString(stateKey, "stateKey", 1_000);
1058
+ const state = normalizeJson(value, { nodes: 0, seen: new Set() }, 0);
1059
+ await this.withLock(() => atomicWriteJson(this.statePath(key), {
1060
+ version: RELIABILITY_STORE_VERSION,
1061
+ cacheKey: key,
1062
+ writtenAt: dateIso(writtenAt, "writtenAt"),
1063
+ value: state,
1064
+ }, this.maxRecordBytes), this.stateLockName(key));
1065
+ }
1066
+ async readState(stateKey) {
1067
+ const key = boundedString(stateKey, "stateKey", 1_000);
1068
+ await this.initialize();
1069
+ return this.readStateValue(key);
1070
+ }
1071
+ async readStateExisting(stateKey) {
1072
+ const key = boundedString(stateKey, "stateKey", 1_000);
1073
+ await this.assertExistingLayout();
1074
+ return this.readStateValue(key);
1075
+ }
1076
+ async updateState(stateKey, update, writtenAt = this.now(), options = {}) {
1077
+ const key = boundedString(stateKey, "stateKey", 1_000);
1078
+ return this.withLock(async () => {
1079
+ const quarantineCurrent = async () => {
1080
+ await durableMove(this.statePath(key), resolve(this.directory, "quarantine", `state-${sha256(key).slice(0, 16)}-${randomUUID()}.json`));
1081
+ };
1082
+ let current;
1083
+ try {
1084
+ current = await this.readStateValue(key);
1085
+ }
1086
+ catch (error) {
1087
+ if (options.recoverCorrupt !== true ||
1088
+ !(error instanceof ReliabilityStoreError) ||
1089
+ error.code !== "CORRUPT_RECORD") {
1090
+ throw error;
1091
+ }
1092
+ await quarantineCurrent();
1093
+ current = null;
1094
+ }
1095
+ if (current !== null &&
1096
+ options.validateCurrent !== undefined &&
1097
+ !options.validateCurrent(current)) {
1098
+ if (options.recoverCorrupt !== true) {
1099
+ throw storeError("CORRUPT_RECORD", "Runtime state value is invalid");
1100
+ }
1101
+ await quarantineCurrent();
1102
+ current = null;
1103
+ }
1104
+ const state = normalizeJson(update(current), { nodes: 0, seen: new Set() }, 0);
1105
+ await atomicWriteJson(this.statePath(key), {
1106
+ version: RELIABILITY_STORE_VERSION,
1107
+ cacheKey: key,
1108
+ writtenAt: dateIso(writtenAt, "writtenAt"),
1109
+ value: state,
1110
+ }, this.maxRecordBytes);
1111
+ return state;
1112
+ }, this.stateLockName(key));
1113
+ }
1114
+ async deleteState(stateKey) {
1115
+ const key = boundedString(stateKey, "stateKey", 1_000);
1116
+ return this.withLock(() => durableUnlink(this.statePath(key), true), this.stateLockName(key));
1117
+ }
1118
+ async writeContextSnapshot(cacheKey, value, writtenAt = this.now()) {
1119
+ const key = boundedString(cacheKey, "cacheKey", 1_000);
1120
+ const snapshot = normalizeJson(value, { nodes: 0, seen: new Set() }, 0);
1121
+ await this.withLock(async () => {
1122
+ await atomicWriteJson(this.cachePath("context", key), {
1123
+ version: RELIABILITY_STORE_VERSION,
1124
+ cacheKey: key,
1125
+ writtenAt: dateIso(writtenAt, "writtenAt"),
1126
+ value: snapshot,
1127
+ }, this.maxSnapshotBytes);
1128
+ await this.pruneContextSnapshotsLocked();
1129
+ });
1130
+ }
1131
+ async readContextSnapshot(cacheKey) {
1132
+ const key = boundedString(cacheKey, "cacheKey", 1_000);
1133
+ await this.initialize();
1134
+ try {
1135
+ const record = contextCacheRecord(await readBoundedJsonFile(this.cachePath("context", key), this.maxSnapshotBytes));
1136
+ if (record.cacheKey !== key ||
1137
+ this.cachePath("context", record.cacheKey) !==
1138
+ this.cachePath("context", key)) {
1139
+ throw storeError("CORRUPT_RECORD", "Context cache record is invalid");
1140
+ }
1141
+ return record.value;
1142
+ }
1143
+ catch (error) {
1144
+ if (isErrno(error, "ENOENT")) {
1145
+ return null;
1146
+ }
1147
+ throw error;
1148
+ }
1149
+ }
1150
+ async listContextSnapshots(options = {}) {
1151
+ const limit = Math.min(positiveInteger(options.limit, 128, "limit"), DEFAULT_MAX_CONTEXT_CACHE_ITEMS);
1152
+ const maxBytes = Math.min(positiveInteger(options.maxBytes, DEFAULT_MAX_CONTEXT_CACHE_BYTES, "maxBytes"), DEFAULT_MAX_CONTEXT_CACHE_BYTES);
1153
+ await this.initialize();
1154
+ return this.withLock(async () => {
1155
+ const directory = resolve(this.directory, "cache", "context");
1156
+ const names = (await readdir(directory))
1157
+ .filter((name) => name.endsWith(".json"))
1158
+ .sort();
1159
+ const records = [];
1160
+ let bytes = 0;
1161
+ let invalid = 0;
1162
+ let truncated = names.length > MAX_CONTEXT_CACHE_SCAN_FILES;
1163
+ for (const name of names.slice(0, MAX_CONTEXT_CACHE_SCAN_FILES)) {
1164
+ const path = resolve(directory, name);
1165
+ let chargedBytes = 0;
1166
+ try {
1167
+ const metadata = await regularFileMetadata(path);
1168
+ if (metadata === null) {
1169
+ continue;
1170
+ }
1171
+ if (bytes + metadata.size > maxBytes) {
1172
+ truncated = true;
1173
+ continue;
1174
+ }
1175
+ bytes += metadata.size;
1176
+ chargedBytes = metadata.size;
1177
+ const record = contextCacheRecord(await readBoundedJsonFile(path, this.maxSnapshotBytes));
1178
+ if (this.cachePath("context", record.cacheKey) !== path) {
1179
+ throw storeError("CORRUPT_RECORD", "Context cache key does not match its filename");
1180
+ }
1181
+ records.push({
1182
+ cacheKey: record.cacheKey,
1183
+ writtenAt: record.writtenAt,
1184
+ value: record.value,
1185
+ });
1186
+ }
1187
+ catch {
1188
+ bytes -= chargedBytes;
1189
+ invalid += 1;
1190
+ await unlink(path).catch(() => undefined);
1191
+ }
1192
+ }
1193
+ if (invalid > 0) {
1194
+ await fsyncDirectory(directory);
1195
+ }
1196
+ records.sort((left, right) => Date.parse(right.writtenAt) - Date.parse(left.writtenAt) ||
1197
+ (left.cacheKey < right.cacheKey
1198
+ ? -1
1199
+ : left.cacheKey > right.cacheKey
1200
+ ? 1
1201
+ : 0));
1202
+ if (records.length > limit) {
1203
+ truncated = true;
1204
+ }
1205
+ return {
1206
+ records: records.slice(0, limit),
1207
+ invalid,
1208
+ truncated,
1209
+ };
1210
+ });
1211
+ }
1212
+ async writePolicySnapshot(cacheKey, compactJws, writtenAt = this.now()) {
1213
+ const key = boundedString(cacheKey, "cacheKey", 1_000);
1214
+ const jws = boundedString(compactJws, "compactJws", this.maxSnapshotBytes);
1215
+ await this.withLock(() => atomicWriteJson(this.cachePath("policy", key), {
1216
+ version: RELIABILITY_STORE_VERSION,
1217
+ cacheKey: key,
1218
+ writtenAt: dateIso(writtenAt, "writtenAt"),
1219
+ compactJws: jws,
1220
+ }, this.maxSnapshotBytes));
1221
+ }
1222
+ async readPolicySnapshot(cacheKey) {
1223
+ const key = boundedString(cacheKey, "cacheKey", 1_000);
1224
+ await this.initialize();
1225
+ try {
1226
+ const record = objectRecord(await readBoundedJsonFile(this.cachePath("policy", key), this.maxSnapshotBytes));
1227
+ if (record.version !== RELIABILITY_STORE_VERSION ||
1228
+ record.cacheKey !== key ||
1229
+ !validIso(record.writtenAt) ||
1230
+ typeof record.compactJws !== "string") {
1231
+ throw storeError("CORRUPT_RECORD", "Policy cache record is invalid");
1232
+ }
1233
+ return record.compactJws;
1234
+ }
1235
+ catch (error) {
1236
+ if (isErrno(error, "ENOENT")) {
1237
+ return null;
1238
+ }
1239
+ throw error;
1240
+ }
1241
+ }
1242
+ async writePublicTrustKeys(keys, updatedAt = this.now()) {
1243
+ const normalized = keys.map((key) => trustKey(key, this.workspaceId));
1244
+ if (new Set(normalized.map(({ kid }) => kid)).size !== normalized.length) {
1245
+ throw storeError("INVALID_INPUT", "Trust key IDs must be unique");
1246
+ }
1247
+ await this.withLock(() => atomicWriteJson(resolve(this.directory, "trust", "public-keys.json"), {
1248
+ version: RELIABILITY_STORE_VERSION,
1249
+ workspaceId: this.workspaceId,
1250
+ updatedAt: dateIso(updatedAt, "updatedAt"),
1251
+ keys: normalized,
1252
+ }, this.maxRecordBytes));
1253
+ }
1254
+ async readPublicTrustKeysValue() {
1255
+ try {
1256
+ const record = objectRecord(await readBoundedJsonFile(resolve(this.directory, "trust", "public-keys.json"), this.maxRecordBytes));
1257
+ if (record.version !== RELIABILITY_STORE_VERSION ||
1258
+ record.workspaceId !== this.workspaceId ||
1259
+ !validIso(record.updatedAt) ||
1260
+ !Array.isArray(record.keys)) {
1261
+ throw storeError("CORRUPT_RECORD", "Trust key record is invalid");
1262
+ }
1263
+ const keys = record.keys.map((value) => {
1264
+ const key = objectRecord(value);
1265
+ return trustKey({
1266
+ workspaceId: String(key.workspaceId),
1267
+ kid: String(key.kid),
1268
+ kty: key.kty,
1269
+ crv: key.crv,
1270
+ x: String(key.x),
1271
+ ...(key.alg === undefined ? {} : { alg: key.alg }),
1272
+ ...(key.use === undefined ? {} : { use: key.use }),
1273
+ ...(key.notBefore === undefined
1274
+ ? {}
1275
+ : { notBefore: String(key.notBefore) }),
1276
+ ...(key.retiredAt === undefined
1277
+ ? {}
1278
+ : {
1279
+ retiredAt: key.retiredAt === null ? null : String(key.retiredAt),
1280
+ }),
1281
+ }, this.workspaceId);
1282
+ });
1283
+ if (new Set(keys.map(({ kid }) => kid)).size !== keys.length) {
1284
+ throw storeError("CORRUPT_RECORD", "Trust key IDs are duplicated");
1285
+ }
1286
+ return keys;
1287
+ }
1288
+ catch (error) {
1289
+ if (isErrno(error, "ENOENT")) {
1290
+ return [];
1291
+ }
1292
+ throw error;
1293
+ }
1294
+ }
1295
+ async readPublicTrustKeys() {
1296
+ await this.initialize();
1297
+ return this.readPublicTrustKeysValue();
1298
+ }
1299
+ async quarantineLegacy(path) {
1300
+ const destination = resolve(this.directory, "quarantine", `legacy-${Date.now()}-${randomUUID()}.json`);
1301
+ await durableMove(path, destination);
1302
+ }
1303
+ async migrateLegacyQueue(legacyDirectory = resolve(this.home, ".lore", "queue")) {
1304
+ await this.initialize();
1305
+ const report = {
1306
+ migrated: 0,
1307
+ duplicates: 0,
1308
+ quarantined: 0,
1309
+ rejected: 0,
1310
+ };
1311
+ let metadata;
1312
+ try {
1313
+ metadata = await lstat(legacyDirectory);
1314
+ }
1315
+ catch (error) {
1316
+ if (isErrno(error, "ENOENT")) {
1317
+ return report;
1318
+ }
1319
+ throw error;
1320
+ }
1321
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
1322
+ throw storeError("UNSAFE_PATH", "Legacy queue must be a real directory");
1323
+ }
1324
+ const names = (await readdir(legacyDirectory))
1325
+ .filter((name) => name.endsWith(".json"))
1326
+ .sort();
1327
+ for (const name of names) {
1328
+ const path = resolve(legacyDirectory, name);
1329
+ try {
1330
+ await regularFileMetadata(path);
1331
+ }
1332
+ catch (error) {
1333
+ if (error instanceof ReliabilityStoreError &&
1334
+ error.code === "UNSAFE_PATH") {
1335
+ report.rejected += 1;
1336
+ continue;
1337
+ }
1338
+ throw error;
1339
+ }
1340
+ let request;
1341
+ let idempotencyKey;
1342
+ let kind;
1343
+ try {
1344
+ const parsed = objectRecord(await readBoundedJsonFile(path, this.maxRecordBytes));
1345
+ request =
1346
+ parsed.request === undefined ? parsed : objectRecord(parsed.request);
1347
+ if (typeof request.idempotencyKey !== "string") {
1348
+ throw storeError("CORRUPT_RECORD", "Legacy entry has no idempotency key");
1349
+ }
1350
+ idempotencyKey = request.idempotencyKey;
1351
+ kind =
1352
+ typeof parsed.kind === "string"
1353
+ ? parsed.kind
1354
+ : typeof request.connector === "string"
1355
+ ? request.connector
1356
+ : "legacy";
1357
+ }
1358
+ catch {
1359
+ await this.quarantineLegacy(path);
1360
+ report.quarantined += 1;
1361
+ continue;
1362
+ }
1363
+ try {
1364
+ const result = await this.enqueue({
1365
+ kind,
1366
+ idempotencyKey,
1367
+ payload: request,
1368
+ });
1369
+ if (result.created) {
1370
+ report.migrated += 1;
1371
+ }
1372
+ else {
1373
+ report.duplicates += 1;
1374
+ }
1375
+ await durableUnlink(path);
1376
+ }
1377
+ catch (error) {
1378
+ if (error instanceof ReliabilityStoreError &&
1379
+ error.code === "CAPACITY_EXCEEDED") {
1380
+ report.rejected += 1;
1381
+ continue;
1382
+ }
1383
+ if (error instanceof ReliabilityStoreError &&
1384
+ (error.code === "IDEMPOTENCY_CONFLICT" ||
1385
+ error.code === "INVALID_INPUT" ||
1386
+ error.code === "SERIALIZATION_LIMIT")) {
1387
+ await this.quarantineLegacy(path);
1388
+ report.quarantined += 1;
1389
+ continue;
1390
+ }
1391
+ throw error;
1392
+ }
1393
+ }
1394
+ return report;
1395
+ }
1396
+ async inspectContents() {
1397
+ const entries = await this.scanOutbox();
1398
+ await Promise.all(entries.map(({ path }) => privateFileMetadata(path)));
1399
+ const countFiles = async (path) => {
1400
+ let count = 0;
1401
+ for (const name of (await readdir(path)).filter((entry) => entry.endsWith(".json"))) {
1402
+ const itemPath = resolve(path, name);
1403
+ try {
1404
+ await privateFileMetadata(itemPath);
1405
+ }
1406
+ catch (error) {
1407
+ if (isErrno(error, "ENOENT")) {
1408
+ continue;
1409
+ }
1410
+ throw error;
1411
+ }
1412
+ count += 1;
1413
+ }
1414
+ return count;
1415
+ };
1416
+ const countCacheRecords = async (kind) => {
1417
+ const directory = resolve(this.directory, "cache", kind);
1418
+ let count = 0;
1419
+ const writtenAt = [];
1420
+ for (const name of (await readdir(directory)).filter((entry) => entry.endsWith(".json"))) {
1421
+ const itemPath = resolve(directory, name);
1422
+ await privateFileMetadata(itemPath);
1423
+ const record = objectRecord(await readBoundedJsonFile(itemPath, this.maxSnapshotBytes));
1424
+ if (record.version !== RELIABILITY_STORE_VERSION ||
1425
+ typeof record.cacheKey !== "string" ||
1426
+ !validIso(record.writtenAt) ||
1427
+ this.cachePath(kind, record.cacheKey) !== itemPath ||
1428
+ (kind === "context" && record.value === undefined) ||
1429
+ (kind === "policy" && typeof record.compactJws !== "string")) {
1430
+ throw storeError("CORRUPT_RECORD", `${kind} cache record is invalid`);
1431
+ }
1432
+ count += 1;
1433
+ writtenAt.push(record.writtenAt);
1434
+ }
1435
+ writtenAt.sort();
1436
+ return {
1437
+ count,
1438
+ oldestWrittenAt: writtenAt[0] ?? null,
1439
+ newestWrittenAt: writtenAt.at(-1) ?? null,
1440
+ };
1441
+ };
1442
+ const oldest = entries
1443
+ .map(({ entry }) => entry.enqueuedAt)
1444
+ .sort()[0] ?? null;
1445
+ const counts = (state) => entries.filter(({ entry }) => entry.state === state).length;
1446
+ await Promise.all([
1447
+ countFiles(resolve(this.directory, "state")),
1448
+ countFiles(resolve(this.directory, "trust")),
1449
+ ]);
1450
+ const [contextCache, policyCache] = await Promise.all([
1451
+ countCacheRecords("context"),
1452
+ countCacheRecords("policy"),
1453
+ ]);
1454
+ const operational = localOperationalMetricsInspection(await this.readStateValue(LOCAL_OPERATIONAL_METRICS_STATE_KEY));
1455
+ return {
1456
+ workspaceId: this.workspaceId,
1457
+ directory: this.directory,
1458
+ outbox: {
1459
+ ready: counts("ready"),
1460
+ inFlight: counts("in-flight"),
1461
+ authBlocked: counts("auth-blocked"),
1462
+ dead: counts("dead"),
1463
+ total: entries.length,
1464
+ bytes: entries.reduce((sum, entry) => sum + entry.bytes, 0),
1465
+ oldestEnqueuedAt: oldest,
1466
+ },
1467
+ cache: {
1468
+ context: contextCache.count,
1469
+ policy: policyCache.count,
1470
+ oldestContextWrittenAt: contextCache.oldestWrittenAt,
1471
+ newestContextWrittenAt: contextCache.newestWrittenAt,
1472
+ },
1473
+ trustKeys: (await this.readPublicTrustKeysValue()).length,
1474
+ quarantined: await countFiles(resolve(this.directory, "quarantine")),
1475
+ operational,
1476
+ };
1477
+ }
1478
+ async inspect() {
1479
+ return this.withLock(() => this.inspectContents());
1480
+ }
1481
+ async inspectExisting() {
1482
+ await this.assertExistingLayout();
1483
+ return this.inspectContents();
1484
+ }
1485
+ }
1486
+ const LOCAL_OPERATIONAL_METRICS_STATE_KEY = "operational-metrics-v1";
1487
+ function metricInteger(value) {
1488
+ return (typeof value === "number" &&
1489
+ Number.isSafeInteger(value) &&
1490
+ value >= 0);
1491
+ }
1492
+ function localOperationalMetricsState(value) {
1493
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1494
+ return null;
1495
+ }
1496
+ const retrieval = value.retrieval;
1497
+ const guard = value.guard;
1498
+ if (value.version !== 1 ||
1499
+ typeof value.since !== "string" ||
1500
+ !validIso(value.since) ||
1501
+ typeof value.updatedAt !== "string" ||
1502
+ !validIso(value.updatedAt) ||
1503
+ retrieval === null ||
1504
+ typeof retrieval !== "object" ||
1505
+ Array.isArray(retrieval) ||
1506
+ guard === null ||
1507
+ typeof guard !== "object" ||
1508
+ Array.isArray(guard)) {
1509
+ return null;
1510
+ }
1511
+ const retrievalValues = [
1512
+ retrieval.attempts,
1513
+ retrieval.livePrimary,
1514
+ retrieval.liveLexicalFallback,
1515
+ retrieval.cachedFallback,
1516
+ retrieval.failed,
1517
+ retrieval.cachedAgeSamples,
1518
+ retrieval.cachedAgeTotalMs,
1519
+ retrieval.maxCachedAgeMs,
1520
+ ];
1521
+ const guardValues = [
1522
+ guard.eligibleActions,
1523
+ guard.liveChecks,
1524
+ guard.reusedDecisions,
1525
+ guard.failures,
1526
+ guard.latencySamples,
1527
+ guard.latencyTotalMs,
1528
+ guard.maxLatencyMs,
1529
+ ];
1530
+ if (!retrievalValues.every(metricInteger) ||
1531
+ !guardValues.every(metricInteger)) {
1532
+ return null;
1533
+ }
1534
+ return value;
1535
+ }
1536
+ function emptyLocalOperationalMetrics(now) {
1537
+ const timestamp = now.toISOString();
1538
+ return {
1539
+ version: 1,
1540
+ since: timestamp,
1541
+ updatedAt: timestamp,
1542
+ retrieval: {
1543
+ attempts: 0,
1544
+ livePrimary: 0,
1545
+ liveLexicalFallback: 0,
1546
+ cachedFallback: 0,
1547
+ failed: 0,
1548
+ cachedAgeSamples: 0,
1549
+ cachedAgeTotalMs: 0,
1550
+ maxCachedAgeMs: 0,
1551
+ },
1552
+ guard: {
1553
+ eligibleActions: 0,
1554
+ liveChecks: 0,
1555
+ reusedDecisions: 0,
1556
+ failures: 0,
1557
+ latencySamples: 0,
1558
+ latencyTotalMs: 0,
1559
+ maxLatencyMs: 0,
1560
+ },
1561
+ };
1562
+ }
1563
+ function incrementMetric(value) {
1564
+ return Math.min(Number.MAX_SAFE_INTEGER, value + 1);
1565
+ }
1566
+ function addMetric(value, addition) {
1567
+ return Math.min(Number.MAX_SAFE_INTEGER, value + addition);
1568
+ }
1569
+ function localOperationalMetricsInspection(value) {
1570
+ const state = localOperationalMetricsState(value);
1571
+ if (state === null) {
1572
+ return {
1573
+ since: null,
1574
+ updatedAt: null,
1575
+ retrieval: {
1576
+ attempts: 0,
1577
+ livePrimary: 0,
1578
+ liveLexicalFallback: 0,
1579
+ cachedFallback: 0,
1580
+ failed: 0,
1581
+ fallbackRate: null,
1582
+ averageCachedAgeMs: null,
1583
+ maxCachedAgeMs: null,
1584
+ },
1585
+ guard: {
1586
+ eligibleActions: 0,
1587
+ liveChecks: 0,
1588
+ reusedDecisions: 0,
1589
+ failures: 0,
1590
+ coverageRate: null,
1591
+ averageLatencyMs: null,
1592
+ maxLatencyMs: null,
1593
+ },
1594
+ };
1595
+ }
1596
+ const fallbackCount = state.retrieval.liveLexicalFallback + state.retrieval.cachedFallback;
1597
+ const protectedActions = state.guard.liveChecks + state.guard.reusedDecisions;
1598
+ return {
1599
+ since: state.since,
1600
+ updatedAt: state.updatedAt,
1601
+ retrieval: {
1602
+ attempts: state.retrieval.attempts,
1603
+ livePrimary: state.retrieval.livePrimary,
1604
+ liveLexicalFallback: state.retrieval.liveLexicalFallback,
1605
+ cachedFallback: state.retrieval.cachedFallback,
1606
+ failed: state.retrieval.failed,
1607
+ fallbackRate: state.retrieval.attempts === 0
1608
+ ? null
1609
+ : fallbackCount / state.retrieval.attempts,
1610
+ averageCachedAgeMs: state.retrieval.cachedAgeSamples === 0
1611
+ ? null
1612
+ : state.retrieval.cachedAgeTotalMs /
1613
+ state.retrieval.cachedAgeSamples,
1614
+ maxCachedAgeMs: state.retrieval.cachedAgeSamples === 0
1615
+ ? null
1616
+ : state.retrieval.maxCachedAgeMs,
1617
+ },
1618
+ guard: {
1619
+ eligibleActions: state.guard.eligibleActions,
1620
+ liveChecks: state.guard.liveChecks,
1621
+ reusedDecisions: state.guard.reusedDecisions,
1622
+ failures: state.guard.failures,
1623
+ coverageRate: state.guard.eligibleActions === 0
1624
+ ? null
1625
+ : protectedActions / state.guard.eligibleActions,
1626
+ averageLatencyMs: state.guard.latencySamples === 0
1627
+ ? null
1628
+ : state.guard.latencyTotalMs / state.guard.latencySamples,
1629
+ maxLatencyMs: state.guard.latencySamples === 0
1630
+ ? null
1631
+ : state.guard.maxLatencyMs,
1632
+ },
1633
+ };
1634
+ }
1635
+ export async function recordLocalRetrievalMetric(store, input, now = new Date()) {
1636
+ await store.updateState(LOCAL_OPERATIONAL_METRICS_STATE_KEY, (current) => {
1637
+ const state = localOperationalMetricsState(current) ??
1638
+ emptyLocalOperationalMetrics(now);
1639
+ state.updatedAt = now.toISOString();
1640
+ state.retrieval.attempts = incrementMetric(state.retrieval.attempts);
1641
+ switch (input.outcome) {
1642
+ case "live_primary":
1643
+ state.retrieval.livePrimary = incrementMetric(state.retrieval.livePrimary);
1644
+ break;
1645
+ case "live_lexical_fallback":
1646
+ state.retrieval.liveLexicalFallback = incrementMetric(state.retrieval.liveLexicalFallback);
1647
+ break;
1648
+ case "cached_fallback": {
1649
+ state.retrieval.cachedFallback = incrementMetric(state.retrieval.cachedFallback);
1650
+ const ageMs = Math.max(0, Math.trunc(input.cacheAgeMs ?? 0));
1651
+ state.retrieval.cachedAgeSamples = incrementMetric(state.retrieval.cachedAgeSamples);
1652
+ state.retrieval.cachedAgeTotalMs = addMetric(state.retrieval.cachedAgeTotalMs, ageMs);
1653
+ state.retrieval.maxCachedAgeMs = Math.max(state.retrieval.maxCachedAgeMs, ageMs);
1654
+ break;
1655
+ }
1656
+ case "failed":
1657
+ state.retrieval.failed = incrementMetric(state.retrieval.failed);
1658
+ break;
1659
+ }
1660
+ return state;
1661
+ }, now, {
1662
+ recoverCorrupt: true,
1663
+ validateCurrent: (current) => localOperationalMetricsState(current) !== null,
1664
+ });
1665
+ }
1666
+ export async function recordLocalGuardMetric(store, input, now = new Date()) {
1667
+ await store.updateState(LOCAL_OPERATIONAL_METRICS_STATE_KEY, (current) => {
1668
+ const state = localOperationalMetricsState(current) ??
1669
+ emptyLocalOperationalMetrics(now);
1670
+ state.updatedAt = now.toISOString();
1671
+ state.guard.eligibleActions = incrementMetric(state.guard.eligibleActions);
1672
+ if (input.outcome === "reused_decision") {
1673
+ state.guard.reusedDecisions = incrementMetric(state.guard.reusedDecisions);
1674
+ }
1675
+ else if (input.outcome === "failed") {
1676
+ state.guard.failures = incrementMetric(state.guard.failures);
1677
+ }
1678
+ else {
1679
+ state.guard.liveChecks = incrementMetric(state.guard.liveChecks);
1680
+ const durationMs = Math.max(0, Math.trunc(input.durationMs ?? 0));
1681
+ state.guard.latencySamples = incrementMetric(state.guard.latencySamples);
1682
+ state.guard.latencyTotalMs = addMetric(state.guard.latencyTotalMs, durationMs);
1683
+ state.guard.maxLatencyMs = Math.max(state.guard.maxLatencyMs, durationMs);
1684
+ }
1685
+ return state;
1686
+ }, now, {
1687
+ recoverCorrupt: true,
1688
+ validateCurrent: (current) => localOperationalMetricsState(current) !== null,
1689
+ });
1690
+ }
1691
+ function normalizedIntegrationId(value) {
1692
+ const integrationId = boundedString(value, "integrationId", 100);
1693
+ if (!/^[a-z0-9](?:[a-z0-9._/-]*[a-z0-9])?$/u.test(integrationId)) {
1694
+ throw storeError("INVALID_INPUT", "integrationId is invalid");
1695
+ }
1696
+ return integrationId;
1697
+ }
1698
+ export function integrationInvocationStateKey(integrationId) {
1699
+ return `integration-invocation-v1:${normalizedIntegrationId(integrationId)}`;
1700
+ }
1701
+ function invocationRecord(value, integrationId) {
1702
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1703
+ return null;
1704
+ }
1705
+ if (Object.keys(value).sort().join(",") !==
1706
+ "integrationId,lastAttempt,lastCompletion,version" ||
1707
+ value.version !== 1 ||
1708
+ value.integrationId !== integrationId) {
1709
+ return null;
1710
+ }
1711
+ const attempt = value.lastAttempt;
1712
+ const completion = value.lastCompletion;
1713
+ const validAttempt = attempt === null ||
1714
+ (typeof attempt === "object" &&
1715
+ !Array.isArray(attempt) &&
1716
+ Object.keys(attempt).sort().join(",") === "at,id,runtimeVersion" &&
1717
+ typeof attempt.id === "string" &&
1718
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(attempt.id) &&
1719
+ typeof attempt.at === "string" &&
1720
+ validIso(attempt.at) &&
1721
+ (attempt.runtimeVersion === null ||
1722
+ (typeof attempt.runtimeVersion === "string" &&
1723
+ attempt.runtimeVersion.trim() !== "" &&
1724
+ attempt.runtimeVersion.length <= 100)));
1725
+ const validCompletion = completion === null ||
1726
+ (typeof completion === "object" &&
1727
+ !Array.isArray(completion) &&
1728
+ Object.keys(completion).sort().join(",") ===
1729
+ "at,attemptId,failureCode,outcome" &&
1730
+ typeof completion.attemptId === "string" &&
1731
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(completion.attemptId) &&
1732
+ typeof completion.at === "string" &&
1733
+ validIso(completion.at) &&
1734
+ ((completion.outcome === "success" &&
1735
+ completion.failureCode === null) ||
1736
+ (completion.outcome === "failure" &&
1737
+ (completion.failureCode === "invalid_input" ||
1738
+ completion.failureCode === "runtime_error"))));
1739
+ if (!validAttempt ||
1740
+ !validCompletion ||
1741
+ (attempt === null && completion !== null)) {
1742
+ return null;
1743
+ }
1744
+ if (attempt !== null &&
1745
+ completion !== null &&
1746
+ typeof completion.attemptId === "string" &&
1747
+ typeof completion.at === "string" &&
1748
+ typeof attempt.id === "string" &&
1749
+ typeof attempt.at === "string" &&
1750
+ (completion.attemptId === attempt.id
1751
+ ? Date.parse(completion.at) < Date.parse(attempt.at)
1752
+ : Date.parse(completion.at) > Date.parse(attempt.at))) {
1753
+ return null;
1754
+ }
1755
+ return value;
1756
+ }
1757
+ function latestInvocationAttempt(record) {
1758
+ const value = record?.lastAttempt;
1759
+ if (value === null ||
1760
+ value === undefined ||
1761
+ typeof value !== "object" ||
1762
+ Array.isArray(value) ||
1763
+ typeof value.id !== "string" ||
1764
+ typeof value.at !== "string") {
1765
+ return null;
1766
+ }
1767
+ return { id: value.id, at: value.at };
1768
+ }
1769
+ export function createIntegrationInvocationAttempt(integrationId, options = {}) {
1770
+ const normalizedId = normalizedIntegrationId(integrationId);
1771
+ const attemptId = randomUUID();
1772
+ const attemptedAt = dateIso(options.now ?? new Date(), "now");
1773
+ const runtimeVersion = options.runtimeVersion === undefined || options.runtimeVersion === null
1774
+ ? null
1775
+ : boundedString(options.runtimeVersion, "runtimeVersion", 100);
1776
+ return {
1777
+ id: attemptId,
1778
+ integrationId: normalizedId,
1779
+ attemptedAt,
1780
+ runtimeVersion,
1781
+ };
1782
+ }
1783
+ export async function recordIntegrationInvocationAttempt(store, attempt) {
1784
+ const normalizedId = normalizedIntegrationId(attempt.integrationId);
1785
+ const attemptId = boundedString(attempt.id, "attemptId", 100);
1786
+ const attemptedAt = dateIso(new Date(attempt.attemptedAt), "attemptedAt");
1787
+ const runtimeVersion = attempt.runtimeVersion === null
1788
+ ? null
1789
+ : boundedString(attempt.runtimeVersion, "runtimeVersion", 100);
1790
+ await store.updateState(integrationInvocationStateKey(normalizedId), (current) => {
1791
+ const previous = invocationRecord(current, normalizedId);
1792
+ const previousAttempt = latestInvocationAttempt(previous);
1793
+ if (previousAttempt !== null) {
1794
+ const chronology = Date.parse(previousAttempt.at) - Date.parse(attemptedAt);
1795
+ if (chronology > 0 ||
1796
+ (chronology === 0 && previousAttempt.id > attemptId) ||
1797
+ previousAttempt.id === attemptId) {
1798
+ return current;
1799
+ }
1800
+ }
1801
+ return {
1802
+ version: 1,
1803
+ integrationId: normalizedId,
1804
+ lastAttempt: {
1805
+ id: attemptId,
1806
+ at: attemptedAt,
1807
+ runtimeVersion,
1808
+ },
1809
+ lastCompletion: previous?.lastCompletion ?? null,
1810
+ };
1811
+ }, new Date(attemptedAt), {
1812
+ recoverCorrupt: true,
1813
+ validateCurrent: (current) => invocationRecord(current, normalizedId) !== null,
1814
+ });
1815
+ }
1816
+ export async function beginIntegrationInvocation(store, integrationId, options = {}) {
1817
+ const attempt = createIntegrationInvocationAttempt(integrationId, options);
1818
+ await recordIntegrationInvocationAttempt(store, attempt);
1819
+ return attempt;
1820
+ }
1821
+ export async function recordIntegrationInvocationResult(store, attempt, outcome, now = new Date()) {
1822
+ const normalizedId = normalizedIntegrationId(attempt.integrationId);
1823
+ const attemptId = boundedString(attempt.id, "attemptId", 100);
1824
+ const attemptedAt = dateIso(new Date(attempt.attemptedAt), "attemptedAt");
1825
+ const runtimeVersion = attempt.runtimeVersion === null
1826
+ ? null
1827
+ : boundedString(attempt.runtimeVersion, "runtimeVersion", 100);
1828
+ const completedAt = dateIso(now, "now");
1829
+ if (Date.parse(completedAt) < Date.parse(attemptedAt)) {
1830
+ throw storeError("INVALID_INPUT", "Invocation completion cannot precede its attempt");
1831
+ }
1832
+ if (!outcome.success &&
1833
+ outcome.failureCode !== "invalid_input" &&
1834
+ outcome.failureCode !== "runtime_error") {
1835
+ throw storeError("INVALID_INPUT", "Invocation failure code is invalid");
1836
+ }
1837
+ await store.updateState(integrationInvocationStateKey(normalizedId), (current) => {
1838
+ const previous = invocationRecord(current, normalizedId);
1839
+ const previousAttempt = latestInvocationAttempt(previous);
1840
+ if (previousAttempt !== null) {
1841
+ const chronology = Date.parse(previousAttempt.at) - Date.parse(attemptedAt);
1842
+ if (chronology > 0 ||
1843
+ (chronology === 0 &&
1844
+ previousAttempt.id !== attemptId &&
1845
+ previousAttempt.id > attemptId)) {
1846
+ return current;
1847
+ }
1848
+ }
1849
+ return {
1850
+ version: 1,
1851
+ integrationId: normalizedId,
1852
+ lastAttempt: {
1853
+ id: attemptId,
1854
+ at: attemptedAt,
1855
+ runtimeVersion,
1856
+ },
1857
+ lastCompletion: outcome.success
1858
+ ? {
1859
+ attemptId,
1860
+ at: completedAt,
1861
+ outcome: "success",
1862
+ failureCode: null,
1863
+ }
1864
+ : {
1865
+ attemptId,
1866
+ at: completedAt,
1867
+ outcome: "failure",
1868
+ failureCode: outcome.failureCode,
1869
+ },
1870
+ };
1871
+ }, new Date(completedAt), {
1872
+ recoverCorrupt: true,
1873
+ validateCurrent: (current) => invocationRecord(current, normalizedId) !== null,
1874
+ });
1875
+ }
1876
+ export async function completeIntegrationInvocation(store, attempt, outcome, now = new Date()) {
1877
+ const normalizedId = normalizedIntegrationId(attempt.integrationId);
1878
+ const attemptId = boundedString(attempt.id, "attemptId", 100);
1879
+ const completedAt = dateIso(now, "now");
1880
+ await store.updateState(integrationInvocationStateKey(normalizedId), (current) => {
1881
+ const record = invocationRecord(current, normalizedId);
1882
+ const lastAttempt = record?.lastAttempt !== null &&
1883
+ typeof record?.lastAttempt === "object" &&
1884
+ !Array.isArray(record.lastAttempt)
1885
+ ? record.lastAttempt
1886
+ : null;
1887
+ if (record === null || lastAttempt?.id !== attemptId) {
1888
+ return current ?? {
1889
+ version: 1,
1890
+ integrationId: normalizedId,
1891
+ lastAttempt: null,
1892
+ lastCompletion: null,
1893
+ };
1894
+ }
1895
+ return {
1896
+ ...record,
1897
+ lastCompletion: outcome.success
1898
+ ? {
1899
+ attemptId,
1900
+ at: completedAt,
1901
+ outcome: "success",
1902
+ failureCode: null,
1903
+ }
1904
+ : {
1905
+ attemptId,
1906
+ at: completedAt,
1907
+ outcome: "failure",
1908
+ failureCode: outcome.failureCode,
1909
+ },
1910
+ };
1911
+ }, new Date(completedAt));
1912
+ }
1913
+ //# sourceMappingURL=reliability-store.js.map