import { type Clock, utcDayKey } from './time.ts' export interface BudgetConfig { /** Max billable model + capability calls per UTC day; 0 disables the cap. */ dailyCapCalls: number /** Fraction of dailyCapCalls reserved for recovery wakes. */ recoveryReserveFraction: number } export const DEFAULT_BUDGET: BudgetConfig = { dailyCapCalls: 500, recoveryReserveFraction: 0.1, } export interface BudgetState { day: string used: number usedRecovery: number } export function emptyBudgetState(clock: Clock): BudgetState { return { day: utcDayKey(clock.now()), used: 0, usedRecovery: 0 } } /** Roll state over to today if the day changed. */ export function rollover(state: BudgetState, clock: Clock): BudgetState { const today = utcDayKey(clock.now()) return state.day === today ? state : { day: today, used: 0, usedRecovery: 0 } } export function isRecoveryWake(kind: string): boolean { return kind === 'deploy_failure' || kind === 'ci_failure' || kind === 'recovery' } /** Pure decision whether a call is allowed under the cap and reserve (spec AC17). */ export function canSpend(config: BudgetConfig, state: BudgetState, kind: string): boolean { if (config.dailyCapCalls <= 0) return true const reserve = Math.floor(config.dailyCapCalls * config.recoveryReserveFraction) if (isRecoveryWake(kind)) { return state.usedRecovery < reserve } const ordinaryUsed = state.used - state.usedRecovery return ordinaryUsed < config.dailyCapCalls - reserve } export function spend(_config: BudgetConfig, state: BudgetState, kind: string): BudgetState { const next = { ...state, used: state.used + 1 } if (isRecoveryWake(kind)) next.usedRecovery = state.usedRecovery + 1 return next }