> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Inbound Dead-Letter Queue

> Never lose a user's message when the LLM fails. Persist, inspect, replay.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Inbound DLQ"
        A[📋 User Message] --> B[🤖 BotSessionManager]
        B --> D[💾 InboundDLQ]
        D --> C[✅ Operator Replay]
    end

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef output fill:#10B981,stroke:#7C90A0,color:#fff
    class A input
    class B agent
    class D tool
    class C output
```

> [!NOTE]
>
> **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**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai bot dlq list
```

**2. Replay through your bot**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai bot dlq replay --config bot.yaml
```

**3. Purge when resolved**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Bot
    participant DLQ
    participant Operator

    User->>Bot: Send message
    Bot->>Bot: agent.chat() fails
    Bot->>DLQ: enqueue(message, error)
    Operator->>DLQ: praisonai bot dlq replay
    DLQ->>Bot: Re-run message
    Bot-->>User: Reply on success
```

## 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](https://praison.ai/docs/docs/features/durable-delivery#age-gated-dead-lettering-attempts--wall-clock).

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):

| Param | Type | Default | Description |
| - | - | - | - |
| `dead_letter_min_age` | `float` (seconds) | `21600` (6h) | Minimum wall-clock age since first receipt before an attempt-exhausted transient failure is quarantined. `0` restores pre-#3521 attempt-only behaviour. |
| `dead_letter_policy` | `DeadLetterPolicyProtocol \| None` | `None` | Optional custom policy. Overrides `max_attempts` + `dead_letter_min_age`. |

Introduced in [PraisonAI PR #3521](https://github.com/MervinPraison/PraisonAI/pull/3521).

***

## Why you want this

> [card] No silent data loss
>
> Failed inbound messages are persisted to a SQLite file before the exception bubbles up.

> [card] Operator-friendly replay
>
> A single CLI command (`praisonai bot dlq replay`) re-runs failed messages through the agent.

> [card] 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](#age-gated-quarantine).

> [card] 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`:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
channels:
  telegram:
    token: "${TELEGRAM_BOT_TOKEN}"
    delivery:
      durable: false   # opt out of durable inbound delivery for this channel
```

Override the store directory:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
channels:
  telegram:
    token: "${TELEGRAM_BOT_TOKEN}"
    delivery:
      store: /var/lib/praisonai/telegram-state
      # → inbound_dlq.sqlite lives in /var/lib/praisonai/telegram-state/
```

## CLI

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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()`.

**dead\_letter\_min\_age** `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.

**dead\_letter\_policy** `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`

**Fields**

* `id: int` — primary key, monotonic.
* `ts: float` — UNIX time of failure.
* `platform: str` — bot platform (`telegram`, `discord`, etc).
* `user_id: str` — platform user id.
* `prompt: str` — the original user message.
* `chat_id: str`, `thread_id: str`, `user_name: str` — metadata when known.
* `error: str` — the error string that caused the failure.
* `attempts: int` — how many times replay has been attempted (and failed).

### Methods

> [tab] Inspect
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> dlq.size()                  # int
> dlq.list(limit=100)         # list[DLQEntry], newest first
> ```

> [tab] Mutate
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> dlq.enqueue(
>     platform="telegram", user_id="12345",
>     prompt="hi", error="LLM 503",
> )
> dlq.purge()                 # delete all
> dlq.evict_expired()         # drop entries older than ttl
> ```

> [tab] Replay
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> async def handler(entry):
>     try:
>         await mgr.chat(agent, entry.user_id, entry.prompt,
>                        chat_id=entry.chat_id,
>                        user_name=entry.user_name)
>         return True   # success → entry deleted
>     except Exception:
>         return False  # keep entry, increment attempts
>
> succeeded, failed = await dlq.replay(handler)
> ```

## Real LLM smoke test

```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
[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

> [accordion] Tune retention for your SLO
>
> Set `max_size` and `ttl_seconds` to match how long you need to retain failed messages. Chronic LLM outages can fill disk quickly.

> [accordion] Retry inline before the DLQ
>
> For transient failures, use `BackoffPolicy` to retry inline first. The DLQ is the last resort, not the first response.

> [accordion] Alert on DLQ growth
>
> Wrap `dlq.enqueue()` with your tracer (e.g. OTEL span). A non-zero `dlq.size()` is a strong SLO trip-wire.

> [accordion] Combine with durable outbound
>
> Pair inbound DLQ with [Durable Outbound Delivery](https://praison.ai/docs/features/durable-delivery.md) so both sides of a conversation survive failures.

> [!WARNING]
>
> **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.

> [!NOTE]
>
> **Thread safety** — every write is guarded by an internal `threading.Lock`. SQLite WAL is enabled. Safe to share one `InboundDLQ` instance across threads.

> [!NOTE]
>
> **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

> [accordion] With Cross-Platform Mirror (W1)
>
> 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.

> [accordion] With BackoffPolicy (resilience)
>
> 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.

> [accordion] With observability
>
> 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.

## Paid upgrade path

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.

***

## Related

> [card] [Delivery Config](https://praison.ai/docs/features/delivery-config.md)
>
> Full reference for the `delivery:` channel config block — defaults, opt-out, and path override

> [card] [Inbound Journal](https://praison.ai/docs/features/inbound-journal.md)
>
> Deduplicate webhook redeliveries and recover in-flight messages after a crash

> [card] [Durable Outbound Delivery](https://praison.ai/docs/features/durable-delivery.md)
>
> Outbound counterpart — persist outgoing messages with retry and idempotency

> [card] [Outbox Operator CLI](https://praison.ai/docs/features/outbox-operator-cli.md)
>
> Outbound counterpart — `praisonai gateway outbox list|stats|retry|purge`

> [card] [Messaging Bots](https://praison.ai/docs/features/messaging-bots.md)
>
> Bot setup where the DLQ is wired automatically


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.