Plan Review · Mem0 Migration

Outcomes AI Chat → Mem0 (Zep swap inside Xano #8497)

Moving Orbiter's Outcomes chat memory off Zep and onto Mem0 as a recall layer for distilled memories — not a transcript store, not the canonical profile store. Built and verified on a sandbox clone first; the live demo endpoint stays pristine until a gated go.

Endpoint openui05/dispatch #8497 Group 1276 · C5i2nPpF Workspace 3 · xh2o-yths-38lt Outcomes-first · 100% before the other 3 chats
Why this doc: Mark's migration spec opens with an Implementation Readiness Callouts block — 6 open decisions + 1 required guardrail he says to resolve before building. The next section answers all 7 directly, grounded in the real #8497 script and table schemas. Everything else here is the build + verification plan around those answers.

1.Mark's open decisions — answered

Mapped 1:1 to Mark's callout titles. Green = decided & in this pass.   Amber = deferred to that surface's own migration.

1 · Transcript writes DECIDED — NO CHANGE
Which API writes user/assistant messages? Does dispatch write transcript rows itself?
#8497 writes no transcript today (verified — there is no DB add to any conversation table in the script) and it won't start. Mem0 is a distilled-memory layer, never the transcript store — "do not send every raw turn to Mem0." Any outcome_conversations #689 persistence stays exactly as-is. First-party transcript persistence would be a separate endpoint, out of scope here.
2 · Meeting prep identity DEFERRED — NON-OUTCOMES
Bridge meeting_prep_context_idconversation_id before wiring Mem0.
Meeting Prep surface, not Outcomes. Resolved during the Meeting Prep migration (after Outcomes is at 100%): add meeting_prep_context_id onto copilot_conversations as the bridge before wiring its run-scope Mem0. Does not touch the Outcomes swap.
3 · Idempotency & retries DECIDED — SOURCE_ID + DEDUP
Deterministic source_id; skip/upsert/create on repeat; persist in ai_memory_event_log.
ai_memory_event_log does not exist (verified — only copilot_memory #658 / copilot_memory_log #659 from the Zep/copilot era). We will not add a table to the demo-critical path in v1. Instead: (a) deterministic source_id in Mem0 add metadata = outcome:{suggestion_request_id}:{hash(query)}; (b) infer:false; (c) dedup approved memories vs the retrieved set. A re-dispatched identical turn becomes a no-op. A durable event-log table is an explicit fast-follow once Outcomes is proven.
4 · Self-context metadata DECIDED — N/A TO MEM0 PATH
Store fn name / id / contract version / timestamp / hash, not just self_markdown.
self_yaml_context arrives as a #8497 input (frontend-generated via get-all-user-context_v2 #13076) and fills the [YOU] context slot only — it is never written to Mem0. No self_markdown blob lives in Mem0, so the staleness-metadata concern doesn't arise on this path. The recommendation applies only if/when self-context snapshots are persisted in Xano — out of scope for the swap.
5 · Streaming & failed turns DECIDED
Persist user before LLM, assistant only on success, extract after a stable row, no partial/failed to Mem0.
#8497 is non-streaming — one synchronous POST that returns the full result. The extractor + Mem0 add run at end-of-stack, after routing completes (the exact slot the Zep write occupies today). Partial/failed turns are never written; the extractor decides per completed turn. Maps cleanly to Mark's rule with no restructuring.
6 · How I Know persistence & summary loop DEFERRED — NON-OUTCOMES
Treat master_person_relationship #729 as the active store.
How I Know surface, not Outcomes. Resolved during the How I Know migration: #729 is the store; #8581 interview-only, #8582 load, #8583 save; Mem0 holds the durable individual memories behind the UI summary. Does not touch the Outcomes swap.
★ Required guardrail · Ownership checks DECIDED — IMPLEMENT
Verify the run row belongs to auth.id before any Mem0 search or write.
When suggestion_request_id > 0, db.get suggestion_request and verify user_id == $userId (owner column confirmed = user_id (int); #8497 is auth = "user" so $userId = $auth.id is trustworthy). On mismatch or missing row → skip all run-scope Mem0 search + write, degrade to user-scope only. User-scope is always keyed by the authenticated user, so it is inherently owned.

2.Ground truth (verified this session)

3.Decisions

#DecisionRationale
D1Extractor = openai/gpt-5-nano via OpenRouter, Anthropic claude-haiku-4-5 fallbackMatches spec; OpenRouter key already wired; no OpenAI key. Verify slug on first run.
D2Build + verify on sandbox clone openui05/dispatch-mem0; leave live #8497 pristineDemo-safety. Cutover is a separate, Robert/Mark-gated step.
D3Mem0 recall fills the existing [NETWORK MEMORY] slotDrop-in; preserves the YOU/TARGET/COMPANY wrapping unchanged.
D4Save confidence:"high" only, dedup vs retrieved; medium/low skipped in v1Per spec save policy; conservative for the real Mem0 graph.
D5Cutover keeps Zep blocks, re-gated to $env.zep != "" && $env.MEM0_API_KEY == ""Honors "Zep stays in dev"; fully reversible by unsetting the key.
D6Ownership guard — verify suggestion_request.user_id == $userId before any run-scope op; mismatch → user-scope onlyMark's required guardrail. Blocks reading/writing another user's run memory via a forged id.
D7Idempotency — deterministic source_id in metadata + infer:false + retrieved-dedup; no new table in v1Event-log table doesn't exist; keep the demo path minimal; durable log is a fast-follow.

4.Implementation steps

  1. Verify the gpt-5-nano slug — one throwaway OpenRouter call for openai/gpt-5-nano → confirm 200 + content. The only remaining unknown.
  2. Create sandbox clone openui05/dispatch-mem0 in group 1276 = verbatim #8497. Known-good base.
  3. Add the ownership guard (top of stack, after $userId): set $run_id and $run_owned = (suggestion_request.user_id == $userId). Gates run-scope read and write. (D6)
  4. Patch the READ block → Mem0 V3 search: user-scope always; run-scope only if $run_owned. Defensive lambda pull → build [NETWORK MEMORY] → assign $mem_context, wrapper untouched. (D3)
  5. Patch the WRITE block → extractor + Mem0 add: build turn JSON, compute source_id (D7), call gpt-5-nano (Anthropic fallback), strip fences + decode, dedup & keep high-confidence (D4), foreach → Mem0 add with infer:false + scope routed by $run_owned; set $mem_ingested.
  6. Add temporary debug fields to the sandbox response (mem_recall, hit counts, run_owned, extractor raw, add ids); removed before cutover.
  7. Server-side verify (curl, user 15): Turn 1 states a durable preference → mem_ingested:true + visible in Mem0. Turn 2 related query → mem_used:true with the preference in context. Negative test: a non-owned suggestion_request_idrun_owned:false, zero run-scope traffic.
  8. UI smoke test (optional, reverted): one-line swap in _endpoints.ts/openui05/dispatch-mem0, select a draft, run a turn, confirm cards render + mem_used/mem_ingested. Revert.
  9. Gated cutover (Robert/Mark go): apply guard + READ + WRITE patches to live #8497, re-gate Zep per D5. Frontend untouched. Delete sandbox + probe.
  10. Update the Mintlify migration doc Outcomes section → done; tick off all 7 readiness callouts; record D1/D5/D6/D7.

5.Verification & safety

Primary
Two-turn curl (write-then-recall) + Mem0 dashboard shows the memory under user_id=15 / run_id=suggestion_request:{id}.
Ownership (D6)
Non-owned suggestion_request_idrun_owned:false and zero run-scope Mem0 traffic.
Idempotency (D7)
Re-run the same turn → no duplicate memory (same source_id, deduped).
End-to-end
Draft-select in the running app (pnpm dev + agent-browser) — mem_used/mem_ingested true, result cards unchanged.
Reversibility
With MEM0_API_KEY unset, sandbox/#8497 behaves exactly as today (graceful no-op).

6.Out of scope (this pass)

Orbiter · Outcomes → Mem0 migration plan · prepared for Robert + Mark review. Build occurs on a sandbox clone; the live demo endpoint #8497 is not edited until a gated go.