Luigit
repositories / will

will

owned by admin

src/shared/budget.ts

Raw
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
}