Files
booth/persistent-memory.md
T
vh 4cfbce5109 fix(blur): .blurred round-trips any rel, and one writer serves both surfaces
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.
2026-09-23 22:05:18 -07:00

20 KiB
Raw Blame History

Persistent memory — booth

Last updated: 2026-09-23

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 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-dev ca0641f) and cce6a20 (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_items support he asks for.
  • ✅ .blurred ROUND-TRIPS ANY REL (operator: "fix the blur"). A JSON array via stdlib-only booth/blur.py, the one writer for both the service and booth 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 .blurred files, 42 rels, 0 with edge whitespace. Item.blurred_self came 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 .blurbooth touch still 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 5ded5ff holds). Marker <booth>/.blurbooth, POST /b/<name>/blurbooth, and booth 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_at via statx, landed_at already 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.0 YET (operator, 2026-09-23). The tag stays 1.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__.py IS A FOURTH STDLIB-ONLY MODULE — scripts/booth executes it before every documented one. Covered by test_stdlib_only.
  • ⚠ Read the staged ref, never a SHA written here — design-dev rebases and rewrites it in place. git show-ref | grep svos, then git 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, because v1.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 rendered javascript: 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 FROM importlib.metadata HERE; it reports a different artifact, and booth/__init__.py turns 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 at docs/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 at docs/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_fragments lost 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 as v0.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-row resolve_booth that 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 at 42ea67f), 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 TOUCHING playwright IN 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 .forever diagnosis 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 answer got 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/booth went 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.json wiped 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.json per 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-finalists is 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 .forever diagnosis 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 from eshpfi into 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 .forever is 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