The AI chat lived only in React state — closing the panel or refreshing lost it, every open re-ran the briefing, and every turn resent the full persona + board context uncached. Now the conversation persists per space and the repeated context is cached.
Added
supabase/migrations/20260620000100_ai_messages.sql—ai_messagestable: one rolling conversation per space (the space is the thread, no separate sessions table). RLS mirrorscards(members read, editors write). Indexed on(space_id, created_at)for chronological load.GET /api/ai?spaceId=…— loads a space's saved thread (RLS-scoped).
Changed
app/api/ai/route.ts- Persists the opening briefing and each Q&A turn (
assistantbrief; then the newuserquestion +assistantreply per follow-up — no duplicates). Best-effort and signed-in/cloud only; local spaces stay ephemeral, guarded by a uuid check + RLS. - Prompt caching — the static persona + board prefix is sent as an Anthropic
cache_control: ephemeralblock, so resending it each turn is billed at a fraction (the biggest cost lever, vs. trimming context and hurting answers). - History cap — replays only the last
MAX_HISTORY(24) turns to the model.
- Persists the opening briefing and each Q&A turn (
components/AIPanel.tsx— on open, resumes the saved thread if one exists (no re-briefing, no scan); only a genuinely fresh space runs the briefing + scan. SendsspaceId; caps replayed turns toMAX_TURNS(24).app/page.tsx— passesspaceId(cloud spaces only) into<AIPanel>.lib/supabase/types.ts— added theai_messagestable type.