llm-relay 0.68.8 → 0.69.0

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 (47) hide show
  1. package/dist/accounting-store.d.ts +18 -1
  2. package/dist/accounting-store.js +130 -224
  3. package/dist/accounting-store.js.map +1 -1
  4. package/dist/backend.d.ts +11 -0
  5. package/dist/backend.js +7 -1
  6. package/dist/backend.js.map +1 -1
  7. package/dist/candidate-runner.d.ts +2 -2
  8. package/dist/candidate-runner.js +47 -21
  9. package/dist/candidate-runner.js.map +1 -1
  10. package/dist/cli.d.ts +15 -0
  11. package/dist/cli.js +77 -47
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.d.ts +2 -0
  14. package/dist/config.js +4 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/dispatch.d.ts +15 -0
  17. package/dist/dispatch.js +20 -0
  18. package/dist/dispatch.js.map +1 -1
  19. package/dist/dynamic-pools.d.ts +1 -1
  20. package/dist/dynamic-pools.js +2 -2
  21. package/dist/dynamic-pools.js.map +1 -1
  22. package/dist/mcp/lane-runner.d.ts +102 -2
  23. package/dist/mcp/lane-runner.js +49 -6
  24. package/dist/mcp/lane-runner.js.map +1 -1
  25. package/dist/mcp/server.d.ts +37 -1
  26. package/dist/mcp/server.js +183 -14
  27. package/dist/mcp/server.js.map +1 -1
  28. package/dist/routes/admin.js +2 -1
  29. package/dist/routes/admin.js.map +1 -1
  30. package/dist/routes/messages.js +5 -23
  31. package/dist/routes/messages.js.map +1 -1
  32. package/dist/routes/openai-front.js +5 -23
  33. package/dist/routes/openai-front.js.map +1 -1
  34. package/dist/server.js +12 -4
  35. package/dist/server.js.map +1 -1
  36. package/dist/setup-claude.d.ts +22 -0
  37. package/dist/setup-claude.js +91 -5
  38. package/dist/setup-claude.js.map +1 -1
  39. package/dist/target-facts.d.ts +10 -0
  40. package/dist/target-facts.js +11 -0
  41. package/dist/target-facts.js.map +1 -1
  42. package/package.json +1 -1
  43. package/skills/llm-relay/SKILL.md +13 -0
  44. package/skills/llm-relay/references/dispatch-lanes.md +26 -0
  45. package/dist/accounting-store-io.d.ts +0 -132
  46. package/dist/accounting-store-io.js +0 -1087
  47. package/dist/accounting-store-io.js.map +0 -1
@@ -9,7 +9,24 @@
9
9
  * - Read-only queries (`readOnly: true`) never mutate or create writer leases.
10
10
  */
11
11
  import { type AccountingRecorder } from "./accounting.js";
12
- import { type SnapshotJournalHooks, type SnapshotMutationResult, type SnapshotWriterResult } from "./accounting-store-io.js";
12
+ export interface SnapshotMutationResult {
13
+ readonly status: "none" | "committed" | "recovered" | "recovery-loss" | "invalid" | "failed";
14
+ readonly transactionId: string | null;
15
+ readonly lowerBoundLoss: boolean;
16
+ readonly error: string | null;
17
+ readonly quarantinedPath: string | null;
18
+ readonly retryable: boolean;
19
+ }
20
+ export interface SnapshotWriterResult {
21
+ readonly status: "acquired" | "released" | "closed" | "failed";
22
+ readonly error: string | null;
23
+ readonly retryable: boolean;
24
+ }
25
+ export interface SnapshotJournalHooks {
26
+ readonly beforeRead?: (path: string) => void;
27
+ readonly beforeStep?: (step: unknown) => void;
28
+ readonly afterStep?: (step: unknown) => void;
29
+ }
13
30
  import { type AccountingDayShard, type AccountingLifetime, type AccountingRequestPacket } from "./accounting-store-schema.js";
14
31
  export { ACCOUNTING_DAY_SCHEMA, ACCOUNTING_DEDUP_SCHEMA, ACCOUNTING_LIFETIME_SCHEMA, ACCOUNTING_MAX_DAY_ROWS, ACCOUNTING_MAX_DEDUP_IDS, ACCOUNTING_MAX_DETAIL_ATTEMPTS, ACCOUNTING_MAX_FILE_BYTES, ACCOUNTING_MAX_LOSS_MARKERS, ACCOUNTING_MAX_MONTHS, ACCOUNTING_MAX_PACKET_ATTEMPTS, ACCOUNTING_MAX_RECENT_ROWS, ACCOUNTING_MAX_ROWS_PER_CELL, ACCOUNTING_MAX_SAMPLES, ACCOUNTING_MINUTE_SCHEMA, ACCOUNTING_RECENT_SCHEMA, ACCOUNTING_STORE_VERSION, } from "./accounting-store-schema.js";
15
32
  export type { AccountingAggregateTokenCellV1 as AggregateTokenCell, AccountingAggregateTokenTotalsV1 as AggregateTokenTotals, AccountingAggregateV1 as AccountingAggregate, AccountingAttemptPacketV1 as AccountingAttemptPacket, AccountingCoverageV1 as AccountingCoverage, AccountingDayShard, AccountingDimensionRowV1 as AccountingAggregateRow, AccountingLifetime, AccountingMetricCellV1 as AggregateMetric, AccountingMinuteShardV1 as AccountingMinuteCell, AccountingRequestPacket, } from "./accounting-store-schema.js";
@@ -3,8 +3,9 @@ import { randomBytes } from "node:crypto";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { ACCOUNTING_UNSPECIFIED_ESTIMATION_METHOD, } from "./accounting.js";
6
- import { createSnapshotJournalIo, SNAPSHOT_IO_HARD_MAX_JOURNAL_BYTES, } from "./accounting-store-io.js";
7
- import { emptyAccountingSpendCell, mergeAccountingSpendCells, ACCOUNTING_DAY_SCHEMA, ACCOUNTING_DEDUP_SCHEMA, ACCOUNTING_LIFETIME_SCHEMA, ACCOUNTING_MAX_DAY_ROWS, ACCOUNTING_MAX_DEDUP_IDS, ACCOUNTING_MAX_DETAIL_ATTEMPTS, ACCOUNTING_MAX_FILE_BYTES, ACCOUNTING_MAX_LOSS_MARKERS, ACCOUNTING_MAX_MONTHS, ACCOUNTING_MAX_PACKET_ATTEMPTS, ACCOUNTING_MAX_RECENT_ROWS, ACCOUNTING_MAX_ROWS_PER_CELL, ACCOUNTING_MAX_SAMPLES, ACCOUNTING_MINUTE_SCHEMA, ACCOUNTING_RECENT_SCHEMA, ACCOUNTING_STORE_VERSION, freezeDeep, parseAccountingAttemptPacketV1, parseAccountingDayShardV1, parseAccountingLifetimeV1, parseAccountingRecentV1, parseAccountingRequestPacketV1, isLoadableId, } from "./accounting-store-schema.js";
6
+ import { readFileSync, unlinkSync } from "node:fs";
7
+ import { atomicWriteJsonSync } from "./storage/json-store.js";
8
+ import { emptyAccountingSpendCell, mergeAccountingSpendCells, ACCOUNTING_DAY_SCHEMA, ACCOUNTING_DEDUP_SCHEMA, ACCOUNTING_LIFETIME_SCHEMA, ACCOUNTING_MAX_DAY_ROWS, ACCOUNTING_MAX_DEDUP_IDS, ACCOUNTING_MAX_DETAIL_ATTEMPTS, ACCOUNTING_MAX_LOSS_MARKERS, ACCOUNTING_MAX_MONTHS, ACCOUNTING_MAX_PACKET_ATTEMPTS, ACCOUNTING_MAX_RECENT_ROWS, ACCOUNTING_MAX_ROWS_PER_CELL, ACCOUNTING_MAX_SAMPLES, ACCOUNTING_MINUTE_SCHEMA, ACCOUNTING_RECENT_SCHEMA, ACCOUNTING_STORE_VERSION, freezeDeep, parseAccountingAttemptPacketV1, parseAccountingDayShardV1, parseAccountingLifetimeV1, parseAccountingRecentV1, parseAccountingRequestPacketV1, isLoadableId, } from "./accounting-store-schema.js";
8
9
  import { DASHBOARD_REQUEST_ID_PATTERN, isDashboardAttemptId, isDashboardFailureKind, isDashboardOutcome, isDashboardSafeId, isDashboardUtcTimestamp, } from "./dashboard-contract.js";
9
10
  import { DEFAULT_FLUSH_DELAY_MS, MAX_FLUSH_DELAY_MS, WriteBehindTimer } from "./write-behind.js";
10
11
  export { ACCOUNTING_DAY_SCHEMA, ACCOUNTING_DEDUP_SCHEMA, ACCOUNTING_LIFETIME_SCHEMA, ACCOUNTING_MAX_DAY_ROWS, ACCOUNTING_MAX_DEDUP_IDS, ACCOUNTING_MAX_DETAIL_ATTEMPTS, ACCOUNTING_MAX_FILE_BYTES, ACCOUNTING_MAX_LOSS_MARKERS, ACCOUNTING_MAX_MONTHS, ACCOUNTING_MAX_PACKET_ATTEMPTS, ACCOUNTING_MAX_RECENT_ROWS, ACCOUNTING_MAX_ROWS_PER_CELL, ACCOUNTING_MAX_SAMPLES, ACCOUNTING_MINUTE_SCHEMA, ACCOUNTING_RECENT_SCHEMA, ACCOUNTING_STORE_VERSION, } from "./accounting-store-schema.js";
@@ -481,7 +482,6 @@ function parsedLifetime(value) { const parsed = parseAccountingLifetimeV1(value)
481
482
  function parsedRecent(value) { const parsed = parseAccountingRecentV1(value); return parsed.ok ? parsed.value : null; }
482
483
  class AccountingStoreImpl {
483
484
  directory;
484
- io;
485
485
  timer = new WriteBehindTimer();
486
486
  retentionDays;
487
487
  recentLimit;
@@ -493,6 +493,7 @@ class AccountingStoreImpl {
493
493
  pendingAttemptLimit;
494
494
  now;
495
495
  readOnly;
496
+ ioHooks;
496
497
  pending = new Map();
497
498
  queued = new Array();
498
499
  days = new Map();
@@ -504,10 +505,6 @@ class AccountingStoreImpl {
504
505
  recentReadState = "missing";
505
506
  dirtyLifetime = false;
506
507
  dirtyRecent = false;
507
- fixedLoadPending = false;
508
- pendingFixedLoss = false;
509
- pendingJournalLoss = false;
510
- deferredGlobalLosses = [];
511
508
  pendingRetention = null;
512
509
  pendingRetentionPlan = null;
513
510
  retryTimer = null;
@@ -529,28 +526,15 @@ class AccountingStoreImpl {
529
526
  this.pendingAttemptLimit = positiveLimit(options.pendingAttemptLimit, ACCOUNTING_MAX_PENDING_ATTEMPTS_PER_REQUEST, ACCOUNTING_MAX_PENDING_ATTEMPTS_PER_REQUEST);
530
527
  this.now = options.now ?? (() => Date.now());
531
528
  this.readOnly = options.readOnly === true;
532
- this.io = createSnapshotJournalIo({
533
- rootDir: this.directory,
534
- targets: [LIFETIME_TARGET, RECENT_TARGET],
535
- acceptTarget: dayTarget,
536
- maxFileBytes: ACCOUNTING_MAX_FILE_BYTES,
537
- maxJournalBytes: SNAPSHOT_IO_HARD_MAX_JOURNAL_BYTES,
538
- maxTargets: 128,
539
- ...(options.ioHooks === undefined ? {} : { hooks: options.ioHooks }),
540
- });
529
+ this.ioHooks = options.ioHooks;
541
530
  if (this.readOnly) {
542
531
  this.writer = { status: "released", error: null, retryable: false };
543
532
  this.last = result("none", null, false, false);
544
533
  this.loadFixedSnapshotsReadOnly();
545
534
  return;
546
535
  }
547
- this.writer = this.io.acquireWriter();
548
- if (this.writer.status === "acquired")
549
- this.recoverAndLoad();
550
- else {
551
- this.last = result("failed", this.writer.error ?? "writer-busy", this.writer.retryable);
552
- this.markGlobalLoss("unknown", "truncated", "writer");
553
- }
536
+ this.writer = { status: "acquired", error: null, retryable: false };
537
+ this.loadFixedSnapshots();
554
538
  }
555
539
  get closed() { return this._closed; }
556
540
  get writerStatus() { return this.writer; }
@@ -590,48 +574,74 @@ class AccountingStoreImpl {
590
574
  return result("none", null, false, false);
591
575
  this.timer.clear();
592
576
  this.clearRetry();
593
- if (this.writer.status !== "acquired")
577
+ if (this.writer.status !== "acquired") {
594
578
  return this.setLast(result("failed", this.writer.error ?? "writer-busy", this.writer.retryable));
595
- const recovered = this.io.recover();
596
- if (recovered.status === "failed" || recovered.status === "invalid")
597
- return this.fail(recovered);
598
- if (this.fixedLoadPending) {
599
- this.recoverAndLoad();
600
- if (this.fixedLoadPending)
601
- return this.last ?? result("failed", "snapshot-read", true, this.lowerBoundLoss);
602
- }
603
- if (recovered.lowerBoundLoss || recovered.status === "recovery-loss")
604
- this.consumeRecoveryLoss(recovered);
605
- if (recovered.status === "recovered" && this.pendingRetentionPlan !== null) {
606
- this.reconcileRecoveredRetention(this.pendingRetentionPlan);
607
579
  }
608
580
  this.drainQueued();
609
- if (this.queued.length > 0)
581
+ if (this.queued.length > 0) {
610
582
  return this.fail(result("failed", "day-read", true, this.lowerBoundLoss));
583
+ }
611
584
  if (this.dirtyDays.size === 0 && !this.dirtyLifetime && !this.dirtyRecent) {
612
- return this.commitRetention([]) ?? this.setLast(result("none", null, false, this.lowerBoundLoss));
585
+ const retentionResult = this.commitRetention([]);
586
+ return retentionResult ?? this.setLast(result("none", null, false, this.lowerBoundLoss));
587
+ }
588
+ const daySnapshots = [];
589
+ for (const date of [...this.dirtyDays].sort()) {
590
+ const day = this.days.get(date);
591
+ const parsed = day === undefined ? null : parsedDay(day);
592
+ if (parsed === null)
593
+ return this.fail(result("invalid", "schema-or-size", false, this.lowerBoundLoss));
594
+ daySnapshots.push({ date, data: parsed });
595
+ }
596
+ let lifetimeSnapshot = null;
597
+ if (this.dirtyLifetime) {
598
+ lifetimeSnapshot = parsedLifetime(this.lifetime);
599
+ if (lifetimeSnapshot === null)
600
+ return this.fail(result("invalid", "schema-or-size", false, this.lowerBoundLoss));
601
+ }
602
+ let recentSnapshot = null;
603
+ if (this.dirtyRecent) {
604
+ recentSnapshot = parsedRecent(this.recent);
605
+ if (recentSnapshot === null)
606
+ return this.fail(result("invalid", "schema-or-size", false, this.lowerBoundLoss));
607
+ }
608
+ try {
609
+ for (const { date, data } of daySnapshots) {
610
+ const filePath = join(this.directory, targetForDay(date));
611
+ if (!atomicWriteJsonSync(filePath, data, { strict: true })) {
612
+ throw new Error(`Failed to write ${targetForDay(date)}`);
613
+ }
614
+ }
615
+ if (lifetimeSnapshot !== null) {
616
+ const filePath = join(this.directory, LIFETIME_TARGET);
617
+ if (!atomicWriteJsonSync(filePath, lifetimeSnapshot, { strict: true })) {
618
+ throw new Error(`Failed to write ${LIFETIME_TARGET}`);
619
+ }
620
+ }
621
+ if (recentSnapshot !== null) {
622
+ const filePath = join(this.directory, RECENT_TARGET);
623
+ if (!atomicWriteJsonSync(filePath, recentSnapshot, { strict: true })) {
624
+ throw new Error(`Failed to write ${RECENT_TARGET}`);
625
+ }
626
+ }
627
+ }
628
+ catch (err) {
629
+ return this.fail(result("failed", err instanceof Error ? err.message : String(err), true, this.lowerBoundLoss));
613
630
  }
614
- if (this.dirtyDays.size + 2 > (this.io.maxTargets ?? 0))
615
- return this.fail(result("invalid", "transaction-targets", false, this.lowerBoundLoss));
616
- const snapshots = this.factSnapshots();
617
- if (snapshots === null)
618
- return this.fail(result("invalid", "schema-or-size", false, this.lowerBoundLoss));
619
631
  const committedDates = [...this.dirtyDays];
620
- const written = this.io.commit(snapshots);
621
- this.last = written;
622
- if (written.status !== "committed" && written.status !== "recovered")
623
- return this.fail(written);
624
632
  this.dirtyDays.clear();
625
633
  this.dirtyLifetime = false;
626
634
  this.dirtyRecent = false;
627
- if (Object.hasOwn(snapshots, LIFETIME_TARGET))
635
+ if (lifetimeSnapshot !== null)
628
636
  this.lifetimeReadState = "ok";
629
- if (Object.hasOwn(snapshots, RECENT_TARGET))
637
+ if (recentSnapshot !== null)
630
638
  this.recentReadState = "ok";
631
639
  this.retryDelay = RETRY_MIN_MS;
632
- const retention = this.commitRetention(committedDates);
633
640
  for (const date of [...committedDates].sort())
634
641
  this.rememberKnownDay(date);
642
+ const written = result("committed", null, false, this.lowerBoundLoss);
643
+ this.last = written;
644
+ const retention = this.commitRetention(committedDates);
635
645
  return retention ?? written;
636
646
  }
637
647
  close() {
@@ -642,10 +652,8 @@ class AccountingStoreImpl {
642
652
  const flushed = this.flush();
643
653
  if (flushed.retryable)
644
654
  return flushed;
645
- const closed = this.io.close();
646
- this._closed = closed.status === "closed";
647
- if (closed.status === "failed")
648
- return this.setLast(result("failed", closed.error, closed.retryable, flushed.lowerBoundLoss));
655
+ this._closed = true;
656
+ this.writer = { status: "closed", error: null, retryable: false };
649
657
  return flushed;
650
658
  }
651
659
  readDay(date) {
@@ -656,17 +664,39 @@ class AccountingStoreImpl {
656
664
  const parsed = parsedDay(cached);
657
665
  return parsed === null ? { status: "corrupt", value: null, error: "schema" } : { status: "ok", value: this.withDayLoss(parsed) };
658
666
  }
659
- const read = this.io.readJson(targetForDay(date), (value) => parseAccountingDayShardV1(value).ok, { quarantineCorrupt: !this.readOnly });
667
+ const read = this.readJsonFile(targetForDay(date), parseAccountingDayShardV1);
660
668
  if (read.status === "missing")
661
669
  return { status: "missing", value: null };
662
670
  if (read.status !== "ok" || read.value === null)
663
- return { status: "corrupt", value: null, error: read.error ?? read.status };
664
- const parsed = parseAccountingDayShardV1(read.value);
671
+ return { status: "corrupt", value: null, error: read.error };
672
+ this.days.set(date, mutableDay(read.value));
673
+ this.rememberKnownDay(date);
674
+ return { status: "ok", value: this.withDayLoss(read.value) };
675
+ }
676
+ readJsonFile(filename, parse) {
677
+ const filePath = join(this.directory, filename);
678
+ this.ioHooks?.beforeRead?.(filePath);
679
+ let text;
680
+ try {
681
+ text = readFileSync(filePath, "utf8");
682
+ }
683
+ catch (err) {
684
+ const code = typeof err === "object" && err !== null && "code" in err ? err.code : undefined;
685
+ if (code === "ENOENT")
686
+ return { status: "missing", value: null };
687
+ return { status: "corrupt", value: null, error: err instanceof Error ? err.message : "read-error" };
688
+ }
689
+ let json;
690
+ try {
691
+ json = JSON.parse(text);
692
+ }
693
+ catch {
694
+ return { status: "corrupt", value: null, error: "invalid-json" };
695
+ }
696
+ const parsed = parse(json);
665
697
  if (!parsed.ok)
666
698
  return { status: "corrupt", value: null, error: "schema" };
667
- this.days.set(date, mutableDay(parsed.value));
668
- this.rememberKnownDay(date);
669
- return { status: "ok", value: this.withDayLoss(parsed.value) };
699
+ return { status: "ok", value: parsed.value };
670
700
  }
671
701
  readDays(datesOrFrom, toOrOptions, maybeOptions) {
672
702
  const options = typeof toOrOptions === "object" ? toOrOptions : maybeOptions;
@@ -724,7 +754,7 @@ class AccountingStoreImpl {
724
754
  readDetail(requestId) {
725
755
  if (!DASHBOARD_REQUEST_ID_PATTERN.test(requestId))
726
756
  return { status: "missing", value: null };
727
- if (this.fixedLoadPending || (this.recentReadState === "corrupt" && !this.dirtyRecent)) {
757
+ if (this.recentReadState === "corrupt" && !this.dirtyRecent) {
728
758
  return { status: "corrupt", value: null, error: "snapshot" };
729
759
  }
730
760
  if (this.recentReadState === "missing" && !this.dirtyRecent)
@@ -862,7 +892,7 @@ class AccountingStoreImpl {
862
892
  const pending = this.pending.get(event.requestId) ?? null;
863
893
  this.pending.delete(event.requestId);
864
894
  const work = { event, pending };
865
- if (!this.fixedLoadPending && this.applyTerminal(work))
895
+ if (this.applyTerminal(work))
866
896
  return;
867
897
  if (this.queued.length >= this.pendingRequestLimit) {
868
898
  this.markGlobalLoss("unknown", "truncated", "pending_terminals");
@@ -895,7 +925,7 @@ class AccountingStoreImpl {
895
925
  return created;
896
926
  }
897
927
  drainQueued() {
898
- if (this.fixedLoadPending || this.queued.length === 0)
928
+ if (this.queued.length === 0)
899
929
  return;
900
930
  const pending = this.queued.splice(0);
901
931
  for (const work of pending)
@@ -903,8 +933,6 @@ class AccountingStoreImpl {
903
933
  this.queued.push(work);
904
934
  }
905
935
  applyTerminal(work) {
906
- if (this.fixedLoadPending)
907
- return false;
908
936
  const date = dayFor(work.event.endedAt);
909
937
  if (date === null || !this.ensureTransactionRoom(date))
910
938
  return false;
@@ -1123,177 +1151,79 @@ class AccountingStoreImpl {
1123
1151
  ensureTransactionRoom(date) {
1124
1152
  if (this.dirtyDays.has(date))
1125
1153
  return true;
1126
- const maximum = this.io.maxTargets;
1127
- if (maximum === null || maximum < 3)
1128
- return false;
1154
+ const maximum = 128;
1129
1155
  if (this.dirtyDays.size + 3 <= maximum)
1130
1156
  return true;
1131
1157
  const flushed = this.flush();
1132
- return (flushed.status === "committed" || flushed.status === "recovered" || flushed.status === "none")
1158
+ return (flushed.status === "committed" || flushed.status === "none")
1133
1159
  && this.dirtyDays.size + 3 <= maximum;
1134
1160
  }
1135
1161
  loadDayForWrite(date) {
1136
1162
  const cached = this.days.get(date);
1137
1163
  if (cached !== undefined)
1138
1164
  return cached;
1139
- const read = this.io.readJson(targetForDay(date), (value) => parseAccountingDayShardV1(value).ok, { quarantineCorrupt: true });
1165
+ const read = this.readJsonFile(targetForDay(date), parseAccountingDayShardV1);
1140
1166
  if (read.status === "missing") {
1141
1167
  const created = emptyDay(date);
1142
1168
  this.days.set(date, created);
1143
1169
  return created;
1144
1170
  }
1145
1171
  if (read.status !== "ok" || read.value === null) {
1146
- if (read.status === "corrupt" || read.status === "oversize") {
1147
- this.markGlobalLoss("corrupt_recovery", "corrupt", "day");
1148
- }
1149
- return null;
1150
- }
1151
- const parsed = parseAccountingDayShardV1(read.value);
1152
- if (!parsed.ok) {
1153
1172
  this.markGlobalLoss("corrupt_recovery", "corrupt", "day");
1154
1173
  return null;
1155
1174
  }
1156
- const mutable = mutableDay(parsed.value);
1175
+ const mutable = mutableDay(read.value);
1157
1176
  this.days.set(date, mutable);
1158
1177
  return mutable;
1159
1178
  }
1160
- deferFixedLoad(value) {
1161
- this.fixedLoadPending = true;
1162
- this.lifetimeReadState = "corrupt";
1163
- this.recentReadState = "corrupt";
1164
- this.last = value;
1165
- if (value.retryable)
1166
- this.scheduleRetry();
1167
- }
1168
- flushDeferredGlobalLosses() {
1169
- const losses = this.deferredGlobalLosses.splice(0);
1170
- for (const loss of losses)
1171
- this.markGlobalLoss(loss.reason, loss.kind, loss.field, loss.count);
1172
- }
1173
1179
  loadFixedSnapshotsReadOnly() {
1174
- const lifetime = this.io.readJson(LIFETIME_TARGET, (value) => parseAccountingLifetimeV1(value).ok, { quarantineCorrupt: false });
1180
+ const lifetime = this.readJsonFile(LIFETIME_TARGET, parseAccountingLifetimeV1);
1175
1181
  this.lifetimeReadState = lifetime.status === "ok" && lifetime.value !== null
1176
1182
  ? "ok"
1177
1183
  : lifetime.status === "missing" ? "missing" : "corrupt";
1178
1184
  if (this.lifetimeReadState === "ok")
1179
1185
  this.lifetime = mutableLifetime(lifetime.value);
1180
- const recent = this.io.readJson(RECENT_TARGET, (value) => parseAccountingRecentV1(value).ok, { quarantineCorrupt: false });
1186
+ const recent = this.readJsonFile(RECENT_TARGET, parseAccountingRecentV1);
1181
1187
  this.recentReadState = recent.status === "ok" && recent.value !== null
1182
1188
  ? "ok"
1183
1189
  : recent.status === "missing" ? "missing" : "corrupt";
1184
1190
  if (this.recentReadState === "ok")
1185
1191
  this.recent = mutableRecent(recent.value);
1186
1192
  }
1187
- recoverAndLoad() {
1188
- const recovered = this.io.recover();
1189
- if (recovered.status === "failed" || recovered.status === "invalid") {
1190
- this.pendingJournalLoss ||= recovered.lowerBoundLoss;
1191
- this.deferFixedLoad(recovered);
1192
- return;
1193
- }
1193
+ loadFixedSnapshots() {
1194
1194
  let fixedLoss = false;
1195
- let fixedReadFailed = false;
1196
- const lifetime = this.io.readJson(LIFETIME_TARGET, (value) => parseAccountingLifetimeV1(value).ok, { quarantineCorrupt: true });
1195
+ const lifetime = this.readJsonFile(LIFETIME_TARGET, parseAccountingLifetimeV1);
1197
1196
  if (lifetime.status === "ok" && lifetime.value !== null) {
1198
- const parsed = parseAccountingLifetimeV1(lifetime.value);
1199
- if (parsed.ok) {
1200
- this.lifetime = mutableLifetime(parsed.value);
1201
- this.lifetimeReadState = "ok";
1202
- }
1203
- else {
1204
- this.lifetimeReadState = "corrupt";
1205
- fixedLoss = true;
1206
- }
1197
+ this.lifetime = mutableLifetime(lifetime.value);
1198
+ this.lifetimeReadState = "ok";
1207
1199
  }
1208
1200
  else if (lifetime.status === "missing") {
1209
1201
  this.lifetimeReadState = "missing";
1210
1202
  }
1211
- else if (lifetime.status === "failed") {
1212
- this.lifetimeReadState = "corrupt";
1213
- fixedReadFailed = true;
1214
- }
1215
1203
  else {
1216
1204
  this.lifetimeReadState = "corrupt";
1217
1205
  fixedLoss = true;
1218
1206
  }
1219
- const recent = this.io.readJson(RECENT_TARGET, (value) => parseAccountingRecentV1(value).ok, { quarantineCorrupt: true });
1207
+ const recent = this.readJsonFile(RECENT_TARGET, parseAccountingRecentV1);
1220
1208
  if (recent.status === "ok" && recent.value !== null) {
1221
- const parsed = parseAccountingRecentV1(recent.value);
1222
- if (parsed.ok) {
1223
- this.recent = mutableRecent(parsed.value);
1224
- this.recentReadState = "ok";
1225
- }
1226
- else {
1227
- this.recentReadState = "corrupt";
1228
- fixedLoss = true;
1229
- }
1209
+ this.recent = mutableRecent(recent.value);
1210
+ this.recentReadState = "ok";
1230
1211
  }
1231
1212
  else if (recent.status === "missing") {
1232
1213
  this.recentReadState = "missing";
1233
1214
  }
1234
- else if (recent.status === "failed") {
1235
- this.recentReadState = "corrupt";
1236
- fixedReadFailed = true;
1237
- }
1238
1215
  else {
1239
1216
  this.recentReadState = "corrupt";
1240
1217
  fixedLoss = true;
1241
1218
  }
1242
- if (fixedReadFailed) {
1243
- this.pendingFixedLoss ||= fixedLoss;
1244
- this.pendingJournalLoss ||= recovered.lowerBoundLoss || recovered.status === "recovery-loss";
1245
- this.deferFixedLoad(result("failed", "snapshot-read", true, this.lowerBoundLoss || this.pendingJournalLoss));
1246
- return;
1247
- }
1248
- fixedLoss ||= this.pendingFixedLoss;
1249
- const journalLoss = this.pendingJournalLoss || recovered.lowerBoundLoss || recovered.status === "recovery-loss";
1250
- this.pendingFixedLoss = false;
1251
- this.pendingJournalLoss = false;
1252
- this.fixedLoadPending = false;
1253
- this.flushDeferredGlobalLosses();
1254
- if (journalLoss)
1255
- this.consumeRecoveryLoss(recovered);
1256
- if (fixedLoss)
1219
+ if (fixedLoss) {
1257
1220
  this.markGlobalLoss("corrupt_recovery", "corrupt", "snapshot");
1221
+ }
1258
1222
  this.seedTrustedDateFromLifetime();
1259
1223
  const today = new Date(this.now()).toISOString().slice(0, 10);
1260
1224
  this.loadDayForWrite(today);
1261
1225
  this.queueRecoveredRetention();
1262
- this.last = recovered;
1263
- }
1264
- factSnapshots() {
1265
- const snapshots = {};
1266
- const encode = (name, value) => {
1267
- let text;
1268
- try {
1269
- text = JSON.stringify(value);
1270
- }
1271
- catch {
1272
- return false;
1273
- }
1274
- const ceiling = this.io.maxFileBytes;
1275
- if (ceiling === null || Buffer.byteLength(text, "utf8") > ceiling)
1276
- return false;
1277
- snapshots[name] = text;
1278
- return true;
1279
- };
1280
- if (this.dirtyLifetime) {
1281
- const lifetime = parsedLifetime(this.lifetime);
1282
- if (lifetime === null || !encode(LIFETIME_TARGET, lifetime))
1283
- return null;
1284
- }
1285
- if (this.dirtyRecent) {
1286
- const recent = parsedRecent(this.recent);
1287
- if (recent === null || !encode(RECENT_TARGET, recent))
1288
- return null;
1289
- }
1290
- for (const date of [...this.dirtyDays].sort()) {
1291
- const day = this.days.get(date);
1292
- const parsed = day === undefined ? null : parsedDay(day);
1293
- if (parsed === null || !encode(targetForDay(date), parsed))
1294
- return null;
1295
- }
1296
- return snapshots;
1226
+ this.last = result("none");
1297
1227
  }
1298
1228
  commitRetention(committedDates) {
1299
1229
  if (this.retentionDays === null)
@@ -1310,15 +1240,12 @@ class AccountingStoreImpl {
1310
1240
  this.pendingRetentionPlan = null;
1311
1241
  return null;
1312
1242
  }
1313
- const maximum = this.io.maxTargets;
1314
- if (maximum === null || maximum < 1)
1315
- return this.fail(result("invalid", "retention-targets", false, this.lowerBoundLoss));
1316
1243
  const current = parsedLifetime(this.lifetime);
1317
1244
  if (current === null)
1318
1245
  return this.fail(result("invalid", "lifetime-schema", false, this.lowerBoundLoss));
1319
1246
  const progress = validDate(current.coverage.retentionFrom) ? current.coverage.retentionFrom : null;
1320
1247
  const start = progress ?? dayFor(current.firstRequestAt ?? "");
1321
- const retention = this.retentionCandidates(start, cutoff, Math.max(0, maximum - 1));
1248
+ const retention = this.retentionCandidates(start, cutoff, 127);
1322
1249
  const candidates = retention.dates;
1323
1250
  if (retention.truncated && candidates.length === 0) {
1324
1251
  return this.fail(result("invalid", "retention-targets", false, this.lowerBoundLoss));
@@ -1335,23 +1262,28 @@ class AccountingStoreImpl {
1335
1262
  markCoverage(next.coverage, "unknown", "truncated", "retention");
1336
1263
  }
1337
1264
  const plan = { cutoff, candidates, next, truncated: retention.truncated };
1338
- const snapshots = {};
1339
1265
  const retainedLifetime = parsedLifetime(next);
1340
1266
  if (retainedLifetime === null)
1341
1267
  return this.fail(result("invalid", "retention-schema-or-size", false, this.lowerBoundLoss));
1342
- const serialized = JSON.stringify(retainedLifetime);
1343
- const ceiling = this.io.maxFileBytes;
1344
- if (serialized === undefined || ceiling === null || Buffer.byteLength(serialized, "utf8") > ceiling) {
1345
- return this.fail(result("invalid", "retention-schema-or-size", false, this.lowerBoundLoss));
1268
+ try {
1269
+ const filePath = join(this.directory, LIFETIME_TARGET);
1270
+ if (!atomicWriteJsonSync(filePath, retainedLifetime, { strict: true })) {
1271
+ throw new Error(`Failed to write ${LIFETIME_TARGET}`);
1272
+ }
1273
+ for (const date of candidates) {
1274
+ try {
1275
+ unlinkSync(join(this.directory, targetForDay(date)));
1276
+ }
1277
+ catch (err) {
1278
+ const code = typeof err === "object" && err !== null && "code" in err ? err.code : undefined;
1279
+ if (code !== "ENOENT")
1280
+ throw err;
1281
+ }
1282
+ }
1283
+ }
1284
+ catch (err) {
1285
+ return this.fail(result("failed", err instanceof Error ? err.message : String(err), true, this.lowerBoundLoss));
1346
1286
  }
1347
- snapshots[LIFETIME_TARGET] = serialized;
1348
- for (const date of candidates)
1349
- snapshots[targetForDay(date)] = null;
1350
- this.pendingRetentionPlan = plan;
1351
- const written = this.io.commit(snapshots);
1352
- this.last = written;
1353
- if (written.status !== "committed" && written.status !== "recovered")
1354
- return this.fail(written);
1355
1287
  this.applyRetentionPlan(plan);
1356
1288
  this.pendingRetentionPlan = null;
1357
1289
  if (plan.truncated) {
@@ -1362,6 +1294,8 @@ class AccountingStoreImpl {
1362
1294
  else {
1363
1295
  this.pendingRetention = null;
1364
1296
  }
1297
+ const written = result("committed", null, false, this.lowerBoundLoss);
1298
+ this.last = written;
1365
1299
  return written;
1366
1300
  }
1367
1301
  retentionCandidates(start, cutoff, limit) {
@@ -1500,35 +1434,7 @@ class AccountingStoreImpl {
1500
1434
  const cutoff = new Date(at - (this.retentionDays - 1) * 86_400_000).toISOString().slice(0, 10);
1501
1435
  return validDate(cutoff) ? cutoff : null;
1502
1436
  }
1503
- consumeRecoveryLoss(_recovered) {
1504
- if (this.lowerBoundLoss)
1505
- return;
1506
- this.lowerBoundLoss = true;
1507
- this.markGlobalLoss("corrupt_recovery", "corrupt", "journal");
1508
- this.scheduleFlush();
1509
- }
1510
1437
  markGlobalLoss(reason, kind, field, count = 1) {
1511
- if (this.fixedLoadPending) {
1512
- const existing = this.deferredGlobalLosses.find((loss) => loss.reason === reason && loss.kind === kind && loss.field === field);
1513
- if (existing !== undefined) {
1514
- if (!increase(existing, "count", count))
1515
- existing.count = Number.MAX_SAFE_INTEGER;
1516
- }
1517
- else if (this.deferredGlobalLosses.length < ACCOUNTING_MAX_LOSS_MARKERS) {
1518
- this.deferredGlobalLosses.push({ reason, kind, field, count });
1519
- }
1520
- else {
1521
- const summary = this.deferredGlobalLosses.find((loss) => loss.field === "deferred_loss");
1522
- if (summary !== undefined) {
1523
- if (!increase(summary, "count", count))
1524
- summary.count = Number.MAX_SAFE_INTEGER;
1525
- }
1526
- else {
1527
- this.deferredGlobalLosses[this.deferredGlobalLosses.length - 1] = { reason: "unknown", kind: "truncated", field: "deferred_loss", count };
1528
- }
1529
- }
1530
- return;
1531
- }
1532
1438
  if (reason === "corrupt_recovery")
1533
1439
  this.lowerBoundLoss = true;
1534
1440
  markCoverage(this.lifetime.coverage, reason, kind, field, count);