One home ~/.optmem, one curl to install; new AGENTS.md block; README cut to 59 lines
This commit is contained in:
parent
2dc0e0ba0b
commit
efde13f0c1
4 changed files with 92 additions and 150 deletions
140
README.md
140
README.md
|
|
@ -5,133 +5,55 @@ reads at wake is always the same size.
|
|||
|
||||

|
||||
|
||||
<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
18
install.sh
Executable 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
76
memo
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
|
|||
8
test.py
8
test.py
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue