From b1e7d9e68664a857b4902491029abd472b60b464 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:49:44 +0100 Subject: [PATCH 1/2] feat: idle-time self-test, context probe, resources roll-up and usage measurement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four intents from one lane, landed together because their surfaces are the same files: the panel, the control plane, the app's composition root, the configuration and the pool. The self-test (itd-2609100457007827) measures every downloaded model while nobody is using the Mac — llama-bench's pp512 and tg128, a concurrent tg128 and the load time — through the pool's ordinary Acquire, never evicting, yielding the moment a client appears, into a bounded 0600 file with a view on the Statistics tab. The context-window probe (itd-2609091301112705) is a second job of that one idle loop: a sweep and a bisection through this Mac's own OpenAI endpoint, the window recorded on the model's registry entry with the provenance that makes it stale, published on the models list as measured_context and measured_bound, adopted as the served window only by the operator's hand. A reading one of the gateway's own bounds stopped is a floor and says so. The resources roll-up (itd-2609091903463596) adds up what the Models tab already knows, with free disk on the snapshot from the one reader. Usage measurement (itd-2609091712141073) gives the request record the facts that decide a served window and a concurrency, the load event the launch sampling, and the store a fifth record kind — a footprint reading every thirty seconds per server, from top's physical figure with a deadline — with three views on the Statistics tab and every field on the reference page. Each intent had two adversarial reviews applied; the decision lines in the records say what changed and why. Assisted-by: Claude:claude-fable-5-1 Claude-Session: https://claude.ai/code/session_01NU6NHFLGFYFuA1z4eqFz8j --- CHANGELOG.md | 67 +- README.md | 13 + docs/context-probe.md | 103 +++ docs/memory-budget-explained.md | 12 + docs/memory-budget.md | 11 + docs/models-list.md | 24 + docs/posture-reference.md | 1 + docs/request-statistics.md | 5 +- docs/self-test-reference.md | 99 +++ docs/self-test.md | 115 +++ docs/statistics-explained.md | 43 + docs/statistics-store-reference.md | 29 +- internal/app/app.go | 105 ++- internal/app/contextprobe.go | 161 ++++ internal/app/contextprobe_test.go | 181 ++++ internal/app/loglevel_test.go | 8 +- internal/app/measurement_test.go | 26 + internal/app/observer_test.go | 5 +- internal/app/selftest.go | 88 ++ internal/app/selftest_test.go | 118 +++ internal/archtest/context_bound_test.go | 17 + internal/archtest/disk_reader_test.go | 39 + .../archtest/enforcement_detection_test.go | 22 +- internal/archtest/event_fields_test.go | 19 + internal/archtest/prompt_content_test.go | 7 +- internal/archtest/selftest_docs_test.go | 56 ++ internal/config/config.go | 99 ++- internal/config/contextprobe_test.go | 74 ++ internal/config/selftest_test.go | 44 + internal/contextprobe/filler.go | 38 + internal/contextprobe/probe.go | 643 ++++++++++++++ internal/contextprobe/probe_test.go | 634 ++++++++++++++ internal/gateway/control.go | 74 +- internal/gateway/control_history_test.go | 2 +- internal/gateway/control_probe_test.go | 123 +++ internal/gateway/control_resources_test.go | 27 + internal/gateway/control_selftest.go | 102 +++ internal/gateway/control_selftest_test.go | 165 ++++ internal/gateway/control_stats_test.go | 2 +- internal/gateway/control_store_test.go | 4 +- internal/gateway/gateway.go | 86 +- internal/gateway/gateway_test.go | 27 +- internal/gateway/measurement_test.go | 125 +++ internal/gateway/observe.go | 33 + internal/gateway/probe_integration_test.go | 239 ++++++ internal/gateway/systemmerge_test.go | 2 + internal/lifecycle/testdata/config_show.json | 3 + internal/mlxtest/fake.go | 78 +- internal/mlxtest/fake_test.go | 75 ++ internal/registry/measurement.go | 223 +++++ internal/registry/measurement_test.go | 190 +++++ internal/registry/registry.go | 15 + internal/runtime/footprint_test.go | 35 + internal/runtime/launcher.go | 71 +- internal/runtime/observer.go | 29 +- internal/runtime/observer_test.go | 123 ++- internal/runtime/pool.go | 122 ++- internal/runtime/pool_test.go | 11 + internal/runtime/provision.go | 5 + internal/runtime/selftest_refusals_test.go | 53 ++ internal/selftest/request.go | 238 ++++++ internal/selftest/results.go | 257 ++++++ internal/selftest/selftest.go | 757 +++++++++++++++++ internal/selftest/selftest_test.go | 804 ++++++++++++++++++ internal/stats/dashboard.go | 182 ++++ internal/stats/measurement_test.go | 180 ++++ internal/stats/stats.go | 145 +++- internal/stats/stats_test.go | 16 +- internal/stats/store.go | 37 +- internal/stats/store_test.go | 2 + internal/stats/summary.go | 2 +- internal/ui/contextprobe_test.go | 61 ++ internal/ui/controls_test.go | 1 + internal/ui/history_test.go | 1 + internal/ui/measurement_test.go | 44 + internal/ui/panel_test.go | 4 +- internal/ui/posture_test.go | 15 +- internal/ui/resources_test.go | 83 ++ internal/ui/selftest_test.go | 54 ++ internal/ui/static/app.js | 305 ++++++- internal/ui/static/index.html | 95 +++ 81 files changed, 8103 insertions(+), 130 deletions(-) create mode 100644 docs/context-probe.md create mode 100644 docs/self-test-reference.md create mode 100644 docs/self-test.md create mode 100644 internal/app/contextprobe.go create mode 100644 internal/app/contextprobe_test.go create mode 100644 internal/app/measurement_test.go create mode 100644 internal/app/selftest.go create mode 100644 internal/app/selftest_test.go create mode 100644 internal/archtest/context_bound_test.go create mode 100644 internal/archtest/disk_reader_test.go create mode 100644 internal/archtest/event_fields_test.go create mode 100644 internal/archtest/selftest_docs_test.go create mode 100644 internal/config/contextprobe_test.go create mode 100644 internal/config/selftest_test.go create mode 100644 internal/contextprobe/filler.go create mode 100644 internal/contextprobe/probe.go create mode 100644 internal/contextprobe/probe_test.go create mode 100644 internal/gateway/control_probe_test.go create mode 100644 internal/gateway/control_resources_test.go create mode 100644 internal/gateway/control_selftest.go create mode 100644 internal/gateway/control_selftest_test.go create mode 100644 internal/gateway/measurement_test.go create mode 100644 internal/gateway/probe_integration_test.go create mode 100644 internal/mlxtest/fake_test.go create mode 100644 internal/registry/measurement.go create mode 100644 internal/registry/measurement_test.go create mode 100644 internal/runtime/footprint_test.go create mode 100644 internal/runtime/selftest_refusals_test.go create mode 100644 internal/selftest/request.go create mode 100644 internal/selftest/results.go create mode 100644 internal/selftest/selftest.go create mode 100644 internal/selftest/selftest_test.go create mode 100644 internal/stats/measurement_test.go create mode 100644 internal/ui/contextprobe_test.go create mode 100644 internal/ui/measurement_test.go create mode 100644 internal/ui/resources_test.go create mode 100644 internal/ui/selftest_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index d42d57a6..09f7ae39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,61 @@ GitHub release notes. ## [Unreleased] +### Added + +- **The usage dashboard shows what the models actually do, so the settings + that matter can be seen before they are chosen.** Every request record now + carries the two windows it was judged against, Gropius's own estimate of + the prompt's size (for a refused request too), how many requests the model + already had, which sampling parameters the client set — the names, never + the values — and the model server's memory as last sampled; every load + carries the sampling the server was launched with; and a new record kind, + `footprint`, is a reading of each running server's memory every thirty + seconds. The Statistics tab gains three views: prompts against the served + window, sampling overrides per parameter, and memory over time. Nothing in + any of it is a prompt or an answer, nothing is exported, and the + [reference page](docs/statistics-store-reference.md) names every field. + +- **The Models tab adds up what your Mac is spending on Gropius.** A line at + its head says how many models are downloaded and loaded, how much disk they + take and how much the volume has left, and the memory budget against what is + resident, naming the part still exiting. Each card says the window the model + declares and, when you have set one below it, the window it is served at. + Nothing new is measured: the figures are the ones the panel already had, + and free disk now rides the state snapshot from the one reader the search + tab uses ([the memory budget](docs/memory-budget.md#what-the-models-tab-adds-up)). + +- **The context-window probe: Gropius measures the largest prompt each + model on this Mac will actually take.** Off until you turn it on — + **Settings → Context probe**, `context_probe` in `config.json` — or per + model with **Measure now** on its card. While the Mac is idle, Gropius + sends prompts of growing length through its own OpenAI endpoint, bisects to + the largest the server accepts, and records the window on the model's entry + with what stopped the next step: the model itself, or one of Gropius's own + limits — the prefill deadline, the served window, the memory guard — in + which case the figure is a floor and says so. It is published on the models + list as `measured_context` and `measured_bound`, changes no charge and + refuses no request until you press **Use this window**, and is marked stale + when the runtime, the budget, the concurrency or the served window changes. + A probe costs about forty minutes of GPU per model, never evicts another + model, and stands down the moment anyone sends a request. The idle threshold + the probe and the self-test share is now a setting, `idle_threshold_sec` + ([how to](docs/context-probe.md)). + +- **The self-test: Gropius measures its own models while nobody is using the + Mac.** Off until you turn it on — **Settings → Self-test**, `self_test` in + `config.json`. While it is on, once the Mac has been idle for five minutes, + Gropius loads one model at a time through the same path a request takes — + and never by evicting another — runs llama-bench's pair and a concurrency figure against it — `pp512`, + `tg128`, `tg128xN` at the decode concurrency — plus the load time, writes + one line of figures to `selftest/results.jsonl` in this account's data + folder, and unloads what it loaded. A request from anyone cancels the run + at once and is recorded as such; a request refused room during a run is + served on its retry. Each model is measured once a day. The + file holds counts and timings only, never a prompt or an answer, and is + bounded at 4 MiB ([how to](docs/self-test.md), + [reference](docs/self-test-reference.md)). + ## [0.6.0] - 2026-09-12 ### Changed @@ -220,7 +275,6 @@ GitHub release notes. differently.** This is one less thing that has to hold for the queue to behave, not a stall anyone was hitting. - - **A client connecting over loopback is told which models are loaded, whether or not an API key is set.** `GET /v1/models` carries `state`, `in_flight`, `last_used` and `pinned`, and used to carry them only on an install that had @@ -239,7 +293,6 @@ GitHub release notes. points its own hostname at `127.0.0.1` is refused. An install with a key behaves as before. - - **Under a shared model cache, every account now keeps its own settings and its own model list.** They used to be one `config.json` and one `registry.json` beside the models, which worked for whichever account ran @@ -427,7 +480,6 @@ GitHub release notes. where the VPN stops. Linked from the README and from the getting-started guide. - - **The chat client finds servers instead of asking you to name one.** Settings now lists every Gropius server advertising itself on your network, with the name it publishes, whether it wants an API key, and how many models @@ -497,7 +549,6 @@ GitHub release notes. closed on a pin it cannot read; and a repository with no pin is unaffected. Contributors whose git identity already matches see no change. - - **A first launch of the chat client no longer dead-ends.** It opened on a documentation example's host name, which resolves for nobody, and the message box stays disabled until a server answers — so the first thing a new user met @@ -507,7 +558,6 @@ GitHub release notes. install order puts there; the empty chat names Settings and has a button that opens it; and the disabled message box says why it is disabled. - - **Saving settings no longer empties the bind address.** The bind control offers two addresses, and a browser's ` + Measure each model’s context window when this Mac is idle + +

Off unless you turn it on. While it is on, whenever this Mac has been idle for the + threshold below, Gropius takes one model with no current measurement, sends it prompts of + growing length through its own endpoint, bisects to the largest the server accepts, and + records the window beside the one the model declares. A measurement changes nothing + until you press Use this window on the model’s card. It costs about + forty minutes of GPU per model, never pushes another model out to make room, and stands + down the moment anyone sends a request. Each model’s card also has + Measure now, which runs one probe whatever this box says. The + context + probe page says what the figure means and what it costs.

+ +

How long the last request must be in the past before the Mac counts as idle, + for the context probe and the self-test alike. Blank is five minutes.

+ + +
+ Self-test + +

Off unless you turn it on. While it is on, whenever nothing has asked this Mac + for a model for five minutes and nothing is downloading, Gropius loads one of your + models, runs the same short set of tests against it — how long the load takes, + how fast it reads a long prompt, how fast it generates, and what that becomes when + several requests arrive at once — writes one line of figures to a file in your + Gropius data folder, and unloads the model again if it was not already loaded. Each + model is measured once a day, and never at the cost of another: a model that would need + one pushed out to make room waits for a time when there is room. A request from anyone + ends the run at once. The file holds counts and timings only, never a prompt or an + answer, and nothing in it leaves this Mac. The + self-test + page says what it costs and where the file is.

+
+
Request statistics