export interface HotSwapAccount { name: string; dir: string; } export interface SessionOutcome { kind: 'ok' | 'capped' | 'no-conversation' | 'switch' | 'needs-login'; exitCode: number; reason?: string; resetAt?: number; /** * How long the session ran. Used to refuse a cap for a session that ended too * fast to have hit one: those are startup failures, and believing them once * capped every account the operator had. */ ranMs?: number; /** * Set when ONE model ran out rather than the account. The account still works * on everything else, so it keeps its place in the rotation and the MODEL is * what changes. */ cappedModel?: string; /** * Nothing was measured: this is the hold raised to get a stuck session * moving, not a limit the account confirmed. It reports no window and makes * no claim about which model is spent, so callers must not record one. */ unproven?: true; /** For kind 'switch': the account the operator asked to switch to, in place. */ switchTo?: string; } export interface HotSwapDeps { /** Next healthy account, excluding the given capped names; null when none remain. */ nextAccount: (excluding: Set) => HotSwapAccount | null; /** Resolve a specific account by name (for an operator-requested switch); null if unusable. */ resolveAccount: (name: string) => HotSwapAccount | null; /** The account the session is actually on now (it may have moved via a seamless swap). */ currentAccount?: () => string; /** * Run one claude session (the real impl runs it inside a PTY). `isContinue` * resumes the same conversation, by id, after a swap. */ runSession: (account: HotSwapAccount, isContinue: boolean, options?: { ignoreLimits?: boolean; }) => Promise; /** Persist a cap so other sessions avoid the account too. */ markCapped: (account: string, reason: string, resetAt: number | undefined) => void; /** ccx status messages (never stdout, and never into Claude's screen). */ notify: (message: string) => void; /** * Somewhere to run when every account has hit a limit. * * Returning null here means REFUSING TO START CLAUDE AT ALL, which is the * worst thing this tool can do and should be reserved for having nothing to * launch. ccx is not the authority on whether the server will serve a * request: usage credits, a limit recorded against the wrong account, or a * window that reset early all make it wrong in the direction of refusing work * that would have succeeded. See src/session/last-resort.ts. */ lastResort?: (excluding: Set) => { account: HotSwapAccount; message: string; } | null; /** * Accounts already known to have a rejected login, so they are skipped without * being launched first. * * Seeded into the same set that a rejection discovered at runtime goes into, * rather than filtered out inside `nextAccount`. That is what keeps the ending * honest: the closing message reads this set to decide between "wait for a * reset" and "sign in again", so an account that is skipped silently would * leave the operator waiting for a reset that cannot fix a sign-in. */ knownDeadAccounts?: () => string[]; /** * Accounts with no stored login at all. * * They are already invisible to selection, so this exists purely so the * ending can tell the truth about them. Without it they are silently absent * and the operator is told to wait for a reset, which never produces a login. * Kept apart from the refused ones because "sign in again" is wrong for an * account that has never been signed in. */ accountsNeverSignedIn?: () => string[]; /** * Accounts that were ALREADY out of room when this run started, from the * ledger. * * The running set only collects accounts that hit a limit during this * session, so without this the ending names none of the accounts that were * capped before it began. That is how an operator with two capped accounts * and two dead logins got told only about the sign-ins, leaving the actual * reason there was nothing to run on unmentioned. * * Used for the ENDING ONLY, never for selection: `nextAccount` is the * authority on what can run, and excluding accounts here as well would let a * stale ledger entry veto an account that has since reset. */ knownCappedAccounts?: () => string[]; /** * The last word, said when there is nothing left to run on and ccx is about * to exit. * * Deliberately NOT `notify`. That channel draws nothing on purpose, because * Claude owns the screen while a session is running and writing there would * scribble over it. Nothing owns the screen once the loop has run out of * accounts, and sending the ending down the silent channel is what left the * operator staring at a blank prompt, running the same command three times, * with the explanation written only to a log file. */ report: (message: string) => void; } /** * Drive an interactive session with transparent hot-swap: run on a healthy * account, and each time it caps, swap to the next healthy account and resume * the SAME conversation, resumed by id, in place. Returns the exit code of the * session that ended normally, or 1 if every account is capped. * * This is the pure orchestration; the PTY I/O and account credential wiring are * injected via `runSession`, so the swap logic is fully testable. */ export declare function runHotSwapSession(deps: HotSwapDeps): Promise;