Quick Start
Easiest path — DurableAdapterMixin
Add three lines to any existing adapter and every
send_durable() call is crash-safe:How It Works
Each message moves through statuses:pending → sending → sent (or failed / permanent_failure). On startup, stale sending entries are reset to pending automatically so crashes never leave orphaned rows.
Choosing the Right Primitive
| I need to… | Use |
|---|---|
| Send one message, retry on transient failures, no persistence | deliver_with_retry() |
| Send a message that may exceed platform length limits | deliver_chunked() |
| Survive process crashes / platform outages with replay | DurableDelivery or DurableAdapterMixin |
| Build a brand-new custom adapter with durability built in | DurableAdapterMixin |
Configuration Options
OutboundQueue
SQLite-backed outbox. All parameters after path are keyword-only.
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | Path | (required) | SQLite file path. Parent dirs are created automatically. |
max_size | int | 50_000 | Max entries kept. Oldest sent entries are evicted when exceeded. |
ttl_seconds | int | 604800 (7 days) | Sent entries older than this are evicted. |
max_attempts | int | 5 | Max delivery attempts before marking permanent failure. |
backoff | BackoffPolicy | BackoffPolicy() | Retry backoff configuration. |
DurableDelivery
Wraps an OutboundQueue and an adapter to provide a simple .send() / .drain_pending() API.
| Parameter | Type | Default | Description |
|---|---|---|---|
outbox | OutboundQueue | (required) | The outbound queue to persist messages. |
adapter | adapter instance | (required) | Bot adapter with a send_message(channel_id, content) method. |
platform | str | "" | Platform name for platform-aware error classification. |
backoff | BackoffPolicy | BackoffPolicy() | Retry backoff configuration. |
max_attempts | int | 3 | Max delivery attempts. |
DurableAdapterMixin.setup_durable_delivery()
Call once in your adapter’s __init__ to wire up the outbox.
| Parameter | Type | Default | Description |
|---|---|---|---|
outbox_path | str | None | None | Path to SQLite outbox. None disables durability. |
platform | str | "" | Platform name for error classification. |
max_attempts | int | 3 | Max delivery attempts per message. |
max_size | int | 50_000 | Max messages in outbox. |
ttl_seconds | int | 604800 (7 days) | TTL for sent messages. |
deliver_with_retry()
Bounded exponential backoff retry without persistence — returns when the message is sent or max attempts is reached.
| Parameter | Type | Default | Description |
|---|---|---|---|
send_func | async callable | (required) | Async callable to execute (the send operation). |
policy | BackoffPolicy | BackoffPolicy(max_attempts=3) | Retry backoff configuration. |
is_recoverable | callable | None | None | Function to classify errors as transient. Defaults to is_recoverable_error(e, platform). |
platform | str | "" | Platform name for platform-specific error rules. |
parked_store | Any | None | None | Optional DLQ for failed sends. |
reply_data | dict | None | None | Optional metadata for DLQ storage. |
deliver_chunked()
Splits a long message at paragraph boundaries and sends each chunk separately. Returns the number of chunks sent.
| Parameter | Type | Default | Description |
|---|---|---|---|
adapter | adapter instance | (required) | Bot adapter with send_message(channel_id, content). |
channel_id | str | (required) | Target channel. |
content | str | (required) | Message text to split and send. |
max_length | int | 4096 | Max characters per chunk (Telegram limit is 4096). |
preserve_fences | bool | True | Keep code fence blocks intact even if they exceed max_length. |
BackoffPolicy
Controls retry timing for both deliver_with_retry and OutboundQueue.drain.
| Attribute | Type | Default | Description |
|---|---|---|---|
initial_ms | float | 2000.0 | Initial delay in milliseconds. |
max_ms | float | 30000.0 | Maximum delay in milliseconds. |
factor | float | 1.8 | Multiplicative factor per attempt. |
jitter | float | 0.25 | Random jitter fraction (0.0–1.0). |
max_attempts | int | 0 | Max retry attempts. 0 = unlimited. |
Idempotency & Drain on Startup
Idempotency Keys
Every message has anidempotency_key — a UUID generated automatically if you omit it. Reusing the same key for the same logical message prevents double-sends across retries.
send() is called again with the same key, the outbox skips the enqueue (SQLite UNIQUE constraint) and marks the existing row sent.
Drain on Startup
Calldrain_pending() once at adapter startup to replay anything that was queued before the last crash:
max_attempts.
Best Practices
Always set platform= for accurate error classification
Always set platform= for accurate error classification
The
is_recoverable_error() function checks platform-specific patterns (e.g., Telegram’s HTTP 409 conflict, rate-limit “retry after” responses) when a platform name is provided. Without it, only generic patterns are checked and some transient errors may be misclassified as permanent.Use a stable idempotency_key derived from the inbound message
Use a stable idempotency_key derived from the inbound message
When bridging a webhook to an outbound reply, derive the key from the inbound message ID. This ensures webhook redeliveries don’t produce duplicate outbound sends.
Keep the outbox on persistent local disk
Keep the outbox on persistent local disk
Store the outbox on a persistent, local filesystem path — not
/tmp and not a Docker tmpfs. The default suggestion is ~/.praisonai/state/outbox.sqlite.Call drain_pending() exactly once per adapter start
Call drain_pending() exactly once per adapter start
Multiple concurrent drainers fight over the same rows via SQLite’s
status = 'sending' claim mechanism. A 5-minute claim timeout releases stale claims, but concurrent drainers still produce redundant work and log noise.Related
Inbound Journal
Inbound counterpart — deduplicate webhook redeliveries and recover in-flight messages
Inbound DLQ
Dead-letter queue for failed inbound message processing
Bot Streaming Replies
Live-edit streaming UX for bot responses
Messaging Bots
Top-level guide to building bots with PraisonAI

