commit 3d9f74bc7ae47926084bfb3867e210701ad8d797
parent d6cc4835975303576070a7530aa49a046dc3095f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 22 Sep 2026 16:15:14 -0400
editor: nothing preserves a retired pause field, because there is none to preserve
The two forms that carried the legacy fields through an unrelated save stop
doing it: /settings no longer spreads `transcriptionsPaused` and
`downloadsPaused`, and the digest settings action no longer rebuilds
`digest.digestsPaused`. Both preserved them as `legacyGateHeld`'s input; that
function is gone. `saveBackfillLaneSettingsAction` is unchanged — it already
wrote `autoQueue.backfill.held` through `withGateHeld`.
Every comment that told the reader a gate lives in a deleted field now names the
key. Three of them moved into `common/views/` since this was first written —
the widget sync payload, the auto-queue status payload and the workers payload —
so they are updated there. `workers.downloadsPaused` on the wire keeps its name:
a pinned widget tab reads it, and the comment says so.
e2e: the migration spec becomes the deletion spec. A settings.json spelling
`digest.digestsPaused: true` and no `held` holds NOTHING, and the first pause
from the digest console writes `autoQueue.digest.held` AND drops the retired
field — the strip happens in writeSettings, which a unit test cannot see.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
16 files changed, 66 insertions(+), 74 deletions(-)
diff --git a/common/views/autoQueueStatus.ts b/common/views/autoQueueStatus.ts
@@ -62,9 +62,10 @@ export type AutoQueueKindStatus = {
// the operations rail show a runner HOLDING for the first time.
//
// THE TWO KINDS ANSWER IT FROM DIFFERENT PLACES, on purpose. Transcription's
- // hold is LIVE, on the worker pool; settings.transcriptionsPaused is only what
- // the boot hook re-applies after a restart, and the e2e harness rewrites
- // settings wholesale between tests while the pool keeps its pausedSnapshot.
+ // hold is LIVE, on the worker pool; the stored `autoQueue.transcription.held`
+ // is only what the boot hook re-applies after a restart, and the e2e harness
+ // rewrites settings wholesale between tests while the pool keeps its
+ // pausedSnapshot.
// Download has no live counterpart: its flag IS the gate, read at dispatch.
held: boolean;
// THE TREE ABOVE IS GENERATED, so the editor for it is read-only and the
diff --git a/common/views/widgetSync.ts b/common/views/widgetSync.ts
@@ -95,8 +95,8 @@ export type WidgetSyncPayload = {
needsMedia: number; // missing-input: needs an opt-in re-download first
videos: number; // videos in the corpus (the denominator)
// held — computed by isGateHeld; nothing downstream inverts anything. The
- // field on disk is `backfill.enabled`, whose polarity is the opposite one;
- // that lives in pauseGates.ts and reaches the wire already resolved. It was
+ // field on disk is `autoQueue.backfill.held`, in the plain polarity; the
+ // inverted `backfill.enabled` it replaced is deleted (S0-pause). It was
// `enabled` here until slice 7, and a pinned widget tab that predates the
// rename reads `undefined ?? false` — not held — until it is reloaded.
held: boolean;
diff --git a/common/views/workers.ts b/common/views/workers.ts
@@ -41,7 +41,8 @@ export type WorkerView = {
export type WorkersPayload = {
paused: boolean;
- // Persisted global downloads pause (settings.json downloadsPaused). Separate
+ // Persisted global downloads pause (settings.json `autoQueue.download.held`;
+ // this payload field keeps the older name, which pinned clients read). Separate
// from `paused` (live transcription-worker pause). Surfaced on the same poll
// so the dashboard/widget controls reflect both without a second request.
downloadsPaused: boolean;
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **A lane's pause is one key on the lane, and the four old pause fields are gone from `settings.json`.** Holding a lane has been `autoQueue.<lane>.held` since the runner work landed; until now the file also still carried the four flags that used to mean it — `transcriptionsPaused`, `downloadsPaused`, `digest.digestsPaused` and the backwards `backfill.enabled` (where *enabled* meant *not held*) — which were read only when a lane had no `held` yet, to carry an older file's pause across. Every lane now carries its own key, so those four are **deleted**: nothing reads them, no form writes them, and the next settings save drops them from the file. A settings.json that still spells one of them holds nothing with it, so a hand-edited file (or a very old backup restored over a newer one) can no longer resurrect a pause you had lifted, or lift one you had set. "Run the backfill lane" on the diarization page and the Hold/Pause buttons write the one key, as they already did. **UPGRADING: boot once on the release that writes `held` before taking this one.** That release is the one that moved the gate onto the lane and carried the old fields across on read; a single boot of it (any settings save, or just starting the editor and pausing/resuming anything) puts `autoQueue.<lane>.held` in your settings.json, after which **nothing you can see changes here** — the same buttons, the same labels, the same pauses. An install that jumps straight from an older release to this one has no `held` keys at all and **loses its pauses**: transcription, downloads and digests come up running, and the backfill lane comes up held. Re-set them from the dashboard, or add the keys by hand before starting.
- **A relocate job says how far it has got.** `rsync` has been printing its progress the whole time (`--info=progress2`) and every frame of it went into the job log as a carriage-return redraw of one line — so a 131 GB move and a 3 MB one looked identical from `/jobs`: a spinner. Now each frame is parsed into the **task bar** every other long job on that page already draws, reading `12.3 GB of 45.6 GB · 27 % · 110.50MB/s · ETA 5:32`, and the log gets **one line per 10 %** instead of several thousand frames of one. The percentage is against the tree the job already measured for its space check, not rsync's own — under incremental recursion that one is a percentage of what it has enumerated so far and walks backwards.
- **`/channels` is where storage is managed now.** Two new columns: **Location** (which volume this channel's media is on — *Internal* when it has not moved) and **Size** (every byte under its `data/`, from its last report, sortable biggest-first). Free space is *not* a column, because it is a fact about a disk and not about a channel: there is one read-out per **volume** in a new bar above the rack, and each chip is also a **filter** — `?location=platter` lists exactly the channels on that drive, and `/storage` links straight here with the biggest first. Beside them, **Free up N GB**: type a number, press *Select largest*, and the largest channels still on the internal disk are ticked until the target is met, ready for the Move button that was already there. Channels already on another volume are never picked (moving one frees nothing on the disk you are emptying) and channels whose report carries no size are **skipped and counted** rather than ranked as empty — which would have put the biggest thing on the disk at the bottom of the list.
- **The corpus volume is a row on `/storage`, and it is the first one.** 523 GB on a disk with 67 GB left is not a footnote under the locations that were added to fix it. It shows the channels in place, what they hold and the free space, and links to its own list. It is the one row with nothing to refresh, re-point, mount, edit or delete — and it says so, once, rather than as five greyed buttons. Every location row grows the same size read-out, and a location whose channels have never had a report says **"size unknown until Refresh report"** rather than claiming a 2 TB drive holds nothing.
diff --git a/editor/app/components/lanes/LaneDeck.tsx b/editor/app/components/lanes/LaneDeck.tsx
@@ -121,8 +121,8 @@ export function LaneDeck({
)}
</>
}
- // `held` is the LIVE pool, off the workers payload — never
- // settings.transcriptionsPaused, which only says what a restart would do.
+ // `held` is the LIVE pool, off the workers payload — never the persisted
+ // `autoQueue.transcription.held`, which only says what a restart would do.
controls={[
pauseLaneControl({
lane: "transcription",
@@ -134,7 +134,8 @@ export function LaneDeck({
);
// ── Downloads ─────────────────────────────────────────────────────────────
- // Persisted in settings.json (downloadsPaused — survives a restart). Pausing
+ // Persisted in settings.json (`autoQueue.download.held` — survives a
+ // restart; the payload field below keeps the older name). Pausing
// gates the auto-download runner on its next loop iteration and makes manual
// download-bearing pipeline actions return a "Downloads are paused" notice;
// store-playlist/enumeration stay allowed.
@@ -332,7 +333,9 @@ export function LaneDeck({
// the operator needs a hold for reasons the scheduler cannot see.
//
// The pause writes THE SAME FIELD the Settings checkbox writes
- // (settings.backfill.enabled) rather than a new `backfillPaused` flag. One
+ // (`autoQueue.backfill.held`, through withGateHeld — it was the inverted
+ // `settings.backfill.enabled` until slice 1.4 and S0-pause deleted that
+ // field) rather than a new `backfillPaused` flag. One
// field, several places to set it, and they cannot drift — which is why
// backfill.spec asserts the settings field through this button's label rather
// than just watching the label flip.
diff --git a/editor/app/operations/actions.ts b/editor/app/operations/actions.ts
@@ -189,7 +189,7 @@ async function setLaneHeld(
held: boolean,
): Promise<LanePauseResult> {
// TRANSCRIPTION IS THE ONE LANE WITH A LIVE HOLD, and it goes first. The pool
- // is the machine; settings.transcriptionsPaused is only what
+ // is the machine; the stored `autoQueue.transcription.held` is only what
// editor/instrumentation.ts re-applies at boot. Every UI surface reads the
// pool, so flipping it first is what makes the button feel immediate.
if (lane === "transcription") {
diff --git a/editor/app/operations/components/settings/LaneSettingsForm.tsx b/editor/app/operations/components/settings/LaneSettingsForm.tsx
@@ -15,11 +15,11 @@ import type { SaveResult } from "../../../settings/actions";
// That is why it takes no operation: there is nothing here that belongs to one.
export function LaneSettingsForm({
initial,
- // THE LANE'S GATE, read by the caller through `isGateHeld`. Not
- // `initial.enabled`: since slice 1.4 the gate is `autoQueue.backfill.held`,
- // and `backfill.enabled` is the retired field it migrated FROM — a checkbox
- // reading it would show the pre-migration answer on a file that has since
- // been written, and would disagree with the pause button beside it.
+ // THE LANE'S GATE, read by the caller through `isGateHeld`. The gate is
+ // `autoQueue.backfill.held` (slice 1.4); the `backfill.enabled` this checkbox
+ // used to read was the retired field it migrated FROM, and S0-pause deleted
+ // it — `initial` no longer has one. There is one key, and the pause button
+ // beside this checkbox writes it too.
held,
}: {
initial: SiteSettings["backfill"];
diff --git a/editor/app/operations/settingsActions.ts b/editor/app/operations/settingsActions.ts
@@ -99,13 +99,6 @@ export async function saveDigestSettingsAction(
// writeSettings runs sanitizeDigestApps over it. Narrowed at exactly
// this field so every OTHER field in the block stays type-checked.
apps: digestApps as SiteSettings["digest"]["apps"],
- // Not edited by this form, and no longer the gate: since slice 1.4 the
- // digest lane's pause is `autoQueue.digest.held`, which the `...current`
- // spread above carries untouched. This field is the migration's INPUT and
- // is preserved rather than rebuilt, because a save that reset it would
- // change how a settings.json that has NOT yet been written through
- // getSettings reads.
- digestsPaused: dD.digestsPaused,
spendCapUsd: Number.parseFloat(
String(formData.get("digestSpendCapUsd") ?? "").trim(),
),
@@ -206,9 +199,8 @@ export async function saveDiarizationSettingsAction(
// `withGateHeld` — a deliberate SECOND WRITER of `autoQueue.backfill.held`,
// beside the pause buttons. It has always been two controls over one switch
// (the checkbox wrote `backfill.enabled`, which `isGateHeld` read inverted);
-// what slice 1.4 changed is that the switch moved, and a form still writing the
-// retired field would be silently ignored by every reader — the mirror image of
-// the bug `withGateHeld` writing only the new key avoids on the other side.
+// slice 1.4 moved the switch onto the lane and S0-pause deleted the old field,
+// so there is exactly one key both controls write.
//
// LaneSettingsForm renders every field of this block and is the only form that
// posts here.
diff --git a/editor/app/settings/actions.ts b/editor/app/settings/actions.ts
@@ -217,14 +217,6 @@ export async function saveSettingsAction(
parallelTranscriptions: PARALLEL_TRANSCRIPTIONS_DEFAULT,
inlineTranscribeOnFallback,
skipLiveDownloads,
- // The two RETIRED pause flags. They are not this form's business and they
- // are not the gate any more either — since slice 1.4 that is
- // `autoQueue[lane].held`, preserved a few lines below with the rest of the
- // policy. These are the migration's INPUT for a settings.json that has not
- // yet been written through getSettings, so they are preserved rather than
- // rebuilt: resetting them here would change how such a file reads.
- transcriptionsPaused: getSettings().transcriptionsPaused,
- downloadsPaused: getSettings().downloadsPaused,
verifyAvailabilityBeforeClean,
buildArchives,
archiveStorage,
diff --git a/editor/e2e/attribution.spec.ts b/editor/e2e/attribution.spec.ts
@@ -115,7 +115,6 @@ function attributionSettings(over: {
// otherwise switching capture off would strand exactly the work it exists to
// protect.
backfill: {
- enabled: true,
concurrency: 1,
allowRedownload: false,
},
diff --git a/editor/e2e/auto-queue.spec.ts b/editor/e2e/auto-queue.spec.ts
@@ -1446,22 +1446,23 @@ test("the digest lane offers Shortest first, and has no Reach axis", async ({
.toBe("newest/gone/gone/held=true");
});
-// THE MIGRATION, FROM THE BROWSER. Every settings.json on disk today spells a
-// lane's pause in one of the four RETIRED fields and carries no `held` at all,
-// so "an unmigrated file still reads as held" is not a historical curiosity —
-// it is how the live corpus reads until something writes it. The unit tests pin
-// all four fields (common/lib/pauseGates.test.ts); this pins the half a unit
-// test cannot: that a real editor, reading a real file, holds the lane, and
-// that the first toggle moves the answer onto the lane where every writer now
-// looks.
+// THE DELETION, FROM THE BROWSER. Until S0-pause a settings.json spelling one
+// of the four RETIRED pause fields and carrying no `held` still held its lane:
+// `isGateHeld` fell back to the field and `getSettings` copied the answer onto
+// the lane. Both are gone, on the precondition that the live settings.json had
+// already been written with all four `held` keys — so a file that still spells
+// a retired field must now hold NOTHING, and must lose the field the first time
+// the editor writes it.
+//
+// That second half is what a unit test cannot see: the strip happens in
+// `writeSettings`, which builds its output from only the known operational
+// fields, and this asserts it through a real save from a real page.
//
// The DIGEST lane, not transcription, and that is the asymmetry rather than a
// shortcut: transcription's live hold is the worker pool, and no UI surface may
// read its stored value (see lib/pauseGates.ts), so a fixture flag could not be
-// observed through a page without contradicting that rule. Its fallback is
-// covered by the unit tests and by the phase-1 numbers script, which prints
-// `held transcription` off the live file through isGateHeld.
-test("a settings.json with no `held` is read through its retired pause field", async ({
+// observed through a page without contradicting that rule.
+test("a retired pause field holds nothing, and does not survive a write", async ({
page,
}) => {
await resetData(null);
@@ -1473,35 +1474,40 @@ test("a settings.json with no `held` is read through its retired pause field", a
minFreeDiskGB: 0,
workers: ONE_WORKER,
// The pre-1.4 spelling, and nothing else: no `autoQueue.digest` block, so
- // no `held` for the sanitizer to find.
+ // no `held` for the sanitizer to find either.
digest: { digestsPaused: true },
});
const res = await page.request.get(`${baseUrl}/api/auto-queue/status`);
expect(res.ok()).toBeTruthy();
const payload = (await res.json()) as Record<string, { held?: boolean }>;
- expect(payload.digest?.held).toBe(true);
+ expect(payload.digest?.held).toBe(false);
await page.goto("/operations/digest");
const digest = page.locator('section[data-lane="digest"]');
await awaitHydration(digest);
- const resume = digest.getByRole("button", { name: "resume digests" });
- await expect(resume).toBeEnabled({ timeout: 30_000 });
- await resume.click();
+ // The lane reads as running, so the control on offer is the pause.
+ const pause = digest.getByRole("button", { name: "pause digests" });
+ await expect(pause).toBeEnabled({ timeout: 30_000 });
+ await pause.click();
- // The first toggle persists the KEY. From here the retired field is dead
- // config — which is why every writer of it was rewritten in the same slice.
+ // The click persists the KEY — and the write drops the retired field, so a
+ // stale settings.json cannot resurrect a pause after an operator lifts one.
await expect
.poll(
- async () =>
- (
- await readJson<{
- autoQueue?: { digest?: { held?: boolean } };
- }>("test-settings.json").catch(() => null)
- )?.autoQueue?.digest?.held ?? null,
+ async () => {
+ const s = await readJson<{
+ autoQueue?: { digest?: { held?: boolean } };
+ digest?: { digestsPaused?: boolean };
+ }>("test-settings.json").catch(() => null);
+ if (!s) return null;
+ return `held=${String(s.autoQueue?.digest?.held)}/retired=${String(
+ "digestsPaused" in (s.digest ?? {}),
+ )}`;
+ },
{ timeout: 20_000 },
)
- .toBe(false);
+ .toBe("held=true/retired=false");
});
test("/jobs: a runner is a lane on a strip, not a card", async ({
diff --git a/editor/e2e/backfill.spec.ts b/editor/e2e/backfill.spec.ts
@@ -83,7 +83,6 @@ function backfillSettings(over: {
...over.diarization,
},
backfill: {
- enabled: true,
concurrency: 1,
allowRedownload: false,
...over.backfill,
@@ -788,8 +787,9 @@ test("the dashboard pauses and resumes the backfill lane", async ({ page }) => {
await writeSettings(backfillSettings());
// THE LANE'S GATE, on the lane. Slice 1.4 moved it off the inverted
- // `backfill.enabled` and onto `autoQueue.backfill.held`, so the polarity here
- // is the plain one: held means held.
+ // `backfill.enabled` (deleted by S0-pause) and onto
+ // `autoQueue.backfill.held`, so the polarity here is the plain one: held
+ // means held.
const laneHeld = async () =>
(
await readJson<{ autoQueue?: { backfill?: { held?: boolean } } }>(
@@ -833,8 +833,9 @@ test("an operation page holds the same lane the dashboard does", async ({
await writeSettings(backfillSettings());
// THE LANE'S GATE, on the lane. Slice 1.4 moved it off the inverted
- // `backfill.enabled` and onto `autoQueue.backfill.held`, so the polarity here
- // is the plain one: held means held.
+ // `backfill.enabled` (deleted by S0-pause) and onto
+ // `autoQueue.backfill.held`, so the polarity here is the plain one: held
+ // means held.
const laneHeld = async () =>
(
await readJson<{ autoQueue?: { backfill?: { held?: boolean } } }>(
diff --git a/editor/e2e/channel-line.spec.ts b/editor/e2e/channel-line.spec.ts
@@ -53,7 +53,6 @@ function laneSettings(enabled: boolean) {
concurrency: 1,
},
backfill: {
- enabled,
concurrency: 1,
allowRedownload: false,
},
diff --git a/editor/e2e/lane-runner.spec.ts b/editor/e2e/lane-runner.spec.ts
@@ -131,7 +131,6 @@ function laneSettings(over: Record<string, unknown> = {}) {
...((over.attribution as Record<string, unknown>) ?? {}),
},
backfill: {
- enabled: true,
concurrency: 1,
allowRedownload: false,
...((over.backfill as Record<string, unknown>) ?? {}),
diff --git a/editor/e2e/operation-settings.spec.ts b/editor/e2e/operation-settings.spec.ts
@@ -127,9 +127,8 @@ test("the lane's switch survives a save of the operation form beside it", async
// "Run the backfill lane" IS the lane's gate, and since slice 1.4 the gate is
// `autoQueue.backfill.held` — checked means running, so held is false. It used
- // to write the inverted `backfill.enabled`; that field is the migration's
- // input now and nothing writes it, so asserting it here would assert a value
- // no click can move.
+ // to write the inverted `backfill.enabled`; S0-pause deleted that field, so
+ // there is no second place a click could land.
const saved = await readJson<{
autoQueue?: { backfill?: { held?: boolean } };
diarization?: { enabled: boolean };
diff --git a/editor/e2e/widget.spec.ts b/editor/e2e/widget.spec.ts
@@ -74,7 +74,6 @@ const BACKFILL_SETTINGS = {
concurrency: 1,
},
backfill: {
- enabled: true,
concurrency: 1,
allowRedownload: false,
},
@@ -803,8 +802,8 @@ test("the widget's controls hold and release the backfill lane", async ({
// Assert the KEY, not just the label: the button writes the same
// `autoQueue.backfill.held` the "Run the backfill lane" checkbox does, and
// the point of that choice is that the two cannot drift. (It was
- // `backfill.enabled` until slice 1.4 moved the gate onto the lane; the
- // polarity is no longer inverted.)
+ // `backfill.enabled` until slice 1.4 moved the gate onto the lane and
+ // S0-pause deleted the field; the polarity is no longer inverted.)
const laneHeld = async () =>
(
await readJson<{ autoQueue?: { backfill?: { held?: boolean } } }>(