What the code actually is
The README calls Always a “self-hosted, voice-first AI assistant.” The source is more specific than that. Four programs, three languages, and one machine:
- A FastAPI gateway (
gateway/server.py, 4,826 lines) that owns the browser WebSocket, buffers microphone audio, calls speech recognition, forwards text to the agent, and streams synthesised audio back. It exposes 26 HTTP routes, one/wsendpoint, and one WebRTC offer endpoint. - A voice worker (
voice-daemon/jcode_voice_worker.py, 1,010 lines) that keeps exactly one Rust agent process alive behind a five-verb unix-socket protocol so a voice turn does not pay harness startup cost. - A vendored Rust agent harness (
jcode/, version 0.12.2, 786.rsfiles, 390,138 lines, a 50-crate Cargo workspace) that does the actual reasoning and tool use. It is third-party code with a small local patch — see The harness. - A Next.js browser client (
apps/zola/, a fork of the Zola chat UI) whose voice logic lives in one 3,141-line React component.
Everything talks over loopback or unix domain sockets. Nothing in the repository requires a hosted
API; the model endpoints are OpenAI-compatible URLs on 127.0.0.1.
- gateway/server.py
- 4,826lines, one file
- HTTP routes
- 26+1 WebSocket, +1 WebRTC
- worker ops
- 5ping · shutdown · ensure_session · history · chat
- WS message types
- 8 in / 34 outclient → gateway → client
- python tests
- 803 unittest files
- systemd units
- 6all
--user
Sourcing
Every number and identifier on this site was read out of the working tree, not the README. Where the documentation and the code disagree — and they do, in eleven places — the code wins and the disagreement is recorded on Drift.
Four paths through one machine
Audio does not make a single round trip. It makes three, plus a fourth that has no user event at its head at all. Reading the four as separate signal chains is the fastest way to understand the codebase, because each chain is owned by a different file and each has its own failure mode.
gateway/server.py,
always_assistant/session_engine.py, voice-daemon/jcode_voice_worker.py,
voice-daemon/ambient_listener.py, asr_shim.py.Why path 4 needs its own socket
The chat client (voice-daemon/jcode_server_client.py) only reads its socket from inside
a chat() call. Events that arrive between turns sit in the kernel buffer until the next
turn reads them — and are then attributed to the wrong turn. Rather than demultiplex one socket
between two consumers, the shipped design opens a second one. That decision, and the attempt that
preceded it, are the subject of Ambient.
Where it runs
There is no container, no orchestrator, and no cloud dependency in the repository. Six
systemd --user units in systemd/user/ start six long-lived processes on a
single Linux desktop; three of those processes are model servers whose weights are deliberately not
checked in.
systemd/user/ plus the defaults in always_assistant/config.py. The dashed
ASR edge matters: the gateway's default recogniser URL is the Qwen3-ASR llama.cpp server on
:19070, and the Parakeet shim on :8788 is opted into by overriding
ALWAYS_QWEN3_ASR_URL. The shim exists precisely so the gateway need not know the
difference.| Endpoint | Bound | Started by | Code in this repo? |
|---|---|---|---|
| Always gateway | :8790 | always-satellite-gateway.service | yes — gateway/ |
| Voice worker | jcode-worker.sock | supervised by the voice daemon | yes — voice-daemon/ |
| jcode agent | jcode-server.sock | jcode serve, spawned by the worker | vendored — jcode/ |
| ASR shim | :8788 (per its own header) | no unit file ships here | yes — asr_shim.py |
| Parakeet ASR | 127.0.0.1:5092 | external | no |
| Qwen3-ASR | 127.0.0.1:19070 | qwen3-asr.service | no — llama.cpp + GGUF |
| Kokoro TTS | 127.0.0.1:8880 | kokoro-tts.service | no — Kokoro-FastAPI |
| EvoCUA VLM | 127.0.0.1:19110 | evocua-llama.service | no — llama.cpp + GGUF |
| Desktop indicator | — | pi-voice-indicator.service | yes — Tkinter overlay |
The gateway publishes two liveness endpoints. /health always returns 200 and reports
whether the worker answered a ping; /ready returns 503 when it did not. Both echo the
audio and auth configuration, which makes them the fastest way to confirm how a running instance is
actually configured:
$ curl -s http://127.0.0.1:8790/health | python3 -m json.tool { "ok": true, "worker_ok": true, "sample_rate": 16000, "tts_enabled": true, "auth_required": true } $ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8790/ready 200
worker_ok is the result of ping_worker(cfg) — a one-second
{"op":"ping"} round trip on the worker's unix socket.
auth_required reports the gateway's configured authentication state back to the
caller, which is what makes /health useful as a deployment check rather than just a
liveness one: it describes the instance, not the code.
The repository, with the dead wood marked
This is a working personal system rather than a released library, and it reads like one: several
modules are the fossil record of earlier harnesses. The naming preserves the lineage
— hermes_voice_worker (a Hermes CLI), then pi_voice_worker
(a Pi Agent RPC loop), then jcode_voice_worker (the current one). Only the last is
wired up; the HERMES_VOICE_* and PI_VOICE_* environment prefixes survive
as compatibility aliases throughout always_assistant/config.py.
| Path | What it holds | State |
|---|---|---|
gateway/ | FastAPI app (4,826 lines), chat store, static fallback UI | live |
always_assistant/ | reusable backend: config, asr, tts, audio_utils, session_engine, worker_client | live |
voice-daemon/ | nine modules: four live, one reachable only via the frontend, one orphaned, two legacy harness workers | mixed |
apps/zola/ | Next.js 16 chat UI fork; voice logic in one 3,141-line component; four always* API routes | live |
jcode/ | vendored Rust harness, 50-crate workspace, 390,138 lines, plus iOS, Figma and demo media | vendored |
asr_shim.py | 227-line FastAPI adapter that impersonates Qwen3-ASR over Parakeet, gated on Silero VAD | live |
desktop-delegate/ | 817-line EvoCUA computer-use CLI; screenshots via the XDG portal | standalone |
systemd/user/ | six unit files, all --user; each runs from a checkout rather than an installed wheel | live |
tests/ | three unittest files, 80 test methods, 74 of them in test_gateway.py | live |
scripts/ | one end-to-end ambient smoke test | cannot pass |
bin/ | hermes-voice (a systemd/PulseAudio wrapper), desktop-delegate (a two-line passthrough) | partly stale |
“This module avoids all three by being a completely independent process-local concern. No pool. No shared sockets. No threads spawning threads. Pure asyncio, one task per WebSocket session, one socket, straight-line reader loop.”voice-daemon/ambient_listener.py, lines 41–44 — verbatim
That paragraph is the most useful sentence in the repository. It is the module docstring of the file that replaced a design the documentation still describes, and it names the three specific failures that killed the earlier attempt. The whole of Ambient is an expansion of it.
What this is not
Reading only the code, several things the surrounding documentation implies are absent from the tree:
- There is no metrics or tracing backend. The gateway computes per-turn timings into a
TurnResult.metrics()dictionary and logs[STAGE]lines; nothing exports them. No Prometheus, no OTLP. - There is no multi-tenancy. A device registry exists and mints per-device tokens, but every session shares one agent process, one model configuration and one home directory.
- There is no waveform or VU display in the product. The browser computes a single RMS scalar
and writes it to the width of one
<span>. NoAnalyserNode, no canvas, no FFT anywhere inapps/zolaorgateway/static. The meter motif on this site is a reading device, not a screenshot. - The Rust is not the product.
jcode/is 6.7× the size of everything first-party in this repository and is a third-party harness. Always contributes roughly three patched files to it.
What is here, and works, is a complete local voice loop with an unusual fourth path bolted onto it. The remaining pages follow the signal in order.