Quick Start
1
Zero Configuration (Retry + Durable Park)
Every adapter now retries transient failures and parks permanent / exhausted failures in a per-platform DLQ by default — no config needed:Slack, Discord, WhatsApp, Email, Linear, and AgentMail behave identically — each writes to its own
~/.praisonai/state/<platform>/outbound_dlq.sqlite file.2
Override the default DLQ path or tune retries
dlq_path is optional — set it only if you need the DLQ on a different filesystem (e.g. a shared volume, a specific bind mount). If you just want to tune retries, all the other keys work the same as before.3
Tune Backoff Parameters
Fine-tune retry behaviour per channel:
How It Works
The mixin (OutboundResilienceMixin) wraps each adapter’s raw send with deliver_outbound(). State is initialised lazily from self.config.outbound_resilience. When no dlq_path is configured, the mixin falls back to resolve_durable_store_dir(<platform>) / "outbound_dlq.sqlite" so the default is durable-by-default without touching existing adapter constructors.
Channel Support
All six channels now share the same durable delivery path:What Gets Retried
Structured error classification
Each failure is tagged with a machine-readableSendErrorKind before the retry decision. Permanent kinds (forbidden, target_not_found, auth_fatal, invalid_request) short-circuit immediately rather than exhausting max_retries against a dead target; transient and rate-limited failures still retry with backoff.
See Send Error Taxonomy for the full kind reference and how to classify your own adapter’s native exceptions.
Automatic crash-recovery on start
Every gateway-managed channel redelivers any reply parked by a crash on a previous run — no operator step, symmetric with the inbound journal’sreplay() sweep.
The gateway schedules WebSocketGateway._replay_outbound_dlq(bot) about 5 seconds after each channel task starts (a grace period for the transport to connect), then calls OutboundDLQ.redeliver(send) — draining parked replies oldest-first through the adapter’s own send_message, so the platform’s normal retry/backoff still applies. A redelivery that fails again is kept for the next boot, bounded by the DLQ’s existing TTL / max_size / attempts invariants. Failure never blocks channel start.
Users receive redelivered replies with a visible duplicate marker:
♻️ Recovered after restart — this reply may be a duplicate. <original reply text>When something is recovered you’ll see this line in the gateway log:
This section is shared with Outbound Resilience → Automatic crash-recovery on start, the canonical page for the boot-time drainer.
When Held Replies Drain
The durable outbox re-attempts held replies on three triggers:- Adapter startup — replays anything queued before the last crash.
- Channel recovery (Issue #4043) — when
ChannelSupervisorsees a channel come back after a transient outage, it re-drains the outbox in the background so held replies go out promptly, without waiting for the next inbound turn. - Lazy on next inbound turn — the next
chat()turn also drains opportunistically.
Configuration Options
The default path is resolved by the shared
resolve_durable_store_dir(<platform>) helper (the same store the inbound journal uses). Set the PRAISONAI_HOME environment variable to relocate the whole ~/.praisonai/ root — handy in tests or containers.
Best Practices
Durable park is on by default — override the path only when you need to
Durable park is on by default — override the path only when you need to
Permanent and exhausted failures are parked in
~/.praisonai/state/<platform>/outbound_dlq.sqlite automatically. Override dlq_path only if you need the DLQ on a specific filesystem (shared volume, bind mount, SSD). To disable durable parking entirely, set outbound_resilience.enabled = false.Respect rate limits with Retry-After
Respect rate limits with Retry-After
The mixin reads the
Retry-After response header and waits exactly that long before the next attempt, so your bot stays within platform rate limits without sleeping longer than necessary.Opt channels out individually
Opt channels out individually
Set
outbound_resilience.enabled = False in a channel’s config to disable durable delivery for that channel only — useful for fire-and-forget channels where retries would send duplicates.Monitor your DLQ
Monitor your DLQ
Parked entries are permanent failures. Set up alerts on DLQ growth to detect channels that are consistently unreachable (e.g. bots kicked from a workspace).
Related
Dead-Target Registry
Short-circuit known-dead channels before sending
Bot Channels
Overview of all supported messaging channels
Delivery Config
Full delivery configuration reference
Inbound DLQ
Dead-letter queue for inbound messages

