Archilyzer · Source

archilyzer

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

commit e0cea7ec237d7fd9ccc6cce7860291bb2b3bcf41
parent 5df886be3bbe8b7d9eb4724752e8b8e7dd3a1c2b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 01:42:57 -0400

channels: a bucket retry keeps its card and run log after it empties the bucket

Every video a retry fetches refreshes the snapshot, and the page's refresh
then handed NeedsCookiesList (and RetryBucketControl inside it) an empty
bucket while the run was still streaming; both returned null, unmounting the
StreamActionLog and its log (the cookies-mode.spec:241 intermittent, "Retry
needs cookies output" not found). The FACTS shape: a ranHere flag set in the
trigger; return null only while the bucket is empty and nothing ran here;
the button disabled while the bucket is empty. Applied to the three bucket
cards on the Download stage and to RetryBucketControl itself. A reload drops
an empty card as before; labels and test ids unchanged.

cookies-mode.spec.ts: the test waits for the refreshed, EMPTY card ("needs
cookies empty") and asserts the log and a disabled "Download with cookies
(0)" in it, which makes the old race certain rather than likely.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Meditor/app/channels/[slug]/components/RetryBucketControl.tsx | 26+++++++++++++++++++++-----
Meditor/app/channels/[slug]/components/stages/DownloadStage.tsx | 21++++++++++++++++++---
Meditor/e2e/cookies-mode.spec.ts | 17+++++++++++++++++
3 files changed, 56 insertions(+), 8 deletions(-)

diff --git a/editor/app/channels/[slug]/components/RetryBucketControl.tsx b/editor/app/channels/[slug]/components/RetryBucketControl.tsx @@ -27,6 +27,9 @@ type Props = { replaceAutoSubs?: boolean; // Overrides the default "Retry (n)" button text. buttonLabel?: string; + // Told when a run STARTS here, so a parent card that hides itself on an + // empty bucket can stay mounted for this run's log (see below). + onRun?: () => void; }; export function RetryBucketControl({ @@ -39,14 +42,24 @@ export function RetryBucketControl({ forceCookies, replaceAutoSubs, buttonLabel, + onRun, }: Props) { const [queue, setQueue] = useState(defaultQueueKey); const [handlingOverride, setHandlingOverride] = useState( replaceAutoSubs ? "transcribe" : "", ); const [abortOnError, setAbortOnError] = useState(false); + // A RETRY EMPTIES ITS OWN BUCKET, and the log must survive that (release 11 + // slice O3; plans/FACTS.md, "A run log lives in the panel's React state"). + // Every video the run fetches refreshes the snapshot, and the page's refresh + // then hands this control an empty `ids` while the run is still streaming — + // returning null there unmounted the StreamActionLog and took the log with + // it (cookies-mode.spec's intermittent "Retry needs cookies output" not + // found). Once a run has started here the control stays, with its button + // disabled while the bucket is empty; a reload drops it as before. + const [ranHere, setRanHere] = useState(false); - if (ids.length === 0) return null; + if (ids.length === 0 && !ranHere) return null; return ( <div @@ -54,8 +67,10 @@ export function RetryBucketControl({ aria-label={`retry ${actionLabel} bucket`} > <StreamActionLog - trigger={() => - retryBucketAction( + trigger={() => { + setRanHere(true); + onRun?.(); + return retryBucketAction( slug, ids, queue, @@ -64,8 +79,9 @@ export function RetryBucketControl({ bucketKey, forceCookies, replaceAutoSubs, - ) - } + ); + }} + disabled={ids.length === 0} cancelAction={cancelJobAction} buttonLabel={ buttonLabel diff --git a/editor/app/channels/[slug]/components/stages/DownloadStage.tsx b/editor/app/channels/[slug]/components/stages/DownloadStage.tsx @@ -352,7 +352,10 @@ function NoTranscriptList({ defaultQueueKey: string; existingQueues: string[]; }) { - if (ids.length === 0) return null; + // Stays mounted once its retry has run: the run empties this bucket, and + // the log must outlive that (RetryBucketControl says why). + const [ranHere, setRanHere] = useState(false); + if (ids.length === 0 && !ranHere) return null; return ( <div className="flex flex-col gap-2 rounded border border-border p-3"> <div> @@ -372,12 +375,14 @@ function NoTranscriptList({ itemAriaLabel={(id) => `no transcript or download ${id}`} /> <RetryBucketControl + key="retry" slug={slug} ids={ids} actionLabel="missing transcript and no download" defaultQueueKey={defaultQueueKey} existingQueues={existingQueues} bucketKey="noTranscript" + onRun={() => setRanHere(true)} /> </div> ); @@ -394,7 +399,10 @@ function PartialDownloadsList({ defaultQueueKey: string; existingQueues: string[]; }) { - if (ids.length === 0) return null; + // Stays mounted once its retry has run: the run empties this bucket, and + // the log must outlive that (RetryBucketControl says why). + const [ranHere, setRanHere] = useState(false); + if (ids.length === 0 && !ranHere) return null; return ( <div className="flex flex-col gap-2 rounded border border-border p-3"> <div> @@ -417,12 +425,14 @@ function PartialDownloadsList({ itemAriaLabel={(id) => `partial download ${id}`} /> <RetryBucketControl + key="retry" slug={slug} ids={ids} actionLabel="resume partial downloads" defaultQueueKey={defaultQueueKey} existingQueues={existingQueues} bucketKey="partialDownloads" + onRun={() => setRanHere(true)} /> </div> ); @@ -439,7 +449,10 @@ function NeedsCookiesList({ defaultQueueKey: string; existingQueues: string[]; }) { - if (ids.length === 0) return null; + // Stays mounted once its retry has run: the run empties this bucket, and + // the log must outlive that (RetryBucketControl says why). + const [ranHere, setRanHere] = useState(false); + if (ids.length === 0 && !ranHere) return null; return ( <div className="flex flex-col gap-2 rounded border border-border p-3"> <div> @@ -464,12 +477,14 @@ function NeedsCookiesList({ itemAriaLabel={(id) => `needs cookies ${id}`} /> <RetryBucketControl + key="retry" slug={slug} ids={ids} actionLabel="needs cookies" defaultQueueKey={defaultQueueKey} existingQueues={existingQueues} bucketKey="needsCookies" + onRun={() => setRanHere(true)} forceCookies /> </div> diff --git a/editor/e2e/cookies-mode.spec.ts b/editor/e2e/cookies-mode.spec.ts @@ -301,6 +301,23 @@ test("defer: excluded from batches, surfaced in Needs cookies, downloads via the // The bucket empties once the snapshot regenerates. await waitForNeedsCookiesBucket(0); + // …and the page refreshes onto the EMPTY bucket with the card still up and + // its run's log in it (release 11 slice O3). The card used to return null on + // an empty bucket, so whichever refresh landed first — the per-video + // snapshot's, mid-run — unmounted the log: the intermittent "Retry needs + // cookies output: element(s) not found" above. Waiting for the empty list + // makes that race certain rather than likely: it renders only after the + // refresh, inside the card that must have survived it. + await expect(page.getByLabel("needs cookies empty")).toBeVisible({ + timeout: 20_000, + }); + await expect(retryLog).toContainText( + "Managed download complete: 1 succeeded, 0 failed", + ); + await expect( + bucket.getByRole("button", { name: /^Download with cookies \(0\)$/ }), + ).toBeDisabled(); + // A fresh page has nothing to show for an empty bucket. await page.reload(); await expect(page.getByLabel("retry needs cookies bucket")).toHaveCount(0); });