# Lumi Companion Protocol v1

Status: experimental. The protocol is versioned independently from Lumi core, the server plugin, companion core, and companion transcription plugin.

## Trust boundaries

The OBS bridge accepts only same-user named-pipe clients and speaks only the local IPC subset. The companion owns pairing and connects to Lumi over TLS WebSockets. Lumi authenticates a device before accepting a WebSocket upgrade. The bridge never receives a Lumi device credential and never connects to the network.

## Server connection

- Endpoint: `wss://<lumi-host>/plugins/lumi_transcription/live`
- Header: `Authorization: LumiDevice <device-id>.<device-secret>`
- Maximum structured message: 64 KiB.
- Maximum binary audio message: 6,464 bytes (64-byte header plus up to 200 ms of PCM).
- The client sends `hello` first. Lumi replies with `hello_ack` or closes with an application error.
- Protocol v1 supports `pcm_s16le`; codec negotiation is still explicit so Opus can be added later.
- Heartbeats use `ping`/`pong`. A new connection is a new session unless `resume_session_id` names a resumable session owned by that device.

Structured messages use the envelope in `schemas/envelope.schema.json`. Supported client events are `hello`, `ping`, `source_update`, `obs_state`, `start`, `stop`, and `ack`. Server events are `hello_ack`, `pong`, `status`, `caption`, `metric`, and `error`.

## Binary PCM frame

All integers are little-endian. The fixed header is 64 bytes:

| Offset | Size | Meaning |
| --- | ---: | --- |
| 0 | 4 | ASCII `LACP` |
| 4 | 1 | protocol version (`1`) |
| 5 | 1 | flags: active `0x01`, muted `0x02` |
| 6 | 2 | header bytes (`64`) |
| 8 | 4 | sequence number |
| 12 | 8 | monotonic capture timestamp, microseconds |
| 20 | 16 | session UUID bytes |
| 36 | 16 | OBS source UUID bytes |
| 52 | 4 | sample rate (`16000`) |
| 56 | 2 | channels (`1`) |
| 58 | 2 | PCM bits (`16`) |
| 60 | 4 | payload bytes |

The payload is mono signed 16-bit little-endian PCM. Receivers reject malformed sizes and unsupported audio formats. Sequence gaps are reported; out-of-order frames are discarded. Every queue drops obsolete data rather than blocking a capture thread.

## Local OBS IPC

Windows transport is a per-user named pipe named `Lumi.Companion.ObsBridge.v1.<user-sid-hash>`. Messages are length-prefixed (32-bit little-endian), capped at 64 KiB for JSON and 6,464 bytes for audio, and begin with a v1 handshake. The bridge accepts only this allowlist: `hello`, `select_sources`, `caption`, `delivery_state`, `shutdown_notice`, and PCM frames. It cannot execute arbitrary commands.

Caption events are revision-aware. Consumers retain the greatest revision for each `(session_id, caption_id)` and discard older revisions, including after reconnect.

## Compatibility

Protocol v1 requires companion capability `transcription.capture.v1` and server capability `transcription.server.v1`. OBS delivery is separately negotiated as `obs.caption.native.v1`; its absence keeps test-mode simulation available but blocks live delivery.
