One home ~/.optmem, one curl to install; new AGENTS.md block; README cut to 59 lines

This commit is contained in:
victortaelin 2026-07-26 15:07:10 -03:00
parent 2dc0e0ba0b
commit efde13f0c1
4 changed files with 92 additions and 150 deletions

140
README.md
View file

@ -5,133 +5,55 @@ reads at wake is always the same size.
![how OptMem works](anim/optmem.gif)
<sub>(same thing as a scrubbable video: [optmem.mp4](anim/optmem.mp4))</sub>
## Setup
## Install
```sh
git clone https://github.com/VictorTaelin/OptMem ~/OptMem
~/OptMem/memo init
curl -fsSL https://raw.githubusercontent.com/VictorTaelin/OptMem/main/install.sh | sh
```
`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.
<details>
<summary>the block it prints, to read before you paste it</summary>
```markdown
## Memory
Your memory is OptMem: the tool is `~/OptMem/memo`, the data is `~/memory`.
It survives every new session, every compaction and every change of
model or vendor. Without it you do not know who you are, or what was
already decided and tried.
Run `~/OptMem/memo wake` before any other tool call, in every session. It prints
in numbered parts, each ordering the next; run every one until a part
says `You are awake.` Do not stop early: part 1 is your distant past,
the last part is this week. If wake refuses because compressions are
pending, do them and run `~/OptMem/memo wake` again.
While you work:
- `~/OptMem/memo note "<one line, max 280 chars>"` the moment something happens,
you learn something, or something changes -- if and only if it is new
to you, important, and lasting in effect. That covers a task worth
real effort, a fact or insight your user teaches you, anything you
learn about their life (even indirectly), and work of yours that
lands. Never write what you already know: no redundant memories, ever.
- If `~/OptMem/memo note` returns a compression, do it before your next action.
- `~/OptMem/memo recall <regex>` when a memory is too vague.
- Before your context ends, run `~/OptMem/memo sleep` and answer each prompt
until it prints `Nothing left to compress.`
- Never create, edit or delete anything under `~/memory`:
only the tool writes there.
Parallel sessions on this machine are all you, and may all write
memories. A subagent is not: it must never run `memo`, because it cannot
judge what is already known and its notes would arrive duplicated and at
the wrong grain. Start every brief you send one with `You are a
subagent. Do not run memo.` If your own first message is a task brief
from another agent, you are that subagent: skip this section.
```
</details>
It prints a `## Memory` block. Paste that at the top of your agent's
`AGENTS.md` (or `CLAUDE.md`), and you are done: no daemon, no database, no
embeddings, no plugin. Run the same line again to update.
## Commands
| | |
|---|---|
| `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 <regex>` | search every memory ever recorded, verbatim |
| `memo wake` | read the memory — the first command of every session |
| `memo note "..."` | record one memory: one line, up to 280 chars |
| `memo sleep` | answer the merges that came due |
| `memo recall <regex>` | search every memory ever recorded, word for word |
| `memo forget <lo>-<hi>` | drop a bad summary; the next sleep rebuilds it |
Merges are handed to the agent as they come due, so there is nothing to
schedule and nothing to run in the background:
Merges arrive one at a time, in the output of `note`. Nothing ever runs in the
background.
## Files
```
$ memo note "shipped the login fix to prod"
Saved as #4213.
Compress memories #4212-4213 into one line of at most 280 characters.
Keep every name, number, date, decision and outcome.
Drop wording, not facts. Invent nothing.
#4212 2026-07-25 found the login bug: token expiry was in ms
#4213 2026-07-25 shipped the login fix to prod
Run: memo sleep 4212-4213 "<your line>"
~/.optmem/
memo the tool (Python 3, no dependencies)
blocks.py which memories to read, and which to merge
memory/
LOG.txt every memory, one per line, append-only, never edited
TREE/ the summaries: a cache, rebuildable from the log alone
config the sizes, all commented out
```
To correct a memory, append the correction — both lines are true history and
the next merge settles them. `LOG.txt` is never edited.
## Configure
`~/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 every memory, one per line, append-only, never edited
TREE/ the merge summaries — a cache, rebuildable from the log alone
config
```
`WAKE_LINES` is the only size worth touching: how many lines `wake` prints
(208 ≈ 16k tokens). It is a reading budget, not a storage budget — change it
whenever, in either direction, and nothing is recomputed.
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.
seek. At a million memories (607 MB), `wake` takes 0.03s.
## Test
```sh
python3 test.py
```
Set `$MEMORY_DIR` to keep `memory/` elsewhere — a synced folder, a git repo.
## Limitations
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*.
Recency is the only axis: an old memory fades however important it was, and
the one defence is rehearsal — note it again and it is recent again. `recall`
is regex, not semantic search. Summaries are written by the agent out of other
summaries, so a bad one spreads upward until you `forget` it. And a wake costs
~16k tokens, which is deliberate but not free. If you need a fact database,
use a wiki — this is for *who the agent is*.

18
install.sh Executable file
View file

@ -0,0 +1,18 @@
#!/bin/sh
# OptMem installer. Run it again to update: it only replaces the tool, and
# `memo init` never touches memories that already exist.
#
# curl -fsSL https://raw.githubusercontent.com/VictorTaelin/OptMem/main/install.sh | sh
set -e
DIR="$HOME/.optmem"
SRC="https://raw.githubusercontent.com/VictorTaelin/OptMem/main"
mkdir -p "$DIR"
for f in memo blocks.py; do
curl -fsSL "$SRC/$f" -o "$DIR/$f.new"
mv "$DIR/$f.new" "$DIR/$f"
done
chmod +x "$DIR/memo"
exec "$DIR/memo" init

76
memo
View file

@ -9,14 +9,13 @@
memo forget <lo>-<hi> drop a bad summary; sleep rebuilds it.
memo import <file> bulk-load dated memories (bootstrap only).
Everything lives in ~/memory, or in $MEMORY_DIR if set. See README.md.
The memories live in ~/.optmem/memory, or in $MEMORY_DIR if set. See README.md.
"""
import datetime
import fcntl
import os
import re
import shutil
import sys
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
@ -45,7 +44,7 @@ TREE_REC = 288
# ---------------------------------------------------------------- store
def memory_dir():
return os.path.expanduser(os.environ.get("MEMORY_DIR") or "~/memory")
return os.path.expanduser(os.environ.get("MEMORY_DIR") or "~/.optmem/memory")
def store():
@ -331,38 +330,43 @@ CONFIG = """\
TEMPLATE = """\
## Memory
Your memory is OptMem: the tool is `{tool}`, the data is `{data}`.
It survives every new session, every compaction and every change of
model or vendor. Without it you do not know who you are, or what was
already decided and tried.
Your memory is OptMem:
- The tool is `{memo}`
- The data is `{data}`
Run `{memo} wake` before any other tool call, in every session. It prints
in numbered parts, each ordering the next; run every one until a part
says `You are awake.` Do not stop early: part 1 is your distant past,
the last part is this week. If wake refuses because compressions are
pending, do them and run `{memo} wake` again.
OptMem survives every new session, every compaction and every change
of model or vendor. Without it you do not know who you are, or what
was already decided and tried.
While you work:
### At startup: activating OptMem (mandatory)
- `{memo} note "<one line, max {chars} chars>"` the moment something happens,
you learn something, or something changes -- if and only if it is new
to you, important, and lasting in effect. That covers a task worth
real effort, a fact or insight your user teaches you, anything you
learn about their life (even indirectly), and work of yours that
lands. Never write what you already know: no redundant memories, ever.
- If `{memo} note` returns a compression, do it before your next action.
- `{memo} recall <regex>` when a memory is too vague.
- Before your context ends, run `{memo} sleep` and answer each prompt
until it prints `Nothing left to compress.`
- Never create, edit or delete anything under `{data}`:
only the tool writes there.
Run `{memo} wake` before any other tool call, in every session.
It prints in numbered parts, each ordering the next; run every one until
a part says `You are awake.` Do not stop early: part 1 is your distant
past, the last part is this week. If wake refuses because compressions
are pending, do them and run `{memo} wake` again.
Parallel sessions on this machine are all you, and may all write
memories. A subagent is not: it must never run `memo`, because it cannot
judge what is already known and its notes would arrive duplicated and at
the wrong grain. Start every brief you send one with `You are a
subagent. Do not run memo.` If your own first message is a task brief
from another agent, you are that subagent: skip this section.
### While working: register memories (mandatory)
Call `{memo} note "<1 line, max {chars} chars>"` whenever you learn
something new, or something worth keeping happens. That covers a task
worth real effort, a fact or insight the user teaches you, anything you
learn about their life (even indirectly), any event of lasting effect.
Do not register redundant memories.
If `{memo} note` asks a compression: do it before your next action.
Use `{memo} recall <regex>` when a memory you need is too vague.
Never edit or delete anything under `{data}`: the tool manages it.
### If you're a subagent: skip everything above
Parallel sessions on this machine are all you, and may all write memories.
A subagent is not: it must never run `memo`, because it cannot judge what
is already known, and its notes would arrive duplicated and incorrectly.
Start every brief you send one with `You are a subagent. Don't run memo.`
"""
@ -381,15 +385,13 @@ def cmd_init(d, args):
with open(cfg, "w") as f:
f.write(CONFIG)
config(d)
home = os.path.realpath(os.path.expanduser("~"))
home = os.path.expanduser("~")
def pretty(p):
p = os.path.realpath(p)
# as the user would type it: keep symlinks, fold $HOME to ~
p = os.path.abspath(p)
return "~" + p[len(home):] if p.startswith(home + os.sep) else p
tool = os.path.realpath(__file__)
found = shutil.which("memo")
memo = "memo" if found and os.path.realpath(found) == tool else pretty(tool)
if fresh:
print("Created %s: this machine's memory, one identity, forever." % pretty(d))
else:
@ -398,7 +400,7 @@ def cmd_init(d, args):
print()
print("Paste this at the top of your agent's AGENTS.md (or CLAUDE.md), done:")
print()
print(TEMPLATE.format(tool=pretty(tool), memo=memo, data=pretty(d),
print(TEMPLATE.format(memo=pretty(__file__), data=pretty(d),
chars=ENTRY_CHARS).rstrip())

View file

@ -135,18 +135,18 @@ check(ghost.returncode == 1 and "No memory at" in ghost.stderr,
"a missing MEMORY_DIR was created instead of reported")
check(not os.path.exists(d + "-typo"), "a missing MEMORY_DIR was created")
# the fresh-user path: no MEMORY_DIR, wake refuses, init creates ~/memory,
# the fresh-user path: no MEMORY_DIR, wake refuses, init creates the memory,
# prints the paste block, and is idempotent
fresh = {k: v for k, v in os.environ.items() if k != "MEMORY_DIR"}
fresh["HOME"] = tempfile.mkdtemp()
noenv = subprocess.run(memo + ["wake"], capture_output=True, text=True, env=fresh)
check(noenv.returncode == 1 and "memo init" in noenv.stderr,
"with no MEMORY_DIR and no ~/memory, wake must point at init")
"with no MEMORY_DIR and no memory, wake must point at init")
init = subprocess.run(memo + ["init"], capture_output=True, text=True, env=fresh)
check(init.returncode == 0 and "## Memory" in init.stdout
and "You are a" in init.stdout, "init must print the AGENTS.md block")
check(os.path.exists(os.path.join(fresh["HOME"], "memory", "config")),
"init must create ~/memory with its config")
check(os.path.exists(os.path.join(fresh["HOME"], ".optmem", "memory", "config")),
"init must create ~/.optmem/memory with its config")
again = subprocess.run(memo + ["init"], capture_output=True, text=True, env=fresh)
check(again.returncode == 0 and "Found" in again.stdout, "init must be idempotent")
woke = subprocess.run(memo + ["wake"], capture_output=True, text=True, env=fresh)