Skip to main content
When agent.chat() fails, the Inbound DLQ persists the user’s message so operators can inspect and replay it later. The user sends a channel message; if the LLM fails, the message is queued for operator replay.
On by default for gateway/bot runs — When your bot starts via praisonai bot start, onboard, or bot.yaml, an InboundDLQ is wired automatically. No code needed. Set delivery.durable: false in your channel config to opt out.

Quick Start

1

List failed messages

2

Replay through your bot

3

Purge when resolved

How It Works

The user sends a message; if the LLM call fails, the message is persisted to the DLQ so an operator can replay it later.

Age-gated quarantine

InboundJournal.replay() and its redelivery path no longer quarantine on attempts alone — they consult the same age-gated policy as Durable Outbound Delivery. An in-flight stale-claimed entry only moves to quarantine when:
  • The failure is a known-permanent error class (short-circuits immediately), or
  • attempts >= max_attempts and the entry is at least dead_letter_min_age (default 6h) old.
Otherwise replay() returns the entry to the pending pool and it will be reprocessed on the next tick. In practice this means a routine LLM outage that burns the claim budget in seconds keeps being retried, instead of every in-flight user message landing in the DLQ. Two new keyword-only params on InboundJournal(...) (also honoured by build_session_manager when threading through channel config): Introduced in PraisonAI PR #3521.

Why you want this

No silent data loss

Failed inbound messages are persisted to a SQLite file before the exception bubbles up.

Operator-friendly replay

A single CLI command (praisonai bot dlq replay) re-runs failed messages through the agent.

Bounded by design

TTL + max_size keep the queue from growing unbounded; oldest entries evict first. Transient LLM outages no longer flood the DLQ — an exhausted entry is only quarantined once it’s both attempt-exhausted and at least dead_letter_min_age (default 6h) old. See Age-gated quarantine.

Zero new dependency

Uses only stdlib sqlite3. On by default for gateway/bot runs — existing bots are upgraded automatically.

Default behaviour (no config needed)

Every bot started through build_session_manager (all shipped adapters) automatically gets a DLQ at:
For example, a Telegram bot stores failed messages at ~/.praisonai/state/telegram/inbound_dlq.sqlite. A Discord bot uses ~/.praisonai/state/discord/inbound_dlq.sqlite. Each platform is fully isolated. Set PRAISONAI_HOME to override the base directory:

Opt out or override path

Add a delivery: block to any channel in your bot.yaml or gateway.yaml:
Override the store directory:

CLI

Advanced: manual instantiation

Most users get the DLQ automatically. For custom setups outside build_session_manager, create it directly:

API reference

str | Path
required
Where the SQLite file lives. Parent directories are created automatically.
int
default:"10_000"
Maximum number of entries kept. When exceeded, oldest entries are dropped first.
int
default:"604800 (7 days)"
Entries older than this are evicted on the next enqueue() or evict_expired().
float
default:"21600 (6h)"
Minimum wall-clock age (seconds) an exhausted transient failure must reach before it is quarantined into the DLQ. 0 restores the pre-#3521 attempt-only behaviour.
DeadLetterPolicyProtocol | None
default:"None"
Optional custom policy from praisonaiagents.gateway. When supplied, overrides max_attempts + dead_letter_min_age. Must implement should_dead_letter(*, attempts, first_seen_epoch, now_epoch, error_class) -> DeadLetterDecision.

DLQEntry

Methods

Real LLM smoke test

Best Practices

Set max_size and ttl_seconds to match how long you need to retain failed messages. Chronic LLM outages can fill disk quickly.
For transient failures, use BackoffPolicy to retry inline first. The DLQ is the last resort, not the first response.
Wrap dlq.enqueue() with your tracer (e.g. OTEL span). A non-zero dlq.size() is a strong SLO trip-wire.
Pair inbound DLQ with Durable Outbound Delivery so both sides of a conversation survive failures.
Disk usage — every failed message + its prompt is written to disk. With chronic LLM outages this can grow fast. Tune max_size and ttl_seconds for your retention policy.
Thread safety — every write is guarded by an internal threading.Lock. SQLite WAL is enabled. Safe to share one InboundDLQ instance across threads.
Fallback — if durability is requested but SQLite fails to initialise (permissions, disk full, etc.), the manager logs a warning and falls back to in-memory delivery automatically.

Combining with other features

The DLQ records platform, user_id, and (if W1’s IdentityResolver is wired) the same user_id resolves the same human across platforms. Replay restores the exact session.
For transient failures use praisonai.bots._resilience.BackoffPolicy to retry inline before falling back to the DLQ. The DLQ is the last resort, not the first.
Wrap dlq.enqueue() with your tracer (e.g. OTEL span) to alert on DLQ growth. A non-zero dlq.size() is a great SLO trip-wire.
OSS now File-backed SQLite DLQ — single-host deploys. Cloud (planned) Multi-region replicated DLQ with web dashboard, automatic alerting, and one-click bulk replay.

Delivery Config

Full reference for the delivery: channel config block — defaults, opt-out, and path override

Inbound Journal

Deduplicate webhook redeliveries and recover in-flight messages after a crash

Durable Outbound Delivery

Outbound counterpart — persist outgoing messages with retry and idempotency

Messaging Bots

Bot setup where the DLQ is wired automatically