Never let legacy migration evict existing per-model configs

Protecting the just-migrated keys during budget enforcement meant legacy
entries could evict the user's existing current-schema configs to make room,
and could still deadlock when unevictable future-schema records were present.
Protect the existing entries instead and let only the just-migrated legacy
entries be evicted when over budget: importing old settings never discards a
newer config, and since existing entries already fit the budget it cannot
deadlock.
This commit is contained in:
danielhanchen 2026-07-17 23:18:52 +00:00
commit 53836cc62e

View file

@ -262,6 +262,10 @@ function migrateLegacyLoadSettingsOnce(): void {
return;
}
const map = readMapRaw();
// Snapshot the user's existing entries before merging: legacy settings are
// imported only into spare budget and must never evict a config the user
// already saved in the current schema.
const existingKeys = new Set(Object.keys(map));
const migratedKeys = mergeLegacyEntries(
map,
legacy as Record<string, unknown>,
@ -270,16 +274,11 @@ function migrateLegacyLoadSettingsOnce(): void {
localStorage.setItem(LEGACY_MIGRATION_FLAG, "1");
return;
}
// Protect the just-migrated entries from eviction, but cap the protected set
// to MAX_ENTRIES: an oversized legacy store would otherwise deadlock the
// budget loop and never finish migrating. On failure the flag stays unset so
// migration retries once space frees.
const protectedKeys = new Set(
migratedKeys.length > MAX_ENTRIES
? migratedKeys.slice(0, MAX_ENTRIES)
: migratedKeys,
);
if (!enforceStorageBudget(map, protectedKeys)) {
// Protect existing entries so only the just-migrated legacy entries are
// evicted when over budget: importing old settings never discards a newer
// config, and since existing entries already fit the budget this cannot
// deadlock. On failure the flag stays unset so migration retries.
if (!enforceStorageBudget(map, existingKeys)) {
return;
}
if (writeMap(map)) {