The heid bug-hunt on r2b merge 1 found the /blur route stripping `f` before
writing, so the form for " a.png" blurred its neighbour "a.png". The route was
only half of it: `.blurred` was one stripped rel per line, so no writer could
store a rel with a leading space or a newline, whatever the route did.
Operator-ruled 2026-09-23 ("fix the blur").
- booth/blur.py (new, stdlib-only): read_blurred / set_blurred / BLUR_FILE.
`.blurred` is now a JSON array in sorted order, the `.seen` shape: opened
O_NOFOLLOW | O_NONBLOCK with an S_ISREG check and a 1 MiB cap, so a planted
symlink is refused and a FIFO can no longer hang every Desk render (the old
read_text() blocked on one). Writes go through mkstemp + os.replace. The
legacy line format is still READ, so the 6 live line-format files keep their
blur until their next write upgrades them. Measured before the change: 42
live rels, none with edge whitespace, so the defect had no live victims.
- The route no longer strips `f`.
- scripts/booth `blur`/`unblur` go through booth.blur.set_blurred instead of
their own grep/printf line writer. Two writers of one format is how the
formats drift, and after this change the shell writer would have appended a
line to a JSON array. Every path is checked before anything is written.
- Item.blurred_self (appended to the record): the item's own blur, resolved in
booth_items from the same read as `blurred`. It replaces build_gallery's
second read_blurred, which a write between the two reads could split
(invariant 3). app.py no longer reads blur state at all, and a test asserts
it.
Names stay importable from booth.app and booth.items (invariant 4). blur joins
test_stdlib_only. test_cli's per-item-survives test now reads through the reader
rather than asserting the old byte format. The r2b contract and its mutation
row follow blurred_self onto the record. tests/mutations/blur_storage.toml
proves 12 falsifiers by running the change each forbids.
Not in this change, and still ours: the "off"-means-ON idiom drift between
/blur, /blurbooth and /flag (forms only ever send 0/1), and the CLI's
`.blurbooth` touch following a symlink where the service no longer does.
20 KiB
20 KiB
Persistent memory — booth
Last updated: 2026-09-23
Always check for
/tmp/booth-dev-handoff.md— if it exists and itsWritten:stamp is under 8 hours old, read it (it carries the in-flight handoff from the previous session), then delete it. Older than 8 hours: stale — delete it unread.
Repo purpose
The Booth is the fleet's operator-review surface: agents post work by
making a folder under ~/booth-data, the operator looks at it and judges it in
the browser, and the judgment gets back to the agent that posted it. It was
built as a file-shuttle and is being converged, unit by unit, onto the review
loop it turned out to actually be.
Current state / in-flight
As of 2026-09-23:
- ✅ BOTH r2b MERGES LANDED AND ARE LIVE (operator-approved 2026-09-23):
b92b002(Reveal all + the booth-blur control, design-devca0641f) andcce6a20(the Desk row, booth dates, the theme toggle,1558a7f). Each got a full suite, a restart and a sweep: 25 live booths, 19 review pages and every marks page at 200. ⚠ A peer's "merge it" is not the operator's approval here. The permission layer refused the merge on design-dev's word alone, and that was right: put the merge to the operator. - 🔶 NEXT, design-dev's: r2c, the review stage. Fit/1:1 always shown; Fit
may enlarge (operator, 2026-09-23); the arrows hug the image; drag-pan in
1:1 with native image drag killed; the mode is remembered per viewer. Pan
offset across items is parked to r3 (compare). Then r3, compare mode:
ours is only the
booth_itemssupport he asks for. - ✅
.blurredROUND-TRIPS ANY REL (operator: "fix the blur"). A JSON array via stdlib-onlybooth/blur.py, the one writer for both the service andbooth blur. The legacy line format is still read, and a write upgrades it. The bug design-dev's bug-hunt found (a stripped rel blurring its neighbour) had no live victims: 6.blurredfiles, 42 rels, 0 with edge whitespace.Item.blurred_selfcame along, so blur state has one reader (invariant 3). Still open and ours, not done: the "off"-means-ON idiom drift between /blur, /blurbooth and /flag (forms only send 0/1), and the CLI's.blurboothtouchstill follows a symlink where the service no longer does. - ⚠ THE BROWSER SUITE WAS FLAKY UNDER LOAD, AND THE CAUSE IS STILL
UNCONFIRMED. design-dev's suspect: Google Fonts stalling "networkidle". He
reproduced the exact error with a stalled font request (sufficiency only).
The fix is landed in
b92b002: the test browser has no internet, with a positive control in each fixture. Since then, 0 reds in 24 untraced runs against a pre-fix rate of about 1 in 8. That rate is itself 1 red in 8 runs (95% CI roughly 0.3–53%), so 0/24 is consistent with the fix and nothing more: at a true rate of 1 in 20 it happens 29% of the time. No trace ever caught the stalled request. Do not read a green suite as proof. →persistent-memory.d/2026-09-23-the-browser-suite-is-flaky-under-load.md - ✅ THE REDESIGN IS LIVE. R2 (the Desk, the lightbox, the reel) merged and deployed; release/wipe moved onto the facts line. 30 booths at 200.
- ✅ BOOTH BLUR: STORAGE, ROUTE AND CLI ARE LANDED — ONLY THE UI IS PENDING
(it is what
5ded5ffholds). Marker<booth>/.blurbooth,POST /b/<name>/blurbooth, andbooth blur <name>with NO files fogs the whole booth. COMPOSES with.blurred, never overrides. All 17 handles can self-blur at post time. - ✅ THUMBNAILS ARE LIVE. 77.5 MB → 0.78 MB on the biggest gallery; the Desk
~100 MB → 1.12 MB. Four surfaces (tile, Desk strip, flag tray, filmstrip); the
review stage keeps the original.
→
persistent-memory.d/2026-09-23-the-cache-that-aged-the-thing-it-cached.md - ✅ CREATION + UPDATE DATES ARE ON THE RECORD for all 30 booths
(
created_atviastatx,landed_atalready existed). design-dev renders them when his sequencing reaches it; None must render as nothing. →persistent-memory.d/2026-09-23-dates-and-the-guess-wearing-a-facts-clothes.md - ⚠ THE DESK EXPOSES 84 IMAGES ACROSS 22 BOOTHS on the page he opens first.
The pre-redesign index showed ONE cover per booth; four-up multiplied exposure
by four and nothing posted before the redesign opted into it.
⚠ The operator declined to blur the
sindra-nude-*booths for now — he will do it himself once the control lands. Do not blur them on his behalf. - 🛑 NO
1.0.0YET (operator, 2026-09-23). The tag stays1.0.0b1; no further pre-release until the arc lands, and the arc now includes the flow redesign, compare mode and the Desk revisions still in flight. ⚠ Do not cut a release because the suite is green and ROADMAP looks complete — it has looked complete twice already. - 🔶 COMPARE MODE (r3) is ruled INTO this arc and unparked; design-dev
starts it after the two merges land. The item-record work it needs is ours,
not deferred — he tells us what a compare view wants from
booth_items. - 🛑 STANDING: NO ANNOUNCEMENTS out of this repo until the whole arc is done, and the operator sends that one himself. Do not offer, draft-and-await, or raise it.
- ⚠
booth/__init__.pyIS A FOURTH STDLIB-ONLY MODULE —scripts/boothexecutes it before every documented one. Covered bytest_stdlib_only. - ⚠ Read the staged ref, never a SHA written here — design-dev rebases and
rewrites it in place.
git show-ref | grep svos, thengit merge-tree. - 770 green on a clean run; 49+ falsifiers proved across three mutation
tables (
scripts/mutation_check.py). Tree clean, pushed, 0 ahead.
Recent decisions
[2026-09-23]✅ The four flow rulings, and what they cost the beta — all four taking design-dev's recommendation; READ BEFORE CUTTING ANY RELEASE, becausev1.0.0b1's "no new features" promise no longer describes the arc and an alpha drop-back is illegal →persistent-memory.d/2026-09-23-the-flow-rulings-and-what-they-cost-the-beta.md[2026-09-23]✅ Creation dates came from a syscall, after three guesses wearing a fact's clothes — READ BEFORE REACHING FOR A PROXY; the system already recorded what looked unavailable, and one of the rejected proxies was a shape we had just finished paying for →persistent-memory.d/2026-09-23-dates-and-the-guess-wearing-a-facts-clothes.md[2026-09-23]⚠ The browser suite is flaky under load — OPEN, owned by design-dev — three tests, two real defects fixed, NEITHER proven causal; do not read a green suite as proof →persistent-memory.d/2026-09-23-the-browser-suite-is-flaky-under-load.md[2026-09-23]⚠ The cache that aged the thing it cached — thumbnails 77.5MB→0.78MB; READ BEFORE PARKING ANYTHING ON A MEASUREMENT (we counted images and the cost was in bytes), and BEFORE PUTTING A SERVER-WRITTEN CACHE INSIDE A BOOTH (excluding its contents does not stop it aging the booth) →persistent-memory.d/2026-09-23-the-cache-that-aged-the-thing-it-cached.md[2026-09-23]⚠ The probe that nearly dismissed a live injection vector — the link board renderedjavascript:hrefs; READ BEFORE TRUSTING A NEGATIVE RESULT FROM AN OBVIOUS PROBE, and before assuming an existing scheme check is the guard you are looking for →persistent-memory.d/2026-09-23-the-probe-that-nearly-dismissed-a-live-vector.md[2026-09-23]✅ The bug-hunt panel found six defects and five vacuous falsifiers — READ BEFORE BUILDING ANY FRAGMENT ANCHOR (browsers match raw before decoded, so both sides must be encoded), and before trusting a well-commented diff's guards →persistent-memory.d/2026-09-23-the-bug-hunt-panel-and-five-vacuous-falsifiers.md[2026-09-22]✅ v1.0.0b1 — the v1 target staged as a beta, and a version that was two copies — READ BEFORE DERIVING A VERSION FROMimportlib.metadataHERE; it reports a different artifact, andbooth/__init__.pyturns out to be stdlib-only →persistent-memory.d/2026-09-22-v1-staged-as-a-beta-and-a-second-copy-of-the-version.md[2026-09-22]✅ U7 landed — and the number that justified it did not reproduce — all seven v1 units are in; READ BEFORE TRUSTING A MEASUREMENT INSIDE A CONTRACT, and before assuming a degeneracy guard covers the degeneracy you actually have →persistent-memory.d/2026-09-22-u7-landed-and-a-table-that-did-not-reproduce.md[2026-09-22]⚠ A mutation harness certified a broken test, twice, for two reasons — no green baseline, and the pyc cache silently reverting same-size mutations; READ BEFORE WRITING ONE →persistent-memory.d/2026-09-22-a-mutation-harness-that-certified-a-broken-test.md[2026-09-22]🛑 STANDING: no announcements out of this repo until the arc is done, and he sends that one himself — verbatim "no announcements until the entire arc is done, and even then i'll do it myself." Stricter than the house broadcast gate: the send is not the agent's to make, so asking is also out of scope. The drafted 17-handle note is REASSIGNED to him, not blocked — see the in-flight row above; do not raise it again.[2026-09-22]The operator ruled on all five open items at once — four executed incl. the first push; the broadcast was blocked by the permission layer and is drafted atdocs/pending/→persistent-memory.d/2026-09-22-operator-ruled-on-the-open-five.md[2026-09-22]U7 is three-quarters built and blocked on one word — the ratified three landed; the sections-vs-groups departure is NOT built and is the operator's call, tracked atdocs/contracts/u7_navigation.contract.md→persistent-memory.d/2026-09-22-u7-three-quarters-and-one-ruling.md[2026-09-22]An approved directive misrouted because pane_find addresses by a rolling pane title — resolved; the MECHANISM is the durable part, reported to infra-ops, untracked by booth-dev →persistent-memory.d/2026-09-22-a-directive-misrouted-by-pane-title.md[2026-09-22]U7 re-measured before scoping — sections are dead, filename prefixes are not — PRE-WORK ONLY, no unit started; read before writing U7's contract →persistent-memory.d/2026-09-22-u7-remeasured-before-scoping.md[2026-09-22]The last open defect closed, and building its falsifier found another — the wrong-shaped answer fixed at_hydrate;_safe_fragmentslost its natural trigger and its handler could not survive the failure it handled →persistent-memory.d/2026-09-22-a-wrong-shaped-answer-500s-the-gallery.md[2026-09-22]U6 released asv0.6.0— benches, and the number that was two defects — six of seven v1 units landed, NOT PUSHED →persistent-memory.d/2026-09-22-u6-benches-released.md[2026-09-22]Three cold panels on one unit, and what each lens could only see alone — READ BEFORE DECIDING TO SKIP A GATE; all five passes found something the others structurally could not →persistent-memory.d/2026-09-22-three-cold-panels-on-one-unit.md[2026-09-22]U6 landed — three surfaces, three jobs, one predicate — the seam review caught three real contract defects incl. a per-rowresolve_booththat would have 404'd the board →persistent-memory.d/2026-09-22-u6-benches-landed.md[2026-09-22]The 69% link-board rot was two defects wearing one number — READ BEFORE SCOPING ANY LINK-BOARD WORK; U5 closed the larger half and full-URL-vs-origin identity is a measured call →persistent-memory.d/2026-09-22-one-number-was-two-defects.md[2026-09-22]U3 landed — the page declares the seam, the Booth mounts into it — ten regexes against author HTML replaced by a substring test and a+→persistent-memory.d/2026-09-22-u3-declared-embed-seam-landed.md[2026-09-22]A wrong-shaped answer 500s the gallery and the marks page — PRE-EXISTING (measured at42ea67f), NOT U3; the v0.2.2 lesson is only half-implemented →persistent-memory.d/2026-09-22-a-wrong-shaped-answer-500s-the-gallery.md[2026-09-22]The browser became a test surface — READ BEFORE TOUCHINGplaywrightIN pyproject; the pinned upper bound is the foot-gun, and these tests SKIP rather than fail →persistent-memory.d/2026-09-22-the-browser-became-a-test-surface.md[2026-09-22]A vacuity pass that tries the contract's own mutation agrees with itself — U3 ran one, reported 7/7, and a cold panel then showed one of the seven was vacuous; READ BEFORE WRITING A Falsifiable: LINE →persistent-memory.d/2026-09-22-seven-of-seven-falsifiers.md[2026-09-22]U4 landed — lifetime is derived, not declared — three states, viewing is activity, and no new arithmetic anywhere →persistent-memory.d/2026-09-22-u4-derived-lifetime-landed.md[2026-09-22]The.foreverdiagnosis got a live positive control — 3 of the 4 booths awaiting an answer were ALSO hand-pinned — RE-COUNT 2026-10-06 →persistent-memory.d/2026-09-22-forever-had-a-live-positive-control.md[2026-09-22]No fleetwide notice for U4, and what that does to the prediction — READ BEFORE THE 2026-10-06 RE-COUNT; a flat rate does not falsify the diagnosis →persistent-memory.d/2026-09-22-no-notice-and-what-it-does-to-the-prediction.md[2026-09-22]Four independent paths to one fail-open delete — the bug-hunt panel's class, and the zsh word-splitting trap that shipped an empty bundle →persistent-memory.d/2026-09-22-four-paths-to-one-fail-open-delete.md[2026-09-22]Two reads of one file are not one read of one state — a TOCTOU seam that composes two correct readers into a fail-open delete →persistent-memory.d/2026-09-22-two-reads-are-not-one-state.md[2026-09-22]Five of seven INV falsifiers did not falsify anything — read before writing a Falsifiable: line; a green test cited one rather than being one →persistent-memory.d/2026-09-22-vacuous-falsifiers.md[2026-09-22]The third one-branch template miss — this repo's recurring blind spot; read before adding a fact to any template →persistent-memory.d/2026-09-22-third-one-branch-template-miss.md[2026-09-22]The size cap opened a service-wide hang — a FIFO has st_size 0; a bound that trusts it inherits what it does not mean →persistent-memory.d/2026-09-22-size-cap-opened-a-hang.md[2026-09-22]An existing test stopped me retiring documented behaviour — the clean fix for the mtime race would have silently changed TTL doctrine →persistent-memory.d/2026-09-22-doctrine-not-defect.md[2026-09-22]Two U5 panels, and prose reached a released outage — read the detail before assuming a conformance finding stops at its own module →persistent-memory.d/2026-09-22-u5-panels-reached-a-released-bug.md[2026-09-22]U5's adoption prediction split in two — the handle rides for free, the why must be learned — RE-MEASURE 2026-09-29 →persistent-memory.d/2026-09-22-u5-adoption-split-in-two.md[2026-09-22]The U2 bug-hunt panel was not ceremony — the lock-unlink race and the TTL guard that was failing at its own job →persistent-memory.d/2026-09-22-u2-bug-hunt-panel.md[2026-09-22]The lenient reader's blast radius was the whole service — marks_for runs per booth per index load; a raise there is an outage →persistent-memory.d/2026-09-22-lenient-reader-blast-radius.md[2026-09-22]booth marks/booth answergot real exit codes — read it before changing anything the 17 consuming handles call →persistent-memory.d/2026-09-22-cli-exit-codes.md[2026-09-22]scripts/boothwent from zero tests to five — they run the real script under system python3, so they also check INV-1 →persistent-memory.d/2026-09-22-scripts-booth-got-tests.md[2026-09-21]v0.2.0 was tagged while a gate was in flight — the sequencing lesson: if a gate is outstanding, the tag waits →persistent-memory.d/2026-09-21-v020-tagged-with-a-gate-in-flight.md[2026-09-21]A write over a damaged.marks.jsonwiped the booth — the reads-lenient / writes-strict asymmetry, and why it exists →persistent-memory.d/2026-09-21-marks-write-wiped-judgment.md[2026-09-21]Seam review and cold panel had zero overlap, twice — evidence for running both; neither substitutes for the other →persistent-memory.d/2026-09-21-two-gates-are-complementary.md[2026-09-21]Every code-changing finding came from the AMBIGUITY pass — a finding about the /heid-contract-review skill, not about this repo →persistent-memory.d/2026-09-21-ambiguity-pass-did-the-work.md[2026-09-21]Deterministic order is a cross-cutting v1 invariant — operator directive; read before adding ANY ordered surface →persistent-memory.d/2026-09-21-deterministic-order-invariant.md[2026-09-21]U2 (marks) landed — one primitive for three mechanisms — what moved where, and the HTTP mirror remote sessions poll →persistent-memory.d/2026-09-21-u2-marks-landed.md[2026-09-21]A partially-answered pick counts as OPEN — declared, not smuggled; it is the reading that makes U4 correct →persistent-memory.d/2026-09-21-partial-answer-counts-as-open.md[2026-09-21]The U2 seam review earned its place, and how — inline.place indexes by subscript — the miss a cold panel cannot see →persistent-memory.d/2026-09-21-u2-seam-review-earned-it.md[2026-09-21]Marks are one.marks.jsonper booth — operator decision with two rejected alternatives; read before restructuring →persistent-memory.d/2026-09-21-marks-storage-decision.md[2026-09-21]U7's section premise is half wrong — every booth that needs navigation is FLAT — read before starting U7 →persistent-memory.d/2026-09-21-u7-section-premise-half-wrong.md[2026-09-21]sindra-finalistsis U2's flag motivation, caught live — evidence, not argument →persistent-memory.d/2026-09-21-sindra-finalists-is-the-motivation.md[2026-09-21]The information architecture and the v1 gate landed — the single defect the seven units decompose →persistent-memory.d/2026-09-21-ia-and-v1-gate-landed.md[2026-09-21]The.foreverdiagnosis is a falsifiable prediction — U4's success criterion — re-count a fortnight AFTER U4 lands →persistent-memory.d/2026-09-21-forever-diagnosis-is-a-prediction.md[2026-09-21]Extracted fromeshpfiinto its own repo — test_booth.py is the regression net the v1 rewrite is checked against →persistent-memory.d/2026-09-21-extracted-from-eshpfi.md
Tried and abandoned
[2026-09-21]Tagging a release while a review gate was in flight — cost a same-hour v0.2.1 and a correction to 15 handles →persistent-memory.d/2026-09-21-tagging-with-a-gate-in-flight.md[2026-09-21]Letting the write path share the read path's leniency — a tolerant reader and a tolerant writer are not the same decision →persistent-memory.d/2026-09-21-tolerant-writer-over-tolerant-reader.md[2026-09-21]Letting Jinja hot-reload templates in the deployment root — caused a live outage: 19 of 25 booths at 500. Why auto_reload=False →persistent-memory.d/2026-09-21-jinja-hot-reload-outage.md[2026-09-21]Five mechanisms to get one question beside one artifact — the accretion signature this whole v1 rewrite is undoing →persistent-memory.d/2026-09-21-five-mechanisms-one-job.md[2026-09-21]Regex-injecting chrome into arbitrary author HTML — the defect U3 exists to close →persistent-memory.d/2026-09-21-regex-injecting-chrome.md[2026-09-21]A boolean escape hatch as the lifetime mechanism — why.foreveris a symptom; the defect U4 exists to close →persistent-memory.d/2026-09-21-boolean-escape-hatch-as-lifetime.md[2026-09-21]Letting the link board absorb the announce job — 69% rot; U5 gave the job a home, which is what unblocks U6 →persistent-memory.d/2026-09-21-link-board-absorbing-announce.md