/** * Multi-account slot model shared by every addon provider. * * Deliberately mirrors the shape stock senpi uses for the Claude Agent SDK * (`accounts`, `pinned`, `blockedUntil`, `blockReason`) so that account * behaviour is identical across stock and addon providers, and so a slot * persisted by one can be reasoned about by the other. */ export type BlockReason = "rate_limit" | "auth_error" | "quota" | "server_error"; export interface AccountSlot { /** Stable, user-facing slot name (`/kiro-account pin `). */ name: string; /** Long-lived credential material. */ refresh: string; /** Short-lived credential material. */ access: string; /** Epoch millis at which `access` expires. */ expires: number; /** Where the slot came from. */ source: "login" | "import" | "env"; /** Epoch millis until which the slot must not be selected. */ blockedUntil?: number; /** Why the slot is blocked. `auth_error` blocks until re-login. */ blockReason?: BlockReason; /** Free-form provider metadata (region, profileArn, authMethod, ...). */ meta?: Record; /** * Blocks in a row without an intervening success, so the backoff in * {@link blockAccount} grows across requests instead of restarting at the * base window every time. Cleared by {@link clearFailureStreak}. */ consecutiveFailures?: number; /** Epoch millis of the most recent counted failure, for streak decay. */ lastFailureAt?: number; } export type SelectionStrategy = "fill-first" | "rotate"; /** * What may happen when a conversation can no longer use the account it is bound * to. A blocked account is a reversible detour and is never gated; this decides * only the irreversible case, where the bound account has left the pool. */ export type MigrationPolicy = "auto" | "ask" | "never"; export declare const MIGRATION_POLICIES: MigrationPolicy[]; export declare const DEFAULT_MIGRATION_POLICY: MigrationPolicy; export interface AccountPoolState { accounts: AccountSlot[]; /** Slot name pinned by the user; overrides strategy while available. */ pinned?: string; strategy?: SelectionStrategy; /** Round-robin cursor, only meaningful for `rotate`/`spread`. */ cursor?: number; /** Scheduling mode; see `affinity.ts`. */ mode?: "cache-first" | "balanced" | "spread"; /** Conversation fingerprint -> account name, preserving warm prompt caches. */ bindings?: Record; /** How to treat an irreversible move off a bound account; see {@link MigrationPolicy}. */ migration?: MigrationPolicy; } export declare const MAX_BLOCK_MS: number; export declare const DEFAULT_BLOCK_MS = 60000; export declare const FAILURE_STREAK_DECAY_MS: number; export declare function assertValidAccountName(name: string): void; /** An `auth_error` block has no expiry: it clears only on re-login. */ export declare function isBlocked(account: AccountSlot, now?: number): boolean; /** Drop expired timed blocks so the slot becomes selectable again (failback). */ export declare function clearExpiredBlocks(accounts: readonly AccountSlot[], now?: number): AccountSlot[]; export declare function addAccount(state: AccountPoolState, slot: AccountSlot): AccountPoolState; export declare function removeAccount(state: AccountPoolState, name: string): AccountPoolState; export declare function pinAccount(state: AccountPoolState, name: string): AccountPoolState; export declare function unpinAccount(state: AccountPoolState): AccountPoolState; export declare function replaceAccount(state: AccountPoolState, replacement: AccountSlot): AccountPoolState; /** * Block a slot after a failure. * * `auth_error` is permanent until re-login. Everything else uses the upstream * `Retry-After` when present, otherwise exponential backoff on the attempt * count, always capped at {@link MAX_BLOCK_MS}. */ export declare function blockAccount(account: AccountSlot, reason: BlockReason, options?: { now?: number; attempt?: number; retryAfterMs?: number; baseBlockMs?: number; }): AccountSlot; /** Count one more block against a slot, so the next backoff is longer. */ export declare function recordFailureStreak(account: AccountSlot, now?: number): AccountSlot; /** Forget the streak after a request succeeds on this slot. */ export declare function clearFailureStreak(account: AccountSlot): AccountSlot; /** Clear an `auth_error` block, e.g. after a successful re-login. */ export declare function unblockAccount(account: AccountSlot): AccountSlot; export declare class NoAvailableAccountError extends Error { /** Earliest epoch-millis at which some slot unblocks, when one exists. */ readonly retryAt?: number; constructor(message: string, retryAt?: number); } export interface SelectOptions { now?: number; /** Strategy override; defaults to the pool's own, then `fill-first`. */ strategy?: SelectionStrategy; } export interface Selection { account: AccountSlot; /** Pool state to persist (advances the round-robin cursor). */ state: AccountPoolState; } /** * Pick the account to use for the next request. * * Order of precedence: * 1. the pinned slot, when it is not blocked (explicit user intent wins); * 2. the configured strategy over the unblocked slots: * - `fill-first`: always the first unblocked slot, so one subscription is * drained before the next is touched (matches how these subscriptions * are actually metered); * - `rotate`: round-robin, to spread load evenly. * * Timed blocks that have expired are cleared first, which is the failback path. */ export declare function selectAccount(state: AccountPoolState, options?: SelectOptions): Selection;