Files
booth/persistent-memory.d/2026-09-22-third-one-branch-template-miss.md
vh c3a97c1b64 feat(u4): a booth's lifetime is derived from its state, not from a boolean
`.forever` was the only way to say three different things — "this is durable",
"I have not answered yet", "I am still looking" — and the census said it was
carrying all three: 17 of 24 live booths (70%, up from 54% the day before).
Three of the four booths in the fleet awaiting an answer had been pinned by
hand as well, and 10 of the 17 were younger than the TTL, so the sentinel had
bought them nothing and was pressed pre-emptively.

Only the first meaning is what `keep` means. The other two are facts the
service already held and did not consult.

    KEPT       `.forever` present                      never swept  (unchanged)
    HELD       an open pick, or marks we cannot read   never swept  (new)
    EPHEMERAL  everything else                         24h          (unchanged)

Viewing is activity: a deliberately-served response from a booth's own page
route writes `.viewed`, which is a dotfile and not a `.lock` dotfile, so
`_newest_mtime` already counts it. There is no new arithmetic — `booth_age_seconds`,
`is_expired` and `expires_in` are unchanged. Machine reads are excluded on
purpose: an agent must not be able to hold its own booth open by polling for
the answer it is waiting on.

The hold is unbounded, and what makes that safe is visibility plus two exits
that already existed. Every surface whose chrome the Booth owns says
`held until answered` where the countdown was, and `booth rm` / the UI x /
`DELETE /b/<n>` take a held booth exactly as they take a kept one. A hold is
protection from the timer, never from the operator.

Three cross-frontier panels ran and each found a class the others could not:

  * the paraphrase panel found that two reads of one file are not one read of
    one state — the contract's `is_held(marks_for(c), read_error(c))` could
    resolve to `([], None)`, the pair that deletes. `hold_read` is one read.
  * the code-review panel found, 4-of-4, that the booth header's board branch
    rendered no lifetime at all; and that five of seven invariant tests passed
    under the change that defeats them.
  * the bug-hunt panel found four more paths where a failed read still
    authorized a delete, and a `record_view` that followed a planted symlink.

`is_held` became `hold_reason`, which returns the reason rather than a bool
beside a string that can disagree with it.

Prediction, to re-count on or after 2026-10-06: the `.forever` rate falls to
the booths that are genuinely durable references. Only 4 booths carry marks at
all, so this rests on both halves of the unit; a null result cannot distinguish
a wrong diagnosis from a habit that outlived its need.

406 tests (341 before). Contract: docs/contracts/u4_derived_lifetime.contract.md
2026-09-22 09:44:25 -07:00

2.3 KiB

The third one-branch template miss — this repo's recurring blind spot

2026-09-22 · booth

All four arms of the U4 code-review panel found the same drift, independently. That is the strongest convergence either panel has produced here.

The booth header's sub-line forks on {% if board %}, and the U4 lifetime macro had been added only to the {% else %}. So a booth carrying links.md rendered a link count and nothing at all about its lifetime — no countdown, no hold — while INV-4 said the templates have no path that renders neither. The standing board being kept by construction (booth link drops .forever on first use) is what hid it; a released board or a hand-made links.md booth is a live non-kept booth on that path, and both are reachable from the UI.

This is the third of the same shape in this repo's short history:

  1. blurtoggle — the blur only patched the image/video <figure>; inline docs render through their OWN branch and shipped unblurred. Suite green; a live look caught it.
  2. verbatim chrome — a verbatim booth's own index.html is served untouched, so the inline marks panel never renders there. Found by looking at the live service during U4, not by the suite.
  3. the board branch — this one.

The pattern: the suite renders the surface the author was thinking about. Every one of these was a second branch of a conditional the author had already satisfied once and stopped reading. A cold reader with no idea which branch was "the real one" finds them; the author does not, and neither does a test the author wrote.

Practical consequence for this repo. When a template gains a fact, grep the template for {% if %} in the block you edited and render EVERY branch in a test — one test per branch, each rendering only its own surface, or the passing test on branch A will mask the omission on branch B. U4 now has one per surface (index card, booth header, board header, marks page) for exactly this reason.

Declined, and worth recording: Regin and Kimi both recommended amending INV-4 to carve the board header out, on the grounds that board layout belongs to U7. Cutting an invariant down to fit an implementation gap is the wrong direction when the fix is one template edit, and U7 owns navigation and section layout — not whether a header states a lifetime.