Skip to content

Memory

The note store on disk: where it is, what a note file holds, how its key is derived, what omh memory lint checks, and what recall matches.

You usually reach a note through omh memory and the two tools a session gets — Commands covers each. This page is what they read and write, for when you open the store yourself.

I want to… Read
edit a note by hand A note file and Three types
read what omh memory lint is refusing What lint refuses, and what it only reports
change how keys are derived Keys
find out why recall did or did not return a note What recall matches

One store per repository, on your machine, never committed.

~/.omh/memory/<repo>/ host — one store per repo, never committed
/omh/memory the same directory, inside a sandbox

<repo> is the checkout’s directory name and a digest of its path, so two checkouts called api are two stores. A key is a path under it: surprise/mounting-a-credential-file-returns-ebusy is the file surprise/mounting-a-credential-file-returns-ebusy.md.

A note is a Markdown file: frontmatter, a title, and sections.

field required
key yes the note’s identity, and its path under the store without .md. omh memory lint refuses a note whose key and filename disagree, so rename one by changing both
type yes surprise, topic or stub — see Three types. Anything else is refused by name
source yes who observed it. A session’s tool writes session s03, claude, from the session it was started for and the harness that connected; the command line writes cli, or what --source names; a stub’s is the document it points at
recorded yes the day it happened, as YYYY-MM-DD — a date that exists, so 2026-13-45 is refused
invalidated_by no what makes the note stale, from the closed set omh memory stale can evaluate

This is what omh memory remember writes, byte for byte:

---
key: surprise/mounting-a-credential-file-returns-ebusy
type: surprise
source: cli
recorded: 2026-10-11
---
# Mounting a credential file returns EBUSY.
## Expected
A bind mount of the token file would persist the login.
## Observed
Mounting a credential file returns EBUSY.
## Evidence
EBUSY from the mount syscall
## Answers
- why does mounting the token file fail?
## Related
- [[credentials-are-a-named-volume]]

The frontmatter is name: value lines between two ---, and nothing more elaborate. omh reads it with a parser of its own, so that it never accepts a file it could not write back.

A required field has to say something. One that is present and empty is refused like one that is missing.

A field omh does not know is ignored rather than refused — a note is your document as much as the agent’s.

A link is [[a-key]]. It takes a key, never a title, because a key can be worked out before the note it names exists.

A note’s type decides which sections it must have. A section is a ## Name heading; the word appearing somewhere in the body does not count.

type what it is sections it must have sections that hold a list and nothing else
surprise something that went differently from what was expected Expected, Observed, Evidence, Answers Answers, Related
topic one subject, filled in as far as it goes none Related
stub a pointer at a document the repo already has Answers Answers

Answers is the questions the note answers, in the words somebody would later ask them in. It is what makes a note findable, and only its writer knows it — which is why remember refuses a note without one:

Terminal window
$ omh memory remember --expected … --observed … --evidence …
omh: `answers` is empty; a note nobody can find is a note nobody wrote
Why some sections hold a list and nothing else

A list and nothing else is a rule about shape standing where a length limit would be. A limit needs a number, and a number needs a store to calibrate it against; bullets-only needs neither.

What lint refuses, and what it only reports

Section titled “What lint refuses, and what it only reports”

omh memory lint names a rule beside each count. Six describe one note’s shape and refuse it; three describe the store and warn.

rule
Unreadable refused a file in the store that does not read back as a note
UnclosedFence refused a code fence that never closes, which quotes everything after it
MissingSection refused a section the note’s type must have
ProseInListSection refused a paragraph in a section that holds a list and nothing else
KeyDisagreesWithPath refused key says one thing and the filename another
UnevaluatableTrigger refused an invalidated_by omh has no way to check
DuplicateKey warning one key claimed by two files
DanglingLink warning a [[link]] to a key that is not in the store
Orphan warning a note nothing links to

Only a refusal makes omh memory lint fail. A warning is reported and does not: Orphan fires on every note nothing links to, so failing on warnings would fail on the store’s shape.

A key is derived, never chosen, from a template per type in <repo>/.omh/memory.toml. This is what ships:

# How a note's key is derived. Identity, not a title — the agent never picks
# one, so the same observation cannot be recorded twice under two spellings.
[keys]
surprise = "surprise/{{slug}}"
topic = "{{slug}}"
stub = "{{path}}"
placeholder bound to
{{slug}} for a surprise, the first sentence of what was observed — up to the first ., ! or ? — lowercased, with every run of anything that is not a letter or a digit collapsed to one -. For the topic that omh init writes, the repo’s name under the same rule
{{path}} for a stub, the document’s path in the repo without .md, each segment put through the same rule: docs/Deploy Notes.md is docs/deploy-notes

omh init writes the file once and never refreshes it, because a template that changed under an existing store would re-key every note written from then on.

A placeholder nothing binds is an error that names it, not a literal {{slugg}} inside a key.

The result has to be slash-separated slugs: no empty segment, none that starts with a ., no backslash. The file is committed, so a clone supplies it, and a key is also a path — that rule is what stops a template writing outside the store.

With no memory.toml, the shipped templates apply. A keys.toml beside it — the file’s name before it held more than keys — is refused by name rather than ignored.

recall is one call: it ranks the notes a question matches, follows each one’s links one hop out, and returns the neighbourhood with every note’s date.

surprise/mounting-a-credential-file-returns-ebusy 2026-08-07
└─ credentials-are-a-named-volume 2026-08-05
  • Whole words, stemmed. A question is split into words and each loses its plural and its tense, so mounted finds mounting. Nothing matches on part of a word.
  • Ranked by rarity. A word in every note cannot tell two notes apart and barely counts; a word in one note counts for a lot. There is no stopword list.
  • A word in the key counts double. Ties break on the more recent note, then on the one more notes link to.
  • Answers is indexed like the rest of the note, not ranked above it.
  • Eight matched notes at most, each with its neighbours. When more matched, the answer ends by saying how many were left out, so that a short answer is never mistaken for a complete one.
  • also_phrased_as takes the same question in other words. A note is found by the wording it was written in, which is rarely the wording it is asked for in.
What was measured

Measured 2026-08-09 on a store of 40 notes built from this repository’s history, with 40 questions:

first place top 3 top 8 notes returned
recall 47.5% 65.0% 90.0% 8
iwe’s lexical (BM25) ranker 55.0% 75.0% 95.0% 25.9
recall, notes with questions declared 55.0% 67.5% 87.5% 8

The two engines are statistically indistinguishable (McNemar p = 0.51), and iwe buys its top-8 by returning 26 notes where recall returns 8. Declared questions fixed three first places and broke none, but forty questions cannot separate that from chance (p = 0.25). It would take several hundred.

So declared questions are indexed and not weighted: ranking Answers above the rest of the note cost ten points of first-place precision. What was not measured is whether an agent answers better with memory than without.

init fills a new store with what the repo already documents — as pointers, not summaries — so it is of use on the first day.

One stub for every Markdown file git tracks, outside .omh/. Its Answers are that file’s ## headings, word for word:

---
key: docs/deploy
type: stub
source: docs/deploy.md
recorded: 2026-08-07
---
# Deploying
Repo document. Read `docs/deploy.md`; this note points at it and does not restate it.
## Answers
- Staging
- Rolling back

A file with no ## heading gets one answer, the whole of `NOTES.md` , because an Answers section with nothing in it would be refused.

And one topic for the repo, keyed by its name: what init derived, each line with the file it came from, and a link to every stub — which is what keeps a new store from being all orphans.

Why pointers rather than summaries

A store that starts empty is no use on the first day. A summary drops the flag, the path and the error string, and then drifts from the page it was made from; a pointer sends the agent to the page itself.

Notes outlive the session that wrote them. omh sNN rm says so when there are any, after its report:

3 notes recorded during this session — `omh memory` to review

It says nothing when the session recorded none, and nothing under --json.

  • omh memory
    List, remember, lint and remove notes from the command line.
  • omh memory stale
    The closed set of triggers invalidated_by can name.
  • Sessions
    What a session is: a container, a git worktree and a branch.