commit d35e8b52ba7193518ab541914ccf339574180f68
parent 595337e5bbdc6dbc802f7ac09198d97b5103ab45
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 12 Sep 2026 12:02:38 -0400
common: the four legacy pause fields are gone, and so is the migration
S0-pause, per one-core-phase-1.md:1130-1139. `transcriptionsPaused`,
`downloadsPaused`, `digest.digestsPaused` and the inverted `backfill.enabled`
leave `SiteSettings`, their sanitizers, their defaults and `writeSettings`'
output literal; `legacyGateHeld` and `migrateHeldToLanes` leave
`lib/laneMigration.ts` (`migrateSweepsToLanes`, slice 1.3's, stays untouched).
`isGateHeld` keeps its signature and reads `autoQueue[lane].held` alone.
THE PRECONDITION HELD. The live settings.json carries all four `held` keys
(transcription/download true, digest/backfill false, checked 2026-09-12), so no
pause is dropped by the deletion. It still spells the legacy fields
(`transcriptionsPaused: true`, `downloadsPaused: false`, `digestsPaused: false`,
`backfill.enabled: true`) and loses them on its next write, because
`writeSettings` builds from only the known operational fields.
ABSENT IS NOT HELD is now the whole rule, and the comments that justified the
opposite (`AutoQueuePolicy.held`, `sanitizePolicy`, `pauseGates.ts`) say why:
there is no second field left to ask, and defaulting a missing key to `true`
would hold four lanes on every fixture that never mentioned a pause.
Tests: the four pause-migration cases in `jobs/laneMigration.test.ts` go with
the function they covered. `pauseGates.test.ts` swaps its two fallback/precedence
cases for the deletion from both ends — a settings object still spelling all four
retired fields holds nothing, and no sanitizer carries one back onto disk. The
two controller pause cases hold their lane through `withGateHeld` instead of
through a retired field. common tests 1077 -> 1073.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
12 files changed, 162 insertions(+), 333 deletions(-)
diff --git a/common/controller/autoRunner.ts b/common/controller/autoRunner.ts
@@ -211,7 +211,7 @@ export type AutoRunnerIdleReason =
// no-workers: pauseAll disables every worker, so by slot count the two look
// identical, and only one of them is fixed by enabling a worker.
| "workers-paused"
- // The lane's own gate is shut (digestsPaused, or backfill.enabled false).
+ // The lane's own gate is shut (`autoQueue[lane].held`).
// DISTINCT from downloads-paused, which names one specific flag, and from
// capped, which is contention rather than intent — the fix for this one is a
// click on the Resume button beside it.
diff --git a/common/controller/laneGuards.test.ts b/common/controller/laneGuards.test.ts
@@ -22,11 +22,22 @@ function settingsWith(patch: Partial<SiteSettings["digest"]>): SiteSettings {
return { digest: { ...defaultDigest(), ...patch } } as SiteSettings;
}
+// The same partial object with the digest lane's gate shut. The gate is
+// `autoQueue.digest.held` and nothing else since S0-pause deleted
+// `digest.digestsPaused`; `isGateHeld` reads `autoQueue` with `?.`, which is
+// what lets a {digest}-only cast keep working everywhere else in this file.
+function heldWith(patch: Partial<SiteSettings["digest"]> = {}): SiteSettings {
+ return {
+ digest: { ...defaultDigest(), ...patch },
+ autoQueue: { digest: { held: true } },
+ } as unknown as SiteSettings;
+}
+
test("a pause holds the lane, and beats every other reason", () => {
const gate = digestGate({
// Paused AND over a spend cap: an operator who paused the lane must be told
// about the pause, not sent to look at a billing setting.
- settings: settingsWith({ digestsPaused: true, spendCapUsd: 1 }),
+ settings: heldWith({ spendCapUsd: 1 }),
appLane: "remote-api",
metered: true,
costUsd: 99,
diff --git a/common/controller/laneGuards.ts b/common/controller/laneGuards.ts
@@ -12,7 +12,7 @@ import { transcriptionActivity } from "./digestYield";
//
// This exists because of a sentence in backfillLaneOperations' own header:
//
-// "digest carries digestsPaused, the yield-to-transcription carve-out (with
+// "digest carries its own pause gate, the yield-to-transcription carve-out (with
// its CPU-worker exemption), spendCapUsd on the metered lane, the
// remoteEnabled fail-fast, the engine probe() fail-fast … and not one of
// them is expressible as backfillLimit()'s single scalar."
diff --git a/common/controller/operationBatch.test.ts b/common/controller/operationBatch.test.ts
@@ -17,6 +17,7 @@ import {
type SiteSettings,
} from "../lib/settings";
import type { Operation, OperationClassification } from "../lib/operations";
+import { withGateHeld } from "../lib/pauseGates";
// Run with:
// pnpm --filter yt-dlp-transcript-common exec tsx --test common/controller/operationBatch.test.ts
@@ -59,8 +60,13 @@ test("the shipped default is off, and holds no disk", () => {
// Stated as a test because these are promises the feature makes: nothing runs
// until an operator says so, and nothing re-downloads media without an
// explicit opt-in.
+ //
+ // "Off" is now the LANE'S ARM, `autoQueue.backfill.enabled`. The
+ // `backfill.enabled` this line used to read was the lane's inverted pause
+ // field, which slice 1.4 moved to `autoQueue.backfill.held` and S0-pause
+ // deleted; an unarmed lane dispatches nothing whatever its gate says.
+ assert.equal(defaultSiteSettings().autoQueue.backfill.enabled, false);
const d = defaultBackfill();
- assert.equal(d.enabled, false);
assert.equal(d.allowRedownload, false);
assert.equal(d.concurrency, 1);
// Concurrency shares clampPositiveInt's floor of 1, so a hand-edited 0 cannot
@@ -246,9 +252,13 @@ function settingsWith(over: Partial<SiteSettings>): SiteSettings {
return { ...defaultSiteSettings(), ...over };
}
-test("the backfill lane holds when the lane switch is off, and says so", () => {
+test("the backfill lane holds when its gate is shut, and says so", () => {
+ // THE GATE IS `autoQueue.backfill.held`. It was the inverted
+ // `backfill.enabled` — the lane's "master switch" — until slice 1.4 moved it
+ // onto the lane and S0-pause deleted the field; `withGateHeld` is the one
+ // writer, so this is the shape every control produces.
const verdict = laneLimit(
- settingsWith({ backfill: { ...defaultBackfill(), enabled: false } }),
+ withGateHeld(settingsWith({ backfill: defaultBackfill() }), "backfill", true),
{
lane: "backfill",
operations: [],
@@ -267,7 +277,7 @@ test("the backfill lane holds when the lane switch is off, and says so", () => {
test("an enabled backfill lane with no GPU-bound operation runs at its slots", () => {
const verdict = laneLimit(
settingsWith({
- backfill: { ...defaultBackfill(), enabled: true, concurrency: 3 },
+ backfill: { ...defaultBackfill(), concurrency: 3 },
}),
{
lane: "backfill",
@@ -292,7 +302,7 @@ test("the GPU carve-out is keyed on contendsFor, not on laneFor existing", () =>
// `lane: { contendsFor: "gpu" }` and NO laneFor is caught, and one with a
// laneFor resolving to CPU is not.
const settings = settingsWith({
- backfill: { ...defaultBackfill(), enabled: true, concurrency: 4 },
+ backfill: { ...defaultBackfill(), concurrency: 4 },
});
const cpuOnly = laneLimit(settings, {
lane: "backfill",
@@ -319,7 +329,7 @@ test("the GPU carve-out is keyed on contendsFor, not on laneFor existing", () =>
assert.equal(gpuBound.limit, 4, "idle: nothing is transcribing in this process");
const weightless = laneLimit(
settingsWith({
- backfill: { ...defaultBackfill(), enabled: true, concurrency: 4 },
+ backfill: { ...defaultBackfill(), concurrency: 4 },
}),
{
lane: "backfill",
@@ -337,7 +347,7 @@ test("the GPU carve-out is keyed on contendsFor, not on laneFor existing", () =>
test("the digest lane holds while digests are paused", () => {
const verdict = laneLimit(
- settingsWith({ digest: { ...defaultDigest(), digestsPaused: true } }),
+ withGateHeld(settingsWith({ digest: defaultDigest() }), "digest", true),
{
lane: "digest",
appLane: "local-gpu",
@@ -352,14 +362,12 @@ test("the digest lane holds while digests are paused", () => {
assert.equal(verdict.hold?.reason, "paused");
});
-// HELD IS A HOLD, NEVER A STOP — at the runner, through the new key.
+// HELD IS A HOLD, NEVER A STOP — at the runner, both lanes at once.
//
-// The two tests above hold their lane through a RETIRED field (`backfill.
-// enabled`, `digest.digestsPaused`); this one holds it through
-// `autoQueue[lane].held`, which is where slice 1.4 put the gate. Both routes
-// have to reach the same zero, because a settings file can spell either one:
-// the retired fields are the migration's input until every lane's key is on
-// disk.
+// The two tests above each hold ONE lane through `autoQueue[lane].held`; this
+// one walks both and pins that the other lane is untouched by it. There is no
+// second spelling to cross-check any more: slice 1.4 moved the gate onto the
+// lane and S0-pause deleted the four retired fields it had migrated from.
//
// WHAT "A HOLD" MEANS HERE is `limit: 0` with `hold.reason === "paused"`, which
// is what makes runPool idle-wait and the runner report `lane-held` while
@@ -374,10 +382,7 @@ function heldLane(lane: "digest" | "backfill", held: boolean): SiteSettings {
const base = defaultSiteSettings();
return {
...base,
- // The retired fields say the OPPOSITE of `held`, so a reader that still
- // consulted them would fail this in both directions.
- digest: { ...defaultDigest(), digestsPaused: !held },
- backfill: { ...defaultBackfill(), enabled: held, concurrency: 2 },
+ backfill: { ...defaultBackfill(), concurrency: 2 },
autoQueue: {
...base.autoQueue,
[lane]: { ...base.autoQueue[lane], held },
diff --git a/common/jobs/autoQueuePolicy.ts b/common/jobs/autoQueuePolicy.ts
@@ -721,13 +721,12 @@ function sanitizePolicy(value: unknown, lane: AutoQueueKind): AutoQueuePolicy {
? defaultOrderFor(lane)
: sanitizeAutoQueueOrder(r.order),
snoozeUntil: sanitizeSnooze(r.snoozeUntil),
- // A BOOLEAN OR NOTHING, and deliberately NOT defaulted. `held` is the lane's
- // pause gate (AutoQueuePolicy.held); every settings.json written before
- // slice 1.4 carries the four legacy pause fields instead, and `undefined`
- // is what makes `isGateHeld` fall back to them. Defaulting it to false here
- // would read a paused corpus as running — the one bug this field could
- // introduce. `undefined` also never reaches the file: JSON.stringify drops
- // it, so a lane that has not been through getSettings' copy stays absent.
+ // A BOOLEAN OR NOTHING. `held` is the lane's pause gate
+ // (AutoQueuePolicy.held) and the only spelling of one since S0-pause
+ // deleted the four legacy fields it migrated from. Not defaulted: an
+ // absent key reads as not held anyway, and `undefined` never reaches the
+ // file (JSON.stringify drops it), so a lane nobody has paused stays absent
+ // rather than gaining a `"held": false` the operator did not write.
held: typeof r.held === "boolean" ? r.held : undefined,
root:
r.root === undefined
diff --git a/common/jobs/laneMigration.test.ts b/common/jobs/laneMigration.test.ts
@@ -2,12 +2,8 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import {
laneRootFromScope,
- migrateHeldToLanes,
migrateSweepsToLanes,
} from "../lib/laneMigration";
-import { isGateHeld } from "../lib/pauseGates";
-import { defaultSiteSettings, type SiteSettings } from "../lib/settings";
-import { LANES, type AutoQueueSettings } from "../lib/autoQueueTypes";
import { sanitizeAutoQueue } from "./autoQueuePolicy";
// The migration is asserted THROUGH THE SANITIZER, because that is how it is
@@ -301,93 +297,3 @@ test("blank and duplicate scope entries are dropped rather than becoming leaves"
],
);
});
-
-// ---------------------------------------------------------------------------
-// The pause fields' migration (slice 1.4): four flags become one key per lane.
-
-// A settings object shaped like the live file: the four RETIRED pause fields
-// spelled, no `held` anywhere. Everything else is the default.
-function withLegacyPauses(over: Partial<SiteSettings> = {}): SiteSettings {
- const base = defaultSiteSettings();
- return {
- ...base,
- transcriptionsPaused: true,
- downloadsPaused: false,
- digest: { ...base.digest, digestsPaused: false },
- // The live file's value, and the one that makes this a real test: `enabled`
- // is INVERTED, so `true` here must come out as `held: false`.
- backfill: { ...base.backfill, enabled: true },
- ...over,
- };
-}
-
-const heldByLane = (autoQueue: AutoQueueSettings) =>
- Object.fromEntries(LANES.map((l) => [l, autoQueue[l].held]));
-
-test("the live shape: transcriptionsPaused becomes transcription.held", () => {
- const settings = withLegacyPauses();
- // The precondition — nothing carries a `held` before the copy, which is what
- // makes the fallback in isGateHeld the thing answering today.
- assert.deepEqual(heldByLane(settings.autoQueue), {
- transcription: undefined,
- download: undefined,
- digest: undefined,
- backfill: undefined,
- });
- assert.deepEqual(heldByLane(migrateHeldToLanes(settings)), {
- transcription: true,
- download: false,
- digest: false,
- backfill: false,
- });
-});
-
-test("a lane that already carries `held` is untouched, false included", () => {
- // `false` is an ANSWER, not an absence. Coercing it would let a retired field
- // resurrect a pause an operator had lifted — and there is no version marker
- // to tell "migrated to false" from "never migrated" apart from this.
- const base = withLegacyPauses();
- const settings: SiteSettings = {
- ...base,
- autoQueue: {
- ...base.autoQueue,
- transcription: { ...base.autoQueue.transcription, held: false },
- backfill: { ...base.autoQueue.backfill, held: true },
- },
- };
- assert.deepEqual(heldByLane(migrateHeldToLanes(settings)), {
- transcription: false,
- download: false,
- digest: false,
- // Kept, though `backfill.enabled: true` says the opposite.
- backfill: true,
- });
-});
-
-test("the pause migration is idempotent, by identity on the second pass", () => {
- const settings = withLegacyPauses();
- const once = migrateHeldToLanes(settings);
- const twice = migrateHeldToLanes({ ...settings, autoQueue: once });
- assert.equal(twice, once, "a migrated file must not be rebuilt");
-});
-
-test("the pause migration never unholds a lane", () => {
- // Every combination of the four retired fields: the copied value is exactly
- // what isGateHeld was already returning, so no reading of a settings file can
- // change on the day this runs.
- for (let mask = 0; mask < 16; mask++) {
- const base = defaultSiteSettings();
- const settings: SiteSettings = {
- ...base,
- transcriptionsPaused: (mask & 1) !== 0,
- downloadsPaused: (mask & 2) !== 0,
- digest: { ...base.digest, digestsPaused: (mask & 4) !== 0 },
- backfill: { ...base.backfill, enabled: (mask & 8) === 0 },
- };
- const before = LANES.map((l) => isGateHeld(settings, l));
- const after = LANES.map((l) =>
- isGateHeld({ ...settings, autoQueue: migrateHeldToLanes(settings) }, l),
- );
- assert.deepEqual(after, before, `mask ${mask}`);
- }
-});
diff --git a/common/lib/autoQueueTypes.ts b/common/lib/autoQueueTypes.ts
@@ -114,15 +114,17 @@ export type AutoQueuePolicy = {
// lib/pauseGates.ts, whose limit()/guard returns 0 so runPool idle-waits. A
// hold, never a stop — see that file's header.
//
- // OPTIONAL, AND ABSENT IS NOT `false`. Until slice 1.4 four separate settings
- // fields carried this — `transcriptionsPaused`, `downloadsPaused`,
- // `digest.digestsPaused` and (inverted) `backfill.enabled` — and every
- // settings.json in existence still spells those and not this. So `undefined`
- // means "ask the legacy field", which is exactly what `isGateHeld` does, and
- // the sanitizer must NOT default it to false: doing so would read every
- // pre-1.4 file as "no lane held" and quietly resume a paused corpus.
- // `getSettings` copies the legacy answer onto this key on read, so the next
- // write persists it and the fallback stops being consulted.
+ // OPTIONAL, and absent now means NOT HELD. Until slice 1.4 four separate
+ // settings fields carried this — `transcriptionsPaused`, `downloadsPaused`,
+ // `digest.digestsPaused` and (inverted) `backfill.enabled` — so `undefined`
+ // meant "ask the legacy field" and the sanitizer deliberately refused to
+ // default it. S0-pause deleted those four, on the precondition that the live
+ // settings.json already carried every `held` key, so there is nothing left to
+ // ask: `isGateHeld` reads this and only this.
+ //
+ // STILL NOT DEFAULTED, for a smaller reason: `undefined` never reaches the
+ // file (JSON.stringify drops it), so a lane that has never been paused stays
+ // absent rather than gaining a `"held": false` the operator did not write.
held?: boolean;
root: AutoQueueGroup;
};
diff --git a/common/lib/laneMigration.ts b/common/lib/laneMigration.ts
@@ -1,9 +1,15 @@
-// THE RETIRED FIELDS, ON READ. Two migrations live here, one per slice, and
-// they share a shape: a settings key that used to live somewhere else is filled
-// in from the field it replaced, in getSettings, when — and only when — the new
-// spelling is absent. Slice 1.3's is the sweeps' scope becoming a lane's tree;
-// slice 1.4's is four pause flags becoming one `held` per lane, at the foot of
-// the file.
+// THE RETIRED FIELDS, ON READ. A settings key that used to live somewhere else
+// is filled in from the field it replaced, in getSettings, when — and only when
+// — the new spelling is absent. What survives here is slice 1.3's: the sweeps'
+// scope becoming a lane's tree.
+//
+// SLICE 1.4's PAUSE MIGRATION USED TO LIVE AT THE FOOT OF THIS FILE and is
+// gone (slice S0-pause). It copied `transcriptionsPaused`, `downloadsPaused`,
+// `digest.digestsPaused` and the inverted `backfill.enabled` onto
+// `autoQueue[lane].held` for one release; those four fields no longer exist in
+// `SiteSettings`, so there is nothing left to copy and `isGateHeld` reads the
+// key alone. A settings.json still spelling one is read past and loses it on
+// the next write.
//
// THE SWEEPS' LAST ACT: their persisted scope becomes a lane's tree.
//
@@ -45,14 +51,10 @@
// would be a second implementation of the sanitizer, and the two would drift.
import {
- LANES,
type AutoQueueGroup,
- type AutoQueueKind,
type AutoQueueLeaf,
type AutoQueueMatch,
- type AutoQueueSettings,
} from "./autoQueueTypes";
-import type { SiteSettings } from "./settings";
// A lane's scope in the terms the sweeps used: some channels, some operations.
// Both empty means "everything", which is what an unscoped sweep meant.
@@ -213,74 +215,3 @@ export function migrateSweepsToLanes(
return autoQueue;
}
-
-// ---------------------------------------------------------------------------
-// THE PAUSE FIELDS' LAST ACT: four settings flags become one key per lane.
-//
-// Same shape as the sweep migration above, one slice later and one level down.
-// A lane's gate lived in four unrelated fields with three different polarities
-// (`transcriptionsPaused`, `downloadsPaused`, `digest.digestsPaused`, and
-// `backfill.enabled` INVERTED); it lives on `autoQueue[lane].held` now, which
-// is where every other per-lane switch already lives.
-//
-// WHY THE LEGACY READ LIVES HERE AND NOT IN pauseGates.ts, which is otherwise
-// the only place polarity is known: `getSettings` has to run this on every read
-// of settings.json, and pauseGates.ts imports lib/operations.ts (for
-// pauseLaneFor), which imports the controller layer. Pulling that into every
-// reader of settings.json — bin/ scripts, the MCP server, the export build —
-// to fill in one boolean would be a much larger change than the boolean is
-// worth. So the RETIRED fields are read here, beside the other retired fields,
-// and `isGateHeld` delegates to this function for its fallback. There is still
-// exactly one place that knows the inversion.
-
-// The gate as the four RETIRED fields spell it. Touches only the named lane's
-// field: laneGuards.test.ts casts a `{ digest }`-only object to SiteSettings,
-// so a reader that reached for `settings.backfill` on the way past would throw.
-export function legacyGateHeld(
- settings: SiteSettings,
- lane: AutoQueueKind,
-): boolean {
- switch (lane) {
- case "transcription":
- // The PERSISTED intent, not the live pool. See pauseGates.ts's header.
- return settings.transcriptionsPaused === true;
- case "download":
- return settings.downloadsPaused === true;
- case "digest":
- return settings.digest.digestsPaused === true;
- case "backfill":
- // INVERTED, and this line is why pauseGates.ts exists.
- return settings.backfill.enabled === false;
- }
-}
-
-// Copy the legacy answer onto `autoQueue[lane].held` for every lane that does
-// not already carry one, so the next write persists it and the fallback stops
-// being consulted.
-//
-// THREE RULES, the same three the sweep migration keeps:
-//
-// 1. IT NEVER UNHOLDS A LANE. The value copied is the one `isGateHeld` was
-// already returning, so this moves where the answer is stored and never
-// what it is. The live corpus has `transcriptionsPaused: true`; a
-// migration that read it as "not held" would resume the GPU by itself.
-// 2. A LANE THAT ALREADY CARRIES `held` IS UNTOUCHED — including a `false`,
-// which is a real answer and not an absence. Idempotent by construction.
-// 3. IT RETURNS THE INPUT OBJECT WHEN NOTHING CHANGED, so "this file was
-// already migrated" is identity, not a deep compare.
-//
-// Takes the MERGED, sanitized settings rather than the parsed file: unlike the
-// sweep migration, "absent" here is a property of the sanitized policy (which
-// deliberately leaves `held` undefined) and the legacy fields it reads have
-// been through their own sanitizers by this point.
-export function migrateHeldToLanes(settings: SiteSettings): AutoQueueSettings {
- const out = { ...settings.autoQueue };
- let changed = false;
- for (const lane of LANES) {
- const policy = out[lane];
- if (!policy || typeof policy.held === "boolean") continue;
- out[lane] = { ...policy, held: legacyGateHeld(settings, lane) };
- changed = true;
- }
- return changed ? out : settings.autoQueue;
-}
diff --git a/common/lib/operations.ts b/common/lib/operations.ts
@@ -1615,7 +1615,7 @@ export function allOperations(settings: SiteSettings): Operation[] {
// backfillLimit(). Handing it digest would SERIALIZE the GPU digest lane
// behind CPU diarization, when the entire reason they hold separate queue
// keys is that they currently overlap.
-// - GUARDS. digest carries digestsPaused, the yield-to-transcription
+// - GUARDS. digest carries its own pause gate, the yield-to-transcription
// carve-out (with its CPU-worker exemption), spendCapUsd on the metered
// lane, the remoteEnabled fail-fast, shortest-first ordering,
// duplicate-cluster sharing and the engine probe() fail-fast. Those are
diff --git a/common/lib/pauseGates.test.ts b/common/lib/pauseGates.test.ts
@@ -1,8 +1,11 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
+ defaultBackfill,
defaultDigest,
defaultSiteSettings,
+ sanitizeBackfill,
+ sanitizeDigest,
type SiteSettings,
} from "./settings";
import { operationCatalog } from "./operations";
@@ -16,14 +19,16 @@ import {
// Run with: node_modules/.bin/tsx --test common/lib/pauseGates.test.ts
//
// What these pin is the thing the file exists to stop: a second opinion about
-// polarity. `backfill.enabled` is inverted and three readers used to spell that
-// inversion for themselves, so the round trip and the explicit fallback case
-// below are the contract, not decoration.
+// polarity. `backfill.enabled` was inverted and three readers used to spell
+// that inversion for themselves, so the round trip below is the contract, not
+// decoration.
//
-// Since slice 1.4 there are TWO places a gate can be spelled — `autoQueue[lane]
-// .held` and the retired field it replaced — which makes precedence part of
-// that contract: the key wins, an absent key asks the field, and a write never
-// touches the field. Those are the three tests after the round trip.
+// Slice 1.4 moved the gate onto `autoQueue[lane].held` and read the four
+// retired fields as a fallback; S0-pause deleted them once the live
+// settings.json carried every key. So there is ONE place a gate can be spelled,
+// and the two tests after the round trip pin the deletion from both ends: a
+// file that still spells a retired field cannot hold a lane with it, and no
+// sanitizer will carry that field back onto disk.
const LANES: PauseLane[] = [
"transcription",
@@ -51,53 +56,61 @@ test("held round-trips through withGateHeld on every lane", () => {
}
});
-test("a lane with no `held` falls back to its retired field, inversion and all", () => {
- // The migration input, pinned. Every settings.json written before slice 1.4
- // spells these four and no `held`, so this is the answer the whole corpus is
- // read with until its first write — and backfill's is INVERTED.
+test("a settings.json still spelling a retired pause field cannot hold a lane", () => {
+ // THE DELETION, FROM THE READ SIDE. Until S0-pause an absent `held` asked
+ // four fields — `transcriptionsPaused`, `downloadsPaused`,
+ // `digest.digestsPaused` and the INVERTED `backfill.enabled`. They are gone
+ // from SiteSettings, so a hand-edited or long-unwritten file that still
+ // carries them is read past: absent is not held, and there is no second
+ // opinion left to disagree with the key.
+ //
+ // The precondition that made this safe is recorded in one-core-phase-2.md
+ // §S0-pause: the live settings.json carried all four `held` keys before the
+ // fields went, so nothing that was paused could come back running.
const base = defaultSiteSettings();
for (const lane of LANES) {
assert.equal(base.autoQueue[lane].held, undefined, `${lane} defaults held`);
}
- const legacy: SiteSettings = {
+ const stale = {
...base,
transcriptionsPaused: true,
- downloadsPaused: false,
+ downloadsPaused: true,
digest: { ...base.digest, digestsPaused: true },
- // enabled TRUE is NOT held — the one line the whole file exists for.
- backfill: { ...base.backfill, enabled: true },
- };
- assert.equal(isGateHeld(legacy, "transcription"), true);
- assert.equal(isGateHeld(legacy, "download"), false);
- assert.equal(isGateHeld(legacy, "digest"), true);
- assert.equal(isGateHeld(legacy, "backfill"), false);
-});
-
-test("`held` wins over the retired field, in both directions", () => {
- // Once the key is on disk the retired field is dead config. A reader that
- // still consulted it would resurrect a pause an operator had lifted (or the
- // reverse), which is precisely why every writer of those fields was rewritten
- // in the same slice.
- const base = defaultSiteSettings();
- const disagreeing: SiteSettings = {
- ...base,
- transcriptionsPaused: true,
- digest: { ...base.digest, digestsPaused: true },
- backfill: { ...base.backfill, enabled: true },
- };
- const held = withGateHeld(
- withGateHeld(withGateHeld(disagreeing, "transcription", false), "digest", false),
- "backfill",
+ // `false` was HELD under the old inversion, which is the value most likely
+ // to be misread by anything that kept looking.
+ backfill: { ...base.backfill, enabled: false },
+ } as unknown as SiteSettings;
+ for (const lane of LANES) {
+ assert.equal(isGateHeld(stale, lane), false, `${lane} read a retired field`);
+ }
+ // And the key still answers on the same object: the stale fields are inert,
+ // not consulted-and-outranked.
+ assert.equal(isGateHeld(withGateHeld(stale, "digest", true), "digest"), true);
+ assert.equal(
+ isGateHeld(withGateHeld(stale, "backfill", true), "backfill"),
true,
);
- assert.equal(isGateHeld(held, "transcription"), false);
- assert.equal(isGateHeld(held, "digest"), false);
- assert.equal(isGateHeld(held, "backfill"), true);
- // And the retired fields are NOT rewritten to agree: withGateHeld writes the
- // new key only.
- assert.equal(held.transcriptionsPaused, true);
- assert.equal(held.digest.digestsPaused, true);
- assert.equal(held.backfill.enabled, true);
+});
+
+test("no sanitizer carries a retired pause field back onto disk", () => {
+ // THE DELETION, FROM THE WRITE SIDE. `writeSettings` builds its output from
+ // ONLY the known operational fields — `sanitizeDigest`, `sanitizeBackfill`
+ // and an explicit top-level literal — so a field no sanitizer names cannot
+ // survive a save. That is what stops a stale file resurrecting a pause on the
+ // next write, and it is the half a read-side test cannot see.
+ const digest = sanitizeDigest({
+ ...defaultDigest(),
+ digestsPaused: true,
+ } as unknown) as Record<string, unknown>;
+ assert.equal("digestsPaused" in digest, false);
+ const backfill = sanitizeBackfill({
+ ...defaultBackfill(),
+ enabled: false,
+ } as unknown) as Record<string, unknown>;
+ assert.equal("enabled" in backfill, false);
+ const base = defaultSiteSettings() as unknown as Record<string, unknown>;
+ assert.equal("transcriptionsPaused" in base, false);
+ assert.equal("downloadsPaused" in base, false);
});
test("holding a lane leaves the rest of its policy alone", () => {
@@ -149,11 +162,16 @@ test("holding a lane leaves the rest of its policy alone", () => {
);
});
-test("isGateHeld touches only its own lane's field", () => {
- // laneGuards.test.ts casts a {digest}-only object to SiteSettings, so a
- // reader that reached for settings.backfill on the way past would throw.
- const partial = { digest: { ...defaultDigest(), digestsPaused: true } } as SiteSettings;
+test("isGateHeld answers for a partial settings object", () => {
+ // laneGuards.test.ts casts a {digest, autoQueue}-only object to SiteSettings,
+ // so a reader that reached for settings.backfill on the way past would throw.
+ // `autoQueue` itself is read with `?.` for the same reason.
+ const partial = {
+ digest: { ...defaultDigest() },
+ autoQueue: { digest: { held: true } },
+ } as unknown as SiteSettings;
assert.equal(isGateHeld(partial, "digest"), true);
+ assert.equal(isGateHeld({} as SiteSettings, "digest"), false);
});
test("pauseLaneFor answers for every catalog id", () => {
diff --git a/common/lib/pauseGates.ts b/common/lib/pauseGates.ts
@@ -1,6 +1,5 @@
import type { SiteSettings } from "./settings";
import type { AutoQueueKind } from "./autoQueueTypes";
-import { legacyGateHeld } from "./laneMigration";
import { operationCatalog } from "./operations";
import {
BACKFILL_QUEUE,
@@ -12,11 +11,12 @@ import {
//
// Four lanes can be held. Their flag lived in four unrelated settings fields
// read with three different polarities in nine places — `backfill.enabled` in
-// particular is INVERTED, held means false, and every reader that forgot it
+// particular was INVERTED, held meant false, and every reader that forgot it
// reported a held lane as idle. This file was the one place that polarity was
-// known; since slice 1.4 the gate is ONE KEY ON THE LANE, `autoQueue[lane].held`,
-// and the four fields survive only as this file's read-time fallback (defined,
-// with the inversion, in lib/laneMigration.ts beside the other retired fields).
+// known; since slice 1.4 the gate is ONE KEY ON THE LANE,
+// `autoQueue[lane].held`, and since S0-pause the four fields are DELETED: they
+// are not in `SiteSettings`, no sanitizer keeps them, and `writeSettings`
+// drops them from the file. There is no second spelling of a pause left.
//
// A PAUSE IS A HOLD, NEVER A STOP. Every gate below is consulted at DISPATCH
// time by a limit() that returns 0, which makes runPool idle-wait: the job stays
@@ -82,33 +82,28 @@ export function pauseLaneFor(operationId: string): PauseLane | null {
// its pause was the one per-lane switch living somewhere else, in four fields
// with three polarities.
//
-// THE FALLBACK IS THE MIGRATION. Every settings.json written before slice 1.4
-// carries the retired fields and no `held`, so an absent `held` — and only an
-// absent one — asks `legacyGateHeld`. `getSettings` copies that answer onto the
-// key on read, so the first write after this slice persists it and the fallback
-// goes quiet; the fields themselves are deleted in a later slice, once the live
-// file carries all four `held` keys.
+// ABSENT IS NOT HELD, and that is now the whole rule. Slice 1.4 read an absent
+// key through the four retired pause fields and `getSettings` copied the answer
+// onto the lane; S0-pause deleted both, on the precondition that the live
+// settings.json already carried all four `held` keys (it did — the migration
+// had persisted them on its first write). So a file that predates the key, or
+// a hand-written one, reads as NOT held: there is no longer another field that
+// could say otherwise, and inventing a default of `true` would hold four lanes
+// on every fixture that never mentioned a pause.
//
-// `autoQueue` is read defensively: laneGuards.test.ts casts a `{ digest }`-only
-// object to SiteSettings, and this must answer for it the way it always has.
+// `autoQueue` is read defensively: laneGuards.test.ts casts a partial object to
+// SiteSettings, and this must answer for it the way it always has.
export function isGateHeld(settings: SiteSettings, lane: PauseLane): boolean {
- const held = settings.autoQueue?.[lane]?.held;
- if (typeof held === "boolean") return held;
- return legacyGateHeld(settings, lane);
+ return settings.autoQueue?.[lane]?.held === true;
}
// Set a lane's gate, returning a NEW settings object. Pure — no I/O; the caller
// writes it.
//
-// IT WRITES THE NEW KEY ONLY, and never the retired field it replaced. Those
-// fields are migration INPUT now: once `held` is on disk, `isGateHeld` stops
-// reading them, so a writer that also flipped `downloadsPaused` would be
-// maintaining a value nothing consults — which is how two sources of truth
-// start. The one other CONTROL over a lane's gate — "Run the backfill lane",
-// in operations/settingsActions.ts — comes through here too, for the
-// mirror-image reason: a form still flipping the retired field would now be
-// silently ignored. (The two settings forms that merely PRESERVE a retired
-// field are not writers; they preserve the whole autoQueue beside it.)
+// IT WRITES THE ONE KEY, and there is no longer a second field it could also
+// write: the four retired flags are deleted. The one other CONTROL over a
+// lane's gate — "Run the backfill lane", in operations/settingsActions.ts —
+// comes through here too, so the two controls cannot answer differently.
//
// SPREAD-AND-OVERRIDE, never a rebuilt literal: the policy also carries the
// lane's TREE, its order and its snooze, and a literal here would drop an
diff --git a/common/lib/settings.ts b/common/lib/settings.ts
@@ -25,10 +25,7 @@ import {
sanitizeChannelPriority,
type ChannelPriority,
} from "./channelPriority";
-import {
- migrateHeldToLanes,
- migrateSweepsToLanes,
-} from "./laneMigration";
+import { migrateSweepsToLanes } from "./laneMigration";
import {
INTERNAL_LOCATION_ID,
migrateMediaRootToLocations,
@@ -162,16 +159,6 @@ export type SiteSettings = {
// video once the stream ends. Per-channel override available
// (ChannelConfig.skipLiveDownloads).
skipLiveDownloads: boolean;
- // Global, restart-surviving pause for transcription workers. When true, the
- // worker pool is pause-all'd at boot (editor/instrumentation.ts) so no new
- // transcriptions start until resumed. The Workers-page Pause button and the
- // dashboard toggle both persist this. Default false. See pauseLaneAction.
- transcriptionsPaused: boolean;
- // Global, restart-surviving pause for downloads. When true, the auto-download
- // runner skips dispatch (checked each loop iteration, like `enabled`) and
- // manual download-bearing pipeline actions return a "Downloads are paused"
- // result. Enumeration/store-playlist stay allowed. Default false.
- downloadsPaused: boolean;
// Whether the transcribed-audio cleanup sweep checks each candidate is still
// available upstream before deleting its audio, pinning (do-not-clean) any
// video found permanently gone. The delete is irreversible and a gone video's
@@ -320,9 +307,6 @@ export type AttributionSettings = {
// yield is the operation's declared `contendsFor`. Slice 1.3 retired the
// `weight` scalar that used to mean both — see backfillLimit().
export type BackfillSettings = {
- // Master switch for the lane. Off means the registry still REPORTS what is
- // missing (that is the indicator's whole job) but nothing runs.
- enabled: boolean;
// Slots the lane may use when it is not standing aside. Kept at 1 by default
// for the same reason diarization.concurrency is: this is CPU-bound work
// competing with GPU feeding and the digest sweep for the same 8 threads.
@@ -473,10 +457,6 @@ export type DigestSettings = {
// Per-app config, keyed by app id — the same id-keyed sub-record shape as
// transcriptionApps.
apps: Record<string, DigestAppConfig>;
- // Global pause. Read at DISPATCH time by the batch (the downloadsPaused
- // pattern), so a pause survives a restart with no boot hook — unlike
- // transcriptionsPaused, which needs editor/instrumentation.ts to re-apply it.
- digestsPaused: boolean;
// Yield the GPU to the transcription lane: while transcription is working, the
// digest batch's limit() returns 0 and the pool idle-waits. ON by default,
// because `digest:local` is deliberately on a different queue from
@@ -1013,7 +993,6 @@ export function defaultDigest(): DigestSettings {
// maxCuesForContext sizes the chunk to it). Seeding a copy of those values
// here would give the same number two homes and let them drift.
apps: {},
- digestsPaused: false,
// ON. Real GPU contention with the transcription engine is a genuine cost
// (re-priced: 11.2 s/chunk idle against 24.9 s/chunk on a contended box), so
// the safe default is to step aside; turning it off is the deliberate choice.
@@ -1087,7 +1066,6 @@ export function sanitizeDigest(value: unknown): DigestSettings {
? r.remoteAppId.trim()
: d.remoteAppId,
apps: sanitizeDigestApps(r.apps),
- digestsPaused: r.digestsPaused === true,
// Defaults to ON when absent — `=== false` rather than `!== true`, so a
// settings file written before this field existed keeps the GPU-safe
// behaviour instead of silently opting into contention.
@@ -1182,8 +1160,6 @@ function defaults(): SiteSettings {
parallelTranscriptions: PARALLEL_TRANSCRIPTIONS_DEFAULT,
inlineTranscribeOnFallback: false,
skipLiveDownloads: true,
- transcriptionsPaused: false,
- downloadsPaused: false,
verifyAvailabilityBeforeClean: true,
buildArchives: true,
archiveStorage: { bucket: "", publicBaseUrl: "" },
@@ -1215,7 +1191,6 @@ export function defaultSiteSettings(): SiteSettings {
export function defaultBackfill(): BackfillSettings {
return {
- enabled: false,
concurrency: 1,
// See BackfillSettings.allowRedownload — this one holds disk.
allowRedownload: false,
@@ -1227,7 +1202,6 @@ export function sanitizeBackfill(value: unknown): BackfillSettings {
if (!value || typeof value !== "object") return d;
const r = value as Record<string, unknown>;
return {
- enabled: r.enabled === true,
// Clamped rather than rejected: a hand-edited 5 means "as much as possible",
// and reading it as 0 would be the opposite of the intent.
concurrency: clampPositiveInt(r.concurrency, d.concurrency, 16),
@@ -1544,12 +1518,6 @@ export function getSettings(): SiteSettings {
if (typeof merged.skipLiveDownloads !== "boolean") {
merged.skipLiveDownloads = true;
}
- if (typeof merged.transcriptionsPaused !== "boolean") {
- merged.transcriptionsPaused = false;
- }
- if (typeof merged.downloadsPaused !== "boolean") {
- merged.downloadsPaused = false;
- }
if (typeof merged.verifyAvailabilityBeforeClean !== "boolean") {
merged.verifyAvailabilityBeforeClean = true;
}
@@ -1602,17 +1570,6 @@ export function getSettings(): SiteSettings {
merged.diarization = sanitizeDiarization(merged.diarization);
merged.backfill = sanitizeBackfill(merged.backfill);
merged.attribution = sanitizeAttribution(merged.attribution);
- // THE PAUSE FIELDS, ON READ. `autoQueue[lane].held` is the gate since slice
- // 1.4; the four retired fields (`transcriptionsPaused`, `downloadsPaused`,
- // `digest.digestsPaused` and the inverted `backfill.enabled`) fill it in for
- // any lane whose policy does not carry one, so the next write persists the
- // key and `isGateHeld` stops consulting them. It never unholds a lane — the
- // value copied is the one isGateHeld was already returning.
- //
- // AFTER the digest and backfill sanitizers above, not beside the sweep
- // migration: two of the four fields it reads live in those blocks, and it
- // must read them normalized. See lib/laneMigration.ts.
- merged.autoQueue = migrateHeldToLanes(merged);
// Workers. When the file predates the worker model (no `workers` key),
// synthesize a default list from the (now-settled) active app + per-app
// configs so existing installs behave identically. Otherwise sanitize the
@@ -1752,6 +1709,13 @@ export async function writeSettings(next: SiteSettings): Promise<void> {
// `next`: getSettings() spreads the raw file, so a settings.json still
// carrying pre-multi-site keys (siteTitle/groups/socialLinks) would otherwise
// smuggle those stale keys back onto disk on every save.
+ //
+ // THIS IS ALSO WHAT RETIRES A FIELD. The four legacy pause flags
+ // (`transcriptionsPaused`, `downloadsPaused`, `digest.digestsPaused` and the
+ // inverted `backfill.enabled`) are gone from the type, from the sanitizers
+ // and from this literal, so a settings.json that still spells one is read
+ // past on load and loses it on the next write. The gate is
+ // `autoQueue[lane].held` and nothing else — see lib/pauseGates.ts.
const merged: SiteSettings = {
adminTitle:
typeof next.adminTitle === "string" && next.adminTitle.trim()
@@ -1781,8 +1745,6 @@ export async function writeSettings(next: SiteSettings): Promise<void> {
),
inlineTranscribeOnFallback: next.inlineTranscribeOnFallback === true,
skipLiveDownloads: next.skipLiveDownloads !== false,
- transcriptionsPaused: next.transcriptionsPaused === true,
- downloadsPaused: next.downloadsPaused === true,
verifyAvailabilityBeforeClean:
next.verifyAvailabilityBeforeClean !== false,
buildArchives: next.buildArchives !== false,