Archilyzer · Source

archilyzer

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

commit 697378098fbc49c58d00b98952baa263d5aa72f5
parent a6e6d463314244d135395627c460a1fe2322602e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue,  8 Sep 2026 16:43:27 -0400

plans: the fourth flake was a remounted panel, and two earlier claims were artifacts

STATE.md's "the job never reaches a terminal state" and deflake-e2e.md's "the log
FILE has no more than the panel does" were both wrong, and both are named as
wrong here so nobody builds on them again: the instrumented trace shows the job
finishing in 65 ms with all five lines written — the summary included — and
finalizing `done`. What failed was the panel, which React had unmounted and
rebuilt with empty state when the new record changed the fragment's child list.

- STATE.md: all four de-flake items are closed; the trace excerpt, the cause and
  the two retired claims.
- deflake-e2e.md: "4, as shipped" — the decisive trace, which hypothesis held
  (H1) and how H2-H5 died, the fix and the deterministic test.
- FACTS.md (one-core Phase 1): a run log lives in the panel's React state, so a
  panel carrying a StreamActionLog must not change position when its record
  appears — the shape to copy, pinned to `a6e6d46`.

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

Diffstat:
Mplans/FACTS.md | 20++++++++++++++++++++
Mplans/STATE.md | 51++++++++++++++++++++++++++++++++++-----------------
Mplans/deflake-e2e.md | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 105 insertions(+), 17 deletions(-)

diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -3524,6 +3524,26 @@ entry calls `missingInput`; its `eligible`/`present` are the playlist and `totals.downloaded`/`totals.transcribed`. `EXTERNAL_BAND_IDS` is now `EXTERNAL_OPERATIONS.map(o => o.id)` rather than a hand-written pair. +### A run log lives in the panel's React state, so the panel must not move (`fae99f1`) + +`StreamActionLog` keeps the streamed log in `useState` and calls `router.refresh()` itself the +moment a run ends (`common/components/StreamActionLog.tsx:156-163`). That refresh re-renders +the SERVER component that draws the panel — with the sidecar the run just wrote now on disk. +So any panel that changes the SHAPE of its child list when its record appears destroys the log +it was showing: React reconciles a fragment's children by POSITION, a moved child is unmounted +and mounted again, and with `log` empty and `running` false `StreamActionLog` renders no +`<pre role="log">` at all — the element the operator (and the spec) is reading is not stale, +it is gone. `SpeakerBodies.tsx` had exactly that: `[empty state, RunOne]` before a record, +`[dl, ul, p, RunOne]` after. Both bodies are one two-child list now, the record body a sibling +of the button, with `<RunOne key="run-one" …>` last in both branches +(`editor/app/channels/[slug]/videos/[id]/components/SpeakerBodies.tsx:86-99,166-179`) — the +position is the fix, the key is the brace behind it. **This is the shape to copy for any new +panel that carries a `StreamActionLog`** — `DigestBody.tsx:71-131` was already written that +way (one return, one ternary, the log at a fixed index), which is why only the speaker panels +flaked — and it is why `attribution.spec.ts:346` flaked 1-2 runs in 10 for a batch that had +finished in 65 ms with every line written. The full trace and the four hypotheses it killed +are in `plans/deflake-e2e.md`'s "4, as shipped". + ### The `editor/content` symlink still blocks e2e in the primary checkout Unchanged from the Phase 0 entry, plus one thing Phase 1 learned twice: **`git add -A` stages diff --git a/plans/STATE.md b/plans/STATE.md @@ -71,8 +71,8 @@ instead — which needs one extra step, a composed fixture site copied into its so the export webServer answers 200 instead of 500. **`git add -A` stages that symlink**, and a commit made that way carries the panic into every worktree of it: add by path. -**Three of the four e2e flakes are fixed** (`plans/deflake-e2e.md`, `a32612a` onward), and the -class is worth remembering: **a debounced write survives a test boundary +**All four e2e flakes are fixed** (`plans/deflake-e2e.md`, `a32612a` → `fae99f1`), and the +first three's class is worth remembering: **a debounced write survives a test boundary until the next reset.** `video-page.spec.ts:216` ("Delete directory wrong-id confirmation") asserted a video directory still existed after a refused delete and found it gone — nothing deleted it, `reconcileVideoDirs` RENAMED it. Snapshot generation moves `data/<name>` to @@ -89,21 +89,38 @@ script has run — the script now marks itself `data-theme-ready` and they wait ask-chat's "Stop aborts mid-sweep" raced a 500 ms route timer; the test holds batch 2 open on a latch it releases itself. No retries, `test.slow`, or serial markers were added anywhere. -**The fourth REPRODUCED, is NOT fixed, and is not what the plan guessed.** -`attribution.spec.ts` "Run from the video page runs that video and no other" fails 1-2 runs in 10 -under `--repeat-each 10` (four runs, six failures), always waiting the full 90 s for the batch's -closing "1 done" line. The unit IS dispatched and the stub DOES answer: every failure freezes on -the same three lines, and the failure snapshot shows the freshness pill already reading -"current" — so `attribution.json` was written, and `attributeOneVideo` logs its closing line -immediately after that write with nothing in between. Meanwhile the server keeps answering -`/api/pulse` with an unchanging `rev`, so **the job never reaches a terminal state**: -`runOperationBatch` does not return and `operationJobs.ts`'s summary line is never emitted. The -hang is in the BATCH, after the unit's write — not in the log panel. Two panel-side fixes were -tried (reconcile against the job's log file when the stream ends; reconcile from the status poll -on a terminal status) and both measured 1-2/10, i.e. no change: the log FILE has no more than -the panel does. Both reverted; `StreamActionLog` is untouched. Next attempt should instrument -the job's registry record and look at `concurrentRunner.ts`'s "a transiently-zero limit does NOT -terminate" wait and `taskHooks.ts`'s per-line progress parsers. +**The fourth is fixed too, and it was never the batch** (`fae99f1`, +[`attribution-batch-hang.md`](attribution-batch-hang.md)). `attribution.spec.ts:346` "Run from +the video page runs that video and no other" failed 1-2 runs in 10 waiting the full 90 s for +the batch's "1 done". Instrumented at `fea2996` — `console.error` with ISO timestamps, never +through the job's own `onLog` — it reproduced 3 times in 25, and every failure reads the same: + +``` +[TRACE 2026-09-08T20:25:21.661Z] attributeOne.post-write attrvid0002 +[TRACE 2026-09-08T20:25:21.661Z] onLog job=01M21B4PDQZC6YWGZH38WYV37N n=4 closed=false :: Attribute attrvid0002 (text-only): 2 speaker(s)… +[TRACE 2026-09-08T20:25:21.662Z] runPool returned attribution-channel +[TRACE 2026-09-08T20:25:21.662Z] onLog job=01M21B4PDQZC6YWGZH38WYV37N n=5 closed=false :: Backfill attribution-channel: 1 done, 0 already current… +[TRACE 2026-09-08T20:25:21.663Z] fn.finally job=01M21B4PDQZC6YWGZH38WYV37N status=done +Error: locator.getAttribute: Test timeout of 120000ms exceeded. + - waiting for getByLabel('Run Speaker names (from the transcript) output') +``` + +The job finished in **65 ms**, wrote all five lines including "1 done", enqueued every one of +them with `closed=false`, and finalized `done`. The log panel was **gone** — not stale, not +short, absent: the failure snapshot shows the record body and the Run button and no +`<pre role="log">` at all. `AttributionBody` returned two fragments of different SHAPES +(`[empty, RunOne]` with nothing on disk, `[dl, ul, p, RunOne]` with a record), React reconciles +a fragment's children by POSITION, so the record landing moved `<RunOne>` down the list — and a +moved child is unmounted and re-mounted, taking `StreamActionLog`'s `log` state with it. The +refresh that lands the record is the one `StreamActionLog` fires itself when the run ends, so +the panel is wiped at the instant the summary line reaches it, and the spec could only pass in +the ~200 ms window before that. **The earlier note here — "the job never reaches a terminal +state", "the hang is in the BATCH" — was the artifact, and so was "the log FILE has no more +than the panel": the file had everything, and no reconcile could have helped a component whose +state had been destroyed.** Do not trust either claim again. Both bodies are one two-child list +now, RunOne last and keyed; the spec asserts the log still says "1 done" AFTER the pill reads +"current", which is red on every run at `fea2996` (5/5) and green at the fix. +`attribution.spec.ts --repeat-each 20`: **20/20**. **The last full suite: 514 passed, 1 failed of 515, 25.1 min** from a worktree of `2133d94`. The red was slice 1.5's own new band assertion and it was right — writing the two new diff --git a/plans/deflake-e2e.md b/plans/deflake-e2e.md @@ -115,6 +115,57 @@ component is untouched. If someone picks this up: the surviving suspects are ins zero `laneLimit`, and `taskHooks.ts`'s `onLog` progress parsers, which run on every line the unit emits. Instrument the job's registry record, not the log panel. +### 4, as shipped — the panel was remounted, and the batch was innocent (`fae99f1`) + +Root-caused under the plan in [`attribution-batch-hang.md`](attribution-batch-hang.md). +Instrumented at `fea2996` (`console.error`, ISO timestamps, the job id, never through the +job's own `onLog`, editor webServer `stdout`/`stderr` piped): **3 failures in 25**, all +identical. The decisive trace, one job: + +``` +[TRACE 2026-09-08T20:25:21.598Z] onLog job=01M21B4PDQZC6YWGZH38WYV37N n=1 closed=false :: Backfill attribution-channel: attribution-text over 1 video +[TRACE 2026-09-08T20:25:21.611Z] onLog job=01M21B4PDQZC6YWGZH38WYV37N n=3 closed=false :: ollama qwen2.5:7b: 100 in / 50 out tokens in 0s wall +[TRACE 2026-09-08T20:25:21.661Z] attributeOne.post-write attrvid0002 +[TRACE 2026-09-08T20:25:21.661Z] onLog job=01M21B4PDQZC6YWGZH38WYV37N n=4 closed=false :: Attribute attrvid0002 (text-only): 2 speaker(s), 2 segment(s) +[TRACE 2026-09-08T20:25:21.662Z] runPool returned attribution-channel +[TRACE 2026-09-08T20:25:21.662Z] onLog job=01M21B4PDQZC6YWGZH38WYV37N n=5 closed=false :: Backfill attribution-channel: 1 done, 0 already current, 0 failed +[TRACE 2026-09-08T20:25:21.663Z] fn.finally job=01M21B4PDQZC6YWGZH38WYV37N status=done +Error: locator.getAttribute: Test timeout of 120000ms exceeded. + - waiting for getByLabel('Run Speaker names (from the transcript) output') +``` + +The whole job took **65 ms**. Every line reached `onLog` with `closed=false`, the summary +included, and the record finalized `done`. So of the five hypotheses the plan ranked, **H1 +held and H2-H5 are dead**: no `stream.cancel` fired in 240 instrumented tests (40 of this +spec), `limit()` never read 0 +(`pool.wait-capacity target=1` throughout), `runOne`'s `finally` timestamps are all present, +finalize ran, and no signal aborted. + +What the panel was doing is the answer, and it is not a stale log — the log ELEMENT was gone. +`AttributionBody` (and `DiarizationBody`) returned two fragments of different SHAPES: +`[empty state, RunOne]` with no record on disk, `[dl, ul, p, RunOne]` with one. React +reconciles a fragment's children by POSITION, so the record appearing moved `<RunOne>` from +index 1 to index 3 — and a moved child is unmounted and mounted again, taking +`StreamActionLog`'s `log` state with it. With `log` empty and `running` false the component +renders no `<pre role="log">` at all, which is exactly what the failure snapshot shows: the +record body, the Run button, and no log. The refresh that lands the record is the one +`StreamActionLog` fires itself at the end of a run (`:163`), so the panel is wiped at the +instant the summary line reaches it, and the spec could only pass in the ~200 ms window +before the refresh landed. + +**This retires two claims made here on 2026-09-08 and both were measurement artifacts**: "the +job never reaches a terminal state" (it reached it in 65 ms) and "the log FILE has no more +than the panel does" (the file had everything). It also explains why the two panel-side +reconciles measured no change — no amount of reconciling helps a component React has just +destroyed and rebuilt with empty state. + +Fix: both bodies are ONE two-child list, the record body a sibling of the button, `RunOne` +last and keyed in both branches. No DOM change, no server change, `StreamActionLog` untouched. +Test: the spec asserts the panel STILL contains "1 done" AFTER the freshness pill reads +"current" — the pill is the proof the refresh landed, which makes it deterministic rather than +a second race. At `fea2996` that assertion is red **5/5** (`element(s) not found`); at +`fae99f1` `attribution.spec.ts --repeat-each 20` is **20/20**. + ## Not changed, deliberately - `reconcileVideoDirs` keeps mutating during snapshot generation; that is the product's