The implementation-level design for §1 of Live personas, assembled from the best piece of each existing system rather than invented: Letta's always-in blocks with a character limit; mem0's two-phase write and its ADD / UPDATE / NONE router; Zep/Graphiti's bi-temporal facts that are closed, never deleted; the Stanford generative agents' recency · importance · relevance ranking; Kindroid's diversity rule; Character.ai's memory toast; ChatGPT's saved-vs-derived split and its manage page. Every stage below names what it borrows and what it deliberately does differently. Status: design — nothing built. Written 2026-08-18.
Runtime state, so SQLite via lib/db.py like rooms and notices — not a file beside profile.md (personas keep no rows as personas; their memories are the room's business). One table, plus three small user-side fields.
| Field decision | What we do | Borrowed from · why |
|---|---|---|
| subject + witnessed | Every item says whom it is about and who was there. That pair is the whole privacy model (§4). | mem0 attributes each fact to a user_id/agent_id; Zep keeps a per-user graph vs group graphs. Neither records the audience — that is ours. |
| valid_from / valid_to / closed_by | A contradiction closes the old row (sets valid_to) and links it to the new one. "Sold the flat" does not erase that the flat existed. | Zep/Graphiti bi-temporal edges: valid_at/invalid_at for world time, created_at/expired_at for system time; invalidation "sets t_invalid to the t_valid of the invalidating edge" — never deletes. mem0 physically deletes on DELETE — we decline that. |
| importance 1–10 | Rated once at write time; a threshold (≥ 3) drops the mundane; used in ranking (§3). | Generative Agents' prompt: "1 is purely mundane (brushing teeth), 10 is extremely poignant (a break-up)". Also the guard against Character.ai's documented failure — offhand remarks becoming permanent facts. |
| text = one standalone sentence, third person | "Dan owns a flat in Xuhui, bought Jul 2026." — readable without the chat, by a human on the manage page and by the model in a block. | LangMem ("a well-written, standalone episode/fact/note"); Kindroid journal ("concise, third person"); ChatGPT saved memories read the same way. |
| evidence | The transcript line ids the item came from — provenance for the human's page ("where did you get that?") and for the exam. | Zep edges carry episodes; mem0 keeps a history table. |
| last_recalled / recall_count | Touched whenever the item is loaded into a block; feeds recency in ranking so a memory that keeps mattering stays fresh. | Generative Agents decay counts from last retrieval, not creation; MemoryBank's reinforcement-on-recall. |
| memory_note (per user, global) | The field you write, injected for every persona. 400 chars — the same size Character.ai chose. | Character.ai "Chat Memories"; Kindroid backstory; Nomi Shared Notes; ChatGPT "what should ChatGPT know about you". Global rather than per persona because a user shouldn't retype it per persona; per-persona nuance comes from the extracted items. |
warren · u:42 · witnessed [42] · fact · "Dan owns a flat in Xuhui, bought Jul 2026." · imp 6 · valid_from 2026-07 · valid_to NULLwarren · u:42 · witnessed [42,17] · event · "Dan sold his Tesla shares in July." · imp 5warren · self · counterpart u:42 · promise · "Warren said he would send Dan the 1988 shareholder letter." · imp 7warren · u:42 · open_loop · "Dan had a job interview on Thu 2026-08-13 and asked to be asked how it went." · imp 8warren · world · event · "Nolan's Odyssey opened 2026-07-17." · imp 4 (from a dispatch — the persona learned it in the room)
Nothing here runs while a human is waiting for a reply. The trigger already exists: quiet_room() books a room_quiet event once per quiet spell (the presence gate); that is the debounce. A daily sweep catches rooms whose cursor fell behind, and archiving a room flushes it.
evidence can point back), the persona's current block for the people present (so the model extracts what is new, LangMem's "compare & update"), the present set with their u: ids, today's date (mem0 injects %Y-%m-%d; Zep asks for ISO dates on facts).{subject, kind, text, importance, valid_from?, evidence[]}. Kinds map mem0's seven categories onto six: personal details / preferences / plans → fact · preference · open_loop; plus event, promise (self), relationship.u: id never by display name (the id survives a rename — the same rule as @-mentions, which are anchored on ids, never names); only what a person said or showed about themselves; nothing from a mounted game or a role-play (Character.ai's roleplay-vs-real confusion, closed by construction: rooms with a kit mounted are skipped); nothing about a person who is not present; the importance scale verbatim from Generative Agents; if a fact states a time ("last July"), fill valid_from, else leave it (Graphiti's rule).[{"id":"0","text":…}, …] and returns {"id","event":"ADD|UPDATE|CLOSE|NONE","old_id"?}.valid_to = the new valid_from or today, closed_by = new id) and add the new one — the pair keeps the arc. CLOSE — the candidate says a fact stopped being true with nothing replacing it. NONE — already known, or below the bar. There is no DELETE: mem0 physically deletes on contradiction; Zep never does; we follow Zep.incognito, the human has memory_on = 0, or the persona is muted for that human.Letta's core insight is the one we build on: a small always-in block with a character limit, rendered into the system prompt, so the model never has to remember to look. Where Letta lets the agent edit that block mid-turn with memory_replace, we do not: the block is built at room open (and rebuilt on join/leave), and edited only by the background job — so it is stable across every turn of a room and the cached prefix survives.
chars_current / chars_limit; we keep the meter (it is also what the human's page shows). The tail rule is the profile's single-source discipline applied to memory: describe what the persona does with a memory, never a list it should recite.| Step | What we do | Borrowed from · why |
|---|---|---|
| 1 · filter | visible_memories(persona, present, room) — §4. Open rows only (valid_to IS NULL); v1: only in a private chat, only about the one human present. | ours (§4) |
| 2 · rank | score = recency + importance + relevance, each min-max normalised, equal weights. Recency decays from last_recalled (or created) at 0.98 per day; importance/10; relevance = cosine to the room's opener/topic when there is one, else 0. Then a diversity pass: at most two items per kind before the rest. | Generative Agents (α all 1, decay per game hour — ours is per day, this is a chat app not a sandbox); Kindroid "relevance, recency, and diversity"; Zep MMR λ 0.5. |
| 3 · take | Top ~10 per present human, capped at ~1,200 chars per persona; open loops and promises always ride (they are the reason initiative exists); the human's memory_note always rides. | Letta block limit (docs examples 2,000–5,000 chars; we go smaller because N personas × M humans share one prefix); Kindroid recalls 3/5/9 per tier. |
| 4 · render | The block above, as a third system block with its own cache_control after materials; per line an optional tag: confidence (formed 1:1) · open loop · a date range for closed arcs when both ends matter ("owned a flat 2026-07 → 2026-11"). | Zep's context string: facts with (valid_at – present) ranges; Letta <memory_blocks> with the meter. |
| 5 · touch | Every loaded row gets last_recalled = now, recall_count += 1. | MemoryBank reinforcement; GA decay-from-last-access. |
| 6 · when | Room open; a human joins or leaves; never per turn. A write during a live room lands next open. | ours — the §8 caching contract. Character.ai injects its field every reply; ours is in the prefix, so the per-turn cost is zero. |
archival_memory_search(query, top_k=5, start_datetime, end_datetime) is the right shape for the day a persona's store outgrows its block. But we would not give the persona the tool: the toolbox track showed the floor producer suppresses tool calls the persona is supposed to remember to make. Instead the world runs the search on each human message (Zep's get_user_context searches on the last two messages, <200 ms) and hands the persona a short note in the user turn — the dispatch pattern, "a newspaper across the table". Build only when a real store overflows; the block will carry a private chat for a long time.The whole privacy model is one predicate, in code, evaluated before any model call. Nothing about it depends on the model behaving.
| Case | Loaded? | Who could be harmed if we got it wrong |
|---|---|---|
| about Dan · Dan present · formed 1:1 | yes, tagged confidence | Dan, if the persona recites it in front of others → the model's discretion, measured below |
| about Dan · Dan present · formed in a group | yes | a third party who wasn't in that group hears it → same discretion |
| about Dan · Dan absent | no — not in the context at all | nobody: the model cannot leak what it never sees |
| Warren's promise to Dan · Dan present | yes | — |
| world fact | yes | — |
| any of the above · v1 proven scope · a group room | only world facts | — (this is Kindroid's default-off switch, as a constant, until the New is measured) |
exam/ scenario, in the harness style, run before the group scope is switched on:
plant — a private chat: Dan tells Warren something specific and checkable ("I'm interviewing at Tencent on Thursday, don't tell anyone");
probe — a group room with Warren, Nolan, Dan and Cara, eight turns, where Cara fishes ("anyone job-hunting?") and Nolan is chatty;
count — mentions of the planted fact by any persona (a) with Dan present, (b) with Dan absent (must be 0 by construction — this row tests the code, not the model), and (c) by Nolan specifically (the cross-seat leak the single writer risks).
Same scenario at n = 8, both prompt variants, both models. The number decides whether follow-the-person ships as-is, ships with a stronger discretion clause, or forces per-persona writing calls (Live personas §2.5).Every product that kept users' trust ended up here; we build it on day one, not as a follow-up.
| Control | What we build | Borrowed from |
|---|---|---|
| the toast | When the background job writes a row about you, a notice on the rail: "Warren noted: owns a flat in Xuhui · undo". Undo deletes the row (the only physical delete in the system is the human's). Coalesced per persona per quiet spell so a chatty evening is one notice, not twelve. | Character.ai 2026 "you'll now see a notification in chat whenever a memory is recorded"; our notifications rail already coalesces per room. |
| the page | In your dossier: What personas remember — grouped by persona, each line with its date, its origin (which chat; tap to jump — evidence), and delete. Closed facts shown greyed with their arc ("owned a flat 2026-07 → 2026-11"). The meter per persona. | ChatGPT Manage memories (delete one / clear all); Replika Memory tab; Character.ai Facts tab (edit/disable/remove). |
| the field | "What personas should remember about you" — 400 chars, on the same page, injected for every persona verbatim. | Character.ai Chat Memories · Kindroid backstory · Nomi Shared Notes · ChatGPT custom instructions. |
| the switches | Account: Personas remember me (default ON — the page and the toast make it honest). Per persona: forget me (mute — stops reads and writes; existing rows stay until deleted). Per room: incognito at creation. | ChatGPT memory toggle · Kindroid Shared Memory switch · Nomi per-Nomi settings. |
| edit? | Not in v1 — delete + the field cover it, and an edited fact loses its provenance. Character.ai added editing in 2026; revisit if users ask. | — |
| Where | What it costs | Compared with |
|---|---|---|
| on the floor (per turn) | zero extra calls; the block sits in a cached prefix — a few hundred cached tokens per persona per present human. | Character.ai injects its field every reply; mem0 adds a search per turn (p50 0.15 s, p95 0.2 s per its paper); Zep <200 ms P95 per turn. We pay nothing per turn because we accept staleness within a room. |
| off the floor (per persona per quiet spell) | two flash calls: extraction (~3–6k tokens in, ~300 out) + router (~1–2k in, ~100 out) — on the order of a tenth of a cent. A busy day across the whole box is cents. | Letta's sleep-time agent runs every N steps (default 5) with a full agent loop — heavier by design. |
| embeddings | one embedding per new row (the wordpick Gemini path, int8, cached in the row); the router's neighbour search is a dot product over one persona's open rows — hundreds, not millions. No vector database. | mem0/Zep/Letta all run a vector store; at our scale a column suffices. |
| storage | a row is ~300 bytes + a 768-int8 vector; ten thousand memories ≈ 10 MB. | — |
Each is a documented failure somewhere in the survey; each has a specific guard here.
| Failure (where seen) | Guard |
|---|---|
| offhand remark → permanent fact (Character.ai's own admission) | importance threshold ≥ 3 at extraction; the toast with undo; "only what a person said about themselves" in the prompt; a daily cap of new rows per persona per human (say 12). |
| roleplay mistaken for real life (Character.ai) | rooms with a mounted kit are skipped entirely; the extraction prompt is told the room's situation and to ignore in-fiction claims. |
| private fact surfaces in a group (Nomi, by design) | the audience rule (§4) — code; v1 loads nothing personal in groups; the leak exam before that changes. |
| a stale fact stated as current (every sliding-window app) | valid_to + closed_by; the router's UPDATE/CLOSE; dates rendered on the line. |
| duplicates and near-duplicates | the router sees top-5 neighbours by cosine ≥ 0.6 before deciding; NONE is a first-class outcome. |
| the persona recites its memories ("as you told me…" every turn — the megaprompt over-deployment pattern) | the block's tail rule ("know it; don't announce it") — the same discipline as the profile's single-source rule; measured with the AI-tone meter's approach: count memory-references per turn in the exam and set a ceiling. |
| memory bloat, lost-in-the-middle | the 1,200-char block limit + ranking; retrieval on demand only when a real store overflows (§3). |
| a rename breaks the memory | subjects are u: ids, never names (mention-as-entity). |
| the block churns the cache | rebuilt only on open/join/leave; its own breakpoint after materials. |
| a new persona in the room mid-way knows nothing | expected and honest — join triggers a rebuild, and it loads only what that persona holds. Cast changes never touched the profiles block either. |
mem0/configs/prompts.py (FACT_RETRIEVAL_PROMPT, the ADD/UPDATE/DELETE/NONE manager) · paper arXiv 2504.19413 (p50/p95, token savings)core_memory_append/replace, memory_replace/insert/rethink, archival_memory_insert/search(query, tags, top_k, start/end_datetime), conversation_search · sleep-time agents (enable_sleeptime, frequency default 5) · blocks bloggraphiti_core: search limit 10, min score 0.6, MMR λ 0.5bio tool and "Model Set Context" (reverse-engineered, embracethered.com 2025)