qqq 0.2.0

Quick Query Quorum: an in-memory multi-user chat with an AI facilitator
qqq-0.2.0 is not a library.

QQQ

QQQ is a single-process Rust server for a small, facilitated conversation. It serves its browser UI, keeps sessions and credentials in memory, and uses an OpenAI-compatible provider for chat, optional transcription, and optional speech synthesis. Restarting the process discards all state.

Install and run

Install the release from crates.io, configure it, then run:

cargo install qqq
cp .env.example .env
qqq

To build and run a local checkout instead:

git clone https://github.com/maxylev/QQQ.git
cd QQQ
cp .env.example .env
cargo run --release

The embedded UI is at http://127.0.0.1:8787 by default.

Docker and HTTPS

Build and run the repository image with the same .env configuration:

docker build -t qqq:dev .
docker run --rm --env-file .env -e QQQ_BIND=0.0.0.0:8787 -p 8787:8787 qqq:dev

Published images are available from GHCR (pin a version tag in production):

docker run --rm --env-file .env -e QQQ_BIND=0.0.0.0:8787 -p 8787:8787 ghcr.io/maxylev/qqq:latest

Put a same-origin HTTPS reverse proxy in front of QQQ in production; browser microphone access normally requires a secure context, and QQQ does not terminate TLS itself. For nginx:

location / {
    proxy_pass http://127.0.0.1:8787;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Architecture and lifecycle

The server is deliberately one Rust source file and has no database, queue service, or accounts. The global session map only locates a session; each session has its own async lock. Provider requests occur outside that lock. Inputs are batched into one serial model loop per session and delivery is FIFO with at most one active delivery.

Creating a session starts private creator onboarding. The provider may continue the private conversation, or explicitly call start_group_session after the creator asks for a concrete group purpose. Only then is the session active and joinable. The generated group prompt is retained as the first history message. Every participant has a stable API name (user_1, user_2, …); display names are optional.

Configuration

Variable Required Default Meaning
OPENAI_API_KEY yes Provider bearer credential
CHAT_MODEL yes Chat-completions model
OPENAI_BASE_URL no https://openrouter.ai/api/v1 OpenAI-compatible API base URL
QQQ_BIND no 127.0.0.1:8787 Listener address
STT_MODEL no empty Transcription model
TTS_MODEL no empty Speech model; must be paired with TTS_VOICE
TTS_VOICE no empty Speech voice; must be paired with TTS_MODEL
TTS_FORMAT no mp3 Requested speech response format
OPENAI_HTTP_REFERER no empty Provider HTTP referer header
OPENAI_APP_TITLE no empty Provider application title header
MAX_SESSIONS no 100 In-memory session cap
MAX_PARTICIPANTS_PER_SESSION no 8 Participant cap
SESSION_TTL_SECONDS no 14400 Idle, socket-free retention period
MAX_HISTORY_MESSAGES no 512 Provider-history cap
MAX_AUDIO_BYTES no 10000000 Multipart audio-file cap
INPUT_BATCH_MS no 350 Input debounce interval
DELIVERY_TIMEOUT_SECONDS no 45 Delivery acknowledgement deadline
DEFAULT_LANGUAGE no en Default browser/provider language
PRIVATE_CONVERSATION_PROMPT no built in Creator onboarding instructions
GROUP_SESSION_PROTOCOL_PROMPT no built in Appended group delivery protocol

Required strings are trimmed and non-empty. Numeric limits must be nonzero. No provider setting is logged.

Audio modes

Browser recording is independent of configured speech services.

STT_MODEL TTS pair Browser audio input Delivery output
unset unset typed input_audio sent to CHAT_MODEL text
set unset audio/transcriptions text
unset set typed input_audio sent to CHAT_MODEL audio plus text
set set audio/transcriptions audio plus text

Direct chat audio includes raw-byte base64 and a required audio format, with a text content part preserving the participant identity. Multipart input accepts only the audio field and supported browser audio MIME types. The server authenticates and validates session/onboarding state before reading multipart bytes or contacting STT.

Authentication and HTTP API

Participant tokens are bearer credentials. They are returned only from creation/join and are never included in public session views. Except creation, every session HTTP route requires Authorization: Bearer <participant_token>.

Method and path Request Success
POST /api/sessions JSON {"browser_language":"en"} {"session_id","participant_token","api_name"}
POST /api/sessions/{id}/join JSON language object same creation shape
GET /api/sessions/{id} public session state
POST /api/sessions/{id}/bootstrap/answer JSON {"text":"…"} or multipart audio {"ok":true,"session_created":bool}
GET /api/sessions/{id}/onboarding/audio creator-only audio bytes
POST /api/sessions/{id}/input/text JSON {"text":"…"} {"ok":true}
POST /api/sessions/{id}/input/audio multipart audio {"ok":true}
POST /api/sessions/{id}/interrupt {"ok":true}
POST /api/sessions/{id}/deliveries/{did}/complete {"ok":true}
GET /api/sessions/{id}/deliveries/{did}/audio authorized active-delivery audio bytes
POST /api/sessions/{id}/leave {"ok":true}

JSON input is content-type checked, bounded, strongly typed, and rejects malformed bodies and unknown fields. Text is trimmed and limited to 4,000 characters for both onboarding and active input. Standard API failures use exactly:

{"error":{"code":"invalid_request","message":"The request could not be completed."}}

Status and code distinguish authorization, missing session, conflicts, provider failures, history limits, and audio-size failures. A stale or unauthorized delivery acknowledgement returns 409 with delivery_conflict and cannot alter a later delivery.

WebSocket and delivery sequencing

Connect to GET /api/sessions/{id}/ws, then send {"type":"hello","token":"…"} within ten seconds. Events are JSON with a type field:

  • session.ready
  • onboarding.prompt: delivery_id, text, audio
  • participant.joined, participant.left, participant.named: api_name
  • ai.thinking
  • delivery.preparing: target (user_N or all)
  • delivery.started: delivery_id, text, audio
  • delivery.completed and delivery.interrupted: delivery_id
  • input.accepted: api_name
  • floor.changed: floor_holder (or null)
  • error: code

Targeted deliveries are FIFO. A reply_to: "all" tool dispatch creates one delivery ID, synthesizes speech once, and sends the same started event to every currently eligible recipient. Each recipient is authorized to retrieve its audio and must acknowledge the shared ID. It completes after every pending recipient acknowledges, disconnects, or the timeout expires; next_speaker is applied once at completion. Public state exposes its target as all.

New accepted input interrupts an active delivery, clears obsolete queued deliveries, emits delivery.interrupted, changes the floor to the input participant, emits floor.changed, then emits input.accepted. Clients must abort playback/text waiting on interruption and must not acknowledge that delivery. Socket queues are bounded; saturation closes/removes the saturated socket rather than silently retaining a healthy-looking connection. Event serialization failures close the connection rather than emitting replacement data.

An authenticated active participant may also call /interrupt before recording. It interrupts active/queued delivery and changes the floor without submitting a model message; its events are delivery.interrupted (when applicable) followed by floor.changed.

Limits and operations

Multipart audio accepts exactly one audio field and is bounded both by MAX_AUDIO_BYTES and a small framing allowance; JSON has a fixed 16 KiB transport bound; provider dispatch text has a 2,000-character cap. Session memory grows only within configured session, participant, history, and audio limits. This is an in-memory service: it offers no persistence, horizontal coordination, account management, delivery recovery after restart, or TLS termination.

Checks and live tests

Offline checks:

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo package --locked
docker build -t qqq:dev .

Ignored live tests use configured provider credentials and may spend credits:

set -a; source .env.test; set +a
cargo test live_ -- --ignored --test-threads=1

The live suite exercises real chat promotion/group delivery and conditionally skips optional STT/TTS round-trip checks when their independent settings are absent.

License

MIT. See LICENSE.