/** * Accounting storage engine and lifecycle store. * * Charter & Invariants: * - Append-only write-ahead journal (`.journal`) with atomic flush and recovery. * - Segmented day shards (`YYYY-MM-DD.json`) with bounded detail/aggregate rollups. * - In-memory recent minute sliding windows for near-real-time quota checks. * - Crash-consistent journal replay on store recovery with loss marker bounds. * - Read-only queries (`readOnly: true`) never mutate or create writer leases. */ import { type AccountingRecorder } from "./accounting.js"; /** * The outcome of one flush/close. Only members a code path can RETURN are declared: the * `"recovered"`/`"recovery-loss"` statuses and the `transactionId`/`quarantinedPath` fields of the * write-ahead journal DR-020 deleted (`751fb52`) were removed on 2026-09-04, so a reader no longer * has to check git history to learn which members are real. */ export interface SnapshotMutationResult { readonly status: "none" | "committed" | "invalid" | "failed"; readonly lowerBoundLoss: boolean; readonly error: string | null; readonly retryable: boolean; } export interface SnapshotWriterResult { readonly status: "acquired" | "released" | "closed" | "failed"; readonly error: string | null; readonly retryable: boolean; } /** * Read seam for tests: observe which shard files the store opens (the `readDaysCap` cache tests * count reads through it). The journal step hooks that used to sit beside it went with DR-020. */ export interface AccountingReadHooks { readonly beforeRead?: (path: string) => void; } import { type AccountingDayShard, type AccountingLifetime, type AccountingRequestPacket } from "./accounting-store-schema.js"; import { type WriterHealth } from "./dashboard-contract.js"; 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"; 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"; /** Sized for a Claude Code session holding many parallel subagent streams in flight. */ export declare const ACCOUNTING_MAX_PENDING_REQUESTS = 256; export declare const ACCOUNTING_MAX_PENDING_ATTEMPTS_PER_REQUEST = 10000; export declare const ACCOUNTING_MAX_READ_DAYS = 31; export declare const ACCOUNTING_DEFAULT_RECENT_ROWS = 100; export declare const ACCOUNTING_DEFAULT_DETAIL_ROWS = 100; export type AccountingReadResult = { readonly status: "ok"; readonly value: T; } | { readonly status: "missing"; readonly value: null; } | { readonly status: "corrupt"; readonly value: null; readonly error: string; }; export interface AccountingDaysRead { readonly status: "ok" | "missing" | "corrupt" | "capped"; readonly days: readonly AccountingDayShard[]; readonly results: readonly { readonly date: string; readonly result: AccountingReadResult; }[]; readonly missingDates: readonly string[]; readonly corruptDates: readonly string[]; readonly capped: boolean; } export interface AccountingReader { readDay(date: string): AccountingReadResult; readDays(dates: readonly string[], options?: { readonly cap?: number; }): AccountingDaysRead; readDays(from: string, to: string, options?: { readonly cap?: number; }): AccountingDaysRead; readLifetime(): AccountingReadResult; readRecent(options?: { readonly limit?: number; } | number): AccountingReadResult; readDetail(requestId: string): AccountingReadResult; } /** * What one credential (optionally narrowed to one deployment) consumed in the CURRENT period, * read straight from in-memory state. Basis vocabulary shared with the dashboard contract's * `localUsedBasis`. This raw `basis` describes tokens only; consumers projecting `requests` * label that independently as relay-counted. */ export interface UsedInWindowReading { /** Completed requests attributed to this credential in the window; null when not visible. */ readonly requests: number | null; /** Reported-or-estimated input+output tokens; null when nothing (or something uncertain) is visible. */ readonly tokens: number | null; /** * How the token figure was obtained. Null when nothing was seen at all — but NOT implied by a * null `tokens`: a window mixing reported and estimated bases reports `basis: "mixed"` with * `tokens: null`, because the split is the honest answer and one blended number would not be. */ readonly basis: "reported" | "estimated" | "mixed" | null; } export interface UsedInWindowOptions { readonly credentialId: string; readonly model?: string | null; readonly period: "minute" | "day" | "month"; /** Injected for deterministic tests; defaults to Date.now. */ readonly now?: number; } export interface AccountingStoreOptions { readonly rootDir?: string; readonly directory?: string; readonly path?: string; readonly retentionDays?: number | null; readonly recentLimit?: number; readonly detailLimit?: number; /** * Test seam for the durable per-day dedup cap, the `recentLimit` pattern: filling the real * 16,384-entry cap costs seconds of insert-sorting, which put the cap test's worst case over * vitest's budget under full-suite load. Bounded by ACCOUNTING_MAX_DEDUP_IDS, which stays the * default — production callers pass nothing. */ readonly dedupLimit?: number; readonly readDaysCap?: number; readonly pendingRequestLimit?: number; readonly pendingAttemptLimit?: number; readonly now?: () => number; readonly ioHooks?: AccountingReadHooks; /** * Out-of-process readers only (`llm-relay cost`): construct without the writer lease, so the * reader performs no write against a directory a live relay may be committing to — it observes * only the committed snapshots and reports the resulting lag rather than repairing it. */ readonly readOnly?: boolean; } export interface AccountingStore extends AccountingRecorder, AccountingReader { readonly directory: string; readonly closed: boolean; readonly writerStatus: SnapshotWriterResult; readonly lastWrite: SnapshotMutationResult | null; flush(): SnapshotMutationResult; close(): SnapshotMutationResult; reader(): AccountingReader; /** The availability lane's narrow in-memory window read (see usedInWindow below). */ usedInWindow(options: UsedInWindowOptions): UsedInWindowReading; /** * Whether this store is still metering (backlog item 19). ONE read-only accessor over * the existing `writerStatus`/`lastWrite` fields — the backlog entry's "zero consumers" * finding — so `llm-relay cost` and `/telemetry` can state when the store's last flush * failed or the writer lease was refused, and "no spend since noon" cannot be mistaken * for "no traffic since noon". Serving never stops on a failed writer: `record()` keeps * accepting events into memory; only persistence stops, and this names the stop. */ writerHealth(): WriterHealth; } /** Accounting shares the common write-behind cadence, exported for callers/tests. */ export declare const ACCOUNTING_FLUSH_DELAY_MS = 250; export declare const ACCOUNTING_MAX_FLUSH_DELAY_MS = 2000; export declare function createAccountingStore(options?: AccountingStoreOptions): AccountingStore;