Archilyzer · Source

archilyzer

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

commit a6e6d463314244d135395627c460a1fe2322602e
parent 4e50a1b902e81695f6765e5bd6c570fb0cdc8fb7
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue,  8 Sep 2026 16:38:38 -0400

editor: the run panel keeps its place when the record lands, so its log survives

`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". The batch
was never the problem. Instrumented at `35b5c5b` (console.error, ISO timestamps,
never through the job's own onLog) and reproduced 3 times in 25:

  [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 job ran to completion in 65 ms, wrote all five lines INCLUDING "1 done",
enqueued every one of them with `closed=false`, and finalized `done`. No
`stream.cancel`, no zero-limit hold, no unsettled unit — H2 through H5 are dead.
What the failure snapshot shows is the panel's record body and its Run button
present and the `<pre role="log">` GONE.

`AttributionBody` returned two fragments of different SHAPES — `[empty, 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:
StreamActionLog's `log` state died with it, and with `log` empty and `running`
false the log element is not rendered at all. 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 the refresh landed. `DiarizationBody` had the same shape.

Both bodies are one two-child list now — the record body is a sibling of the
button, and RunOne is last and keyed in both branches. Nothing about the DOM or
the disk changes; this is only what React is allowed to keep.

The spec asserts it deterministically: after the freshness pill reads "current"
(proof the refresh landed) the panel must STILL contain "1 done". That assertion
is red on every run at `35b5c5b` and green at this one. Not touched: lane hold
semantics, `retries`, `test.slow`, serial markers, `StreamActionLog` itself.

Plan: plans/attribution-batch-hang.md.

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

Diffstat:
Meditor/app/channels/[slug]/videos/[id]/components/SpeakerBodies.tsx | 64++++++++++++++++++++++++++++++++++++++++++++++------------------
Meditor/e2e/attribution.spec.ts | 14++++++++++++++
2 files changed, 60 insertions(+), 18 deletions(-)

diff --git a/editor/app/channels/[slug]/videos/[id]/components/SpeakerBodies.tsx b/editor/app/channels/[slug]/videos/[id]/components/SpeakerBodies.tsx @@ -72,20 +72,39 @@ export function DiarizationBody({ view: OperationPanelView; data: DiarizationBodyData; }) { - const record = data.record; - if (!record) { - return ( - <> + // ONE CHILD LIST, WHATEVER IS ON DISK — and that is what keeps the run log + // alive. React reconciles a fragment's children BY POSITION, so the old shape + // (`[empty state, RunOne]` before a record exists, `[provenance, …, RunOne]` + // after) MOVED <RunOne> down the list the moment the record appeared. A moved + // child is not re-rendered, it is unmounted and mounted again — taking + // StreamActionLog's `log` state with it. So the run finished, its own + // router.refresh() landed the new record, and the operator's log vanished at + // the instant the summary line reached it. `attribution.spec.ts:346` was the + // 1-in-10 witness: it waits for the batch's "1 done" and the panel was gone. + // The record body is a SIBLING of the button now, and RunOne is last and keyed + // in both branches, so nothing can move it. + return ( + <> + {data.record ? ( + <DiarizationRecordBody record={data.record} /> + ) : ( <p aria-label="diarization empty" className="text-sm text-muted-foreground" > No speaker turns have been captured for this video. </p> - <RunOne slug={slug} videoId={videoId} view={view} /> - </> - ); - } + )} + <RunOne key="run-one" slug={slug} videoId={videoId} view={view} /> + </> + ); +} + +function DiarizationRecordBody({ + record, +}: { + record: NonNullable<DiarizationBodyData["record"]>; +}) { const e = record.engine; return ( <> @@ -126,7 +145,6 @@ export function DiarizationBody({ <p aria-label="diarization summary" className="text-sm"> {record.speakers} speakers · {record.turns} turns </p> - <RunOne slug={slug} videoId={videoId} view={view} /> </> ); } @@ -142,20 +160,31 @@ export function AttributionBody({ view: OperationPanelView; data: AttributionBodyData; }) { - const record = data.record; - if (!record) { - return ( - <> + // Same two-child shape as DiarizationBody above, and for the same reason: + // <RunOne> must not change position when the record appears, or React + // remounts it and StreamActionLog loses the log the run just wrote. + return ( + <> + {data.record ? ( + <AttributionRecordBody record={data.record} /> + ) : ( <p aria-label="attribution empty" className="text-sm text-muted-foreground" > No speaker names for this video yet. </p> - <RunOne slug={slug} videoId={videoId} view={view} /> - </> - ); - } + )} + <RunOne key="run-one" slug={slug} videoId={videoId} view={view} /> + </> + ); +} + +function AttributionRecordBody({ + record, +}: { + record: NonNullable<AttributionBodyData["record"]>; +}) { const p = record.provenance; return ( <> @@ -223,7 +252,6 @@ export function AttributionBody({ <p aria-label="attribution summary" className="text-sm"> {record.segments} segments · {record.warnings} warnings </p> - <RunOne slug={slug} videoId={videoId} view={view} /> </> ); } diff --git a/editor/e2e/attribution.spec.ts b/editor/e2e/attribution.spec.ts @@ -374,6 +374,20 @@ test("Run from the video page runs that video and no other", async ({ await expect(page.getByLabel("attribution-text freshness")).toHaveText( "current", ); + + // AND THE LOG SURVIVES THAT REFRESH. The pill above is the proof the refresh + // landed: the record now exists, so the panel draws its record body instead + // of the empty state. When that swap MOVED the run button down the fragment's + // child list, React remounted StreamActionLog and the log the operator was + // reading disappeared — the whole log element with it. That is also what made + // the wait above flaky (1-2 runs in 10): it could only pass in the window + // between the summary line arriving and the refresh landing, and on a fast + // box the two are ~200 ms apart. Asserted AFTER the pill so the refresh has + // definitely happened, which is what makes this deterministic rather than a + // second race. + await expect( + page.getByLabel("Run Speaker names (from the transcript) output"), + ).toContainText("1 done"); }); // ---------------------------------------------------------------------------