Changes:
- host_vars/astro-orbiter/vars.yml: add llm_router_models_max: 4 (overrides
conservative default of 1). Detailed VRAM OOM risk note included inline:
worst-case 3-model co-residency ~31GB > 24GB RTX 3090. LRU eviction
mitigates in single-user operation; flagged for Ryan's review.
- playbooks/day2_bump_router_models_max.yml: new targeted playbook; deploys
updated router unit, restarts the live service, verifies /health 200 and
/v1/models lists all 3 GGUFs post-restart.
- group_vars/all/semaphore.yml: add llm_router_update_unit template pointing
at the new playbook.
- roles/llm-inference-multimodel/defaults/main.yml: update comment to reflect
the var is now overridden in host_vars rather than 'hardcoded to 1'.
- roles/llm-inference-multimodel/templates/llama-server-router.service.j2:
correct stale 'HARDCODED TO 1' comment — value is variable-driven.
Constraints honored:
- --parallel 1 left untouched (not in scope, not modified anywhere)
- No ad-hoc SSH/systemctl/curl state mutation; all execution via Semaphore
- No installed/vendored code patched
Adds idempotent, data-driven GGUF staging for the two new router models on
astro-orbiter alongside the production Qwen3.6-35B-A3B-UD-Q4_K_S. Both files
were already staged live (byte-exact); this commit codifies them in Ansible so
future re-runs and any new model adds are version-controlled and audit-friendly.
Changes:
- roles/llm-inference-multimodel/tasks/stage_model.yml (NEW)
Idempotent per-model task: stat -> exact byte-size guard -> conditional
get_url -> ownership/mode ensure -> notify router restart handler only on
actual download. Loops from models.yml; nothing hardcoded.
- roles/llm-inference-multimodel/tasks/models.yml
Appends the stage_model.yml loop (tagged: models) after the existing Qwen3.6
download tasks. Data driven from host_vars/astro-orbiter/vars.yml.
- roles/llm-inference-multimodel/defaults/main.yml
Adds llm_staged_models: [] default (empty = safe no-op for hosts with no
staged model list defined).
- roles/llm-inference-multimodel/handlers/main.yml
Adds 'restart llama-server-router on new GGUF' handler. Only fires when
stage_model.yml performs an actual download or corrects ownership/mode.
Normal idempotent re-runs (files already correct) do NOT fire this handler.
- host_vars/astro-orbiter/vars.yml
Adds llm_staged_models list with the two new models:
* Phi-3.5-mini-instruct-Q8_0.gguf (4,061,222,688 bytes,
bartowski/Phi-3.5-mini-instruct-GGUF)
* Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf (4,920,739,232 bytes,
bartowski/Meta-Llama-3.1-8B-Instruct-GGUF)
- playbooks/day1_deploy_llm_inference_multimodel.yml
Updates header comment: removes stale 'Semaphore broken' note, documents
the correct execution channel (Semaphore template
llm_inference_multimodel_stage_models, --tags models).
- group_vars/all/semaphore.yml
Adds llm_inference_multimodel_stage_models template entry (config-as-code).
Template is scoped to --tags models explicitly. Phase 4 (verify) is
EXCLUDED: verify.yml starts llama-server-qwen on :8002, which would collide
with the production llama-server-router.service already running on :8002.
Semaphore template created via API: project 1 / template id 19.
Execution: triggered immediately after this commit via Semaphore REST API.
Promote llama-server-router to production on port 8002.
- Stops + disables llama-server-qwen (unit preserved as rollback target)
- Redeploys llama-server-router unit with --port 8002 (not 8003)
- PORT DECISION: rebind router to :8002 — zero Hermes profile config changes needed
- Re-runs validation gates 1-3 against production endpoint (hard gates)
- Gate 4: bundled SvelteKit UI check (HTTP 415 from ansible.builtin.uri is
a false negative — llama.cpp requires Accept-Encoding: gzip; real browsers work)
- Rollback via --tags cutover_rollback (uses 'never' Ansible tag to prevent
accidental execution during normal cutover run)
All 4 gates PASSED on cutover run 2026-08-12:
Gate 1: n_ctx=65536 >= 64000 PASS
Gate 2: finish_reason=tool_calls PASS
Gate 2b: finish_reason=stop (no spurious tool_calls) PASS
Gate 3: 20410 MiB / 23000 MiB ceiling PASS
Gate 4: SvelteKit HTML confirmed via curl + gunzip PASS
Production endpoint: llama-server-router on :8002 (enabled + running)
Rollback target: llama-server-qwen unit at /etc/systemd/system/ (stopped, disabled)
- New ansible/roles/jmri role: installs OpenJDK 21 headless, creates
jmri service user, downloads JMRI 5.10, deploys JmriFaceless systemd unit
- Handles dialout group membership for serial device access
- Config restore task for post-reinstall recovery from GitHub backup
- host_vars/main-street-station: profile_id and serial device (TODO: fill in)
- Inventory: jmri_server group with main-street-station at 192.168.10.45
- Playbook: day1_deploy_jmri.yml (linux-baseline + jmri)
Collection not installed in Semaphore's Ansible environment.
Direct HTTP API call to Technitium on :5380 is simpler, has no
collection dependency, and is naturally idempotent (add returns ok
on duplicate records).
Two fixes uncovered while wiring up the first new Traefik route
(hall-of-presidents.local.mk-labs.cloud for honcho):
* traefik_host changed from 10.1.71.35 -> lightning-lane.local.mk-labs.cloud
so wed/.ssh/config rules (Host *.local.mk-labs.cloud) match and the
rsync step uses the right IdentityFile.
* requirements.yml: pin effectivelywild.technitium_dns >=1.1.0 — the
playbook depends on it but the collection was not declared. Bare
checkouts would fail without it being installed manually.
The half-disk LV pattern affects ~90% of mk-labs VMs. Treating the
fix-up as application-specific (as it was in day1_deploy_honcho.yml)
means future deploys would each carry the same boilerplate, and any
day1 author could forget it.
This commit:
* Adds playbooks/day0_expand_root_lv.yml — standalone day0 step,
targets {{ target | default("all") }}, honors a per-host
expand_root_lv_skip opt-out for multi-LV layouts.
* Adds playbooks/day0_provision.yml — umbrella playbook chaining
day0_linux_baseline + day0_expand_root_lv, so the operator runs
ONE command per new VM.
* Removes expand_root_lv from day1_deploy_honcho.yml — day0 is
assumed complete before day1 begins (cleaner separation of
concerns, matches the convention day1_deploy_semaphore already
follows).
* Updates the role README to document the lifecycle position and
the opt-out flag for hosts with multi-LV plans.
Reclaims the half-disk LV left by the Ubuntu Server autoinstall
template default. Idempotent — no-ops cleanly when there are no free PE
in the VG, and exits the play cleanly on hosts without LVM.
Supports ext4 and xfs. Does not handle partition resize (cloud-utils
growpart) — out of scope for the template fix-up case.
Wired into day1_deploy_honcho.yml ahead of the honcho role so newly
provisioned VMs get the fix-up automatically. Suitable to add to any
day1 playbook by simply listing it before the application role.
Deploys Honcho (plastic-labs/honcho) as a rootful Podman + Quadlet
service on the lincoln VM (10.1.71.132). Three containers on a
user-defined network:
- honcho-postgres pgvector/pgvector:pg16
- honcho-api FastAPI on :8000
- honcho-deriver background worker for theory-of-mind derivations
LLM provider: Anthropic Claude (claude-sonnet-4-5). Switching providers
is two env-var changes — see README.
Traefik route hall-of-presidents.local.mk-labs.cloud -> lincoln:8000
added under boilerplates/traefik/dynamic/. JARVIS itself talks to
Honcho directly at lincoln:8000 (east-west); the Traefik alias exists
only for browser access to the Swagger /docs UI.
Requires three new vault entries before first run:
- vault_honcho_database_password
- vault_honcho_jwt_secret
- vault_honcho_anthropic_api_key
Complete rewrite of the semaphore role. Supersedes three prior
iterations whose admin-user-creation logic was unreliable across
Semaphore CLI versions.
Architecture:
- Rootful Podman Quadlet under /etc/containers/systemd/
- Separate PostgreSQL 16-alpine container on a user-defined
podman network (semaphore-net)
- Named volumes for both data stores (semaphore_data,
semaphore_postgres_data) so container recreation is
non-destructive
- Pinned image tags: semaphoreui/semaphore:v2.18.5-ansible2.16.5
and postgres:16-alpine
- Post-deploy HTTP health check fails the playbook if Semaphore
doesn't respond on /api/ping within ~60s
Admin user creation remains intentionally manual after first deploy;
the role README documents the exact podman exec command.
Removes the duplicate deploy_semaphore.yml and the now-unneeded
cleanup_semaphore.yml; day1_deploy_semaphore.yml is the canonical
entry point.
Introduces a single, idempotent baseline role to supersede the
overlapping day0-baseline and common roles. Capabilities are
feature-flagged so they can be toggled per-host:
- packages (common + OS-family + per-host extras)
- timezone + locale
- chrony time sync against sundial
- baseline users (jarvis admin account with SSH key + NOPASSWD sudo)
- SSH hardening via /etc/ssh/sshd_config.d/ drop-in
- unattended security upgrades (Debian family)
- sysctl drop-in at /etc/sysctl.d/99-mk-labs.conf
- journald retention caps
- branded MOTD
Ubuntu/Debian is first-class; vars/RedHat.yml provides a placeholder
for future distros via the ansible_os_family pattern.
The legacy day0-baseline and common roles remain in place for now and
will be removed during the playbook cleanup sweep, alongside the
existing playbook naming inconsistencies.
- Explicit playbook for removing old container, systemd services, and quadlets
- Optional semaphore_force_clean variable for data removal
- Safer than tags for destructive operations
- Parse Host() rules from router definitions
- Supports multiple hostnames per service file (e.g. semaphore + imagineering)
- More robust and future-proof
- Move all roles from playbooks/roles/ to roles/
- Update roles_path in ansible.cfg
- Add cast user to common role
- Create standalone podman role
- Add semaphore role with Podman + Quadlet support