audit: recall matches the whole line; races answered honestly

- recall searched only the text, so a date or an id found nothing;
  it now matches the rendered line '#id date text'
- a wake refusal read pending twice, so a nap paid in parallel between
  the reads printed a literal None; the nap itself now decides
- a session resubmitting a block another session settled was told
  'Wrong block' (rc 1) for doing what its prompt ordered; it now hears
  'already settled' (rc 0), and the tree_put race message no longer
  claims 'written' when the block may have been forgotten
This commit is contained in:
victortaelin 2026-07-25 19:26:48 -03:00
parent a6abc264c5
commit 58019db0db
2 changed files with 33 additions and 13 deletions

23
memo
View file

@ -343,11 +343,12 @@ def cmd_wake(d, args):
% (T, plural(now, "entry"))) % (T, plural(now, "entry")))
# 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) nap = next_nap(d, T)
if n: if nap:
n = max(1, pending_count(d, T))
print("Cannot wake: %s pending. Do %s, then run memo wake again.\n" print("Cannot wake: %s pending. Do %s, then run memo wake again.\n"
% (plural(n, "compression"), "it" if n == 1 else "them")) % (plural(n, "compression"), "it" if n == 1 else "them"))
print(next_nap(d, T)) print(nap)
sys.exit(1) sys.exit(1)
if not T: if not T:
print("No memories yet. Record the first with: memo note \"<one line>\"") print("No memories yet. Record the first with: memo note \"<one line>\"")
@ -403,11 +404,14 @@ def cmd_sleep(d, args):
print("Nothing left to compress.") print("Nothing left to compress.")
return return
if (lo, hi) != todo[0]: if (lo, hi) != todo[0]:
die("Wrong block: %s. Blocks are built in order; the next is " if tree_get(d, lo, hi) is not None:
"%d-%d. Run: memo sleep" print("%d-%d is already settled." % (lo, hi - 1))
% (args[0], todo[0][0], todo[0][1] - 1)) else:
if not tree_put(d, lo, hi, check(args[1])): die("Wrong block: %s. Blocks are built in order; the next is "
print("Another session already wrote %d-%d." % (lo, hi - 1)) "%d-%d. Run: memo sleep"
% (args[0], todo[0][0], todo[0][1] - 1))
elif not tree_put(d, lo, hi, check(args[1])):
print("%d-%d was settled or forgotten meanwhile." % (lo, hi - 1))
else: else:
print("%d-%d saved." % (lo, hi - 1)) print("%d-%d saved." % (lo, hi - 1))
nap = next_nap(d, T) nap = next_nap(d, T)
@ -445,7 +449,8 @@ def cmd_recall(d, args):
pat = re.compile(args[0], re.I) pat = re.compile(args[0], re.I)
except re.error as e: except re.error as e:
die("bad regex: %s" % e) die("bad regex: %s" % e)
hits = [e for e in log_slice(d, 0, log_len(d)) if pat.search(e[2])] hits = [e for e in log_slice(d, 0, log_len(d))
if pat.search("#%d %s %s" % e)]
if not hits: if not hits:
print("No match.") print("No match.")
return return

23
test.py
View file

@ -167,6 +167,7 @@ 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, check("run memo wake again" in r.stdout,
"the refusal must order the agent back to wake") "the refusal must order the agent back to wake")
check("None" not in r.stdout, "the refusal printed a Python None")
# sleep loop, with a fake compressor # sleep loop, with a fake compressor
naps = 0 naps = 0
@ -229,10 +230,17 @@ r = run("sleep", "0-1", "attempted overwrite")
check(r.returncode == 0 and "Nothing left to compress" in r.stdout, check(r.returncode == 0 and "Nothing left to compress" in r.stdout,
"sleep with nothing pending must say so and write nothing") "sleep with nothing pending must say so and write nothing")
# recall reaches memories the summaries lost # recall reaches memories the summaries lost, and matches the whole line:
# id and date included, not just the text
r = run("recall", "memory number 7,") r = run("recall", "memory number 7,")
check(r.returncode == 0 and "#7 " in r.stdout, "recall missed a memory") check(r.returncode == 0 and "#7 " in r.stdout, "recall missed a memory")
check("1 match." in r.stdout, "a single match is not `1 matches`: " + r.stdout) check("1 match." in r.stdout, "a single match is not `1 matches`: " + r.stdout)
r = run("recall", "^#7 ")
check("memory number 7," in r.stdout, "recall cannot find a memory by id")
r = run("recall", "2020-01-02")
check("#7 " in r.stdout and "5 matches." in r.stdout,
"recall cannot find memories by date: " + r.stdout)
def treesize(): def treesize():
t = os.path.join(d, "TREE") t = os.path.join(d, "TREE")
@ -244,11 +252,18 @@ check("16-31" in r.stdout, "forget did not report the block: " + r.stdout + r.st
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 # a settled block cannot be rewritten. Resubmitting one (two sessions paid
# is ever accepted # the same nap) is not an error: say it is settled, write nothing
mid = treesize()
r = run("sleep", "0-1", "attempted overwrite") r = run("sleep", "0-1", "attempted overwrite")
check(r.returncode == 0 and "already settled" in r.stdout,
"resubmitting a settled block was not reported as settled: " + r.stderr)
check(treesize() == mid, "resubmitting a settled block wrote something")
# a block that is neither settled nor next (here: a dropped ancestor,
# submitted before its half is rebuilt) is a real mistake
r = run("sleep", "0-31", "out of order")
check(r.returncode == 1 and "Wrong block" in r.stderr, check(r.returncode == 1 and "Wrong block" in r.stderr,
"rewriting a settled block was allowed") "an out-of-order block was accepted")
n = 0 n = 0
while True: while True:
r = run("sleep") r = run("sleep")