Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit eeb781d94d914f5ad90db64c9cad21241018c5c9
parent 0828507f6da65f21d831c1bf07e78977d76d72ea
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 20 Sep 2026 18:30:42 -0400

plans: S5 and S6, what shipped and what was deliberately left

FACTS gains the seams — totalMediaBytes and why an absent one is not zero, the
synthetic internal row and why it must never be stored, the one-verb Storage
panel, the /channels columns and params, the rsync progress shape and why the
fraction divides by the measured tree, relocateDir and the store's record field,
and the watch's three refusals (write only on a transition, never mid-relocation,
never the thing that flips a default priority document).

storage-locations.md records how S5 differs from its own design: a watch rather
than a per-unit re-check (the operator asked for a FLAG, and a per-unit refusal
produces no state), `reason: "storage"` on the tier rather than a /review
section — and that `assertRelocationRootPresent` is the one piece of S5 not done.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mplans/FACTS.md | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplans/STATE.md | 18+++++++++++++++++-
Mplans/storage-locations.md | 49++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 230 insertions(+), 2 deletions(-)

diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -4148,3 +4148,168 @@ never pulls `reader-fs.ts` into a client chunk. has no `runner`, so the operator presses Run. The obvious next step is for the download lane to run it for a channel whose filter has unscanned listed videos — which is exactly the backlog the snapshot already carries. + +## Storage locations S5–S6 (verified 2026-09-20, branch `storage/locations-s5-s6`) + +Six things, in the order they were built. Trust these over re-deriving them. + +### Bytes: `snapshot.totalMediaBytes` + +- **EVERY byte under `data/<id>/`**, not the audio. `totalAudioBytes` is a + different question (what a CLEANUP could reclaim); this is what a volume holds + and what a move carries, and on a real channel the two differ by the whole + transcript/cues/metadata mass. +- Gathered in the EXISTING per-video walk in `channelSnapshot.ts` (~746): the + `audioSizes` loop became one loop over `files.entries`, statting each, feeding + both numbers. No second walk; the added cost is a stat per non-audio entry, + warm inode cache, on a pass that already reads several sidecars per video. + Sub-directories count as nothing rather than recursing — a video dir is flat. +- **OPTIONAL, and the distinction is load-bearing.** A snapshot written before + the field lacks it, and every reader must render that as "size unknown until + Refresh report", NEVER as 0: a zero ranks a 400 GB channel bottom of the very + list the operator opened to find it. `LocationRollup` therefore carries + `bytes` AND `unknownBytes`, and `storageBytesText(bytes, unknown)` in + `views/storage.ts` is the one wording. + +### The corpus volume as a row: `INTERNAL_LOCATION_ID = "internal"` + +- **SYNTHETIC, never stored in `settings.storage.locations`.** There is nothing + to configure (the root is wherever the corpus is), nothing to re-point, and — + the load-bearing reason — a stored entry would make `locationOfDataDir` match + every unrelocated channel, breaking the rule the whole design rests on: a + channel is on location L iff its `dataDir` is under `L.root`, and an in-place + channel HAS no `dataDir`. +- Assembled in `views/storage.ts` (`internalRow`) from `StorageRowsInputs.internal + = { root, freeBytes? }`, which the shell fills. It sorts FIRST, is + `available` by construction, and every one of its five actions is present with + `offered: false` and the same sentence — a greyed button with no reason is + what that page exists not to be. +- `channelsOnLocation({ includeInternal: true })` rolls the no-`dataDir` channels + under that id. A `dataDir` under a root NOBODY named is counted in NEITHER; the + nudge is to name that root on /storage. +- `StorageRowsPayload` exposes `bytesOnLocation` (by row id, internal included) + and `bytesInPlace`. + +### The Storage panel has ONE verb + +- `MoveOut`/`MoveBack` folded into `MoveMedia` (`StorageStage.tsx`). The corpus + volume is a destination (`__internal`) and picking it calls + `moveChannelMediaBackAction` — unchanged, by that name, so the `/api/ops/*` + routes on `feat/ops-api` keep working. +- While the media IS on a location, `__internal` is the ONLY option offered: the + controller refuses a location-to-location move by name ("already relocated to + X. Move it back in place first."), and a select of options that all refuse is + worse than one that works plus a sentence. No preview gate on the way back. +- One log, one accessible name: `getByLabel("Move media output")` for both + directions. The button LABEL still reads "Move back in place" when the + destination is internal. + +### `/channels` is the storage working surface + +- Two columns: **Location** (`aria-label="media location for <slug>"`, the + volume label with the existing reachability badge beside it) and **Size** + (`aria-label="media size for <slug>"`, sort key `size`, first click biggest + first, "—" and never "0 B" when unmeasured). Sort keys `location` and `size` + joined `SortKey`; `colSpan` went 10 → 12 + columns. +- **Free space is NOT a column.** It is a fact about a disk; 71 copies of one + number is what the focus bar already taught us not to do. It lives in + `ChannelVolumeBar` (`aria-label="storage volumes"`), one chip per volume + (`aria-label="volume <id>"`), and the chip IS the filter. +- **`?location=<id|internal>`**, a URL param beside `?site=`, validated on the + server against what is on screen (a stale /storage link must show the page, + not an empty table). A volume filter FLATTENS the grouped render: a group is a + partition of a site, and half a group is not a group. +- **`?sort=size`** seeds the sort ONCE. Sorting is otherwise client state, + deliberately — a `router.replace` races the global AutoRefresh's + `router.refresh()` and gets dropped. +- **"Free up N GB"** lives in the volume bar, not the deck: the deck only exists + once something is ticked, and this is the control that does the ticking. The + deck shows the resulting note (`aria-label="free up selection note"`). The + rule is pure in `common/views/freeUpSelection.ts`: only in-place channels + (moving one already on the platter frees nothing on the disk being emptied), + unmeasured ones excluded AND COUNTED, largest first, last pick overshoots. +- **Per-volume free space is `volumeFreeBytes` (controller), two syscalls per + root and never a probe.** The `stat` is what stops `getFreeBytes` reporting an + unmounted root's PARENT volume — which on this machine is the disk being + emptied. Tables do not shell out. + +### Relocate progress + +- `rsync --info=progress2` was ALREADY on the copy; every frame went to the job + log as a carriage-return redraw. `parseRsyncProgress` (jobs/progressParsers.ts) + reads one frame — `30,000,000 85% 1.03GB/s 0:00:03 (xfr#1, to-chk=1/3)` — + into `{bytes, percent, rate, etaSeconds}`. Grouping separators are stripped + with `\D`, not assumed to be commas. +- **The fraction divides by the MEASURED tree, not by rsync's percentage.** Under + incremental recursion (the default) rsync's percentage is of what it has + enumerated so far and walks BACKWARDS. The controller already measured the + whole tree for its space check. +- `relocateChannelMedia`/`relocateSavedVideos` take `onProgress`, keep frames out + of the log, and write one decile line: `Copying… 12.3 GB of 45.6 GB · 27 % · + 110.50MB/s · ETA 5:32`. Same wording in the job's task detail. +- The job publishes it as a **`JobTask` of kind `"relocate"`** (new member of + `JobTaskKind`; the two `Record<JobTaskKind, …>` tables in + `JobProgressBars.tsx` and `MonitorWidget.tsx` are the compile errors that + catch a third). NOT a `JobProgress` metric — that counts artifacts + re-countable from disk, and a copy in flight is neither. The task is added on + the FIRST FRAME, not at start: the preflight and a resumed run's verify pass + transfer nothing. + +### `relocateDir.ts` and the movable saved-video store + +- `common/controller/relocateDir.ts` is `relocateChannelMedia.ts`'s core lifted + out UNCHANGED: `measureTree`, `linkOrDirState`, the marker read/write/clear, + `rsyncTree` (takes `rsyncBin`, not `Paths`), `COPY_ARGS`, `verifyCopy` (dry run + + re-measure, one `.d..t` retry) and `makeProgressSink`. What stayed behind is + everything channel-specific. The channel mover's 24 tests pass against it. +- `relocateSavedVideos({ paths, locationId, io? })`: `""` moves back. + `<store>` becomes a symlink to `<root>/saved-videos`; + `savedVideoRoot()` (lib/savedVideo.ts) is untouched and every reader follows + the link. Marker: `transcripts/.relocating-saved-videos.json`, the SAME + `{target, direction, startedAt, phase}` shape a channel's uses. +- **`settings.storage.savedVideosLocationId`** is a RECORD of where bytes are, + not a preference — so unlike `defaultLocationId` it is NOT fallen back to + another location when the named one is deleted; it sanitizes to `""`. +- `ChannelConfig.savedVideosDir` (the per-channel override) is neither read nor + rewritten: it is an absolute path the operator set. +- Job kind `relocate-saved-videos`, in `NO_REGEN_KINDS`, on + `relocationQueueKey()` with the channel move and the re-point. A store move or + a re-point freezes every /storage row (`runningRepoint` checks both kinds); a + CHANNEL move deliberately does not. +- `/storage` grows `SavedVideosStoreCard` — size, location, Move / Resume / + Clear marker. The store is WALKED (`measureTree`) because it holds one + container per pinned or kept-latest video, not one per video; if it ever grows + to corpus scale, `listSavedVideos` (pointers carry their own `bytes`) is the + cheaper answer waiting. + +### The drive watch (S5) + +- `common/controller/storageWatch.ts`. Every existing guard runs at the START of + a piece of work; none is a DETECTOR, so a channel on a vanished drive sits + being refused with nothing saying why. `runStorageWatchPass` probes every + location (`refresh: true` — the 10 s memo is for page renders), auto-pauses the + channels it cannot reach and restores them when it can. + `startStorageWatch()` arms it at `STORAGE_WATCH_INTERVAL_MS` (5 min). +- **`ChannelPriorityEntry.autoPaused = {reason: "storage", since, previousTier}`.** + `autoPauseForMedia` no-ops on an already-auto-paused channel (a flapping drive + must not overwrite `previousTier` with `paused`) and on one the OPERATOR + paused. `restoreAfterMedia` no-ops without a record. `clearAutoPause` is called + by the one priority writer on every manual tier change — the operator's word + always wins. +- **The sanitizer now KEEPS a rank on an auto-paused channel.** It still drops + one from a channel paused everywhere, which is right for a pause the operator + meant and catastrophic for a temporary one: an unplugged cable would otherwise + destroy the queue order and restore the channel unranked. +- **THE PASS PAYS THE LEGACY SEED.** `laneDispatchRoot` is all-or-nothing on + `isDefaultChannelPriority`, so auto-pausing one channel on a corpus with an + empty document would switch it off its hand-made lane trees. Same condition and + same remedy as `saveChannelPriorityAction` (S2/S3 review, finding 3), and the + lanes are recompiled in the SAME `writeSettings`. +- Writes at most once per pass, and only on a transition — a quiet pass bumps no + pulse revision. `in-transition` (a relocation marker) is never a reason to + pause: the relocate job is what an auto-pause would be refusing. +- Armed in `instrumentation.ts` BELOW the idle gate, unlike the boot probe above + it: the probe is read-only, the WRITE is work, and `ARCHILYZER_IDLE_BOOT` + refuses work. +- The flag: `autoPauseReasonOf` is the one sentence, rendered as a "storage" chip + by `ChannelTierSelect` (`aria-label="auto-paused reason for <slug>"`). diff --git a/plans/STATE.md b/plans/STATE.md @@ -3,7 +3,23 @@ The working memory for the local-AI derived-corpus work. Rewritten at the end of every session, before context is cleared. See [`README.md`](README.md) for the protocol. -**Last updated:** 2026-09-15 (Phase 3 slice 1 and the rack landed; see the Next block) — 2026-09-13: **gate A passed and `main` moved; the interlude shipments and one-core Phase 2 are all merged +**Last updated:** 2026-09-20 — **storage locations S5 + S6 shipped** on branch +`storage/locations-s5-s6` (worktree `/home/user/Projects/storage-locations-s5-s6`, off `main` +@ `60183f0`, unmerged). The operator's two asks that evening — *"disk space and current storage +volume as columns on the channels menu, and let me filter by volume"* and *"progress on +relocate jobs since rsync gives progress"* — plus the four things around them: per-channel +media bytes in the snapshot (`totalMediaBytes`), the corpus volume as a first-class `/storage` +row (`internal`, synthetic), the saved-video store made movable (`relocateDir.ts` is the +channel mover's factored core; `relocateSavedVideos` is new), and a five-minute drive watch +that auto-pauses a channel whose drive went away and restores its tier when it comes back +(`ChannelPriorityEntry.autoPaused`). Slice record and what was deliberately left out: +[`storage-locations.md`](storage-locations.md#s6--the-storage-surfaces-the-operator-asked-for-shipped-2026-09-20). +Seams: [`FACTS.md`](FACTS.md#storage-locations-s5s6-verified-2026-09-20-branch-storagelocations-s5-s6). +**Left out, named:** `assertRelocationRootPresent` (a move can still `mkdir` under an absent +mount), a `/review` section for auto-paused channels, and the per-unit reachability re-check in +the two lane runners. + +**Previously:** 2026-09-15 (Phase 3 slice 1 and the rack landed; see the Next block) — 2026-09-13: **gate A passed and `main` moved; the interlude shipments and one-core Phase 2 are all merged on one branch**, `integrate/2026-09-storage-priority`, tip **`bd3d4ec`**, unmerged. Off `e74f005`: `relocate-channel-media` (from `storage/relocate-media`), then `channel-priority` (from `channel-priority/s5`), then Phase 2's five slices in the order their reviews cleared — diff --git a/plans/storage-locations.md b/plans/storage-locations.md @@ -1,6 +1,7 @@ # Storage locations: named, refreshable, re-pointable places a channel's media lives -Status: **S0–S4 shipped 2026-09-19** (`4b55e3c`); S5 written, not started. Facts pinned to `main` @ `6b4f25b`. Slices ship on +Status: **S0–S4 shipped 2026-09-19** (`4b55e3c`); **S5 + S6 shipped 2026-09-20** on +`storage/locations-s5-s6` (unmerged) — see the two status blocks at the foot of this file. Facts pinned to `main` @ `6b4f25b`. Slices ship on `storage/locations-s{0,1,2,3,4}` branches; this file is updated as each lands. ## Context @@ -317,5 +318,51 @@ The S3/S4 gate list. Full suite on the final tip. Offline against the real host: ### Status +- 2026-09-20 — **S5 and S6 shipped** on `storage/locations-s5-s6` (unmerged). What landed + differs from the design above in three ways, all deliberate: + - **(a) is a watch, not a per-unit re-check.** The plan put an `inspectChannelMedia` beside + the marker check in `autoRunner.run(picked)` and `operationBatch`'s GUARD 5. The operator's + ask was "an automatic disable and flag", and a flag is a STATE — a per-unit refusal + produces none, it just declines work more often. `common/controller/storageWatch.ts` is a + five-minute pass that probes, auto-pauses and restores; the existing start-of-work guards + are untouched, and the per-unit re-check is still available as a follow-up if a lane is ever + seen writing into a dangling link. + - **(c) is `reason: "storage"`, not `"media-unreachable"`**, and it lives beside the tier as + a chip on `/channels` rather than as a `/review` section. `/review` is a follow-up. + - **(b), `assertRelocationRootPresent`, is NOT done.** The absolute-path `mkdir` in moveOut + and moveBack can still materialise a mount when the volume is absent. It is the one piece + of S5 left and it is self-contained. - 2026-09-18 — written; dispatch after S4 merges (`storage/locations-s5`). - 2026-09-19 — S4 (`storage/locations-s4`, `c4384bd`→`f007263`) reviewed and merged as `4b55e3c`: badge names the location, destination by name with Resume move, bulk by name, docs + changelog. Full suite on the branch tip 527/538 under load, all 11 rerun green after two spec fixes (`a939e0e`, `f007263`). **S0–S4 complete.** Next: S5 (unreachable media) — see below. + + +## S6 — the storage surfaces the operator asked for (shipped 2026-09-20) + +Two asks on the evening of 2026-09-20, verbatim: *"I'd like to see disk space and current +storage volume as columns on the channels menu, and let me filter by volume for easy checking +of things that may need moving or other processing"* and *"It'd be nice to see a progress on +relocate jobs since rsync gives progress"* — against an internal disk 96 % full (67 GB free, +523 GB of channel `data/` on it) with a 2 TB platter registered as `platter`. + +Shipped on `storage/locations-s5-s6`, in this order: + +1. **Bytes.** `snapshot.totalMediaBytes` in the existing per-video walk; + `channelsOnLocation` sums it with an `unknownBytes` count beside it; + `views/storage.ts` exposes `bytesOnLocation` / `bytesInPlace` / per-row `freeBytes`. +2. **The corpus volume is a first-class row**, synthetic, id `internal`, first, with every + action withheld and a link to its own channel list. The channel Storage panel's + destination select offers it and routes to `moveChannelMediaBackAction`; "Move back in + place" is no longer a second section. +3. **`/channels` is the working surface**: Location and Size columns, `?location=` filter, + `?sort=size`, one free-space read-out per volume in a volume bar, and "free up N GB". +4. **Relocate progress**: `parseRsyncProgress` + a `relocate` JobTask + one log line per + decile. +5. **The saved-video store is movable**: `relocateDir.ts` (the factored core), + `relocateSavedVideos`, `settings.storage.savedVideosLocationId`, a `/storage` card. + +Every seam is pinned in +[`FACTS.md`](FACTS.md#storage-locations-s5s6-verified-2026-09-20-branch-storagelocations-s5-s6). + +**Not done, and named rather than forgotten:** `assertRelocationRootPresent` (S5(b)) — the +move can still `mkdir` a target under an absent mount; a `/review` section for auto-paused +channels; and the per-unit reachability re-check in the two lane runners.