2026-07-25 18:58:35 +02:00
|
|
|
#!/usr/bin/env python3
|
2026-07-26 19:35:22 +02:00
|
|
|
"""OptMem: a permanent, append-only memory for AI agents.
|
2026-07-25 18:58:35 +02:00
|
|
|
|
2026-07-26 19:28:25 +02:00
|
|
|
memo init create this machine's memory, print the setup block.
|
2026-07-25 22:38:56 +02:00
|
|
|
memo wake [part [T]] read your memory. Run first, every session.
|
|
|
|
|
memo note "..." record one memory: one line, at most 280 chars.
|
|
|
|
|
memo sleep [id "..."] do the pending compressions.
|
|
|
|
|
memo recall <regex> search every memory ever recorded.
|
|
|
|
|
memo forget <lo>-<hi> drop a bad summary; sleep rebuilds it.
|
|
|
|
|
memo import <file> bulk-load dated memories (bootstrap only).
|
2026-07-25 18:58:35 +02:00
|
|
|
|
2026-07-26 20:07:10 +02:00
|
|
|
The memories live in ~/.optmem/memory, or in $MEMORY_DIR if set. See README.md.
|
2026-07-25 18:58:35 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
|
import fcntl
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
from blocks import cover # noqa: E402
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
ENTRY_CHARS = 280
|
2026-07-25 23:49:37 +02:00
|
|
|
WAKE_LINES = 208 # ~16k tokens of dense text, in 3 parts
|
2026-07-25 18:58:35 +02:00
|
|
|
RAW_MAX = 16 # blocks up to this many memories compress from the raw log
|
|
|
|
|
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
# Every harness truncates a command that prints too much, and each drops a
|
|
|
|
|
# different piece: Claude Code cuts the middle at 30,000 chars, pi cuts the
|
|
|
|
|
# head at 50 KB, Codex budgets 10,000 tokens. So the memory is handed over in
|
|
|
|
|
# parts that fit all of them. These are transport limits, not memory limits.
|
|
|
|
|
PART_CHARS = 20000
|
|
|
|
|
PART_LINES = 500
|
2026-07-25 19:22:29 +02:00
|
|
|
|
2026-07-25 18:58:35 +02:00
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
# Records are FIXED WIDTH, so a memory or a block is found by seeking to its
|
|
|
|
|
# offset -- no scanning, no index file to keep in sync. Position IS identity:
|
|
|
|
|
# memory i lives at i*LOG_REC of LOG.txt, and block [k*s,(k+1)*s) lives at
|
|
|
|
|
# k*TREE_REC of TREE/<s>. Padding costs ~2x on disk and buys O(1) everywhere.
|
|
|
|
|
LOG_REC = 320
|
|
|
|
|
TREE_REC = 288
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 18:58:35 +02:00
|
|
|
# ---------------------------------------------------------------- store
|
|
|
|
|
|
2026-07-26 19:28:25 +02:00
|
|
|
def memory_dir():
|
2026-07-26 20:07:10 +02:00
|
|
|
return os.path.expanduser(os.environ.get("MEMORY_DIR") or "~/.optmem/memory")
|
2026-07-26 19:28:25 +02:00
|
|
|
|
|
|
|
|
|
2026-07-25 18:58:35 +02:00
|
|
|
def store():
|
2026-07-26 19:28:25 +02:00
|
|
|
d = memory_dir()
|
|
|
|
|
# The directory is only ever created by `memo init`: creating it IS
|
|
|
|
|
# creating the identity, and that is a deliberate act. If any other
|
|
|
|
|
# command created it, a typo in MEMORY_DIR would silently open an empty
|
|
|
|
|
# store, and the agent would wake with no past and write a second
|
|
|
|
|
# identity.
|
2026-07-25 23:41:26 +02:00
|
|
|
if not os.path.isdir(d):
|
2026-07-26 19:28:25 +02:00
|
|
|
die("No memory at %s.\nTo create one, run: memo init\n"
|
|
|
|
|
"To use an existing one, point MEMORY_DIR at it." % d)
|
2026-07-25 19:34:29 +02:00
|
|
|
os.makedirs(os.path.join(d, "TREE"), exist_ok=True)
|
|
|
|
|
p = os.path.join(d, "LOG.txt")
|
|
|
|
|
if not os.path.exists(p):
|
|
|
|
|
open(p, "a").close()
|
2026-07-25 18:58:35 +02:00
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def config(d):
|
2026-07-26 19:28:25 +02:00
|
|
|
"""Optional overrides in the memory directory's `config`. `memo init`
|
|
|
|
|
writes it fully commented out: an uncommented copy of the defaults would
|
|
|
|
|
freeze them, and updating the tool would stop changing how it behaves."""
|
2026-07-25 19:22:29 +02:00
|
|
|
global ENTRY_CHARS, WAKE_LINES, PART_CHARS, PART_LINES
|
2026-07-25 18:58:35 +02:00
|
|
|
p = os.path.join(d, "config")
|
|
|
|
|
if not os.path.exists(p):
|
|
|
|
|
return
|
|
|
|
|
for line in open(p):
|
|
|
|
|
line = line.split("#")[0].strip()
|
|
|
|
|
if "=" not in line:
|
|
|
|
|
continue
|
|
|
|
|
k, v = (s.strip() for s in line.split("=", 1))
|
|
|
|
|
if k == "ENTRY_CHARS":
|
|
|
|
|
ENTRY_CHARS = int(v)
|
|
|
|
|
elif k == "WAKE_LINES":
|
|
|
|
|
WAKE_LINES = int(v)
|
2026-07-25 19:22:29 +02:00
|
|
|
elif k == "PART_CHARS":
|
|
|
|
|
PART_CHARS = int(v)
|
|
|
|
|
elif k == "PART_LINES":
|
|
|
|
|
PART_LINES = int(v)
|
2026-07-25 19:49:34 +02:00
|
|
|
if ENTRY_CHARS > min(TREE_REC - 8, LOG_REC - 40):
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
die("config: ENTRY_CHARS=%d does not fit the %d/%d-byte records."
|
2026-07-25 19:49:34 +02:00
|
|
|
% (ENTRY_CHARS, LOG_REC, TREE_REC))
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
def log_path(d):
|
|
|
|
|
return os.path.join(d, "LOG.txt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tree_path(d, size):
|
|
|
|
|
return os.path.join(d, "TREE", str(size))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def count(path, rec):
|
|
|
|
|
try:
|
|
|
|
|
return os.path.getsize(path) // rec
|
|
|
|
|
except OSError:
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def log_len(d):
|
|
|
|
|
return count(log_path(d), LOG_REC)
|
|
|
|
|
|
|
|
|
|
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
def repair(path, rec):
|
|
|
|
|
"""Drop a partial trailing record left by a crash. It was never
|
|
|
|
|
acknowledged. Without this the next append lands at a wrong offset and
|
|
|
|
|
every later record is misaligned. Callers hold the lock."""
|
|
|
|
|
try:
|
|
|
|
|
n = os.path.getsize(path)
|
|
|
|
|
except OSError:
|
|
|
|
|
return
|
|
|
|
|
if n % rec:
|
|
|
|
|
with open(path, "r+b") as f:
|
|
|
|
|
f.truncate(n - n % rec)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse(line):
|
|
|
|
|
head, _, rest = line.partition(" ")
|
|
|
|
|
date, _, text = rest.partition(" ")
|
|
|
|
|
return int(head[1:]), date, text
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
def log_get(d, i):
|
|
|
|
|
"""(id, date, text) of memory i, in one seek."""
|
|
|
|
|
with open(log_path(d), "rb") as f:
|
|
|
|
|
f.seek(i * LOG_REC)
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
return parse(f.read(LOG_REC).decode().rstrip())
|
2026-07-25 19:34:29 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def log_slice(d, lo, hi):
|
2026-07-25 19:49:34 +02:00
|
|
|
"""Memories [lo,hi) in one read. Records are sliced as BYTES and decoded
|
|
|
|
|
one by one -- slicing decoded text would shift every boundary after the
|
|
|
|
|
first multi-byte character."""
|
2026-07-25 19:34:29 +02:00
|
|
|
with open(log_path(d), "rb") as f:
|
|
|
|
|
f.seek(lo * LOG_REC)
|
2026-07-25 19:49:34 +02:00
|
|
|
buf = f.read((hi - lo) * LOG_REC)
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
return [parse(buf[i * LOG_REC:(i + 1) * LOG_REC].decode().rstrip())
|
|
|
|
|
for i in range(hi - lo)]
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
def tree_get(d, lo, hi):
|
|
|
|
|
"""The summary of block [lo,hi), in one seek. None if not built yet."""
|
|
|
|
|
size = hi - lo
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
try:
|
|
|
|
|
with open(tree_path(d, size), "rb") as f:
|
|
|
|
|
f.seek((lo // size) * TREE_REC)
|
|
|
|
|
rec = f.read(TREE_REC)
|
|
|
|
|
except OSError:
|
|
|
|
|
return None
|
2026-07-25 19:34:29 +02:00
|
|
|
return rec.decode().rstrip() or None
|
|
|
|
|
|
2026-07-25 18:58:35 +02:00
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
def pad(text, rec):
|
|
|
|
|
b = text.encode()
|
|
|
|
|
if len(b) > rec - 1:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
die("Too long: %d bytes. The record holds %d." % (len(b), rec - 1))
|
2026-07-25 19:34:29 +02:00
|
|
|
return b + b" " * (rec - 1 - len(b)) + b"\n"
|
2026-07-25 18:58:35 +02:00
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
|
|
|
|
|
def locked(d):
|
2026-07-25 18:58:35 +02:00
|
|
|
lock = open(os.path.join(d, ".lock"), "w")
|
|
|
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
2026-07-25 19:34:29 +02:00
|
|
|
return lock
|
|
|
|
|
|
|
|
|
|
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
def log_append(d, items):
|
|
|
|
|
"""Append memories, items = [(date, text)]. The only way LOG.txt ever
|
|
|
|
|
changes. Ids are assigned INSIDE the lock: two sessions noting at the same
|
|
|
|
|
moment must not be handed the same id. Returns the first id used."""
|
2026-07-25 19:34:29 +02:00
|
|
|
lock = locked(d)
|
2026-07-25 18:58:35 +02:00
|
|
|
try:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
repair(log_path(d), LOG_REC)
|
|
|
|
|
base = log_len(d)
|
2026-07-25 19:34:29 +02:00
|
|
|
with open(log_path(d), "ab") as f:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
for k, (date, text) in enumerate(items):
|
|
|
|
|
f.write(pad("#%d %s %s" % (base + k, date, text), LOG_REC))
|
2026-07-25 18:58:35 +02:00
|
|
|
f.flush()
|
|
|
|
|
os.fsync(f.fileno())
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
return base
|
2026-07-25 18:58:35 +02:00
|
|
|
finally:
|
|
|
|
|
lock.close()
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
def tree_put(d, lo, hi, text):
|
|
|
|
|
"""Write block [lo,hi). Blocks are built in order, so this only ever
|
|
|
|
|
appends one record to one level file."""
|
|
|
|
|
size = hi - lo
|
|
|
|
|
lock = locked(d)
|
2026-07-25 18:58:35 +02:00
|
|
|
try:
|
2026-07-25 19:34:29 +02:00
|
|
|
p = tree_path(d, size)
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
repair(p, TREE_REC)
|
2026-07-25 19:34:29 +02:00
|
|
|
if count(p, TREE_REC) != lo // size:
|
|
|
|
|
return False
|
|
|
|
|
with open(p, "ab") as f:
|
|
|
|
|
f.write(pad(text, TREE_REC))
|
2026-07-25 18:58:35 +02:00
|
|
|
f.flush()
|
|
|
|
|
os.fsync(f.fileno())
|
2026-07-25 19:34:29 +02:00
|
|
|
return True
|
|
|
|
|
finally:
|
|
|
|
|
lock.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tree_drop(d, lo, hi):
|
|
|
|
|
"""Forget block [lo,hi) and every block built from it, by truncating each
|
|
|
|
|
level back to that point. Later blocks at those levels go too and are
|
|
|
|
|
rebuilt; the log is never touched, so nothing is lost."""
|
|
|
|
|
gone, size = [], hi - lo
|
|
|
|
|
lock = locked(d)
|
|
|
|
|
try:
|
|
|
|
|
while size <= log_len(d):
|
|
|
|
|
p, k = tree_path(d, size), lo // size
|
|
|
|
|
n = count(p, TREE_REC)
|
|
|
|
|
if n > k:
|
|
|
|
|
gone += [(i * size, (i + 1) * size) for i in range(k, n)]
|
|
|
|
|
with open(p, "r+b") as f:
|
|
|
|
|
f.truncate(k * TREE_REC)
|
|
|
|
|
size *= 2
|
|
|
|
|
return gone
|
2026-07-25 18:58:35 +02:00
|
|
|
finally:
|
|
|
|
|
lock.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def die(msg):
|
|
|
|
|
print(msg, file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 23:41:26 +02:00
|
|
|
def plural(n, word):
|
|
|
|
|
if n == 1:
|
|
|
|
|
return "1 " + word
|
|
|
|
|
if word.endswith("y"):
|
|
|
|
|
word = word[:-1] + "ie"
|
|
|
|
|
elif word.endswith(("s", "h", "x")):
|
|
|
|
|
word += "e"
|
|
|
|
|
return "%d %ss" % (n, word)
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 18:58:35 +02:00
|
|
|
def check(text):
|
|
|
|
|
text = text.strip()
|
|
|
|
|
if not text:
|
2026-07-25 23:41:26 +02:00
|
|
|
die("Empty. A memory is one line of text.")
|
2026-07-25 18:58:35 +02:00
|
|
|
if "\n" in text or "\r" in text:
|
2026-07-25 22:38:56 +02:00
|
|
|
die("%d lines. A memory is one line: merge them, or note them "
|
|
|
|
|
"separately." % (text.count("\n") + 1))
|
2026-07-25 19:49:34 +02:00
|
|
|
n = len(text.encode())
|
|
|
|
|
if n > ENTRY_CHARS:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
die("Too long: %d bytes, limit %d. Accented characters cost 2 bytes. "
|
|
|
|
|
"Compress it further." % (n, ENTRY_CHARS))
|
2026-07-25 18:58:35 +02:00
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- naps
|
|
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
def pending(d, T, limit=None):
|
|
|
|
|
"""Blocks that can be built and have not been, smallest first. Each level
|
|
|
|
|
file holds a dense prefix, so its length says exactly how far that level
|
|
|
|
|
got: this costs one stat per level, never a scan."""
|
|
|
|
|
todo, size = [], 2
|
|
|
|
|
while size <= T:
|
|
|
|
|
have = count(tree_path(d, size), TREE_REC)
|
|
|
|
|
for k in range(have, T // size):
|
|
|
|
|
todo.append((k * size, (k + 1) * size))
|
|
|
|
|
if limit and len(todo) >= limit:
|
|
|
|
|
return todo
|
|
|
|
|
size *= 2
|
|
|
|
|
return todo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pending_count(d, T):
|
2026-07-25 23:41:26 +02:00
|
|
|
"""How many blocks pending() would list, without listing them. A level can
|
|
|
|
|
hold MORE blocks than T needs -- T is a snapshot, and memories keep
|
|
|
|
|
arriving while an agent reads -- so each level is clamped at zero."""
|
2026-07-25 19:34:29 +02:00
|
|
|
n, size = 0, 2
|
|
|
|
|
while size <= T:
|
2026-07-25 23:41:26 +02:00
|
|
|
n += max(0, T // size - count(tree_path(d, size), TREE_REC))
|
2026-07-25 19:34:29 +02:00
|
|
|
size *= 2
|
|
|
|
|
return n
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def nap_prompt(d, lo, hi, left):
|
2026-07-25 18:58:35 +02:00
|
|
|
if hi - lo <= RAW_MAX:
|
2026-07-25 19:34:29 +02:00
|
|
|
body = "\n".join(" #%d %s %s" % e for e in log_slice(d, lo, hi))
|
2026-07-25 18:58:35 +02:00
|
|
|
else:
|
2026-07-25 23:41:26 +02:00
|
|
|
mid, halves = (lo + hi) // 2, []
|
|
|
|
|
for a, b in ((lo, mid), (mid, hi)):
|
|
|
|
|
s = tree_get(d, a, b)
|
|
|
|
|
if s is None:
|
|
|
|
|
die("Summary %d-%d is missing. Run: memo sleep" % (a, b - 1))
|
|
|
|
|
halves.append(" #%d-%d %s" % (a, b - 1, s))
|
|
|
|
|
body = "\n".join(halves)
|
|
|
|
|
tail = "" if not left else "\n%s after this one." % (
|
|
|
|
|
"1 compression remains" if left == 1 else
|
|
|
|
|
"%d compressions remain" % left)
|
2026-07-25 22:38:56 +02:00
|
|
|
return ("Compress memories #%d-%d into one line of at most %d characters.\n"
|
|
|
|
|
"Keep every name, number, date, decision and outcome.\n"
|
|
|
|
|
"Drop wording, not facts. Invent nothing.\n\n"
|
2026-07-25 23:41:26 +02:00
|
|
|
"%s\n%s\n"
|
2026-07-25 22:38:56 +02:00
|
|
|
"Run: memo sleep %d-%d \"<your line>\""
|
|
|
|
|
% (lo, hi - 1, ENTRY_CHARS, body, tail, lo, hi - 1))
|
2026-07-25 19:34:29 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def next_nap(d, T):
|
|
|
|
|
todo = pending(d, T, limit=1)
|
|
|
|
|
if not todo:
|
|
|
|
|
return None
|
|
|
|
|
lo, hi = todo[0]
|
|
|
|
|
return nap_prompt(d, lo, hi, pending_count(d, T) - 1)
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- commands
|
|
|
|
|
|
2026-07-26 19:28:25 +02:00
|
|
|
CONFIG = """\
|
2026-07-26 19:35:22 +02:00
|
|
|
# OptMem sizes for this memory. Uncomment a line to override it; an
|
2026-07-26 19:28:25 +02:00
|
|
|
# absent line tracks the tool's default.
|
|
|
|
|
#
|
|
|
|
|
# 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
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
TEMPLATE = """\
|
|
|
|
|
## Memory
|
|
|
|
|
|
2026-07-26 20:07:10 +02:00
|
|
|
Your memory is OptMem:
|
|
|
|
|
- The tool is `{memo}`
|
|
|
|
|
- The data is `{data}`
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
### At startup: activating OptMem (mandatory)
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
### 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.`
|
2026-07-26 19:28:25 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
2026-07-26 19:39:55 +02:00
|
|
|
def cmd_init(d, args):
|
2026-07-26 19:28:25 +02:00
|
|
|
"""The one command that may create the memory directory, and the whole
|
|
|
|
|
setup: make the store, write the size knobs, print the block the user
|
2026-07-26 19:39:55 +02:00
|
|
|
pastes into their agent's instruction file. Re-running it is safe: it
|
|
|
|
|
only ever creates what is missing, and never rewrites what is there."""
|
2026-07-26 19:28:25 +02:00
|
|
|
if args:
|
|
|
|
|
die("usage: memo init")
|
|
|
|
|
fresh = not os.path.isdir(d)
|
|
|
|
|
os.makedirs(os.path.join(d, "TREE"), exist_ok=True)
|
|
|
|
|
open(log_path(d), "a").close()
|
|
|
|
|
cfg = os.path.join(d, "config")
|
|
|
|
|
if not os.path.exists(cfg):
|
|
|
|
|
with open(cfg, "w") as f:
|
|
|
|
|
f.write(CONFIG)
|
|
|
|
|
config(d)
|
2026-07-26 20:07:10 +02:00
|
|
|
home = os.path.expanduser("~")
|
2026-07-26 19:28:25 +02:00
|
|
|
|
|
|
|
|
def pretty(p):
|
2026-07-26 20:07:10 +02:00
|
|
|
# as the user would type it: keep symlinks, fold $HOME to ~
|
|
|
|
|
p = os.path.abspath(p)
|
2026-07-26 19:28:25 +02:00
|
|
|
return "~" + p[len(home):] if p.startswith(home + os.sep) else p
|
|
|
|
|
|
|
|
|
|
if fresh:
|
|
|
|
|
print("Created %s: this machine's memory, one identity, forever." % pretty(d))
|
|
|
|
|
else:
|
|
|
|
|
print("Found %s: %s." % (pretty(d), plural(log_len(d), "memory")))
|
|
|
|
|
print("Sizes live in %s/config; the defaults are fine." % pretty(d))
|
|
|
|
|
print()
|
|
|
|
|
print("Paste this at the top of your agent's AGENTS.md (or CLAUDE.md), done:")
|
|
|
|
|
print()
|
2026-07-26 20:07:10 +02:00
|
|
|
print(TEMPLATE.format(memo=pretty(__file__), data=pretty(d),
|
2026-07-26 19:28:25 +02:00
|
|
|
chars=ENTRY_CHARS).rstrip())
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 19:22:29 +02:00
|
|
|
def paginate(lines):
|
|
|
|
|
"""Split the document into parts that survive any harness's output cap."""
|
|
|
|
|
parts, cur, size = [], [], 0
|
|
|
|
|
for line in lines:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
n = len(line.encode()) + 1
|
2026-07-25 19:49:34 +02:00
|
|
|
if cur and (len(cur) >= PART_LINES or size + n > PART_CHARS):
|
2026-07-25 19:22:29 +02:00
|
|
|
parts.append(cur)
|
|
|
|
|
cur, size = [], 0
|
|
|
|
|
cur.append(line)
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
size += n
|
2026-07-25 19:22:29 +02:00
|
|
|
if cur:
|
|
|
|
|
parts.append(cur)
|
|
|
|
|
return parts
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 18:58:35 +02:00
|
|
|
def cmd_wake(d, args):
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
now = log_len(d)
|
|
|
|
|
k, T = 1, now
|
|
|
|
|
if args:
|
|
|
|
|
if len(args) > 2 or not all(a.isdigit() for a in args):
|
|
|
|
|
die("usage: memo wake [part [T]]")
|
|
|
|
|
k = int(args[0])
|
|
|
|
|
if len(args) == 2:
|
|
|
|
|
T = int(args[1])
|
|
|
|
|
if T > now:
|
2026-07-25 23:41:26 +02:00
|
|
|
die("T=%d, but the memory holds %s. Run: memo wake"
|
|
|
|
|
% (T, plural(now, "entry")))
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
# A part is rendered as of T, so a note landing between two parts cannot
|
|
|
|
|
# shift a boundary and drop a line.
|
2026-07-26 00:26:48 +02:00
|
|
|
nap = next_nap(d, T)
|
|
|
|
|
if nap:
|
|
|
|
|
n = max(1, pending_count(d, T))
|
2026-07-25 22:38:56 +02:00
|
|
|
print("Cannot wake: %s pending. Do %s, then run memo wake again.\n"
|
2026-07-25 23:41:26 +02:00
|
|
|
% (plural(n, "compression"), "it" if n == 1 else "them"))
|
2026-07-26 00:26:48 +02:00
|
|
|
print(nap)
|
2026-07-25 18:58:35 +02:00
|
|
|
sys.exit(1)
|
2026-07-25 19:34:29 +02:00
|
|
|
if not T:
|
2026-07-25 22:38:56 +02:00
|
|
|
print("No memories yet. Record the first with: memo note \"<one line>\"")
|
|
|
|
|
print("You are awake.")
|
2026-07-25 18:58:35 +02:00
|
|
|
return
|
2026-07-25 19:34:29 +02:00
|
|
|
lines = []
|
|
|
|
|
for lo, hi in cover(T, WAKE_LINES):
|
|
|
|
|
if hi - lo == 1:
|
|
|
|
|
lines.append("#%d %s %s" % log_get(d, lo))
|
|
|
|
|
else:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
s = tree_get(d, lo, hi)
|
|
|
|
|
if s is None:
|
|
|
|
|
die("Summary %d-%d is missing. Run: memo sleep" % (lo, hi - 1))
|
|
|
|
|
lines.append("#%d-%d %s" % (lo, hi - 1, s))
|
2026-07-25 19:22:29 +02:00
|
|
|
parts = paginate(lines)
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
if not 1 <= k <= len(parts):
|
2026-07-25 23:41:26 +02:00
|
|
|
die("No part %d: the memory has %s. Run: memo wake"
|
|
|
|
|
% (k, plural(len(parts), "part")))
|
2026-07-25 19:22:29 +02:00
|
|
|
if len(parts) > 1:
|
2026-07-25 22:38:56 +02:00
|
|
|
print("Your memory, part %d of %d, oldest first." % (k, len(parts)))
|
2026-07-25 19:22:29 +02:00
|
|
|
print("\n".join(parts[k - 1]))
|
|
|
|
|
if k < len(parts):
|
2026-07-25 22:20:01 +02:00
|
|
|
print("Run: memo wake %d %d" % (k + 1, T))
|
2026-07-25 22:12:11 +02:00
|
|
|
else:
|
|
|
|
|
# always, even for a one-part memory: the contract an agent is given
|
|
|
|
|
# is "run parts until one says awake", so it must always arrive
|
2026-07-25 22:20:01 +02:00
|
|
|
print("You are awake.")
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_note(d, args):
|
|
|
|
|
if len(args) != 1:
|
|
|
|
|
die("usage: memo note \"<one line, at most %d chars>\"" % ENTRY_CHARS)
|
|
|
|
|
text = check(args[0])
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
i = log_append(d, [(datetime.date.today().isoformat(), text)])
|
2026-07-25 22:38:56 +02:00
|
|
|
print("Saved as #%d." % i)
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
nap = next_nap(d, i + 1)
|
2026-07-25 19:34:29 +02:00
|
|
|
if nap:
|
|
|
|
|
print("\n" + nap)
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_sleep(d, args):
|
2026-07-25 23:48:22 +02:00
|
|
|
T, said = log_len(d), False
|
2026-07-25 18:58:35 +02:00
|
|
|
if args:
|
2026-07-25 23:48:22 +02:00
|
|
|
said = True
|
2026-07-25 18:58:35 +02:00
|
|
|
if len(args) != 2:
|
|
|
|
|
die("usage: memo sleep <lo>-<hi> \"<one line>\"")
|
|
|
|
|
m = re.fullmatch(r"(\d+)-(\d+)", args[0])
|
|
|
|
|
if not m:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
die("'%s' is not a block id. Copy it from the prompt." % args[0])
|
2026-07-25 22:38:56 +02:00
|
|
|
lo, hi = int(m.group(1)), int(m.group(2)) + 1
|
2026-07-25 19:34:29 +02:00
|
|
|
todo = pending(d, T, limit=1)
|
|
|
|
|
if not todo:
|
2026-07-25 22:38:56 +02:00
|
|
|
print("Nothing left to compress.")
|
|
|
|
|
return
|
2026-07-25 19:34:29 +02:00
|
|
|
if (lo, hi) != todo[0]:
|
2026-07-26 00:26:48 +02:00
|
|
|
if tree_get(d, lo, hi) is not None:
|
|
|
|
|
print("%d-%d is already settled." % (lo, hi - 1))
|
|
|
|
|
else:
|
|
|
|
|
die("Wrong block: %s. Blocks are built in order; the next is "
|
|
|
|
|
"%d-%d. Run: memo sleep"
|
|
|
|
|
% (args[0], todo[0][0], todo[0][1] - 1))
|
|
|
|
|
elif not tree_put(d, lo, hi, check(args[1])):
|
|
|
|
|
print("%d-%d was settled or forgotten meanwhile." % (lo, hi - 1))
|
2026-07-25 18:58:35 +02:00
|
|
|
else:
|
2026-07-25 22:38:56 +02:00
|
|
|
print("%d-%d saved." % (lo, hi - 1))
|
2026-07-25 19:34:29 +02:00
|
|
|
nap = next_nap(d, T)
|
|
|
|
|
if not nap:
|
2026-07-25 22:38:56 +02:00
|
|
|
print("Nothing left to compress.")
|
2026-07-25 18:58:35 +02:00
|
|
|
return
|
2026-07-25 23:48:22 +02:00
|
|
|
print(("\n" if said else "") + nap)
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_forget(d, args):
|
|
|
|
|
"""A summary can be wrong -- mistyped, or a bad compression. Drop it and
|
|
|
|
|
everything built on top of it; the next sleep computes them again. The log
|
|
|
|
|
is untouched, so nothing is ever actually lost."""
|
|
|
|
|
if len(args) != 1:
|
|
|
|
|
die("usage: memo forget <lo>-<hi>")
|
|
|
|
|
m = re.fullmatch(r"(\d+)-(\d+)", args[0])
|
|
|
|
|
if not m:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
die("'%s' is not a block id." % args[0])
|
2026-07-25 22:38:56 +02:00
|
|
|
lo, hi = int(m.group(1)), int(m.group(2)) + 1
|
2026-07-25 19:34:29 +02:00
|
|
|
size = hi - lo
|
|
|
|
|
if size < 2 or size & (size - 1) or lo % size:
|
2026-07-25 22:38:56 +02:00
|
|
|
die("%s is not a block. Copy the id printed by wake, like 16-31."
|
|
|
|
|
% args[0])
|
2026-07-25 19:34:29 +02:00
|
|
|
gone = tree_drop(d, lo, hi)
|
|
|
|
|
if not gone:
|
2026-07-25 22:38:56 +02:00
|
|
|
die("No summary at %s." % args[0])
|
2026-07-25 23:41:26 +02:00
|
|
|
print("Forgot %s, from %d-%d up. Run: memo sleep"
|
|
|
|
|
% (plural(len(gone), "summary"), gone[0][0], gone[0][1] - 1))
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_recall(d, args):
|
|
|
|
|
if len(args) != 1:
|
|
|
|
|
die("usage: memo recall <regex>")
|
|
|
|
|
try:
|
|
|
|
|
pat = re.compile(args[0], re.I)
|
|
|
|
|
except re.error as e:
|
|
|
|
|
die("bad regex: %s" % e)
|
2026-07-26 00:26:48 +02:00
|
|
|
hits = [e for e in log_slice(d, 0, log_len(d))
|
|
|
|
|
if pat.search("#%d %s %s" % e)]
|
2026-07-25 18:58:35 +02:00
|
|
|
if not hits:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
print("No match.")
|
2026-07-25 18:58:35 +02:00
|
|
|
return
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
# Newest first is what a search is usually for, and the output has to fit
|
|
|
|
|
# the same cap `wake` respects.
|
|
|
|
|
out, size = [], 0
|
|
|
|
|
for e in reversed(hits):
|
|
|
|
|
line = "#%d %s %s" % e
|
|
|
|
|
size += len(line.encode()) + 1
|
|
|
|
|
if size > PART_CHARS:
|
|
|
|
|
break
|
|
|
|
|
out.append(line)
|
|
|
|
|
print("\n".join(reversed(out)))
|
|
|
|
|
if len(out) < len(hits):
|
2026-07-25 23:41:26 +02:00
|
|
|
print("Newest %d of %s. Narrow the regex."
|
|
|
|
|
% (len(out), plural(len(hits), "match")))
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
else:
|
2026-07-25 23:41:26 +02:00
|
|
|
print("%s." % plural(len(hits), "match"))
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_import(d, args):
|
|
|
|
|
"""Bulk-append historical memories: 'YYYY-MM-DD <text>' per line.
|
|
|
|
|
For bootstrapping an identity from older records. Used once."""
|
|
|
|
|
if len(args) != 1:
|
|
|
|
|
die("usage: memo import <file> # lines of 'YYYY-MM-DD <text>'")
|
2026-07-25 23:41:26 +02:00
|
|
|
try:
|
|
|
|
|
src = open(args[0]).readlines()
|
|
|
|
|
except OSError as e:
|
|
|
|
|
die("Cannot read %s: %s" % (args[0], e.strerror))
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
last = log_get(d, log_len(d) - 1)[1] if log_len(d) else "0000-00-00"
|
2026-07-25 18:58:35 +02:00
|
|
|
out = []
|
2026-07-25 23:41:26 +02:00
|
|
|
for i, line in enumerate(src, 1):
|
2026-07-25 18:58:35 +02:00
|
|
|
line = line.rstrip("\n")
|
|
|
|
|
if not line.strip():
|
|
|
|
|
continue
|
|
|
|
|
date, _, text = line.partition(" ")
|
|
|
|
|
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
|
|
|
|
|
die("line %d: expected 'YYYY-MM-DD <text>', got: %s" % (i, line))
|
|
|
|
|
if date < last:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
die("line %d: date %s precedes the previous memory (%s)."
|
|
|
|
|
% (i, date, last))
|
2026-07-25 18:58:35 +02:00
|
|
|
text = text.strip()
|
2026-07-25 19:49:34 +02:00
|
|
|
if not text or len(text.encode()) > ENTRY_CHARS:
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
die("line %d: %d bytes, limit %d." % (i, len(text.encode()), ENTRY_CHARS))
|
|
|
|
|
out.append((date, text))
|
2026-07-25 18:58:35 +02:00
|
|
|
last = date
|
2026-07-25 23:41:26 +02:00
|
|
|
if not out:
|
|
|
|
|
die("%s has no memories." % args[0])
|
terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.
Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.
Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
(wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 20:43:08 +02:00
|
|
|
base = log_append(d, out)
|
2026-07-25 23:41:26 +02:00
|
|
|
print("Imported %s, #%d to #%d."
|
|
|
|
|
% (plural(len(out), "memory"), base, base + len(out) - 1))
|
2026-07-25 19:34:29 +02:00
|
|
|
n = pending_count(d, log_len(d))
|
|
|
|
|
if n:
|
2026-07-25 23:41:26 +02:00
|
|
|
print("%s pending. Run: memo sleep" % plural(n, "compression"))
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
2026-07-26 19:39:55 +02:00
|
|
|
COMMANDS = {"init": cmd_init, "wake": cmd_wake, "note": cmd_note,
|
|
|
|
|
"sleep": cmd_sleep, "recall": cmd_recall, "forget": cmd_forget,
|
|
|
|
|
"import": cmd_import}
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2026-07-25 23:41:26 +02:00
|
|
|
if len(sys.argv) < 2:
|
2026-07-25 18:58:35 +02:00
|
|
|
print(__doc__.strip())
|
2026-07-25 23:41:26 +02:00
|
|
|
sys.exit(0)
|
2026-07-26 19:39:55 +02:00
|
|
|
cmd = sys.argv[1]
|
|
|
|
|
if cmd not in COMMANDS:
|
|
|
|
|
print("No such command: %s\n" % cmd, file=sys.stderr)
|
2026-07-25 23:41:26 +02:00
|
|
|
print(__doc__.strip(), file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
2026-07-26 19:39:55 +02:00
|
|
|
# `init` is the only command that may run without an existing memory:
|
|
|
|
|
# it is the one that creates it. Every other command refuses, so a typo
|
|
|
|
|
# in MEMORY_DIR is an error instead of a second, empty identity.
|
|
|
|
|
if cmd == "init":
|
|
|
|
|
cmd_init(memory_dir(), sys.argv[2:])
|
|
|
|
|
return
|
2026-07-25 18:58:35 +02:00
|
|
|
d = store()
|
|
|
|
|
config(d)
|
2026-07-26 19:39:55 +02:00
|
|
|
COMMANDS[cmd](d, sys.argv[2:])
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|