/** * Monthly spend cap for System One decisions — the CLEO-enforced COST budget * of owner decision D11159 (spec `system-one-integration` §7). * * Every CLEO process on the machine records provider-reported cost (integer * micro-dollars) into ONE ledger, `/decide/spend.json`, guarded by a * `proper-lockfile` lock. Once the month-to-date spend reaches the cap * (`decide.budget.monthlyMicros`, default * {@link DEFAULT_MONTHLY_SPEND_CAP_MICROS} = $1), every site degrades to its * heuristic with fallback reason `budget` until the UTC month rolls over. It * never fails a command. * * ## Reserve, then commit * * A caller reserves its estimated cost BEFORE the provider call, under the * same lock as the cap check, and commits the reported cost (or releases the * reservation) after it. Concurrent callers therefore see each other's * in-flight spend and cannot overshoot the cap together. A reservation still * pending after {@link RESERVATION_TTL_MS} (its process died, or exited * before its commit landed) is CHARGED at its estimate, not dropped: the * request may have been billed, and the cap must not undercount it. * * The lock retries for about a second with backoff: a decision may wait for * the ledger, but a cost is never dropped because 40 processes recorded at * once (review of #1685: the first version lost 33 of 40 concurrent records). * * The ledger also carries the key-limit stop: a 403 `key_limit_exceeded` * (an unverified provider extension) means the key's monthly decision limit * is reached, so decisions stop until the end of the UTC month. * * A corrupt ledger fails closed (every site uses its heuristic) and * `cleo decide status` says so; `cleo decide budget reset` repairs it, moving * the bad file aside as a receipt. It refuses a readable ledger unless * `--force` is given, and then keeps the month-to-date spend, so a reset can * never lift a reached cap. * * @task T12664 * @epic T12486 */ /** Default monthly cap: 1,000,000 micro-dollars = $1 (D11159). */ export declare const DEFAULT_MONTHLY_SPEND_CAP_MICROS = 1000000; /** Config key holding the monthly cap in micro-dollars. */ export declare const MONTHLY_SPEND_CAP_KEY = "decide.budget.monthlyMicros"; /** * How long a reservation stays pending, ms. After that it is charged at its * estimate (it may have been billed). Decision deadlines are hundreds of ms, * so a live call always commits or releases long before this. */ export declare const RESERVATION_TTL_MS = 60000; /** Verdict of a spend check or reservation. */ export type SpendVerdict = 'ok' | 'over_budget' | 'key_limited' | 'unavailable'; /** Month-to-date state, as `cleo decide status` reports it. */ export interface SpendStatus { /** UTC month, `YYYY-MM`. */ readonly month: string; /** Micro-dollars committed this month. */ readonly spentMicros: number; /** Micro-dollars reserved by calls still in flight. */ readonly reservedMicros: number; /** Epoch ms until which the key's monthly limit stops decisions, when set. */ readonly keyLimitedUntil?: number; } /** Result of {@link SpendLedger.reserve}. */ export interface SpendReservation { /** The verdict; only `ok` carries a reservation. */ readonly verdict: SpendVerdict; /** Reservation id to commit or release. */ readonly id?: string; } /** Result of {@link SpendLedger.inspect}. */ export type SpendLedgerHealth = 'ok' | 'absent' | 'corrupt' | 'unavailable'; /** A monthly spend ledger. Implementations never throw. */ export interface SpendLedger { /** * Check the cap and, when there is room for `estimateMicros` on top of the * committed and reserved spend, reserve it — atomically. */ reserve(capMicros: number, estimateMicros: number): Promise; /** Replace a reservation with the provider-reported cost. */ commit(id: string, costMicros: number): Promise; /** Drop a reservation (the call was not billed). */ release(id: string): Promise; /** Add a cost with no reservation. */ record(costMicros: number): Promise; /** Stop decisions until the end of the current UTC month (403 key limit). */ markKeyLimited(): Promise; /** Current month-to-date state; `null` when unreadable. */ status(): Promise; } /** * The UTC month of `epochMs`, `YYYY-MM`. * * @param epochMs - Epoch milliseconds. * @returns The month string. */ export declare function utcMonth(epochMs: number): string; /** * Epoch ms of the first instant of the next UTC month. * * @param epochMs - Epoch milliseconds. * @returns Start of the next UTC month. */ export declare function startOfNextUtcMonth(epochMs: number): number; /** Clock option shared by both ledgers. */ export interface SpendLedgerOptions { /** Wall clock, epoch ms. Default `Date.now`. */ readonly now?: () => number; } /** * A process-local ledger (tests, callers that must not touch disk). * * @param opts - Clock. * @returns A {@link SpendLedger} that does NOT coordinate across processes. */ export declare function createMemorySpendLedger(opts?: SpendLedgerOptions): SpendLedger; /** Options for {@link createFileSpendLedger}. */ export interface FileSpendLedgerOptions extends SpendLedgerOptions { /** Ledger path. Default {@link defaultSpendStatePath}. */ readonly statePath?: string; } /** * Default ledger location: `/decide/spend.json`. * * @returns Absolute path. */ export declare function defaultSpendStatePath(): string; /** * A ledger shared by every process on the machine through a locked JSON file. * A lock or read failure (including a corrupt file) makes `reserve` answer * `unavailable`, which the client treats as a fallback: the cap fails closed, * like the request bucket. * * @param opts - Clock and state path. * @returns A {@link SpendLedger}. */ export declare function createFileSpendLedger(opts?: FileSpendLedgerOptions): SpendLedger; /** * Whether the ledger file is readable. * * @param statePath - Ledger path. Default {@link defaultSpendStatePath}. * @returns `ok`, `absent`, `corrupt` or `unavailable` (unreadable for another reason). */ export declare function inspectSpendLedger(statePath?: string): SpendLedgerHealth; /** Receipt of {@link resetSpendLedger}. */ export interface SpendResetReceipt { /** Ledger path. */ readonly statePath: string; /** Health before the reset. */ readonly before: SpendLedgerHealth; /** Where the previous file was moved, when there was one. */ readonly backupPath?: string; /** ISO time of the reset. */ readonly resetAt: string; /** Month-to-date spend carried into the new ledger (0 when the old file was unreadable). */ readonly spentMicros: number; /** Whether `force` overrode the refusal to reset a readable ledger. */ readonly forced: boolean; } /** Options for {@link resetSpendLedger}. */ export interface SpendResetOptions { /** * Reset a ledger that is NOT corrupt. The month-to-date spend is still * carried over; only in-flight reservations and the key-limit stop are * cleared (e.g. after raising the key's limit in the provider console). */ readonly force?: boolean; } /** * Thrown by {@link resetSpendLedger} when the ledger is not corrupt and * `force` was not given. A reached cap is not a fault to repair. */ export declare class SpendResetRefusedError extends Error { /** Health of the ledger that was not reset. */ readonly health: SpendLedgerHealth; /** @param health - The ledger's health (anything but `corrupt`). */ constructor(health: SpendLedgerHealth); } /** * Reset the ledger (`cleo decide budget reset`): move the current file aside * to `spend.json.reset-` and start a new ledger for this month. The * repair for a corrupt ledger, which otherwise fails closed forever. * * Refuses (throws {@link SpendResetRefusedError}) unless the ledger is * corrupt or `opts.force` is set, so an agent cannot reset its way past a * reached cap. When the old file is readable its month-to-date spend * (expired reservations included) is carried over; only a corrupt file's * spend is lost, and the moved file is kept as the receipt of it. * * @param statePath - Ledger path. Default {@link defaultSpendStatePath}. * @param now - Wall clock, epoch ms. * @param opts - `force` resets a ledger that is not corrupt. * @returns What was reset, the spend carried over and where the old file went. * @throws {SpendResetRefusedError} When the ledger is not corrupt and `force` is not set. */ export declare function resetSpendLedger(statePath?: string, now?: number, opts?: SpendResetOptions): Promise; //# sourceMappingURL=spend.d.ts.map