# Milestone 3, Phase 2: Feedback Loop -- Signal Writes Update User State ## Phase Deliverable When a signal event is written (view, like, skip, hide, block, completion, share), the database atomically updates multiple state targets: the item's signal ledger, the user's preference vector, the user-to-creator interaction weight, and the user-state bitmap indexes. One `db.signal()` call, multiple state updates, zero application logic. This phase closes the feedback loop inside the database. Before m3p2, signals only updated item-level aggregates. After m3p2, signals also update user-level state: the preference embedding shifts toward items the user engages with, interaction weights strengthen toward creators the user prefers, and hard negatives (hide/block) permanently exclude items and creators from future queries. The phase delivers four components: (1) user preference vector EMA update with configurable learning rate, (2) interaction weight ledger using the existing decay infrastructure, (3) hard negative storage with WAL-backed durability, and (4) an atomic signal dispatch that wires all state updates into a single transactional signal write. ## Acceptance Criteria - [ ] `db.signal("view", item_id, 1.0, ts)` with user context atomically: updates item signal ledger, marks item as seen in `UserStateIndex`, increments user->creator interaction weight - [ ] `db.signal("like", item_id, 1.0, ts)` with user context atomically: updates item signal ledger, shifts user preference vector toward item embedding (EMA), increments user->creator interaction weight - [ ] `db.signal("skip", item_id, 1.0, ts)` with user context atomically: updates item signal ledger, shifts user preference vector away from item embedding, decays user->creator interaction weight - [ ] `db.signal("hide", item_id, 1.0, ts)` with user context atomically: writes permanent hide edge, adds item to `UserBlockedSet.hidden_items`, excludes from all future queries - [ ] `db.signal("block", user_id, creator_id, ...)` atomically: writes permanent block edge, adds creator to `UserBlockedSet.blocked_creators`, excludes all creator items from all future queries - [ ] Preference vector EMA: `pref_new = normalize(alpha * item_embedding + (1 - alpha) * pref_old)` with configurable alpha (default 0.1) - [ ] Interaction weights use the same `DecayModel::Exponential` infrastructure from m1p4 - [ ] Hard negatives (hide/block) are WAL-backed and survive crash + replay - [ ] Property test: for any sequence of hide/block/signal events, a RETRIEVE query NEVER returns a hidden item or blocked creator's items - [ ] All updates visible to the next query (no eventual consistency lag within the process) - [ ] Signal dispatch overhead < 50 microseconds beyond the base item signal write ## Dependencies - **Requires:** m3p1 (user/creator entities, relationship graph, `UserStateIndex`, `CreatorItemsBitmap`), m1p4 (signal ledger, decay infrastructure), m1p5 (signal write API), m2p1 (vector index for embedding reads) - **Blocks:** m3p3 (Personalized Profiles need updated preference vectors and interaction weights), m3p4 (User State Filters need populated seen/blocked bitmaps) ## Research References - [docs/research/tidaldb_signal_ledger.md](../../../research/tidaldb_signal_ledger.md) -- Three-tier storage, signal dispatch - [docs/research/ann_for_tidaldb.md](../../../research/ann_for_tidaldb.md) -- User preference vector management - [thoughts.md](../../../../thoughts.md) -- Part V.16 (user preference vector as database-managed embedding) ## Task Index | # | Task | Delivers | Depends On | Complexity | |---|------|----------|------------|------------| | 01 | User Preference Vector | EMA update, normalization, learning rate config, cold-start initialization, storage codec | None | L | | 02 | Interaction Weight Ledger | User-to-creator weights using decay infrastructure, update on engagement signals, read API | Task 01 | M | | 03 | Hard Negatives | Hide/block permanent storage, WAL-backed durability, crash-safe replay, bitmap integration | None | L | | 04 | Atomic Signal Dispatch | `UserContext` wiring, multi-target signal dispatch, property tests for correctness invariants | Tasks 01, 02, 03 | L | ## Task Dependency DAG ``` Task 01: User Preference Vector Task 03: Hard Negatives | | v | Task 02: Interaction Weight Ledger | | | +-----------------------------------+ | v Task 04: Atomic Signal Dispatch ``` Tasks 01 and 03 can be built in parallel. Task 02 depends on Task 01 (needs entity lookup patterns). Task 04 depends on all three (wires everything together). ## File Layout ``` tidal/src/ entities/ preference.rs -- PreferenceVector, EMA update, normalization, storage (Task 01) interaction.rs -- InteractionWeightLedger, decay-based weights (Task 02) hard_neg.rs -- HardNegativeStore, WAL event types, replay (Task 03) db/ signal_dispatch.rs -- UserSignalContext, atomic multi-target dispatch (Task 04) mod.rs -- Extended signal() API with user context tidal/tests/ m3p2_feedback_loop.rs -- Phase integration tests ``` ## Open Questions 1. **Preference vector learning rate scheduling**: Should alpha decay over time (fewer updates = smaller shifts as the model converges), or remain constant? Recommendation: constant alpha for M3. Adaptive alpha is a M6 refinement that requires tracking update count per user. 2. **Negative preference update formula**: When a user skips an item, should the preference vector move directly away (`pref - alpha * item_embedding`) or toward the orthogonal complement? Recommendation: simple subtraction + normalization for M3. The orthogonal complement approach is mathematically cleaner but adds complexity without proven benefit at this scale. 3. **Interaction weight initial value**: When a user first interacts with a creator they have no history with, what is the initial interaction weight? Recommendation: `1.0` as the initial weight, with decay applied from the first interaction timestamp. This means new interactions start at full strength and decay naturally. 4. **WAL event format for hide/block**: Should hide/block use the same WAL event format as signal writes, or a dedicated relationship-change event type? Recommendation: extend the WAL event format with a `RelationshipChange` variant. This keeps the WAL as the single source of truth for all durable state changes and makes replay straightforward.