Agentic epidemiology: building MemeLab by voice, from anywhere
Open WebUI, Whisper, Ollama and custom plugins turn opencode and claude-code into a voice-driven remote lab for MemeLab, a pattern that fits any field.

Some of the recent work on MemeLab was dispatched from a phone, by voice. Not "I SSH'd in from a phone and suffered." I opened a chat app, held the microphone button, described the change I wanted, and a coding agent on the lab machines did the work: read the code, made the edit, ran the type-checker and the test suite, and reported back into the same chat thread. When I got back to the desk, the diff was waiting for review.
I've started calling the practice agentic epidemiology: an epidemic simulator whose invariants are machine-checkable, long-horizon coding agents that can be trusted with hour-long tasks, and a dispatch surface (voice, chat, any device) thin enough that the science moves at the speed of deciding, not typing. The epidemiology part is my field. The agentic part, as I'll argue at the end, belongs to anyone with a model and a test suite.
This post is the story of that setup: Open WebUI as the front-end, Whisper for speech-to-text, Ollama serving Qwen3.8 27B and similar local models for the everyday chat duties, and a pair of custom Open WebUI plugins that bridge chat conversations to opencode and claude-code sessions running in the MemeLab repository on the backend.
Why chat, not a terminal
Terminal coding agents changed how I work on MemeLab at the desk. Both opencode and claude-code are good at the loop this codebase demands: read the docs, make a change, then prove it didn't break anything, because MemeLab's failure modes are silent. Its invariants (population conservation, tick-for-tick determinism, permalink completeness) don't throw when violated; results just quietly stop being reproducible. An agent that runs the type-check and the Vitest suite after every edit is safer here than a tired human who "will run the tests before pushing, promise".
But terminal agents assume you are at a terminal. Away from the desk, the things I wanted were small and specific: kick off a task, check on a task, answer an agent's question. All three are chat-shaped. And the best-in-class self-hosted chat front-end is Open WebUI: it installs as a PWA on the phone, keeps threaded history, and has built-in voice input backed by Whisper, so a spoken request arrives at the backend as clean text.
The missing piece was the middle. Open WebUI speaks to LLM APIs (my instance was already wired to Ollama serving local models) but it doesn't speak to coding agents working in a repository. So I built it.
Why this works now: the leash got longer
Three years ago this whole setup would have been a toy, and it's worth being precise about why it isn't one anymore. The best measurement I know of is METR's time-horizon work (Measuring AI Ability to Complete Long Tasks, March 2025, now maintained as a live leaderboard): take a large set of software tasks, measure how long each takes a human professional, and find the task length at which a model succeeds 50% of the time when working autonomously.
The numbers are stark. In METR's current dataset, GPT-4, the frontier model of March 2023, had a 50%-success horizon of about 4 minutes of human-professional work. Claude Sonnet 3.7 (February 2025) reached about an hour; Claude Opus 4.5 (November 2025) about five. Claude Opus 4.6 (February 2026) measures about 12 hours, and Claude Mythos Preview (April 2026) about 17 hours, with wide error bars, as every point on that curve has. Point estimate to point estimate, that is a ~250× longer leash in three years, and the trend behind it is exponential: METR's fit since 2023 doubles every 129 days (their confidence interval runs 104–158), against a slower ~188-day doubling over the whole 2019-onward series.
Every dot is a model in METR's v1.1 dataset (fetched 29 August 2026); only some are labelled. The amber marker is Qwen3.8 27B's release date: METR hasn't measured it yet, so it gets a date, not a height.
Which raises the question of where the local model in my picker sits. Nobody has measured Qwen3.8 27B on METR's ruler, so I did the next best thing: find a second ruler that both sides have been measured with. Seven of the models in METR's dataset also sit on the Terminal-Bench 2.0 leaderboard (an agentic terminal-task benchmark), and so does the previous generation of my local model, Qwen3.6 27B. Its score there, 59.3%, is identical to Claude Opus 4.5's, the model METR clocks at about five hours.
Two rulers on the same models: horizon rises with Terminal-Bench score. The amber marker is last generation's 27B open model, tying Opus 4.5 on the benchmark both have taken. Terminal-Bench 2.0 scores from the leaderboard mirror at llm-stats (fetched 29 August 2026); horizons from METR v1.1.
The 3.8 generation then moved the goalposts. Alibaba's own model card runs both 27B models on the newer Terminal-Bench 2.1 harness: Qwen3.6 27B scores 63.4, Qwen3.8 27B scores 73.0, and Claude Opus 4.6 Max, METR's ~12-hour model, scores 78.2 on the same run. On SWE-bench Pro the card has the 27B at 61.7 against 53.4 for Opus 4.6 Max. Vendor numbers, not time horizons. But read the two rulers together and the picture is consistent: last year's 27B kept pace with a five-hour model, and this year's is within five points of a twelve-hour one, on an Apache-2.0 checkpoint that fits on a single 24 GB GPU. That is why it earned a seat beside the cloud agents, and why wiring it in as a third agent backend, through opencode's support for local models, is my next experiment rather than a fantasy.
Why it matters here: the tasks I speak into a phone are 30-to-90-minute errands in human terms. In 2023 that was an order of magnitude past the frontier: METR's original study found models near-perfect under 4 minutes and under 10% beyond 4 hours. In 2026 a one-hour errand sits comfortably inside the measured envelope. The remote loop didn't get clever; the agents got long.
One caveat about that ruler, because it is easy to over-read. METR measures software tasks, timed against software professionals. So does Terminal-Bench. Neither measures whether a model can do epidemiology, and I am not claiming they do. What I dispatch from the phone is coding work that happens to live in a scientific codebase (wire up a cost term, make two code paths consistent, run the suite), and that is squarely the thing METR timed. The epidemiological judgement, which experiment is worth running and whether its answer means anything, never leaves the desk. The horizon curve tells me the typing half is now long enough to hand over. It says nothing about the deciding half, and no benchmark I know of does.
The stack
| Layer | Component | Job |
|---|---|---|
| Phone | Open WebUI as a PWA, over a VPN to the lab | Chat UI, threads, mic button |
| Speech | Whisper (local speech-to-text in Open WebUI) | Voice → text before anything sees it |
| Everyday chat | Ollama serving Qwen3.8 27B + similar local models | Quick questions and drafts; never touches the repo |
| Bridge | Custom Open WebUI pipe plugins (Python) | Expose each agent as a "model" in the picker |
| Agents | opencode and claude-code, headless mode | The actual reading, editing, testing |
| Ground truth | The MemeLab repo + npm run typecheck + Vitest | What "done" has to mean |
| Iron | Four machines presented as one system | Where all of the above runs |
That last row deserves a sentence, because "the lab machine" in this post is really four: a couple of consumer desktops, a laptop, and an NVIDIA DGX Spark, bought one at a time over the last seven years. They are stitched together so that, from any device I'm holding, they behave like a single operating system: one place to log in, one view of the repository, GPUs where the local models live. I'm deliberately not describing how (that's an operational-security line I keep), and nothing in it is exotic. But the effect is a small supercomputer in the honest sense, more aggregate compute than any one box I own, and it's what lets Whisper, Ollama, two coding agents and a Vite dev server all run at once without anyone waiting on anyone.
One loop, end to end: speak on the phone, Whisper transcribes, a pipe plugin routes the text to a headless agent session in the repo, and the agent's report streams back into the chat thread.
The plugin: a pipe from chat to agent
Open WebUI's plugin system has a concept called a pipe: a small Python class that shows up in the model picker like any other model, but that can do whatever it wants with the incoming conversation. My instance's picker already listed the local models Ollama serves: Qwen3.8 27B for quick questions and drafts. The pipes add two more entries beside them that aren't models at all: instead of forwarding the message to an LLM API, each one spawns a headless coding-agent process in the MemeLab working tree and streams its output back into the chat.
Both agents ship a non-interactive mode built for exactly this: claude-code
emits its progress as a stream of structured events; opencode has an
equivalent run command. The pipe hands the transcribed message to that
process, watches the stream, and turns it back into chat: tool activity
becomes status lines, the agent's prose becomes the reply.
The two details that took iteration to get right:
- Thread ↔ session mapping. A chat thread in Open WebUI must map to one persistent agent session, or every follow-up message starts an amnesiac agent. The pipe remembers each thread's agent session on the first response and resumes it on every message after. Once that worked, "actually, make it hexagonal too" behaved exactly like it does at the terminal.
- Rendering tool events, not hiding them. Early versions yielded only the
agent's final text, which meant staring at a spinner for minutes. Now every
tool event becomes a one-line status in the chat (
▸ read src/sim/engine.ts,▸ npm run typecheck), so from the phone I can see what the agent is doing and interrupt if it's off in the weeds.
A second, near-identical pipe wraps opencode, so switching agents is just switching "models" mid-thread.

The whole point: a spoken sentence goes in, a reviewable diff comes out.
Guardrails: what "done" means for this codebase
Handing write access to an agent you supervise from a phone only works if
"done" is machine-checkable. MemeLab is unusually well suited to this,
because the repository already defines done precisely: the per-change
checklist in docs/ROADMAP.md, and invariants that are enforced by tests
rather than by vigilance.
So the pipes append a standing instruction to every task: run the type-check and the test suite before reporting done, and quote the output. The agent's final chat message therefore ends with something I can actually trust from a phone screen: the Vitest summary from the suite in the public repo (github.com/TheMemeticist/MemeLabV3), currently 9 test files and 159 tests, all passing. That suite includes the tests that pin the two silent invariants, same-seed determinism and S+E+I+R+D population conservation. If those pass, the change didn't quietly break reproducibility; if they fail, the agent says so in the thread and waits.
Two rules I hold regardless of what the agent claims:
- Agents never push. The loop ends at a local commit at most. Publishing happens at the desk, after I've read the diff.
- Structural changes get re-read. Anything touching the engine's RNG,
the buffer swap, or
SimConfigfields (which must round-trip through the permalink codec) gets a human read even with green tests, because a test suite only pins the invariants someone thought to pin.
"Done" is a claim the pipeline makes, not the agent.
What voice actually changes
Voice input wasn't the gimmick I expected. Dictating code is miserable; dictating intent is great. The messages that work are the ones you'd say to a colleague:
"In the cost model, the lockdown day-cost should scale with compliance, not with raw uptake. Check how the quarantine cost does it and make it consistent. Run the tests."
Whisper handles that fine. Where it stumbles is project jargon: "Voronoi" has arrived as "for annoy", and SEIR as "say ear". The fix was boring and effective: a small custom-word list in the transcription settings, and the habit of naming files by role ("the geometry layer") instead of by path.
The other change is rhythm. A phone session is dispatch: describe the task, watch the first status lines to see the agent has understood, pocket the phone. The desk session is judgment: read the diff, run the app, decide. Splitting those turned dead time (a commute, a queue) into the cheap half of development, and kept the half that needs a human at a screen where a human is actually looking.
Agentic epidemiology, and agentic everything else
Strip the epidemiology out and what's left is a three-part pattern: a model of some corner of the world whose correctness a machine can check, agents whose time horizon covers one unit of work on it, and a dispatch surface cheap enough to reach from a queue at the shops. MemeLab happens to model contagion. Nothing in the loop knows that.
That's why I think the label generalises. An ecologist with a predator–prey model and a conservation law, an economist with an agent-based market and a budget identity, a materials group with a lattice solver and a symmetry check: each already owns the hard part, the invariant. Each could be speaking experiments into a phone this afternoon with the same open components. The acceleration isn't that the agent is smarter than the scientist; it's that the scientist's bottleneck moves from typing the experiment to deciding which experiment, and deciding is what they were for anyway.

The loop doesn't know it's doing epidemiology. Any field with a checkable model can borrow it.
The caveat is the same invariant, inverted: this only accelerates fields that can say what "wrong" looks like in code. Hand a long-horizon agent a model with no conservation law, no reference run, no reproducibility test, and you get exponentially more unverified output, which is a slower way of being wrong, not a faster way of being right. Build the gates first. Then buy the microphone.
Honest limitations
- Long tasks vs. chat streaming. A multi-minute agent run holds a streaming response open; a flaky mobile connection can drop it. Session resumption makes that recoverable rather than fatal.
- Permission prompts don't exist in a pipe. Headless agents can't ask "may I run this?" mid-flight, so the pipes run with a pre-approved, scoped allowlist (file edits, npm scripts, git, no network beyond the package registry). Anything outside it fails; the agent reports the refusal.
Reproduce it
The setup generalises to any repository with a real test gate, and every piece is open source. On the dev box: install Open WebUI (which brings the chat front-end and Whisper-backed voice input), point it at Ollama with Qwen3.8 27B or whichever local model you like for everyday chat, and install claude-code and opencode. Verify each agent's headless mode by hand from a terminal in your repo first, asking it to run your test suite and summarize, then wrap that same invocation in an Open WebUI pipe function (added under Admin → Functions), one per agent.
Install Open WebUI as a PWA on the phone over your VPN, and the model picker becomes a remote control for your repo. For MemeLab the gate is public: clone MemeLabV3, install, run the type-check and tests. The simulator the loop exists to improve is live at epi.meme.