This is the current streaming path for HAVPE and other long-lived WebSocket sources. Capture durability and semantic Conversation creation are deliberately separate state machines.
- Audio persistence never waits for or queries a Conversation.
- A WebSocket/recovery attempt maps to one
AudioCaptureSessionand one Redis WAL. - Only deliberate intent or detected meaningful speech creates a Conversation.
- The active Conversation pointer coordinates semantic jobs and plugins only; it never routes audio bytes.
- Finalization attaches range claims to immutable capture chunks; it does not move them.
HAVPE sends one audio-start for a connection and then streams PCM until disconnect.
The relay forwards Wyoming events to the backend WebSocket. Session initialization
creates the Redis session state and Mongo AudioCaptureSession, then starts independent
consumers over one stream:
audio:stream:{capture_session_id}
|
+-> streaming STT or windowed batch STT -> transcription results
+-> audio persistence -> immutable Mongo capture chunks
`-> wake-word detector -> wakeword:detections
The consumers do not own one another. In particular, an STT outage cannot make the audio persistence consumer discard bytes.
missing
|
| init_session + AudioCaptureSession.insert
v
active
|
| flush producer buffer -> append terminal WAL marker -> mark finalizing
v
finalizing
|
| persistence drains pending + new WAL entries, commits Mongo, then ACKs
v
complete
Mongo commit precedes Redis XACK. A retry reclaims pending messages and looks up the
deterministic source-message identity, so a crash after commit and before ACK cannot
duplicate audio.
Each Mongo chunk stores user_id, capture_source_id, capture_session_id, sequence,
absolute captured_at, duration, codec data, and WAL provenance. It has no Conversation
foreign key or Conversation-relative coordinates.
listening_for_speech
|
| meaningful transcript gate (and optional enrolled-speaker check)
v
materialize_detected_conversation
| deterministic segmentation_key
| set active_conversation_id in typed session state
v
monitoring
| update live transcript; stop on inactivity, close request, session end, or cap
v
finalizing semantic claim
| wait for persistence to reach claim end
| attach AudioRangeRef[] over existing chunks
| persist transcript artifact + Conversation transcript revision
| trim the claim, if configured
v
speaker / summary / memory-policy / plugin jobs
|
| clear active semantic pointer; re-arm speech detection if capture is active
v
listening_for_speech
The materializer can expose a live Conversation before persistence catches up, but the Conversation is not a persistence prerequisite. At finalize, an empty unbacked semantic shell is discarded; a real transcript is retained even if its audio claim could not be attached, because losing speech evidence is worse than keeping an audio-less record.
If live segmentation is off or yields no transcript, the capture still persists. After the capture (or a bounded off-mode compute window) completes, batch fallback:
- claims the available capture ranges;
- transcribes those ranges;
- persists an immutable
TranscriptArtifact; - runs the meaningful-speech gate; and
- creates a detected Conversation only when the gate passes.
No-speech output remains queryable processing/capture evidence and creates no Recordings row.
- File upload is deliberate: create a visible Conversation immediately, persist an idempotent finite capture, and attach its range.
- ScreenPipe is continuous evidence: persist the complete mixed window first, profile it
with VAD, and create visible detected Conversations only for speech-bearing clipped
ranges. Silence has an
AudioEvidenceSpanbut no Conversation. - ScreenPipe Conversations skip per-Conversation vault memory; settled Timeline-day memory owns the continuous-capture memory unit.
Wake-word detection is a sibling stream consumer. The dispatcher may read
active_conversation_id from typed session state to star or request closure of the
semantic Conversation. With no active Conversation the request is rejected rather than
stored for a future, unrelated interval.
| State | Purpose |
|---|---|
audio:session:{id} |
Typed capture-session state, format, status, active semantic pointer |
audio:stream:{id} |
Raw-audio WAL shared by independent consumer groups |
transcription:results:{id} |
Live STT evidence consumed by detection/monitoring |
transcription:interim:{id} |
Ephemeral UI updates |
transcription:complete:{id} |
Provider completion signal |
speech_detection_job:{id} |
Single-flight detection job ownership |
open_conversation:session:{id} |
Open semantic-job bookkeeping |
session:signal:{id} |
Immediate finalize/close wake-up |
wakeword:detections |
Wake-word events for the dispatcher |
There is no conversation:current:{id} audio-routing key and no always_persist
Conversation. The only Conversation pointer is a semantic field inside typed session
state.
The models and range resolver support one Conversation claiming several capture sessions. Automatically continuing an open detected Conversation across a WebSocket reconnect is still follow-up orchestration; reconnect does not compromise raw capture durability in the meantime.