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:
parent
af17f94c2f
commit
1f1ec1511e
3 changed files with 132 additions and 104 deletions
112
README.md
112
README.md
|
|
@ -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
80
memo
|
|
@ -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
44
test.py
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue