Phase 19 — User Preferences Store

Goal: Give Gemma a remember_this tool that stores explicit user preferences. Preferences are loaded passively at session start and injected into the system prompt — no recall tool call required. Preferences are keyed to a user_id so they are ready for a multi-user / login system.

Key design insight: Preferences are fundamentally different from episodic memory. Episodic engrams are recalled on demand when semantically relevant. Preferences are always true — they must be present on every turn without Gemma having to search for them. The delivery mechanism is passive injection into the system prompt, not search_memory.
---

1. Architecture Overview

User: "remember to make URLs clickable when you present them"
  │
  └─► Gemma calls remember_this("User wants URLs formatted as clickable Markdown links")
            │
            └─► PreferenceService.write(user_id, text)
                      │
                      └─► ChromaDB  user.preferences  collection
                                    {user_id, text, timestamp}

On next session start:
  GeneratorAgent.__init__ / configure()
    │
    └─► PreferenceService.load(user_id) → ["User wants URLs as Markdown links", ...]
              │
              └─► Injected as [User Preferences] block into system prompt
                        │
                        └─► Gemma sees it on every turn — no tool call needed

2. User Identity

Current state (no login)

A single user_id is used for all sessions. It is set in config/system.yaml and defaults to "default". This makes the preferences store functional immediately without any auth plumbing.

Future state (with login)

When a login system is added, the user_id is resolved from the authenticated session at WebSocket connect time and passed through the query.received envelope. GeneratorAgent reads it and loads that user's preferences on each turn (or caches them at session start). No change to PreferenceService or the ChromaDB schema is needed — the user_id key is already the discriminator.

Decision: user_id flows as a first-class field through the query pipeline from day one. It just defaults to "default" until login exists. This avoids a future migration of the preference store.

3. New Components

3a. PreferenceService NEW

Thin wrapper around a dedicated ChromaDB collection (user.preferences). Separate from MemoryService because preferences have a different access pattern: full-load by user_id, not semantic search.

# src/local/services/preference_service.py

class PreferenceService:
    def write(self, user_id: str, text: str) -> str:
        """Store a preference. Returns the preference ID."""

    def load(self, user_id: str) -> list[str]:
        """Return all preference texts for user_id, oldest first."""

    def delete(self, preference_id: str) -> None:
        """Remove a preference by ID."""

    def list(self, user_id: str) -> list[dict]:
        """Return [{id, text, timestamp}] for the user."""

ChromaDB document format:

id:        uuid
document:  "User wants URLs formatted as clickable Markdown links"
metadata:  {user_id: "default", timestamp: 1234567890.0}

No embedding needed for load-by-user_id (metadata filter is exact). Embeddings can be added later if semantic deduplication is wanted.

3b. RememberThisTool NEW

A standard BaseTool subclass. Gemma calls it when the user says "remember X", "don't forget that I prefer X", etc.

# src/local/tools/remember_this_tool.py
# config/remember_this.yaml

Tool name:  remember_this
Parameter:  preference (string) — the preference to store, in plain English
Bus:        tool.call.remember_this  →  tool.result.remember_this

Returns to Gemma: "Preference saved: <text>"

The tool reads user_id from the envelope's session context (defaults to "default" until login is wired). It calls PreferenceService.write(user_id, text) and returns a confirmation string.

No forget_this tool in this phase. Preference deletion will be handled via the memory browser UI (Phase 20) where the user can see and delete preferences directly. Gemma-callable deletion can be added later.

3c. Preference injection in GeneratorAgent MODIFIED

At session start (when the first query.received arrives for a new session), GeneratorAgent loads preferences for the current user_id and prepends them to the system prompt as a [User Preferences] block.

# In generator_agent.py, _build_system_prompt() or equivalent:

prefs = self._preference_service.load(user_id)
if prefs:
    pref_block = "[User Preferences]\n" + "\n".join(f"- {p}" for p in prefs)
    system_prompt = pref_block + "\n\n" + base_system_prompt

Preferences are loaded once per session and cached. If remember_this is called mid-session, the new preference takes effect on the next session (or we can hot-reload — TBD).

4. Bus Subjects

SubjectPublisherSubscriber
tool.call.remember_thisToolDispatcherRememberThisTool
tool.result.remember_thisRememberThisToolGeneratorAgent
tool.activity.remember_thisRememberThisToolToolWindow (observer)

Add TOOL_CALL_REMEMBER_THIS and TOOL_RESULT_REMEMBER_THIS to protocol/subjects.py. Add both to ws_bridge.py CHAT_OBSERVE so the tool chip appears in the web UI.

5. Config

# config/remember_this.yaml
description: |
  Store a preference that will be applied to every future conversation.
  Call this when the user says "remember that I...", "always...", "don't forget...",
  or states a persistent behavioral preference. Do NOT use for factual information
  or one-time context — use this only for durable preferences about how to respond.

param_preference: |
  The preference to store, written as a complete sentence describing what the user wants.
  Example: "User wants URLs formatted as Markdown clickable links."
# config/system.yaml  (add field)
user_id: "default"

6. Files Changed

FileChange
src/local/services/preference_service.pyNEW PreferenceService
src/local/tools/remember_this_tool.pyNEW RememberThisTool
config/remember_this.yamlNEW tool description + param
config/system.yamlMOD add user_id: "default"
src/local/protocol/subjects.pyMOD add TOOL_CALL/RESULT_REMEMBER_THIS
src/local/agents/generator_agent.pyMOD inject preferences into system prompt
src/local/run.pyMOD instantiate PreferenceService, inject into tool + generator
src/local/api/ws_bridge.pyMOD add remember_this to CHAT_OBSERVE
tests/test_preference_service.pyNEW
tests/test_remember_this_tool.pyNEW

7. Out of Scope (Phase 19)

8. Implementation Order

  1. Add subjects to subjects.py
  2. Write PreferenceService + tests
  3. Write RememberThisTool + tests + config
  4. Add user_id to system.yaml
  5. Modify GeneratorAgent to load + inject preferences
  6. Wire into run.py + ws_bridge.py
  7. Story: "remember URLs should be clickable" → next session asks for a URL → it's formatted as a link