# Maven Voice Protocol > Auto-generated from `internal/voice/wire.go`, `internal/voice/errors.go`, > `internal/voice/frame.go`, `internal/voice/client.go`. If this file and > those files disagree, the code wins. ## Transport TCP, length-prefixed JSON. Each frame is: ``` [4 bytes big-endian uint32 length][JSON payload] ``` The length is the number of **bytes** of the JSON payload that follows (excludes the 4-byte length prefix itself). Maximum frame size is **64 MiB** (`maxFrame = 64 << 20`), enough for ~33 minutes of 16k mono int16 PCM audio. The server binds a TCP address inside the WireGuard tunnel (production) or `127.0.0.1:9100` (local smoke test). The reference server port is configured via `voice.bind` in `mavend.json`. ## Frame types Three frame shapes share the same length-prefixed envelope. A reader distinguishes them by shape rather than a type tag: | Frame | Has `id`? | Has `kind`? | Direction | |-------|-----------|-------------|-----------| | Request | yes (`id` + `m`) | no | client → server | | Response | yes (`id`) | no | server → client | | Push | no | yes (`kind`) | server → client (async) | ### Request (client → server) ```json { "id": 1, "m": "push_to_talk", "p": { ... } } ``` | Field | Type | Description | |-------|------|-------------| | `id` | uint64 | Chosen by client, monotonically increasing per connection. Server echoes it back in the matching Response | | `m` | string | Method name (see Methods below) | | `p` | object | Method-specific params (omitempty) | ### Response (server → client) ```json { "id": 1, "r": { ... }, "e": null } ``` | Field | Type | Description | |-------|------|-------------| | `id` | uint64 | Matches the Request this replies to | | `r` | object | Result payload (omitempty). Set exactly when `e` is null | | `e` | object | Error payload (omitempty). See Error codes | A Response always matches a prior Request. It is written on the same connection immediately after handling. The client can block reading — there is exactly one Response per Request for today's synchronous methods. ### Push (server → client, async) ```json { "kind": "audio_nudge", "p": { ... } } ``` | Field | Type | Description | |-------|------|-------------| | `kind` | string | Push kind (see Push kinds below) | | `p` | object | Push-specific params (omitempty) | A Push is server-initiated. It can arrive on a connection that is also awaiting a Response. The client distinguishes them by checking whether `id` is present (Response) or `kind` is present (Push). ## Methods ### `push_to_talk` The reactive round-trip: client sends captured audio, server replies with synthesised audio + reply text. **Request params** (`PushToTalkReq`): ```json { "audio": { "fmt": "pcm_16k_mono", "data": "" }, "lang": "ru", "surface": "pc_client" } ``` | Field | Type | Description | |-------|------|-------------| | `audio` | Audio | Captured PCM. `fmt` is one of `pcm_16k_mono`, `pcm_44k_stereo`, etc. (see `internal/audio`). `data` is base64-encoded raw PCM bytes | | `lang` | string | Recognition language hint: `"ru"`, `"en"`, `"mixed"`, or unset for daemon default | | `surface` | string | Client's auth surface. Today the floor always sets `"pc_client"`; future mTLS/passkey handshakes populate this | **Response result** (`PushToTalkResp`): ```json { "reply_audio": { "fmt": "pcm_16k_mono", "data": "" }, "reply_text": "тихий режим включён. буду реже напоминать.", "transcript": "тихий режим", "routed_channels": null } ``` | Field | Type | Description | |-------|------|-------------| | `reply_audio` | Audio | TTS-synthesised reply audio (always present on success) | | `reply_text` | string | Same reply in plain text | | `transcript` | string | STT transcription of the input audio (omitempty) | | `routed_channels` | [string] | Away channels the dispatcher also delivered to (empty for today's reactive-only path) | ### `pong` Liveness response. The client sends this in reply to a `ping` Push to refresh its last-active timestamp on the server. **Request params:** none (empty `"p": null` or omitted). **Response result:** `null`. ## Push kinds ### `audio_nudge` Server has a proactive nudge to deliver. The client should play the audio. **Params** (`AudioNudgePush`): ```json { "rule_name": "water", "severity": 2, "audio": { "fmt": "pcm_16k_mono", "data": "" }, "text": "кажется, ты давно не пил воду.", "ts": "2026-07-03T12:00:00Z" } ``` | Field | Type | Description | |-------|------|-------------| | `rule_name` | string | The proactive rule that fired | | `severity` | int | 1–4 where 4 = most urgent | | `audio` | Audio | TTS-synthesised nudge body | | `text` | string | Same body as plain text | | `ts` | datetime | When the nudge was sent (RFC3339) | ### `ping` Liveness probe from the server. The client should respond with a `pong` Request to keep its session alive. **Params:** none (`"p": null` or omitted). ## Audio format The `Audio` type is always: ```json { "fmt": "pcm_16k_mono", "data": "" } ``` - **Sample rate:** 16000 Hz - **Channels:** 1 (mono) - **Sample format:** signed 16-bit little-endian int (int16) - **Encoding:** raw PCM, no header - **Wire encoding:** base64 inside the JSON frame The reference client wraps/unwraps WAV headers at the file edge (`audio.WAVFromPCM`, `audio.PCMFromWAV`). The wire never carries WAV. ## Error codes Errors are returned as an object in the Response `e` field: ```json { "id": 1, "r": null, "e": { "c": "unknown_method", "m": "optional diagnostic" } } ``` | Code | Meaning | Has message? | |------|---------|-------------| | `unknown_method` | The method name is not recognised | yes (the method name) | | `bad_params` | Params failed to parse / validate | yes (parse error text) | | `forbidden` | The surface is not authorised for this method | no | | `internal` | Server-side error (transient, retryable) | yes (diagnostic only) | The `message` field is never authority-bearing. Auth refusals carry `forbidden` with no message. ## Auth surface The server enforces capability scopes per surface. Today's floor assigns `SurfacePCClient` to every connection (full L3 access). Future passkey handshakes will set the surface from `mTLS` metadata / `WebAuthn` enrollment. Known surface values: - `voice` — voice/chat channel (structurally capped below destructive acts) - `pc_client` — reference desktop client - `authed_page` — mavweb /tools page - `telegram` — Telegram inbound ## Session lifecycle 1. Client connects via TCP to the voice address. 2. Server registers a Session (assigns an opaque ID, records `lastActive`). 3. Client sends Requests and receives Responses + Pushes on the same conn. 4. Proactive delivery routes to the most-recently-active session by `lastActive` timestamp. If no session is live, the dispatcher falls through to away channels. 5. On disconnect (EOF / read error / shutdown), the session is removed. ## Reference client `cmd/mavenclient` implements this protocol. Use it to smoke-test: ```shell # one-shot: send audio.wav → get reply.wav mavenclient -addr 127.0.0.1:9100 -in audio.wav -out reply.wav # listen mode: stay connected, write incoming Pushes to disk mavenclient -addr 127.0.0.1:9100 -listen -out-prefix /tmp/nudge- ```