2026-07-25 18:58:35 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""OptMem invariants, checked against a synthetic life of 5000 memories.
|
|
|
|
|
|
|
|
|
|
Uses a fake compressor (join + truncate) so the run is deterministic and free.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
HERE = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
|
sys.path.insert(0, HERE)
|
|
|
|
|
from blocks import complete, cover # noqa: E402
|
|
|
|
|
|
|
|
|
|
N = 2000
|
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
|
|
|
WAKE_LINES = 256
|
|
|
|
|
PART_CHARS = 20000
|
|
|
|
|
# Verified caps of the harnesses in the wild: Claude Code cuts a command's
|
|
|
|
|
# output at 30,000 chars (middle), pi at 50 KB / 2000 lines (head), Codex
|
|
|
|
|
# budgets 10,000 tokens. A part must fit the strictest of each kind.
|
|
|
|
|
CAP_CHARS, CAP_LINES = 30000, 2000
|
2026-07-25 18:58:35 +02:00
|
|
|
ok, fail = 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check(cond, msg):
|
|
|
|
|
global ok, fail
|
|
|
|
|
if cond:
|
|
|
|
|
ok += 1
|
|
|
|
|
else:
|
|
|
|
|
fail += 1
|
|
|
|
|
print("FAIL: " + msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- pure block math -------------------------------------------------
|
|
|
|
|
|
|
|
|
|
for T in list(range(1, 400)) + [1000, 4096, 10000, 65536, 100003]:
|
|
|
|
|
c = cover(T, WAKE_LINES)
|
|
|
|
|
check(len(c) <= WAKE_LINES, "T=%d: %d lines > budget" % (T, len(c)))
|
|
|
|
|
check(c[0][0] == 0 and c[-1][1] == T, "T=%d: does not span [0,T)" % T)
|
|
|
|
|
for a, b in zip(c, c[1:]):
|
|
|
|
|
check(a[1] == b[0], "T=%d: gap or overlap at %s %s" % (T, a, b))
|
|
|
|
|
for lo, hi in c:
|
|
|
|
|
s = hi - lo
|
|
|
|
|
check(s & (s - 1) == 0 and lo % s == 0,
|
|
|
|
|
"T=%d: [%d,%d) is not an aligned power-of-two block" % (T, lo, hi))
|
|
|
|
|
for a, b in zip(c, c[1:]):
|
|
|
|
|
check(b[1] - b[0] <= a[1] - a[0],
|
|
|
|
|
"T=%d: detail does not increase toward the present" % T)
|
|
|
|
|
|
|
|
|
|
check(cover(300, 320) == [(i, i + 1) for i in range(300)],
|
|
|
|
|
"under budget, memory should be verbatim")
|
|
|
|
|
|
|
|
|
|
# every block a cover ever needs must be buildable
|
|
|
|
|
seen = set()
|
|
|
|
|
for T in range(1, 3000):
|
|
|
|
|
seen.update(b for b in cover(T, WAKE_LINES) if b[1] - b[0] > 1)
|
|
|
|
|
buildable = set(complete(3000))
|
|
|
|
|
check(seen <= buildable, "a cover wants a block that complete() never yields")
|
|
|
|
|
|
|
|
|
|
# work never spikes: naps created by one new memory
|
|
|
|
|
worst, prev = 0, 0
|
|
|
|
|
for T in range(1, N):
|
|
|
|
|
cur = len(complete(T))
|
|
|
|
|
worst = max(worst, cur - prev)
|
|
|
|
|
prev = cur
|
|
|
|
|
check(worst <= 16, "a single memory created %d naps" % worst)
|
|
|
|
|
|
|
|
|
|
# ---- the real CLI ----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
d = tempfile.mkdtemp(prefix="optmem-test-")
|
|
|
|
|
env = dict(os.environ, MEMORY_DIR=d)
|
|
|
|
|
memo = [sys.executable, os.path.join(HERE, "memo")]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run(*args):
|
|
|
|
|
return subprocess.run(memo + list(args), env=env, capture_output=True,
|
|
|
|
|
text=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
r = run("note", "x" * 281)
|
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
|
|
|
check(r.returncode == 1 and "Too long" in r.stderr, "over-long note accepted")
|
2026-07-25 18:58:35 +02:00
|
|
|
r = run("note", "two\nlines")
|
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
|
|
|
check(r.returncode == 1 and "one line" in r.stderr, "multi-line note accepted")
|
2026-07-25 18:58:35 +02:00
|
|
|
r = run("note", " ")
|
|
|
|
|
check(r.returncode == 1, "empty note accepted")
|
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
|
|
|
check("No memories yet" in run("wake").stdout, "empty wake should say so")
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
with open(os.path.join(d, "seed.txt"), "w") as f:
|
|
|
|
|
day = datetime.date(2020, 1, 1)
|
|
|
|
|
for i in range(N):
|
|
|
|
|
f.write("%s memory number %d, a thing that happened\n"
|
|
|
|
|
% ((day + datetime.timedelta(days=i // 5)).isoformat(), i))
|
|
|
|
|
r = run("import", os.path.join(d, "seed.txt"))
|
|
|
|
|
check("imported %d" % N in r.stdout, "import failed: " + r.stdout + r.stderr)
|
|
|
|
|
|
|
|
|
|
r = run("wake")
|
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
|
|
|
check(r.returncode == 1 and "Cannot wake" in r.stdout,
|
2026-07-25 18:58:35 +02:00
|
|
|
"wake must refuse while work is pending")
|
|
|
|
|
|
|
|
|
|
# sleep loop, with a fake compressor
|
|
|
|
|
naps = 0
|
|
|
|
|
r = run("sleep")
|
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
|
|
|
while "You are awake" not in r.stdout:
|
2026-07-25 18:58:35 +02:00
|
|
|
line = [l for l in r.stdout.splitlines() if l.strip().startswith("memo sleep ")]
|
|
|
|
|
check(bool(line), "no command offered:\n" + r.stdout + r.stderr)
|
|
|
|
|
if not line:
|
|
|
|
|
break
|
|
|
|
|
bid = line[0].split()[2]
|
|
|
|
|
body = [l.strip() for l in r.stdout.splitlines()
|
|
|
|
|
if l.startswith(" #") or (l.startswith(" ") and l.strip()
|
|
|
|
|
and not l.strip().startswith("memo"))]
|
|
|
|
|
r = run("sleep", bid, (" ".join(body)[:280]).strip() or "empty")
|
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
|
|
|
check(r.returncode == 0, "sleep rejected a valid nap: " + r.stderr)
|
2026-07-25 18:58:35 +02:00
|
|
|
naps += 1
|
|
|
|
|
check(naps == len(complete(N)), "did %d naps, expected %d" % (naps, len(complete(N))))
|
|
|
|
|
|
|
|
|
|
r = run("wake")
|
|
|
|
|
check(r.returncode == 0, "wake still refuses after a full sleep")
|
2026-07-25 19:22:29 +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
|
|
|
# the document survives pagination, and every part fits every harness's cap
|
2026-07-25 19:22:29 +02:00
|
|
|
parts, k = [], 1
|
|
|
|
|
while True:
|
|
|
|
|
r = run("wake", str(k))
|
|
|
|
|
if r.returncode != 0:
|
|
|
|
|
break
|
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
|
|
|
body = [l for l in r.stdout.splitlines() if l.startswith("#")]
|
|
|
|
|
check(len(r.stdout) < CAP_CHARS, "part %d is %d chars, over the %d cap"
|
|
|
|
|
% (k, len(r.stdout), CAP_CHARS))
|
|
|
|
|
check(len(r.stdout.splitlines()) < CAP_LINES, "part %d is over %d lines"
|
|
|
|
|
% (k, CAP_LINES))
|
2026-07-25 19:22:29 +02:00
|
|
|
parts.append(body)
|
|
|
|
|
k += 1
|
|
|
|
|
check(len(parts) > 1, "a %d-line memory should need more than one part" % WAKE_LINES)
|
|
|
|
|
lines = [l for p in parts for l in p]
|
2026-07-25 18:58:35 +02:00
|
|
|
check(len(lines) == WAKE_LINES, "woke with %d lines, want %d" % (len(lines), WAKE_LINES))
|
|
|
|
|
check(lines[-1].startswith("#%d " % (N - 1)), "newest memory not last / not raw")
|
|
|
|
|
check(lines[0].startswith("#0-"), "oldest line should be a summary block")
|
2026-07-25 19:22:29 +02:00
|
|
|
check("memo wake 2" in run("wake").stdout, "part 1 must name the next command")
|
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
|
|
|
check("awake." in run("wake", str(len(parts))).stdout, "last part must say it is last")
|
2026-07-25 19:22:29 +02:00
|
|
|
check(run("wake", str(len(parts) + 1)).returncode == 1, "a nonexistent part should fail")
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
# append-only: nothing was ever rewritten
|
2026-07-25 19:34:29 +02:00
|
|
|
logsz = os.path.getsize(os.path.join(d, "LOG.txt"))
|
2026-07-25 18:58:35 +02:00
|
|
|
run("note", "one more thing happened today")
|
2026-07-25 19:34:29 +02:00
|
|
|
check(os.path.getsize(os.path.join(d, "LOG.txt")) > logsz, "note did not append")
|
|
|
|
|
check(logsz % 320 == 0, "LOG.txt is not a whole number of records")
|
|
|
|
|
for f in os.listdir(os.path.join(d, "TREE")):
|
|
|
|
|
check(os.path.getsize(os.path.join(d, "TREE", f)) % 288 == 0,
|
|
|
|
|
"TREE/%s is not a whole number of records" % f)
|
2026-07-25 18:58:35 +02:00
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
# a block already written cannot be rewritten
|
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
|
|
|
check(run("sleep", "0-2", "attempted overwrite").returncode == 1,
|
2026-07-25 19:34:29 +02:00
|
|
|
"rewriting a settled block was allowed")
|
2026-07-25 18:58:35 +02:00
|
|
|
|
|
|
|
|
# recall reaches memories the summaries lost
|
|
|
|
|
r = run("recall", "memory number 7,")
|
|
|
|
|
check(r.returncode == 0 and "#7 " in r.stdout, "recall missed a memory")
|
|
|
|
|
|
2026-07-25 19:34:29 +02:00
|
|
|
def treesize():
|
|
|
|
|
t = os.path.join(d, "TREE")
|
|
|
|
|
return sum(os.path.getsize(os.path.join(t, f)) for f in os.listdir(t))
|
|
|
|
|
|
|
|
|
|
before, logsize = treesize(), os.path.getsize(os.path.join(d, "LOG.txt"))
|
2026-07-25 18:58:35 +02:00
|
|
|
r = run("forget", "16-32")
|
|
|
|
|
check("16-32" in r.stdout, "forget did not report the block: " + r.stdout + r.stderr)
|
2026-07-25 19:34:29 +02:00
|
|
|
check(treesize() < before, "forget did not shrink the tree")
|
2026-07-25 18:58:35 +02:00
|
|
|
check(os.path.getsize(os.path.join(d, "LOG.txt")) == logsize, "forget touched the log")
|
|
|
|
|
check(run("wake").returncode == 1, "wake should refuse after a forget")
|
|
|
|
|
n = 0
|
2026-07-25 19:34:29 +02:00
|
|
|
while True:
|
2026-07-25 18:58:35 +02:00
|
|
|
r = run("sleep")
|
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 "You are awake" in r.stdout:
|
2026-07-25 19:34:29 +02:00
|
|
|
break
|
2026-07-25 18:58:35 +02:00
|
|
|
bid = [l for l in r.stdout.splitlines() if l.strip().startswith("memo sleep ")][0].split()[2]
|
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
|
|
|
check(run("sleep", bid, "rebuilt after forget").returncode == 0, "rebuild rejected")
|
2026-07-25 18:58:35 +02:00
|
|
|
n += 1
|
2026-07-25 19:34:29 +02:00
|
|
|
check(n > 0, "forget created no work")
|
2026-07-25 18:58:35 +02:00
|
|
|
check(run("wake").returncode == 0, "wake still refuses after rebuilding")
|
2026-07-25 19:34:29 +02:00
|
|
|
check(treesize() == before, "tree did not return to its original size")
|
|
|
|
|
check(run("forget", "17-33").returncode == 1, "forgetting a non-block should fail")
|
|
|
|
|
check(run("forget", "999998-1000000").returncode == 1, "forgetting a missing block should fail")
|
2026-07-25 18:58:35 +02:00
|
|
|
|
2026-07-25 19:49:34 +02:00
|
|
|
# UTF-8: multi-byte characters must not shift record boundaries or dodge limits
|
|
|
|
|
run("note", "reunião com João em São Paulo: ação aprovada, coração tranquilo")
|
|
|
|
|
run("note", "a plain ascii memory right after the accented one")
|
|
|
|
|
r = run("recall", "coração")
|
|
|
|
|
check("João" in r.stdout, "recall lost the accented memory: " + r.stdout + r.stderr)
|
|
|
|
|
r = run("recall", "plain ascii memory right after")
|
|
|
|
|
check("#%d " % (N + 2) in r.stdout, "record after a multi-byte one reads shifted")
|
|
|
|
|
r = run("note", "ã" * 150)
|
|
|
|
|
check(r.returncode == 1 and "300 bytes" in r.stderr,
|
|
|
|
|
"multi-byte note dodged the byte limit: " + r.stderr)
|
|
|
|
|
|
|
|
|
|
# note landed -> its blocks are pending; settle before the final wake check
|
|
|
|
|
while True:
|
|
|
|
|
r = run("sleep")
|
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 "You are awake" in r.stdout:
|
2026-07-25 19:49:34 +02:00
|
|
|
break
|
|
|
|
|
bid = [l for l in r.stdout.splitlines() if l.strip().startswith("memo sleep ")][0].split()[2]
|
|
|
|
|
run("sleep", bid, "settled")
|
|
|
|
|
check(run("wake").returncode == 0, "wake refuses at the very end")
|
|
|
|
|
|
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 mid-wake cannot shift a
|
|
|
|
|
# boundary and silently drop a line
|
|
|
|
|
T0 = os.path.getsize(os.path.join(d, "LOG.txt")) // 320
|
|
|
|
|
before = run("wake", "1", str(T0))
|
|
|
|
|
check(before.returncode == 0, "as-of-T wake failed: " + before.stdout + before.stderr)
|
|
|
|
|
run("note", "a note that lands between two wake calls")
|
|
|
|
|
check(run("wake", "1", str(T0)).stdout == before.stdout,
|
|
|
|
|
"a note between parts changed an already-rendered part")
|
|
|
|
|
check(run("wake", "1", str(T0 + 99)).returncode == 1, "wake accepted a future T")
|
|
|
|
|
|
|
|
|
|
# recall must not hand back more than a harness will carry
|
|
|
|
|
r = run("recall", "memory number")
|
|
|
|
|
check(len(r.stdout) < CAP_CHARS, "recall returned %d chars" % len(r.stdout))
|
|
|
|
|
check("Narrow the regex" in r.stdout, "recall did not say it had been capped")
|
|
|
|
|
|
|
|
|
|
# ---- concurrency and crash recovery ----------------------------------
|
|
|
|
|
|
|
|
|
|
d2 = tempfile.mkdtemp(prefix="optmem-race-")
|
|
|
|
|
env2 = dict(os.environ, MEMORY_DIR=d2)
|
|
|
|
|
P = 16
|
|
|
|
|
procs = [subprocess.Popen(memo + ["note", "parallel note %d" % i], env=env2,
|
|
|
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
for i in range(P)]
|
|
|
|
|
for p in procs:
|
|
|
|
|
p.wait()
|
|
|
|
|
with open(os.path.join(d2, "LOG.txt"), "rb") as f:
|
|
|
|
|
recs = [f.read(320) for _ in range(P)]
|
|
|
|
|
ids = [r.decode().split()[0] for r in recs if r.strip()]
|
|
|
|
|
check(len(ids) == P, "%d of %d parallel notes survived" % (len(ids), P))
|
|
|
|
|
check(len(set(ids)) == P, "parallel notes collided on an id: %s" % sorted(ids))
|
|
|
|
|
check(sorted(ids) == sorted("#%d" % i for i in range(P)),
|
|
|
|
|
"parallel note ids are not 0..%d: %s" % (P - 1, sorted(ids)))
|
|
|
|
|
|
|
|
|
|
# a crash mid-append leaves a partial record; the next append must drop it,
|
|
|
|
|
# or every later record is misaligned forever
|
|
|
|
|
with open(os.path.join(d2, "LOG.txt"), "ab") as f:
|
|
|
|
|
f.write(b"#99 2026-01-01 a half-written record killed by a power cut")
|
|
|
|
|
r = subprocess.run(memo + ["note", "the memory right after a torn write"],
|
|
|
|
|
env=env2, capture_output=True, text=True)
|
|
|
|
|
check(r.returncode == 0, "note failed after a torn write: " + r.stderr)
|
|
|
|
|
sz = os.path.getsize(os.path.join(d2, "LOG.txt"))
|
|
|
|
|
check(sz % 320 == 0, "LOG.txt left misaligned after a torn write: %d" % sz)
|
|
|
|
|
check("saved as #%d" % P in r.stdout, "torn record was counted as a memory")
|
|
|
|
|
r = subprocess.run(memo + ["recall", "right after a torn write"], env=env2,
|
|
|
|
|
capture_output=True, text=True)
|
|
|
|
|
check("#%d " % P in r.stdout, "the memory after a torn write reads wrong")
|
|
|
|
|
|
|
|
|
|
shutil.rmtree(d2)
|
2026-07-25 18:58:35 +02:00
|
|
|
shutil.rmtree(d)
|
|
|
|
|
print("\n%d passed, %d failed" % (ok, fail))
|
|
|
|
|
sys.exit(1 if fail else 0)
|