- BOOTSTRAP__AI_Agent_Quickstart.md: new category 9 under Common Mistakes pointing to today's incident (local/remote config sync pitfalls) - REFERENCE__Common_Agent_Mistakes.md: three new entries — 16) local "shadow field" silently bypasses an admin-synced master field 17) SWR await after a write does not mean dependent caches are fresh 18) conditional/stateful sync gates are effectively undebuggable - PROJECT__Stores_Svelte5_Migration.md: updated the canonical Migration Pattern example to stamp __version in the serializer (the old example silently didn't, which is exactly what caused the leads_loc/pres_mgmt_loc bugs fixed today); flagged badges_loc/launcher_loc/events_auth_loc as not yet fixed (dormant, not harmful) and idaa_loc as the next migration that should get this from day one - TODO__Agents.md: cross-referenced today's Pres Mgmt config sync overhaul in the LCI October restoration section for context, without touching the open items in that list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.5 KiB
Project: Svelte 4 Store → Svelte 5 State Migration
Status: Events module — COMPLETE. Core / IDAA — In Progress (field cleanup done, PersistedState pending). Priority: High Created: 2026-03-30 Last Updated: 2026-06-11
Background
All core Aether stores were built with svelte-persisted-store (Svelte 4 contract). This provides
coarse reactivity: any write to any field notifies all subscribers and re-serializes the entire
object. For large stores like ae_loc and ae_events_loc, this caused unnecessary re-renders and
was the root cause of the IDAA "Access Denied" corruption bug (a bootstrap write to ae_loc would
overwrite authenticated_access if a persisted value was slightly different, corrupting IDAA
member state stored in the same key).
The migration target: replace all persisted() stores with runed's PersistedState, which uses
Svelte 5 fine-grained reactivity — a write to one field only triggers effects that read that field.
Completed: Events Module (2026-06-11)
All ae_events_stores sub-modules have been promoted to their own PersistedState stores and
events_loc (the old persisted() store) has been fully retired.
| Store | File | localStorage key | Status |
|---|---|---|---|
badges_loc |
ae_events_stores__badges.svelte.ts |
ae_badges_loc |
✅ Done (2026-04-02) |
leads_loc |
ae_events_stores__leads.svelte.ts |
ae_leads_loc |
✅ Done (2026-04-03) |
pres_mgmt_loc |
ae_events_stores__pres_mgmt.svelte.ts |
ae_pres_mgmt_loc |
✅ Done (2026-04-03) |
launcher_loc |
ae_events_stores__launcher.svelte.ts |
ae_launcher_loc |
✅ Done (2026-06-11) |
events_auth_loc |
ae_events_stores__auth.svelte.ts |
ae_events_auth_loc |
✅ Done (2026-06-11) |
events_loc |
(retired) | ae_events_loc |
✅ Store removed (2026-06-11) |
ae_events_stores.ts now only exports events_sess (in-memory writable, no migration needed),
events_slct, events_trig, events_trig_kv, events_trigger, and the EVENTS_MODULE_TITLE
constant. The file no longer imports svelte-persisted-store.
store_versions.ts still calls _check_and_wipe('ae_events_loc', AE_EVENTS_LOC_VERSION) to clean
old data out of users' browsers — this is intentional and should be kept for at least one year.
In Progress / Remaining: ae_stores.ts and ae_idaa_stores.ts
Both stores had their unused default properties pruned (2026-06-11), reducing migration scope:
ae_loc (in ae_stores.ts) — still persisted('ae_loc', ...):
- Remaining fields: auth/identity, theme, permissions, ui config, file upload tracking, query prefs
- This is the highest-impact remaining migration — used in nearly every route
- Root cause of IDAA "Access Denied" bug (coarse write during bootstrap stomps on permission fields)
idaa_loc (in ae_idaa_stores.ts) — still persisted('ae_idaa_loc', ...):
- Remaining fields:
novi_uuid/verified/ts,novi_admin_li/trusted_li,archives/bb/recovery_meetingssub-objects - Fields pruned (2026-06-11):
ds,idaa_cfg_json, top-levelqry__*,novi_*_base_url,novi_rate_limited_until
Migration Pattern (Established)
Each sub-store follows this pattern, using the badges store as the canonical reference:
1. Defaults file (*_defaults.ts)
Define the shape and defaults as a plain TypeScript object (and interface if complex):
export interface BadgesLocState { ... }
export const badges_loc_defaults: BadgesLocState = { ... };
2. Store file (*.svelte.ts)
Updated 2026-06-16 — stamp __version in the serializer. The original pattern below (bare
JSON.stringify) looked fine but has a real failure mode: store_versions.ts's
_check_and_wipe() compares a __version field inside the stored JSON, and a bare
JSON.stringify never writes one. Two consequences were found in production code:
pres_mgmt_loc's version bump was silently a no-op (never wired into _check_and_wipe at
all), and leads_loc's was wired in, so parsed?.__version was always undefined —
always failing the comparison — wiping ae_leads_loc on every single page load. Both
fixed 2026-06-16. badges_loc, launcher_loc, and events_auth_loc still use the old bare
pattern below and are not yet wired into _check_and_wipe — not actively harmful (the
wipe never runs), but apply this fix before wiring any of them in.
import { PersistedState } from 'runed';
import { badges_loc_defaults } from './ae_events_stores__badges_defaults';
import { AE_BADGES_LOC_VERSION } from './store_versions';
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults, {
serializer: {
// Stamp __version on every write so store_versions.ts's _check_and_wipe() can
// detect a breaking schema change and clear stale browsers on next load. This
// import also guarantees store_versions.ts's wipe side-effect runs before this
// PersistedState reads from localStorage (ES module execution order).
serialize: (value) =>
JSON.stringify({ ...value, __version: AE_BADGES_LOC_VERSION }),
// Merge with defaults so new fields added after first session get their defaults,
// and strip __version back out so it doesn't pollute the typed state object.
deserialize: (raw: string) => {
const { __version, ...stored } = JSON.parse(raw);
return { ...badges_loc_defaults, ...stored };
}
}
});
Then in store_versions.ts's startup block:
_check_and_wipe('ae_badges_loc', AE_BADGES_LOC_VERSION);
See ae_events_stores__pres_mgmt.svelte.ts and ae_events_stores__leads.svelte.ts for the
two real, currently-correct examples of this pattern.
3. Consumer syntax
// Import (note .svelte extension, not .svelte.ts):
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
// Read:
badges_loc.current.fulltext_search_qry_str
// Write (fine-grained — only triggers effects that read this field):
badges_loc.current.fulltext_search_qry_str = 'hello';
// Bulk reset:
badges_loc.current = { ...badges_loc_defaults };
Key differences from old svelte-persisted-store pattern:
Old (persisted()) |
New (PersistedState) |
|
|---|---|---|
| Import | import { events_loc } from '...ae_events_stores' |
import { badges_loc } from '...ae_events_stores__badges.svelte' |
| Read | $events_loc.badges.field |
badges_loc.current.field |
| Write | $events_loc.badges.field = x |
badges_loc.current.field = x |
| Reactivity | Coarse — entire store notified | Fine-grained — only affected fields |
In $effect |
Subscribes to entire store | Only subscribes to fields you read |
Next Steps
-
idaa_loc→ PersistedState — Highest priority for IDAA stability. Promotesnovi_uuid/verified,archives,bb,recovery_meetingssub-objects to their own stores following the same pattern. Primary benefit: eliminates the IDAA "Access Denied" corruption fromae_locbootstrap writes. Use the__version-stamping serializer from the start (see Migration Pattern above) and wire it into_check_and_wipe()immediately —idaa_locholds auth state, so a real wipe-on-schema- change matters more here than anywhere else this pattern has been applied so far. -
ae_loc→ PersistedState — Largest scope (~every route in the app). Defer until afteridaa_locis done. Consider extractingauth_loc(the identity/permission fields) as the first sub-store since those are the fields implicated in the IDAA corruption bug.