import { type LimitProbeResult } from './limit-probe.js'; /** * Deciding whether limit-looking text has actually earned a cap, and how wide. * * Writing a cap is expensive to get wrong: an account-wide cap takes an account * out of rotation for hours, and enough of them make ccx refuse to start at all. * So the bar is evidence, and the evidence has to be about the RIGHT account. * * Two ways this went wrong in production, both on the same day: * * The interactive path asked the SHARED session credential, so with two runs * rotating at once an exhausted account answered for a healthy one. Five * accounts were capped inside 87 seconds, one of them with 97% of its * five-hour window still free. * * The headless path asked nothing at all. Any output matching the cap patterns * became an account-wide cap, so a Fable-only limit took the whole account out * even though it still ran on every other model. * * The scope matters as much as the verdict: a model-scoped cap leaves the * account usable on other models, which is the difference between "switch to * Opus and carry on" and "everything is capped, come back in five hours". */ export interface CapDecision { /** Write a cap only when this is true. */ limited: boolean; /** Set when ONE model is out. The account still works on the others. */ model?: string; /** When the window that is actually spent comes back. */ resetAt?: number; /** For the log, so a refusal can be understood after the fact. */ detail?: string; /** * The probe said limited and ccx could not account for it in any window it * can read. Not a cap, and never treated as one here, but the one refusal * that can be WRONG about a limit that is really happening: everything else * this file refuses is a case where the API positively reported room. */ unverified?: true; } export interface ConfirmCapDeps { /** Injected in tests; the real one is a plain GET that costs no tokens. */ probe?: (credentialsFile: string, renderedText: string) => Promise; /** Used only before an account is chosen, when nothing can be capped yet. */ sessionCredentials?: string; /** * The model the session is actually running. * * A spent window for a model you are NOT using is not a limit on you. The * session moves to Opus, the account's Fable stays at 100% forever, and any * limit-looking text (a resumed conversation replays the old cap message * every time) re-confirmed that spent Fable and ended the session. Measured: * ten rotations in six minutes, each session lasting under twenty seconds. * * Unknown keeps the old behaviour, since a session with nothing pinned is * running the default model, which is the one a spent window most likely * refers to. */ modelInUse?: string | null; } /** * Ask the account's own credential whether it is really out, and how widely. * * Refuses on anything short of a confirmed limit. An unreachable endpoint, a * missing token, a 429: all of those mean "not proven", and not proven must * never become a cap. A session that keeps hitting a real limit will trigger * this again; a healthy account wrongly capped stays broken for hours. */ export declare function confirmCap(accountDir: string | null | undefined, renderedText: string, deps?: ConfirmCapDeps): Promise; /** A cap decision that also says WHOSE credential answered. */ export interface SessionCapDecision extends CapDecision { askedOf: 'session' | 'profile'; } /** * Confirm a limit for an INTERACTIVE session, asking the session's own login. * * The old rule here was the profile's credential, "the account we are about to * cap", written when every session shared one directory and the session * credential could belong to any concurrent run. One directory per session * inverted that: the session's login is now exactly the identity that rendered * the limit banner on screen, and the profile is the guess. Asking the profile * is how a session signed in as somebody else (a mid-session /login) deadlocked * for hours: the actual account's banner on screen, the believed account * answering "not capped", and the switch never came. * * The caller resolves WHO the session credential belongs to (session-identity) * and records the cap against that account. This function only answers "is the * login this session is running on out of room, and how widely". * * Falls back to the believed profile when the session has no usable login of * its own, which is the one case where the profile is the better guess. */ export declare function confirmSessionCap(input: { sessionDir: string; believedDir: string | null; }, renderedText: string, deps?: ConfirmCapDeps): Promise;