UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

118 lines 4.55 kB
import { isTransientQuotaReason } from './driver/index.js'; /** * How often to read the quota when everything is healthy. A read spawns the * whole agent CLI (~5s), and the agent's own usage fetch is refused upstream if * asked too often, so this is deliberately slow: the boundary moves over days, * which a 5-minute sample rate resolves comfortably. */ export const DEFAULT_POLL_MS = 5 * 60 * 1000; /** Longest gap between reads once backoff has stretched it. */ export const MAX_POLL_MS = 30 * 60 * 1000; /** * Keeps a recent quota reading on hand (#525). * * Polling is slow by design and backs *off* on failure rather than retrying * into it: the agent's usage fetch is refused upstream when asked too often, * and the penalty window is minutes long, so an eager retry loop would keep the * number permanently unavailable — the opposite of the goal. */ export class QuotaPoller { opts; envelope = { latest: undefined, lastGood: undefined, lastGoodAt: undefined, lastFailureAt: undefined }; timer; running = false; currentIntervalMs; stopped = false; now; constructor(opts) { this.opts = opts; this.currentIntervalMs = opts.intervalMs ?? DEFAULT_POLL_MS; this.now = opts.now ?? (() => Date.now()); } /** What we currently believe. */ current() { return { ...this.envelope }; } /** The gap before the next read, which grows while the fetch keeps being refused. */ get intervalMs() { return this.currentIntervalMs; } /** Whether the poller has given up (an authoritative failure, or {@link stop}). */ get isStopped() { return this.stopped; } /** * Read once, now, and fold the result in. Safe to call on demand (e.g. right * after a turn settles) alongside the timer. */ async poll() { let quota; try { quota = await this.opts.read(); } catch { // A driver that throws is the same story as one that reports a failed // fetch: this attempt told us nothing, and it may work next time. quota = { available: false, reason: 'fetch-failed' }; } this.envelope = { ...this.envelope, latest: quota }; if (quota.available) { this.onGood(quota); } else { this.onBad(quota.reason); } return quota; } /** * Begin polling, starting with a read right now rather than one interval from * now: a poller whose first reading lands five minutes in is no use to an agent * that just started, and the session's own measurement needs a baseline. Not * awaited — the read takes ~5s and nothing should wait on it. Idempotent. */ start() { if (this.running || this.stopped) return; this.running = true; void this.poll().finally(() => this.schedule()); } /** Stop polling. Idempotent. */ stop() { this.stopped = true; this.running = false; if (this.timer) clearTimeout(this.timer); this.timer = undefined; } onGood(quota) { this.envelope = { ...this.envelope, lastGood: quota, lastGoodAt: this.now() }; // Reset the backoff: the fetch is working again. this.currentIntervalMs = this.opts.intervalMs ?? DEFAULT_POLL_MS; } onBad(reason) { this.envelope = { ...this.envelope, lastFailureAt: this.now() }; if (!isTransientQuotaReason(reason)) { // Authoritative: no subscription, or no agent. Asking again changes // nothing, and the retained reading would misrepresent the account. this.envelope = { ...this.envelope, lastGood: undefined, lastGoodAt: undefined }; this.stop(); return; } // Transient, so keep lastGood and ask again later — but later than last time. this.currentIntervalMs = Math.min(this.currentIntervalMs * 2, this.opts.maxIntervalMs ?? MAX_POLL_MS); } schedule() { if (this.stopped) return; this.timer = setTimeout(() => { void this.poll().finally(() => { this.timer = undefined; this.schedule(); }); }, this.currentIntervalMs); // Don't hold the process open just to read a quota; the daemon's own work // decides its lifetime. Unlike the read's timeout, nothing awaits this. this.timer.unref?.(); } } //# sourceMappingURL=quota-poller.js.map