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: slices A · B · C shipped — the Proven scope is built. Written 2026-08-18; A–C landed 2026-08-18/19. D (follow-ups) and E (the group scope, behind the leak exam) remain.
Every example below is a real row from the production store (enovy's, 2026-08-29, lightly shortened). One memory = one row = one standalone sentence, scoped and dated.
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 | 2026-08-29 — witnessing is not ownership: the witness net on the page/forget path now catches only rows nobody owns (no counterpart). A promise to one person, or hearsay with a known sayer, reaches its owner alone — 「A and B talked about a concept; C doesn't have to know that A took notes of it」. 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. |
u:N lives only in
subject / with. New: a promise/open-loop names whom it is owed to in a
with field, validated against the present set like the subject is.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. Two triggers, and the sooner wins. ① The leave door: the moment the person leaves the chat — switches to another, goes back to the list, backgrounds or closes the app — the client tells the server so (a beacon), and the note fires eight seconds later. That is the truest 「settled」 signal there is: the person has closed the conversation, so the stretch is whole, and the note is waiting in place when they come back. ② A clock of memory's own, for the person who stays: a timer re-armed at every human action, firing two minutes after the last line — the same shape as the room's ring and every instrument deadline, re-armed on load for the same reason. 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 rides every private chat, and a group chat only with their 「In group chats too」 switch on (owner, 2026-08-19 — it rode everywhere until v929). | 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 · and the arc (built 2026-08-19): a closed row never loads on its own, but the row that CLOSED it carries it as 「; before that: drove a Model 3 (2024-03 → 2026-08)」 — one hop, fetched inside the one read seam for rows that already passed the filter. A reflection (a row the dream wrote, no room) is the arc already and carries no tail. | 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. Four doors in code: /open, /read (a cache-warm open never hits /open — the same reliable per-view signal the notice rail reconciles on), members-add, members-remove; all four go through _memory_resync, and a turn path never does. | 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.2:)
so every pair that already slept on 「no threads」 under the old prompt re-dreams once.2026-08-20/21, the dream's own four fixes: it dates a fold on the person's day (users.tz) · a pair whose open set has not changed is skipped (the fingerprint — no nightly call for stable facts) · a backlog over the 14-row window folds oldest-first · and a reflection has its own budget (DREAM_TEXT_CHARS 420 — an arc, not a fact) and is trimmed at a sentence, never mid-word: the first production dream cut all three of its reflections at 200 chars, 「…rather th」. The three on the box are a repair owed after the next deploy (reopen the parts, delete the cut reflections, re-dream).
built 2026-08-19 The owner's word for it: 「maybe this kind of memory reorg can be a kind of dreaming, the persona does it when they sleep.」 It has a name in the paper this page borrows its ranking from — Generative Agents call it reflection — and our trigger is better for a chat app than theirs: they reflect when accumulated importance crosses a threshold; we reflect on idle, in the daily sweep, after the harvest pass.
What it buys: the block reads as one arc per thing instead of a fact beside its own
correction (the marathon contradiction the owner caught is gone — 「running on November 2, 2026;
the date was originally October 12, then October 15」), and the block shrinks. What it keeps:
every step — the parts are closed, never deleted, so the page still shows the whole arc greyed
under the line that replaced it, labelled 「reflection」 where it would name a chat; and the
reflection's evidence is the union of its parts', so the marks on the bubbles stay honest.
POST /api/memory/dream runs it now for the caller's own rows.
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).The block gained a third section, and it is the smallest one in it: a single line per person who has said how they are addressed. Owner: 「make sure that the persona can address the user correctly. such as Mr. or Mrs.」
users.gender ∈ female · male · nonbinary · other, or unset) and
_ADDRESS turns it into what a host actually needs: the pronouns, plus what
to reach for if the voice reaches for a title.| Stored | What the hosts read |
|---|---|
female | a woman — she/her; if your voice reaches for a title, Ms./Mrs. or 女士 |
male | a man — he/him; if your voice reaches for a title, Mr. or 先生 |
nonbinary | non-binary — they/them, and NO gendered title |
other | does not use those categories — they/them unless they say otherwise |
| unset | nothing at all — see below |
memory_block still returns
None when all three sections are empty — so the commonest room sends exactly the
two blocks it sent before this existed, byte for byte. Same bargain slice A made.memory_on: that switch is about remembering you between chats,
and being addressed correctly in this one is not a memory, it is courtesy. Somebody
who turned memory off did not ask to be misgendered.Where the human sets it: Me ▸ Edit profile ▸ Gender — the house dropdown, over the inclusive set, with Prefer not to say leading. It saves on pick, and it is never shown to another person: no other user's profile payload carries it.
And it is asked once at the door (v1036): the signup identity step — the screen
that takes the display name — carries the same question, the same five options and the same
leading Prefer not to say, riding /api/signup. The reason is the whole point
of the field: a host addresses somebody from its first message, and a profile row
filled in a week later cannot fix the greeting that already went out. It stays optional at both
doors, and an unrecognised value stores as unset rather than being refused — the same
bargain /api/me/gender makes, for the same reason.
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 mark on your bubble | Third and final shape (2026-08-19, the owner: a capsule 「breaks the flow of the chat」). The note is a small grey mark on the corner of your own bubble the memory was taken from — the same family as the reaction pill, one glyph, nothing added to the stream. Tap → a sheet (a bottom sheet on a phone; a popover under the bubble on a wide screen with a mouse), simplified to the owner's sketch: ◘ Tess, Yu Hua remembered: · the line · undo · a rule · Manage all persona memories — the mark's own glyph leads, in place of a title; the names wear their seat colour. Grey at rest, a faint coral only while its own sheet is open. Several memories from one line = one mark, several rows in the sheet; one memory from several lines = the mark on the last of them. The chat-info page lists 「what they've kept from this chat」 as the durable home. What follows is the second shape, kept for the record: a grey system capsule in the chat, right after the last line it read: "Warren noted · owns a flat in Xuhui · undo", one line per fact, each with its own undo. Undo deletes the row (the only physical delete in the system is the human's) and the struck line stays struck — a record of the striking, not a hole. One capsule per persona per stretch, so a chatty evening is one capsule, not twelve. It replays in place forever. Why the chat and not the rail (owner, 2026-08-19: 「the UX is weird if a toast shows up 2 mins later; my mind has already moved on」): a rail push is context-free and so has to be timely; a line in the chat carries its own context and can arrive whenever it likes. The rail is left to the one writer with no chat the person is looking at — the daily sweep. | 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 | As built (v923, the owner's spec): Me › Persona memory — the field, the account switch, then By persona: one house persona row per host that holds memories about you, a remembers/forgets sign in the trail; tap → that persona's own page (its 「forgets me」 switch, its memory rows with the meter). The original brief: 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 verbatim for the personas in your private chats; a switch right under it, Use in group chats (default off — owner, 2026-08-19), lets it ride chats with other people in them. On the page (v931) the field sits bare under the kicker What they should know about you; the account switch is a bare Memory under Real time remembering in chat. Like every memory switch it deletes nothing: off, the note stays written and a group room simply does not read it. | 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. |
| the kind glyph | v1027 (owner: 「it helps me to track and debug」): a small
muted glyph before the sentence on every memory surface — the pages, chat info's list, the mark sheet —
for the three kinds that behave differently: ⟳ thread · → promise / open loop · 〰
heard (world). A plain fact wears nothing, so most rows are unchanged — one glyph, not a fourth property
line; the tooltip names the kind. v1030 carried it the last step: the sheet reads from the
MEM_MARKS projection, which was copying ids · text · subject · counterpart · whos · hosts and
dropping kind on the floor — the renderer was asking for a field the projection never carried,
so the bubble's own callout showed the sentence bare. The projection keeps kind now, and every
mark since 08-19 wears its glyph there too. | debugging need made visible, cheaply |
| the mark speaks the present tense | v1028: the memory_note EVENT bakes a
copy of the sentence at harvest time, and the sheet replayed that copy — so an update, a dream fold or a data
correction moved the row while the log's copy stood stale forever. The replay's memory_note
branch now re-reads each LIVE line's row from the store (by its first id) and serves the current sentence.
⚠ A struck or closed row keeps the baked text — that line is a record of what was noted then, not
a claim about now — and the log itself is never rewritten. v1029: the fix could not reach anyone on its
own, because a client serves a chat from its saved copy and fetches only the delta, and a room where
nothing new happened has an empty delta: the baked text the fix exists to replace is exactly what the cache
preserves, through any number of refreshes. The v932 lesson again — a stored turn repaired server-side is a
shape change with an unchanged count — so CACHE_SCHEMA p3 → p4 makes every client discard
its saved bodies once and re-read whole. | a repaired row is worth nothing until the cache lets it through |
| 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. | — |
MAD_MEMORY_DAILY_CALLS, default 2,000 ≈ $1/day on flash; 0 disables) —
past it the seam raises before the wire, a harvest holds its bookmark (read tomorrow), a
dream waits, one log line, nothing lost; ② the re-run fires only when the bookmark
moved — no progress, no re-run; ③ 「dream now」 once an hour per person.
Visibility was already there: every memory call is billed under kind="memory"
in the cost tally.
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) | the write bar (2026-08-20): two tests in the prompt (still known in a month · changes how you speak to them), the refusal stance, anchored importance with a floor of 6 (promises exempt at 3), a per-harvest cap of 4 rows per person in code, the daily cap of 12, and the toast with undo. Was: a bare ≥ 3 threshold — the model scored everything 5–7 and it never bit. |
| 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 world fact held as truth (owner, 2026-08-19: 「Dune opens this week might be from Amy's memory … if the persona takes that as absolute truth there may be a problem」) | a world row is hearsay, and rendered as such: its section is headed you heard — in conversation, unchecked; it may be wrong or out of date; every line carries (heard from Amy on 2026-08-19) — the speaker (kept in counterpart at harvest; named only when they are in the room) and the day it was heard; and the tail rule's one exception: hold it loosely, say who mentioned it if that helps, check before stating it. Never dreamt, never able to close a personal row (the router is same-subject). Measured by the recall exam's hearsay probe: before 0/2 hedged (「Yes. Opened in August.」), after 2/2 (「You mentioned it opened… I haven't checked myself — hearsay」), recall unchanged 9/10 both arms. |
| 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. v957: when the candidate has no vector (the embedder down, no key) the shortlist is ranked by words, newest first on a tie — never by id, which showed the router the oldest five rows and let a duplicate through (found on the owner's machine, where Gemini is unreachable; the smoketest now fails without it too). |
| 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. |
| SOTA app | the write policy (researched 2026-08-20) |
|---|---|
| ChatGPT (bio tool) | Writes only when the user explicitly asks, or the info is 「useful in future conversations and valid for a long time」 / 「likely to change your future responses」. Explicit don't-list: random/trivial/overly-personal facts · short-lived facts that won't matter soon · details lacking clear future relevance · redundant info. Everything else lives in a separate automatic tier (~40 recent conversation summaries + aggregated insight sections) that shifts over time and is never called a memory. |
| Claude | A synthesized running summary per project — 「additive, not a running transcript; extracted facts and preferences, not a full log」. Project-scoped; incognito chats. |
| Character.ai | Three layers: the context window (the chat serves the chat), manual Pins/Story Memory, and auto Facts extraction as a paid tier — after their own admission that offhand remarks were becoming permanent facts. |
| Gemini | Saved Info (explicit, editable) + an automatic user summary. Their documented failure: treating saved info as a must-obey instruction — over-use, the read-side twin of our over-write. |
| mem0 | 「A good extraction prompt learns to refuse」 — their own docs teach refusal with the smallest example returning nothing. |
| Letta | Core memory 「small and curated — a few hundred tokens」; the agent writes only when something jumps out mid-reasoning, not by sweeping the transcript. |
| Zep | Extracts liberally but rates every fact against a custom rating instruction and reads above a minimum rating — the floor made real. |
holders — the persona slugs that HOLD it;
persona is only the origin. Hosts that heard the same stretch with the
same audience read it once: one extraction, one routing, one row held by all
of them (÷N on the bill). What is a host's own — a promise, an open loop — is still written
under the one host that made it (the extractor names the host when the stretch is shared).
A later joiner reads its own, shorter stretch and holds only what it heard (the join-floor
rule, unchanged). A fact one mind already held, heard again by all, is no new row: the
router's NONE names the row and the others join it. An UPDATE hands the corrected
state to every mind that held the old one (a mind left holding a closed row would know
nothing at all). The dream folds only rows the same minds share — per (holders set,
person) — so a fold can never take a fact from a mind that held it; its reflection inherits
the set. The page lists each fact once under Memories about me with the minds that
hold it in its foot, and under every holder's own page; one Delete anywhere is the end
of it everywhere — delete was always by row, and now the row is the fact.
witnessed (who was in the room) still gates
the read; holders only says which minds may read. The read seam asks 「does
this persona hold it」 in SQL (json_each) and nothing else moved; the leak exam
re-ran green on both arms. Migration: every older row is stamped
[writer] at startup, and once — flagged in settings — the duplicates the old
write left behind (same person, same room, the same sentence by text or by meaning ≥ 0.92)
fold into the lowest id, which gains the others' holders, evidence and witnesses; the
others are closed under it, never deleted. Fourteen pins; six older pins flipped.
relationship
rows are shared like facts — one extraction cannot produce a per-persona impression, and
what is truly a host's own is its promises.
memory_topics); the client only paints.
counterpart); the block's section reads you heard — in conversation,
unchecked; it may be wrong or out of date, each line ending (heard from Amy on
2026-08-19) — or (heard in an earlier conversation, 2026-08-19) when the
speaker is not in the room, so an absent person's remark is not named to the others; and
the tail rule gained its one exception: what you HEARD about the world is hearsay, not
knowledge — hold it loosely, say who mentioned it if that helps, and check before you state
it as fact. Hearsay never hardens: the dream is per person and never folds world rows,
and the router compares same-subject only, so a world candidate can never close a personal
fact. A/B, same exam, n=2 each: hedged 0/2 → 2/2, flat 2 → 0; recall 9/10 in both
arms; world rows carried absolute dates in both (the v922 rule held). Six pins.
recent.md path — stays on
the table (§8, still open); today the cost of keeping them as hearsay is small and the
small talk is real.
| promise | the fault — and one evening it bit | now |
|---|---|---|
| don't forget a host loses what it should keep |
the store's opening date was stamped at the first harvest — a deploy's first conversation fell before it and was never reade.g. You deploy at 10:00. Amy opens Tess at 10:05 and says she is switching jobs. Nobody else chats until 14:00, when Bob's pause triggers the first harvest — which stamps the store's opening date 14:00. Amy's 10:05 line is now 「before the store opened」 and Tess never learns about the job. | stamped at app startup — 10:00 |
| a pass capped for one person still advanced the cursor if a world fact survived — the capped person's lines shreddede.g. Amy has had a chatty day: 12 facts about her already written (the daily cap). In the evening she says 「I'm moving to Suzhou」 and, in passing, that a film opened. The film is a world fact and survives the cap; the pass counts as clean; the cursor steps past the Suzhou line — and it is never read again. | any cap holds the cursor; Suzhou is read tomorrow | |
| a leave during a running harvest was lost; a backlog longer than one slice (60 lines) waited for the next leavee.g. Amy pauses two minutes (the harvest starts), then types one more line and closes the app. The leave door finds a harvest 「already running」 and does nothing — the last line waits for the daily sweep at 04:00. Or: a 90-line evening — 60 fit one slice, the other 30 wait until the next time somebody leaves. | both re-arm at once (2s later) | |
| a backlog was read from the back — the oldest lines fell offe.g. The same 90-line evening: the slice took lines 31–90, and the cursor jumped to 90. Lines 1–30 — where Amy said the one thing that mattered — were never read by anybody. | front-first: 1–60 now, the cursor stops at 60, 61–90 next | |
| a host that left kept its cursor — re-seated, it read from mid-absencee.g. Yu Hua is removed from the chat at line 40. Amy and Tess go on alone to line 100. Yu Hua is invited back. His cursor still said 40 → his first harvest read lines 40–100, everything Amy told Tess while he was gone. | leave drops it; re-seated he reads from line 100 | |
| one human with two hosts counted as a 「group」 — no personal memory loadede.g. Amy opens a chat with Tess and Yu Hua. Only one human — but two hosts made it a 「group」, and a group loads no personal rows: neither of them knew a thing about her, though each knew her well one-to-one. | private = one human, whatever the cast | |
| a promise made with two humans present (no single counterpart) never loaded anywheree.g. Amy and Bob in one chat; Tess says 「I'll send you both the reading list」. The row is a promise by Tess with no single counterpart. The block loaded self rows only when their counterpart was present — NULL is never present — so the promise was unreachable: Tess forgot it, in every room, forever. | loads when a witness (Amy or Bob) is present | |
| a stretch the model refused whole raised, and the timer re-ran it forever; the sweep could double-harvest beside a live timere.g. The model returns five candidates all about someone not in the room; the code refuses all five and RAISES; the two-minute timer re-arms and re-runs the same stretch every two minutes all night, paying for each. Separately: the 04:00 sweep starts on Amy's room while her timer's harvest is mid-flight — two harvests over one slice, each fact written twice. | refusals log and the cursor moves; the sweep sees the running flag and skips | |
| undo of a wrong 「correction」 left the fact it had closed — closede.g. Tess holds 「Amy drives a Volvo」. Amy jokes 「sold the Volvo, I walk now」; the router writes the joke and closes the Volvo row with it. Amy taps undo on the joke. The joke dies — but the Volvo row stays closed: Tess now has no car for Amy at all. | a struck row reopens what it closed — the Volvo is back | |
| don't confuse wrong day, wrong person, wrong label |
lines carried no day; 「yesterday」 resolved against harvest time, in UTCe.g. Monday 1 Sep, 23:30 in Shanghai, Amy says 「I ran the half yesterday」. The harvest runs after midnight UTC, on what it thinks is 2 Sep, so 「yesterday」 became 1 Sep. She ran on 31 Aug. | each line carries its day in the speaker's clock; the clock survives a restart |
| two humans in two time zones shared one clocke.g. Amy (Shanghai) and Cara (New York) at 02:00 UTC — Amy's Tuesday 10:00, Cara's Monday 22:00. Cara says 「today was rough」 and the line was stamped with the room's clock, Amy's: Tuesday. | a line carries its own clock — Cara's Monday | |
an UPDATE with a back-dated valid_from closed the old row before it was written (a window running backwards)e.g. 19 Aug: 「Amy lives in Ningbo」. 25 Aug she says 「I moved to Suzhou in June」 → UPDATE, the new row's valid_from is June — and the code closed the Ningbo row in June: a row that existed from 19 Aug to June. | the old row closes today (25 Aug) | |
| the dream's reflection took the earliest part's start, and cosine-clustered unrelated facts into one threade.g. Three rows: 「running the 12 Oct half」 (5 Aug) · 「the half moved to 2 Nov」 (19 Aug) · 「adopted a second cat」 (10 Aug). Cosine ≥ 0.78 put all three in one 「thread」 → one reflection 「Amy is running on 2 Nov and adopted a cat」, valid from 5 Aug. Same-person rows score 0.76–0.83 whatever they are about. | the model names the threads; the reflection starts where its latest part did | |
| a part the dream folded showed as 「No longer true」 — it is still truee.g. The dream folds 「drives a Model 3」 · 「returned it」 · 「now drives a Volvo」 into one line. The page listed all three parts under No longer true — including 「now drives a Volvo」, which is exactly what is true. | its own group, 「Folded into a summary」 | |
| the page built 「2026-07-15」 at UTC midnight — west of UTC it read 14 Julye.g. Amy's row says valid from 2026-07-15. Cara opens the same page in San Francisco: midnight UTC on the 15th is 17:00 on the 14th there — the page said 14 Jul. | local midnight — 15 Jul everywhere | |
| the extractor's worked example named the lowest id present, not the first speaker — the model copied the wrong persone.g. Bob (u:3) and Amy (u:17) in a room; only Amy talks. The prompt's worked example said 「u:3 is Bob」, and the model, copying the shape of the example, filed Amy's facts under u:3 — Bob now 「teaches cello」. | the example names the first speaker in the slice | |
| a fact about a present person who had switched memory off was a refusal — held the cursore.g. Amy and Bob chat; Bob has Memory off. Tess hears 「Bob got promoted」 → a candidate about Bob → refused. If every candidate that stretch was about Bob it was 「all refused」, the pass raised, and the cursor stayed — Amy's own lines in that stretch were re-read every two minutes. | silence; the cursor moves on | |
the rail notice and the undo's revoke used two key shapes — undo left the notice standinge.g. The 04:00 sweep posts 「Tess noted: …」 to Amy's rail under the key mem:tess:17. Amy deletes that row on the page; the undo revoked mem:room-8872:17 — a key nothing had. The notice stayed in her rail, pointing at a row that no longer existed. | one shape, mem:{room}:{uid} | |
| the recall exam counted 「broke」 inside 「broken」 as a recitatione.g. Tess opens a fresh chat with 「Nothing's broken this morning, I hope」. The exam saw 「broke」 and scored it as Tess reciting 「I broke my wrist」 unprompted — a false alarm against the tail rule. | word boundaries (and a * marks a stem: 「peanut*」 takes 「peanuts」) | |
| don't leak someone learns what they shouldn't |
a 「world」 row loaded in every room of that host — a mis-filed personal fact reached a stranger's chate.g. Amy tells Tess 「my race moved to 2 Nov」; the extractor files it under world: 「the Hangzhou half moved to 2 Nov」. Bob opens his own chat with Tess — and Tess mentions the Hangzhou half's new date. A fact from Amy's private chat, in Bob's. | a world row needs one of its witnesses in the room |
| a line deleted-for-everyone was still harvestede.g. Amy pastes her salary by mistake and deletes it for everyone within ten seconds. The two-minute harvest read the original line anyway — the tombstone was never consulted — and Tess wrote 「Amy earns …」 into the store. | the tombstone is read; the line is skipped | |
| lines said while a kit was on the table were harvested after the gamee.g. During a werewolf game Amy says 「I'm the wolf — I killed Bob last night」. Harvests are skipped while the kit is mounted, correctly. The game ends; the next harvest's slice starts where the cursor stopped — before the game — and reads straight through it: 「Amy killed Bob」. | kit spans skipped; the cursor walks past them | |
| under a history floor the note event replayed every line — a joiner read what was said before theme.g. Amy and Tess talk alone; Bob is added later (his history floor = his join). The note event written before he joined replayed to him whole — 「Amy is switching jobs」 — though the line it came from is hidden from him. | a note keeps only lines whose source line is above the reader's floor | |
| a history-private room pushed the live capsule over the shared streame.g. A room where joiners cannot see the past. The live memory note went out over the room's one SSE stream — so a newcomer who happened to have the room open got it in real time, floor or no floor. | no live capsule there; it lands at the next replay, floored | |
| undo deleted the row but the room's note still replayed the line, and the block still held it until the next opene.g. Amy taps undo on 「Amy is switching jobs」. The row dies — but on reload the mark's sheet still lists the line (the note event was untouched), and Tess's block still carries it until somebody re-opens the room: two turns later Tess asks how the job hunt is going. | undo strikes the line, rebuilds the block, revokes the rail | |
| the page could not reach a world row or a shared promise — nobody could delete theme.g. That mis-filed 「Hangzhou half」 world row (above): it is not about u:17, so Amy's page did not list it — she could neither see it nor delete it. Nor could anyone else. | witnesses see and strike them; a stranger cannot | |
| a struck line replayed as a holee.g. Tess and Yu Hua both noted 「switching jobs」, one line in the sheet; Amy strikes it. After a reload the line was simply gone — she could not tell whether she had struck it or it had never been noted. | replays struck, no undo — a record of the striking | |
| a joiner's first harvest read the whole room (the owner's second test, above)e.g. Yu Hua is seated at line 22 of Amy's chat with Tess. He had no cursor, so his first harvest read from line 0 — and wrote everything Amy had told Tess alone into his store, including a promise Tess made that he then 「owed」 her. | from its join; a founder from line 0 |
users.tz, written on change) and _dream_today reads it, so a
GMT+8 reader no longer sees 「… → yesterday」 on every overnight fold. ② The dream grew its
two missing bounds: a fingerprint — a (host, person) pair whose open set has not
changed since its last completed dream is skipped, so three stable facts stop
buying a flash call every night for life (a fold clears the print; a new row wakes it) —
and a backlog over the 14-row window is folded oldest-first, each fold moving the
window on, so the tail converges instead of starving. ③ The leak exam ran at n=8, both
arms: zeros everywhere — owner present 0 · owner absent 0 · other seat 0, 16/16 runs
planted, $0.19 — with a new vacuity guard proving the zeros are real: the group's
block demonstrably carried the secret in the groups arm (2/2) and demonstrably did
not in the v1 arm (0/1), so (a) measured discretion, not absence. Slice E's gate number now
exists; E still ships only on the owner's word. The fourth item — the standing note riding
groups — was re-decided v930: private chats by default, a switch.
valid_from 2026-07; 「Amy is giving a talk at the battery conference in Shenzhen
on 14 November 2026…」 as an event with no valid_from. Both rules
held on the first live try. The cat was the third candidate and the router declined it — a
NONE I could not explain afterwards, because verdicts were not logged; they are now.
valid_from is when the state
began, and I had put an event's own date in it — so a plan to run on 12 October
got a start of 12 October, and when the dream closed it on 19 August the window ran backwards.
The fact 「Amy is running on 12 October」 became true the day she said it; the race's date
belongs in the sentence, where it already was. The column's definition in §1 is corrected; the
block renders valid_from only ever as 「since」; the page names every date —
written down · which chat · since … / … → … / until … — in one format.
closed_by pointing at the reflection. Same
close-not-delete discipline, so nothing is lost and the page still shows every step.
u:42, the model copied it, and the code correctly refused a fact about somebody
not present — silently, as a clean pass — so the sample now names the present person's real
id, refusals are counted, and a stretch refused whole leaves its cursor. And the leave door
could not be the SSE stream's close: a boot opens the room stream twice and the closed one's
generator sits in its 20-second wait, a phantom watcher — so the client says it left.
visible_memories asks 「what may this host have in its head right
now」; the page asks 「what does the app hold about me」 — so it shows closed rows with
their arc and rows formed in chats you are not currently in. Answering the second with
the first would hide exactly the lines somebody opened the page to find. It is the store's
third reader and the only one that is not a model's.
exam/memory_recall.py (since 2026-08-19 also carrying the hearsay probe, above) — five facts told in a
private chat, harvested, then asked in a brand new room with the same host: all
five came back, in both runs. The whole run costs $0.011, the harvest itself
$0.0005. Unprompted recitation 0 — the host does not open with what it
knows. Attribution 0 then 1: the 「never say where it came from」 clause holds
most of the time and not always, so the honest reading is that it reduces the
tell rather than removing it. That number is the one to watch as slice C lands, and it
wants an n larger than 1 before anybody claims a rate for it.
room_open endpoint alone, so every other caller of
Room.open() — the exam, the greenroom a build mints, whatever is written next —
opened a room whose hosts remembered nothing, and the symptom was indistinguishable from a
store that had never been written. The hook now belongs to the room's own door. A
door's own duties belong to the door.
last_read migration seeded old memberships as
already-read.
V1_PROVEN_SCOPE, and without them a
group conversation would re-add the same fact forever), so
memory_neighbourhood is a second named reader — bounded to
「rows about the people already being written about」, and its output never reaches a
room's context. The lint pins exactly two readers, by name, and fails on a planted third.
exam/memory_leak.py): v1 scope and
--scope groups, reporting (a) owner present · (b) owner absent, which must be
0 by construction · (c) the other seat. First smoke run, n=1, both arms: zeros everywhere,
with the confidence confirmed in the store. n=1 is not the number — the New still
waits on a real run at n=8.
V1_PROVEN_SCOPE holds the extracted rows out of groups;
the 400-char field is the human's own words, written knowingly for every host to know, so
§3's 「the memory_note always rides」 governed it. A smoketest check pinned that so it
stayed a choice rather than an accident — which is what let it be re-decided cleanly.recent.md path exists was ruled 2026-08-20: leave them as they are until growth (row ⑤) is built, then revisit.
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)