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>
|
## 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
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 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())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
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")
|
"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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue