radio: darkmux in plain textInteractive help and a plain-text way to run things. Ask radio anything about darkmux and a local model answers from the command tree and your machine’s state; say what you want done and it routes to one advertised command, or refuses rather than guess. Two model calls at most, no tools, nothing invented.
radio is interactive help. The answering seat is handed the whole command tree, every runnable verb with its options and one line each, plus your config, the mission board, and the session’s recent outputs. A question about darkmux is a lookup, not a guess.
darkmux machine status. It will show exactly what models are loaded in LMStudio right now, grouped by ownership and matching profile.darkmux machine list to check the fleet roster and per-machine reachability. I don't have live telemetry on your other Mac, but that command will tell you exactly whether it's online and reachable.darkmux lab run inspect. You'll need the run ID; if you don't have it handy, run darkmux lab run list to pull one from the recent history. If you just want a quick overview of what happened, append --summary.Verbatim, captured 2026-08-28 on an M5 Max: qwen3.6-35b-a3b answering, qwen3-4b routing, humor at the shipped default of 50 unless shown otherwise.
Since 4.0 there is a command for that first question. /machine-status is a built-in advertised command that runs darkmux machine status, so a question like "which models are loaded?" has a command the router can send it to, and when it does, radio prints the loaded models instead of telling you which verb to run. Whether a given wording routes there is up to the routing model. It is read-only; nothing that unloads a model is advertised.
A setup problem is not a refusal. If the routing seat cannot run, radio says why once, names the fix, and exits 1, so a script can tell. No registry, a placeholder model, a model that is not downloaded, the LM Studio server down, and a missing lms each get their own message.
darkmux init to bootstrap one, or create it manually (see profiles.example.json in the darkmux repo).Captured from a home directory with no ~/.darkmux at all. Exit status 1.
One LM Studio instance serves one request at a time. If the answering seat's model is the same instance a coder is using, a question asked mid-run would queue inside LM Studio, silently, until the call's 300-second ceiling. radio checks first. Just before sending, it reads what LM Studio reports for that instance (lms ps: idle, reading a prompt, generating a reply) and whether another darkmux process holds it in use. If either says busy, radio answers at once: which model, what LM Studio reports, and the run it is busy with when darkmux knows it, or the darkmux process holding it. When neither is found, radio says what it checked: no live run on that model in the last day of darkmux's records, and no darkmux process it can verify holding it. It does not guess whose work it is, and a run that has been going for longer than a day may not be named. The check reads facts, not timing, but it is a check made just before the send: work that starts on the instance in the moment between the two still makes the question wait. On the CLI the exit status is 1, since no answer was given; the same words appear in the editor panel.
The routing seat is the exception. It runs on the machine's utility model, which also compacts context for running workers, and a routing call that lands during a compaction waits behind it by design. After ten seconds of waiting radio says what LM Studio reports for the utility model, one of: other requests are waiting on it (for example a compaction), so the routing call is sharing it; it is busy with nothing waiting, which is the routing call itself; it is idle, so nothing is being served ahead of the call; or darkmux cannot tell (an older lms without a queue count, the model not listed, or lms ps unreadable). If another darkmux process has the utility model loaded, radio names it alongside that reading; having it loaded does not by itself put a request ahead of the routing call, so radio says the call may be sharing the model with that process only when LM Studio's queue count is not available. Then it keeps waiting. A seat on a hosted endpoint is not checked: the queue, if any, is on the provider's side.
Two ways around a busy answering seat: ask again when the run finishes, or point radio.answerer_profile at a profile whose model the run is not using.
The answering seat has a persona, RADIO, and one knob: radio.humor, 0 to 100. It ships at 50. Under about 40 the model answers plainly; 100 is the full voice.
Verbatim, captured 2026-08-28 on an M5 Max: qwen3.6-35b-a3b answering, qwen3-4b routing, humor at the shipped default of 50 unless shown otherwise.
Two keys under radio in config.json; darkmux init writes them visible with their defaults. Each has an env override that wins live. The routing seat has no key: it runs on the machine's utility model, declared once as internal.utility in profiles.json (with its own n_ctx), the same model that compacts context for your workers. That model is never selectable for a task, and the router's calls show up in the fleet total as utility, never as runs.
| Key | Env | Default | Effect |
|---|---|---|---|
radio.answerer_profile | DARKMUX_RADIO_ANSWERER_PROFILE | unset | Profile for the answering seat. Same fall-through via role_profiles.radio-host. |
radio.humor | DARKMUX_RADIO_HUMOR | 50 | 0 to 100, substituted into the persona. Out-of-range values are clamped when read. |
The answering seat honors runtime.max_tokens_per_call as its per-call budget and otherwise uses 16,384; a reasoning model that spends the whole budget thinking gets reported as a failed answer, not a blank line. In the editor integration (darkmux acp) the host profile and humor are also session pickers (presets 10 / 50 / 75 / 100) that never touch config.json.
The same two seats sit behind darkmux acp, an agent server for editors that speak the Agent Client Protocol. In Zed, register it once and the advertised catalog becomes slash commands in the agent panel: /review, plus whatever you advertise yourself. Type without a slash and the message goes to the routing seat first, a small model that maps it onto exactly one advertised command or refuses; a refusal falls through to the answering seat with the session’s recent outputs in its grounding. A gated step asks for permission in Zed’s own dialog before it runs. The host profile and humor are session pickers there, so you can try a different answering seat without touching config.json.
// Zed settings.json
"agent_servers": {
"darkmux": {
"type": "custom",
"command": "darkmux",
"args": ["acp"],
"env": { "DARKMUX_ORCHESTRATOR": "zed" }
}
}
If darkmux is not on the PATH Zed launches with, give command the full path ($(brew --prefix)/bin/darkmux for a Homebrew install). DARKMUX_ORCHESTRATOR stamps every flow record the session writes, so the viewer can tell editor-driven work from terminal work.
radio only routes to what is advertised. Two commands ship built in, /review and the read-only /machine-status; a mission config in ~/.darkmux/mission-configs/ joins the catalog by carrying a panel block, and its description is what the router reads. A step that changes state carries "gate": "operator" and waits for your confirmation, at the terminal or in the editor.
{
"id": "pr-merge",
"name": "PR Merge",
"panel": { "description": "Merge a pull request once every check is green.", "hint": "[PR number]" },
"phases": [{ "id": "run", "tasks": [{
"id": "merge-task", "description": "merge the named PR", "reads": ["__panel_args__"],
"steps": [{ "id": "merge-step", "kind": "procedural.shell", "gate": "operator",
"config": { "command": "gh pr merge $DARKMUX_STEP_INPUT___PANEL_ARGS__ --squash --delete-branch" } }]
}]}]
}
__panel_args__ hands the step the text you typed after the command’s intent ("merge PR 2049" carries "PR 2049"), as an environment variable. darkmux ships no forge verbs of its own: GitHub, GitLab, and Perforce want different vocabularies, so gh lives in your config, never in darkmux. A graph with no model step runs in-process; one with a model step launches as darkmux mission launch <id>. The full config shape is in Task/Step graphs.
The answering seat has no tools. Everything it can say was gathered before it ran: the command catalog, the command index, your config list, a mission-board summary, the session’s recent outputs, and one named mission’s detail when you ask about it. If radio.answerer_profile resolves to a hosted endpoint, only the catalog and the command index are sent; your config, board, and outputs stay on the machine. Keep the seat local if you want answers about this machine’s state.