Gopher's Ollama box runs one model, pinned: qwen3.8:27b-mtp-bf16, 74GB of BF16, held at a 262K context with keep_alive=-1. The whole point of a pinned model is that it never moves. Every chat message was moving it.
The Reload Tax
Two config ghosts were doing it. Gopher's .env still pointed at a host that no longer exists, asking for a model that isn't on the box. And every chat call forced num_ctx=32768 against a KV cache pinned at 262144 -- Ollama's only move was to evict the model and reload 74GB from disk. Per message. The fix was to stop doing both: point at the real box, request the pinned width every time, and log a warning whenever load_duration exceeds two seconds so a rogue client that evicts the pin gets named in one log line.
The Interim Fix That Died the Same Day It Was Born
The first fix probed Ollama's /api/ps and adopted whatever context width was already loaded -- don't reload, adapt. Reasonable. Then the actual need clarified: chat should always run at 262K, because the box is pinned at 262K. The adaptation machinery became dead code, so it was deleted. Net change: minus 53 lines, in the same session it was written.
That's the loop. Add when a need appears. Subtract when the need changes. Small commits make flip-flops free -- the policy went adopt-then-delete inside one afternoon because neither direction was expensive to reverse.
The Embeddings That Never Were
Found incidentally: gopher had an embeddings pipeline that had never written a single vector. The vector column existed, the code path ran, and every insert failed -- asyncpg has no vector encoder, and the pgvector codec was never registered. A broad except meant to cover "pgvector not installed" swallowed the real error for weeks. A silent except: pass around a database write is how a total pipeline failure hides in plain sight.
The fix was subtractive too: one codec registered as a pool init callback, one dedicated embedding model (qwen3-embedding:0.6b, 1024-dim), one auto-migration from vector(768) to vector(1024) driven by a width check at startup. The first embedding in gopher's history was persisted and verified end-to-end the same day.
Why Vectors At All
Gopher already runs vectors in production. Every face the cameras see becomes a 512-dim InsightFace vector, matched against the known_people table by cosine distance -- that is how "person detected" becomes "Ovais is at the door." The new app_states column is the same idea pointed at text.
Every five minutes gopher caches a snapshot of each connected app -- inbox0 tasks, meeting action items, connections. A JSON cache that knows must_count: 3 can tell you the count. It cannot answer "what am I forgetting about the API migration," because no field in that JSON is phrased like the question. An embedding of the question lands near the embedding of the summary that mentions it -- no shared words required. That is the entire trick: cosine distance between two vectors is semantic distance between two texts.
When that read path is needed, it is a SELECT with <=> over cached state instead of a live fan-out HTTP call to five apps. Not there yet -- the column is write-path today, deliberately. But it is a real write path now, so the future semantic query is a SELECT instead of a migration-and-backfill project. Same Postgres, one column, no new vector service. If the need never shows up, the column gets subtracted.
Subtracting 2,278 Lines
While in there, gopher's monolith got the same treatment. server.py went from 2,724 lines to 446: layers extracted first, then five route modules, none over ~530 lines, with the 56-route table diffed identical after every step. A test suite went from zero to 63 tests and caught three real bugs on its first run. Ruff cleared 63 findings. The pattern held to the end -- the day finished by syncing the architecture diagram to the code that actually runs, because even the docs follow the need.
Why This Way
Every app in this suite exists because a need existed, not because a roadmap said so. Gopher owns no data; it queries the apps that do. When a feature is needed it gets added; when the need moves on, the feature gets deleted. This week the additions were small -- a pinned context, a working codec, a warning log -- and the subtractions were huge: dead config, dead code, dead branches of a monolith, and a bug that turned out to be invisible architecture.
Just enough features is the discipline. Build what you need, add or subtract as the need changes. Every feature carried is a feature maintained, secured, and explained. Deleting isn't cleanup. It's the product.