`.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
2.8 KiB
U4 landed — lifetime is derived, not declared
2026-09-22 · booth
A booth's lifetime stopped being a boolean somebody remembered to press.
Three states now, and sweep_once is the only thing that honours the first two:
KEPT `.forever` present never swept (unchanged)
HELD an open pick, or marks we cannot read never swept (new)
EPHEMERAL everything else 24h (unchanged)
Plus viewing is activity: a deliberately-served response from a booth's own
page route writes .viewed. That dotfile is not a .lock dotfile, so
_newest_mtime already counts it — there is no new arithmetic anywhere.
booth_age_seconds, is_expired and expires_in are byte-for-byte what they
were. A view is one more thing in the tree, which is the same trick .booth.json
used in U5.
What counts as a view, and why the exclusions matter more than the inclusions.
/b/<n>/ (gallery, verbatim report, ?download=1 zip), /b/<n>/view and
/b/<n>/marks count. /b/<n>/marks.json, asset GETs, /, /healthz and a
zoom URL that 404s do NOT. The marks.json exclusion is load-bearing: an agent
must not be able to hold its own booth open by polling for the answer it is
waiting on. /b/<n>/asks is a 308 into /marks and records through it — one
call, not two.
Checked because it would have been silent: nothing in the fleet polls a booth
page. Homepage's siteMonitor for the Booth is /healthz, which is on the
not-a-view list. Had it been pointed at a booth URL, every booth would have
become immortal on deploy and nothing would have reported it.
The hold is unbounded and that is the point — unanswered is unfinished. What
makes it safe is visibility plus two exits that already existed: the card and
every Booth-owned header say 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.
Release is activity, stated rather than accidental. Releasing a kept board
still buys a full TTL — unchanged — but now because booth_unkeep calls
record_view, which is a rule, and no longer because unlinking a file happened
to bump a directory's mtime, which is not. The CLI warning against
"unkeep and let it expire" stays and stays true.
⚠ Running scripts/layout-probe.py over booth pages resets every booth's
clock, because a GET of a booth page is a view and the probe is not exempt
from its own rule. Harmless, recoverable, and noted in the probe so nobody
debugs it later as a sweeper that stopped working.
Contract: docs/contracts/u4_derived_lifetime.contract.md. Both heid panels ran
and the bug hunt after them; see the sibling entries.