2026-07-25 18:58:35 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""OptMem: a permanent, append-only memory for AI agents.
|
|
|
|
|
|
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
|
|
|
|
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
|
|
|
Everything lives in $MEMORY_DIR. 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
|
|
|
|
|
|
|
|
|
|
def store():
|
|
|
|
|
d = os.environ.get("MEMORY_DIR")
|
|
|
|
|
if not d:
|
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("MEMORY_DIR is not set. Example: export MEMORY_DIR=~/memory")
|
2026-07-25 18:58:35 +02:00
|
|
|
d = os.path.expanduser(d)
|
2026-07-25 23:41:26 +02:00
|
|
|
# The directory is never created here. A typo in MEMORY_DIR would then
|
|
|
|
|
# open an empty store, and the agent would wake with no past and start
|
|
|
|
|
# writing a second identity. Making the directory IS creating the
|
|
|
|
|
# identity, and that is a deliberate act: mkdir.
|
|
|
|
|
if not os.path.isdir(d):
|
|
|
|
|
die("MEMORY_DIR=%s does not exist.\nIf this is a new identity, run: "
|
|
|
|
|
"mkdir -p %s" % (d, 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-25 23:41:26 +02:00
|
|
|
"""Optional overrides in $MEMORY_DIR/config. The file is never written:
|
|
|
|
|
a store that kept its own 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-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.
|
|
|
|
|
n = pending_count(d, T)
|
|
|
|
|
if n:
|
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"))
|
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(next_nap(d, T))
|
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-25 22:38:56 +02:00
|
|
|
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))
|
2026-07-25 19:34:29 +02:00
|
|
|
if not tree_put(d, lo, hi, check(args[1])):
|
2026-07-25 22:38:56 +02:00
|
|
|
print("Another session already wrote %d-%d." % (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-25 19:34:29 +02:00
|
|
|
hits = [e for e in log_slice(d, 0, log_len(d)) if pat.search(e[2])]
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
COMMANDS = {"wake": cmd_wake, "note": cmd_note, "sleep": cmd_sleep,
|
|
|
|
|
"recall": cmd_recall, "forget": cmd_forget, "import": cmd_import}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
if sys.argv[1] not in COMMANDS:
|
|
|
|
|
print("No such command: %s\n" % sys.argv[1], file=sys.stderr)
|
|
|
|
|
print(__doc__.strip(), file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
2026-07-25 18:58:35 +02:00
|
|
|
d = store()
|
|
|
|
|
config(d)
|
|
|
|
|
COMMANDS[sys.argv[1]](d, sys.argv[2:])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|