/** Pure outbound-fetch quota helpers (no kody imports; unit-testable). */ /** Account usage for `outbound_fetches_per_day` (from `usageGet`). */ export type FetchQuota = { current: number limit: number percent: number day: string | null } export type QuotaLevel = 'ok' | 'warn' | 'critical' /** At or above this share of the daily cap, tick backs off and warns. */ export const WARN_PERCENT = 80 /** At or above this share, tick tells agents to pause non-essential ticks. */ export const CRITICAL_PERCENT = 95 export const WARN_MIN_POLL_SECONDS = 300 export const CRITICAL_MIN_POLL_SECONDS = 900 /** Pick `outbound_fetches_per_day` out of a `usageGet` result. */ export function readFetchQuota(usage: unknown): FetchQuota | null { const u = usage as { day?: unknown; resources?: Array> } | null const row = Array.isArray(u?.resources) ? u!.resources!.find((r) => r?.resource === 'outbound_fetches_per_day') : undefined const current = Number(row?.current) const limit = Number(row?.limit) if (!row || !Number.isFinite(current) || !Number.isFinite(limit) || limit <= 0) return null return { current, limit, percent: Math.round((current / limit) * 1000) / 10, day: typeof u?.day === 'string' ? u.day : null, } } export function quotaLevel(quota: FetchQuota | null): QuotaLevel { if (!quota) return 'ok' if (quota.percent >= CRITICAL_PERCENT) return 'critical' if (quota.percent >= WARN_PERCENT) return 'warn' return 'ok' } /** Raise the poll interval near the cap; never lowers it. */ export function adjustPoll(pollAfterSeconds: number | undefined, level: QuotaLevel): number | undefined { if (pollAfterSeconds == null || level === 'ok') return pollAfterSeconds const floor = level === 'critical' ? CRITICAL_MIN_POLL_SECONDS : WARN_MIN_POLL_SECONDS return Math.max(pollAfterSeconds, floor) } export function quotaWarning(quota: FetchQuota, level: QuotaLevel): string | null { if (level === 'ok') return null const used = `${quota.current.toLocaleString('en-US')} of ${quota.limit.toLocaleString('en-US')} outbound fetches (${quota.percent}%) used today (UTC); the cap resets at 00:00 UTC` return level === 'critical' ? `${used}. Pause ticks on PRs that are only waiting, and spend what is left on merges and review replies.` : `${used}. Tick slower (pollAfterSeconds is raised) and avoid ticking PRs that are only waiting.` } /** Kody's daily plan-cap refusal ("Plan limit reached …"). */ export function isPlanLimitError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error ?? '') return /plan limit reached/i.test(message) } /** Next 00:00 UTC after `nowMs` (when daily caps reset). */ export function nextUtcMidnight(nowMs: number): Date { const d = new Date(nowMs) return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1)) }