repositories / will
will
owned by admin
src/shared/budget.ts
Rawimport { 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
}