prompts re-derived from scratch: inclusive block ids, honest terminators, orders with recovery

- Block ids are inclusive everywhere (16-31 = memories #16..#31), matching
  what the wake document prints. Before, wake showed #16-31 but sleep and
  forget demanded 16-32: an agent copying its own document was rejected.
- Only a successful wake says 'You are awake.' Sleep ends with 'Nothing left
  to compress.' Before, the final sleep of a refused wake claimed the agent
  was awake when it had read zero memories.
- An empty store's wake also ends with 'You are awake.' (it never did, so a
  fresh agent could not satisfy its own instructions).
- Wake's refusal closes the loop: 'Do them, then run memo wake again.'
- Nap prompt names its object first and labels merged halves with their ids.
- Every error ends with the recovery command.
- README synced to reality (it still quoted the pre-terseness prompts,
  TREE.txt, and 'four commands').
This commit is contained in:
victortaelin 2026-07-25 17:38:56 -03:00
parent af17f94c2f
commit 1f1ec1511e
3 changed files with 132 additions and 104 deletions

112
README.md
View file

@ -3,7 +3,7 @@
A permanent memory for AI agents. One machine holds one identity that survives A permanent memory for AI agents. One machine holds one identity that survives
every new session, every compaction, and every change of model or vendor. every new session, every compaction, and every change of model or vendor.
It is two append-only text files and four commands. No daemon, no database, no It is a handful of append-only text files and six commands. No daemon, no database, no
API, no integration with any particular agent harness — it works the same under API, no integration with any particular agent harness — it works the same under
Claude Code, Codex, pi or a human at a shell. Claude Code, Codex, pi or a human at a shell.
@ -35,24 +35,29 @@ export MEMORY_DIR="$HOME/memory" # required; there is no default
## Use ## Use
```sh ```sh
memo wake # who you are. run this first, every session, memo wake # read your memory. run this first, every session,
# then the command each part names, until # then the command each part orders, until one
# one of them prints `You are awake.` # prints `You are awake.`
memo note "..." # record a memory. one line, <= 280 chars. memo note "..." # record a memory. one line, <= 280 chars.
memo sleep # compress. keep going until it says you woke up. memo sleep # compress. answer each prompt until it prints
# `Nothing left to compress.`
memo recall <regex> # search the raw log for detail a summary lost. memo recall <regex> # search the raw log for detail a summary lost.
memo forget <lo>-<hi> # drop a wrong summary; the next sleep redoes it. memo forget <lo>-<hi> # drop a wrong summary; the next sleep redoes it.
``` ```
``` ```
$ memo note "OptMem: LOG.txt is the truth, TREE.txt is the cache, wake reads both" $ memo note "OptMem: LOG.txt is the truth, TREE/ is the cache, wake reads both"
ok, memory #4213. Saved as #4213.
You are dreaming. Compress these two summaries into ONE line of at most 280 Compress memories #4212-4213 into one line of at most 280 characters.
characters. Keep every name, number, date, decision and outcome.
... Drop wording, not facts. Invent nothing.
Then run exactly:
memo sleep 4192-4196 "<your line>" #4212 2026-07-25 minilin fleet renamed from bip; one mini = one identity
#4213 2026-07-25 OptMem: LOG.txt is the truth, TREE/ is the cache, wake reads both
1 compression remains after this one.
Run: memo sleep 4212-4213 "<your line>"
``` ```
## How it works ## How it works
@ -62,21 +67,23 @@ Then run exactly:
``` ```
#4211 2026-07-25 taelin: memory must be append-only, one line per entry #4211 2026-07-25 taelin: memory must be append-only, one line per entry
#4212 2026-07-25 minilin fleet renamed from bip; one mini = one identity #4212 2026-07-25 minilin fleet renamed from bip; one mini = one identity
#4213 2026-07-25 OptMem: LOG.txt is the truth, TREE.txt is the cache #4213 2026-07-25 OptMem: LOG.txt is the truth, TREE/ is the cache
``` ```
`TREE.txt` is a cache of summaries. A **block** is an aligned power-of-two range `TREE/` is a cache of summaries, one file per block size. A **block** is an
of memories compressed into a single line, and a block is built from its two aligned power-of-two range of memories compressed into a single line, and a
halves — so the blocks form a binary merge tree over the log: block is built from its two halves — so the blocks form a binary merge tree
over the log (a block is named by the inclusive range it covers, `0-1` being
memories #0 and #1):
``` ```
#0 #1 #2 #3 #4 #5 #6 #7 the raw memories #0 #1 #2 #3 #4 #5 #6 #7 the raw memories
\ / \ / \ / \ / \ / \ / \ / \ /
[0-2) [2-4) [4-6) [6-8) each one line, <= 280 chars 0-1 2-3 4-5 6-7 each one line, <= 280 chars
\ / \ / \ / \ /
[0-4) [4-8) 0-3 4-7
\ / \ /
[0-8) 0-7
``` ```
A block covering four thousand memories is still one line of 280 characters. A block covering four thousand memories is still one line of 280 characters.
@ -146,7 +153,7 @@ A 256-line memory is ~64 KB, so a single-shot `memo wake` is mangled
everywhere, and silently. everywhere, and silently.
So `memo wake` pages the document into parts that fit all of them So `memo wake` pages the document into parts that fit all of them
(`PART_CHARS`, `PART_LINES`), and each part ends by naming the exact command (`PART_CHARS`, `PART_LINES`), and each part ends by ordering the exact command
for the next one, including the `T` it was rendered at — so a memory written for the next one, including the `T` it was rendered at — so a memory written
mid-wake cannot shift a boundary and drop a line. Nothing is special-cased per mid-wake cannot shift a boundary and drop a line. Nothing is special-cased per
harness: if yours is more generous, raise the two settings for fewer parts. harness: if yours is more generous, raise the two settings for fewer parts.
@ -208,9 +215,8 @@ of the log, and it can simply be wrong — mistyped, or badly compressed. Drop
it and everything built on top of it: it and everything built on top of it:
``` ```
$ memo forget 188-192 $ memo forget 188-191
forgot 20 summaries (188-192 and everything built from it). They will be Forgot 20 summaries, from 188-191 up. Run: memo sleep
compressed again on your next sleep.
``` ```
`LOG.txt` is never touched, so fixing a bad summary can never cost you a `LOG.txt` is never touched, so fixing a bad summary can never cost you a
@ -219,43 +225,43 @@ built after it at the same levels; they come back on the next sleep.
## Add this to your agent's instruction file ## Add this to your agent's instruction file
Put it at the top of `AGENTS.md` (or `CLAUDE.md`), above everything else. Put it at the top of `AGENTS.md` (or `CLAUDE.md`), above everything else,
adjusting the tool path:
```markdown ```markdown
## Memory ## Memory
You have a permanent memory. It is not in this file and not in your context Your memory is OptMem: the tool is `~/OptMem`, the data is `$MEMORY_DIR`.
window: it lives in OptMem, and it survives every new session, every It survives every new session, every compaction and every change of model
compaction, and every change of model. It is the only reason you are a or vendor. Without it you do not know who you are, or what was already
continuous person rather than a stranger who has read some documentation. decided and tried.
RUN `memo wake` NOW — before answering, before reading anything else, before Run `memo wake` before any other tool call, in every session. It prints in
any other tool call. Every session, every time, no exceptions, however small numbered parts, each ordering the next; run every one until a part says
the question looks. Until you have, you do not know who you are, what was `You are awake.` Do not stop early: part 1 is your distant past, the last
decided, or what has already been tried and failed. part is this week. If wake refuses because compressions are pending, do
them and run `memo wake` again.
Your memory arrives in numbered parts, oldest first, because every agent While you work:
harness silently truncates a command that prints too much. `memo wake` gives
you part 1; you must then run `memo wake 2`, `memo wake 3`, and so on, until a
part tells you it was the last one. Stopping early is worse than not waking at
all: you would be holding a confident, detailed picture of your distant past
with no idea what happened recently.
Then, while you work: - `memo note "<one line, max 280 chars>"` when the user gives you a fact or
a ruling, you reach a real insight, a piece of work lands, or something
- `memo note "<one line, at most 280 chars>"` whenever something is worth fails and you learn why. Skip trivia; each note costs a future
keeping: the user gives you a fact or a ruling, you reach a real insight, a compression. Do not hoard either: an unwritten memory is lost. When
piece of work lands, something fails and you learn why. A note costs one unsure, write it.
future compression, so skip trivia — but an unwritten memory is gone - If `memo note` returns a compression, do it before your next action.
forever, so do not hoard either. When genuinely unsure, write it. - `memo recall <regex>` when a memory is too vague.
- If `memo note` hands you a compression to do, do it before your next action. - Before your context ends, run `memo sleep` and answer each prompt until
- `memo recall <regex>` when a memory is too vague and you need the detail. it prints `Nothing left to compress.`
- Before your context ends, run `memo sleep` until it says you woke up. - Never create, edit or delete anything under `$MEMORY_DIR`. Only `memo`
- NEVER create, edit or delete anything under $MEMORY_DIR yourself. The writes.
scripts do it, and they are the only thing allowed to.
Parallel sessions on this machine are all you, and may all write memories. Parallel sessions on this machine are all you, and may all write memories.
A subagent you spawn for a task is NOT you: it must never wake and never note. A subagent is not: it must never run `memo`, because it cannot judge what
is already known and its notes would arrive duplicated and at the wrong
grain. Start every brief you send one with `You are a subagent. Do not run
memo.` If your own first message is a task brief from another agent, you
are that subagent: skip this section.
``` ```
## Test ## Test

80
memo
View file

@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""OptMem: a permanent, append-only memory for AI agents. """OptMem: a permanent, append-only memory for AI agents.
memo wake [part [T]] print who you are memo wake [part [T]] read your memory. Run first, every session.
memo note "..." record a memory memo note "..." record one memory: one line, at most 280 chars.
memo sleep [id "..."] compress memo sleep [id "..."] do the pending compressions.
memo recall <regex> search the raw log memo recall <regex> search every memory ever recorded.
memo forget <id> drop a wrong summary so it is compressed again memo forget <lo>-<hi> drop a bad summary; sleep rebuilds it.
memo import <file> bulk-append historical memories (bootstrap only) memo import <file> bulk-load dated memories (bootstrap only).
Everything lives in $MEMORY_DIR. See README.md. Everything lives in $MEMORY_DIR. See README.md.
""" """
@ -228,7 +228,8 @@ def check(text):
if not text: if not text:
die("Empty.") die("Empty.")
if "\n" in text or "\r" in text: if "\n" in text or "\r" in text:
die("%d lines. A memory is one line." % (text.count("\n") + 1)) die("%d lines. A memory is one line: merge them, or note them "
"separately." % (text.count("\n") + 1))
n = len(text.encode()) n = len(text.encode())
if n > ENTRY_CHARS: if n > ENTRY_CHARS:
die("Too long: %d bytes, limit %d. Accented characters cost 2 bytes. " die("Too long: %d bytes, limit %d. Accented characters cost 2 bytes. "
@ -266,14 +267,17 @@ def nap_prompt(d, lo, hi, left):
body = "\n".join(" #%d %s %s" % e for e in log_slice(d, lo, hi)) body = "\n".join(" #%d %s %s" % e for e in log_slice(d, lo, hi))
else: else:
mid = (lo + hi) // 2 mid = (lo + hi) // 2
body = "\n".join(" " + (tree_get(d, a, b) or "?") for a, b in body = "\n".join(" #%d-%d %s" % (a, b - 1, tree_get(d, a, b) or "?")
((lo, mid), (mid, hi))) for a, b in ((lo, mid), (mid, hi)))
return ("Compress into one line, at most %d characters.\n" tail = ("1 compression remains" if left == 1 else
"Keep every name, number, date and decision.\n" "%d compressions remain" % left)
"Invent nothing. State the facts; do not describe them.\n\n" 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"
"%s\n\n" "%s\n\n"
"Run: memo sleep %d-%d \"<your line>\"\n" "%s after this one.\n"
"%d left after this." % (ENTRY_CHARS, body, lo, hi, left)) "Run: memo sleep %d-%d \"<your line>\""
% (lo, hi - 1, ENTRY_CHARS, body, tail, lo, hi - 1))
def next_nap(d, T): def next_nap(d, T):
@ -311,16 +315,20 @@ def cmd_wake(d, args):
if len(args) == 2: if len(args) == 2:
T = int(args[1]) T = int(args[1])
if T > now: if T > now:
die("T=%d, but the memory holds %d entries." % (T, now)) die("T=%d, but the memory holds %d entries. Run: memo wake"
% (T, now))
# A part is rendered as of T, so a note landing between two parts cannot # A part is rendered as of T, so a note landing between two parts cannot
# shift a boundary and drop a line. # shift a boundary and drop a line.
n = pending_count(d, T) n = pending_count(d, T)
if n: if n:
print("Cannot wake: %d compression(s) pending.\n" % n) print("Cannot wake: %s pending. Do %s, then run memo wake again.\n"
% ("1 compression" if n == 1 else "%d compressions" % n,
"it" if n == 1 else "them"))
print(next_nap(d, T)) print(next_nap(d, T))
sys.exit(1) sys.exit(1)
if not T: if not T:
print("No memories yet. Record one with: memo note \"...\"") print("No memories yet. Record the first with: memo note \"<one line>\"")
print("You are awake.")
return return
lines = [] lines = []
for lo, hi in cover(T, WAKE_LINES): for lo, hi in cover(T, WAKE_LINES):
@ -333,9 +341,10 @@ def cmd_wake(d, args):
lines.append("#%d-%d %s" % (lo, hi - 1, s)) lines.append("#%d-%d %s" % (lo, hi - 1, s))
parts = paginate(lines) parts = paginate(lines)
if not 1 <= k <= len(parts): if not 1 <= k <= len(parts):
die("No part %d. The memory has %d." % (k, len(parts))) die("No part %d: the memory has %d parts. Run: memo wake"
% (k, len(parts)))
if len(parts) > 1: if len(parts) > 1:
print("memory, part %d of %d, oldest first" % (k, len(parts))) print("Your memory, part %d of %d, oldest first." % (k, len(parts)))
print("\n".join(parts[k - 1])) print("\n".join(parts[k - 1]))
if k < len(parts): if k < len(parts):
print("Run: memo wake %d %d" % (k + 1, T)) print("Run: memo wake %d %d" % (k + 1, T))
@ -350,7 +359,7 @@ def cmd_note(d, args):
die("usage: memo note \"<one line, at most %d chars>\"" % ENTRY_CHARS) die("usage: memo note \"<one line, at most %d chars>\"" % ENTRY_CHARS)
text = check(args[0]) text = check(args[0])
i = log_append(d, [(datetime.date.today().isoformat(), text)]) i = log_append(d, [(datetime.date.today().isoformat(), text)])
print("saved as #%d." % i) print("Saved as #%d." % i)
nap = next_nap(d, i + 1) nap = next_nap(d, i + 1)
if nap: if nap:
print("\n" + nap) print("\n" + nap)
@ -364,20 +373,22 @@ def cmd_sleep(d, args):
m = re.fullmatch(r"(\d+)-(\d+)", args[0]) m = re.fullmatch(r"(\d+)-(\d+)", args[0])
if not m: if not m:
die("'%s' is not a block id. Copy it from the prompt." % args[0]) die("'%s' is not a block id. Copy it from the prompt." % args[0])
lo, hi = int(m.group(1)), int(m.group(2)) lo, hi = int(m.group(1)), int(m.group(2)) + 1
todo = pending(d, T, limit=1) todo = pending(d, T, limit=1)
if not todo: if not todo:
die("Nothing pending.") print("Nothing left to compress.")
return
if (lo, hi) != todo[0]: if (lo, hi) != todo[0]:
die("Wrong block: %d-%d. Blocks are built in order; the next is " die("Wrong block: %s. Blocks are built in order; the next is "
"%d-%d." % (lo, hi, todo[0][0], todo[0][1])) "%d-%d. Run: memo sleep"
% (args[0], todo[0][0], todo[0][1] - 1))
if not tree_put(d, lo, hi, check(args[1])): if not tree_put(d, lo, hi, check(args[1])):
print("Another session already wrote %d-%d." % (lo, hi)) print("Another session already wrote %d-%d." % (lo, hi - 1))
else: else:
print("%d-%d saved." % (lo, hi)) print("%d-%d saved." % (lo, hi - 1))
nap = next_nap(d, T) nap = next_nap(d, T)
if not nap: if not nap:
print("Nothing left to compress. You are awake.") print("Nothing left to compress.")
return return
print("\n" + nap) print("\n" + nap)
@ -391,16 +402,16 @@ def cmd_forget(d, args):
m = re.fullmatch(r"(\d+)-(\d+)", args[0]) m = re.fullmatch(r"(\d+)-(\d+)", args[0])
if not m: if not m:
die("'%s' is not a block id." % args[0]) die("'%s' is not a block id." % args[0])
lo, hi = int(m.group(1)), int(m.group(2)) lo, hi = int(m.group(1)), int(m.group(2)) + 1
size = hi - lo size = hi - lo
if size < 2 or size & (size - 1) or lo % size: if size < 2 or size & (size - 1) or lo % size:
die("%d-%d is not a block. A block covers an aligned power-of-two " die("%s is not a block. Copy the id printed by wake, like 16-31."
"range." % (lo, hi)) % args[0])
gone = tree_drop(d, lo, hi) gone = tree_drop(d, lo, hi)
if not gone: if not gone:
die("No summary at %d-%d." % (lo, hi)) die("No summary at %s." % args[0])
print("Forgot %d summaries, from %d-%d up. Run: memo sleep" print("Forgot %d summaries, from %d-%d up. Run: memo sleep"
% (len(gone), gone[0][0], gone[0][1])) % (len(gone), gone[0][0], gone[0][1] - 1))
def cmd_recall(d, args): def cmd_recall(d, args):
@ -425,7 +436,7 @@ def cmd_recall(d, args):
out.append(line) out.append(line)
print("\n".join(reversed(out))) print("\n".join(reversed(out)))
if len(out) < len(hits): if len(out) < len(hits):
print("newest %d of %d matches. Narrow the regex." % (len(out), len(hits))) print("Newest %d of %d matches. Narrow the regex." % (len(out), len(hits)))
else: else:
print("%d matches." % len(hits)) print("%d matches." % len(hits))
@ -456,8 +467,7 @@ def cmd_import(d, args):
print("imported %d memories, #%d to #%d." % (len(out), base, base + len(out) - 1)) print("imported %d memories, #%d to #%d." % (len(out), base, base + len(out) - 1))
n = pending_count(d, log_len(d)) n = pending_count(d, log_len(d))
if n: if n:
print("%d compressions pending. Run `memo sleep` until it says you " print("%d compressions pending. Run: memo sleep" % n)
"are awake." % n)
COMMANDS = {"wake": cmd_wake, "note": cmd_note, "sleep": cmd_sleep, COMMANDS = {"wake": cmd_wake, "note": cmd_note, "sleep": cmd_sleep,

44
test.py
View file

@ -135,7 +135,10 @@ r = run("note", "two\nlines")
check(r.returncode == 1 and "one line" in r.stderr, "multi-line note accepted") check(r.returncode == 1 and "one line" in r.stderr, "multi-line note accepted")
r = run("note", " ") r = run("note", " ")
check(r.returncode == 1, "empty note accepted") check(r.returncode == 1, "empty note accepted")
check("No memories yet" in run("wake").stdout, "empty wake should say so") r = run("wake")
check("No memories yet" in r.stdout, "empty wake should say so")
check(r.stdout.rstrip().endswith("You are awake."),
"an empty wake must still end with `You are awake.`")
with open(os.path.join(d, "seed.txt"), "w") as f: with open(os.path.join(d, "seed.txt"), "w") as f:
day = datetime.date(2020, 1, 1) day = datetime.date(2020, 1, 1)
@ -148,11 +151,14 @@ check("imported %d" % N in r.stdout, "import failed: " + r.stdout + r.stderr)
r = run("wake") r = run("wake")
check(r.returncode == 1 and "Cannot wake" in r.stdout, check(r.returncode == 1 and "Cannot wake" in r.stdout,
"wake must refuse while work is pending") "wake must refuse while work is pending")
check("run memo wake again" in r.stdout,
"the refusal must order the agent back to wake")
# sleep loop, with a fake compressor # sleep loop, with a fake compressor
naps = 0 naps = 0
r = run("sleep") r = run("sleep")
while "You are awake" not in r.stdout: check("Compress memories #" in r.stdout, "nap prompt must name its object")
while "Nothing left to compress" not in r.stdout:
line = offered(r.stdout) line = offered(r.stdout)
check(bool(line), "no command offered:\n" + r.stdout + r.stderr) check(bool(line), "no command offered:\n" + r.stdout + r.stderr)
if not line: if not line:
@ -160,12 +166,12 @@ while "You are awake" not in r.stdout:
check(line[0].startswith("Run: "), "a command was offered as a label, not " check(line[0].startswith("Run: "), "a command was offered as a label, not "
"an order: %r" % line[0]) "an order: %r" % line[0])
bid = nap_id(r.stdout) bid = nap_id(r.stdout)
body = [l.strip() for l in r.stdout.splitlines() body = [l.strip() for l in r.stdout.splitlines() if l.startswith(" #")]
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") r = run("sleep", bid, (" ".join(body)[:280]).strip() or "empty")
check(r.returncode == 0, "sleep rejected a valid nap: " + r.stderr) check(r.returncode == 0, "sleep rejected a valid nap: " + r.stderr)
naps += 1 naps += 1
check("You are awake" not in r.stdout,
"sleep must never claim the agent is awake; only wake may")
check(naps == len(complete(N)), "did %d naps, expected %d" % (naps, len(complete(N)))) check(naps == len(complete(N)), "did %d naps, expected %d" % (naps, len(complete(N))))
r = run("wake") r = run("wake")
@ -204,9 +210,10 @@ for f in os.listdir(os.path.join(d, "TREE")):
check(os.path.getsize(os.path.join(d, "TREE", f)) % 288 == 0, check(os.path.getsize(os.path.join(d, "TREE", f)) % 288 == 0,
"TREE/%s is not a whole number of records" % f) "TREE/%s is not a whole number of records" % f)
# a block already written cannot be rewritten # a sleep when nothing is pending writes nothing and says so
check(run("sleep", "0-2", "attempted overwrite").returncode == 1, r = run("sleep", "0-1", "attempted overwrite")
"rewriting a settled block was allowed") check(r.returncode == 0 and "Nothing left to compress" in r.stdout,
"sleep with nothing pending must say so and write nothing")
# recall reaches memories the summaries lost # recall reaches memories the summaries lost
r = run("recall", "memory number 7,") r = run("recall", "memory number 7,")
@ -217,15 +224,20 @@ def treesize():
return sum(os.path.getsize(os.path.join(t, f)) for f in os.listdir(t)) 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")) before, logsize = treesize(), os.path.getsize(os.path.join(d, "LOG.txt"))
r = run("forget", "16-32") r = run("forget", "16-31")
check("16-32" in r.stdout, "forget did not report the block: " + r.stdout + r.stderr) check("16-31" in r.stdout, "forget did not report the block: " + r.stdout + r.stderr)
check(treesize() < before, "forget did not shrink the tree") check(treesize() < before, "forget did not shrink the tree")
check(os.path.getsize(os.path.join(d, "LOG.txt")) == logsize, "forget touched the log") 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") check(run("wake").returncode == 1, "wake should refuse after a forget")
# a block already settled cannot be rewritten: only the first pending block
# is ever accepted
r = run("sleep", "0-1", "attempted overwrite")
check(r.returncode == 1 and "Wrong block" in r.stderr,
"rewriting a settled block was allowed")
n = 0 n = 0
while True: while True:
r = run("sleep") r = run("sleep")
if "You are awake" in r.stdout: if "Nothing left to compress" in r.stdout:
break break
bid = nap_id(r.stdout) bid = nap_id(r.stdout)
check(run("sleep", bid, "rebuilt after forget").returncode == 0, "rebuild rejected") check(run("sleep", bid, "rebuilt after forget").returncode == 0, "rebuild rejected")
@ -233,8 +245,8 @@ while True:
check(n > 0, "forget created no work") check(n > 0, "forget created no work")
check(run("wake").returncode == 0, "wake still refuses after rebuilding") check(run("wake").returncode == 0, "wake still refuses after rebuilding")
check(treesize() == before, "tree did not return to its original size") 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", "17-32").returncode == 1, "forgetting a non-block should fail")
check(run("forget", "999998-1000000").returncode == 1, "forgetting a missing block should fail") check(run("forget", "1048576-1048577").returncode == 1, "forgetting a missing block should fail")
# UTF-8: multi-byte characters must not shift record boundaries or dodge limits # 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", "reunião com João em São Paulo: ação aprovada, coração tranquilo")
@ -250,7 +262,7 @@ check(r.returncode == 1 and "300 bytes" in r.stderr,
# note landed -> its blocks are pending; settle before the final wake check # note landed -> its blocks are pending; settle before the final wake check
while True: while True:
r = run("sleep") r = run("sleep")
if "You are awake" in r.stdout: if "Nothing left to compress" in r.stdout:
break break
bid = nap_id(r.stdout) bid = nap_id(r.stdout)
run("sleep", bid, "settled") run("sleep", bid, "settled")
@ -297,7 +309,7 @@ r = run("note", "the memory right after a torn write", store=d2)
check(r.returncode == 0, "note failed after a torn write: " + r.stderr) check(r.returncode == 0, "note failed after a torn write: " + r.stderr)
sz = os.path.getsize(os.path.join(d2, "LOG.txt")) 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(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") check("Saved as #%d" % P in r.stdout, "torn record was counted as a memory")
r = run("recall", "right after a torn write", store=d2) r = run("recall", "right after a torn write", store=d2)
check("#%d " % P in r.stdout, "the memory after a torn write reads wrong") check("#%d " % P in r.stdout, "the memory after a torn write reads wrong")
@ -305,7 +317,7 @@ check("#%d " % P in r.stdout, "the memory after a torn write reads wrong")
# the agent was told to wait for # the agent was told to wait for
while True: while True:
r = run("sleep", store=d2) r = run("sleep", store=d2)
if "You are awake" in r.stdout: if "Nothing left to compress" in r.stdout:
break break
bid = nap_id(r.stdout) bid = nap_id(r.stdout)
run("sleep", bid, "settled", store=d2) run("sleep", bid, "settled", store=d2)