Chronicle has one streaming durability path:
backend buffer -> Redis Stream WAL -> journaled Mongo AudioChunkDocument -> XACK
The owner of this path is a technical AudioCaptureSession, never a Conversation.
There is no alternate persistence fallback. An error stops the transition and leaves
the last durable state intact.
The guarantee begins when Redis returns success from XADD. Redis is configured with
AOF appendfsync always, so success follows a filesystem sync. Before that point bytes
exist only in transport/backend memory. If ingress cannot commit them, Chronicle closes
the connection instead of pretending the capture stayed contiguous.
| From | Required commit | To | Failure behavior |
|---|---|---|---|
| Disconnected | audio-start begins initialization | Connecting | Reject connection |
| Connecting | unique Redis session/WAL, Mongo AudioCaptureSession, and persistence job are ready |
Active | Do not accept audio |
| Active | transaction verifies ACTIVE and XADDs capture fields |
Active | Preserve producer buffer; stop ingress |
| Active | residual buffer and terminal marker are appended, then status changes | Draining | Leave session retryable |
| Draining | producer declares no more legal XADDs |
Producer-finished | Preserve WAL/session |
| Producer-finished | persistence group has zero lag, zero pending entries, and no local PCM | Persistence-complete | Retain stream if proof is absent |
| Persistence-complete | every required consumer group is drained | Stream deletable | Retain stream |
Every connection attempt has a unique capture/session ID. Reconnect cannot reset or append behind an older WAL that is still draining.
| State | Evidence | Only legal next transition |
|---|---|---|
| Accepted/unread | persistence-group lag is non-zero | XREADGROUP makes entry pending |
| Pending | pending entry retains source bytes | encode and commit Mongo chunk |
| Mongo committed | chunk has capture identity and exact stream-message provenance | update capture metadata |
| Metadata committed | all Mongo writes succeeded | XACK exact source IDs |
| Acknowledged | group no longer owns the IDs | eligible for drain proof |
The chunk key (source_stream, source_first_message_id) is unique. A crash after Mongo
commit but before ACK therefore replays as validation plus ACK, not duplicate audio.
The separate unique (capture_session_id, sequence) index protects ingest order.
Each committed chunk contains:
user_id;capture_source_id;capture_session_idand sequence;- immutable absolute
captured_at; - duration/format/Opus bytes; and
- the exact Redis message IDs folded into it.
The Redis message carries an explicit capture time where available; otherwise its
Redis stream timestamp anchors captured_at. A missing/invalid absolute time is an
invariant failure, not permission to guess.
No WAL entry or Mongo chunk requires a Conversation ID. Semantic segmentation cannot interrupt or rotate persistence.
The persistence worker tracks two independent axes:
- reader:
recovering_pending -> tailing_new; - session:
active -> draining.
Completion is legal only at (tailing_new, draining) after the group is proven
drained. Any exception fails the RQ attempt; unread and pending Redis state remains,
and the retry re-enters pending recovery.
Uploads, ScreenPipe windows, and approved imports use convert_audio_to_chunks rather
than the Redis WAL. Their deterministic retry contract is:
- hash the complete source PCM;
- insert or load the specified
AudioCaptureSession; - reject any identity/format/hash/time mismatch;
- validate already stored contiguous chunks; and
- resume only missing chunks.
A completed retry returns the same IDs. ScreenPipe persists its full mixed window before VAD, so silence and semantic-processing failures cannot discard source audio.
Raw audio:stream:* keys must never be capped, age-expired, independently trimmed,
administratively ACKed without the Mongo side effect, or deleted while any required
consumer has unknown/non-zero lag or pending entries. Age is not durability evidence.
Likewise, deleting or archiving a Conversation cannot delete capture documents. Raw deletion requires a separate retention policy and a complete claimant check.
tests/test_audio_durability.py: append failure, Mongo failure, pending recovery, commit-before-ACK replay, retention gates, and legal transitions.tests/test_audio_persistence_lifecycle.py: close/finalize race.tests/test_streaming_persistence_invariant.py: capture initialization and reconnect isolation.tests/test_audio_persistence_mongodb.py: finite-capture retry convergence.tests/test_device_audio_ingest.py: silent ScreenPipe capture persists without a semantic Conversation.