import { assertConfig } from "./schema.js"; import { DEFAULT_ANALYTICS_RETENTION_DAYS, defaultRosterConfig } from "./defaults.js"; import type { UltraConfig } from "../types.js"; export function migrateConfig(value: unknown): UltraConfig { if (!value || typeof value !== "object" || Array.isArray(value)) { assertConfig(value); throw new Error("Unreachable config migration state"); } const migrated = structuredClone(value) as Record; const telemetry = migrated.telemetry; if (telemetry && typeof telemetry === "object" && !Array.isArray(telemetry)) { const settings = telemetry as Record; delete settings.enabled; delete settings.weeklyExportRequiresApproval; settings.analyticsRetentionDays ??= DEFAULT_ANALYTICS_RETENTION_DAYS; } const writer = migrated.boundedWriter; if (writer && typeof writer === "object" && !Array.isArray(writer)) delete (writer as Record).maxParallel; const deep = migrated.deep; if (deep && typeof deep === "object" && !Array.isArray(deep)) delete (deep as Record).maxParallel; // warRoom.maxMembers never did anything: every reader took min(3, it) and the round always built // the same three members. Dropped rather than left looking like a fleet-size control. const warRoom = migrated.warRoom; if (warRoom && typeof warRoom === "object" && !Array.isArray(warRoom)) { const room = warRoom as Record; delete room.maxMembers; room.maxSpecialists ??= 2; } // Configs written before the roster became declarable carry the profile's built-in list // implicitly. Backfilling it is lossless: the old schema admitted nothing else. if (migrated.models === undefined && (migrated.profile === "private" || migrated.profile === "free")) migrated.models = defaultRosterConfig(migrated.profile); assertConfig(migrated); return migrated; }