Listen + Pusher Pipeline — Sequence Diagrams
Last updated: 2026-07-09 (recording-scoped desktop listen lifecycle binding) These diagrams document the real behavior observed during E2E testing with live services (backend, pusher, STT providers, embedding API). Update when the pipeline changes.
1. Connection + Streaming + Transcription
The STT provider is selected at session start viaSTT_SERVICE_MODELS env var
(e.g., modulate-velma-2,dg-nova-3). The first provider supporting the
requested language wins. Listen startup loads transcription preferences once and
reuses the embedded user language for translation targeting, avoiding a second
user-preference read on WebSocket startup. All providers implement the
STTSocket ABC and are wrapped by the universal GatedSTTSocket VAD gate.
Segments buffered in realtime_segment_buffers are sorted by start time
before processing — this corrects non-deterministic WebSocket arrival order
from providers like Modulate whose internal parallelism can deliver shorter
utterances before longer ones.
When /v4/listen receives a client-generated client_conversation_id, that
UUID is the authoritative identity for the recording. The backend creates or
resumes that exact conversation and does not fall back to the legacy
user-global in-progress pointer. This prevents a stale Redis or Firestore row
from being rebound to a new desktop recording.
The backend emits a conversation_session event carrying that recording
identity:
conversation_id and recording_session_id match their persisted client UUID.
For an identified session, lifecycle/finalization uses that exact identity; it
must not timestamp-match or force-process a nearby user-global conversation.
Timestamp/source matching is legacy-only compatibility behavior.
Capture-device provenance
Every capture client with WebSocket-upgrade header support sendsX-App-Platform and X-Device-Id-Hash on /v4/listen. The backend records
that provenance on the conversation stub, allowing extracted canonical
memories to participate in the This device filter. Browsers cannot attach
custom upgrade headers, so /v4/web/listen carries the hash in its required
first auth message; the backend fixes its platform to web before creating
the conversation. Missing provenance remains unknown rather than being
assigned to a different device.
2. Conversation Lifecycle (Silence Timeout Path)
This is the normal path when the client stays connected but stops speaking. Key design rule (#6061): Listen NEVER processes conversations locally. All conversation processing routes through pusher viarequest_conversation_processing().
When pusher is unavailable, conversations stay buffered in pending_conversation_requests
and are flushed when pusher reconnects.
3. Disconnect Path
What happens when the WS connection closes. Teardown invariant: flush any remaining multi-channel mix viaaudio_bytes_send before pusher_close() so ListenPusherSession._flush() can deliver tail audio to pusher.
3.1 Pusher Reconnect & Pending Flush
When pusher reconnects after a disconnection, all buffered conversations are replayed.4. Speaker ID Lifecycle (2-Session Flow)
Speaker identification requires two sessions: one to store the embedding, one to match against it. The user’s own voice is also identified via their speech profile embedding (loaded at session start alongside person embeddings).5. Private Cloud Sync (Audio Upload)
Whenprivate_cloud_sync_enabled is set for the user.
6. Event Wire Protocol
Server → Client (JSON over WS text frames)
Event Types
Client → Server
7. Timing Constants
8. WebSocket Task Supervision
Bothtranscribe.py and pusher.py use an asyncio.wait(FIRST_COMPLETED) supervisor loop instead of asyncio.gather() to manage background tasks. This prevents ghost connections where a hung background task blocks cleanup forever.
Supervisor exits on:
- Client disconnect (receive task completes)
- Background task crash (exception)
- Lifetime task normal completion (e.g. heartbeat inactivity timeout)
- Finite task normal completion (e.g.
process_pending_conversations,speaker_identification_task)
BG_DRAIN_TIMEOUT (30s) before force-cancel. The connection gauge (inc/dec) is always paired in try/finally.