/** * Single-flight AI-credential re-pull that also repoints a **running** session * at a different AI provider config (seat) when the platform hands one back. * * Three independent triggers converge here — a `credentialRotated` broadcast * naming a new seat, any mint result that comes back pinned to a different * seat (including the proactive scheduler's), and a driver-reported usage-limit * block. They can race, so they coalesce onto one in-flight re-pull. * * Extracted from `serve.ts` so the restash / restart / coalescing properties * are directly testable: `serve.ts` is one 4600-line closure. * * Spec: skaile-ai/workspaces#567, skaile-ai/platform#2976. * @docLink packages/runner/dev-guide#ai-credential-mediation */ import type { CredentialMint, CredentialRejection } from "@skaile/workspaces/types"; /** Result of one re-pull, plus whether it moved the session to another seat. */ export interface AiProviderSwitchOutcome { /** The mint as returned by the host (already persisted on success). */ mint: CredentialMint; /** True when the session was repointed at a DIFFERENT AI provider config. */ switched: boolean; /** The config id in effect after this re-pull. */ configId?: string; } /** Collaborators the switcher drives. All injected so tests need no session. */ export interface AiProviderConfigSwitcherDeps { /** * Re-pull the AI credential through the host mediator, rewriting * `.credentials.json` on success. `serve.ts` passes * `mintAndPersistAiCredential`. */ mint: (args: { configId?: string; reason: "refresh" | "retry-401"; rejection?: CredentialRejection; }) => Promise; /** The AI provider config the session is currently pinned to. */ getConfigId: () => string | undefined; /** Repoint the session at `configId` (runner stash + live session config). */ setConfigId: (configId: string) => void; /** * Queue a driver restart on the serialized restart chain. MUST be * fire-and-forget — see {@link createAiProviderConfigSwitcher}. */ scheduleRestart: (reason: string) => void; log: (line: string) => void; /** * Warn-level sink. A seat switch is the kind of event that has to be * reconstructible from a production log, where the floor is WARN * (skaile-ai/platform#3540). */ logWarn: (line: string) => void; } /** The coalescing re-pull entry point. */ export interface AiProviderConfigSwitcher { repull(args: { /** Short label naming the trigger, for the log line. */ trigger: string; /** Seat to target; defaults to the currently pinned one. */ configId?: string; /** Mediation reason forwarded to `host.refresh_credential`. */ reason?: "refresh" | "retry-401"; /** * Which rejection drove a `retry-401`. Only set by a caller that actually * classified the failure; it also ranks the coalescing (see * {@link createAiProviderConfigSwitcher}). */ rejection?: CredentialRejection; }): Promise; /** * Apply the seat named by an ALREADY-completed mint. For the reactive 401 * path, whose bounded transient-failure retry wraps the mint itself and so * cannot ride {@link repull}. * * This is the path that self-heals with no broadcast at all: the platform * re-resolves a parked seat during an ordinary refresh and simply mints for a * peer, and the difference in the result is the only notification there is. */ applyMint(mint: CredentialMint, args: { trigger: string; requestedConfigId?: string; }): AiProviderSwitchOutcome; } /** * Build the switcher. * * **The driver restart is scheduled, never awaited.** A restart runs through * the driver swap gate, which waits for the active turn to settle — and the * limit-block trigger is invoked from *inside* that turn. Awaiting it here * would deadlock the very failover it is meant to complete. The driver owns the * urgent half anyway: it drops its query and respawns the CLI on the replay, * which is what actually clears a resident CLI's cached limit state. The queued * restart only brings the live driver's own `aiProviderConfigId` up to date, * and can safely land after the turn. * * **Coalescing covers the re-pull only,** for the same reason: if the shared * promise included the restart, a broadcast-triggered switch parked on the * active turn would be awaited by a limit failover running inside that turn. */ export declare function createAiProviderConfigSwitcher(deps: AiProviderConfigSwitcherDeps): AiProviderConfigSwitcher; //# sourceMappingURL=ai-provider-switch.d.ts.map