commit 65ad76b909588a5d218156f9ccda9026497e950c
parent 2fd9f84421cdc3e18bf453d3f65ceda42eacc7d9
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 02:33:49 -0400
editor: /jobs shows why the boot pass cancelled a job
`JobMeta.cancelReason` (release 9, B4b) was written for every meta the boot
pass closed, and shown nowhere but the job's log. It now travels
meta → `JobListEntry.cancelReason` (only on a `cancelled` meta) →
`fromEntry` → `JobRowView.cancelReason`, and is drawn where the cancelled
pill is: under the pill in the /jobs table's Status cell, beside it on a
card row, as the pill's title on a compact one, and as a full-width
"Cancelled because" cell on the job's page. `data-testid="cancel-reason"`.
No existing label changed.
Tests: listJobs.test.ts +1 (a cancelled meta's reason reaches the entry, a
`done` one's is dropped), jobRows.test.ts +1 (fromEntry carries it; no key
without one); jobs-filters.spec.ts +1 (the row and the page draw it, a plain
cancel draws none).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
8 files changed, 176 insertions(+), 2 deletions(-)
diff --git a/common/jobs/listJobs.test.ts b/common/jobs/listJobs.test.ts
@@ -90,6 +90,43 @@ test("listAllJobs before-cursor skips newer entries", async () => {
});
});
+// The boot pass (bootQueuedJobs.ts) closes a meta a restart left `queued` as
+// `cancelled` with a `cancelReason`; /jobs draws it from the entry.
+test("a cancelled meta's cancelReason reaches the entry; any other status drops it", async () => {
+ await withJobs(async (paths) => {
+ const write = async (id: string, status: string) => {
+ await writeFile(path.join(paths.jobsDir, `${id}.log`), "log\n");
+ await writeFile(
+ path.join(paths.jobsDir, `${id}.meta.json`),
+ JSON.stringify({
+ id,
+ kind: "whisper-all",
+ queueKey: "q",
+ status,
+ queuedAt: BASE,
+ endedAt: BASE + 1,
+ cancelReason: "queued before the last restart, stale",
+ }),
+ );
+ };
+ const cancelled = ulid(BASE);
+ const done = ulid(BASE + 1);
+ await write(cancelled, "cancelled");
+ await write(done, "done");
+ const one = await getJobEntry(paths, cancelled);
+ assert.equal(one?.status, "cancelled");
+ assert.equal(one?.cancelReason, "queued before the last restart, stale");
+ const page = await listAllJobs(paths);
+ const byId = new Map(page.entries.map((e) => [e.id, e]));
+ assert.equal(
+ byId.get(cancelled)?.cancelReason,
+ "queued before the last restart, stale",
+ );
+ // A reason beside a status it does not explain is not carried.
+ assert.equal(byId.get(done)?.cancelReason, undefined);
+ });
+});
+
test("getJobEntry resolves one job and null for unknown ids", async () => {
await withJobs(async (paths, seed) => {
const id = await seed(0, BASE);
diff --git a/common/jobs/listJobs.ts b/common/jobs/listJobs.ts
@@ -30,6 +30,11 @@ export type JobListEntry = {
// A short phrase naming what this particular job is for, when its kind alone
// does not say (a fetch-window job's requester and clip). See jobSpecDetail.
detail?: string;
+ // Why a `cancelled` job was cancelled, when something other than a person
+ // pressing Cancel decided it — today only the boot pass (bootQueuedJobs.ts),
+ // e.g. "server restarted; the scheduler re-derives syncs". From the sidecar;
+ // absent on every other job.
+ cancelReason?: string;
};
export type JobsPage = {
@@ -135,6 +140,11 @@ async function buildEntry(
inRegistry: false,
replayable: Boolean(meta.spec),
detail: jobSpecDetail(meta.kind, meta.spec),
+ // Only on a job that did end `cancelled`: a reason beside any other
+ // status would explain something that did not happen.
+ ...(meta.status === "cancelled" && meta.cancelReason
+ ? { cancelReason: meta.cancelReason }
+ : {}),
logPath,
logSize,
};
diff --git a/common/views/jobRowView.ts b/common/views/jobRowView.ts
@@ -94,6 +94,10 @@ export type JobRowView = {
// jobs/jobDetail.ts from the job's own replay spec; absent for every kind
// that has nothing to add, so no existing row changes.
detail?: string;
+ // Why a `cancelled` row was cancelled when no person pressed Cancel — the
+ // boot pass's reason for a job a restart left queued ("server restarted;
+ // the scheduler re-derives syncs"). History rows only; see listJobs.ts.
+ cancelReason?: string;
// Which adapter built it. Never rendered; tests and the merge read it —
// except "runner": an auto-queue lane's in-flight unit (fromInFlight), which
// links to a job page only when `inRegistry` says it has one.
diff --git a/common/views/jobRows.test.ts b/common/views/jobRows.test.ts
@@ -146,6 +146,28 @@ test("fromEntry keeps the history fields, archived included", () => {
assert.equal(row.source, "archive");
// An old log with no sidecar has no kind at all; the cell renders "—".
assert.equal(fromEntry({ ...e, kind: undefined }).kind, "");
+ // No reason on the entry, no reason on the row (not even the key).
+ assert.equal("cancelReason" in row, false);
+});
+
+test("fromEntry carries a cancelled job's cancelReason to the row", () => {
+ const e: JobListEntry = {
+ id: newJobId(),
+ kind: "sync",
+ channelSlug: "teamrcn",
+ status: "cancelled",
+ queuedAt: 1,
+ endedAt: 9,
+ inRegistry: false,
+ replayable: true,
+ logPath: "/x.log",
+ logSize: 64,
+ cancelReason: "server restarted; the scheduler re-derives syncs",
+ };
+ assert.equal(
+ fromEntry(e).cancelReason,
+ "server restarted; the scheduler re-derives syncs",
+ );
});
test("orderLiveRows: running, then queued IN QUEUE ORDER, then what just ended", () => {
diff --git a/common/views/jobRows.ts b/common/views/jobRows.ts
@@ -282,6 +282,7 @@ export function fromEntry(e: JobListEntry): JobRowView {
inRegistry: e.inRegistry,
replayable: e.replayable,
detail: e.detail,
+ ...(e.cancelReason ? { cancelReason: e.cancelReason } : {}),
source: "archive",
};
}
diff --git a/editor/app/jobs/[id]/page.tsx b/editor/app/jobs/[id]/page.tsx
@@ -92,6 +92,16 @@ export default async function JobDetailPage({
label="Exit code"
value={typeof job.exitCode === "number" ? String(job.exitCode) : "—"}
/>
+ {/* Why, when no person pressed Cancel: the boot pass's reason for a
+ job a restart left queued (JobMeta.cancelReason). */}
+ {job.status === "cancelled" && job.cancelReason && (
+ <Cell
+ label="Cancelled because"
+ value={job.cancelReason}
+ className="col-span-2 sm:col-span-4"
+ testId="cancel-reason"
+ />
+ )}
</dl>
<p className="text-xs text-muted-foreground font-mono">
log: {path.relative(getPaths().monorepoRoot, job.logPath)}
@@ -106,9 +116,22 @@ export default async function JobDetailPage({
);
}
-function Cell({ label, value }: { label: string; value: string }) {
+function Cell({
+ label,
+ value,
+ className = "",
+ testId,
+}: {
+ label: string;
+ value: string;
+ className?: string;
+ testId?: string;
+}) {
return (
- <div className="rounded border border-border px-3 py-2">
+ <div
+ className={`rounded border border-border px-3 py-2 ${className}`}
+ data-testid={testId}
+ >
<div className="text-xs uppercase tracking-wide text-muted-foreground">
{label}
</div>
diff --git a/editor/app/jobs/components/JobRow.tsx b/editor/app/jobs/components/JobRow.tsx
@@ -122,6 +122,31 @@ export function statusColor(status: string): string {
}
}
+// WHY A JOB WAS CANCELLED, when no person pressed Cancel: the boot pass's
+// reason for a job a restart left queued (JobRowView.cancelReason, from the
+// meta). Drawn wherever the `cancelled` pill is, as plain text under or beside
+// it; absent on every other row.
+function cancelReasonOf(job: JobRowView): string | undefined {
+ return job.status === "cancelled" ? job.cancelReason : undefined;
+}
+
+function CancelReason({ job, inline }: { job: JobRowView; inline?: boolean }) {
+ const reason = cancelReasonOf(job);
+ if (!reason) return null;
+ return (
+ <span
+ data-testid="cancel-reason"
+ className={
+ inline
+ ? "text-xs text-muted-foreground"
+ : "self-start max-w-72 text-xs text-muted-foreground break-words"
+ }
+ >
+ {inline ? `· ${reason}` : reason}
+ </span>
+ );
+}
+
// A row with a page at /jobs/<id>. A runner's in-flight unit that is a task
// on its runner's job (a transcription, a digest) is not a registry job, so it
// does not link to one; a download unit is, and carries its id.
@@ -221,10 +246,12 @@ function JobRowHeading({
? `uppercase tracking-wide px-1.5 py-0.5 rounded text-[10px] ${statusColor(job.status)}`
: `text-xs uppercase tracking-wide px-2 py-0.5 rounded ${statusColor(job.status)}`
}
+ title={cancelReasonOf(job)}
>
{job.status}
</span>
)}
+ {statusPill && !compact && <CancelReason job={job} inline />}
<span
className={compact ? "shrink-0 font-mono" : "text-xs font-medium"}
title={job.kind}
@@ -512,6 +539,7 @@ function TableRow({
>
{j.status}
</span>
+ <CancelReason job={j} />
{j.stuck && (
<span
data-stuck={j.stuck.reason}
diff --git a/editor/e2e/jobs-filters.spec.ts b/editor/e2e/jobs-filters.spec.ts
@@ -21,6 +21,9 @@ type SeedJob = {
startedAt?: number;
endedAt?: number;
exitCode?: number;
+ // Written by the boot pass (common/jobs/bootQueuedJobs.ts) on a job a
+ // restart left queued.
+ cancelReason?: string;
};
async function seedJob(job: SeedJob): Promise<void> {
@@ -65,6 +68,52 @@ test("archived jobs keep their metadata from the sidecar", async ({ page }) => {
);
});
+// Release 10 (L2): a job the boot pass closed says WHY, on its row and on its
+// page — the reason used to be only in the meta and the log's last line.
+test("a job the boot pass cancelled shows its reason on /jobs and on its page", async ({
+ page,
+}) => {
+ await resetData();
+ const reason = "server restarted; the scheduler re-derives syncs";
+ await seedJob({
+ id: "boot-cancelled-1",
+ kind: "sync",
+ status: "cancelled",
+ channelSlug: "alpha",
+ queueKey: "platform:youtube",
+ queuedAt: 1000,
+ endedAt: 2000,
+ cancelReason: reason,
+ });
+ // A cancel with no recorded reason (a person pressed Cancel) draws none.
+ await seedJob({
+ id: "plain-cancelled-1",
+ kind: "sync",
+ status: "cancelled",
+ channelSlug: "alpha",
+ queueKey: "platform:youtube",
+ queuedAt: 1000,
+ startedAt: 1000,
+ endedAt: 2000,
+ });
+
+ await page.goto("/jobs");
+ const row = rowById(page, "boot-cancelled-1");
+ await expect(row).toContainText("cancelled");
+ await expect(row.getByTestId("cancel-reason")).toHaveText(reason);
+ await expect(
+ rowById(page, "plain-cancelled-1").getByTestId("cancel-reason"),
+ ).toHaveCount(0);
+
+ await page.goto("/jobs/boot-cancelled-1");
+ const cell = page.getByTestId("cancel-reason");
+ await expect(cell).toContainText("Cancelled because");
+ await expect(cell).toContainText(reason);
+ await page.goto("/jobs/plain-cancelled-1");
+ await expect(page.getByText("Exit code")).toBeVisible();
+ await expect(page.getByTestId("cancel-reason")).toHaveCount(0);
+});
+
test("filters hide refresh-report by default, persist, and reset", async ({
page,
}) => {