commit 9dc0cec1a1330088c0ad6f306761df3aa7745123
parent 18e0407b7ad06de5ae93627150e43e82b2273ee3
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 25 Aug 2026 23:52:03 -0400
operations/<id>: resolve the runner and the sweep lane from the registry
Four hardcoded guesses decided what an operation page rendered, none of them
consulting the descriptor:
runnerKind = id === "transcription" || id === "download" ? id : null
laneJobKinds = JOB_KINDS_BY_LANE[id === "digest" ? "digest" : "backfill"]
sweepLaneIdFor(id) -> "backfill" for anything not digest
railStates: every band with no state of its own took the backfill lane's
All four were right only by coincidence of today's registry — two runners, two
sweep lanes, nothing else. A registered `transcode` (external, no runner) would
have rendered the BACKFILL SWEEP'S CONSOLE under a "Transcode" heading: its
band, its plan, its scope, and a live Start button that arms a corpus-scale GPU
commitment. A console for the wrong lane is worse than no console, because
every control on it works.
`dispatch` alone cannot resolve it: download and transcription are `external`
WITH a runner, transcode would be `external` with none. So the registry states
it. `ExternalOperation`/`OperationDescriptor` gain `runner?: AutoQueueKind`,
imported as `import type` — erased at compile time, so it cannot create a cycle
whatever jobs/autoQueueState.ts imports.
`sweepLaneIdFor` moves to app/operations/lanes.ts (where SweepLaneId and the
queue keys already live), takes the DESCRIPTOR, reads `lane.queueKey`, and
returns null for an operation neither sweep runs. Null is a real answer:
OperationDetail renders it as a "no console here" panel — the band, the
dependency chain, the cost basis, and a sentence pointing at `op.hint` for what
does dispatch it. It emits no data-lane, so no test can scope lane controls to
a page that has none.
railStates' band loop asks THE PAYLOAD rather than the registry:
`lane.operations` is the live membership the server already computed, so it
cannot drift the way a second registry read could. A band in neither lane gets
no entry, and the existing contract supplies the rest — no lane reads "Off",
never a borrowed "Idle".
Registering `transcode` itself is not this commit.
tsc clean; common 822/822 (one new test: the two runners declare `runner`,
every sweep-dispatched kind leaves it unset).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat:
6 files changed, 192 insertions(+), 26 deletions(-)
diff --git a/common/lib/backfillKinds.test.ts b/common/lib/backfillKinds.test.ts
@@ -1197,6 +1197,23 @@ test("the catalog covers every operation, dispatched here or not", () => {
}
});
+test("`runner` names the auto-queue runner, and only for the two that have one", () => {
+ // The console reads this instead of asking whether the id happens to be
+ // "download" or "transcription". `dispatch` cannot answer it: both of those
+ // are `external` WITH a runner, and a future `transcode` would be `external`
+ // with none — so an id-shaped guess would hand transcode the transcription
+ // runner's controls.
+ const runners = new Map(operationCatalog().map((o) => [o.id, o.runner]));
+ assert.equal(runners.get("download"), "download");
+ assert.equal(runners.get("transcription"), "transcription");
+ // Everything the sweep dispatches must leave it unset — a backfill kind with
+ // a runner would render a runner console over a lane no runner feeds.
+ for (const op of operationCatalog()) {
+ if (op.id === "download" || op.id === "transcription") continue;
+ assert.equal(op.runner, undefined, `${op.id} declares a runner`);
+ }
+});
+
test("every catalogued operation declares a group and a cost basis", () => {
// Both are read unconditionally by the UI — the transit line groups stations
// by `group`, and every armed operation prints `costBasis` beside its
diff --git a/common/lib/backfillKinds.ts b/common/lib/backfillKinds.ts
@@ -69,6 +69,10 @@ import {
DIGEST_REMOTE_QUEUE,
TRANSCRIPTION_QUEUE,
} from "./queueKeys";
+// TYPE-ONLY, and that is what keeps this safe: the import is erased at compile
+// time, so naming the runner union here cannot create a cycle no matter what
+// jobs/autoQueueState.ts imports.
+import type { AutoQueueKind } from "../jobs/autoQueueState";
import {
DIGEST_FILENAME,
isDigestSectionKind,
@@ -1195,6 +1199,12 @@ export type ExternalOperation = {
lane: BackfillLane;
dependsOn?: readonly string[];
dispatch: "external";
+ // The auto-queue runner that dispatches this, when one does. `dispatch` alone
+ // cannot answer it: download and transcription are `external` WITH a runner,
+ // and a future `transcode` would be `external` with none. A console that
+ // guessed from the id would hand transcode the transcription runner's
+ // controls — a live Start button over the wrong lane.
+ runner?: AutoQueueKind;
};
export const EXTERNAL_OPERATIONS: readonly ExternalOperation[] = [
@@ -1210,6 +1220,7 @@ export const EXTERNAL_OPERATIONS: readonly ExternalOperation[] = [
// the dependency graph, not dispatch.
lane: { queueKey: "download:<platform>", contendsFor: "network" },
dispatch: "external",
+ runner: "download",
},
{
id: "transcription",
@@ -1221,6 +1232,7 @@ export const EXTERNAL_OPERATIONS: readonly ExternalOperation[] = [
lane: { queueKey: TRANSCRIPTION_QUEUE, contendsFor: "gpu" },
dependsOn: ["download"],
dispatch: "external",
+ runner: "transcription",
},
];
@@ -1236,6 +1248,9 @@ export type OperationDescriptor = {
lane: BackfillLane;
dependsOn?: readonly string[];
dispatch: BackfillDispatch;
+ // See ExternalOperation.runner. Absent for every backfill kind: those are
+ // dispatched by the sweep and the arbiter, not by a runner.
+ runner?: AutoQueueKind;
};
export function operationCatalog(): OperationDescriptor[] {
diff --git a/editor/app/operations/[id]/page.tsx b/editor/app/operations/[id]/page.tsx
@@ -4,10 +4,14 @@ import { getPaths } from "yt-dlp-transcript-common/lib/paths";
import { listChannelConfigs } from "yt-dlp-transcript-common/controller/channels";
import { PLATFORM_VALUES } from "yt-dlp-transcript-common/lib/platform";
import { selectableBucketsForKind } from "yt-dlp-transcript-common/jobs/autoQueuePolicy";
-import { operationCatalog } from "yt-dlp-transcript-common/lib/backfillKinds";
+import {
+ operationCatalog,
+ operationLabel,
+} from "yt-dlp-transcript-common/lib/backfillKinds";
import type { AutoQueueKind } from "yt-dlp-transcript-common/jobs/autoQueueState";
import { getRegistry } from "yt-dlp-transcript-common/jobs/registry";
import { buildAutoQueueStatusPayload } from "../status";
+import { sweepLaneIdFor, type SweepLaneId } from "../lanes";
import { OperationDetail } from "../components/OperationDetail";
export const dynamic = "force-dynamic";
@@ -27,7 +31,7 @@ const BUCKETS_BY_KIND = {
//
// Keyed by LANE rather than by operation, because that is the truth: one job
// on the shared queue is doing whichever kinds the sweep armed.
-const JOB_KINDS_BY_LANE: Record<string, readonly string[]> = {
+const JOB_KINDS_BY_LANE: Record<SweepLaneId, readonly string[]> = {
digest: [
"digest-sweep",
"digest-channel-local",
@@ -63,8 +67,14 @@ export default async function OperationPage({
const op = descriptorFor(id);
if (!op) notFound();
- const runnerKind: AutoQueueKind | null =
- id === "transcription" || id === "download" ? id : null;
+ // OFF THE DESCRIPTOR, never off the id. `dispatch` alone cannot answer this —
+ // download and transcription are `external` WITH a runner, and a future
+ // `transcode` would be `external` with none — so the registry states it and
+ // this reads it.
+ const runnerKind: AutoQueueKind | null = op.runner ?? null;
+ // Likewise for the sweep lane. Null for an operation neither sweep dispatches;
+ // OperationDetail renders that as "no console here" rather than borrowing one.
+ const laneId = sweepLaneIdFor(op);
const [initial, channels] = await Promise.all([
buildAutoQueueStatusPayload(),
@@ -75,9 +85,8 @@ export default async function OperationPage({
name: c.config.name ?? null,
}));
- const laneJobKinds = runnerKind
- ? []
- : (JOB_KINDS_BY_LANE[id === "digest" ? "digest" : "backfill"] ?? []);
+ const laneJobKinds =
+ runnerKind || !laneId ? [] : JOB_KINDS_BY_LANE[laneId];
const activeJobs =
laneJobKinds.length === 0
? []
@@ -116,6 +125,11 @@ export default async function OperationPage({
bucketsByKind={BUCKETS_BY_KIND}
activeJobs={activeJobs}
runnerKind={runnerKind}
+ laneId={laneId}
+ dependsOn={[...(op.dependsOn ?? [])].map((depId) => ({
+ id: depId,
+ label: operationLabel(depId),
+ }))}
/>
</div>
);
diff --git a/editor/app/operations/components/OperationDetail.tsx b/editor/app/operations/components/OperationDetail.tsx
@@ -2,11 +2,12 @@
import type { AutoQueueKind } from "yt-dlp-transcript-common/jobs/autoQueueState";
import type { AutoQueueStatusPayload } from "../status";
+import type { SweepLaneId } from "../lanes";
import { HowPriorityWorks } from "./HowPriorityWorks";
import { OperationRail } from "./OperationRail";
import { RunnerOperationView } from "./RunnerOperationView";
import { SweepLane } from "./SweepLane";
-import { railStates, sweepLaneIdFor } from "./railStates";
+import { railStates } from "./railStates";
import { useHydrated, useOperationsStatus } from "./useOperationsStatus";
import { type Channel } from "./dispatch";
import { StateBand } from "../../components/pipelines/StateBand";
@@ -30,6 +31,8 @@ export function OperationDetail({
bucketsByKind,
activeJobs,
runnerKind,
+ laneId,
+ dependsOn,
}: {
id: string;
initial: AutoQueueStatusPayload;
@@ -40,10 +43,17 @@ export function OperationDetail({
// all (RunningJobsList returns null), so a quiet page carries no second
// in-flight list beside the lane's own.
activeJobs: RunningJobsListItem[];
- // Set for the two operations a RUNNER dispatches; null for everything the
- // registry sweeps. Resolved on the server against operationCatalog() so this
- // component never re-decides which is which.
+ // Set for the operations a RUNNER dispatches; null for everything else.
+ // Resolved on the server FROM THE DESCRIPTOR (`op.runner`) so this component
+ // never re-decides which is which — and so an operation the registry gains
+ // gets the right answer without a branch here.
runnerKind: AutoQueueKind | null;
+ // The sweep lane that dispatches this, or null for one neither sweep runs.
+ // Also resolved on the server, off the descriptor's queue key.
+ laneId: SweepLaneId | null;
+ // This operation's declared inputs, labelled. Only rendered when there is no
+ // console — where it is most of what the page has to say.
+ dependsOn: { id: string; label: string }[];
}) {
const { data, refresh } = useOperationsStatus(initial);
@@ -68,13 +78,16 @@ export function OperationDetail({
onRefresh={refresh}
/>
</>
- ) : (
+ ) : laneId ? (
<SweepOperationView
id={id}
+ laneId={laneId}
data={data}
activeJobs={activeJobs}
onRefresh={refresh}
/>
+ ) : (
+ <NoConsoleView id={id} data={data} dependsOn={dependsOn} />
)}
</div>
);
@@ -90,17 +103,18 @@ export function OperationDetail({
// lane holding three operations has no single total that means anything.
function SweepOperationView({
id,
+ laneId,
data,
activeJobs,
onRefresh,
}: {
id: string;
+ laneId: SweepLaneId;
data: AutoQueueStatusPayload;
activeJobs: RunningJobsListItem[];
onRefresh: () => Promise<void>;
}) {
const hydrated = useHydrated();
- const laneId = sweepLaneIdFor(id);
const lane = data.lanes[laneId];
const band = data.lanes.bands.find((b) => b.id === id) ?? null;
// The other operations this lane would dispatch, named. Not a count: "shares
@@ -155,6 +169,70 @@ function SweepOperationView({
);
}
+// AN OPERATION WITH NO CONSOLE HERE — registered, real, and dispatched by
+// nothing this page can drive.
+//
+// The page it replaces was the reason this whole step exists. Before it, an
+// operation with no runner and no sweep lane fell through to the backfill
+// lane's console: its band, its plan, its scope, and a live START button that
+// would have armed THE BACKFILL SWEEP — a corpus-scale GPU commitment — under
+// this operation's heading. A console for the wrong lane is worse than none,
+// because every control on it works.
+//
+// So this says the four true things instead: what state the corpus is in (the
+// band), what has to happen first (dependsOn), what one unit costs, and — in a
+// sentence taken from the descriptor's own hint — what does dispatch it. That
+// last one is why `hint` is worth keeping honest: it is the whole answer here.
+function NoConsoleView({
+ id,
+ data,
+ dependsOn,
+}: {
+ id: string;
+ data: AutoQueueStatusPayload;
+ dependsOn: { id: string; label: string }[];
+}) {
+ const hydrated = useHydrated();
+ const band = data.lanes.bands.find((b) => b.id === id) ?? null;
+
+ return (
+ <section
+ // NO data-lane, deliberately: there is no lane, and emitting one would
+ // let a test scope lane controls to a page that has none.
+ data-operation={id}
+ data-no-console="true"
+ data-hydrated={hydrated ? "true" : undefined}
+ className="flex flex-col gap-4"
+ >
+ {band && (
+ <div className="flex flex-col gap-2 rounded-lg border border-border bg-card px-4 py-3">
+ <StateBand band={band} size="rail" />
+ </div>
+ )}
+
+ <div className="flex flex-col gap-2 rounded-lg border border-border bg-card px-4 py-3">
+ <h2 className="font-display text-sm font-semibold tracking-tight">
+ Not dispatched from here
+ </h2>
+ <p className="text-sm text-muted-foreground">
+ Neither auto-queue runner nor either sweep lane runs this operation,
+ so there is nothing on this page to start or hold. The figures above
+ are the corpus state; see the description at the top of the page for
+ what does dispatch it.
+ </p>
+ {dependsOn.length > 0 && (
+ <p className="text-sm text-muted-foreground">
+ Needs{" "}
+ {joinLabels(dependsOn.map((d) => d.label))} first — a video without
+ that is counted under “needs re-acquiring”, not under
+ work that is ready.
+ </p>
+ )}
+ </div>
+ </section>
+ );
+}
+
// "A", "A and B", "A, B and C". Written out because the sentence it lands in is
// the one that keeps an operator from reading a per-operation page as a
// per-operation switch.
diff --git a/editor/app/operations/components/railStates.ts b/editor/app/operations/components/railStates.ts
@@ -1,5 +1,5 @@
import type { AutoQueueStatusPayload } from "../status";
-import type { SweepLaneStatus } from "../lanes";
+import type { SweepLaneId, SweepLaneStatus } from "../lanes";
import { deriveLaneState } from "../../components/lanes/laneState";
import type { RailLaneState } from "./OperationRail";
import { idleReasonText } from "./dispatch";
@@ -57,20 +57,39 @@ export function railStates(
});
out.digest = sweepState(data.lanes.digest);
- // Every OTHER catalog operation is dispatched by the one backfill lane, so it
- // takes that lane's state — including the ones with no band of their own.
- const backfill = sweepState(data.lanes.backfill);
+
+ // Each remaining band takes the state of the lane THAT BAND'S OPERATION
+ // actually rides — asked of the payload, not assumed from the id.
+ //
+ // This loop used to hand every stateless band the backfill lane's state, on
+ // the same reasoning the old id-shaped sweepLaneIdFor used: everything that
+ // is not digest is backfill. Both were true only because those are the only
+ // two lanes registered TODAY. An operation on a third queue would still get a
+ // band (the rail is built from allBackfillKinds — every switched-on kind,
+ // whatever its queue) and would then have advertised a hold belonging to a
+ // lane that would never dispatch it.
+ //
+ // `lane.operations` is the live membership the server already computed, so
+ // this cannot drift from it the way a second registry read could. A band in
+ // NEITHER lane gets no entry at all, and railStates' contract above supplies
+ // the rest: no lane means "Off", never a borrowed "Idle".
+ const laneStates: Record<SweepLaneId, RailLaneState> = {
+ digest: out.digest,
+ backfill: sweepState(data.lanes.backfill),
+ };
+ const laneIdOf = (operationId: string): SweepLaneId | null => {
+ for (const laneId of ["digest", "backfill"] as const) {
+ if (data.lanes[laneId].operations.some((op) => op.id === operationId)) {
+ return laneId;
+ }
+ }
+ return null;
+ };
for (const band of data.lanes.bands) {
if (out[band.id]) continue;
- out[band.id] = backfill;
+ const laneId = laneIdOf(band.id);
+ if (!laneId) continue;
+ out[band.id] = laneStates[laneId];
}
return out;
}
-
-// The lane that dispatches an operation, in the two ids the payload is keyed
-// by. Digest has its own queue; everything else the registry dispatches rides
-// the shared backfill lane. Stated once, because the page that renders a lane
-// and the page that names it must not answer this differently.
-export function sweepLaneIdFor(operationId: string): "digest" | "backfill" {
- return operationId === "digest" ? "digest" : "backfill";
-}
diff --git a/editor/app/operations/lanes.ts b/editor/app/operations/lanes.ts
@@ -9,6 +9,7 @@ import {
operationLabel,
operationsActionLabel,
operationsGroupLabel,
+ type OperationDescriptor,
} from "yt-dlp-transcript-common/lib/backfillKinds";
import { buildSweepChannelCounts } from "yt-dlp-transcript-common/controller/sweepPreview";
import { loadSweepDates } from "yt-dlp-transcript-common/controller/sweepRecency";
@@ -49,6 +50,28 @@ import {
export type SweepLaneId = "digest" | "backfill";
+// The sweep lane that dispatches an operation — or NULL for one neither sweep
+// runs. Takes the DESCRIPTOR, and reads its queue key.
+//
+// The id-shaped version of this ("digest" ? digest : backfill) answered
+// "backfill" for every operation that was not digest, including ones no sweep
+// dispatches at all. It was right only by coincidence of today's registry: a
+// registered `transcode` — external, no runner — would have rendered the
+// backfill sweep's whole console, with a live Start button ARMING THE BACKFILL
+// SWEEP, under a "Transcode" heading. The queue key is what the arbiter
+// actually dispatches on, so it is what this asks.
+//
+// Null is a real answer, not an error. A caller must render it as "no console
+// here" rather than falling back to a lane.
+export function sweepLaneIdFor(
+ op: Pick<OperationDescriptor, "lane"> | null | undefined,
+): SweepLaneId | null {
+ const key = op?.lane.queueKey;
+ if (key === DIGEST_LOCAL_QUEUE || key === DIGEST_REMOTE_QUEUE) return "digest";
+ if (key === BACKFILL_QUEUE) return "backfill";
+ return null;
+}
+
export type SweepLaneStatus = {
id: SweepLaneId;
label: string;