Phase 23 final verification complete — all green.
```
VERIFIED (criteria)
1 /api/llm/snapshot windows ≈2 s live (1.98–2.00), /api/system window_seconds ≈10 s (9.996–10.005);
/api/config llm.rate_window_seconds=2=LLM_INTERVAL_SECONDS; browser re-check ALL CHECKS PASSED
(data/logs/23-window-{llm,system}.png — /llm prints Δ2.0s, /system footnote Δ10.0s)
2 Phase-13 pins unchanged: floor, 2×span ceiling, 8× jump gate + independent anchor — all pass
(only import/span-line diffs in tests/; git diff confirms no assertion rewritten)
3 Live proof re-derived from artifacts: 780 rows, 0 unlabelled, windows 0.61–3.38 s, 5 starts +
5 ends settled by poll 2 (0 unsettled), 1 m rollups max 42.3 vs raw 201.6, 0 over 300 tok/s;
counterfactual 10 s window: median settle poll 3, |ln| 0.924 vs 0.037
4 uv run pytest: 1494 passed / 15 skipped · coverage 98.94% (floor 94) · ruff clean · pyright 0
errors · node --check clean · bash .agents/validate.sh exit 0 · live 11 passed · store_perf 4 passed
Dev server up, all nine sources ok. No git add/commit run; everything left in the working tree.
DOCS PASS — 4 inconsistencies fixed in AGENTS.md (last two runs above include them)
- UI conventions hero example still said the `.sub` footnote reads `Δ10.0s`; the only hero that
prints a Δ is /llm's → now `Δ2.0s` on /llm, "at the current load" on /ups
- gap 9: "found 10 busy-edges" contradicted its own artifact (17 edges, 10 judged, 7 reported-not-
judged) → 17; and the poll-2 ratios were a 3-of-5 cherry-pick → full five values both windows
- gap 12: the responsive-table fix is now scheduled — names 27_mobile_tables_and_audit
README, config table, .env.example, rates/prom docstrings, app/api bullets and the testing-policy
pins all re-checked against code and found consistent (store pragmas, budgets, throttle, fleet).
NOTES
- Advisory only, not phase 23: fastapi 0.153.0 → 0.156.8 available; dependencies untouched here,
so rule 8's "update on purpose" is left to the operator.
- NOTES.md is empty in the working tree (its items became phases 24–27); left as-is, untracked-file
rules forbid me removing it.
- Next phase: 24_dev_mtls_rollout (25–27 follow).
```
4.9 KiB
Phase 23 — LLM rate responsiveness
Source: TODO.md L8 — "The LLM monitor has a strange delay and a 'ramp up/ramp down' effect that makes it look innacurate. Data reporting should be accurate to as small a delta as possible."
Story: n/a
Context: src/dashboards/app.py line 122 (build_hub builds one RateEstimator(span=settings.rate_window) and hands it to both host_jobs (line 124) and llm_jobs (line 130)), src/dashboards/config.py lines 197–199 (llm_interval 2 s, llm_timeout 3 s, rate_window 10 s), src/dashboards/collectors.py line 477 (llm_jobs(settings, client, rates) — the estimator is a parameter, so a second one is a wiring change, not a signature redesign), src/dashboards/rates.py (line 55 MIN_WINDOW_SECONDS = 0.5, line 61 MAX_WINDOW_RATIO = 2.0, the phase-13 jump gate — all untouched by this phase), src/dashboards/web/static/page-llm.js (line 39 pollMs: 2000, the Δ<window> labels printed from the payload's window), src/dashboards/sources/llm.py (the rate cells carry computed:Δ<series>/Δt + the honest Δ<window>; the window the estimator used travels with the value — rule 2), the mechanism the TODO describes (gap 9 / phase 13 in AGENTS.md: a 10 s window over a 2 s poll ramped the hero up over ~5 polls when a slot started and decayed over ~5 polls when it ended — the "ramp up/ramp down" the operator sees; the sub-8× skew family and the stall are already closed by phases 13/14 and stay closed: this phase changes the window size, nothing else)
Objective
The LLM throughput rates are measured over a window as small as the app's own cadence allows — the LLM source gets its own RateEstimator whose span is the LLM poll interval (2 s), so a slot start reads steady within two polls instead of ramping over ten, while the host source keeps the 10 s RATE_WINDOW_SECONDS window its 10 s cadence needs (a 2 s window over a 10 s poll would refuse every window — the estimator's floor and ceiling would answer n/a forever). The phase-13 protections (the 0.5 s floor, the 2 × span ceiling, the 8× jump gate with its independent-anchor confirmation) apply to the smaller window unchanged; the Δ<window> label prints the smaller window it actually used, so the number always says what it is.
Dependencies
- — (none) — the phase touches
app.py/collectors.pywiring and the LLM page; it is scheduled after22_disks_hide_unconfiguredonly to keep the fleet-changing phase 24 last.
Tasks
01_llm_window_per_poll.md— the second estimator:build_hubhandsllm_jobsaRateEstimator(span=settings.llm_interval), the host keeps the 10 s one02_window_pins.md— the pins: LLM rate cells label the 2 s window, host cells keep 10 s, the phase-13 gates still hold over the smaller window03_live_verification.md— the live proof: a slot start reads steady within two polls (no 10 s ramp), theΔ2.0slabel travels with the value, screenshots
Testing & Quality
- Unit/integration:
tests/test_llm.py(or wherever theparse_gufo_metrics/parse_vllm_metricstests live — greprate_window/RateEstimatorintests/first) — the window-value pins for both sources, the phase-13 tests (floor refusal, ceiling, jump gate, the stale-anchor tests) run unchanged and green;tests/test_api.py's/api/llm/snapshotshape pins stay green (the shape is unchanged — only thewindowvalues move) - Coverage: >90% on new/modified code
- The phase is "Complete" only when the entire project test suite passes 100% (every test in the project, not just this phase's)
Documentation & Consistency
- The final pass reviews
README.mdandAGENTS.mdin full against the project as it stands after this phase — fixing every inconsistency found, including pre-existing ones not caused by this phase. AGENTS.md'sapp.py/poller.pybullets (the "oneRateEstimator" sentence) andrates.py's docstring context gain the two-estimator fact; gap 9's "the estimator no longer turns a slow fetch into 0.2 s arithmetic" history stays — this phase is the window-size decision on top of it, and theRATE_WINDOW_SECONDS=10line in.env.examplestays (it is the host's knob now, and gains a comment saying so if it has none).
Completion Criteria
/api/llm/snapshotrate cells carry awindow≈ 2 s (the label printsΔ2.0son the page) and/api/system/snapshotrate cells keep ≈ 10 s (pinned both ways)- the phase-13 tests (floor, ceiling, jump gate, stale anchor) pass unchanged
- live: a slot start reads steady within two polls and a slot end decays within two — the 10 s ramp is gone (screenshots + the stored 1 m rollup in
data/logs/) uv run pytestgreen, coverage not down,ruff+pyrightcleanbash .agents/validate.shgreen- no behavior change in completed work: the host rates, the estimator's gates, the rollups and the
Δ<window>labelling are all unchanged in kind