MoriMori
Mori

Memory

Mori keeps a persistent, self-maintaining memory of who you are — one plain markdown file per fact, stored locally, curated by a nightly pass, and walled off from anyone who is not you.

One file per fact

As of 1.7.0, Mori's long-term memory is a directory of markdown files, one file per fact, living in the app's data folder (per profile and workspace). Each file carries frontmatter — name (a kebab-case slug, the filename), description (one line, capped at 200 characters — this is the recall key, written to be findable later), type (user, feedback, project, or reference), created and updated timestamps, links to related memories, and an optional pinned flag — followed by the fact itself as the body. Feedback-type memories get structured Why: and How to apply: sections so a correction is stored as something actionable, not a bare one-liner.

Memories can reference each other with [[wiki-style links]] in the body; link targets are parsed out automatically and recorded in the frontmatter. Because everything is plain files on disk, you can read, grep, and edit your own memory with any text editor — and the new Memory page in the app gives you a browser for it.

If you were on an earlier version, the old flat memory.json store is migrated automatically the first time 1.7.0 touches memory: every entry becomes a file (pinned entries stay pinned, your profile paragraph becomes a user-type memory), the original is kept untouched as memory.json.bak, and a sentinel file makes the migration strictly one-time — a re-run can never clobber newer content. Signed-in sync moves per-file with last-write-wins stamps, and deletions travel as tombstones, so removing a memory on one machine actually removes it everywhere.

The index: a tiny map that is always loaded

Alongside the fact files sits INDEX.md — an auto-maintained table of contents, one line per memory (name — description), rebuilt on every write and hard-capped at about 1,800 characters. This index rides in the system prompt on every single turn, so Mori always knows *what* she knows, even when a fact's full body isn't loaded. When the index outgrows its budget, the oldest non-pinned lines drop from the index only — the files stay on disk and remain openable by name.

The index is frozen once per session: it never changes byte-for-byte between turns, which keeps the prompt prefix stable and prompt caching effective. The frozen snapshot from before — your synthesized profile paragraph plus pinned memories, capped at 3,200 characters — still rides alongside it. The index text also carries the write discipline as standing instructions: update-not-duplicate, and delete-or-fix when you correct her.

Recall: ranked by description, injected in full

Each turn, a background prefetch (capped at 800ms so it can never stall your message) ranks every memory's description against what you just said — token overlap weighted by rarity, the same mechanism skills use. The top 4 ranked memories are injected as full bodies, and pinned memories always ride regardless of relevance, all within a 6,000-character budget. When an injected memory [[links]] to another, the linked memory's description comes along as a one-line pointer — never its full body, so a chain of links can't blow the budget.

Anything the ranking didn't surface is still one call away: the memory_view tool opens any memory file in full by name, and the always-present index tells her which names exist. The injection block itself instructs her to weave in at most one remembered detail when it genuinely fits — never to recite or inventory what she knows.

Update, don't duplicate — and forget for real

Writes go through one tool, memory_write, with three actions — add, update, delete — and the discipline is enforced in code, not vibes:

  • A near-duplicate add becomes an update. Before any add, the store ranks the incoming description against every existing one; a match over the similarity threshold forces an update of that file instead of creating a twin. Rephrasing the same fact five ways yields one file, freshly stamped — not five.
  • Delete is first-class. Tell Mori a fact is wrong and the file is removed — by exact name or by a unique substring. If a substring matches more than one memory, she refuses and lists the candidates rather than sweep-deleting on a vague match.
  • Deletes leave tombstones. A deletion is recorded as evidence (up to 200 kept), which propagates through sync to your other machines and to the brain's knowledge graph — the nightly pass removes the matching brain page so a deleted fact can't be resurrected from the graph later.
  • Every write is scanned. Incoming text passes the same injection-and-credential scan as before; role-fakes, "ignore previous instructions", and live secrets are refused at the write, so nothing stored now can be replayed as a command later.

The older remember and forget tools still exist and now converge on the file store — a legacy remember becomes a well-formed memory_write add with the dedupe check applied.

You: actually the demo moved to Friday

Mori → memory_write(action: "add",
         description: "Finn's investor demo is Friday (Jul 24, 2026)",
         body: "The demo moved to Friday (Jul 24, 2026).")
     ← merged into existing memory "investor-demo"
       (update-not-duplicate).

What she captures automatically

After every real conversation turn, a cheap background pass on your provider's fast tier (or local Ollama when present) mines the exchange for zero to three durable facts: identity, preferences, projects and their status, tools and stack, people and pets, dates that matter, things you said you would do. Each fact is written as one short third-person sentence that leads with the names and nouns future-you would search for — the sentence doubles as the recall key. Explicitly excluded: one-off task mechanics, moods, pleasantries, and anything the repo, git, or filesystem already records.

Relative dates are the classic way memories rot — "the demo is Friday" is useless three weeks later. The capture model is instructed to convert relative dates to absolute ones, and a deterministic regex pass backs it up: surviving mentions of "tomorrow", "tonight", "today", "next week", and bare weekday names are annotated with the actual date — "Friday (2026-07-24)" — so the anchor never rots even if the model forgets.

Capture knows when not to run: rapid-fire chat is throttled to one pass per 20 seconds, scheduled runs and subagent turns are never mined, and stranger conversations never touch your memory. Everything is fire-and-forget — capture never delays a reply, and failures are silent.

There is one more capture path worth knowing about: the pre-compaction flush. When a long conversation is about to compact — the ~70% auto-compaction or your own /compress — the exact turns about to be folded away get one silent pass first: any durable facts not yet in memory are written through memory_write before the detail is destroyed. The flush is best-effort — it gets four seconds and never blocks compaction — but a fact it catches would otherwise have been gone.

Pinning: memories that cannot expire

Pinned memories are never auto-pruned, never dropped from the index, and always injected into every prompt regardless of relevance. Three ways to pin:

  • Say so. An explicit command — remember, don't forget, keep in mind — stores the fact as user-sourced, which pins it. Re-remembering a fact she captured automatically upgrades it to pinned.
  • The Save to memory button. Every assistant reply in chat carries a small "Save to memory" button. One tap pins up to 700 characters of that reply, stamped with the date, into permanent memory — she carries it into every future chat on every surface. Use it for gold: a workload template she nailed, a decision that must not drift.
  • The Memory page. Edit any memory file directly and set it pinned there.

The inverse still holds: contradict a stored fact and she is instructed to believe you and delete or fix the stale file rather than argue with her own notes.

Dream consolidation — and you can audit it

Once a day, while the app is idle and on AC power, Mori runs a consolidation pass over everything she has accumulated. As of 1.7.0 this pass actively *fixes* rather than just reporting:

  • Near-duplicate memory files merge — descriptions overlapping past the similarity bar fold the older body into the newer file and delete the twin.
  • Relative dates get absolutized in memory bodies, referenced to each file's own write time.
  • The dual store reconciles — every memory delete tombstones the matching page in the brain's knowledge graph, and found contradictions are fixed newest-wins, not just listed.
  • Playbooks and lessons dedupe — near-duplicate approaches merge, superseded lessons drop. These stores had never been consolidated before 1.7.0.

Every step is individually fenced: a failing step skips, and the sweep can never break the app. The whole pass writes a nightly report — one page per night, dated — that the Memory page's Dream log tab renders, so "what did she change overnight" is a thing you can actually read, with honest counts and noted gaps rather than invented totals.

The Memory page

Settings used to have a small memory viewer; 1.7.0 replaces it with a full Memory page, in the sidebar. Three tabs:

  • Memories — every memory file, searchable, with the exact INDEX.md recall table the model reads shown collapsed at the top. Open any memory to read or edit its description, type, body, and pinned flag, or delete it outright. Edits and deletes go through the same store as Mori's own writes — tombstones and all.
  • Entities — the entity registry (next section): every person, file, build, app, URL, and past chat she has learned to recognize, with how often each has been used.
  • Dream log — the nightly consolidation reports, newest first.

Everything on this page is plain files and JSON on your disk. Nothing here requires an account.

The entity resolver: slight terms find the real thing

"Email the guy from the demo." "Open that site we built last week." "Find my resume." Before 1.7.0, vague references like these either got a lucky guess or a wrong one. Now they go through a universal entity registry — a persistent store of things Mori has encountered, across seven kinds: build, file, app, person, url, chat, decision. It fills itself from existing choke points, no new surveillance: projects she builds, files she creates or you attach, people extracted during memory capture, pages she navigates, past conversations, installed apps. The registry caps at 400 entries with usage-weighted eviction — entities you actually use stick around, and anything seen in the last 24 hours is never evicted, because "the thing we just made" is exactly what needs resolving.

Resolution runs a ladder, deterministic first: exact name or alias match, then normalized containment and rarity-weighted token scoring, then — only if confidence is still low — a semantic pass through the brain when it's available, and for files a bounded recursive scan of Desktop, Downloads, and Documents (three levels deep, hard 400ms budget) so "my resume" finds Documents/job/Resume2026.pdf even though no one ever told her about it.

Two things make this land in conversation. First, a fast pre-turn pass detects vague-reference shapes in your message and injects a RESOLVED REFERENCES block — candidates, their real paths/addresses/URLs, and confidence — before she starts answering. Second, the resolve_entity tool lets her resolve mid-task. The contract in both is explicit: below-confidence means ask one short question, never guess. She will say "did you mean the ChainCats site or the Mewterns one?" instead of confidently opening the wrong thing.

Total recall: every past conversation, word for word

Session memory used to mean dense one-paragraph summaries — good for "what were we doing yesterday", useless for "what was that npm flag you mentioned three weeks ago", because the flag never survived summarization. As of 1.7.0, every conversation's full transcript is indexed for search on your machine: a dependency-free full-text index over the raw text, with your existing history backfilled once (including, if you're signed in, history synced from your account — pulled gently in the background).

Search is AND-first — results containing every query word outrank partial matches — scored by term frequency and rarity with a proximity bonus, so an exact phrase beats the same words scattered across an hour of chat. Every hit comes back with a snippet of the surrounding text. Reach it three ways: ask naturally ("what did we decide about the pricing page?"), the upgraded search_sessions tool, or type /sessions <query> for instant local search with no model turn. The summary-based session log still exists and still powers "pick up where we left off" openers — full-text search reaches where summaries can't.

Any conversation can also leave the app: /export (or the export_session tool) writes the transcript to your Downloads folder as clean Markdown or a self-contained styled HTML page. Live credentials are always scrubbed on export — API-key shapes, GitHub and AWS tokens, bearer tokens and JWTs, private-key blocks, and anything labeled like a secret are replaced with [REDACTED] whether you ask or not, because an export is a file that leaves the machine. --redact additionally strips injection-shaped content using the same scanners that guard memory writes. Strangers' bot chats are never indexed and never exportable from your history.

Long conversations

Within a conversation, a recent window of messages is always sent verbatim. Older turns do not vanish: a background pass maintains a running summary of everything that has scrolled out of that window — under 200 words, dense, preserving goals, decisions, names, file paths, unresolved threads, and your preferences while dropping chit-chat. The summary is persisted per conversation and prepended to the turn context on every send.

The summary refreshes incrementally as messages fall out of the verbatim window, anchored to message IDs so sliding histories cannot break the bookkeeping. The send path never waits on summarization — the summary simply lags the conversation by roughly one turn. And since 1.7.0, compaction is no longer lossy for facts: the pre-compaction flush writes durable facts to memory before older turns are folded away. The practical effect: an hours-long session keeps its goals and file paths coherent, and what mattered in it survives into every future session.

Strangers on Discord and Telegram

When Mori runs as a bot, you — the owner — are verified by your immutable platform account ID, not a display name, so impersonation is impossible. Everyone else is a stranger, and strangers face a hard wall.

Before any stranger turn, memory suppression flips on: memory refreshes return nothing and the frozen memory snapshot is blanked, so not one byte of your saved memory, playbooks, lessons, or task history can enter the prompt where it could be socially engineered out of the model. Strangers also get no tools — no files, no screen, no terminal, no machine access; their answers come from the model alone. The wall runs both ways: nothing a stranger says is ever captured into your memory, their conversations are excluded from summarization, the pre-compaction flush checks for stranger sessions and skips them, and their chats never enter the full-text session index.

Strangers do get their own memory — just never yours. Each non-owner gets a per-user history keyed to their platform account ID, kept to a 36-message window, so Mori greets returning server members by name and picks up where the last chat left off. Your own bot chats work the same way per channel: a 36-message live window backed by a stable conversation ID, restored in full after an app restart. And approval gating survives the distance — irreversible or costly actions from a bot chat still require your explicit yes, with typed approval IDs that expire in two minutes and fail closed on anything but a clear yes.