Files
command-center/agent/managed-skills/fleet-second-llm-endpoint/SKILL.md
T
Frank 135bb2194d agent: repoint roles to frank-medium/dankclaw-3090; sync niri tweaks, memories, managed skills
- 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
2026-09-17 12:27:08 -07:00

98 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: fleet-second-llm-endpoint
description: "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 N` DIVIDES
`--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
1. 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).
2. Planner: add the service to `SERVICE_ORDER` AND the target's service set in
`scripts/deploy-plan.py`, map its paths (including "the unit/env/serve trio
selects only me"), add it to the inventories in `deploy.sh` and
`deploy-remote.sh`, and the dispatch case.
3. 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, and `scripts/test-changed.sh` mappings are the usual victims
(a mapping to a test file retired earlier fails the pre-commit wrapper
silently for weeks).
4. 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`, `/cache` and `/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
```bash
./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 |