import { type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value"; import type { Settings } from "./settings"; export interface ConfiguredModelBindings { modelRoles?: Record; agentModelOverrides?: Record; } /** Synchronizes config-owned model bindings while preserving user edits. */ export class ModelBindingsApplier { #targetSettings: Settings | undefined; #bindings: ConfiguredModelBindings | undefined; #appliedRoles = new Set(); #appliedAgentOverrides = new Set(); #roleBaselines = new Map(); #agentBaselines = new Map(); #lastAppliedRoles = new Map(); #lastAppliedAgentOverrides = new Map(); /** The currently configured bindings (as installed at startup), for baseline lookup. */ getBindings(): ConfiguredModelBindings | undefined { return ( this.#bindings && { modelRoles: this.#cloneBindings(this.#bindings.modelRoles), agentModelOverrides: this.#cloneBindings(this.#bindings.agentModelOverrides), } ); } setBindings(bindings: ConfiguredModelBindings | undefined): void { this.#bindings = bindings && { modelRoles: this.#cloneBindings(bindings.modelRoles), agentModelOverrides: this.#cloneBindings(bindings.agentModelOverrides), }; } applyTo(targetSettings: Settings): void { if (this.#targetSettings && this.#targetSettings !== targetSettings) { this.#restoreTarget(this.#targetSettings); this.#clearTargetLifecycle(); } this.#targetSettings = targetSettings; this.apply(); } /** * Re-assert configured bindings into the target override slots, bypassing * the user-edit-preservation heuristic. Used after a session-scoped profile * reset removes profile-installed keys, so configured role/agent routing is * restored exactly as it was installed at startup. */ forceApplyTo(targetSettings: Settings): void { if (this.#targetSettings && this.#targetSettings !== targetSettings) { this.#restoreTarget(this.#targetSettings); this.#clearTargetLifecycle(); } this.#targetSettings = targetSettings; const bindings = this.#bindings; if (!targetSettings) return; const modelRoles = { ...(targetSettings.get("modelRoles") ?? {}) }; this.#forceSync( modelRoles, bindings?.modelRoles ?? {}, this.#appliedRoles, this.#roleBaselines, this.#lastAppliedRoles, ); targetSettings.override("modelRoles", modelRoles); const agentOverrides = { ...(targetSettings.get("task.agentModelOverrides") ?? {}) }; this.#forceSync( agentOverrides, bindings?.agentModelOverrides ?? {}, this.#appliedAgentOverrides, this.#agentBaselines, this.#lastAppliedAgentOverrides, ); targetSettings.override("task.agentModelOverrides", agentOverrides); } #forceSync( target: Record, configured: Record, applied: Set, baselines: Map, lastApplied: Map, ): void { for (const [key, value] of Object.entries(configured)) { if (!baselines.has(key)) baselines.set(key, this.#clone(target[key])); target[key] = this.#clone(value)!; lastApplied.set(key, this.#clone(value)!); } applied.clear(); for (const key of Object.keys(configured)) applied.add(key); } apply(): void { const targetSettings = this.#targetSettings; if (!targetSettings) return; const bindings = this.#bindings; const nextModelRoles = { ...(targetSettings.get("modelRoles") ?? {}) }; this.#sync( nextModelRoles, bindings?.modelRoles ?? {}, this.#appliedRoles, this.#roleBaselines, this.#lastAppliedRoles, ); targetSettings.override("modelRoles", nextModelRoles); const nextAgentOverrides = { ...(targetSettings.get("task.agentModelOverrides") ?? {}) }; this.#sync( nextAgentOverrides, bindings?.agentModelOverrides ?? {}, this.#appliedAgentOverrides, this.#agentBaselines, this.#lastAppliedAgentOverrides, ); targetSettings.override("task.agentModelOverrides", nextAgentOverrides); } #restoreTarget(targetSettings: Settings): void { const modelRoles = { ...(targetSettings.get("modelRoles") ?? {}) }; this.#sync(modelRoles, {}, this.#appliedRoles, this.#roleBaselines, this.#lastAppliedRoles); targetSettings.override("modelRoles", modelRoles); const agentOverrides = { ...(targetSettings.get("task.agentModelOverrides") ?? {}) }; this.#sync( agentOverrides, {}, this.#appliedAgentOverrides, this.#agentBaselines, this.#lastAppliedAgentOverrides, ); targetSettings.override("task.agentModelOverrides", agentOverrides); } #clearTargetLifecycle(): void { this.#appliedRoles.clear(); this.#appliedAgentOverrides.clear(); this.#roleBaselines.clear(); this.#agentBaselines.clear(); this.#lastAppliedRoles.clear(); this.#lastAppliedAgentOverrides.clear(); } #sync( target: Record, configured: Record, applied: Set, baselines: Map, lastApplied: Map, ): void { const configuredKeys = new Set(Object.keys(configured)); for (const key of applied) { if (configuredKeys.has(key)) continue; const previous = lastApplied.get(key); if (previous !== undefined && this.#equal(target[key], previous)) { const baseline = baselines.get(key); if (baseline === undefined) delete target[key]; else target[key] = this.#clone(baseline)!; } baselines.delete(key); lastApplied.delete(key); } for (const [key, value] of Object.entries(configured)) { const previous = lastApplied.get(key); if (!baselines.has(key)) baselines.set(key, this.#clone(target[key])); if (previous === undefined || this.#equal(target[key], previous)) { const appliedValue = this.#clone(value)!; target[key] = appliedValue; lastApplied.set(key, this.#clone(appliedValue)!); } } applied.clear(); for (const key of configuredKeys) applied.add(key); } #cloneBindings( bindings: Record | undefined, ): Record | undefined { if (!bindings) return undefined; const copy: Record = {}; for (const [key, value] of Object.entries(bindings)) copy[key] = this.#clone(value)!; return copy; } #clone(value: ModelSelectorValue | undefined): ModelSelectorValue | undefined { return Array.isArray(value) ? [...value] : value; } #equal(left: ModelSelectorValue | undefined, right: ModelSelectorValue | undefined): boolean { const leftSelectors = normalizeModelSelectorValue(left); const rightSelectors = normalizeModelSelectorValue(right); return ( leftSelectors.length === rightSelectors.length && leftSelectors.every((value, index) => value === rightSelectors[index]) ); } }