Skip to main content
Inspect why an agent reply is stuck, retry it after a channel recovers, or drop a genuinely-dead one โ€” one entry at a time.

Quick Start

1

Run an agent bot (writes to the outbox automatically)

A running gateway bot persists every outbound reply to ~/.praisonai/state/gateway_outbox.sqlite โ€” no code change needed.
2

Find stuck replies

3

Retry one after the channel recovers

4

Or drop a genuinely-dead one


User interaction flow

An operator moves a stuck reply from failed to delivered without restarting the gateway.

Commands

Four subcommands mounted under praisonai gateway outbox.
outbox stats gives an at-a-glance breakdown instead of a single counter:
Clear everything in one shot (asks for confirmation unless --yes):

Where the outbox lives

outbox resolves the SQLite path in a fixed precedence order.

Entry keys

outbox list prints a full tracking key per entry in the form target:idempotency_key:id, e.g. slack:C0abc:msg-d4e5:18. outbox retry and outbox purge consume that key verbatim. Keys are always printed un-truncated so operators can copy them directly. The key is matched in full against the stored row, so a stale key from another outbox โ€” or a mistyped one โ€” can never mutate the wrong message.

Safe to run alongside a live gateway

All four subcommands โ€” outbox list, outbox stats, outbox retry, outbox purge โ€” open the SQLite store with read_only=True. read_only=True only skips the sending โ†’ recovered crash-recovery migration. Targeted ops still work: retry only touches failed/permanent_failure rows, and purge <key> refuses sending rows. purge --all is an explicit, confirmed operator action and still deletes every row, including sending ones. Fixed in PraisonAI PR #5954 (issue #5953) โ€” before the fix, retry and purge opened the store read-write and could duplicate a live gatewayโ€™s in-flight sends.
Operator CLI opens must never run crash recovery on a live database. Flipping in-flight sending rows to recovered while the owning gateway is mid-send lets a later drain re-claim and duplicate a message. All four operator subcommands (list, stats, retry, purge) open the store with read_only=True, which skips the sending โ†’ recovered migration while leaving targeted ops working.

Python API

The CLI mirrors four OutboundQueue methods. Wire them from Python directly.
read_only=True only skips the sending โ†’ recovered migration โ€” targeted writes still succeed. Runtime drainers (the gateway itself, DurableAdapterMixin adapters) still open the store read-write and run crash recovery on open; this guidance is for operator code running alongside a live gateway.

list(*, status=None, target=None, limit=100)

Returns โ€” a list of OutboundEntry (newest first). Never mutates state. Status filter values: pending, sending, recovered, sent, failed, permanent_failure.

stats() -> dict[str, int]

Returns โ€” a per-status count map; only statuses with at least one entry are present.

retry(key) -> bool (async)

Resets a failed/permanent_failure entry back to pending and clears its attempts so the next drain() re-dispatches it. Returns True if requeued, False if the key is unknown, already sent, in-flight (sending), or crash-recovered.

purge_entry(key) -> bool

Deletes a single entry by its full tracking key. Returns True if removed, False on unknown/mismatched key or an in-flight sending entry.

Best Practices

outbox list prints keys un-truncated on their own line. retry and purge match the full target:idempotency_key:id โ€” a partial or edited key is rejected, never guessed.
A crash-recovered entry was in-flight when the process last crashed, so its delivery outcome is unknown. The next drain() already reconciles or re-sends with a โ€œpossible duplicateโ€ annotator. Flipping it to pending manually strips that safeguard and risks an unlabelled duplicate.
purge_entry refuses an in-flight sending row. The gateway may have already handed the payload to the channel API and be awaiting mark_sent/mark_failed. Deleting the row makes mark_sent a no-op and the caller report a spurious failure despite delivery. Wait until it transitions.
Opening OutboundQueue(path=...) without read_only=True while the gateway is running flips every in-flight sending row to recovered โ€” the gatewayโ€™s next drain then re-dispatches those rows, producing duplicate replies. Use read_only=True for any operator-side script that touches a live outbox; retry and purge_entry still succeed under it.
praisonai gateway status --deep now prints an outbox hint pointing at outbox list, so the diagnostic surface leads straight to the operator commands.

Inbound DLQ

Inbound counterpart โ€” same shape, praisonai bot dlq list|replay|purge

Durable Outbound Delivery

How the outbox is populated, drained, and reconciled

Outbound Resilience

Retry-with-backoff and dead-lettering that feed the outbox

Gateway

Where the durable outbox is wired into the gateway server