Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697/** * 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<string, ModelDescriptor>(); 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<string>(); 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<string>(); 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<string, RuntimeControlState>;
/** 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<string, unknown> => 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<RuntimeControlState, 'modelError'> { const { modelError: _omit, ...rest } = state; return rest;}function withoutModeError(state: RuntimeControlState): Omit<RuntimeControlState, 'modeError'> { 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;}