Memory
The notes store, durable markdown memories, recall and remember, and the nightly consolidation pass.
Live.
Two stores sit behind one pair of tools. recall searches both. remember writes
to whichever fits.
The notes store
Operational state, machine-local, mode 0600, never leaving the box. Four record kinds share one file because they share one lifecycle: written once, searched often, occasionally closed.
| Kind | What it holds |
|---|---|
fact | a standing truth about you or the world you operate in |
loop | something owed, by you or to you, with an optional due date |
saved | a link, document or clipping parked for later |
write | an outbound change Otto made, plus how to reverse it |
This exists because before it, the only places a fact could live were CLAUDE.md edited by hand, the session's own memory which a fired trigger never sees, and trigger action text, which recipes had started abusing as a database. The price-watch recipe stored last week's price inside its own prompt and rewrote the prompt to update it. That is a key-value store spelled as a paragraph.
Search is substring-and-token, deliberately not fuzzy. The caller is a language model that can retry with a different word, and a scoring function that surfaced near-misses would let a stale fact answer a question it does not actually cover. Results sort due things first, soonest first, then most recently touched.
Loops are the part you feel
When you say you will do something, or you are waiting on someone else, it is
recorded with a due date if there is one, and it keeps surfacing in the brief
until it is closed. Closing is explicit: done or dropped.
How loudly a loop is allowed to speak is the escalation curve.
Writes and undo
After every calendar event or task Otto creates, it records a write note whose
undo field says exactly how to reverse it, in plain English, executable with no
other context. That is what makes "undo that" answerable tomorrow rather than only
in the conversation where it happened.
Durable memories
Knowledge that should outlive the channel lives as markdown files under the Claude
memory directories, so every Claude Code session in every project can read it.
Four types: user, feedback, project, reference.
The files are the source of truth. There is no index to rebuild.
recall reads them directly and forget deletes one. Because they are plain
markdown with frontmatter, they are equally editable by hand.
Nightly consolidation
At 04:00 the session distils itself into memory: what happened, what was decided,
what is worth keeping. consolidate_memory forces a pass out of band.
The division of labour matters. Do not hand-write memories the nightly pass would have caught anyway; do write the things only Otto saw, in the moment, on Telegram.
Surviving compaction
The session runs for weeks, and when its context compacts, anything that lived only in the conversation goes with it: a draft shown but not yet approved, a trigger armed a minute ago, the thread being worked. Durable state survives because it is on disk. Conversational state does not, and the first sign of the loss is Otto answering "approve it" with a blank look.
A hook fires before compaction and writes one note: what is held in the send queue, what fires within six hours, and the last few exchanges. The post-compact turn reads it back instead of asking.
It cannot serialise understanding, and it does not try. It writes down the facts that make recovery cheap, and it is deliberately forgiving: any failure inside it is swallowed, because a wedged compaction would be worse than a lost note.
When to reach for recall
Before answering anything about your history, your preferences, or a decision already made. Before asking you something you may already have said. When a trigger or a scan names a person or project with no context attached.
Nothing found is not evidence that it never happened.
Not built yet
These do not exist. The substrate each one reads is live, so every item below is a self-contained piece of work rather than a rewrite. The button copies a prompt that builds it in your own checkout.
When `remember` writes a fact, check it against existing facts on the same subject and supersede rather than append — old value retained with its date, new one current, the change queryable ("address: Milan → São Paulo, 12 Jun"); upsertNote just pushes today, so two contradictory truths coexist and BM25 picks whichever scores higher.
Every fact carries where it came from and when it was last confirmed, and recall renders "(from an email 4 Mar, unconfirmed since)", with a one-tap [still true?] appended to a message Otto was already sending; stops it stating an old address with the same confidence as this morning's flight time.
A long voice note with no question in it is parsed into structured records — loops, facts, people, decisions, places — and answered with one compact confirm list with per-item buttons plus a [just listening] that suppresses all extraction; they talks for a minute walking to the car and it lands as six typed records instead of a wall of transcript.
A `decision` note kind capturing the call, the options rejected, the one assumption that would flip it, and a review date that auto-arms a one-shot trigger firing only if that assumption visibly changed; six months later Otto answers "why did I pick Bun" in their own words instead of a reconstruction.
While the hourly scan is already reading the mail, cross-check open loops against new messages, calendar events, task completions and merged PRs, and propose closure with [close / still open] when the evidence is clear; nagging them about things they finished on Tuesday is the fastest way the sweep becomes noise they mutes.
Sentences stating a want with no date — "I should really learn Rust", "I want to see the Pantanal before we leave" — are filed silently as `want` notes with the date they said it; they never nag, never appear in the brief, and cost nothing, but nothing offhand is lost.
A `place` kind capturing restaurants, doctors, barbers and cafes with city, who recommended it, when and one line of why, captured by a tap on any message where a place gets named; when they land in a city the untried list becomes one brief line.
Every recall that returns nothing and ends with Otto asking them a question logs the query as a miss, and the weekly self-audit reports the top misses as "things you've told me that I couldn't find" with the specific memories to write; retrieval failure becomes a fixable list instead of a silent tax.
A `/otto:think` skill that recalls the history then argues the side they aren't on, one question at a time, refusing to converge until they names the constraint that decides it and ending with "so you're choosing X because Y" and nothing else; they stop using Otto as a mirror.
Nightly, detect the moments they corrected Otto ("too long", "stop pinging me about that", "always show BRL first"), file each as a scoped feedback memory with its trigger condition, compile the active set into a house-rules block injected into the brief, and offer the CLAUDE.md diff with [apply / no]; rules that never fire in a month get proposed for retirement.
Instrument recall to stamp hits and lastHit on every note and durable memory, then a quarterly pass surfaces dead weight, contradictions and near-duplicate memories written by different nights' consolidation; they only sees the contradictions, the rest is pruned on approval.
For each topic they keep circling — moving back to Italy, leaving the job, the two-currency mess — one living markdown page accumulates what they're said over months, dated, with contradictions flagged rather than smoothed; "read me back on Italy" returns the evolution, including what they're said three times without acting on.
Sample the frontmost-app and dirty-repo probe every five minutes into a rolling weekly log keyed by project, joined with calendar time, so "where did this week actually go" has hours per thing they cares about — never volunteered, never an app-shaming screen-time report.
On demand only, at most three options grounded in their `want` notes, where they actually is, the weather, the real length of the calendar gap and any loop that would genuinely feel better closed — with "do nothing" always one of the three; a short list from someone who knows what they already said they wanted.
Otto accumulates the questions that would make it better — a preference it's been guessing, a loop with no real deadline, two memories that disagree — and may never send one alone: at most one appended to a message they already started, a few times a week; curiosity that costs them no notification.
A `user` memory holds the three to five things they said matter this year, in their words with dates, and once a month Otto compares them to evidence — attention hours, loops closed by tag, what they actually talked about — sending two lines only when a gap is large and durable; no score, no streak, nothing when nothing drifted.
A unified timeline.jsonl of every message, email subject, event, note write and journal line, embedded by a small local model on the existing MLX sidecar and exposed as `recall(mode:"timeline", when:"March")`; embeddings only where lexical genuinely fails — "that restaurant someone mentioned in March" shares no keyword with anything on disk.