commit 840dbdd9eecfeb00a82957a96d785cd83133330f
parent ffef935acd5d25306465315d347196a36681e8ae
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 13:14:41 -0400
plans: slice W1's record — the editor lows, as shipped; FACTS: the Diagnostics cards closed, a queued cancel's sidecar
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 210 insertions(+), 1 deletion(-)
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -3714,7 +3714,13 @@ above: `ranHere` set in the trigger (and `onRun` to tell the card), null only wh
stage's Fetch audio control is covered too (its section stays while `autoSubsCount > 0`). **NOT
Diagnostics:** its two grids drop an emptied bucket's card before the control renders
(`DiagnosticsStage.tsx:156` `populated`, `:694` `listed`), so a Diagnostics retry that empties its
-bucket still loses its log — open.
+bucket still loses its log — open. **Closed in release 13 slice W1:** both grids keep a card whose
+Retry ran on the page — a `ran` set (`useRanBuckets` in `DiagnosticsStage.tsx`), filled by
+`RetryBucketControl`'s `onRun`, keyed by the card's aria label (channel health) or status
+(availability); `AvailabilitySummary`'s hook sits above its `total === 0` return, which yields to
+a ran card. `diagnostics-retry-log.spec.ts` waits for the refreshed EMPTY list in each grid. An
+availability bucket is never emptied by its own retry (a download records availability history
+only, `updateTopLevel: false`); a probe landing mid-run empties it.
The persist case needed a fixture: `editor/e2e/fixtures/bin/fake-ytdlp.mjs:680-725` (at
`12d1778`) grows an app-extraction branch — the LAST branch checked, matched on the media
@@ -6482,6 +6488,13 @@ Line numbers are `plans/FACTS.md` lines at `e172749b`, before this record's in-p
timeout of its own) and logged when it fires; the storage pass is not cancelled. `cancelReason`
is drawn on `/jobs` (`JobListEntry` → `JobRowView.cancelReason`, only on a `cancelled` meta;
`data-testid="cancel-reason"` on the row and the job page's "Cancelled because" cell).
+ **Release 13 (W1): an operator's cancel of a QUEUED job now writes its `cancelled` sidecar**
+ (before, it kept `queued`, and this pass could re-queue it). `registry.cancel()` marks the record
+ terminal (`markTerminal`) before `scheduler.cancel()` fires `onCancel`, and streamCommand's
+ `onCancel` persists a terminal record. **Graceful shutdown still leaves `queued`:**
+ `shutdownCancel.ts` calls `registry.beginShutdown()` first, so a queued job reaches `onCancel`
+ still queued and nothing is written — this pass keeps its input. A job's meta writes are chained
+ (`metaWriter` in `streamCommand.ts`), so the enqueue's write never lands after the terminal one.
- **The hub embeds `public/hub-summary.json`** at `compose:hub` (`common/controller/poolSummary.ts`
shared with `compose-homepage`; `common/lib/hubSummary.ts` projects `official` + per-site figures
from the same `buildHomepageSummary`). Optional end to end: missing/404/malformed → cards without
diff --git a/plans/release-13.md b/plans/release-13.md
@@ -40,4 +40,200 @@ Then one integration gate on `main` (`r13/integration`) and the runbook
## Record
+### Slice W1, as shipped — the editor lows (2026-09-28)
+
+Branch `r13/lows-editor` off `main` `bf6904e8` (`441bdbb2` merged first, fast-forward), worktree
+`/home/user/Projects/r13-lows-editor`, one Opus implementer beside W2 and W3. Seven lows left by
+release 11. No settings, site or channel key; nothing on disk moves. Scratch files `w1-*` in the
+job's `tmp/overnight`.
+
+**1 — a Diagnostics retry keeps its card and its log** (`DiagnosticsStage.tsx`).
+- The O3 hole on the other stage: both Diagnostics grids dropped an emptied bucket's card
+ (`populated` for channel health, `listed` for availability), and a retry empties its bucket while
+ it streams (the per-video snapshot regen, then the end-of-run refresh). The card took
+ `RetryBucketControl`, its `StreamActionLog` and the log with it.
+- `useRanBuckets()`: a `ran` set of the buckets whose Retry ran on this page, filled by
+ `RetryBucketControl`'s existing `onRun`. Keyed by the card's aria label (channel health) or its
+ status (availability). A ran card stays, its button disabled at **Retry (0)** (the control's own
+ `ranHere` already did that). `AvailabilitySummary`'s hook sits above its `total === 0` early
+ return, and that return yields to a ran card. A reload drops an empty card. Labels and test ids
+ unchanged.
+- **An availability bucket is never emptied by its own retry:** a download records availability
+ *history* only (`updateTopLevel: false`), so only a probe rewrites `availability.json`. The
+ spec lands one before the click (as a check running beside the retry would), and the regen the
+ retry's download triggers is the first to read it.
+- `diagnostics-retry-log.spec.ts` (new, 2), modelled on `cookies-mode.spec.ts:241`: a **Missing
+ metadata.info.json** retry (an empty `data/vidnometa1/`; the fake writes the metadata) and a
+ **Needs auth** retry. Each waits for the refreshed EMPTY list, which renders only inside a card
+ that survived, then asserts the log, the disabled **Retry (0)**, and that a reload drops it.
+
+**2 — the `transcript-source.spec.ts:76` flake** (spec only).
+- The cause is the prompt's: `resetData`'s invalidate-cache clears the snapshot scheduler's TIMER,
+ but a regeneration already in flight runs on and writes `snapshot.json` from the tree it read,
+ before the spec's swap. `generateReport` returns as soon as any `snapshot.json` exists. The race
+ is the harness reset's, not product code's: in production a change made through the app arms its
+ own regen after the fact.
+- The test now deletes `snapshot.json` after the swap and clicks **Refresh report** until the report
+ shows `nonStandardVtt` with one video (`chat-only.spec`'s `refreshReport` shape).
+
+**3 — `/sites` "built <when>" refreshes when a homepage build lane ends** (`JobLane.tsx`,
+`HomepageBuildButtons.tsx`).
+- `JobLane` gets `onSettled(outcome)`, with `LaneOutcome = "done" | "failed" | "cancelled" |
+ "error"` ("error" = the trigger refused or threw, no job). It is called once, from one `settle()`
+ that also sets the chip. The prop is read through a ref, because the one-shot launch effect would
+ otherwise call the first render's. A `settledRef` makes "once" the prop's contract, not the
+ effect's shape (O4's Strict Mode fix is untouched). It is not called for a lane that a newer
+ launch unmounted.
+- `HomepageBuildButtons` calls `router.refresh()` when a lane that BUILDS (**Build homepage**,
+ **Build & deploy homepage**) ends with any job outcome. **Wider than the prompt's "ends done", on
+ purpose:** a failed or cancelled build may already have rewritten or emptied `homepage/out`, and
+ the line is what **Deploy homepage** would ship. `StreamActionLog` refreshes after any run that
+ started, for the same reason. No refresh on "error" or for a deploy lane.
+- **AutoRefresh already covered most of it:** the pulse token carries each job's status and
+ `endedAt`, so with passive refresh on (5 s by default) `/sites` re-rendered once the job ended
+ anyway. The explicit refresh is immediate, and it is the only one when
+ `autoRefreshIntervalSeconds` is 0.
+- **e2e cannot build the homepage** (O4's rule: `homepage/out` is the checkout's own directory), so
+ the new `sites-homepage.spec` test proves the RE-RENDER on the one terminal state it may reach,
+ a cancel:
+ - passive refresh is off, and both queues are held, as before;
+ - Build homepage queues, then a site is written to disk;
+ - the queued job is cancelled from OUTSIDE the page, through the harness reset (newest first, so
+ nothing is promoted). The lane's own Cancel is a server action that revalidates, and its
+ response re-renders the page by itself (`server-action-reducer.js`: a revalidating action
+ navigates to the current URL);
+ - the new site's link appears without a reload.
+
+**4 — `/jobs` labels** (`jobKinds.ts`).
+- Six entries: `build-hub` **Build hub**, `deploy-hub` **Deploy hub**, `build-deploy-hub`
+ **Build & deploy hub**, `build-homepage` **Build homepage**, `deploy-homepage` **Deploy
+ homepage**, `build-deploy-homepage` **Build & deploy homepage** (the `/sites` lanes' titles).
+- Shape: `queueKeyStrategy: "custom"` (`BUILD_QUEUE` / `DEPLOY_QUEUE`), not drainable, not
+ replayable (no JobSpec), no `needsMedia` (no channelSlug, and none of them opens a channel's
+ `data/`).
+- **Correction to the prompt:**
+ - There are no "existing build/deploy entries" to copy. No publish kind has an entry (O4's record
+ says so), so the shape is the table's own.
+ - The prompt named four kinds. `deploy-hub` and `build-deploy-hub` are in too, so the hub's trio
+ is not half-labelled.
+- **Consumers checked:**
+ - `jobKindLabel` renders on `/jobs` (`JobRow`, `JobsTable`) and on the job page.
+ - `jobKinds.test.ts` enumerates the table: `ADDED_KINDS` +6.
+ - No spec matches these kinds by row text. `ops-api` uses them as API verbs only, and
+ `sites-homepage` reads `kind` from the metas.
+ - The publish kinds (`build-export`, `build-deploy`, `deploy-export`, `build-index`, …) are still
+ unlabelled. Left: not asked.
+
+**5 — a job cancelled while queued writes its cancelled sidecar** (`registry.ts`,
+`streamCommand.ts`, `shutdownCancel.ts`).
+- **The bug.** `onCancel` only closed the stream and settled `done`, and `registry.cancel()` marked
+ the record terminal only after `scheduler.cancel()` had fired it. The sidecar kept its enqueue's
+ `queued`. `/jobs` read an evicted one back as queued, and the release-9 boot pass could
+ **re-queue a job the operator had cancelled** (the newest of its spec, under 24 h).
+- **The fix.** `registry.cancel()` marks a queued record terminal (`markTerminal`, now shared with
+ `finalize`) BEFORE `scheduler.cancel()`. `finalize()` cannot go first: its `scheduler.complete`
+ would drop the entry without firing `onCancel`, and `done` would never settle. Both `onCancel`s
+ persist the sidecar when the record is terminal.
+- **Found: the naive fix breaks the boot pass.** `shutdownCancel.ts` cancels every queued job on
+ SIGTERM/SIGINT (so the exit cannot promote one into a child), and `bootQueuedJobs.ts` exists to
+ settle exactly those `queued` metas on the next start. A graceful restart would have written
+ `cancelled` over each (or torn it mid-exit). So `registry.beginShutdown()`, called first by the
+ reaper, makes `cancel()` skip the early mark. A queued job then reaches `onCancel` still queued,
+ and nothing is written. **`shutdownCancel.ts` is outside the prompt's Owns list** (no other slice
+ owns it); the change is three lines. The call is optional (`?.()`), because under `next dev` the
+ registry on `globalThis` can predate the method.
+- **Meta writes are chained** (`metaWriter`). The enqueue's write and the terminal one used to be
+ two unordered `writeFile`s, so a cancel landing within milliseconds could leave `queued` on disk,
+ or a shorter JSON over a longer one's tail.
+- **Tests.** `registry.test.ts` +1: `onCancel` sees the record already `cancelled`, with `endedAt`.
+ `streamCommand.test.ts` (new, 3): a function job and a command job queued behind a holder,
+ cancelled, end `cancelled` on disk with no `startedAt`; after `beginShutdown()` the sidecar stays
+ `queued` (that test replaces the process registry afterwards). `sites-homepage.spec`'s Build
+ homepage test polls the job's sidecar to `cancelled` (the UI path).
+
+**6 — L7: doctor's worker engine from `paths`** (`doctor.ts`, the engine line only). The default
+engine was `app.defaultBin()`, which is this process's `getPaths()`. Now it is `paths.whisperBin` for
+whisper-cpp and `paths.parakeetBin` for parakeet, as the model line beside it uses `paths`. chough
+has no Paths field, so it keeps its app default. `doctor.test.ts` +1, and the test Paths gain
+`whisperBin` and `parakeetBin`. W3 adds an image check to the same file.
+
+**7 — `editor/package.json` `test`:** `tsx --test "app/**/*.test.ts"`. It works as `pnpm test` in
+`editor/` and as `pnpm --filter editor test` from the root: 85/85. `plans/tools/implementer-rules.md`
+is W2's, so the gate-list line is in the report for the parent to join.
+
+| sha | what |
+|---|---|
+| `2fa120cd` | 1: `useRanBuckets`, both grids keep a ran card; `diagnostics-retry-log.spec.ts` (new, 2) |
+| `aad7b3bf` | 2: `transcript-source.spec` drops `snapshot.json` after the swap and refreshes until the bucket reads 1 |
+| `cd0781f5` | 5: `markTerminal` before `onCancel`, `onCancel` persists a terminal record, `beginShutdown`, `metaWriter`; `registry.test.ts` +1, `streamCommand.test.ts` (new, 3) |
+| `5c6118df` | 3: `JobLane` `onSettled`; `HomepageBuildButtons` refreshes after a build lane; `sites-homepage.spec` +1, and the Build test polls the sidecar |
+| `79997a8d` | 4: six `JOB_KINDS` entries; `jobKinds.test.ts` `ADDED_KINDS` +6 |
+| `6a805323` | 6: doctor's default engine from `paths`; `doctor.test.ts` +1 |
+| `c91c9ce9` | 7: the editor `test` script |
+| `5da692f7` | `changelog:` `[Unreleased]` above `[0.10.0]` in `editor/CHANGELOG.md` (items 1, 3, 4, 5) |
+| _this_ | `plans:` this record; FACTS (the bucket-card paragraph closed, the boot-pass bullet amended) |
+
+**Gates** (logs `w1-*.log`):
+- **tsc** (`pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit`) was clean on the full
+ tree before the code commits (`w1-tsc1.log`). The commits split that tree by file, and none
+ depends on another.
+- **Unit tests** (`w1-units1.log`, on `c91c9ce9`):
+ - **common 2,119/2,119.** That is +5: registry +1, streamCommand +3, doctor +1. jobKinds grew
+ inside an existing test. So the base at `bf6904e8` is 2,114, not the prompt's 2,112.
+ - **editor unit 85/85**, **`test:scripts` 185 + 1 skipped of 186**, **mcp 269/269**.
+- **Editor build** `pnpm --filter editor exec next build` ok (`w1-build1.log`, compiled in 16 s).
+ The export build was not run: nothing under `export/` changed.
+- **e2e**, all queued and detached. The lock was free each time. `export/public` was linked per
+ path, with no dangling links. Mid-slice, an added worktree moved this one's block to 4311/4310.
+
+ | run | specs | passed | failed | time |
+ |---|---|---|---|---|
+ | A | `retry-bucket transcript-source cookies-mode sites-homepage jobs-retry diagnostics-retry-log queues cancel ops-api exclude-from-counts availability`, `--repeat-each=3` (186) | **109** | 77 | 10.7 min |
+ | A2 | `transcript-source diagnostics-retry-log sites-homepage retry-bucket queues`, `--repeat-each=3` | **57** | **0** | 7.2 min |
+ | B (bite) | `diagnostics-retry-log sites-homepage` on `main`'s six source files | 2 | 4 | 2.1 min |
+
+ - **Run A was killed by the machine, not the code.** At 13:00:39 the kernel OOM killer ran (user
+ journal: `session.slice: The kernel OOM killer killed some processes`). The live editor's
+ parakeet worker held 2.5 GB. After test 110 every request got `ERR_CONNECTION_REFUSED`.
+ - Tests 1–109 were the whole first repetition, all 11 specs 62/62, and the second through
+ `queues.spec:40`.
+ - A2 re-ran, ×3, everything the second and third repetitions had not reached, plus
+ `transcript-source` (**9/9** there; **3/3** in A's first repetition).
+ - `queues` (`cancels a queued job without disturbing…`) and `cancel` exercise the changed cancel
+ path; `ops-api` polls metas; `exclude-from-counts` and `availability` render the two grids.
+ - Checked after every run: the worktree has no `homepage/out`, and `homepage/public` holds no
+ ignored data.
+- **Numbers tool:** none.
+
+**They bite:**
+- **Unit** (`w1-bite-units.log`, `main`'s files swapped in, then restored by a trap):
+ - item 5: `main`'s registry, streamCommand and shutdownCancel fail 4 of 10. That is the registry
+ test, both sidecar tests, and the shutdown test (no `beginShutdown` there).
+ - **The naive fix** (`onCancel` always persists, `cancel()` always marks first) fails the shutdown
+ test, 1 of 10, which is the guard for the boot pass.
+ - The new registry with `main`'s `onCancel` (writes nothing) fails both sidecar tests, 2 of 3.
+ - item 4: `main`'s `jobKinds.ts` fails "added kinds carry their pinned label", 1 of 4.
+ - item 6: `main`'s `doctor.ts` fails the new test, 1 of 9.
+- **e2e** (run B, `w1-e2e-bite.log`; `main`'s `DiagnosticsStage`, `JobLane`, `HomepageBuildButtons`,
+ `registry`, `streamCommand`, `shutdownCancel`):
+ - both Diagnostics tests fail at the empty-list wait (`missing metadata empty`, `availability
+ needs_auth empty`), and the grid had dropped the card;
+ - the Build homepage test fails at the sidecar poll (`Expected "cancelled"`, `Received "queued"`);
+ - the re-render test fails at the new site's link;
+ - the two untouched `sites-homepage` tests pass.
+- **Item 2 cannot be made to bite on demand:** it is a race between a previous spec's in-flight
+ regen and this one. The fix is structural (no stale `snapshot.json` can satisfy the wait), and
+ `transcript-source` passed 12 of 12 across A and A2.
+- **Item 7** is a script, not a test.
+
+**Found and left**
+- **`shutdownCancel.ts` was touched** (item 5, above), outside the prompt's Owns list.
+- **The hub's lanes do not refresh `/sites` when they end** (`HubBuildButtons`, not this slice's
+ file). Nothing there reads the hub's `export/out` build time the way the homepage line does, so
+ there is nothing stale to show.
+- **The publish kinds are still unlabelled on `/jobs`** (`build-export`, `build-deploy`,
+ `deploy-export`, `build-index`, `build-stats`, `archive-*`).
+- **Run A's OOM:** the machine has 15 GB, and the live parakeet worker plus several worktrees'
+ servers crowd it. A full-suite gate tonight may meet the same killer.
+
## Rollout