The repo came out of eshpfi one day ago with neither piece of its house
furniture, so everything non-obvious about it lived in docstrings inside a
998-line app.py — which is a bad place to keep an invariant that breaks
silently on every fleet host when someone violates it.
CLAUDE.md carries the five that do exactly that:
* links.py and asks.py are stdlib-only because scripts/booth imports them
under the system python3 with no venv, so one third-party import breaks
`booth ask` everywhere and fails in an agent's session, not in ours
* the filesystem is the state, and booth_items()'s dotfile skip is why a
per-booth dotfile is the right shape for new operator state
* booth_items() is the only thing that classifies a file or resolves a
caption (U1's INV-1) — the zoom-loses-the-annotation bug was three
readers of one truth, not a rendering bug
* moved names stay importable from booth.app, asserted by a test
* sidecar writes are atomic; render_doc returns raw text on purpose
Plus the distinction that decided this session's storage call: links.md is
an append log because 17 handles write it concurrently, and marks have one
writer. Different problem, different shape — ask which you have first.
persistent-memory.md carries what CLAUDE.md is structurally unable to: the
dated decisions, the `.forever` prediction and its re-measure date, and the
foot-gun log. Two entries are load-bearing for the next unit — the settled
mark storage shape with the reasoning that picked it, and a measured
correction to U7's premise: every booth that actually needs navigation is
flat, so subfolder sections are worth shipping but are not the nav fix.
No version bump — docs and memory, both on the SemVer skip list.
8.5 KiB
8.5 KiB
Persistent memory — booth
Last updated: 2026-09-21
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-21:
- v1 is gated on seven units in
ROADMAP.md, ordered by dependency: U1 → U2 → {U3, U4, U5} → U7, with U6 independent of all of them. - U1 (one item record) has landed at
ce598b3and is verified against its own invariants, not just its commit message: INV-1 holds (noclassify/doc_kind/read_blurred/render_doccall survives in a route body), the zoom and doc templates render the caption they now receive, the re-exports are asserted by a test. 192 tests green,0.1.15. - U2 (marks) is next, and its storage shape is settled (see the 2026-09-21 decision below). Not yet started: no contract written, no blast-radius pass run.
- Next concrete step: graphify + grep blast-radius pass over
booth.asks's surface and everyask/answercall-site (includingscripts/booth), then authordocs/contracts/u2_marks.contract.md. Full House Code Discipline — U2 is a new primitive with a CLI surface and 17 consuming handles, nowhere near surgical. The seam review is the load-bearing gate: U2's whole risk is thatpickclaims to preserve ask semantics and then drifts from them. - Open, operator's call: whether U6 (benches) runs in parallel with U2 or strictly after it. Nothing blocks on the answer; U6 touches different storage and a different surface, so it cannot be broken by U2.
- Live service is
activeon:8090(systemd--user), 25 booths.
Recent decisions
[2026-09-21]Marks are stored as one.marks.jsonper booth, atomic temp-file +os.replace,fcntllock on the read-modify-write — operator decision, this session. Two alternatives were weighed and lost: a sidecar per item (<rel>.marks.json) and extending the existing<stem>.ask.jsonshape. Rationale, and the reason it is notlinks.md-shaped: (a) U4 makes "does this booth owe an answer?" a hot question — the sweep asks it per booth per tick and the index asks it per card per page load, so per-item sidecars turn it into a full walk of all 25 booths, one of which holds 270 files; (b)links.mdis anO_APPENDcontent-hash log because 17 agent handles write it concurrently, whereas marks have exactly one writer (the operator, in one browser) and many readers — a different problem that must not inherit the append-log design; (c).blurred/.pins/.foreveralready establish the per-booth dotfile as the house shape for operator state, andbooth_items()'s dotfile skip means it costs nothing in counts, galleries or zips. Accepted cost: a corrupt.marks.jsonloses that booth's marks rather than one item's. Implementation deferred to U2 — tracked atROADMAP.mdU2 and by this entry.[2026-09-21]U7's section premise is half wrong, and it is the half that matters — found by re-measuring~/booth-datarather than trusting the IA doc. The IA says sections come from subfolders that already exist on disk; true, but every booth that actually needs navigation is flat:pancake-v3-full(270 items, 0 subfolders),pancake-v4-full(270, 0),sindra20-engines(98 items + 99 caption sidecars, 0),sindra-finalists(86 + 87, 0). Subfolders exist on exactly two booths —pewpew-ui-brief(7, nested to_ds/powerpellet-design-system-<uuid>/preview) anddfa-concepts(1) — and both are reports, the job where grid navigation matters least. So sections stay worth shipping andItem.sectionstays right, but they are not "most of the navigation fix": the rail, the filters and grid keyboard are all of it. Worth noting for whoever writes U7:sindra20-enginesencodes its structure in the filename prefix (b2-s1-<subject>-<seed>), which is where a grouping heuristic would actually pay. The IA doc's claim about what sections buy needs a line struck — not yet edited.[2026-09-21]sindra-finalistsis U2'sflagmotivation caught in the act — 86 items, every one captioned, and the booth's entire name is "the ones the operator picked." That loop currently runs through chat, which is the defectflagcloses. Evidence, not argument.[2026-09-21]The information architecture and the v1 gate landed (726822b):docs/design/information-architecture.mdnames the single defect — one lifetime (24h from last touch) and one shape (a folder), serving five jobs with different lifetimes and different shapes — andROADMAP.mdgates v1 on seven units, each closing a measured defect rather than a wish. Both were written after a measurement pass over the live service, and the measurements are the load-bearing part.[2026-09-21]The.foreverdiagnosis is a stated, falsifiable prediction. U4 (derived lifetime) predicts the kept-rate falls to the genuinely-durable booths. Re-measured today: 14 of 25 booths kept (56%), against the 54% the IA doc recorded. Re-count a fortnight after U4 lands. If it does not move, the diagnosis was wrong and the boolean was doing something else. Tracked in the IA doc's Booth section and by this entry.[2026-09-21]Extracted fromeshpfiinto its own repo. The accreted service came over whole, tests included, sotests/test_booth.py(1581 lines) is the regression net the v1 rewrite is checked against.
Tried and abandoned
[2026-09-21]Five separate mechanisms to get one question next to one artifact —.forever, the link board,inline.py's placeholder DSL,wrap_verbatim_html's six regexes, and the floating amber asks chip plus/b/<n>/asks. Every one is a correct local fix to the same global mismatch, which is exactly why they accumulated without anyone making a bad call. The foot-gun is the sixth one: the next "just add a small thing for this case" reads as reasonable and is the pattern. The git log carries the signature — every feature ships, then takes 2–5 patches for cases the single shape did not anticipate. Check the ROADMAP gate before adding a mechanism.[2026-09-21]Regex-injecting chrome into arbitrary author HTML (wrap_verbatim_html+_HEAD_CLOSE_RE,_HTML_OPEN_RE,_DOCTYPE_RE,_BODY_CLOSE_RE,_HTML_CLOSE_RE,_ICON_RE, and the doctype/charset ordering constraints they thread). It works today and is still live — but it is the single most fragile thing in the service and it is load-bearing for the operator's most important workflow. Slated for deletion at U3 in favour of a declared seam (/_booth/embed.js, mounted through a real DOM API), which costs an author one line and removes the whole class. Do not extend the regex set in the meantime; if a verbatim page breaks, that is an argument for U3, not for a seventh pattern.[2026-09-21]A boolean escape hatch as the lifetime mechanism..foreverwas added because a 24h TTL genuinely did not fit some booths — and then 56% of live booths ended up on it, which means it is not "ephemeral with an exception", it is two lifetimes wearing one lifetime's clothes, with the operator doing the sorting by hand. Replaced at U4 by lifetime derived from state (an open mark pins; viewing is activity;keepsurvives as an explicit reasoned pin rather than the only way to say "not yet").[2026-09-21]Letting the link board absorb the announce job.booth linkis anO_APPENDwrite with no identity and no stated rule, so re-announcing a bench appends a row instead of updating one, and a booth URL rots the moment its booth is swept — 145 of 211 rows (69%) pointed at nothing, and 22 were the same target re-posted (talk 5×, peedlar 4×). The rot is structural, not drift. The lesson that cost the most: enforcing the link rule without first giving the announce job a home (.booth.jsonprovenance on the index, U5) just makes it homeless.