commit 5f3c62f4957b9b2af03cfcdc40bac7af39738a5a
parent 47870ae59c13ab7c4bf47f14e5196b4e93f1e9df
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 21 Sep 2026 02:30:57 -0400
ops: a fan-out that started jobs must say which
`pnpm ops relocate --wait` did not wait. The route answers
`{ok, queued:[slug], skipped:[]}` and carries no `jobId`, so the script's
id-extraction found nothing, took the "not a job-starting action" branch and
returned 0 the instant the response arrived — reporting success about a 130 GB
copy that had not begun. `relocate-back` had it too, and so did every other
route built on `queueResponse`.
`QueueOutcome` gains `jobIds`, parallel to `queued` and in the same order.
`queueResponse` emits it, plus a bare `jobId` when exactly one job started so
the single-slug case reads like every other job-starting route. Both ADDITIVE:
`queued` is the documented body shape of two public routes and a UI reads it as
slugs, so it keeps its meaning and its spelling.
The script reads `jobIds` FIRST — a bulk response now carries both, and
checking `jobId` first would follow one job out of five.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
5 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/editor/app/api/ops/_lib.ts b/editor/app/api/ops/_lib.ts
@@ -200,10 +200,22 @@ export function okResponse(
// A bulk fan-out's { queued, skipped }. A skip is not a failure — the caller
// gets both lists and decides, exactly as the bulk bar in the UI does.
+//
+// IT ALSO RETURNS THE JOB IDS, and that was not a nicety: without them a
+// job-starting fan-out was indistinguishable from an action that started
+// nothing, so `pnpm ops relocate --wait` returned 0 the moment the response
+// arrived and reported success about a copy that had not begun.
+//
+// `jobIds` is parallel to `queued`; `jobId` is present only when exactly one
+// job was started, so the single-slug case reads like every other job-starting
+// route (`jobResponse`). Both are ADDITIVE — `queued` and `skipped` keep their
+// meaning and their spelling.
export function queueResponse(outcome: QueueOutcome): NextResponse {
return NextResponse.json({
ok: true,
queued: outcome.queued,
skipped: outcome.skipped,
+ jobIds: outcome.jobIds,
+ ...(outcome.jobIds.length === 1 ? { jobId: outcome.jobIds[0] } : {}),
});
}
diff --git a/editor/app/channels/bulkStorageActions.ts b/editor/app/channels/bulkStorageActions.ts
@@ -75,6 +75,7 @@ export async function bulkRelocateChannelMediaAction(
);
const refuseAll = (reason: string): BulkRelocateResult => ({
queued: [],
+ jobIds: [],
skipped: slugs.map((slug) => ({ slug, reason })),
});
if ("error" in resolved) return refuseAll(resolved.error);
diff --git a/editor/app/channels/lib/queueForSlugs.ts b/editor/app/channels/lib/queueForSlugs.ts
@@ -10,6 +10,16 @@ import type { StreamActionResult } from "yt-dlp-transcript-common/jobs/streamCom
export type QueueOutcome = {
queued: string[];
skipped: { slug: string; reason: string }[];
+ // THE JOB IDS, IN THE SAME ORDER AS `queued`. Added because a fan-out's
+ // response carried no way to FOLLOW what it started: `pnpm ops relocate
+ // --wait` read `{ok, queued:[slug], skipped:[]}`, found no `jobId`, decided
+ // nothing had been started and returned 0 immediately — reporting success
+ // about a 130 GB copy that had not begun.
+ //
+ // A separate array rather than turning `queued` into objects: `queued` is the
+ // documented body shape of two public routes and a UI reads it as slugs.
+ // Additive, never renamed.
+ jobIds: string[];
};
export async function queueForSlugs(
@@ -21,6 +31,7 @@ export async function queueForSlugs(
},
): Promise<QueueOutcome> {
const queued: string[] = [];
+ const jobIds: string[] = [];
const skipped: { slug: string; reason: string }[] = [];
for (const slug of slugs) {
const reason = opts.skip ? await opts.skip(slug) : null;
@@ -34,10 +45,11 @@ export async function queueForSlugs(
continue;
}
queued.push(slug);
+ jobIds.push(result.jobId);
// Nobody will read the stream here — cancel it so the buffered chunks
// can be GC'd. The job keeps running and writes to its log file via
// runManagedFunction's onLog regardless.
void result.stream.cancel();
}
- return { queued, skipped };
+ return { queued, jobIds, skipped };
}
diff --git a/editor/e2e/ops-api.spec.ts b/editor/e2e/ops-api.spec.ts
@@ -35,6 +35,7 @@ type OpsResponse = {
error?: string;
jobId?: string;
queued?: string[];
+ jobIds?: string[];
skipped?: { slug: string; reason: string }[];
};
@@ -399,6 +400,11 @@ test("refresh-report regenerates snapshot.json", async ({ request }) => {
expect([...(all.body.queued ?? []), ...(all.body.skipped ?? []).map((s) => s.slug)]).toContain(
SLUG,
);
+ // ONE JOB ID PER QUEUED SLUG, in the same order. This is what `--wait`
+ // follows; without it a fan-out response was indistinguishable from an
+ // action that started nothing.
+ expect(all.body.jobIds?.length).toBe(all.body.queued?.length);
+ for (const id of all.body.jobIds ?? []) expect(id).toBeTruthy();
const both = await ops(request, "refresh-report", { slug: SLUG, all: true });
expect(both.status).toBe(400);
@@ -438,6 +444,12 @@ test("relocate refuses a busy channel with the sentence the panel shows", async
// A SKIP IS NOT A FAILURE — the bulk bar renders both numbers, and so does
// this. The reason is channelMediaBusyReason's, word for word.
expect(refused.body.queued).toEqual([]);
+ // AND NO JOB IDS, which is the half `--wait` reads: a fan-out that started
+ // nothing must be distinguishable from one that started something. Before
+ // `jobIds` existed a relocate response carried no id at all, so
+ // `pnpm ops relocate --wait` found none, concluded nothing had been started
+ // and returned 0 — reporting success about a 130 GB copy that had not begun.
+ expect(refused.body.jobIds).toEqual([]);
expect(refused.body.skipped?.[0]?.slug).toBe(SLUG);
expect(refused.body.skipped?.[0]?.reason).toMatch(
/running\/queued job\(s\) for this channel/,
diff --git a/scripts/archilyzer-ops.mjs b/scripts/archilyzer-ops.mjs
@@ -216,11 +216,18 @@ async function main() {
console.log(JSON.stringify(payload, null, 2));
if (!res.ok || payload.ok === false) return 1;
if (!parsed.wait) return 0;
- const jobIds = payload.jobId
- ? [payload.jobId]
- : Array.isArray(payload.jobs)
- ? payload.jobs.map((j) => j.jobId)
- : [];
+ // `jobIds` FIRST: a bulk fan-out (relocate, relocate-back) returns an array
+ // and also a single `jobId` when it started exactly one, so reading `jobId`
+ // first would follow one job out of five. Before `jobIds` existed those
+ // routes carried no id at all and --wait returned immediately, reporting
+ // success about a copy that had not begun.
+ const jobIds = Array.isArray(payload.jobIds)
+ ? payload.jobIds
+ : payload.jobId
+ ? [payload.jobId]
+ : Array.isArray(payload.jobs)
+ ? payload.jobs.map((j) => j.jobId)
+ : [];
if (jobIds.length === 0) {
// Not a job-starting action (or it queued nothing). --wait is satisfied.
return 0;