/** * runtime-controls — the PURE session-keyed runtime-control state slice for the web * product (saga vr7kgx.6). * * The selected session's runtime exposes discovered **model** and **mode** controls. Their * authority is the wire: daemon-authored `sync` carries the runtime's secret-free * `capabilities` + current `mode`; `list_models`/`list_modes` return catalogs + the active * selection; `set_model`/`set_mode` mutate; `model_changed`/`mode_changed` broadcasts converge * multi-client state. The durable **session id** is the public key; the private `agent_id` * route handle lives only in `WebConnection` and never appears here. * * This module owns the PURE data projection: every reducer is a deterministic function of a * `RuntimeControlsMap` snapshot + plain inputs, returning a new snapshot. It performs no I/O, * holds no sockets, reads no browser globals, and is fully testable in isolation. The * imperative `WebConnection` (in `./connection`) drives it: it issues correlated * `list_models`/`list_modes`/`set_model`/`set_mode` and feeds the responses here; generation + * route-epoch + request-sequence fencing keep late/stale responses from mutating the wrong * session. * * Boundary: imports only TYPES from `@/contracts/model`. No runtime dep on the websocket * client; no browser globals. */ import type { ModelDescriptor } from '@/contracts/model'; import type { ReasoningLevelDescriptor } from '@/contracts/reasoning'; import { CAP_STREAMING_DELTAS, CAP_MODEL_CATALOG, CAP_MODEL_SWITCH, CAP_REASONING_CATALOG, CAP_REASONING_SWITCH, CAP_MODE_CATALOG, CAP_MODE_SWITCH, KNOWN_CAPABILITIES, } from '@/protocol/capabilities'; // Re-export the secret-free descriptor so the connection/UI have one import surface. export type { ModelDescriptor }; export { isKnownCapability } from '@/protocol/capabilities'; // ── capabilities ────────────────────────────────────────────────────────────────── /** The known runtime capabilities a `sync` may advertise (PROTOCOL §9.1). */ export const RUNTIME_CAPABILITIES = KNOWN_CAPABILITIES; export type RuntimeCapability = (typeof RUNTIME_CAPABILITIES)[number]; /** Stable references for the known capability strings (ergonomic web use). */ export const CAPABILITY = { streamingDeltas: CAP_STREAMING_DELTAS, modelCatalog: CAP_MODEL_CATALOG, modelSwitch: CAP_MODEL_SWITCH, reasoningCatalog: CAP_REASONING_CATALOG, reasoningSwitch: CAP_REASONING_SWITCH, modeCatalog: CAP_MODE_CATALOG, modeSwitch: CAP_MODE_SWITCH, } as const; // ── wire-safe secret-free descriptors ────────────────────────────────────────────── /** Strict parser for a secret-free `ModelDescriptor`. Returns `undefined` when malformed. */ export function parseModelDescriptor(v: unknown): ModelDescriptor | undefined { if (!isRecord(v)) return undefined; if (typeof v.id !== 'string' || v.id.length === 0) return undefined; if (typeof v.provider !== 'string' || v.provider.length === 0) return undefined; if (typeof v.model !== 'string' || v.model.length === 0) return undefined; if (v.context_window !== undefined) { if (typeof v.context_window !== 'number' || !Number.isInteger(v.context_window) || v.context_window < 0) return undefined; } // Reject leaked secret/extra fields defensively (the contract is closed). if (Object.keys(v).some((k) => k !== 'id' && k !== 'provider' && k !== 'model' && k !== 'context_window')) return undefined; return v.context_window !== undefined ? { id: v.id, provider: v.provider, model: v.model, context_window: v.context_window } : { id: v.id, provider: v.provider, model: v.model }; } /** A mode descriptor: bounded `name` + human-facing `description`. */ export interface ModeDescriptor { readonly name: string; readonly description: string; } /** Strict parser for a secret-free `ModeDescriptor`. Returns `undefined` when malformed. * Name bound matches the canonical `ModeDescriptorSchema` (max 64, saga vr7kgx.6). */ export function parseModeDescriptor(v: unknown): ModeDescriptor | undefined { if (!isRecord(v)) return undefined; if (typeof v.name !== 'string' || v.name.length === 0 || v.name.length > 64) return undefined; if (typeof v.description !== 'string' || v.description.length === 0 || v.description.length > 512) return undefined; if (Object.keys(v).some((k) => k !== 'name' && k !== 'description')) return undefined; return { name: v.name, description: v.description }; } /** A model catalog: `models` + the active descriptor (which must be present in `models`). */ export interface ModelCatalog { readonly models: readonly ModelDescriptor[]; readonly active: ModelDescriptor; } /** Strict parser for a `list_models`/`model_changed` catalog payload. Rejects unknown * top-level fields and requires `active` to structurally equal its catalog entry (not * merely share an id) so a stale/mismatched active cannot sneak in (saga vr7kgx.6). */ export function parseModelCatalog(v: unknown): ModelCatalog | undefined { if (!isRecord(v)) return undefined; if (Object.keys(v).some((k) => k !== 'models' && k !== 'active')) return undefined; // closed top-level shape if (!Array.isArray(v.models)) return undefined; const models: ModelDescriptor[] = []; const byId = new Map(); for (const item of v.models) { const descriptor = parseModelDescriptor(item); if (descriptor === undefined) return undefined; if (byId.has(descriptor.id)) return undefined; // unique ids byId.set(descriptor.id, descriptor); models.push(descriptor); } if (models.length === 0) return undefined; const active = parseModelDescriptor(v.active); if (active === undefined) return undefined; const entry = byId.get(active.id); if (entry === undefined) return undefined; // active must be represented in the catalog if (!sameModel(entry, active)) return undefined; // active must structurally equal its catalog entry return { models, active }; } /** Authoritative selectable reasoning levels for the active model. */ export interface ReasoningCatalog { readonly model: ModelDescriptor; readonly levels: readonly ReasoningLevelDescriptor[]; readonly active: string; readonly effective: 'next_provider_call'; } export function parseReasoningCatalog(v: unknown): ReasoningCatalog | undefined { if (!isRecord(v) || Object.keys(v).some((k) => !['model', 'levels', 'active', 'effective'].includes(k))) return undefined; const model = parseModelDescriptor(v.model); if (model === undefined || !Array.isArray(v.levels) || v.levels.length === 0) return undefined; const levels: ReasoningLevelDescriptor[] = []; const ids = new Set(); for (const item of v.levels) { if (!isRecord(item) || Object.keys(item).some((k) => !['id', 'label', 'description'].includes(k))) return undefined; if (typeof item.id !== 'string' || item.id.length === 0 || ids.has(item.id)) return undefined; if (typeof item.label !== 'string' || item.label.length === 0) return undefined; if (item.description !== undefined && typeof item.description !== 'string') return undefined; ids.add(item.id); levels.push(item.description === undefined ? { id: item.id, label: item.label } : { id: item.id, label: item.label, description: item.description }); } if (typeof v.active !== 'string' || !ids.has(v.active) || v.effective !== 'next_provider_call') return undefined; return { model, levels, active: v.active, effective: v.effective }; } /** A mode catalog: `modes` (unique names) + the active name (which must occur in `modes`). */ export interface ModeCatalog { readonly modes: readonly ModeDescriptor[]; readonly active: string; } /** Strict parser for a `list_modes` catalog payload (`{ modes, active }`). Rejects unknown * top-level fields (saga vr7kgx.6). */ export function parseModeCatalog(v: unknown): ModeCatalog | undefined { if (!isRecord(v)) return undefined; if (Object.keys(v).some((k) => k !== 'modes' && k !== 'active')) return undefined; // closed top-level shape if (!Array.isArray(v.modes)) return undefined; const modes: ModeDescriptor[] = []; const names = new Set(); for (const item of v.modes) { const descriptor = parseModeDescriptor(item); if (descriptor === undefined) return undefined; if (names.has(descriptor.name)) return undefined; // unique names names.add(descriptor.name); modes.push(descriptor); } if (modes.length === 0) return undefined; if (typeof v.active !== 'string' || v.active.length === 0 || !names.has(v.active)) return undefined; return { modes, active: v.active }; } /** Strict parser for a `sync.capabilities` array. Returns `undefined` when malformed. */ export function parseCapabilities(v: unknown): string[] | undefined { if (v === undefined) return []; // optional on the wire → empty (degrade to no controls) if (!Array.isArray(v)) return undefined; const out: string[] = []; for (const item of v) { if (typeof item !== 'string' || item.length === 0 || item.length > 64) return undefined; out.push(item); } return out; } /** Strict parser for the additive `sync.runtime_epoch` (saga vr7kgx.6): a non-negative * integer the backend bumps on a same-route child-runtime replacement. Absent on older * backends → `undefined` (the browser degrades to route-epoch-only fencing). Malformed → * `undefined` so a bad value cannot poison the control state. */ export function parseRuntimeEpoch(v: unknown): number | undefined { if (v === undefined || v === null) return undefined; // absent — treat compatibly if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) return undefined; return v; } // ── per-session control state ───────────────────────────────────────────────────── /** * Per-session runtime-control state. Keyed by durable `session_id` (NOT the private route). * `routeEpoch` is the connection generation that owns the attached route; a newer epoch * (reconnect) clears stale catalogs + pending state. `runtimeEpoch` is the backend's * additive `sync.runtime_epoch` (saga vr7kgx.6): a same-route runtime replacement bumps it, * clearing old catalogs/pending and fencing stale responses even when the socket generation * is unchanged. `requestSeq` stamps the latest issued request per kind so a late response * cannot settle a superseding request. `supersededModelSeq`/`supersededModeSeq` record a * pending mutation whose correlated response was overtaken by a newer authoritative * broadcast (saga vr7kgx.6) so the stale local response cannot overwrite it. */ export interface RuntimeControlState { /** Authoritative capabilities from `sync` (secret-free; unknown strings retained for diagnostics). */ capabilities: readonly string[]; /** Authoritative active mode from `sync`/`mode_changed`. */ mode: string; /** Model catalog + active descriptor, when discovered. */ readonly models?: ModelCatalog; /** Mode catalog, when discovered. */ readonly modes?: ModeCatalog; /** Reasoning levels for the active model, when supported and discovered. */ readonly reasoning?: ReasoningCatalog; loadingModels: boolean; loadingModes: boolean; loadingReasoning?: boolean; pendingModel: boolean; pendingMode: boolean; pendingReasoning?: boolean; /** Last model operation error, when retained. */ readonly modelError?: string; /** Last mode operation error, when retained. */ readonly modeError?: string; readonly reasoningError?: string; readonly reasoningSuccessRevision?: number; /** Connection generation that owns the attached route for this state. */ readonly routeEpoch?: number; /** Authoritative child-runtime epoch from `sync.runtime_epoch` (absent on older backends). */ readonly runtimeEpoch?: number; /** Latest issued request sequence per kind (fences stale responses). */ readonly requestSeq?: { modelCatalog?: number; modeCatalog?: number; reasoningCatalog?: number; setModel?: number; setMode?: number; setReasoning?: number }; /** Monotonic local correlated `set_model` acceptance marker. Broadcasts never advance it; * the UI uses it instead of inferring success from `pendingModel` becoming false. */ readonly modelSuccessRevision?: number; /** A pending `set_model` seq overtaken by a newer `model_changed` broadcast (late response is inert). */ readonly supersededModelSeq?: number; /** A pending `set_mode` seq overtaken by a newer `mode_changed` broadcast (late response is inert). */ readonly supersededModeSeq?: number; } /** The session-keyed control-state slice of `WebConnectionState`. */ export type RuntimeControlsMap = Record; /** Does the session's current capabilities include a given capability string? */ export function hasCapability(state: RuntimeControlState | undefined, capability: string): boolean { return state !== undefined && state.capabilities.includes(capability); } // ── helpers (exactOptionalPropertyTypes-safe) ────────────────────────────────────── const isRecord = (v: unknown): v is Record => typeof v === 'object' && v !== null && !Array.isArray(v); /** Structural equality for two secret-free model descriptors (no leaked extras by parser). */ function sameModel(a: ModelDescriptor, b: ModelDescriptor): boolean { return a.id === b.id && a.provider === b.provider && a.model === b.model && a.context_window === b.context_window; } /** Strip the optional `error` field from a control state (omit-key clear). */ function withoutModelError(state: RuntimeControlState): Omit { const { modelError: _omit, ...rest } = state; return rest; } function withoutModeError(state: RuntimeControlState): Omit { const { modeError: _omit, ...rest } = state; return rest; } /** Strip the optional `supersededModelSeq` field (cleared on a fresh mutation start). */ function withoutSupersededModel(state: RuntimeControlState): RuntimeControlState { if (state.supersededModelSeq === undefined) return state; const { supersededModelSeq: _omit, ...rest } = state; return rest; } function withoutSupersededMode(state: RuntimeControlState): RuntimeControlState { if (state.supersededModeSeq === undefined) return state; const { supersededModeSeq: _omit, ...rest } = state; return rest; } /** Is a response fenced out by a stale route/runtime epoch? `issueRoute`/`issueRuntime` are * the epoch values captured when the request was issued; the current state's epochs are * authoritative. An absent `runtimeEpoch` (older backend) never fences (treat absent * compatibly, saga vr7kgx.6). Pure. */ function isStaleEpoch( state: RuntimeControlState, issueRoute: number, issueRuntime?: number, ): boolean { if (state.routeEpoch !== undefined && state.routeEpoch !== issueRoute) return true; if (issueRuntime !== undefined && state.runtimeEpoch !== undefined && state.runtimeEpoch !== issueRuntime) return true; return false; } function baseControlState(): RuntimeControlState { return { capabilities: [], mode: '', loadingModels: false, loadingModes: false, loadingReasoning: false, pendingModel: false, pendingMode: false, pendingReasoning: false, }; } /** Get/create a control state for a session (returns a new map when created). Pure. */ function controlFor(map: RuntimeControlsMap, sessionId: string): { state: RuntimeControlState; map: RuntimeControlsMap } { const existing = map[sessionId]; if (existing !== undefined) return { state: existing, map }; return { state: baseControlState(), map: { ...map, [sessionId]: baseControlState() } }; } /** Replace one session's control state immutably. */ function withControl(map: RuntimeControlsMap, sessionId: string, next: RuntimeControlState): RuntimeControlsMap { return { ...map, [sessionId]: next }; } function bumpSeq(seq: RuntimeControlState['requestSeq'], kind: 'modelCatalog' | 'modeCatalog' | 'setModel' | 'setMode'): { modelCatalog?: number; modeCatalog?: number; setModel?: number; setMode?: number } { const n = (seq?.[kind] ?? 0) + 1; return { ...seq, [kind]: n } as { modelCatalog?: number; modeCatalog?: number; setModel?: number; setMode?: number }; } // ── reducers ────────────────────────────────────────────────────────────────────── /** * Apply an authoritative `sync` to a session's control state: replace capabilities + mode. * When the route epoch changed (a fresh attach/reconnect) OR the runtime epoch changed (a * same-route runtime replacement, saga vr7kgx.6), clear stale model/mode catalogs + * pending/error state so a previous generation's choices cannot leak into the new runtime. * An absent `runtimeEpoch` (older backend) never triggers a reset — it degrades to * route-epoch-only fencing. Pure. */ export function applySyncToControls( map: RuntimeControlsMap, sessionId: string, sync: { capabilities?: string[]; mode: string; runtimeEpoch?: number }, routeEpoch: number, ): RuntimeControlsMap { const capabilities = sync.capabilities ?? []; const { state, map: withState } = controlFor(map, sessionId); const epochChanged = state.routeEpoch !== undefined && state.routeEpoch !== routeEpoch; const runtimeChanged = sync.runtimeEpoch !== undefined && state.runtimeEpoch !== undefined && state.runtimeEpoch !== sync.runtimeEpoch; const reset = epochChanged || runtimeChanged; const runtimeEpoch = sync.runtimeEpoch !== undefined ? { runtimeEpoch: sync.runtimeEpoch } : {}; // A reset clears stale catalogs + pending/error state so a previous generation/runtime's // choices cannot leak into the new runtime. A same-epoch/same-runtime sync only refreshes // capabilities + mode (an in-flight refresh may still land). const next: RuntimeControlState = reset ? { ...baseControlState(), capabilities, mode: sync.mode, routeEpoch, ...runtimeEpoch } : { ...withoutModelError(withoutModeError(state)), capabilities, mode: sync.mode, routeEpoch, ...runtimeEpoch }; return withControl(withState, sessionId, next); } /** Apply a successful `list_models` catalog, clearing the loading flag + model error. Pure. */ export function applyModelCatalog( map: RuntimeControlsMap, sessionId: string, catalog: ModelCatalog, epoch: number, seq: number, runtimeEpoch?: number, ): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch)) return map; // stale route/runtime epoch if (state.requestSeq?.modelCatalog !== undefined && state.requestSeq.modelCatalog > seq) return map; // superseded const next: RuntimeControlState = { ...withoutModelError(state), models: catalog, loadingModels: false, }; return withControl(withState, sessionId, next); } /** Apply a successful `list_modes` catalog, clearing the loading flag + mode error. Pure. */ export function applyModeCatalog( map: RuntimeControlsMap, sessionId: string, catalog: ModeCatalog, epoch: number, seq: number, runtimeEpoch?: number, ): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch)) return map; if (state.requestSeq?.modeCatalog !== undefined && state.requestSeq.modeCatalog > seq) return map; const next: RuntimeControlState = { ...withoutModeError(state), modes: catalog, loadingModes: false, }; return withControl(withState, sessionId, next); } /** Record a catalog-load failure (retain last authoritative catalog; clear loading). Pure. */ export function applyCatalogError( map: RuntimeControlsMap, sessionId: string, kind: 'model' | 'mode', message: string, epoch: number, seq: number, runtimeEpoch?: number, ): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch)) return map; const seqKey = kind === 'model' ? 'modelCatalog' : 'modeCatalog'; if (state.requestSeq?.[seqKey] !== undefined && state.requestSeq[seqKey] > seq) return map; if (kind === 'model') { const next: RuntimeControlState = { ...state, loadingModels: false, modelError: message }; return withControl(withState, sessionId, next); } const next: RuntimeControlState = { ...state, loadingModes: false, modeError: message }; return withControl(withState, sessionId, next); } /** Stamp a catalog refresh as in flight (increment the request sequence). Pure. Returns the * issue-time route + runtime epochs so the caller can fence the late response. */ export function markCatalogLoading( map: RuntimeControlsMap, sessionId: string, kind: 'model' | 'mode', epoch: number, ): { map: RuntimeControlsMap; seq: number; runtimeEpoch?: number } { const { state, map: withState } = controlFor(map, sessionId); const seqKey = kind === 'model' ? 'modelCatalog' : 'modeCatalog'; const requestSeq = bumpSeq(state.requestSeq, seqKey); const seq = requestSeq[seqKey]!; const next: RuntimeControlState = { ...state, routeEpoch: state.routeEpoch ?? epoch, requestSeq, ...(kind === 'model' ? { loadingModels: true } : { loadingModes: true }), }; return { map: withControl(withState, sessionId, next), seq, ...(state.runtimeEpoch !== undefined ? { runtimeEpoch: state.runtimeEpoch } : {}), }; } /** Begin a model mutation (pending flag + request sequence). Pure. Returns the issue-time * route + runtime epochs so the caller can fence the late response. */ export function markModelMutationStart(map: RuntimeControlsMap, sessionId: string, epoch: number): { map: RuntimeControlsMap; seq: number; runtimeEpoch?: number } { const { state, map: withState } = controlFor(map, sessionId); const requestSeq = bumpSeq(state.requestSeq, 'setModel'); const seq = requestSeq.setModel!; // A fresh mutation clears any prior broadcast-supersession marker for this kind. const next: RuntimeControlState = { ...withoutSupersededModel(withoutModelError(state)), pendingModel: true, routeEpoch: state.routeEpoch ?? epoch, requestSeq }; return { map: withControl(withState, sessionId, next), seq, ...(state.runtimeEpoch !== undefined ? { runtimeEpoch: state.runtimeEpoch } : {}), }; } /** Begin a mode mutation (pending flag + request sequence). Pure. */ export function markModeMutationStart(map: RuntimeControlsMap, sessionId: string, epoch: number): { map: RuntimeControlsMap; seq: number; runtimeEpoch?: number } { const { state, map: withState } = controlFor(map, sessionId); const requestSeq = bumpSeq(state.requestSeq, 'setMode'); const seq = requestSeq.setMode!; const next: RuntimeControlState = { ...withoutSupersededMode(withoutModeError(state)), pendingMode: true, routeEpoch: state.routeEpoch ?? epoch, requestSeq }; return { map: withControl(withState, sessionId, next), seq, ...(state.runtimeEpoch !== undefined ? { runtimeEpoch: state.runtimeEpoch } : {}), }; } /** Apply a successful `set_model` result: adopt the active descriptor, clear pending. Pure. * A response whose pending seq was overtaken by a newer `model_changed` broadcast * (`supersededModelSeq`) is inert — the authoritative broadcast must not be overwritten. */ export function applyModelMutationSuccess( map: RuntimeControlsMap, sessionId: string, active: ModelDescriptor, epoch: number, seq: number, runtimeEpoch?: number, ): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch)) return map; if (state.supersededModelSeq !== undefined && state.supersededModelSeq === seq) return map; // overtaken by a broadcast if (state.requestSeq?.setModel !== undefined && state.requestSeq.setModel > seq) return map; const existing = state.models; const models = existing !== undefined ? { models: existing.models.map((m) => (m.id === active.id ? active : m)), active } : { models: [active], active }; const next: RuntimeControlState = { ...withoutModelError(withoutSupersededModel(state)), models, pendingModel: false, modelSuccessRevision: (state.modelSuccessRevision ?? 0) + 1, }; return withControl(withState, sessionId, next); } /** Apply a successful `set_mode` result: adopt the active mode, clear pending. Pure. */ export function applyModeMutationSuccess( map: RuntimeControlsMap, sessionId: string, mode: string, epoch: number, seq: number, runtimeEpoch?: number, ): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch)) return map; if (state.supersededModeSeq !== undefined && state.supersededModeSeq === seq) return map; // overtaken by a broadcast if (state.requestSeq?.setMode !== undefined && state.requestSeq.setMode > seq) return map; const modes = state.modes !== undefined && state.modes.modes.some((m) => m.name === mode) ? { modes: state.modes.modes, active: mode } : state.modes; const next: RuntimeControlState = { ...withoutModeError(withoutSupersededMode(state)), mode, pendingMode: false, ...(modes !== undefined ? { modes } : {}) }; return withControl(withState, sessionId, next); } /** Record a mutation failure (retain last authoritative value; clear pending). Pure. A * failure whose pending seq was overtaken by a newer broadcast is inert. */ export function applyMutationError( map: RuntimeControlsMap, sessionId: string, kind: 'model' | 'mode', message: string, epoch: number, seq: number, runtimeEpoch?: number, ): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch)) return map; const seqKey = kind === 'model' ? 'setModel' : 'setMode'; if (state.requestSeq?.[seqKey] !== undefined && state.requestSeq[seqKey] > seq) return map; if (kind === 'model') { if (state.supersededModelSeq !== undefined && state.supersededModelSeq === seq) return map; // overtaken — drop the stale error const next: RuntimeControlState = { ...state, pendingModel: false, modelError: message }; return withControl(withState, sessionId, next); } if (state.supersededModeSeq !== undefined && state.supersededModeSeq === seq) return map; // overtaken — drop the stale error const next: RuntimeControlState = { ...state, pendingMode: false, modeError: message }; return withControl(withState, sessionId, next); } /** * Apply a `model_changed` broadcast to the session whose route matches. Idempotent: re- * selecting the already-active descriptor is a no-op beyond updating the active marker. * Supersession (saga vr7kgx.6): a broadcast arriving while a `set_model` mutation is pending * is authoritative + newer than the pending correlated response — record the pending seq as * superseded so the late local response cannot overwrite the broadcast. A matching * initiating-client broadcast (the client's own confirmed change) adopts the same active and * supersedes its pending response idempotently (the late response is inert, no churn). Pure. */ export function applyModelBroadcast( map: RuntimeControlsMap, sessionId: string, active: ModelDescriptor, epoch: number, capabilities?: readonly string[], ): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (state.routeEpoch !== undefined && state.routeEpoch !== epoch) return map; const existing = state.models; // Idempotent: re-selecting the already-active descriptor with no pending mutation is a // no-op (the initiating client's correlated result already settled state; a later matching // broadcast must not churn references or clobber a newer pending marker). if (existing !== undefined && existing.active.id === active.id && sameModel(existing.active, active) && !state.pendingModel && capabilities === undefined) { return map; } const models = existing !== undefined && existing.models.some((m) => m.id === active.id) ? { models: existing.models.map((m) => (m.id === active.id ? active : m)), active } : { models: [active], active }; // A pending mutation is overtaken by this authoritative broadcast — fence its late response. const supersededModelSeq = state.pendingModel ? state.requestSeq?.setModel : state.supersededModelSeq; const { reasoning: _reasoning, reasoningError: _reasoningError, ...withoutReasoning } = state; const next: RuntimeControlState = { ...withoutReasoning, ...(capabilities !== undefined ? { capabilities } : {}), models, loadingReasoning: false, pendingReasoning: false, pendingModel: false, ...(supersededModelSeq !== undefined ? { supersededModelSeq } : {}), }; return withControl(withState, sessionId, next); } /** Apply a `mode_changed` broadcast to the session whose route matches. Idempotent. Pure. * See `applyModelBroadcast` for the supersession rule (mirrored for modes, saga vr7kgx.6). */ export function applyModeBroadcast( map: RuntimeControlsMap, sessionId: string, mode: string, epoch: number, ): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (state.routeEpoch !== undefined && state.routeEpoch !== epoch) return map; // Idempotent: re-selecting the already-active mode with no pending mutation is a no-op. if (state.mode === mode && !state.pendingMode) { return map; } const modes = state.modes !== undefined && state.modes.modes.some((m) => m.name === mode) ? { modes: state.modes.modes, active: mode } : state.modes; // A pending mutation is overtaken by this authoritative broadcast — fence its late response. const supersededModeSeq = state.pendingMode ? state.requestSeq?.setMode : state.supersededModeSeq; const next: RuntimeControlState = { ...state, mode, pendingMode: false, ...(modes !== undefined ? { modes } : {}), ...(supersededModeSeq !== undefined ? { supersededModeSeq } : {}), }; return withControl(withState, sessionId, next); } export function markReasoningCatalogLoading(map: RuntimeControlsMap, sessionId: string, epoch: number): { map: RuntimeControlsMap; seq: number; runtimeEpoch?: number } { const { state, map: withState } = controlFor(map, sessionId); const seq = (state.requestSeq?.reasoningCatalog ?? 0) + 1; const { reasoningError: _omit, ...rest } = state; const next = { ...rest, loadingReasoning: true, routeEpoch: state.routeEpoch ?? epoch, requestSeq: { ...state.requestSeq, reasoningCatalog: seq } }; return { map: withControl(withState, sessionId, next), seq, ...(state.runtimeEpoch !== undefined ? { runtimeEpoch: state.runtimeEpoch } : {}) }; } export function applyReasoningCatalog(map: RuntimeControlsMap, sessionId: string, catalog: ReasoningCatalog, epoch: number, seq: number, runtimeEpoch?: number): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch) || (state.requestSeq?.reasoningCatalog ?? 0) > seq) return map; const { reasoningError: _omit, ...rest } = state; return withControl(withState, sessionId, { ...rest, reasoning: catalog, loadingReasoning: false }); } export function applyReasoningCatalogError(map: RuntimeControlsMap, sessionId: string, message: string, epoch: number, seq: number, runtimeEpoch?: number): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch) || (state.requestSeq?.reasoningCatalog ?? 0) > seq) return map; return withControl(withState, sessionId, { ...state, loadingReasoning: false, reasoningError: message }); } export function markReasoningMutationStart(map: RuntimeControlsMap, sessionId: string, epoch: number): { map: RuntimeControlsMap; seq: number; runtimeEpoch?: number } { const { state, map: withState } = controlFor(map, sessionId); const seq = (state.requestSeq?.setReasoning ?? 0) + 1; const { reasoningError: _omit, ...rest } = state; const next = { ...rest, pendingReasoning: true, routeEpoch: state.routeEpoch ?? epoch, requestSeq: { ...state.requestSeq, setReasoning: seq } }; return { map: withControl(withState, sessionId, next), seq, ...(state.runtimeEpoch !== undefined ? { runtimeEpoch: state.runtimeEpoch } : {}) }; } export function applyReasoningMutationSuccess(map: RuntimeControlsMap, sessionId: string, active: string, epoch: number, seq: number, runtimeEpoch?: number): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch) || (state.requestSeq?.setReasoning ?? 0) > seq) return map; const reasoning = state.reasoning === undefined ? undefined : { ...state.reasoning, active }; const { reasoningError: _omit, ...rest } = state; return withControl(withState, sessionId, { ...rest, ...(reasoning ? { reasoning } : {}), pendingReasoning: false, reasoningSuccessRevision: (state.reasoningSuccessRevision ?? 0) + 1 }); } export function applyReasoningMutationError(map: RuntimeControlsMap, sessionId: string, message: string, epoch: number, seq: number, runtimeEpoch?: number): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (isStaleEpoch(state, epoch, runtimeEpoch) || (state.requestSeq?.setReasoning ?? 0) > seq) return map; return withControl(withState, sessionId, { ...state, pendingReasoning: false, reasoningError: message }); } export function applyReasoningBroadcast(map: RuntimeControlsMap, sessionId: string, model: ModelDescriptor, active: string, epoch: number): RuntimeControlsMap { const { state, map: withState } = controlFor(map, sessionId); if (state.routeEpoch !== undefined && state.routeEpoch !== epoch) return map; if (state.reasoning?.model.id !== model.id || !state.reasoning.levels.some((level) => level.id === active)) return map; return withControl(withState, sessionId, { ...state, reasoning: { ...state.reasoning, model, active }, pendingReasoning: false }); } export function disableReasoning(map: RuntimeControlsMap, sessionId: string): RuntimeControlsMap { const state = map[sessionId]; if (state === undefined) return map; const { reasoning: _reasoning, reasoningError: _error, ...rest } = state; return withControl(map, sessionId, { ...rest, capabilities: state.capabilities.filter((capability) => capability !== CAP_REASONING_CATALOG && capability !== CAP_REASONING_SWITCH), loadingReasoning: false, pendingReasoning: false }); } export function invalidateReasoning(map: RuntimeControlsMap, sessionId: string): RuntimeControlsMap { const state = map[sessionId]; if (state === undefined) return map; const { reasoning: _reasoning, reasoningError: _error, ...rest } = state; return withControl(map, sessionId, { ...rest, loadingReasoning: false, pendingReasoning: false }); } /** Drop a session's control state (tab close / detach). Pure. */ export function clearRuntimeControls(map: RuntimeControlsMap, sessionId: string): RuntimeControlsMap { if (map[sessionId] === undefined) return map; const next = { ...map }; delete next[sessionId]; return next; }