commit e65a37bbe606ea0ba994d168ea77c3b259a5946e
parent 7ae37867a19834c79055cd8c6c6dac8e6885673c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 09:26:05 -0400
plans: slice DT after review — the parent's rulings (the half-interval floor stays; a lowered cap is reached as calls return; doctor's line is slice SG's); the review's findings to their commits; the cap at most 8 in the record and FACTS; the re-gates
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 57 insertions(+), 25 deletions(-)
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -7682,7 +7682,8 @@ shipped". Anchors are at slice DT's tip (`r15/drive-timings`).
detect it reliably: the root's inode is in the kernel's cache whenever the drive was used lately.
- **The timings are settings: `settings.storage.health`** (slice DT). `budgetMs` (default 3000,
500–60000), `passIntervalMs` (15000, 5000–300000), `probeTimeoutMs` (3000, 500–30000),
- `clearAfterCleanPasses` (2, 1–10), `inFlightPerLocation` (4, 1–16); the defaults, ranges, sanitizer
+ `clearAfterCleanPasses` (2, 1–10), `inFlightPerLocation` (4, 1–8: at most half the editor's 16
+ file-access threads, review M1); the defaults, ranges, sanitizer
and words are `common/lib/storageHealthTimings.ts` (pure; the /storage form imports it). A read
clamps into the range and keeps only a value that differs from its default (an untuned file has no
`health` key); the /storage form refuses out of range with a sentence. Every number is read through
@@ -7690,7 +7691,7 @@ shipped". Anchors are at slice DT's tip (`r15/drive-timings`).
`globalThis` — no file read per call. `applyHealthTimings(stored)` (`:193`) sets them: the health
pass on every pass (from the settings it reads), /storage's save at once
(`saveHealthTimingsAction`), and the `index` and `build stats` bins once at start. Any other process
- with no pass (a CLI, `archilyzer doctor`) runs on the defaults. A changed interval is told to
+ with no pass (a CLI, `archilyzer doctor` until slice SG adds its line) runs on the defaults. A changed interval is told to
`onPassIntervalChange` (`:214`) subscribers, which re-arms the armed pass's timer; a raised cap
admits waiting calls, a lowered one is reached as calls return. `resetStorageHealth` keeps the
timings (configuration, not health); `setDriveCallBudget` is a test seam below the 500 ms floor and
@@ -7706,7 +7707,7 @@ shipped". Anchors are at slice DT's tip (`r15/drive-timings`).
`lib/storageVolumes.ts:607`). The root's device from the last pass (findmnt `-J -T <root> -o
SOURCE,UUID`, raced against `probeTimeoutMs` (3 s), only when there is none or its `/sys` entry stops reading;
another volume's UUID names none; `[…]` stripped, `/dev/mapper` resolved, basename); then
- `/sys/class/block/<dev>/stat` (`parseBlockStat`, `storageHealth.ts:826`): completed = fields 1 + 5
+ `/sys/class/block/<dev>/stat` (`parseBlockStat`, `storageHealth.ts:831`): completed = fields 1 + 5
+ 12 + 16 (reads, writes, discards, flushes), in flight = field 9. Stalled ⇔ in flight at both
samples AND nothing completed between; samples at least `minCounterIntervalMs()` apart
(`storageVolumes.ts:472`: min(10 s, interval − 5 s), floored at half the interval — 10 s at the
diff --git a/plans/release-15.md b/plans/release-15.md
@@ -1112,7 +1112,7 @@ location (`DRIVE_CALLS_IN_FLIGHT`, 4). The constants are gone; tsc named every r
| `passIntervalMs` | 15000 | 5000–300000 | at once on a save from `/storage` (the armed pass re-arms its timer); a hand edit, at the next pass |
| `probeTimeoutMs` | 3000 | 500–30000 | the next pass or Refresh (child `stat` and findmnt) |
| `clearAfterCleanPasses` | 2 | 1–10 | the next answer |
- | `inFlightPerLocation` | 4 | 1–16 | the next slot taken; a raised cap admits waiting calls at once, a lowered one is reached as calls return |
+ | `inFlightPerLocation` | 4 | 1–8 (review M1: at most half the editor's 16 file-access threads) | the next slot taken; a raised cap admits waiting calls at once, a lowered one is reached as calls return |
- The type, defaults, ranges, sanitizer, words and `SETTINGS.md` docs are one pure module,
`common/lib/storageHealthTimings.ts` (the `/storage` form, a client file, imports it).
@@ -1177,18 +1177,23 @@ location (`DRIVE_CALLS_IN_FLIGHT`, 4). The constants are gone; tsc named every r
| `41ad2f65` | `editor:` the Drive health timing form, its action and parse (+ unit test), the page; the Refresh note, the stalled line, the media notice, the volume chip's title; the comments. |
| `b92df1fe` | `editor(e2e):` `storage-locations.spec.ts`: the drive health timing case. |
| `4372abaf` | `common:` the cap's hint says to keep it well under the editor's 16 file-access threads. |
-| this commit | `plans:` this section and the slices row; FACTS; the changelog. |
+| `fd2b8d63` | `plans:` the first version of this section and the slices row; FACTS; the changelog. |
+| `85c2407b` | `common:` review M1: `inFlightPerLocation` at most 8; the hint, the docs string, `SETTINGS.md`, the test values (the form's too). |
+| `6a24ac4e` | `common:` review L4: a timeout on no known location names the budget the call ran against. Test. |
+| `e7a525f3` | `editor:` review L2: a storage patch of the locations keeps `storage.health` (a `saveSettings` merge case). |
+| this commit | `plans:` the rulings and the review in this section; FACTS; the report. |
**Tests** (unit; no test stalls a real drive)
| File | What it pins |
|---|---|
-| `lib/storageHealthTimings.test.ts` (7, new) | The defaults are the constants they replace, each in its range. Absent, empty, an array, a string, a number: every default, and no block (`getSettings()` with no file, `defaultSiteSettings()`). A read clamps (200 → 500, 900000 → 300000), rounds (7.6 → 8), drops a string and an unknown key, and drops a value equal to its default (also one that rounds onto it). **The settings.json round trip** through `writeSettings`/`getSettings`: `{budgetMs: 4000, clearAfterCleanPasses: 2}` is written as `{budgetMs: 4000}` and read back; a save of the default removes the key; a hand-edited 99 reads as 16. The block survives the mediaRoot migration. The spacing (15 s → 10 s, 300 s → 10 s, 12 s → 7 s, 10 s → 5 s, 8 s → 4 s, 5 s → 2.5 s). The words. |
-| `lib/storageHealth.test.ts` (+6) | **The accessor feeds the watchdog:** a stored `budgetMs: 200` applies as 500 ms (the floor), and a 700 ms unit is refused and marks the location ("within 0.5 s"); on the defaults the same unit answers. **The cap:** with `inFlightPerLocation: 2` the third call waits, and runs when a slot frees. A cap raised from 1 to 3 admits the two waiting calls at once; lowered to 1 with three in flight, two returns bring it to one and the fourth call still waits, and runs on the third return. **The clear count:** with 3, two clean answers do not clear and the third does; with 1, one does. A changed interval is told to the subscribers once, an unchanged one and other keys are not, and an unsubscribed one hears nothing. The test seam's budget wins, and a reset keeps the timings. The existing constants' assertions read `HEALTH_TIMING_DEFAULTS`. |
+| `lib/storageHealthTimings.test.ts` (7, new) | The defaults are the constants they replace, each in its range. Absent, empty, an array, a string, a number: every default, and no block (`getSettings()` with no file, `defaultSiteSettings()`). A read clamps (200 → 500, 900000 → 300000), rounds (7.6 → 8), drops a string and an unknown key, and drops a value equal to its default (also one that rounds onto it). **The settings.json round trip** through `writeSettings`/`getSettings`: `{budgetMs: 4000, clearAfterCleanPasses: 2}` is written as `{budgetMs: 4000}` and read back; a save of the default removes the key; a hand-edited 99 reads as 8 (16 before review M1). The block survives the mediaRoot migration. The spacing (15 s → 10 s, 300 s → 10 s, 12 s → 7 s, 10 s → 5 s, 8 s → 4 s, 5 s → 2.5 s). The words. |
+| `lib/storageHealth.test.ts` (+7) | **The accessor feeds the watchdog:** a stored `budgetMs: 200` applies as 500 ms (the floor), and a 700 ms unit is refused and marks the location ("within 0.5 s"); on the defaults the same unit answers. **The cap:** with `inFlightPerLocation: 2` the third call waits, and runs when a slot frees. A cap raised from 1 to 3 admits the two waiting calls at once; lowered to 1 with three in flight, two returns bring it to one and the fourth call still waits, and runs on the third return. **The clear count:** with 3, two clean answers do not clear and the third does; with 1, one does. A changed interval is told to the subscribers once, an unchanged one and other keys are not, and an unsubscribed one hears nothing. The test seam's budget wins, and a reset keeps the timings. The existing constants' assertions read `HEALTH_TIMING_DEFAULTS`. After review L4: a call on a hand-typed root whose budget changes while it is out (80 ms, then 5 s) is refused naming 0.08 s (the pre-fix code named 5 s). |
| `lib/storageHealthCounters.test.ts` (+1) | The spacing follows the applied interval: at 8 s, samples 3,999 ms apart give no verdict and 4,000 ms apart compare (stalled); at 300 s it is 10 s. The existing case reads `minCounterIntervalMs()` (10 s). |
| `lib/storageHealthProbe.test.ts` (+1) | With `probeTimeoutMs: 500` applied and no `timeoutMs` passed, a child that sleeps 20 s is `stalled` after 0.5–2.5 s. |
| `controller/storageWatch.test.ts` (+2) | **Every pass applies what it reads:** `clearAfterCleanPasses: 3` and `budgetMs: 5000` in the settings are in force after the first pass; its stall line says "until it answers 3 times in a row"; two clean passes do not clear and the third does; a pass handed its locations reads no settings and leaves the timings. **The re-arm:** the armed watch, told `passIntervalMs: 5000` (the save's apply), logs the re-arm and runs its second pass 4.5–7 s later (15 s at the default); a stopped watch re-arms nothing; one armed with an explicit interval does not follow. |
-| `editor/app/storage/lib/healthTimingsForm.test.ts` (5, new) | One field per key, in order, with accessible names none of which contains another. Empty and blank fields write nothing. A value in range is kept and trimmed; one equal to its default is not written. Out of range is refused with the field, range and value, for a millisecond field and both counts. "3.5", "-1", "3e3", "abc", "0x10" and "two" are refused as not whole numbers. |
+| `editor/app/storage/lib/healthTimingsForm.test.ts` (5, new) | One field per key, in order, with accessible names none of which contains another. Empty and blank fields write nothing. A value in range is kept and trimmed; one equal to its default is not written. Out of range is refused with the field, range and value, for a millisecond field and both counts (the cap: 8 kept, 9 refused, after review M1). "3.5", "-1", "3e3", "abc", "0x10" and "two" are refused as not whole numbers. |
+| `editor/app/settings/saveSettings.test.ts` (+1, review L2) | A storage patch of `{ locations, defaultLocationId }` (what the /storage location actions write) keeps `storage.health` and `savedVideosLocationId`. |
**e2e** (`storage-locations.spec.ts`, new case "the drive health timing saves to settings.json and
reads back"): after hydration, the block is collapsed and says "defaults"; opened, `read budget` is
@@ -1201,19 +1206,20 @@ refused with the sentence and the file is unchanged; emptied and saved, "A read
#### Gates (logs `$T/dt-*.log`)
- **tsc** (all workspaces): clean before every commit — 58 s on the tree of the first three code
- commits, 51 s after the hint's.
+ commits, 51 s after the hint's; after the review, 44 s (M1) and 58 s (L4, L2).
- **Unit:**
| Suite | Result |
|---|---|
- | common | **2,318/2,318**, 50 s (`main`'s 2,301 + 17) |
- | editor unit | **100/100** (95 + 5) |
+ | common | **2,318/2,318**, 50 s (`main`'s 2,301 + 17); after the review **2,319/2,319**, 50 s (+1, L4) |
+ | editor unit | **100/100** (95 + 5); after the review **101/101** (+1, L2) |
| `test:scripts` | 194 passed, 2 skipped (196), as at DS |
| mcp | not run: no mcp file and nothing it imports changed |
-- **Docs:** `settings example --check`, `docs env --check` and `docs files --check` all exit **0**
- (`SETTINGS.md` regenerated in `13411edc`: the `health` row and the `storage.health` table;
- `settings.json.example` unchanged, the default block has no `health`).
+- **Docs:** `settings example --check`, `docs env --check` and `docs files --check` all exit **0**,
+ before and after the review (`SETTINGS.md` regenerated in `13411edc`: the `health` row and the
+ `storage.health` table; and in `85c2407b` for the cap's range; `settings.json.example` unchanged,
+ the default block has no `health`).
- **Build:** the editor's `next build`, with the primary's `transcripts/` linked in (`ln -sT`) and
capped at 5 GB with no swap: **34 s, max RSS 1,648,128 KB**, exit 0. The link was removed after the
build, and nothing ran through it.
@@ -1226,20 +1232,20 @@ refused with the sentence and the file is unchanged; emptied and saved, "A read
| 2 | `4372abaf` (the code as shipped) | **39 passed, 0 failed, 12 skipped, 2.5 min** |
DS's 38 plus the new case. The 12 skips are `channels-rack-audit`, which needs `E2E_RACK_SHOTS`.
- Neither run waited in the queue.
+ Neither run waited in the queue. Not re-run after the review, as the parent directed: the fixes
+ change a range, a message and a unit test, and the review's own run of `storage-locations` at
+ `fd2b8d63` passed 9/9.
- **Numbers tool:** none.
#### Found and left
- **Other CLI processes run on the defaults.** The `index` and `build stats` bins apply the settings;
- `archilyzer doctor` (`common/bin/doctor.ts`, another slice's file) and any other command whose
- inspects go through `onDrive` race them against the default 3 s. A one-line
- `applyHealthTimings(settingsFromFile(paths.settingsFile).storage.health)` at its start is all it
- needs.
-- **`inFlightPerLocation` may be set to 16, the editor's whole thread pool** (`UV_THREADPOOL_SIZE`,
- 16 in `start`). At 16, one drive that stops answering can hold every file-access thread, which is
- what DS exists to prevent; two drives at 8 can too. The range is the ruling's; the form's hint and
- `SETTINGS.md` say to keep it well under 16.
+ `archilyzer doctor` and any other command whose inspects go through `onDrive` race them against the
+ default 3 s. **Doctor's line belongs to slice SG**, which owns `common/bin/doctor.ts` (ruled below):
+ `applyHealthTimings(settingsFromFile(paths.settingsFile).storage.health)` at its start.
+- **Two drives at the cap's maximum can still hold every thread.** Review M1 lowered the maximum to
+ 8, half of `UV_THREADPOOL_SIZE` (16), so one drive that stops answering cannot hold them all; two
+ such drives at 8 each can, as two at 4 hold half. The cap is per location, not per process.
- **A hand edit of `passIntervalMs`** re-arms at the next pass, so it can wait up to the old interval
(at most 5 minutes). A save from `/storage` re-arms at once.
- **A refused save clears the typed value.** React resets a form after its action returns, so the
@@ -1251,13 +1257,14 @@ refused with the sentence and the file is unchanged; emptied and saved, "A read
| What I assumed | The alternative |
|---|---|
-| The counters' spacing is the ruling's min(10 s, interval − 5 s), **floored at half the interval**. Without the floor it is 0 at the 5 s minimum interval (1 s at 6 s), so a Refresh just after a pass would compare two samples milliseconds apart, the case DS's review kept the spacing for. From 10 s up the two agree. | The formula as ruled, 0 at 5 s. Or a higher minimum interval (10 s) |
+| The counters' spacing is the ruling's min(10 s, interval − 5 s), **floored at half the interval**. Without the floor it is 0 at the 5 s minimum interval (1 s at 6 s), so a Refresh just after a pass would compare two samples milliseconds apart, the case DS's review kept the spacing for. From 10 s up the two agree. **Ruled at review: the floor stays.** | The formula as ruled, 0 at 5 s. Or a higher minimum interval (10 s) |
| A read clamps an out-of-range value (the schema's convention); the `/storage` form refuses it with a sentence and writes nothing. | The form clamps too, and says what it stored |
| Only a value that differs from its default is written, so a save of 3000 for the budget writes nothing, and a later release's new default reaches it. | Write what the operator saved, pinning the default of the day |
| The slice's test as specified (`budgetMs: 200` → a 300 ms unit refused) is below the ruled 500 ms floor, so the test stores 200, shows it clamped to 500, and refuses a 700 ms unit; the default passes the same unit. | Lower the floor so 200 applies |
| The timings are applied into the health state (the pass on every pass, the save at once, the two bins), not read from `settings.json` on each call: lib has no settings memo, and `onDrive` is on every page's hot path. | A time-limited memo of `getSettings()` inside the accessor (a file read at most every few seconds, and lib/storageHealth.ts no longer free of I/O) |
| A save re-arms the pass's timer at once, through a subscription on `globalThis`. | Leave the running timer; the new interval at the next restart |
-| A lowered cap is reached as calls return; calls already in flight are not refused. A raised cap admits waiting calls at once. | Refuse the calls over the new cap |
+| A lowered cap is reached as calls return; calls already in flight are not refused. A raised cap admits waiting calls at once. **Ruled at review: it stays.** | Refuse the calls over the new cap |
+| **After review M1:** the cap's maximum is 8, half the editor's 16 file-access threads (the ruling said 16). | 16, with a warning on the form and in `SETTINGS.md` (as first shipped) |
| The form's inputs are text with a numeric keypad, so the action's sentence is the only validation. | `type="number"` with `min`/`max`: the browser's own bubble, and "3.5" blocked before the action |
| `resetStorageHealth` (a test seam and the e2e `invalidate-cache` route) keeps the applied timings. | Reset them to the defaults until the next pass |
@@ -1267,6 +1274,30 @@ is rebuilt and restarted. Nothing is written until the operator saves the form;
the editor runs on today's numbers. A CLI `archilyzer index` or `build stats` reads the settings
itself.
+#### Rulings (parent, 2026-09-30)
+
+| Question | Ruling | Where |
+|---|---|---|
+| The counters' spacing: the ruled min(10 s, interval − 5 s) is 0 at a 5 s interval | The half-interval floor stays | as built (`13411edc`) |
+| A lowered cap: refuse the calls in flight over it, or reach it as they return | Reached as calls return; nothing in flight is refused | as built (`13411edc`) |
+| `archilyzer doctor` runs on the default timings | Its one `applyHealthTimings` line goes to slice SG, which owns `common/bin/doctor.ts` | "Found and left" |
+
+#### Review
+
+**Verdict: SHIP AFTER FIXES** (`dt-review.md` in the job's scratch). No High. The review re-ran every
+gate (tsc, common 2,318, editor unit 100, the three docs checks, `storage-locations` 9/9, a clean
+`merge-tree` against `main`), checked the writer paths, the migration, the accessor's `globalThis`
+home, the re-arm, the live cap's arithmetic and 20 FACTS anchors, and found no accessible-name
+collision in the three specs that open `/storage`.
+
+| Finding | Ruling | Where |
+|---|---|---|
+| M1: `inFlightPerLocation` could be 16, the editor's whole thread pool, so one drive that stops answering could hold every thread; the hint only warned | The maximum is 8; the hint, the docs string, `SETTINGS.md`, the test values and these records | `85c2407b`, this commit |
+| L1: the parent's rulings were not in the record | The block above; the doctor bullet names SG | this commit |
+| L2: no test pinned that a location write keeps `storage.health` | One `mergeSettingsPatch` case | `e7a525f3` |
+| L3: a refused save clears the typed value (React's form reset) | As recorded in "Found and left" | as built |
+| L4: the no-location timeout's words read the budget at throw time, not the one the call ran against | The detail carries `secondsText(budget)`; a test that changes the budget mid-call | `6a24ac4e` |
+
## Rollout
Release 15 is slices IG (`r15/index-hold`, merged `ccf90892`), UT (`r15/umtool-trace`, `07c991be`),