fix(marks): v0.2.2 — nine findings from the cross-frontier bug-hunt panel
`/heid-bug-hunt` on 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.lock` on the no-op path so a booth that had never been marked was left exactly as it was found. `flock` binds 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_mtime` seeds 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>.lock` dotfiles from `_newest_mtime`, and restore the directory's mtime after creating one. THE READ PATH'S BLAST RADIUS `_clean_text` did `(text or "").replace(...)` and `marks_for` sorts on `(created, id)`, so a stored `text` that was a dict or a `created` that was a number raised out of the read path. `list_booths` reads 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_safe` backstop that cannot raise — and an unreadable mark now renders as ⚠ broken rather than as an empty note. ALSO - import_legacy_asks stamped `created` at 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. - `/answer` 500'd on a non-string `notes` form value where `/note` handled 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 marks` printed a traceback and exited 0 on a failed read, and `--wait` emitted a whole JSON document per poll. `booth answer --wait` read 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/booth` had 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.
This commit is contained in:
+53
-9
@@ -1,6 +1,6 @@
|
||||
# Persistent memory — booth
|
||||
|
||||
_Last updated: 2026-09-21_
|
||||
_Last updated: 2026-09-22_
|
||||
|
||||
> **Always check for `/tmp/booth-dev-handoff.md`** — if it exists and its
|
||||
> `Written:` stamp is under 8 hours old, read it (it carries the in-flight
|
||||
@@ -17,13 +17,15 @@ loop it turned out to actually be.
|
||||
|
||||
## Current state / in-flight
|
||||
|
||||
_As of 2026-09-21 (late):_
|
||||
_As of 2026-09-22:_
|
||||
|
||||
- **v1 is gated on seven units** in `ROADMAP.md`, dependency-ordered
|
||||
**U1 → U2 → {U3, U4, U5} → U7**, with **U6 independent**.
|
||||
- **U1 and U2 are landed and released.** Current version `0.2.1`, deployed to the
|
||||
live service, 253 tests green, tree clean. U1 `ce598b3`; U2 `c7f9437` released as
|
||||
`v0.2.0`, then `5e41108` as `v0.2.1` carrying four cross-frontier panel findings.
|
||||
- **U1 and U2 are landed and released.** Current version `0.2.2`, deployed to the
|
||||
live service, 275 tests green, tree clean, 25/25 booth pages verified 200 after
|
||||
the deploy. U1 `ce598b3`; U2 `c7f9437` released as `v0.2.0`, then `5e41108` as
|
||||
`v0.2.1` (four contract-panel findings), then `v0.2.2` carrying the
|
||||
**bug-hunt panel's** nine (below).
|
||||
- **U5 is the next unit** (operator, 2026-09-21): **self-announcing booths.**
|
||||
`.booth.json` carrying `{handle, title, why, created}`, written by the CLI from
|
||||
`$ALTHING_HANDLE`; the index card gains provenance and a one-line purpose, and
|
||||
@@ -38,16 +40,58 @@ _As of 2026-09-21 (late):_
|
||||
and that rule must stay stated. Also worth knowing before scoping: enforcing the
|
||||
link rule without giving job 5 a home first just makes it homeless — that is the
|
||||
lesson from the 69% rot, and U5 is the home.
|
||||
- **One heid dispatch is still outstanding**: the `/heid-bug-hunt` on U2's diff,
|
||||
msg `01M33XEC1H0298C0D968FWBN7A`, thread same. Triage it the same five-category
|
||||
way when it lands. **Do not assume it is ceremony** — the contract-review panel
|
||||
that preceded it found three real defects in already-released code.
|
||||
- **No heid dispatch is outstanding.** The `/heid-bug-hunt` on U2's diff landed
|
||||
2026-09-22 and shipped as `v0.2.2`; see the dated entry below.
|
||||
- Live service `active` on `:8090`, 25 booths, verified 25 × 3 page types after the
|
||||
last deploy. The booth set churns: `sindra20-engines` and `sindra-finalists` were
|
||||
swept during the session, `cr123a-to-d-sleeve` and `sindra` appeared.
|
||||
|
||||
## Recent decisions
|
||||
|
||||
- `[2026-09-22]` **The U2 bug-hunt panel landed and it was not ceremony —
|
||||
`v0.2.2`.** Nine adopted findings across four arms; eight were real against
|
||||
live code and one was already fixed. The headline was **4/4 convergent from
|
||||
four different angles**: `_Locked.__exit__` unlinked `.marks.lock` on the no-op
|
||||
path, and `flock` binds to an INODE — so a writer blocked on the old inode
|
||||
proceeds while the next writer creates a fresh lock file and takes it at once.
|
||||
Two processes then run the read-modify-write concurrently and the later
|
||||
`os.replace` drops a mark, with both of them obeying the protocol. **The
|
||||
cleanup existed to protect the booth's TTL and it was failing at that too**:
|
||||
creating and removing a directory entry bumps the DIRECTORY's mtime, which is
|
||||
what `_newest_mtime` actually seeds from, so a no-op reset the clock it was
|
||||
written to leave alone. Same code region, two defects, one fix — never unlink
|
||||
the lock, exempt `.<name>.lock` dotfiles from `_newest_mtime`, and put the
|
||||
directory's mtime back after creating one. Full triage in
|
||||
`persistent-memory.d/2026-09-22-bug-hunt-panel.md`.
|
||||
- `[2026-09-22]` **The lenient reader's blast radius was the whole service, not
|
||||
one booth.** `_clean_text` did `(text or "").replace(...)` and `marks_for`
|
||||
sorts on `(created, id)`, so a stored `text` that was a dict or a `created`
|
||||
that was a number raised out of the READ path — and `list_booths` reads every
|
||||
booth's marks on every index load. One hand-edited file 500'd `/` and
|
||||
`/healthz` for all 25 booths. Fixed in two layers, matching the house posture:
|
||||
a named type check (`_entry_type_error`) plus a `_hydrate_safe` backstop that
|
||||
cannot raise, and the panel now RENDERS an unreadable mark as ⚠ broken instead
|
||||
of as an empty note. **The general shape: a lenient reader is only lenient if
|
||||
the leniency is bounded by where it runs.** `marks_for` was written for one
|
||||
booth's page and is called in a loop over every booth.
|
||||
- `[2026-09-22]` **`booth marks` / `booth answer` got real exit codes**, because
|
||||
a read that CRASHED was indistinguishable from a read that said no. `marks`
|
||||
printed a traceback and exited 0 (a caller's `jq` saw success and got
|
||||
nothing); `answer --wait` read a damaged file as "not yet" and spun for the
|
||||
full hour before blaming the operator. Now `0 ok · 1 unanswered/timed-out ·
|
||||
2 no such pick · 3 unreadable`, and `read_error()` was added to `marks.py` so
|
||||
the CLI can ask the question the browser must not: the page stays lenient, the
|
||||
machine consumer gets the truth. Also `--wait` now prints ONCE — it was
|
||||
emitting a whole JSON document per poll, so a captured `--wait` held several
|
||||
concatenated values and parsed as none of them.
|
||||
- `[2026-09-22]` **`scripts/booth` had zero tests and now has five**
|
||||
(`tests/test_cli.py`). The panel's guard-strength tables returned UNVERIFIED
|
||||
for every CLI claim because nothing in the suite executed the script — two of
|
||||
the round's findings lived in exactly that gap. The new tests run the real
|
||||
script under the system `python3`, which makes them a live check on INV-1
|
||||
(stdlib-only) as a side effect: a third-party import in `marks.py` now fails
|
||||
in the suite the same way it would fail on a fleet host.
|
||||
|
||||
- `[2026-09-21]` **v0.2.0 cut and announced; v0.2.1 fixed what the announcement
|
||||
was already wrong about.** Operator approved the minor (a v1 unit closed plus a
|
||||
CLI surface change for 17 consuming handles clears the release-note bar). The
|
||||
|
||||
Reference in New Issue
Block a user