> ## 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 Journal

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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(name="durable-bot", instructions="Process webhook messages exactly once.")
agent.start("Handle webhook delivery with deduplication and crash recovery.")
```

<Note>
  **Different scope from the [Run-State Journal](/docs/features/run-state-journal).** The **inbound journal** deduplicates and recovers *webhook messages*. The **run-state journal** persists the *execution cursor* of a single run. A webhook may spawn a run; the two journals track the two halves of the pipeline.
</Note>

Inbound Journal tracks messages before agents process them, providing deduplication of webhook redeliveries and crash-safe replay of in-flight messages.

The user sends a webhook message; the journal deduplicates redeliveries and tracks in-flight work.

<Note>
  **On by default for gateway/bot runs** — Every bot started via `praisonai bot start`, `onboard`, or `bot.yaml` gets the journal automatically. No code needed. The journal lives at `~/.praisonai/state/<platform>/ingress.sqlite`. Set `delivery.durable: false` to opt out.
</Note>

## Automatic crash-replay

Every gateway-managed channel calls `replay()` for you at start-up and hot-reload — no startup hook needed.

Every channel started via `praisonai bot start`, `onboard`, `bot.yaml`, or on hot-reload of a single channel automatically calls `InboundJournal.replay()` for its journal. What `replay()` does:

* Resets stale `claimed` rows back to `pending` so they can be reprocessed via redelivery or session-resume (`_resume_interrupted_turns()`, [PraisonAI #3379](https://github.com/MervinPraison/PraisonAI/pull/3379)).
* Quarantines genuine poison entries to the inbound DLQ under the shared age-gated policy (see [Inbound DLQ](/docs/features/inbound-dlq)).
* Does **not** synthesise per-adapter native message objects to self-dispatch — that duplication is deliberately left to the session-resume path.

<Note>
  Best-effort: any failure is logged and skipped, so channel start never aborts. Reset counts are logged at `INFO` so operators can see the recovery happened.
</Note>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Inbound Journal"
        Webhook[📨 Webhook] --> Receive[📒 receive]
        Receive -->|new| Claim[🔒 claim]
        Receive -->|duplicate| Skip[⏭️ skip]
        Claim --> Agent[🤖 agent.chat]
        Agent --> Complete[✅ complete]
    end

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef success fill:#10B981,stroke:#7C90A0,color:#fff
    classDef skip fill:#F59E0B,stroke:#7C90A0,color:#fff

    class Webhook input
    class Receive,Claim process
    class Agent,Complete success
    class Skip skip

```

The outbound reply path is now symmetric — see [Outbound Resilience → Automatic crash-recovery on start](/docs/features/outbound-resilience#automatic-crash-recovery-on-start).

## Default behavior (no config needed)

Every bot started through `build_session_manager` (all shipped adapters) automatically gets an Inbound Journal at:

```
~/.praisonai/state/<platform>/ingress.sqlite
```

For example, a Telegram bot journals messages at `~/.praisonai/state/telegram/ingress.sqlite`. A Discord bot uses `~/.praisonai/state/discord/ingress.sqlite`. Each platform is fully isolated — no cross-platform dedup collisions.

Set `PRAISONAI_HOME` to override the base directory:

```
$PRAISONAI_HOME/state/<platform>/ingress.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
      # → ingress.sqlite lives in /var/lib/praisonai/telegram-state/
```

## Quick Start (advanced: manual setup)

Most users get the journal automatically. For custom setups outside `build_session_manager`:

<Steps>
  <Step title="Create journal that survives restarts">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.bots import InboundJournal, BotSessionManager

    journal = InboundJournal(path="~/.praisonai/state/telegram/ingress.sqlite")
    ```
  </Step>

  <Step title="Enable on your bot">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    session = BotSessionManager(platform="telegram", ingress_journal=journal)

    agent = Agent(name="Support Bot", instructions="Help users with their questions")
    ```
  </Step>
</Steps>

<Note>
  **Default behaviour:** `BotSessionManager.chat()` marks the journal entry complete for you on clean exit — no `complete()` call needed. **Deferred mode (opt-in, safer):** construct `BotSessionManager(defer_journal_completion=True)` and `chat()` leaves the entry pending; your adapter then calls `complete_last_journal_entry(journal_key)` **after** it has durably delivered the reply. This closes the crash window where the agent finished, the inbound was settled, and a crash before send lost the user's reply with no replay. See [Defer completion until reply is delivered](#defer-completion-until-reply-is-delivered-opt-in). You only call `complete()` directly if you drive `InboundJournal` without `BotSessionManager`.
</Note>

***

## How It Works

<Note>
  Before [PR #1980](https://github.com/MervinPraison/PraisonAI/pull/1980), the default `chat()` path claimed entries but never called `complete()`. On startup, `journal.replay()` could re-issue messages the user had already received answers to. Upgrade to a release after 2026-06-19 — replay then only covers genuinely incomplete entries.
</Note>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Platform as Telegram/Discord/Slack
    participant Adapter as Bot Adapter
    participant Journal as InboundJournal
    participant Agent

    Platform->>Adapter: webhook(message_id=42)
    Adapter->>Journal: receive(platform, account, chat_id, 42)
    alt First delivery
        Journal-->>Adapter: key
        Adapter->>Journal: claim(key)
        Adapter->>Agent: chat(prompt)
        Agent-->>Adapter: response
        Adapter->>Journal: complete(key)
        Adapter->>Platform: send reply
    else Redelivery — prior attempt still pending or stale-claimed
        Journal-->>Adapter: same key (retry)
        Adapter->>Agent: chat(prompt)
        Agent-->>Adapter: response
        Adapter->>Journal: complete(key)
        Adapter->>Platform: send reply
    else Redelivery — already completed or actively claimed
        Journal-->>Adapter: None
        Adapter-->>Platform: 200 OK (silent skip)
    end
```

| Stage | Purpose | What happens |
| - | - | - |
| **receive** | Deduplication & retry | Returns a key for new messages, retries pending or stale-claimed duplicates, returns `None` only for already-completed or actively-claimed entries |
| **claim** | Crash protection | Marks the entry as being processed (auto-released on stale timeout) |
| **complete** | Cleanup | Marks successful processing — `BotSessionManager.chat()` calls this for you automatically |

***

## Defer completion until reply is delivered (opt-in)

By default `chat()` settles the inbound entry the instant the agent finishes — **before** your adapter sends the reply. A crash in that window loses the user's answer *and* marks the inbound complete, so `replay()` never re-drives it ([Issue #5152](https://github.com/MervinPraison/PraisonAI/issues/5152)).

Set `defer_journal_completion=True` to keep the entry pending until your adapter confirms the reply was durably delivered.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Adapter
    participant Session as BotSessionManager
    participant Journal as InboundJournal
    participant Agent
    participant Platform

    Note over Session,Journal: defer_journal_completion=True (opt-in, safe)
    Adapter->>Session: chat(prompt)
    Session->>Journal: claim(key)
    Session->>Agent: chat(prompt)
    Agent-->>Session: reply
    Note over Session,Journal: Entry stays PENDING — not completed yet
    Session-->>Adapter: reply
    Adapter->>Platform: send(reply)
    Platform-->>Adapter: ack
    Adapter->>Session: complete_last_journal_entry(key)
    Session->>Journal: complete(key)
```

Enable it on the manager:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
session = BotSessionManager(
    platform="telegram",
    ingress_journal=journal,
    defer_journal_completion=True,
)
```

Then your adapter owns settlement: after `session.chat()` returns, capture the deferred key from `session._last_journal_key`, send the reply, and only on durable delivery call `session.complete_last_journal_entry(journal_key)`. If the process dies between `chat()` returning and delivery, the entry stays `pending`, so `replay()` on the next boot (or platform redelivery) resurfaces it.

<Warning>
  Always pass the explicit `journal_key` when your adapter can handle concurrent turns. Different users run under different per-storage-key locks, so two turns can be in flight at once — the no-arg convenience path settles the manager-wide "last" slot, which is ambiguous under concurrency and can settle another turn's still-undelivered inbound.
</Warning>

| Aspect | `defer_journal_completion=False` (default) | `defer_journal_completion=True` (opt-in) |
| - | - | - |
| When inbound is settled | On agent clean exit, **before** send | On adapter's explicit call **after** durable send |
| Crash between agent-done and send | Reply permanently lost, `replay()` won't re-drive | Entry stays pending, `replay()` / redelivery re-drives it |
| Adapter responsibility | None | Call `complete_last_journal_entry(journal_key)` after send |
| Concurrent-turn safety | N/A | Must pass explicit `journal_key` argument |
| Backward compatibility | Byte-for-byte legacy behaviour | New opt-in path |

### When to opt in

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start{Adapter delivers<br/>replies durably?} -->|Yes, and I own the send path| Enable[Enable defer_journal_completion=True]
    Start -->|No, adapter is fire-and-forget| Keep[Keep default False]
    Enable --> Contract{Concurrent turns<br/>from multiple users?}
    Contract -->|Yes| Explicit[Always pass explicit journal_key]
    Contract -->|Single-user bot| Convenience[No-arg complete_last_journal_entry OK]

    classDef question fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef action fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef safe fill:#10B981,stroke:#7C90A0,color:#fff

    class Start,Contract question
    class Enable,Explicit action
    class Keep,Convenience safe
```

***

## Automatic crash recovery under the gateway

The gateway calls `InboundJournal.replay()` for you — at channel start-up and on every hot-reload — so a gateway killed mid-turn recovers stranded messages without you invoking `replay()` yourself.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Op as Operator
    participant Gateway
    participant Journal as InboundJournal
    participant Session as _resume_interrupted_turns

    Op->>Gateway: bot start / hot-reload
    Gateway->>Journal: replay()
    Journal-->>Gateway: N stale claims reset to pending
    Gateway->>Session: reprocess exactly-once
    Session-->>Op: log line: reset N stranded messages
```

`replay()` resets stale `claimed` rows back to `pending` so a redelivery — or the gateway's session-level resume ([Issue #3379](https://github.com/MervinPraison/PraisonAI/pull/3379)) — can reprocess them exactly once, and quarantines genuine poison entries to the inbound DLQ. Reset counts are logged at INFO so operators can grep for recovery:

```
Inbound crash-recovery: reset N stranded journaled message(s) to pending on start (unblocked for redelivery/resume)
```

<CardGroup cols={2}>
  <Card title="Gateway Restart Continuation" icon="rotate-right" href="/docs/features/gateway-restart-continuation">
    How in-flight turns resume after a gateway restart
  </Card>

  <Card title="Gateway Session Continuity" icon="link" href="/docs/features/gateway-session-continuity">
    Session-level exactly-once resume path (#3379)
  </Card>
</CardGroup>

***

## When to use which option

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start{Need durability?} -->|Webhook redeliveries| Journal[InboundJournal]
    Start -->|LLM might fail| DLQ[InboundDLQ]
    Start -->|Both| Both[Use both — journal first, DLQ on exception]

    classDef question fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef option fill:#189AB4,stroke:#7C90A0,color:#fff

    class Start question
    class Journal,DLQ,Both option
```

| Feature | **InboundJournal** | **InboundDLQ** |
| - | - | - |
| **When triggered** | Every inbound message | Only on agent failure |
| **Solves** | Webhook redeliveries, crash recovery | Failed LLM calls |
| **Performance** | Fast dedup check | No overhead until failure |
| **Use together** | ✅ Journal → agent → DLQ on exception | ✅ Complete durability stack |

***

## Configuration Options

| Option | Type | Default | Description |
| - | - | - | - |
| `path` | `str \| Path` | **required** | SQLite file path. Parent dirs auto-created. `~` is expanded. |
| `max_size` | `int` | `50_000` | Max entries kept. Excess evicted: completed first, then oldest pending. |
| `ttl_seconds` | `int` | `2_592_000` (30 days) | Completed entries older than this are evicted. |
| `claim_timeout` | `int` | `300` (5 min) | Claimed entries older than this are considered stale and replayed. |
| `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 `praisonaiagents.gateway` policy. When supplied, overrides `max_attempts` + `dead_letter_min_age`. |

### Age-gated quarantine

`replay()` and the redelivery path no longer quarantine on attempts alone. An exhausted entry is only moved to the DLQ once it is **both** attempt-exhausted **and** at least `dead_letter_min_age` (default 6h) old — unless the failure is a known-permanent error class, which short-circuits immediately. A routine LLM outage that burns the claim budget in seconds keeps being retried instead of quarantining every in-flight message. See [Durable Outbound Delivery → Age-gated dead-lettering](/docs/docs/features/durable-delivery#age-gated-dead-lettering-attempts--wall-clock) for the shared policy and decision diagram.

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

***

## Per-platform examples

<Tabs>
  <Tab title="Telegram">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.bots import InboundJournal, BotSessionManager

    journal = InboundJournal(path="~/.praisonai/state/telegram/ingress.sqlite")
    session = BotSessionManager(platform="telegram", ingress_journal=journal)

    agent = Agent(
        name="Telegram Bot",
        instructions="Respond helpfully to Telegram users"
    )

    # In your webhook handler:
    response = await session.chat(
        agent=agent,
        user_id=update.effective_user.id,
        prompt=update.message.text,
        message_id=str(update.message.message_id),
        account="my_telegram_bot"
    )
    ```
  </Tab>

  <Tab title="Discord">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.bots import InboundJournal, BotSessionManager

    journal = InboundJournal(path="~/.praisonai/state/discord/ingress.sqlite")
    session = BotSessionManager(platform="discord", ingress_journal=journal)

    agent = Agent(
        name="Discord Bot",
        instructions="Help Discord server members"
    )

    # In your message handler:
    response = await session.chat(
        agent=agent,
        user_id=str(message.author.id),
        prompt=message.content,
        message_id=str(message.id),
        account="my_discord_bot"
    )
    ```
  </Tab>

  <Tab title="Slack">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.bots import InboundJournal, BotSessionManager

    journal = InboundJournal(path="~/.praisonai/state/slack/ingress.sqlite")
    session = BotSessionManager(platform="slack", ingress_journal=journal)

    agent = Agent(
        name="Slack Bot",
        instructions="Assist Slack workspace users"
    )

    # In your event handler:
    response = await session.chat(
        agent=agent,
        user_id=event["user"],
        prompt=event["text"],
        message_id=event["ts"],
        account="my_slack_bot"
    )
    ```
  </Tab>

  <Tab title="WhatsApp">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.bots import InboundJournal, BotSessionManager

    journal = InboundJournal(path="~/.praisonai/state/whatsapp/ingress.sqlite")
    session = BotSessionManager(platform="whatsapp", ingress_journal=journal)

    agent = Agent(
        name="WhatsApp Bot",
        instructions="Help WhatsApp users with their questions"
    )

    # In your webhook handler:
    response = await session.chat(
        agent=agent,
        user_id=message["from"],
        prompt=message["text"]["body"],
        message_id=message["id"],
        account="my_whatsapp_bot"
    )
    ```
  </Tab>

  <Tab title="Email">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.bots import InboundJournal, BotSessionManager

    journal = InboundJournal(path="~/.praisonai/state/email/ingress.sqlite")
    session = BotSessionManager(platform="email", ingress_journal=journal)

    agent = Agent(
        name="Email Assistant",
        instructions="Respond to email inquiries professionally"
    )

    # In your email processor:
    response = await session.chat(
        agent=agent,
        user_id=email["from"],
        prompt=email["body"],
        message_id=email["message_id"],
        account="support@company.com"
    )
    ```
  </Tab>

  <Tab title="AgentMail">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.bots import InboundJournal, BotSessionManager

    journal = InboundJournal(path="~/.praisonai/state/agentmail/ingress.sqlite")
    session = BotSessionManager(platform="agentmail", ingress_journal=journal)

    agent = Agent(
        name="AgentMail Bot",
        instructions="Handle AgentMail messages efficiently"
    )

    # In your AgentMail handler:
    response = await session.chat(
        agent=agent,
        user_id=message.sender,
        prompt=message.content,
        message_id=message.id,
        account="my_agentmail_account"
    )
    ```
  </Tab>
</Tabs>

***

## Common Patterns

### Pattern 1: Dedup-only (webhook redelivery protection)

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Minimal setup for webhook deduplication
journal = InboundJournal(
    path="~/.praisonai/state/telegram/ingress.sqlite",
    claim_timeout=60  # Fast timeout for quick processing
)

session = BotSessionManager(platform="telegram", ingress_journal=journal)
# Behaviour on duplicate webhook deliveries:
#   - already completed      → skipped (no agent call)
#   - actively being handled → skipped (another worker has the claim)
#   - prior attempt still pending or stale-claimed → retried automatically
```

### Pattern 2: Manual replay (only when driving `InboundJournal` outside the gateway)

<Note>
  Gateway-managed bots get startup and hot-reload replay for free (see [Automatic crash-replay](#automatic-crash-replay)). This pattern is only for custom setups that drive `InboundJournal` directly.
</Note>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Setup with crash recovery
journal = InboundJournal(
    path="~/.praisonai/state/telegram/ingress.sqlite",
    claim_timeout=600  # 10 minutes for complex agent tasks
)

# On startup, replay any stale claimed entries
replayed_count = journal.replay()
print(f"Replayed {replayed_count} stale entries from previous crash")

session = BotSessionManager(platform="discord", ingress_journal=journal)
```

<Tip>
  After PR #1989, redelivery of a still-pending message also retries automatically — you no longer need to wait for a restart plus `replay()` to recover from a mid-flight crash if the platform redelivers the same `message_id`. `replay()` is still the right call on startup to recover any orphaned claims. Under the gateway, both mechanisms now compose: `replay()` unblocks stranded claimed rows at start-up, and redelivery + session-resume then reprocess them exactly once.
</Tip>

### Pattern 3: Combining with InboundDLQ for full durability stack

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.bots import InboundJournal, InboundDLQ, BotSessionManager

# Both journal and DLQ for complete durability
journal = InboundJournal(path="~/.praisonai/state/slack/ingress.sqlite")
dlq = InboundDLQ(path="~/.praisonai/state/slack/inbound_dlq.sqlite")

session = BotSessionManager(
    platform="slack",
    ingress_journal=journal,  # Handles dedup + crash recovery
    dlq=dlq                   # Handles agent failures
)

# Flow: webhook → journal.receive() → agent.chat() → dlq on exception
```

### Pattern 4: Defer inbound completion until reply is delivered (opt-in, safest)

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai.bots import InboundJournal, BotSessionManager

journal = InboundJournal(path="~/.praisonai/state/telegram/ingress.sqlite")
session = BotSessionManager(
    platform="telegram",
    ingress_journal=journal,
    defer_journal_completion=True,  # opt in
)

agent = Agent(name="Support Bot", instructions="Help users with their questions")

# In your adapter's webhook handler:
reply = await session.chat(
    agent=agent,
    user_id=update.effective_user.id,
    prompt=update.message.text,
    message_id=str(update.message.message_id),
    account="my_telegram_bot",
)

# Capture the deferred key BEFORE sending (single-turn slot).
journal_key = session._last_journal_key

# Deliver the reply durably (retry, DLQ-park, etc.).
await telegram.send_message(chat_id=..., text=reply)

# Settle the inbound ONLY after delivery succeeded.
session.complete_last_journal_entry(journal_key)
```

<Warning>
  Always pass the explicit `journal_key` when your adapter can handle concurrent turns (different users under different per-storage-key locks). The no-arg convenience path settles the manager-wide "last" slot, which is ambiguous under concurrency and can settle another turn's still-undelivered inbound.
</Warning>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Defer completion when your send path is durable">
    When your adapter's send path is durable (retry + DLQ-park, or `DurableAdapterMixin`), enable `defer_journal_completion=True`. This closes the crash window between agent completion and reply delivery — a crash mid-send leaves the inbound pending and `replay()` will re-drive it, instead of silently losing the user's answer.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    session = BotSessionManager(
        platform="slack",
        ingress_journal=journal,
        defer_journal_completion=True,
    )
    ```

    Then call `session.complete_last_journal_entry(journal_key)` from your adapter after each successful send.
  </Accordion>

  <Accordion title="Use the same path across restarts">
    The journal's SQLite file must survive restarts for crash recovery to work. Use an absolute path or a location that persists across deployments.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # ✅ Good: persists across restarts
    journal = InboundJournal(path="/var/lib/myapp/ingress.sqlite")

    # ❌ Bad: temporary path, lost on restart
    journal = InboundJournal(path="/tmp/journal.sqlite")
    ```
  </Accordion>

  <Accordion title="Tune claim_timeout to match your agent latency">
    Set `claim_timeout` to be longer than your p99 `agent.chat()` latency to avoid false stale entry detection.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # If your agent takes up to 30 seconds, use a longer timeout
    journal = InboundJournal(
        path="~/.praisonai/state/telegram/ingress.sqlite",
        claim_timeout=900  # 15 minutes safety margin
    )
    ```
  </Accordion>

  <Accordion title="Under the gateway: replay() is automatic">
    When running under the gateway (`praisonai bot start`, `onboard`, `bot.yaml`, `bot.WebSocketGateway`), `replay()` is called automatically at channel start-up and hot-reload — you do not need to call it yourself. Reset counts are logged. See [Automatic crash recovery under the gateway](#automatic-crash-recovery-under-the-gateway).
  </Accordion>

  <Accordion title="Driving InboundJournal directly: call replay() in your startup hook">
    When you drive `InboundJournal` yourself (bare `BotSessionManager`, custom adapter loops), replay stale entries when your bot starts to recover from crashes.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    def startup_hook():
        # Replay any messages that were claimed but not completed
        replayed = journal.replay()
        logger.info(f"Startup replay: {replayed} messages recovered")

    # Call this before starting your bot's event loop
    startup_hook()
    ```
  </Accordion>

  <Accordion title="Keep account stable per bot instance">
    The `account` parameter is part of the deduplication key. Keep it consistent for each bot instance.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # ✅ Good: stable account identifier
    ACCOUNT = "production_telegram_bot_v1"

    await session.chat(..., account=ACCOUNT)

    # ❌ Bad: changing account breaks deduplication
    await session.chat(..., account=f"bot_{random.randint(1,100)}")
    ```
  </Accordion>

  <Accordion title="Set claim_timeout below the platform's redelivery window">
    Platforms redeliver webhooks at fixed intervals (Telegram retries within seconds, Slack waits \~3s before its first retry, etc.). Set `claim_timeout` **shorter** than your platform's redelivery window so that a crashed worker's claim becomes stale before the next redelivery arrives — that's what enables automatic recovery without a restart.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Slack retries after 3s; 30s leaves room for normal processing
    # while ensuring a crashed claim is stale on the next redelivery
    journal = InboundJournal(
        path="~/.praisonai/state/slack/ingress.sqlite",
        claim_timeout=30,
    )
    ```
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Delivery Config" icon="shield-check" href="/docs/features/delivery-config">
    Full reference for the `delivery:` channel config block — defaults, opt-out, and path override
  </Card>

  <Card title="Inbound DLQ" icon="inbox" href="/docs/features/inbound-dlq">
    Failure-side durability when agent execution fails
  </Card>

  <Card title="Durable Outbound Delivery" icon="shield-check" href="/docs/features/durable-delivery">
    Outbound counterpart — persist outgoing messages with retry and idempotency
  </Card>

  <Card title="Bot Routing" icon="route" href="/docs/features/bot-routing">
    Multi-channel session routing for complex bot setups
  </Card>

  <Card title="Visible-Outcome Guarantee" icon="eye" href="/docs/features/visible-outcome-guarantee">
    The delivery-side half of "no inbound message ends without a recorded outcome"
  </Card>
</CardGroup>

<Note>
  Automatic crash-replay introduced in [PraisonAI PR #3622](https://github.com/MervinPraison/PraisonAI/pull/3622) (fixes [#3621](https://github.com/MervinPraison/PraisonAI/issues/3621)).
</Note>


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