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.

169 lines 8.24 kB
/** * The quota boundary (#879): how much of the account's week The Framework may * have spent by now. * * The whole policy is one line — the boundary is the pro-rated share of the week's allowance that * has elapsed, rising continuously with the clock rather than once a day (#960 Edit) — and it * replaces the configurable limits of #519. There is nothing to configure: the boundary is derived * from the account's own week, which the agent reports. * * Two properties fall out of it, and they are the point: * - Nothing is left on the floor. The boundary rises on its own and reaches the full allowance * exactly as the week resets, so a quiet week still gets spent rather than expiring. * - Low-priority work cannot starve high-priority work. Work the user asks for * borrows against the days still to come; unattended work stands down once it * passes the boundary, by default a half-day cushion beyond it (#960 Edit) — * see {@link QuotaLimit}. */ /** The quota week, in ms. */ export const QUOTA_WEEK_MS = 7 * 24 * 60 * 60 * 1000; /** A day of it, in ms. */ const ONE_DAY_MS = 24 * 60 * 60 * 1000; /** Days in the quota week, i.e. the denominator of `n/7`. */ const WEEK_DAYS = 7; const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; /** * `Jul 25 at 7am (Asia/Jerusalem)`, with the minutes and the zone both optional. Newer Claude * Code prints a comma where older versions print `at` — `Jul 25, 7am` — and both are in the wild. */ const RESETS_AT = /^([a-z]{3})\s+(\d{1,2})(?:\s+at\s+|,\s*)(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*(?:\(([^)]+)\))?$/i; /** How far `zone` is ahead of UTC at `at`, in ms. */ function zoneOffsetMs(at, zone) { const parts = new Intl.DateTimeFormat('en-US', { timeZone: zone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }).formatToParts(at); const read = (type) => Number(parts.find(p => p.type === type)?.value); // `hour: '2-digit'` with hour12 off prints midnight as 24 in some runtimes. const hour = read('hour') % 24; const asUtc = Date.UTC(read('year'), read('month') - 1, read('day'), hour, read('minute'), read('second')); return asUtc - at; } /** The epoch of a wall-clock time in `zone`, resolving the offset at that instant rather than now. */ function zonedTimeToEpoch(year, month, day, hour, minute, zone) { const wall = Date.UTC(year, month - 1, day, hour, minute); const first = wall - zoneOffsetMs(wall, zone); // One correction settles it: the first guess is only wrong when it landed on // the far side of a DST change, and the second offset is the right one. const second = wall - zoneOffsetMs(first, zone); return second; } /** * Parse the agent's reset prose into an epoch. * * The agent prints no year (`Jul 25 at 7am (Asia/Jerusalem)`), which is why the * driver keeps this as text. It is recoverable here because we know something * the driver does not: a *weekly* window resets within seven days, so of the * candidate years exactly one lands anywhere near now. * * `undefined` for anything that does not parse, which the callers treat as "we * do not know where the week is" rather than as a boundary of zero. */ export function parseResetsAt(text, now) { const match = RESETS_AT.exec(text.trim()); if (!match) return undefined; const [, monthName, dayText, hourText, minuteText, meridiem, zoneText] = match; const month = MONTHS.indexOf((monthName ?? '').toLowerCase()) + 1; if (month === 0) return undefined; const day = Number(dayText); const hour12 = Number(hourText); if (hour12 < 1 || hour12 > 12) return undefined; const hour = (hour12 % 12) + ((meridiem ?? '').toLowerCase() === 'pm' ? 12 : 0); const minute = minuteText === undefined ? 0 : Number(minuteText); const zone = zoneText?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone; const nowYear = new Date(now).getUTCFullYear(); let best; for (const year of [nowYear - 1, nowYear, nowYear + 1]) { let at; try { at = zonedTimeToEpoch(year, month, day, hour, minute, zone); } catch { // An unknown zone name. Nothing to fall back to that wouldn't be a guess. return undefined; } // Feb 29 in a non-leap year rolls into March; that candidate isn't the date // the agent printed. if (new Date(at + zoneOffsetMs(at, zone)).getUTCDate() !== day) continue; if (best === undefined || Math.abs(at - now) < Math.abs(best - now)) best = at; } return best; } /** * Where the boundary sits, given when the week resets. * * `percent` is continuous — the plain elapsed share of the week (#960 Edit) — rather than a value * that jumps once a day, so it always names the actual instant `now` falls on, on any axis that * measures the week the same way. A stepped version once unlocked a whole day's allowance the * moment a new day began (including the entire week's worth on the last day), which read as * generous on paper but let a burst of spending land the instant the clock ticked over rather than * pacing with it; continuous keeps the line honest about what has actually elapsed at the cost of * that burst. * * `day` is 1-based and still names which day of the week `now` falls on — for callers that want to * say "day 4 of 7" rather than a percentage — and steps at the exact second the week's own day * rolls over, independently of `percent`. */ export function boundaryFromResetsAt(resetsAt, now) { const startsAt = resetsAt - QUOTA_WEEK_MS; const elapsedMs = Math.min(Math.max(now - startsAt, 0), QUOTA_WEEK_MS); const day = Math.min(WEEK_DAYS, Math.floor(elapsedMs / ONE_DAY_MS) + 1); return { startsAt, resetsAt, day, percent: (elapsedMs / QUOTA_WEEK_MS) * 100 }; } /** The model name a `week-model` window is about, e.g. `Current week (Fable)` -> `fable`. */ function windowModel(label) { return /\(([^)]+)\)/.exec(label)?.[1]?.trim().toLowerCase(); } /** * Measure the account's windows against the boundary (#879). * * Both weekly windows bind at once — the account's week and, per Rom's edit, the * selected model's own week — so each is measured against the same boundary and * whichever reaches it first is the one that stops the work. The model's window * is only included when we can tell which model it belongs to; an unrecognized * one is left out rather than allowed to stop work for a model nobody selected. * * `undefined` when there is no reading, or when the week's reset cannot be * placed. That is "we do not know", and each caller decides what to do with it: * the per-agent guard carries on, unattended work stands down. */ export function quotaBoundaryStatus(input) { const week = input.windows.find(w => w.kind === 'week'); if (!week?.resetsAtText) return undefined; const resetsAt = parseResetsAt(week.resetsAtText, input.now); if (resetsAt === undefined) return undefined; const boundary = boundaryFromResetsAt(resetsAt, input.now); const model = input.model?.toLowerCase(); const inForce = input.windows.filter(w => { if (w.kind === 'week') return true; if (w.kind !== 'week-model' || !model) return false; const name = windowModel(w.label); return name !== undefined && model.includes(name); }); // Clamped, so a limit dragged past either end of the week stops at the week rather than // becoming unreachable (which would read as "never stop") or negative (as "always stopped"). const offset = input.limitOffset ?? 0; const limit = { percent: Math.min(Math.max(boundary.percent + offset, 0), 100), offset }; const windows = inForce.map(w => ({ label: w.label, percentUsed: w.percentUsed, reached: w.percentUsed >= limit.percent, })); return { boundary, limit, windows, reached: windows.find(w => w.reached) ?? null }; } //# sourceMappingURL=quota-boundary.js.map