Files
2026-07-13 13:39:12 +08:00

125 lines
3.1 KiB
TypeScript

/**
* Auto-Combo Adaptation Persistence
*
* Saves and restores scoring adaptation state so learned provider
* preferences survive server restarts.
*/
import fs from "fs";
import path from "path";
import { resolveDataDir } from "@/lib/dataPaths";
export interface AdaptationState {
comboId: string;
providerScores: Record<string, number>;
exclusionHistory: Array<{
provider: string;
excludedAt: string;
cooldownMs: number;
reason: string;
}>;
modePackHistory: Array<{ pack: string; activatedAt: string }>;
totalDecisions: number;
explorationHits: number;
lastUpdated: string;
}
const PERSISTENCE_DIR = resolveDataDir();
const STATE_FILE = path.join(PERSISTENCE_DIR, "auto_combo_state.json");
let stateCache = new Map<string, AdaptationState>();
/**
* Save adaptation state for a combo.
*/
export function saveAdaptationState(state: AdaptationState): void {
stateCache.set(state.comboId, { ...state, lastUpdated: new Date().toISOString() });
persistToDisk();
}
/**
* Load adaptation state for a combo.
*/
export function loadAdaptationState(comboId: string): AdaptationState | null {
if (stateCache.size === 0) loadFromDisk();
return stateCache.get(comboId) || null;
}
/**
* List all saved adaptation states.
*/
export function listAdaptationStates(): AdaptationState[] {
if (stateCache.size === 0) loadFromDisk();
return [...stateCache.values()];
}
/**
* Delete adaptation state for a combo.
*/
export function deleteAdaptationState(comboId: string): boolean {
const existed = stateCache.delete(comboId);
if (existed) persistToDisk();
return existed;
}
/**
* Record a routing decision in the adaptation state.
*/
export function recordDecision(
comboId: string,
provider: string,
score: number,
wasExploration: boolean
): void {
let state = stateCache.get(comboId);
if (!state) {
state = {
comboId,
providerScores: {},
exclusionHistory: [],
modePackHistory: [],
totalDecisions: 0,
explorationHits: 0,
lastUpdated: new Date().toISOString(),
};
}
// Exponential moving average for provider scores
const alpha = 0.1;
const prev = state.providerScores[provider] || 0.5;
state.providerScores[provider] = prev * (1 - alpha) + score * alpha;
state.totalDecisions++;
if (wasExploration) state.explorationHits++;
state.lastUpdated = new Date().toISOString();
stateCache.set(comboId, state);
// Persist every 10 decisions
if (state.totalDecisions % 10 === 0) persistToDisk();
}
function persistToDisk(): void {
try {
if (!fs.existsSync(PERSISTENCE_DIR)) {
fs.mkdirSync(PERSISTENCE_DIR, { recursive: true });
}
const data = Object.fromEntries(stateCache);
fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2));
} catch {
/* disk write failure — non-fatal */
}
}
function loadFromDisk(): void {
try {
if (fs.existsSync(STATE_FILE)) {
const raw = fs.readFileSync(STATE_FILE, "utf-8");
const data = JSON.parse(raw) as Record<string, AdaptationState>;
stateCache = new Map(Object.entries(data));
}
} catch {
/* disk read failure — start fresh */
}
}