feat(email): persist triage preferences across sessions (#1288)#1633
Conversation
EmailTriageAgent previously had no persistent memory across sessions. After this change it composes MemoryMixin alongside DatabaseMixin, with memory.db namespaced to ~/.gaia/email/ so it coexists with state.db without conflict. Memory tools (remember/recall/update_memory/forget/ search_past_conversations) are registered through _register_tools().
Priority/low-priority senders and category-default actions set via the preference tools were stored only in self._session_preferences and wiped on every restart. They now persist to the MemoryStore wired in #1114 under a stable entity key (email:preferences) and are restored into _session_preferences on agent construction. Persistence uses an idempotent upsert (get_by_entity -> update by id, or store if absent) so the record never accumulates duplicates. clear_session_preferences wipes the persisted copy too. When memory is disabled (GAIA_MEMORY_DISABLED=1 or Lemonade unreachable) the tools still work in-process and the write is skipped. Closes #1288
Categorization-regression check: resolved structurally (no benchmark regression possible)The LLM-affecting-change concern for these PRs is whether wiring
A formal |
Formal eval gate: PASSRan the email categorization integration gate on the stack tip against local Lemonade (
|
SummarySolid, well-scoped persistence layer — the idempotent single-record upsert ( Issues Found🟡 Important — User docs now contradict the code (
|
f5a6f5f to
741b6d5
Compare
|
🟡 All four def _invoke_set_priority_sender(agent: EmailTriageAgent, email: str) -> dict:
from gaia.agents.base.tools import _TOOL_REGISTRY
entry = _TOOL_REGISTRY.get("set_priority_sender") # ← ignores `agent`
result = entry["function"](email)Each
Suggested fix — invoke through the agent's own tool objects, or at minimum remove the misleading parameter: def _invoke_set_priority_sender(email: str) -> dict:
from gaia.agents.base.tools import _TOOL_REGISTRY
entry = _TOOL_REGISTRY.get("set_priority_sender")
assert entry is not None
return json.loads(entry["function"](email))And update every call-site to drop the |
|
looks like you need to address a couple issues pointed out by claude: docs and bypassing incognito. |
…-persist-preferences # Conflicts: # hub/agents/python/email/gaia_agent_email/agent.py
The four _invoke_* dispatch helpers in test_email_preferences_persist.py accepted an agent parameter that was never used — each helper dispatches through the module-global _TOOL_REGISTRY, which holds closures from the most-recently-built agent. Drop the parameter from all four signatures and update the ten call sites accordingly.
|
@kovtcharov-amd — docs and incognito are already addressed in the current branch; the "Request changes" review predates the force-push that landed those fixes:
I also resolved the merge conflict (a squash-merge stack artifact — main carried #1114 squashed while this branch re-applied the original #1114 commits): merged main back in, keeping this branch's superset (#1114 wiring + #1288's |
|
@claude review this PR and confirm if all request changes have been addressed. |
|
All four blocking/important items from the earlier review are addressed in the current branch. Confirmed against the actual diff (PR is the four #1288 files vs 🟡 Docs contradiction — fixed ( 🟡 Incognito bypass — fixed ( 🟢 🟢 Unused One minor left open (was flagged optional): in the four tools, Couldn't execute the suite here (no Net: the two "Request changes" items (docs + incognito) and the two relevant minors are resolved. No new concerns. |
|
For the record, all three Request-changes items are addressed on the current tip (
Plus the squash-merge conflict is resolved and the diff is scope-clean (the 4 #1288 files only). CI is green (37 checks pass, 0 failures); PR is MERGEABLE. Deferring to @claude's independent re-review to confirm. |
|
Re-reviewed the tip (
Diff is scope-clean (the 4 #1288 files), and the idempotent upsert is verified by Two non-blocking notes for whoever does the final merge call (a maintainer, not me):
|
Both #1288 (PR #1633) and #1289 (PR #1642) have been squash-merged to main. Merge main in so the branch diff is limited to only the #1290 behavioral-learning additions. Conflict in profile_tools.py resolved by keeping the branch superset: #1289 interaction substrate + #1290 reply constants, _evaluate_promotions, and _record_reply_interaction — all additions present in main are included.
Closes #1288
Why this matters
Before: email triage preferences — priority senders, low-priority senders, category defaults — lived in
_session_preferencesand were wiped on every restart. You had to re-tell the agent who matters each session, and any "always treat my boss as urgent" intent evaporated. After: those preferences persist through the memory store wired in #1114 and are reapplied at triage, so a sender you mark priority stays priority across restarts and their mail is classifiedurgentautomatically — no re-teaching.Persistence uses a single memory record (
email:preferences) viaget_by_entity+update(idempotent upsert — never accumulates duplicate rows). When memory is disabled (GAIA_MEMORY_DISABLED=1/ embeddings unreachable), preferences fall back to session-only with no error.Test plan
test_email_preferences_persist.py+ 20 carried feat(email): wire MemoryMixin into the email agent (persistence enabler) #1114 tests →pytest hub/agents/python/email/tests/ -x -q= 28 passed. Includestest_no_duplicate_memory_records(single-row idempotency) and a memory-disabled fallback test.python util/lint.py --allclean.nomic-embedembeddings, memory ENABLED on all):_session_preferences→ triage classifies itinformational→urgent; single-record idempotency; category-default persists.close_db()releases the SQLite handle with noPermissionError;~/.gaia/email/resolves toC:\Users\tomas\.gaia\email\; single record.Caveat (honest)
The macOS live-inbox end-to-end flip was not captured in one shot: driving the connector from a bare script hit a pre-existing connector token-bridge timeout (sync→async, #1579-family). Live triage of both Gmail + Outlook was proven separately under #1114; the restored-preference classification effect is proven on all three OSes via the agent's triage path. A live single-shot demo can be added on request.