Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions advanced/signal-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,19 @@ impl SessionStore for SessionAdapter {
}
```

### Flush scheduling: send vs. receive

*When* the dirty Signal cache reaches the backend differs by direction, because the two directions have different recovery properties:

- **Send** (DM, group, and status sends) flushes **synchronously, before the stanza reaches the wire**, and propagates a persistence failure by aborting the send. Reusing an outbound counter reuses its message key and IV, so the ratchet advance must be durable before anyone can act on the ciphertext — the send must not transmit an advance it couldn't save.
- **Receive** (live traffic, outside the offline-drain batcher) routes through a single-flight coalescing scheduler (`src/signal_flush.rs`) instead of flushing per stanza: a burst of receives folds into one flush per ~25ms window, retried with exponential backoff (up to a 5s cap) on backend failure. This is safe because a lost receive-side advance simply re-derives forward on the next message (the receiving chain derives `CK_n → CK_n+1`), and a consumed one-time prekey stays buffered until its session is durable — a crash inside the window is recoverable.

The scheduler is generation-scoped (embeds the connection generation in its atomic state), so a reconnect during an in-flight flush needs no explicit reset: a stale worker from the previous connection cannot mutate the new generation's state, and stands down when it observes a foreign generation.

The offline drain, retry-receipt recovery, identity-change recovery, and teardown all keep their own **synchronous** flushes — they gate acks, receipts, or follow-up reads on durability and are not routed through the receive coalescer. See [Inbound Durability Hook](/advanced/inbound-durability) for the drain-batch commit ordering, which this coalescing does not change.

Call [`Client::flush_pending_signal_state()`](/api/client#flush_pending_signal_state) to force a deterministic settle — e.g. before reading persisted Signal state directly, or ahead of a non-graceful shutdown. Never call it from inside an `InboundDurabilityHook` or a synchronous, inline `EventHandler::handle_event` implementation, since settling re-enters the processing permit those run under and would deadlock during an offline-sync drain. Ordinary `Bot` closure handlers are unaffected — both default delivery modes run the callback in a detached task off the permit.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## Security Considerations

### Identity key trust
Expand Down
23 changes: 23 additions & 0 deletions api/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,29 @@ Send pre-marshaled plaintext bytes through the noise socket. The bytes must be a
This bypasses node logging and `wait_for_sent_node` waiter resolution. Use [`send_node`](#send_node) for normal stanza sending. This method is intended for performance-critical paths where you already have marshaled bytes.
</Warning>

### flush_pending_signal_state

```rust
pub async fn flush_pending_signal_state(&self) -> Result<(), anyhow::Error>
```

Forces any pending write-behind Signal cache state to the backend, returning once the flush completes (or fails).

The live receive path schedules a coalesced flush instead of writing through (sends already flush synchronously before the stanza hits the wire). On success the backend normally trails the cache by about the coalescing window, but that is **not** a hard wall-clock bound — the timer can slip under runtime starvation, and the flush can wait on locks or slow/failing storage (a backend outage extends it until the retry loop succeeds). Use this to settle durability deterministically before reading persisted state directly, or ahead of a non-graceful shutdown. Check the returned `Result` — a failure leaves state pending.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

<Warning>
Never call this from inside an [`InboundDurabilityHook`](/advanced/inbound-durability) — during an offline-sync drain it runs while the processing permit is held, and settling routes through that same permit, so re-entering it would deadlock. The same risk applies to a custom `EventHandler::handle_event` implementation that itself blocks synchronously inline (dispatch is synchronous). It does **not** apply to ordinary [`Bot`](/api/bot) closure handlers (`.on_message()`, etc.) — both the default concurrent and ordered delivery modes run your callback in a detached task that never holds the permit, so calling `flush_pending_signal_state()` from inside one of those is safe.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
</Warning>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**Example:**
```rust
// Force durability before reading Signal state directly, or before a
// non-graceful shutdown (process kill, container stop).
client.flush_pending_signal_state().await?;
```

See [Signal Protocol — flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive) for the full durability model.

### generate_message_id

```rust
Expand Down
6 changes: 5 additions & 1 deletion api/send.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ pub async fn send_message(
Contains the `message_id` (unique ID for tracking receipts, edits, revokes) and `to` (resolved recipient JID). Use `send_result.message_key()` to get a `wa::MessageKey` for album child linking, pinning, or other operations that reference this message.
</ResponseField>

<Note>
For DMs, group, and status sends, the outbound Signal ratchet advance is persisted to the backend **synchronously, before the stanza is transmitted** — reusing an outbound counter would reuse its message key and IV, so the advance must be durable before anyone can act on the ciphertext. If that persistence write fails, `send_message` returns `Err` instead of transmitting an advance that couldn't be saved. See [Signal Protocol — flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive) for the full durability model.
</Note>
Comment on lines +46 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use concise sentences and active voice.

The note merges multiple ideas into a single long sentence and uses passive voice ("is persisted"). As per coding guidelines, documentation must use concise sentences (one idea per sentence) and active voice.

📝 Proposed rewrite
 <Note>
-For DMs, group, and status sends, the outbound Signal ratchet advance is persisted to the backend **synchronously, before the stanza is transmitted** — reusing an outbound counter would reuse its message key and IV, so the advance must be durable before anyone can act on the ciphertext. If that persistence write fails, `send_message` returns `Err` instead of transmitting an advance that couldn't be saved. See [Signal Protocol — flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive) for the full durability model.
+For DMs, group, and status sends, the client persists the outbound Signal ratchet advance to the backend **synchronously, before the stanza is transmitted**. Reusing an outbound counter would reuse its message key and IV. Therefore, the advance must be durable before anyone can act on the ciphertext. If that persistence write fails, `send_message` returns `Err` instead of transmitting an advance that it could not save. See [Signal Protocol — flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive) for the full durability model.
 </Note>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Note>
For DMs, group, and status sends, the outbound Signal ratchet advance is persisted to the backend **synchronously, before the stanza is transmitted** — reusing an outbound counter would reuse its message key and IV, so the advance must be durable before anyone can act on the ciphertext. If that persistence write fails, `send_message` returns `Err` instead of transmitting an advance that couldn't be saved. See [Signal Protocol — flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive) for the full durability model.
</Note>
<Note>
For DMs, group, and status sends, the client persists the outbound Signal ratchet advance to the backend **synchronously, before the stanza is transmitted**. Reusing an outbound counter would reuse its message key and IV. Therefore, the advance must be durable before anyone can act on the ciphertext. If that persistence write fails, `send_message` returns `Err` instead of transmitting an advance that it could not save. See [Signal Protocol — flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive) for the full durability model.
</Note>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/send.mdx` around lines 46 - 48, Rewrite the Note in api/send.mdx using
concise, active-voice sentences: state that the system synchronously persists
the outbound Signal ratchet advance before transmitting DMs, group, or status
stanzas; explain that this prevents outbound counter, message-key, and IV reuse;
state that send_message returns Err when persistence fails; and retain the
existing flush-scheduling reference.

Source: Coding guidelines


### SendResult

Result of a successfully sent message. Provides the message ID and a convenience method to construct a `MessageKey` for follow-up operations like album child linking.
Expand Down Expand Up @@ -1252,7 +1256,7 @@ pub enum SendError {
- `Iq` — IQ request required by the send path failed
- `InvalidRequest` — the send request was malformed (e.g., invalid JID, bad message shape)
- `Client` — underlying transport/connection error
- `Internal` — catch-all for errors not yet assigned a typed variant
- `Internal` — catch-all for errors not yet assigned a typed variant. Includes a failure to durably persist the outbound Signal ratchet advance before the stanza was sent — see the durability note under [`send_message`](#send_message).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use concise sentences.

The description merges two separate directives with an em dash. As per coding guidelines, documentation must use concise sentences with one idea per sentence.

📝 Proposed rewrite
-- `Internal` — catch-all for errors not yet assigned a typed variant. Includes a failure to durably persist the outbound Signal ratchet advance before the stanza was sent — see the durability note under [`send_message`](`#send_message`).
+- `Internal` — catch-all for errors not yet assigned a typed variant. It includes a failure to durably persist the outbound Signal ratchet advance before the stanza was sent. See the durability note under [`send_message`](`#send_message`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `Internal` — catch-all for errors not yet assigned a typed variant. Includes a failure to durably persist the outbound Signal ratchet advance before the stanza was sent — see the durability note under [`send_message`](#send_message).
- `Internal` — catch-all for errors not yet assigned a typed variant. It includes a failure to durably persist the outbound Signal ratchet advance before the stanza was sent. See the durability note under [`send_message`](`#send_message`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/send.mdx` at line 1259, Rewrite the Internal error description in the
send_message documentation as separate concise sentences, keeping the catch-all
meaning and the note about failures to durably persist the outbound Signal
ratchet advance before stanza transmission.

Source: Coding guidelines


**Example:**

Expand Down