-
fix(marks): v0.2.2 — nine findings from the cross-frontier bug-hunt panel
released this
2026-09-22 00:20:58 -07:00 | 125 commits to main since this release/heid-bug-hunton U2's diff, four arms, artifact-only. Eight findings were
real against live code; a ninth was already closed by v0.2.1 and is recorded as
declined. Full triage in persistent-memory.d/2026-09-22-bug-hunt-panel.md.THE LOCK LIFECYCLE (4/4 convergent, and two defects in one place)
_Locked.__exit__unlinked.marks.lockon the no-op path so a booth that had
never been marked was left exactly as it was found.flockbinds to an INODE:
unlinking it under a blocked waiter leaves that waiter holding an exclusive
lock on a deleted file while the next writer creates a fresh lock and takes it
immediately. Two processes then run the read-modify-write concurrently, the
later os.replace drops the earlier one's mark, and both obeyed the protocol.The cleanup existed to protect the booth's TTL, and was failing at that too:
creating or removing a directory entry bumps the DIRECTORY's mtime, which is
what_newest_mtimeseeds from. The guard's comment reasons about the lock
file's own mtime and misses that the directory moved underneath it.One fix: never unlink the lock, exempt
.<name>.lockdotfiles from
_newest_mtime, and restore the directory's mtime after creating one.THE READ PATH'S BLAST RADIUS
_clean_textdid(text or "").replace(...)andmarks_forsorts on
(created, id), so a storedtextthat was a dict or acreatedthat was a
number raised out of the read path.list_boothsreads every booth's marks on
every index load, so one hand-edited file returned 500 for/and/healthz
across all 25 booths. Guarded in two layers — a named type check and a
_hydrate_safebackstop that cannot raise — and an unreadable mark now renders
as ⚠ broken rather than as an empty note.ALSO
- import_legacy_asks stamped
createdat whole-second resolution, so two
sidecars from the same second lost the ordering the importer had just
established and re-sorted alphabetically. Microseconds, per the stated
(mtime, name)rule. - The five mark-write routes ran a blocking flock on the event loop; they now
dispatch through run_in_threadpool, asserted structurally like INV-1. /answer500'd on a non-stringnotesform value where/notehandled it.- The inline-doc tile had a flag control and no note field.
- The marks panel was suppressed on any booth carrying a links.md.
- The viewer's arrow keys and Escape threw away a note being typed.
CLI
booth marksprinted a traceback and exited 0 on a failed read, and--wait
emitted a whole JSON document per poll.booth answer --waitread a damaged
file as "not yet" and spun the full hour. Both now use real exit codes —
0 ok, 1 unanswered/timed-out, 2 no such pick, 3 unreadable — and--wait
prints once.marks.read_error()lets the CLI ask what the page must not: the
browser stays lenient, the machine consumer gets the truth.scripts/boothhad no tests; it has five now, run against the real script
under the system python3, which also makes them a live check on INV-1.275 tests (253 before). Live service restarted, 25/25 booth pages verified 200.
Downloads
- import_legacy_asks stamped