Make the AI user profile backend-owned for every client; delete the Dream state pass

GI
17m ago
+2869-4016

What changed and why

David's decision (2026-11-16): one backend-owned AI user profile for every client, replacing both the client-owned v1 profile and the never-activated Dream state-pass profile v2.

Backend owns the profile. utils/ai_profile_refresh.py selects the sources server-side (canonical prompt memories, 50 tasks, active goals, 20 conversations from the last 7 days, 30 chat messages), runs the existing two-stage synthesis (utils/llm/ai_user_profile.py, memories feature) and writes users/{uid}.ai_user_profile with source and refreshed_at.

  • Cadence: at most once per owner per ~20h, triggered after a processed conversation (same fan-out as goal progress) and when GET /v1/users/ai-profile sees a stale profile. GET never blocks on generation.
  • Spend guards: per-owner Redis lock held for an hour (no Redis, no refresh), generated-compute admission, kill switch AI_PROFILE_BACKEND_REFRESH_ENABLED (default on; off stops automatic refreshes only).
  • New routes: POST /v1/users/ai-profile/refresh (8/day; 402/409/422/502 typed errors) and DELETE /v1/users/ai-profile. PATCH records source (user_edit/exploration).
  • No history: the stored text is the past profile for the next synthesis. A user edit is merged as a correction; an edit made while a refresh runs wins (the write is a transactional compare-and-set).
  • POST /v1/users/ai-profile/synthesize stays only for released desktop builds (documented as deprecated).

macOS and Windows read it. Local generation, /synthesize calls, local history tables, the daily trigger and the fire-and-forget mirror PATCH are removed. Both apps read the cloud profile through an in-memory, account-bound cache and send refresh/edit/delete (macOS also exploration) through the backend. Forward migrations drop the local ai_user_profiles tables; the chat SQL tool no longer lists them. Settings keeps its layout; copy now says the profile is generated automatically, and refresh/save/delete failures show an inline message.

Mobile needs no client change: the notes agent, MCP profile and owner-name fallback already read ai_user_profile, which is now populated for mobile-only owners too.

Dream state pass deleted. The weekly state pass and its ref-grounded ai_user_profile_v2 only ever ran in shadow with no reader (7-day shadow readout on 2026-11-15: 115 of 365 passes completed, 250 failed on response truncation or input budget). Removed with its flags, env, tests, query drivers and runbook text. Existing ai_user_profile_v2 fields on user docs are left in place.

Gate F ratchet. legacy_memory_surface_inventory.py tracked standalone profile synthesis for retirement under the proposed INV-MEM-6. Per the ruling above, the backend owner files (routers/users.py, database/users.py, utils/llm/ai_user_profile.py) leave that rule; client paths stay tracked and their baseline only shrinks (macOS service 47→41, Windows service 3→1, Windows db 10→1). INV-MEM-6 (status: proposed) lists "standalone profile synthesis" and "profile snapshot authority" among its MUST NOTs; this profile writes no memories, but the invariant text should be reconciled with this ruling when INV-MEM-6 is next revised.

Cost estimate (not measured): one refresh is ~5–15k input and ~1k output Luna tokens (≈$0.001–0.002 at the 2026-09-22 card) plus a few hundred bounded Firestore reads, once per active owner per day. Desktop owners already paid this daily client-side; mobile-only owners are new. Usage lands on the memories feature in llm usage. Kill switch above.

Product invariants affected

INV-AUTH-1: the desktop profile cache is in-memory and bound to the signed-in owner; an owner transition clears it and a read started under the previous owner is discarded on completion. No persisted owner-scoped state is added (the local table is dropped).

INV-CHAT-1: chat reads the profile as prompt context only; no transcript, session or agent state store is added. The macOS chat prompt now reads the cached profile per turn instead of once per session.

INV-MEM-4: the profile is a derived summary written to the user doc; it never creates, promotes or routes memory items.

INV-TASK-2: task assistants only read the profile text as context; no task or Candidate writer is added or changed.

How it was verified

  • Backend, each file in its own process (as CI runs): test_ai_profile_refresh.py 16 passed (cadence, kill switch, lock/compute denial, user-edit correction, edit-during-synthesis, failures never write, source formatting, transactional compare-and-set on the strict Firestore fake); test_ai_user_profile_route.py 9 passed; test_firestore_query_shapes.py 596 passed and test_firestore_runtime_witnesses.py 25 passed (new caller uses the existing start/end-date shape with a witness); plus the profile helper, cache, MCP, user-name and composer-context files.
  • State-pass removal: 81 affected unit files (every importer of dream_agent/dream_store/dream_judge, every test_dream_*, runtime-env and flag-registry tests) run per file; all pass after the query-witness fix. Pyright clean on the three Dream modules.
  • Checks: async-blocker scan 0 findings; route-policy baseline current; import side effects 0; stub pollution 0; app-client OpenAPI compatibility passed vs merge base; legacy memory ratchet PASS; flag registry regenerated; app-client OpenAPI and generated Swift/TS clients regenerated.
  • macOS: swift build --build-tests; 44 tests passed (AIUserProfileServiceTests, LocalProfileHistoryRetirementMigrationTests, ChatDiscoverabilityTests, ProactivityRetirementMigrationTests); SwiftLint 0 violations; swift-format, test-quality, flow-lint and strict e2e flow coverage clean.
  • Windows: pnpm typecheck, pnpm lint and pnpm test (5548 passed) clean.
  • Not run: a live backend or a named desktop bundle against the new routes.

Line-count exceptions

Line-Count-Exception: backend/routers/users.py | 2802 -> 2850 | DELETE and refresh routes live beside the existing GET/PATCH/synthesize AI-profile routes; a separate router would need its own role-table entry for one feature
Line-Count-Exception: backend/database/users.py | 2521 -> 2566 | write/delete/fresh-read helpers share the user_ai_profile cache policy and read path already in this module
Line-Count-Exception: backend/database/redis_db.py | 1568 -> 1578 | per-owner AI profile refresh lock, same shape as the neighbouring daily locks
Line-Count-Exception: backend/utils/conversations/process_conversation.py | 3414 -> 3417 | one fire-and-forget refresh call beside goal progress
Line-Count-Exception: desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift | 3470 -> 3471 | registers the migration that drops the local ai_user_profiles table

UI evidence

UI-Evidence: none -- Settings layout unchanged; copy and an inline error line only

Failure class (fixes)

Failure-Class: none

🤖 Generated with Claude Code