/** * Session pinner — FR-006, FR-007, FR-008, FR-024. * * Pins a session to one model after the initial routing decision. * Pin holds across subsequent turns until a qualified break event. * * Break rules (FR-008, exhaustive): * 1. History compaction * 2. Context overflow (estimated tokens exceed pinned model window) * 3. Explicit operator/user override * 4. Qualified loop escalation (threshold managed externally) * 5. Cache-warmup economics when a cross-provider switch is proposed * * Sub-routing (FR-024): tool-result turns below the payload threshold * may use an economical model on the same provider without breaking the pin. */ import type { ModelProfile, PinReason, PriceCatalog, RoutingRequest, SaarConfig, SaarSessionState, SessionPin } from '../types/index.js'; import type { QuotaWindowPosition } from '../types/entities.js'; import { type VirtualCostV2Config } from '../types/schemas.js'; import type { StorePort } from '../types/store-port.js'; import { type CacheEconomicsConfig } from './cache-economics.js'; import { type CacheBreakevenResult } from './cache-breakeven.js'; import { FlipFlopGuard, type FlipFlopObservation, type FlipFlopSessionState } from './flip-flop-guard.js'; export type PinAction = 'use_pin' | 'sub_route' | 'saar_route' | 'break' | 'no_pin' | 'force_rejected'; export type PinSaarReason = 'saar_buffer_active' | 'saar_hard_lock' | 'saar_idle_reopen' | 'saar_tier_upgrade'; export type PinFlipFlopReason = 'flip_flop_tier_pinned'; /** * SP-209 / #121 — force_model_id could not be honored. Surfaced as the routing * decision `reason_code` so explain / `SMART_ROUTER_LOG_ROUTING=1` makes the * rejection explicit instead of silently remapping across provider families. */ export declare const FORCE_REJECTED_NOT_IN_FLEET: "force_rejected_not_in_fleet"; export declare const FORCE_REJECTED_UNHEALTHY: "force_rejected_unhealthy"; export interface PinLookupResult { readonly action: PinAction; readonly pinnedModel?: ModelProfile; readonly subRouteModel?: ModelProfile; readonly saarRouteModel?: ModelProfile; readonly saarReason?: PinSaarReason; readonly flipFlopReason?: PinFlipFlopReason; readonly breakReason?: PinReason; /** SP-209 / #121: rejection reason code when force_model_id cannot be honored. */ readonly forceRejectionReason?: string; /** SP-209 / #121: the force_model_id value that was rejected. */ readonly forceModelId?: string; } export interface SessionPinnerConfig { /** FR-024: max payload size (bytes or token estimate) for sub-routing. Default 2048. */ readonly toolResultSizeThreshold?: number; /** Optional persistence — pins survive process restart when set. */ readonly store?: StorePort; /** * FR-008 rule #4: cache-warmup economics thresholds. */ readonly cacheEconomicsConfig?: CacheEconomicsConfig; /** SAAR pin policy (SP-122). When omitted, SAAR behavior is disabled. */ readonly saarConfig?: SaarConfig; /** Injectable clock for SAAR idle-timeout tests. */ readonly saarClock?: () => number; /** * Emergency pin-only fallback (#83, SP-161). When true, warm sessions always * return use_pin after break rules — SAAR sub-routes and tier upgrades are skipped. */ readonly pinOnlyFallback?: boolean; /** * Break pin when estimated input tokens exceed the pinned model's * max_input_tokens multiplied by this margin. Default 0.90. */ readonly contextOverflowSafetyMargin?: number; /** Flip-flop tier pin guard (SP-155, #82). Injectable for tests. */ readonly flipFlopGuard?: FlipFlopGuard; } /** Virtual cost v2 context for subscription-aware breakeven (SP-149). */ export interface ModelSwitchBreakevenContext { readonly priceCatalog?: PriceCatalog | null; readonly quotaWindowPosition?: QuotaWindowPosition; readonly virtualCostV2Config?: VirtualCostV2Config; } /** Breakeven result with v2 economics observability (SP-149). */ export interface ModelSwitchBreakevenResult extends CacheBreakevenResult { readonly quota_premium_usd: number; readonly kv_cache_credit_usd: number; } /** * Per-turn marginal savings from switching pinned → candidate on this request. * Uses SP-148 virtual cost v2 when context is provided (SP-149). */ export declare function computeMarginalSwitchSavings(pinnedModel: ModelProfile, candidateModel: ModelProfile, estimatedInputTokens: number, context?: ModelSwitchBreakevenContext): number; /** * SAAR cache breakeven gate (#73) — shared by turn_envelope and session_pin. * `warmPrefixTokens` is 0 on cold sessions (no pin); otherwise the warm prefix size. * Composes KV-cache savings credit into total benefit when v2 context is set (SP-149). */ export declare function evaluateModelSwitchBreakeven(pinnedModel: ModelProfile, candidateModel: ModelProfile, estimatedInputTokens: number, warmPrefixTokens: number, saarConfig?: SaarConfig, breakevenContext?: ModelSwitchBreakevenContext): ModelSwitchBreakevenResult; export declare class SessionPinner { private readonly pins; private readonly saarTrackers; private readonly flipFlopGuard; private readonly toolResultSizeThreshold; private readonly store; private readonly cacheEconomicsConfig; private readonly saarConfig; private readonly saarClock; private readonly pinOnlyFallback; private readonly contextOverflowSafetyMargin; private lastFleet; private lastFlipFlopObservation; constructor(config?: SessionPinnerConfig); /** * Hydrate in-memory pin state for a session from persistent storage. * Call on session start after a pi restart so lookupPin stays synchronous. */ restoreSessionPin(sessionId: string): Promise; /** * Synchronous pin lookup — must complete in <1ms. * All data is in-memory (Map); no I/O. */ lookupPin(request: RoutingRequest, fleet: readonly ModelProfile[]): PinLookupResult; /** * Create or update a session pin after a routing decision. */ recordPin(sessionId: string, modelId: string, reason: PinReason): SessionPin; /** * Delete a session pin — used by loop escalation or external callers. */ breakPin(sessionId: string): void; /** * Record a completed turn for SAAR turn-index and hard-lock tracking. * Pipeline callers invoke after routing (SP-123). */ recordSaarTurn(sessionId: string): SaarSessionState | null; /** Read-only SAAR runtime state for telemetry (SP-126). */ getSaarState(sessionId: string): SaarSessionState | null; /** * Hydrate a pin from persistent storage (e.g. SQLite restore). */ loadPin(pin: SessionPin): void; /** * Read-only access to the current pin (telemetry, inspection). */ getPin(sessionId: string): SessionPin | null; /** Read-only flip-flop guard state for telemetry (SP-155). */ getFlipFlopState(sessionId: string): FlipFlopSessionState | null; /** Last shadow observation from the most recent lookupPin call (SP-155). */ getLastFlipFlopObservation(): FlipFlopObservation | null; private observeShadowTier; private resolveShadowTier; private resolveFlipFlopPin; private isFlipFlopTierChangeBlocked; private getOrCreateSaarTracker; private evaluateSaarPolicy; private evaluateBreakRules; private evaluateContextOverflowBreak; private evaluateCacheEconomicsBreak; /** * SP-209 / #121: resolve an explicit force_model_id override. * * Healthy in-fleet target → pin to it (`use_pin`). Unavailable target → fail * closed (`force_rejected`) carrying an explicit reason code so the pipeline * can surface the rejection in explain / SMART_ROUTER_LOG_ROUTING instead of * silently remapping to a different provider family. A rejected force is a * no-op on pin state — the existing pin (if any) is preserved. */ private resolveForceOverride; private persistPin; private deletePersistedPin; private evaluateSubRouting; } //# sourceMappingURL=session-pinner.d.ts.map