Work queue — workload visibility (running/queued/next-scheduled per runtime lane)
Schedule firings, a user's scorecard runs, and one-off runs are all workloads received by the control plane and queued/dispatched. This document is the SSOT for the read-only visibility slice that surfaces that queue in a single view.
Questions → answers
- What's the current work-queue state? — all scorecard batches + standalone runs in
queued/runningstate. - Which runtime is it scheduled on? — lanes are classified by the
runtime(placement.target) axis captured on the record. - What's running on each runtime right now? — the lane's
running[](batches include progress). - What's next? — the front of the lane's
queued[]FIFO + the next firing of active schedules (upcoming[]).
Data (mig 0040, additive)
RunRecord.runtime/ScorecardRecord.runtime— stamps the runtime the workload was placed on at submit time (RunService.submit: explicit runtime ?? case placement.target;ScorecardService.submit: input.runtime; a batch's child runs get the same value). NULL = default backend or a legacy record. Lightweight → included inlist.- Lane keys:
''(default backend) · registered runtime id ·self:<runnerId>(self-hosted). Registered runtimes expose empty lanes too ("this runtime is idle" is information).
Unit (design decision — user-confirmed)
A batch (scorecard) = 1 job: case fan-out (child runs) is not expanded into items but collapsed into the batch's
progress (progress { done, active, total? }) — done/active are child-run counts,
total is the dataset case count (omitted if it can't be resolved). A standalone run stays 1 job.
Two queues (scope separation — user-confirmed)
The workspace queue and the personal queue are different queues. workspace = work requested in the workspace
that runs on shared runtimes (default backend '' + registered infra) — self:* items never appear here.
personal = the requester's own self-hosted runner (self:<runnerId>) queue (lane label = runner hostname).
Other members' personal runner queues are not exposed (same as the runner ownership model) and are excluded from totals aggregation.
Service/transport (BFF↔MCP parity)
QueueService.snapshot(tenant, subject?) (packages/application-control/src/queue/queue-service.ts) — assembles { workspace: lanes[], personal: lanes[] } from store listings (lightweight) alone (personal scope determined by myRunners(subject)):
the active states of scorecards + runs (standalone) + ScheduleService.list's nextFireTimes (Temporal
authoritative; if absent, upcoming is omitted — cron approximation is the web schedule screen's domain) + RuntimeRegistry.list.
- HTTP:
GET /queue(runs:read, viewer+) - MCP:
get_queue(same gate)
Time series (GET /metrics) — the Prometheus half (the snapshot above answers "now"; this answers
"since when / how often / how long"). Zero-dep text exposition: scrape-time gauges (queue depth, per-backend
in-flight + memory, per-workspace in-flight/queued, open circuits) + counters at the dispatch seam
(everdict_dispatch_total{runtime,outcome}, spillovers, breaker open transitions, speculation fired/won, OOM
escalations) + a per-runtime case-duration histogram. UNAUTHENTICATED by design (standard scrape practice —
firewall the path in deployments). Live: one dead+kind shard batch registered breaker_open 1, spillover_total 3,
dispatch infra 3 / ok 6, duration count 6 in a single scrape. Every dispatch (runs, batch cases, judges) flows
through one metered dispatcher wrapper, so coverage needs no per-caller wiring.
Scheduler observability (the seeing half of the fairness/envelope machinery — docs/execution-backends.md):
the snapshot carries a workspace scheduler slice ({queued, inFlight, quota?} — THIS tenant's numbers only)
and each workspace lane an admission view ({inFlight, memInFlightMb?, memoryBudgetMb?, maxConcurrent?, circuit?}). Lane mapping: a tenant runtime's backends rt:<tenant>:<id>@<ver> sum into the id's lane (another
tenant's same-id runtime never counts — filtered inside the service); bare-named global env backends aggregate
into the '' (default) lane; self-hosted lanes are lease queues → no admission. circuit is the spillover
breaker state (open = dispatches currently route around this runtime). The web lane header shows the memory
envelope (used/budget Mb) and an open-circuit badge; live-verified: an autoscaled batch showed
queued 4 / inFlight 8 + memInFlightMb 4096, and a dead-runtime shard surfaced circuit {open, consecutive 3}.
Web (the infra panel's work tab, widgets/infra-panel)
Not a nav page — the work tab of the floating infra panel, opened from the vertical rail on the right edge (the rail's work button always carries the running+queued badge, fed by a slow background poll). Two groups: workspace queue / my personal queue (self-hosted). Each lane card (Server/Laptop icon + label + count, idle badge) renders the flow vertically: next-scheduled ⇢ queued (FIFO, a 'next' badge at the front) ⇢ running (progress bar). Items are fixed one-line rows: EntityRef refs + executor avatar + timestamp; a running single run offers a watch-live shortcut that opens it in the panel's runs tab (live screen + log tail) in place. Polling: 4s while the panel is open, 20s closed (badge only), skipped while the tab is hidden.
Correctness: orphan recovery on boot
Batches/runs are tracked in-process inside the control-plane process (single-process assumption) — on restart, the
previous process's in-flight records become ghosts with no owner to take them over, and the queue shows 'running'
forever. On boot, recoverInterrupted (startup-recovery.ts) finalizes queued/running batches, children, and
standalone runs as failed(INTERRUPTED). If two control planes share the same DB, another's in-flight records
will also be recovered, so keep the single-control-plane assumption.
The scheduler's REAL queue (entries + controls)
The lanes above are a record-status projection (scorecard/run records grouped by runtime). The control
plane's actual dispatch queue is the Scheduler's WFQ, and it is surfaced separately as
snapshot.scheduler.entries — this workspace's waiting entries in the scheduler's effective scan order
(the exact order pump() will try: urgent class first — interactive / operator-promoted / aged past agingMs
— then the rest, tenant-fair WFQ within each class; position 1 is next). Each entry carries its identity
(caseId / runId / batchId / harness / pinned target / evalCase.tags — ["judge"] marks a
control-plane judge job) plus enqueuedAt/waitedMs/urgent/promoted and a stable handle id (q<seq>).
Two controls, gated runs:submit (same as submitting work), tenant-guarded in QueueService (another
workspace's — or an already-placed — entry reads 404, no existence leak):
DELETE /queue/entries/:id(MCPcancel_queued_job) — remove the WAITING entry and settle its dispatch as CANCELLED (the kill switch, e.g. a judge job left queued by a reclaimed batch). In-flight work is untouched.POST /queue/entries/:id/promote(MCPpromote_queued_job) — move it to the front of the effective order (FairQueue.promotetakes the head's virtual-finish time + the urgent class; fairness bookkeeping is untouched, repeated promotions stack newest-first). This is the queue-reorder primitive: promote in reverse desired order to fully reorder.
The work tab renders the entries under the scheduler headline with the wait time, judge/urgent/front badges, and the two per-row actions (immediate re-poll after a mutation).
Limitations / follow-ups
- LANE queue order is a createdAt FIFO approximation; the authoritative order is
scheduler.entries(managed lanes). Exposing the measured self-hosted lease-queue depth is a follow-up. - Entry cancel settles through the dispatch caller's own machinery — a batch case may re-dispatch it if the batch's retry budget classifies CANCELLED as retryable; cancelling the scorecard remains the batch-level stop.
- upcoming is only present when a Temporal driver exists (nextFireTimes). dev (no driver) has an empty column.