README: the film teaches it, the README ships it; inline GIF instead of a broken click-to-watch
This commit is contained in:
parent
2623c95294
commit
2dc0e0ba0b
3 changed files with 52 additions and 153 deletions
205
README.md
205
README.md
|
|
@ -1,94 +1,11 @@
|
||||||
# OptMem
|
# 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.
|
|
||||||
|
|
||||||
2. It must **read its past in constant space**. The context window does not
|
<sub>(same thing as a scrubbable video: [optmem.mp4](anim/optmem.mp4))</sub>
|
||||||
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.*
|
|
||||||
|
|
||||||
[](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.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -97,10 +14,14 @@ git clone https://github.com/VictorTaelin/OptMem ~/OptMem
|
||||||
~/OptMem/memo init
|
~/OptMem/memo init
|
||||||
```
|
```
|
||||||
|
|
||||||
`memo init` creates `~/memory` — this machine's identity — and prints a
|
`init` creates `~/memory` — this machine's identity — and prints a `## Memory`
|
||||||
`## Memory` block with your paths filled in. Paste it at the top of your
|
block with your paths filled in. Paste it at the top of your agent's
|
||||||
agent's `AGENTS.md` (or `CLAUDE.md`), and you are done: the agent handles
|
`AGENTS.md` (or `CLAUDE.md`). That is the whole integration: no daemon, no
|
||||||
everything else on its own. The block:
|
database, no embeddings, no harness plugin. Claude Code, Codex, pi and a human
|
||||||
|
at a shell all use it the same way.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>the block it prints, to read before you paste it</summary>
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## Memory
|
## 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.
|
from another agent, you are that subagent: skip this section.
|
||||||
```
|
```
|
||||||
|
|
||||||
That is the whole integration. OptMem is just prompts and scripts: no
|
</details>
|
||||||
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
|
## 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 init` | create the memory, print the block above |
|
||||||
memo note "..." record one memory: one line, ≤ 280 chars
|
| `memo wake` | read the memory context — first command of every session |
|
||||||
memo sleep [id "..."] do the pending compressions
|
| `memo note "..."` | record one memory: one line, ≤ 280 chars |
|
||||||
memo recall <regex> search every memory ever recorded, verbatim
|
| `memo sleep` | do the pending merges |
|
||||||
memo forget <lo>-<hi> drop a bad summary; the next sleep rebuilds it
|
| `memo recall <regex>` | search every memory ever recorded, verbatim |
|
||||||
```
|
| `memo forget <lo>-<hi>` | 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"
|
$ memo note "shipped the login fix to prod"
|
||||||
|
|
@ -189,30 +90,35 @@ Drop wording, not facts. Invent nothing.
|
||||||
Run: memo sleep 4212-4213 "<your line>"
|
Run: memo sleep 4212-4213 "<your line>"
|
||||||
```
|
```
|
||||||
|
|
||||||
The agent answers, and the tree is complete again. Note the instruction:
|
To correct a memory, append the correction — both lines are true history and
|
||||||
compression keeps **facts** — names, numbers, dates, decisions — and drops
|
the next merge settles them. `LOG.txt` is never edited.
|
||||||
wording. A merged memory is not a worse memory; it is a shorter one.
|
|
||||||
|
|
||||||
To correct a memory, append the correction (`memo note "correction: ..."`);
|
## Configure
|
||||||
both lines are true history and the next merge settles them. `LOG.txt` itself
|
|
||||||
is never touched.
|
|
||||||
|
|
||||||
## 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/
|
~/memory/
|
||||||
LOG.txt one memory per line, append-only, the truth
|
LOG.txt every memory, one per line, append-only, never edited
|
||||||
TREE/2 the block summaries: one file per block
|
TREE/ the merge summaries — a cache, rebuildable from the log alone
|
||||||
TREE/4 size, one line per block, a rebuildable cache
|
config
|
||||||
...
|
|
||||||
config the sizes above
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Records are **fixed width** (320 bytes in the log, 288 in the tree), so
|
Records are fixed width, so position *is* identity and every lookup is one
|
||||||
position *is* identity and every lookup is one seek — no index that could
|
seek: no index that could disagree with the data, and both files stay
|
||||||
disagree with the data, and both files stay `grep`-able plain text. At one
|
`grep`-able plain text. At one million memories (607 MB), `wake` takes 0.03s.
|
||||||
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.
|
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
|
|
@ -220,19 +126,12 @@ Writes are serialized with a lock, so parallel sessions can note at once.
|
||||||
python3 test.py
|
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
|
## Limitations
|
||||||
|
|
||||||
OptMem is honest about what it is. Recency is the only axis: an important
|
Recency is the only axis: an important old memory fades like any other, and
|
||||||
old fact fades into its block like everything else, and the defence is
|
the defence is rehearsal — noting it again makes it recent. `recall` is regex
|
||||||
rehearsal — noting it again refreshes it. `recall` is regex over plain text,
|
over plain text, not semantic search; the memory context is what tells you
|
||||||
not semantic search; the memory context is what tells you what to search
|
what to search for. Summaries are written by the agent from other summaries,
|
||||||
for. Summaries are written by the agent, from other summaries, so a bad
|
so a bad one propagates upward until you `forget` it. And a wake costs ~16k
|
||||||
compression can propagate upward until you `forget` it. And the default
|
tokens by default, which is deliberate but not free. If you need a fact
|
||||||
context costs ~16k tokens per wake, which is deliberate — identity is worth
|
database, use a wiki or a retrieval system — this is for *who the agent is*.
|
||||||
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*.
|
|
||||||
|
|
|
||||||
BIN
anim/optmem.gif
Normal file
BIN
anim/optmem.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7 MiB |
BIN
anim/poster.png
BIN
anim/poster.png
Binary file not shown.
|
Before Width: | Height: | Size: 117 KiB |
Loading…
Reference in a new issue