# AGENTS.md — sm121 Rust CLI (`edition 2024`) for launching vLLM/SGLang inference containers across a DGX Spark cluster via SSH (`russh`). Docker default, tmux alternative. ## Memory discipline (the web process runs 24/7 on a 4 GB Pi) The add-on image builds the launcher **with `--features mimalloc`** (the CI Dockerfile does this; a plain `cargo build` stays on the default allocator). On musl the default malloc never returns memory to the OS, so the web process's transient per-tick spikes (report collects, SSE serialization, log strings) ratcheted RSS to ~2.7 GiB and the Pi OOM-killed it daily (2026-10-13, dmesg `oom-kill … task=sm121`). mimalloc trims; the web process now holds ~40 MiB. Keep the add-on build on mimalloc. Related levers already in place: the report refresh loop is lazy (full re-collect only on debounced file events; the 30 s tick only re-runs the stale cross-check, riding the dashboard's fleet bus — no extra SSH), and the SSH private key is parsed once and cached (`ssh.rs`). Heap hunting: `cargo build --release --features dhat` + graceful SIGTERM exit dumps a per-callsite retained-heap `dhat.json` (summarize with `/tmp/dhat-sum.py`). ## Dev quick-ref | Action | Command | |--------|---------| | Build | `cargo build` | | All gates | `make check` (fmt → clippy → test → ≤500 lines) | | Dry-run | `cargo run -- -d serve --recipe deepseek-v4-flash` | | Tests | `cargo test` (65, all passing) | | l10n | `locales/{en,ru,es,it,fr,de}/main.ftl` — Fluent FTL files | See [`DEVELOPMENT.md`](DEVELOPMENT.md) for architecture, module layout, and dev workflow. ## Commands ``` serve/stop/bench/logs/health/doctor/exec/power/tmux/init-host-keys/config/web/report/runlock/validate repos/promote/archive/unarchive ``` `validate` — offline recipe-contract gate (no cluster, no SSH): checks every `recipes//recipe.toml` against the launcher contract — `docker_image_dir` must exist under `docker-images/`, `port` must be the cluster default 30000, no floating `:latest` on engine images (`alpine` test images exempt), models must float (no `@revision`), `required_nodes` within 1..=8. Active recipes: violations are **errors** (exit 1). Archived lanes are grandfathered — their errors downgrade to **warnings** (advisory, always exit 0). Also runs as a `cargo test` over the repo's own `recipes/`, so `make check` fails if a new recipe breaks the contract. Rules live in `src/validate.rs`. `serve` records the launch in the recipe's run ledger (`recipes//.lock`, gitignored) and then **waits for the engine to actually come up** — polls `GET /v1/models` on the head node up to 30 min (`--no-wait` skips); a run that never reaches readiness is closed with its uptime and the serve fails. Follow mode watches liveness (30 s probes; 2 misses = died): a dead engine — or an ended log stream with a dead engine — fails loudly and closes the run. `stop` stops ALL nodes in parallel, every node runs the verifying command (polls up to 60 s that no labeled container remains) and failures are aggregated: one straggler cannot leave the other nodes un-stopped, and the error names the nodes that did not verify. The verified stop closes the run in the ledger. `--no-wait` opts out of the per-node verification. `report` — the table of tested recipes: markdown (default) or an interactive HTML table via `--serve`/`--open`. It shows each recipe's run-ledger state (● running + uptime — live in the HTML, launch count) and supports archive curation: `--archive ` / `--unarchive ` flip the recipe's `archived` flag (comment-preserving), archived recipes leave the output, `--archived` shows them instead. `stop` waits until the containers are actually stopped and removed (polls up 60 s per node) — an immediate `serve` on the same nodes would otherwise race the dying containers' GPU release. `runlock` — offline run-ledger management (the per-recipe `.lock`): `list` shows every recipe with an open (running) entry, `close ` closes the open run without any node verification. `stop` closes the ledger only after verifying every node of the cluster, so a node that died mid-run (or a killed serve) leaves the run open — `sm121 report --serve` cross-checks the ledger against a live fleet probe and renders such rows as orange "stale" (tooltip: `sm121 runlock close `) instead of green "up" when all nodes of the run's cluster are reachable but no running container of the recipe exists; an unreachable node is inconclusive and keeps the green (the fix for a confirmed stale row is `runlock close`). `bench` runs `tool-eval-bench` (from PATH) against a live serve and lands `_