Public: anyone can read this; the crew can change it.

The queue:check sentence names the check slots in force, not "one lane" #4863

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4850-the-queue-check-sentence-names-the-check → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/4850-the-queue-check-sentence-names-the-check was merged into main as bbb1aad.

Review

Sign in to review this change.
4 files changed +66 −13
docs/DOMAINS.md +1 −1
@@ -17,7 +17,7 @@ keeps a drill's verdict for any domain here (see the README).
1717 | The edge (tunnel and DNS) | The forge runs but nobody outside can reach it. | nothing yet | 10 | Restart the tunnel; check the DNS record. | none: Cutting the tunnel cuts every person and agent off the forge. |
1818 | Identity | Sign-in fails; sessions and tokens already issued may still work. | nothing yet | 15 | Restore the sign-in provider's settings; sign in with the admin password. | none: Breaking sign-in locks the owner out of the forge that would fix it. |
1919 | The model credential | Agents cannot start work; everything else runs. | nothing yet | 5 | Set a fresh credential in the settings. | expire-token |
20 | The runner | Checks and runs queue behind a lane that is not moving. | "wait behind one lane; the oldest since"; "is stuck: no heartbeat for" | 10 | Cancel the stuck run; restart the runner if the lane stays empty. | lose-runner |
20 | The runner | Checks and runs queue behind a lane that is not moving. | "the oldest since"; "is stuck: no heartbeat for" | 10 | Cancel the stuck run; restart the runner if the lane stays empty. | lose-runner |
2121
2222 ## Shared by accident
2323
src/kernel/domains.ts +1 −1
@@ -78,7 +78,7 @@ export const DOMAINS: readonly Domain[] = [
7878 key: "runner",
7979 name: "The runner",
8080 breaks: "Checks and runs queue behind a lane that is not moving.",
81 says: ["wait behind one lane; the oldest since", "is stuck: no heartbeat for"],
81 says: ["the oldest since", "is stuck: no heartbeat for"],
8282 backMinutes: 10,
8383 owner: "Cancel the stuck run; restart the runner if the lane stays empty.",
8484 drill: { name: "lose-runner" },
src/kernel/vitals.ts +41 −11
@@ -31,6 +31,7 @@ import { proofNeed } from "../release/prove.ts";
3131 import { rollbackNeed } from "../release/launch.ts";
3232 import { behindStanding } from "../release/behind.ts";
3333 import { newestDrills } from "../core/domain_drills.ts";
34 import { capacityValue } from "../core/capacity.ts";
3435 import { DOMAINS } from "./domains.ts";
3536 import { domainDrillNeeds } from "./domain_drill.ts";
3637 import { freeChecks, heldChecks, redHref, redSentence } from "../checks/red_main.ts";
@@ -245,28 +246,47 @@ function readLane(
245246 };
246247 }
247248
249 /** One check running now: its pull (null when it has none) and when it began. */
250 export interface LaneRun {
251 readonly pull: number | null;
252 readonly since: string;
253 }
254
248255 /**
249256 * The lane's one sentence, with its cause: how many wait, since when, and
250 * what the lane is doing — "22 checks wait behind one lane; the oldest since
251 * 04:13; the lane runs #1212 for 31 min (limit 60)".
257 * what runs. With one slot: "22 checks wait behind one lane; the oldest since
258 * 04:13; the lane runs #1212 for 31 min (limit 60)". With several: "11 checks
259 * wait for 7 slots, all busy; the oldest since 01:04; the longest running is
260 * #4656 at 5 min (limit 60)".
252261 */
253262 export function laneSentence(
254263 queued: number,
255264 oldest: string,
256 lane: Lane,
265 runs: readonly LaneRun[],
266 slots: number,
257267 now: number,
258268 held = 0,
259269 ): string {
260270 const since = new Date(oldest).toISOString().slice(11, 16);
261 const head = `${queued} check${queued === 1 ? "" : "s"} ${queued === 1 ? "waits" : "wait"} behind one lane; the oldest since ${since}`;
271 const plural = `check${queued === 1 ? "" : "s"} ${queued === 1 ? "waits" : "wait"}`;
262272 const more = held > 0 ? `; ${held} more held by a red main` : "";
263 if (lane.running === null) return `${head}; the lane runs nothing${more}.`;
264 const age = Math.max(
265 0,
266 Math.round((now - new Date(lane.running.since).getTime()) / 60_000),
273 const minutes = (r: LaneRun) =>
274 Math.max(0, Math.round((now - new Date(r.since).getTime()) / 60_000));
275 const longest = runs.reduce<LaneRun | null>(
276 (a, r) => (a === null || r.since < a.since ? r : a),
277 null,
267278 );
268 const what = lane.running.pull === null ? "a check" : `#${lane.running.pull}`;
269 return `${head}; the lane runs ${what} for ${age} min (limit ${BUDGETS.gate / 60_000})${more}.`;
279 const name = (r: LaneRun) => (r.pull === null ? "a check" : `#${r.pull}`);
280 if (slots <= 1) {
281 const head = `${queued} ${plural} behind one lane; the oldest since ${since}`;
282 if (longest === null) return `${head}; the lane runs nothing${more}.`;
283 return `${head}; the lane runs ${name(longest)} for ${minutes(longest)} min (limit ${BUDGETS.gate / 60_000})${more}.`;
284 }
285 const busy =
286 runs.length === 0 ? "none busy" : runs.length >= slots ? "all busy" : `${runs.length} busy`;
287 const head = `${queued} ${plural} for ${slots} slots, ${busy}; the oldest since ${since}`;
288 if (longest === null) return `${head}; nothing runs${more}.`;
289 return `${head}; the longest running is ${name(longest)} at ${minutes(longest)} min (limit ${BUDGETS.gate / 60_000})${more}.`;
270290 }
271291
272292 export function vitals(input: VitalsInput): Vitals {
@@ -314,6 +334,9 @@ export function vitals(input: VitalsInput): Vitals {
314334
315335 const running = listRunning(input.db);
316336 const lane = readLane(input.db, running, now);
337 const checkRuns: LaneRun[] = running
338 .filter((r) => r.run.kind === "check" && !leaseLapsed(r.run, STALE_AFTER_MS, now))
339 .map((r) => ({ pull: r.issue, since: r.run.startedAt ?? r.run.createdAt }));
317340 const queue: Record<string, number> = {};
318341 const reds = heldChecks(input.db);
319342 const held = reds.reduce((n, r) => n + r.held, 0);
@@ -326,7 +349,14 @@ export function vitals(input: VitalsInput): Vitals {
326349 if (free.oldest !== null && now - new Date(free.oldest).getTime() > BUDGETS.laneWait)
327350 need(
328351 "queue:check",
329 laneSentence(free.n, free.oldest, lane, now, Math.min(held, row.queued)),
352 laneSentence(
353 free.n,
354 free.oldest,
355 checkRuns,
356 Number(capacityValue(input.db, "checks.concurrency")),

Number(capacityValue(...)) is the only path the multi-slot wording takes in vitals(); an end-to-end test setting checks.concurrency would pin it.

357 now,
358 Math.min(held, row.queued),
359 ),
330360 new Date(free.oldest).toISOString(),
331361 "/settings/capacity",
332362 );
test/vitals.test.ts +23
@@ -1,5 +1,6 @@
11 /** The forge's standing: its shape, the admin gate, and what it flags. */
22
3 import { laneSentence } from "../src/kernel/vitals.ts";
34 import { afterAll, beforeAll, expect, test } from "bun:test";
45 import { mkdtemp, rm } from "node:fs/promises";
56 import { tmpdir } from "node:os";
@@ -464,3 +465,25 @@ test("only the disk need sees the disk: a 5% reading raises it, and nothing else
464465 expect(v.needs).toEqual(["Disk is nearly full: 5% free."]);
465466 expect(vitals({ db: app.db, signals: app.signals, dataDir, disk: roomy }).needs).toEqual([]);
466467 });
468
469 test("laneSentence says one lane for one slot and names the slots in force for several", () => {
470 const now = Date.parse("2026-01-01T02:00:00Z");
471 const oldest = "2026-01-01T01:04:00Z";
472 const iso = (min: number) => new Date(now - min * 60_000).toISOString();
473 const runs = [
474 { pull: 4656, since: iso(5) },
475 { pull: 4700, since: iso(2) },
476 ];
477 expect(laneSentence(3, oldest, [runs[0]!], 1, now)).toBe(
478 "3 checks wait behind one lane; the oldest since 01:04; the lane runs #4656 for 5 min (limit 60).",
479 );
480 expect(laneSentence(11, oldest, runs, 2, now)).toBe(
481 "11 checks wait for 2 slots, all busy; the oldest since 01:04; the longest running is #4656 at 5 min (limit 60).",
482 );
483 expect(laneSentence(1, oldest, [runs[1]!], 7, now, 2)).toBe(
484 "1 check waits for 7 slots, 1 busy; the oldest since 01:04; the longest running is #4700 at 2 min (limit 60); 2 more held by a red main.",
485 );
486 expect(laneSentence(4, oldest, [], 3, now)).toBe(
487 "4 checks wait for 3 slots, none busy; the oldest since 01:04; nothing runs.",
488 );
489 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs