From efde13f0c19bb02faa83a653dec95f743cf45d1c Mon Sep 17 00:00:00 2001 From: victortaelin Date: Sun, 26 Jul 2026 15:07:10 -0300 Subject: [PATCH] One home ~/.optmem, one curl to install; new AGENTS.md block; README cut to 59 lines --- README.md | 140 ++++++++++++----------------------------------------- install.sh | 18 +++++++ memo | 76 +++++++++++++++-------------- test.py | 8 +-- 4 files changed, 92 insertions(+), 150 deletions(-) create mode 100755 install.sh diff --git a/README.md b/README.md index d499e9f..ad7c307 100644 --- a/README.md +++ b/README.md @@ -5,133 +5,55 @@ reads at wake is always the same size. ![how OptMem works](anim/optmem.gif) -(same thing as a scrubbable video: [optmem.mp4](anim/optmem.mp4)) - -## 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. - -
-the block it prints, to read before you paste it - -```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 ""` 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 ` 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. -``` - -
+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 ` | 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 ` | search every memory ever recorded, word for word | | `memo forget -` | 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 "" +~/.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*. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..b21d9a0 --- /dev/null +++ b/install.sh @@ -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 diff --git a/memo b/memo index 7668862..4c6f274 100755 --- a/memo +++ b/memo @@ -9,14 +9,13 @@ memo forget - drop a bad summary; sleep rebuilds it. memo import 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 ""` 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 ` 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 ` 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()) diff --git a/test.py b/test.py index 5d73c22..20c01e1 100755 --- a/test.py +++ b/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)