diff --git a/README.md b/README.md index ed65ab4..d499e9f 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,11 @@ # OptMem -How do you make an AI agent remember its whole life? +Permanent memory for AI agents. Nothing is ever deleted, and what the agent +reads at wake is always the same size. -1. It must **never forget**. You cannot tell *today* what will matter in a - *year*, so deleting anything is a gamble you always eventually lose. +![how OptMem works](anim/optmem.gif) -2. It must **read its past in constant space**. The context window does not - grow with age; a memory that scrolls past it might as well not exist. - -Most agent memories pick one: keep everything (and drown), or keep a small -curated file (and forget). OptMem does both. *At once.* - -[![watch the animation](anim/poster.png)](anim/optmem.mp4) - -*↑ click to watch: 3 minutes, the whole idea.* - -## The idea - -A **memory** is one short note about something the agent learned: - -``` -#4211 2026-07-25 Tom asked for a flight to Japan -``` - -The agent appends memories to `LOG.txt`, an **append-only log**. Nothing in it -is ever edited or deleted. That file is the truth, forever. - -**PROBLEM:** after two months, that life would not fit in the model's context. -1,000 memories is roughly 80,000 tokens. - -Current solutions keep one small long-term memory file, and have the agent -*delete* stale memories when it fills. But that is the gamble from point 1: -the agent is guessing, today, what a year from now will need. - -**OUR ANSWER:** memories are not deleted. They **merge**: - -``` -Tom asked for a flight to Japan -Tom booked a hotel in Tokyo - ↓ - Tom planned a Tokyo trip -``` - -The result is a memory too — it just holds less detail. So it can merge -again, and again, all the way up, forming a **binary merge tree** over the -log: - -``` -#0 #1 #2 #3 #4 #5 #6 #7 the raw memories - \ / \ / \ / \ / - 0-1 2-3 4-5 6-7 each one line, ≤ 280 chars - \ / \ / - 0-3 4-7 - \ / - 0-7 -``` - -A block covering four thousand memories is still one line. Nothing in the -system is ever bigger than one line. - -## The memory context - -At wake, the agent reads a **constant-sized** document: a set of blocks that -tiles the whole log, big old blocks first, raw recent memories last. With -10,000 memories and the default budget of 208 lines: - -``` - block size: 1 2 4 8 16 32 64 128 256 - how many: 42 21 21 21 22 21 21 21 18 - └ the last 42, verbatim ───────────▶ the first 4,600, 256:1 -``` - -**Detail is proportional to recency.** The oldest years are recalled as a -vague shape, the newest days word for word, and the transition is smooth — -which is roughly how you remember your own life. When something old matters -again, the vague shape says what to search for, and `memo recall` finds the -original, verbatim: it was never deleted. - -## No background job - -There is no dreaming, no nightly cleanup, no compaction spike. The moment two -halves of a block exist, the agent is handed that one merge and does it **on -the spot** — about one small compression per memory written, nine at the very -worst (measured over 20,000). So `memo wake` never waits: the blocks it needs -were built long ago. - -And because *which* memories merge is decided by position and age alone — -never by judgement — the tree is a pure function of the log: a cache. A bad -summary can be dropped and rebuilt (`memo forget`), and it can never cost you -a memory. +(same thing as a scrubbable video: [optmem.mp4](anim/optmem.mp4)) ## Setup @@ -97,10 +14,14 @@ git clone https://github.com/VictorTaelin/OptMem ~/OptMem ~/OptMem/memo init ``` -`memo init` creates `~/memory` — this machine's identity — and prints a -`## Memory` block with your paths filled in. Paste it at the top of your -agent's `AGENTS.md` (or `CLAUDE.md`), and you are done: the agent handles -everything else on its own. The block: +`init` creates `~/memory` — this machine's identity — and prints a `## Memory` +block with your paths filled in. Paste it at the top of your agent's +`AGENTS.md` (or `CLAUDE.md`). That is the whole integration: no daemon, no +database, no embeddings, no harness plugin. Claude Code, Codex, pi and a human +at a shell all use it the same way. + +
+the block it prints, to read before you paste it ```markdown ## Memory @@ -139,41 +60,21 @@ subagent. Do not run memo.` If your own first message is a task brief from another agent, you are that subagent: skip this section. ``` -That is the whole integration. OptMem is just prompts and scripts: no -daemon, no database, no embeddings, no API. It works the same under Claude -Code, Codex, pi, or a human at a shell. - -## Configure - -The sizes live in `~/memory/config`, written by `init` with everything -commented out: - -``` -# WAKE_LINES=208 # the memory context: how many lines wake prints (~16k tokens) -# ENTRY_CHARS=280 # the longest a single memory may be, in bytes -# PART_CHARS=20000 # output paging: largest part, in bytes -# PART_LINES=500 # output paging: largest part, in lines -``` - -`WAKE_LINES` is the knob that matters: it is the size of the memory context, -so it is a *reading* budget, not a storage budget. You can change it at any -time, in either direction, with nothing to recompute — it only selects which -already-built lines get printed. (`PART_*` exist because every harness -truncates long command output at a different cap; wake pages itself to -survive all of them, each part ordering the next.) +
## Commands -``` -memo init one-time setup: create the memory, print the block above -memo wake [part [T]] read your memory context. First command, every session -memo note "..." record one memory: one line, ≤ 280 chars -memo sleep [id "..."] do the pending compressions -memo recall search every memory ever recorded, verbatim -memo forget - drop a bad summary; the next sleep rebuilds it -``` +| | | +|---|---| +| `memo init` | create the memory, print the block above | +| `memo wake` | read the memory context — first command of every session | +| `memo note "..."` | record one memory: one line, ≤ 280 chars | +| `memo sleep` | do the pending merges | +| `memo recall ` | search every memory ever recorded, verbatim | +| `memo forget -` | drop a bad summary; the next sleep rebuilds it | -When a note completes a block, `memo` hands the agent the merge right there: +Merges are handed to the agent as they come due, so there is nothing to +schedule and nothing to run in the background: ``` $ memo note "shipped the login fix to prod" @@ -189,30 +90,35 @@ Drop wording, not facts. Invent nothing. Run: memo sleep 4212-4213 "" ``` -The agent answers, and the tree is complete again. Note the instruction: -compression keeps **facts** — names, numbers, dates, decisions — and drops -wording. A merged memory is not a worse memory; it is a shorter one. +To correct a memory, append the correction — both lines are true history and +the next merge settles them. `LOG.txt` is never edited. -To correct a memory, append the correction (`memo note "correction: ..."`); -both lines are true history and the next merge settles them. `LOG.txt` itself -is never touched. +## Configure -## The store +`~/memory/config`, written by `init` with every knob commented out: + +``` +# WAKE_LINES=208 # the memory context: how many lines wake prints (~16k tokens) +# ENTRY_CHARS=280 # the longest a single memory may be, in bytes +# PART_CHARS=20000 # output paging: largest part, in bytes +# PART_LINES=500 # output paging: largest part, in lines +``` + +`WAKE_LINES` is the one that matters. It is a *reading* budget, not a storage +budget: change it at any time, in either direction, with nothing to recompute. + +## Data ``` ~/memory/ - LOG.txt one memory per line, append-only, the truth - TREE/2 the block summaries: one file per block - TREE/4 size, one line per block, a rebuildable cache - ... - config the sizes above + LOG.txt every memory, one per line, append-only, never edited + TREE/ the merge summaries — a cache, rebuildable from the log alone + config ``` -Records are **fixed width** (320 bytes in the log, 288 in the tree), so -position *is* identity and every lookup is one seek — no index that could -disagree with the data, and both files stay `grep`-able plain text. At one -million memories (607 MB), `memo wake` takes 0.03s and `memo note` 0.02s. -Writes are serialized with a lock, so parallel sessions can note at once. +Records are fixed width, so position *is* identity and every lookup is one +seek: no index that could disagree with the data, and both files stay +`grep`-able plain text. At one million memories (607 MB), `wake` takes 0.03s. ## Test @@ -220,19 +126,12 @@ Writes are serialized with a lock, so parallel sessions can note at once. python3 test.py ``` -Drives the real CLI through a synthetic life, checking that the context -always tiles the log, never exceeds its budget, always gains detail toward -the present, that every block is written exactly once, and that nothing is -ever rewritten. - ## Limitations -OptMem is honest about what it is. Recency is the only axis: an important -old fact fades into its block like everything else, and the defence is -rehearsal — noting it again refreshes it. `recall` is regex over plain text, -not semantic search; the memory context is what tells you what to search -for. Summaries are written by the agent, from other summaries, so a bad -compression can propagate upward until you `forget` it. And the default -context costs ~16k tokens per wake, which is deliberate — identity is worth -more than the tokens — but it is not free. If what you need is a fact -database, use a wiki or a retrieval system; this is for *who the agent is*. +Recency is the only axis: an important old memory fades like any other, and +the defence is rehearsal — noting it again makes it recent. `recall` is regex +over plain text, not semantic search; the memory context is what tells you +what to search for. Summaries are written by the agent from other summaries, +so a bad one propagates upward until you `forget` it. And a wake costs ~16k +tokens by default, which is deliberate but not free. If you need a fact +database, use a wiki or a retrieval system — this is for *who the agent is*. diff --git a/anim/optmem.gif b/anim/optmem.gif new file mode 100644 index 0000000..2191bb8 Binary files /dev/null and b/anim/optmem.gif differ diff --git a/anim/poster.png b/anim/poster.png deleted file mode 100644 index 8bf7a03..0000000 Binary files a/anim/poster.png and /dev/null differ