/**
 * ModelSwapService — §5.3 of docs/specs/FABLE-MODEL-ESCALATION-SPEC.md.
 *
 * The narrow, server-side mid-session model swap (Trigger #1 only). The
 * reconciler hook only SIGNALS — this service is the single authority that
 * performs a swap, and it verifies every swap with an independent oracle.
 *
 * Contract (each clause spec-cited):
 *  - `:name` resolves by EXACT match against the live session registry —
 *    never globbed, substring-matched, or concatenated into the tmux target
 *    (round-2 Security-N1). The only string ever sent to the pane is the
 *    server-derived `/model <validated-id>`.
 *  - The model id is derived server-side via the §5.1 resolver — NEVER
 *    accepted from the caller (Sec-F5).
 *  - Refuses protected sessions (authorization boundary) and sessions that
 *    are not idle-with-empty-input (round-2 Security-F6 live-input
 *    collision window).
 *  - Honors `enabled:false` / `dryRun:true`; dryRun evaluates every gate
 *    but acquires nothing and injects nothing.
 *  - Injects via tmux `send-keys -l --` + a SEPARATE Enter (SessionManager
 *    .sendInput — the same hardened primitive the input route uses).
 *  - **Canary read-back via an oracle independent of the Session.model
 *    field this swap would write** (round-3 Integration-NEW-1): the pane's
 *    own acknowledgment of the /model command, parsed to exclude the echo
 *    of our injected input. Only on a confirmed match is Session.model
 *    updated. Unconfirmed ⇒ Session.model untouched, behaviourally default,
 *    and a SILENT maturation-track audit breadcrumb — never an Attention
 *    item (TOPIC-PROFILE-SPEC §11/§14: the `maturing-feature-health-no-alerts`
 *    operator directive, 2026-06-10, superseding round-1 Adversarial-H5 /
 *    Lessons-C3's per-event item, which produced three "Model swap
 *    unconfirmed" Attention topics in one day).
 *  - **Accounting fails toward counting** (round-2 Adversarial-NEW-2): the
 *    escalation is counted at injection, before the canary, once per
 *    (instance, transition) episode.
 *  - Server-side dwell guard: never swaps the same session twice within
 *    `minTierDwellMs` (§5.5 hysteresis backstop; suppressed flaps audited).
 */
import type { Session } from './types.js';
import { type EscalationTier, type TierEscalationConfig } from './ModelTierEscalation.js';
import type { EscalationGovernor } from './EscalationGovernor.js';
/** The narrow SessionManager surface the swap service is allowed to touch. */
export interface SwapSessionFacade {
    listRunningSessions(): Session[];
    captureMeaningfulTail(tmuxSession: string, lines: number): string | null;
    sendInput(tmuxSession: string, input: string): boolean;
}
export interface ModelSwapServiceDeps {
    stateDir: string;
    sessions: SwapSessionFacade;
    /** Persist a mutated Session record (StateManager.saveSession). */
    saveSession: (session: Session) => void;
    protectedSessions: () => string[];
    getConfig: () => TierEscalationConfig;
    governor: Pick<EscalationGovernor, 'admitEscalation' | 'recordInjection'>;
    /**
     * TOPIC-PROFILE-SPEC §9 — the server-side pin consult. Given a session,
     * return the session's topic profile consult, or null when the session has
     * no bound topic / no pin. The consult runs on the AUTHORITY side (this
     * endpoint), never in the FABLE hook (its pure-filesystem contract holds):
     *  - `suppressEscalation`: the topic's `escalationOverride === 'suppress'`
     *    pin — an 'escalated' swap is REFUSED (the operator explicitly opted
     *    this topic out of the heavy-work ultra mandate). `inherit` pins never
     *    set this (the mandate keeps firing even on a pinned topic).
     *  - `baselineModel`: the topic's pinned BASELINE (explicit model or
     *    resolved tier pin) — a 'default' de-escalation lands HERE, never the
     *    global default (otherwise the swap-back silently drops the pin).
     *    Already clamped through the §10.2 closed enum by the resolver.
     * NOT gated by `topicProfiles.enabled` — honor-on-read covers the
     * escalation arm (§5.2(c): disabling the feature must not silently flip a
     * "never escalate this topic" into 2x-cost escalation).
     */
    topicProfileConsult?: (session: Session) => {
        suppressEscalation: boolean;
        baselineModel: string | null;
    } | null;
    /** Canary attempts/cadence — injectable for tests. */
    canaryAttempts?: number;
    canaryIntervalMs?: number;
    wait?: (ms: number) => Promise<void>;
    now?: () => number;
}
export type SwapStatus = 'swapped' | 'unconfirmed' | 'dry-run' | 'noop' | 'refused';
export interface SwapResult {
    status: SwapStatus;
    reason?: string;
    /** The server-derived target model id (when one resolved). */
    model?: string;
    /** True only when the independent oracle confirmed the live model. */
    confirmed?: boolean;
}
/**
 * Does the pane tail show a prompt-ready idle state WITH an empty input
 * line? Fail-closed: anything ambiguous returns false and the swap is
 * refused as retryable (round-2 Security-F6).
 */
export declare function paneIdleWithEmptyInput(tail: string | null): boolean;
/**
 * Independent-oracle parse (§5.3 canary): does the pane acknowledge that
 * the model is now `modelId`, on a line that is NOT the echo of our own
 * injected `/model …` input? Conservative by design: an unrecognized format
 * reads as NOT confirmed (the spec's honest-degrade direction).
 *
 * The REAL CLI acks with the model's DISPLAY NAME, not the id (live-canary
 * finding, 2026-06-09): `/model claude-fable-5` → "Set model to Fable 5 and
 * saved as your default for new sessions". We accept the exact id OR the
 * display form derived from the id (closed-enum ids only ever reach here,
 * so the derivation is over a known, validated vocabulary): family token
 * capitalized + up to two leading version components dot-joined —
 * claude-fable-5 → "Fable 5", claude-opus-4-8 → "Opus 4.8".
 */
export declare function paneConfirmsModel(tail: string | null, modelId: string): boolean;
export declare class ModelSwapService {
    private readonly deps;
    private readonly auditPath;
    private readonly now;
    private readonly wait;
    /** In-memory per-session last-swap timestamps (dwell backstop). Reset on
     *  restart — the safe direction (a restart may swap immediately). */
    private readonly lastSwapAt;
    constructor(deps: ModelSwapServiceDeps);
    /**
     * Swap `name`'s session to `tier`. Body-validated upstream; this method
     * re-validates everything it relies on (defense in depth).
     */
    swap(name: string, tier: EscalationTier): Promise<SwapResult>;
    private refuse;
    /** Structured-fields-only audit appender (Sec-F7) — same trail as the
     *  governor so one file tells the whole escalation story. */
    private audit;
}
//# sourceMappingURL=ModelSwapService.d.ts.map