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) ![how OptMem works](anim/optmem.gif)
<sub>(same thing as a scrubbable video: [optmem.mp4](anim/optmem.mp4))</sub> ## Install
## Setup
```sh ```sh
git clone https://github.com/VictorTaelin/OptMem ~/OptMem curl -fsSL https://raw.githubusercontent.com/VictorTaelin/OptMem/main/install.sh | sh
~/OptMem/memo init
``` ```
`init` creates `~/memory` — this machine's identity — and prints a `## Memory` It prints a `## Memory` block. Paste that at the top of your agent's
block with your paths filled in. Paste it at the top of your agent's `AGENTS.md` (or `CLAUDE.md`), and you are done: no daemon, no database, no
`AGENTS.md` (or `CLAUDE.md`). That is the whole integration: no daemon, no embeddings, no plugin. Run the same line again to update.
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>
## Commands ## Commands
| | | | | |
|---|---| |---|---|
| `memo init` | create the memory, print the block above | | `memo wake` | read the memory — the first command of every session |
| `memo wake` | read the memory context — first command of every session | | `memo note "..."` | record one memory: one line, up to 280 chars |
| `memo note "..."` | record one memory: one line, ≤ 280 chars | | `memo sleep` | answer the merges that came due |
| `memo sleep` | do the pending merges | | `memo recall <regex>` | search every memory ever recorded, word for word |
| `memo recall <regex>` | search every memory ever recorded, verbatim |
| `memo forget <lo>-<hi>` | drop a bad summary; the next sleep rebuilds it | | `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 Merges arrive one at a time, in the output of `note`. Nothing ever runs in the
schedule and nothing to run in the background: background.
## Files
``` ```
$ memo note "shipped the login fix to prod" ~/.optmem/
Saved as #4213. memo the tool (Python 3, no dependencies)
blocks.py which memories to read, and which to merge
Compress memories #4212-4213 into one line of at most 280 characters. memory/
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>"
```
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 LOG.txt every memory, one per line, append-only, never edited
TREE/ the merge summaries — a cache, rebuildable from the log alone TREE/ the summaries: a cache, rebuildable from the log alone
config config the sizes, all commented out
``` ```
`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 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 seek. At a million memories (607 MB), `wake` takes 0.03s.
`grep`-able plain text. At one million memories (607 MB), `wake` takes 0.03s.
## Test Set `$MEMORY_DIR` to keep `memory/` elsewhere — a synced folder, a git repo.
```sh
python3 test.py
```
## Limitations ## Limitations
Recency is the only axis: an important old memory fades like any other, and Recency is the only axis: an old memory fades however important it was, and
the defence is rehearsal — noting it again makes it recent. `recall` is regex the one defence is rehearsal — note it again and it is recent again. `recall`
over plain text, not semantic search; the memory context is what tells you is regex, not semantic search. Summaries are written by the agent out of other
what to search for. Summaries are written by the agent from other summaries, summaries, so a bad one spreads upward until you `forget` it. And a wake costs
so a bad one propagates upward until you `forget` it. And a wake costs ~16k ~16k tokens, which is deliberate but not free. If you need a fact database,
tokens by default, which is deliberate but not free. If you need a fact use a wiki — this is for *who the agent is*.
database, use a wiki or a retrieval system — 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 forget <lo>-<hi> drop a bad summary; sleep rebuilds it.
memo import <file> bulk-load dated memories (bootstrap only). 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 datetime
import fcntl import fcntl
import os import os
import re import re
import shutil
import sys import sys
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
@ -45,7 +44,7 @@ TREE_REC = 288
# ---------------------------------------------------------------- store # ---------------------------------------------------------------- store
def memory_dir(): 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(): def store():
@ -331,38 +330,43 @@ CONFIG = """\
TEMPLATE = """\ TEMPLATE = """\
## Memory ## Memory
Your memory is OptMem: the tool is `{tool}`, the data is `{data}`. Your memory is OptMem:
It survives every new session, every compaction and every change of - The tool is `{memo}`
model or vendor. Without it you do not know who you are, or what was - The data is `{data}`
already decided and tried.
Run `{memo} wake` before any other tool call, in every session. It prints OptMem survives every new session, every compaction and every change
in numbered parts, each ordering the next; run every one until a part of model or vendor. Without it you do not know who you are, or what
says `You are awake.` Do not stop early: part 1 is your distant past, was already decided and tried.
the last part is this week. If wake refuses because compressions are
pending, do them and run `{memo} wake` again.
While you work: ### At startup: activating OptMem (mandatory)
- `{memo} note "<one line, max {chars} chars>"` the moment something happens, Run `{memo} wake` before any other tool call, in every session.
you learn something, or something changes -- if and only if it is new It prints in numbered parts, each ordering the next; run every one until
to you, important, and lasting in effect. That covers a task worth a part says `You are awake.` Do not stop early: part 1 is your distant
real effort, a fact or insight your user teaches you, anything you past, the last part is this week. If wake refuses because compressions
learn about their life (even indirectly), and work of yours that are pending, do them and run `{memo} wake` again.
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.
Parallel sessions on this machine are all you, and may all write ### While working: register memories (mandatory)
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 Call `{memo} note "<1 line, max {chars} chars>"` whenever you learn
the wrong grain. Start every brief you send one with `You are a something new, or something worth keeping happens. That covers a task
subagent. Do not run memo.` If your own first message is a task brief worth real effort, a fact or insight the user teaches you, anything you
from another agent, you are that subagent: skip this section. 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: with open(cfg, "w") as f:
f.write(CONFIG) f.write(CONFIG)
config(d) config(d)
home = os.path.realpath(os.path.expanduser("~")) home = os.path.expanduser("~")
def pretty(p): 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 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: if fresh:
print("Created %s: this machine's memory, one identity, forever." % pretty(d)) print("Created %s: this machine's memory, one identity, forever." % pretty(d))
else: else:
@ -398,7 +400,7 @@ def cmd_init(d, args):
print() print()
print("Paste this at the top of your agent's AGENTS.md (or CLAUDE.md), done:") print("Paste this at the top of your agent's AGENTS.md (or CLAUDE.md), done:")
print() 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()) 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") "a missing MEMORY_DIR was created instead of reported")
check(not os.path.exists(d + "-typo"), "a missing MEMORY_DIR was created") 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 # prints the paste block, and is idempotent
fresh = {k: v for k, v in os.environ.items() if k != "MEMORY_DIR"} fresh = {k: v for k, v in os.environ.items() if k != "MEMORY_DIR"}
fresh["HOME"] = tempfile.mkdtemp() fresh["HOME"] = tempfile.mkdtemp()
noenv = subprocess.run(memo + ["wake"], capture_output=True, text=True, env=fresh) noenv = subprocess.run(memo + ["wake"], capture_output=True, text=True, env=fresh)
check(noenv.returncode == 1 and "memo init" in noenv.stderr, 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) init = subprocess.run(memo + ["init"], capture_output=True, text=True, env=fresh)
check(init.returncode == 0 and "## Memory" in init.stdout check(init.returncode == 0 and "## Memory" in init.stdout
and "You are a" in init.stdout, "init must print the AGENTS.md block") 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")), check(os.path.exists(os.path.join(fresh["HOME"], ".optmem", "memory", "config")),
"init must create ~/memory with its config") "init must create ~/.optmem/memory with its config")
again = subprocess.run(memo + ["init"], capture_output=True, text=True, env=fresh) 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") 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) woke = subprocess.run(memo + ["wake"], capture_output=True, text=True, env=fresh)