Skip to main content
from praisonaiagents import Agent

agent = Agent(name="dlq-agent", instructions="Process messages from the dead-letter queue.")
agent.start("Inspect and retry failed messages from the dead-letter queue.")
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

praisonai bot dlq list
2

Replay through your bot

praisonai bot dlq replay --config bot.yaml
3

Purge when resolved

praisonai bot dlq purge --yes

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.

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.

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:
~/.praisonai/state/<platform>/inbound_dlq.sqlite
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:
$PRAISONAI_HOME/state/<platform>/inbound_dlq.sqlite

Opt out or override path

Add a delivery: block to any channel in your bot.yaml or gateway.yaml:
channels:
  telegram:
    token: "${TELEGRAM_BOT_TOKEN}"
    delivery:
      durable: false   # opt out of durable inbound delivery for this channel
Override the store directory:
channels:
  telegram:
    token: "${TELEGRAM_BOT_TOKEN}"
    delivery:
      store: /var/lib/praisonai/telegram-state
      # → inbound_dlq.sqlite lives in /var/lib/praisonai/telegram-state/

CLI

# List failed messages (newest first)
praisonai bot dlq list

# List from a custom path
praisonai bot dlq list --path /var/lib/myapp/dlq.sqlite --limit 50

# Replay through your bot's configured agent
praisonai bot dlq replay --config bot.yaml

# Purge everything (asks for confirmation)
praisonai bot dlq purge
praisonai bot dlq purge --yes  # skip confirmation

Advanced: manual instantiation

Most users get the DLQ automatically. For custom setups outside build_session_manager, create it directly:
from praisonai.bots import BotSessionManager, InboundDLQ

dlq = InboundDLQ(path="~/.praisonai/state/telegram/inbound_dlq.sqlite")
mgr = BotSessionManager(platform="telegram", dlq=dlq)
# ↑ that's it — failed agent.chat() now lands in the DLQ

API reference

path
str | Path
required
Where the SQLite file lives. Parent directories are created automatically.
max_size
int
default:"10_000"
Maximum number of entries kept. When exceeded, oldest entries are dropped first.
ttl_seconds
int
default:"604800 (7 days)"
Entries older than this are evicted on the next enqueue() or evict_expired().

DLQEntry

Methods

dlq.size()                  # int
dlq.list(limit=100)         # list[DLQEntry], newest first

Real LLM smoke test

[1] Sending failing message: 'What is 2 plus 2? Answer with a single digit.'
   Caught expected error: simulated LLM 503
   DLQ size after fail: 1  ✅

[2] Replaying DLQ via real LLM …
   succeeded=1, failed=0, remaining=0

[Real LLM reply] 4

PASS: DLQ → replay → real LLM produced expected '4'.

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