Public: anyone can read this; the crew can change it.
The queue:check sentence names the check slots in force, not "one lane" #4863
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4850-the-queue-check-sentence-names-the-check → main 1 commit fork 1 thread · 1 unresolved
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). | ||
| 17 | 17 | | 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. | |
| 18 | 18 | | 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. | |
| 19 | 19 | | 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 | | |
| 21 | 21 | |
| 22 | 22 | ## Shared by accident |
| 23 | 23 |
src/kernel/domains.ts +1 −1
| @@ -78,7 +78,7 @@ export const DOMAINS: readonly Domain[] = [ | ||
| 78 | 78 | key: "runner", |
| 79 | 79 | name: "The runner", |
| 80 | 80 | 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"], | |
| 82 | 82 | backMinutes: 10, |
| 83 | 83 | owner: "Cancel the stuck run; restart the runner if the lane stays empty.", |
| 84 | 84 | drill: { name: "lose-runner" }, |
src/kernel/vitals.ts +41 −11
| @@ -31,6 +31,7 @@ import { proofNeed } from "../release/prove.ts"; | ||
| 31 | 31 | import { rollbackNeed } from "../release/launch.ts"; |
| 32 | 32 | import { behindStanding } from "../release/behind.ts"; |
| 33 | 33 | import { newestDrills } from "../core/domain_drills.ts"; |
| 34 | import { capacityValue } from "../core/capacity.ts"; | |
| 34 | 35 | import { DOMAINS } from "./domains.ts"; |
| 35 | 36 | import { domainDrillNeeds } from "./domain_drill.ts"; |
| 36 | 37 | import { freeChecks, heldChecks, redHref, redSentence } from "../checks/red_main.ts"; |
| @@ -245,28 +246,47 @@ function readLane( | ||
| 245 | 246 | }; |
| 246 | 247 | } |
| 247 | 248 | |
| 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 | ||
| 248 | 255 | /** |
| 249 | 256 | * 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)". | |
| 252 | 261 | */ |
| 253 | 262 | export function laneSentence( |
| 254 | 263 | queued: number, |
| 255 | 264 | oldest: string, |
| 256 | lane: Lane, | |
| 265 | runs: readonly LaneRun[], | |
| 266 | slots: number, | |
| 257 | 267 | now: number, |
| 258 | 268 | held = 0, |
| 259 | 269 | ): string { |
| 260 | 270 | 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"}`; | |
| 262 | 272 | 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, | |
| 267 | 278 | ); |
| 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}.`; | |
| 270 | 290 | } |
| 271 | 291 | |
| 272 | 292 | export function vitals(input: VitalsInput): Vitals { |
| @@ -314,6 +334,9 @@ export function vitals(input: VitalsInput): Vitals { | ||
| 314 | 334 | |
| 315 | 335 | const running = listRunning(input.db); |
| 316 | 336 | 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 })); | |
| 317 | 340 | const queue: Record<string, number> = {}; |
| 318 | 341 | const reds = heldChecks(input.db); |
| 319 | 342 | const held = reds.reduce((n, r) => n + r.held, 0); |
| @@ -326,7 +349,14 @@ export function vitals(input: VitalsInput): Vitals { | ||
| 326 | 349 | if (free.oldest !== null && now - new Date(free.oldest).getTime() > BUDGETS.laneWait) |
| 327 | 350 | need( |
| 328 | 351 | "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")), | |
| 357 | now, | |
| 358 | Math.min(held, row.queued), | |
| 359 | ), | |
| 330 | 360 | new Date(free.oldest).toISOString(), |
| 331 | 361 | "/settings/capacity", |
| 332 | 362 | ); |
test/vitals.test.ts +23
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | /** The forge's standing: its shape, the admin gate, and what it flags. */ |
| 2 | 2 | |
| 3 | import { laneSentence } from "../src/kernel/vitals.ts"; | |
| 3 | 4 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 4 | 5 | import { mkdtemp, rm } from "node:fs/promises"; |
| 5 | 6 | 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 | ||
| 464 | 465 | expect(v.needs).toEqual(["Disk is nearly full: 5% free."]); |
| 465 | 466 | expect(vitals({ db: app.db, signals: app.signals, dataDir, disk: roomy }).needs).toEqual([]); |
| 466 | 467 | }); |
| 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 | }); |
new above
offline
Number(capacityValue(...)) is the only path the multi-slot wording takes in vitals(); an end-to-end test setting checks.concurrency would pin it.