diff --git a/CLAUDE.md b/CLAUDE.md index 5ba62d2..1ba629e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,12 +63,24 @@ No database. `ls ~/booth-data` tells you everything the service knows. Per-booth operator state is a **dotfile inside the booth**: `.forever` (keep), `.viewed` (last deliberate look — U4's "viewing is activity"), `.blurred` (one -rel per line), `.marks.json` + `.marks.lock` (judgment), `.pins` (link-board pin +rel per line — ⚠ see below), `.seen` (R2: rels looked at full size, a JSON +ARRAY), `.marks.json` + `.marks.lock` (judgment), `.pins` (link-board pin ids), `.uploaded` (upload-booth marker). `booth_items()` skips `name.startswith(".")`, so a new dotfile costs nothing in item counts, galleries or zips. That skip is why the dotfile is the right shape for new operator state — use it rather than inventing a sidecar-per-item. +⚠ **`.seen` is a JSON array where `.blurred` is one stripped rel per line, and +the difference is a latent bug in the older one.** A rel may carry a leading +space or a newline; line-stripped storage does not round-trip it, so blurring +`" a.png"` can blur `a.png` instead. `.seen` was written as JSON for exactly +that reason (design-dev, R2), and it also opens `O_NOFOLLOW | O_NONBLOCK` with +an `S_ISREG` check — a planted symlink is refused and a FIFO cannot hang the +read, which is the outage in +`persistent-memory.d/2026-09-22-size-cap-opened-a-hang.md`. **Any new dotfile +inherits that shape, not `.blurred`'s.** `.blurred` itself is unfixed and +pre-existing. + ### 3. One resolver for item facts `booth.items.booth_items(booth)` is the only place a file is classified, a