/** * BbsRoomBudgetTracker — Atomic reserve / commit / release token-bucket * backed by better-sqlite3 (ADR-164.1 §3-§5). * * Closes the read-then-write race in ADR-164's first-draft tracker by routing * every state transition through a single `BEGIN IMMEDIATE` transaction that * acquires the SQLite write lock atomically with the gate check. The * `_lock_bump` column write is a belt-and-suspenders explicit write inside * the transaction so the lock acquisition is visible to code review and * query-log analysis (§3.2 peer-review clarification). * * Architectural constraints (ADR-164.1): * - Default OFF: this module is opt-in behind `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`. * The pod-tick.mjs file-based stub remains the default until the flag is * flipped per ADR-164.1 §12.4. * - `committed_post_expiry` state machine implemented per §5.3 — late * commits on expired reservations ARE accepted (the API spend already * happened), transition to `committed_post_expiry`, charge the budget, * and return `warned: 'COMMIT_AFTER_EXPIRY'`. This closes the §8.1 * Expired Commit Leak surfaced by peer review on 2026-06-29. * - `expires_at` is clamped to [5_000, 300_000] ms (§3.2). * - WAL + synchronous=NORMAL + busy_timeout=500 for write throughput. * * The sweeper interval (`sweepExpired()`) is exposed as a manual method; * callers are expected to drive it on a setInterval — that integration lives * at the call site, not in this file, per ADR-164.1 §7.1. * * @module @claude-flow/cli/business-pods/bbs-budget-tracker */ export interface SqliteDatabase { pragma(name: string): unknown; prepare(sql: string): SqliteStatement; exec(sql: string): void; close(): void; } export interface SqliteStatement { run(...params: unknown[]): { changes: number; lastInsertRowid: number | bigint; }; get(...params: unknown[]): Record | undefined; all(...params: unknown[]): Record[]; } export declare const RESERVATION_EXPIRY_FLOOR_MS = 5000; export declare const RESERVATION_EXPIRY_CEILING_MS = 300000; export declare const RESERVATION_EXPIRY_DEFAULT_MS = 60000; export declare function clampReservationExpiry(raw: number | undefined): number; export type ReserveResult = { ok: true; reservationId: string; remainingAfterReserve: number; } | { ok: false; error: 'BUDGET_EXCEEDED' | 'ROOM_NOT_FOUND'; }; export type CommitResult = { ok: true; committed: true; finalRemaining: number; } | { ok: true; warned: 'COMMIT_AFTER_EXPIRY'; finalRemaining: number; } | { ok: false; error: 'NOT_FOUND' | 'ALREADY_FINALIZED'; }; export type ReleaseResult = { ok: true; released: true; } | { ok: false; error: 'NOT_FOUND' | 'ALREADY_FINALIZED'; }; export declare const SCHEMA_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA busy_timeout = 500;\n\nCREATE TABLE IF NOT EXISTS bbs_budget_rooms (\n room_id TEXT NOT NULL PRIMARY KEY,\n monthly_cap_usd REAL NOT NULL CHECK (monthly_cap_usd >= 0),\n billing_month TEXT NOT NULL,\n _lock_bump INTEGER NOT NULL DEFAULT 0\n);\n\nCREATE TABLE IF NOT EXISTS bbs_budget_reservations (\n reservation_id TEXT NOT NULL PRIMARY KEY,\n room_id TEXT NOT NULL REFERENCES bbs_budget_rooms(room_id),\n caller_node_id TEXT NOT NULL,\n estimated_usd REAL NOT NULL CHECK (estimated_usd >= 0),\n actual_usd REAL,\n state TEXT NOT NULL\n CHECK (state IN ('reserved','committed','released','expired','committed_post_expiry')),\n reserved_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n committed_at INTEGER,\n audit_envelope_id TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_reservations_room_month\n ON bbs_budget_reservations (room_id, reserved_at);\n\nCREATE INDEX IF NOT EXISTS idx_reservations_expiry\n ON bbs_budget_reservations (state, expires_at);\n"; export interface BudgetAuditSink { emit(eventType: 'reservation.committed_post_expiry' | 'reservation.committed' | 'reservation.released' | 'reservation.reserved' | 'reservation.budget_exceeded', payload: Record): void; } export interface AtomicBbsRoomBudgetTrackerOptions { /** SQLite database handle (caller-owned, opened in WAL mode). */ db: SqliteDatabase; /** Override the default 60s reservation window (clamped to [5s, 300s]). */ defaultExpiryMs?: number; /** Audit sink — receives reservation lifecycle events. */ audit?: BudgetAuditSink; /** Inject Date.now() for tests. */ clock?: () => number; } export declare class AtomicBbsRoomBudgetTracker { private readonly db; private readonly defaultExpiryMs; private readonly audit; private readonly clock; constructor(opts: AtomicBbsRoomBudgetTrackerOptions); /** * Register (or update) a room and its monthly cap. Idempotent. */ registerRoom(roomId: string, monthlyCapUsd: number): void; /** * Atomically check budget and insert a reservation row in a single * BEGIN IMMEDIATE transaction. See ADR-164.1 §5.2. */ reserve(roomId: string, callerId: string, estimatedUsd: number, opts?: { auditEnvelopeId?: string; expiryMs?: number; }): ReserveResult; /** * Commit the reservation with the actual cost. Late commits (expired * before commit landed) ARE accepted, transitioned to * 'committed_post_expiry', charged to the budget, and surfaced via * `warned: 'COMMIT_AFTER_EXPIRY'` plus a `reservation.committed_post_expiry` * audit emit. See ADR-164.1 §5.3 + §8.1. */ commit(reservationId: string, actualUsd: number): CommitResult; /** * Release a reservation (caller decided not to proceed). State must be * 'reserved'; any other state is ALREADY_FINALIZED. */ release(reservationId: string): ReleaseResult; /** * Sweep expired reservations: transition 'reserved' rows whose expiry has * passed to 'expired'. Callers should drive this on a setInterval per * ADR-164.1 §7.1 (default cadence 5s; this method does NOT install the * timer — that's the integration site's responsibility). * Returns the number of rows updated. */ sweepExpired(): number; /** * Diagnostic read: list reservations for a room. Not on the hot path. */ listReservations(roomId: string): Array>; } /** * Feature-flagged factory per ADR-164.1 §12.3. Returns an atomic tracker * when `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`, otherwise returns null so the * caller can keep using the file-based stub in pod-tick.mjs. */ export declare function createAtomicTrackerIfEnabled(opts: AtomicBbsRoomBudgetTrackerOptions): AtomicBbsRoomBudgetTracker | null; //# sourceMappingURL=bbs-budget-tracker.d.ts.map