- config.yml: default/commit -> frank-medium (dankserver); task/tiny/smol/vision -> frank-small (dankclaw-3090); vision xhigh - models.yml: add llamacpp (dankclaw-3090) provider; frank-large preserve_thinking + context notes - niri: focus-ring/border/shadow layout, hot-corners off, re-add noctalia.kdl include; blankpad eDP-1 scale 1.0 - memories: flush rollouts; retire frank_large + marreta_authentik skills; add auth-chain/kv-pool/quadlet-log skills - managed-skills: 40+ new fleet runbook skills - agent/skills: import built-in skill set
5.3 KiB
name, description
| name | description |
|---|---|
| fleet-second-llm-endpoint | Add a second LLM endpoint to a host that already serves one on the Dank fleet (new port, unit shape, co-tenancy budget, harness provider/roles, monitoring, cutover, verification) — with the port-bind race, the reasoning_effort template trap, and the tools/--jinja trap that silently break subagents. |
Adding a second LLM endpoint to a Dank fleet host
Use when a host must serve two models at once (e.g. an iGPU runtime plus a
second card), or when one endpoint is moving and a new one takes the freed
hardware. Rebuilt from the 2026-09-16 Dankclaw split (halogen frank-large on
:7770 + llama.cpp frank-small on :7771).
0. Decide the shape before touching hardware
- One provider per HOST:PORT in
~/.omp/agent/models.yml(on the box where the harness runs, not on the servers). A second endpoint is a new provider block, not a second model in the existing one. - Read the budget as an inequality, not a wish. For a unified-memory runtime
the engine's own fit check is the arbiter (
budget = host − pinned weights − reserve;need = KV + arena + vision); for a discrete card, weight the KV by counting only the full-attention layers — and remember--parallel NDIVIDES--ctx-size, it does not multiply. - A co-tenant is a budget line, not a footnote. Free host RAM by changing the POOL or the ARENA (the reserve is a fit-check term, never an allocation), and drop optional charges (a vision tower is ~2 GiB) rather than hoping.
1. Repo first, and retire in the same commit
- New service files:
scripts/<svc>.env(documented tunables),.service(root unit for a native binary; a Quadlet only for a container),serve-<svc>.sh(defaults + hard guards for missing artifacts). - Planner: add the service to
SERVICE_ORDERAND the target's service set inscripts/deploy-plan.py, map its paths (including "the unit/env/serve trio selects only me"), add it to the inventories indeploy.shanddeploy-remote.sh, and the dispatch case. - Retire what the cutover obsoletes into
.AGENTTRASH/with a suffix — and check what pointed at it: bench harnesses, a chat template only the old runtime read, andscripts/test-changed.shmappings are the usual victims (a mapping to a test file retired earlier fails the pre-commit wrapper silently for weeks). - Seams: one bats file per service asserting the invariants (
pool == 2 × ctx, KV bytes ≤ card budget, port separation), not just constant values. A negative grep must strip comments (grep -vE '^\s*#' file | grep -q ...) or it will match the prose that explains the very thing you removed.
2. The port-bind race (rootless podman)
pasta binds the published port at container CREATE. A predecessor that has just stopped still holds it, so a naive handler starts a doomed container:
Deactivated successfully <- predecessor gone
pasta: Listen failed for HOST TCP port */PORT: Address already in use
exit 126 <- only Restart=on-failure saves you
Order the handler: retire the old manager's unit → stop THIS service's own unit (no-op on a first cutover) → wait for every socket on the port to clear (listener, accepted, TIME_WAIT) → install → start. Without the second step a redeploy deadlocks against its own gate.
3. Wire the surfaces, and refuse impossible signals
- Harness roles: point the small/fast + image roles at the new endpoint, and move the vision role with the vision tower (only one endpoint should own each capability).
shared/homepage/services.json: a card per endpoint, probed from the monitor's own host (loopback is legal there; the LAN address otherwise).- Prometheus: only add a job if the runtime actually exposes Prometheus text.
Halogen publishes
/health,/cacheand/v1/*and nothing else — scraping it would be red by construction, so the job is REMOVED with the reason written down rather than left as a permanent red.
4. Cutover, then verify by driving it
./scripts/deploy.sh <target> --service <large> --service <small> # order matters
Then, per endpoint: /health; /v1/models reports the alias; a chat completion
returns content AND reasoning_content; a request carrying tools returns
tool_calls; an image request is answered (or refused with a named reason);
the runtime's own speculation counters move; and the unit is enabled /
generated with linger on. Read the engine's boot ledger for a silent clamp.
Finally spend one real request through the harness per changed role — routing
labels lie, the servers' own journals do not: journalctl -u <svc> shows which
endpoint actually served it.
Traps that look like model faults
| symptom | cause |
|---|---|
500 tools param requires --jinja flag |
the endpoint's runtime lacks the Jinja chat-template path, so every tool-using agent on it is broken. mainline llama.cpp defaults it on — pin it |
500 Jinja Exception: Unexpected reasoning effort <level> |
the model's own template has a narrower accepted set (Qwen3.8: xhigh/medium/low only) than the harness's levels; fold them in compat.reasoningEffortMap |
| the same request works by hand but a subagent dies | subagents run at a different thinking level — check both |
| the container dies instantly although the predecessor "stopped" | the port race above |