Undelivered Message Notice

Close the loop when a gateway reply cannot be delivered — plain-text notice + operator hook

Tell your user (and your operators) when a reply can’t be delivered instead of losing it silently.

Diagram

success

permanent fail

📤 Outbound Reply

📡 Deliver?

✅ Delivered

🔁 Close the Loop

💬 Plain-text notice

🪝 MESSAGE_UNDELIVERED

📦 DLQ park + mark dead

Quick Start

  1. Turn it on

    One boolean flips the feature on. A permanent delivery failure now sends a built-in plain-text notice on the same channel.

    yaml
    gateway:
      notify_on_undelivered: true
  2. Customise the notice

    Replace the built-in message with your own wording. ${VAR} substitution applies.

    yaml
    gateway:
      notify_on_undelivered: true
      undelivered_template: "⚠️ We couldn't deliver that reply. Please try again."
  3. Route the failure from an Agent

    Subscribe to MESSAGE_UNDELIVERED to mirror, alert, or re-queue — no adapter patching.

    python
    from praisonaiagents import Agent
    from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
    
    registry = HookRegistry()
    
    @registry.on(HookEvent.MESSAGE_UNDELIVERED)
    def route_to_ops(event_data):
        # event_data is a MessageUndeliveredInput
        print(
            f"[UNDELIVERED] {event_data.platform}:{event_data.channel_id} "
            f"error={event_data.error} notice_delivered={event_data.notice_delivered}"
        )
        return HookResult.allow()
    
    agent = Agent(name="broadcaster", instructions="Send updates.", hooks=registry)

How It Works

A reply that fails permanently — the target is confirmed dead, or delivery exhausted its retries — parks in the DLQ and marks the target dead. With notify_on_undelivered on, the router also sends a short notice on the same channel and fires the hook.

Diagram
MESSAGE_UNDELIVEREDDeadTargetRegistryDeliveryRouterAdapterUserMESSAGE_UNDELIVEREDDeadTargetRegistryDeliveryRouterAdapterUseralt[notify_on_undelivered=true]send(reply)403 / 404 / 410 (permanent)mark_dead(platform, channel)send(short plain notice)⚠️ delivery-failure text (best-effort)MessageUndeliveredInput(notice_delivered=…)raise original error
StepBehaviour
ClassifyA permanent failure is detected via 403 / 404 / 410 or platform text patterns.
Mark deadThe target is flagged in the dead-target registry (if attached).
NoticeBest-effort plain-text note on the same channel — a one-liner may land where a large reply did not.
HookMESSAGE_UNDELIVERED fires with the outcome, whether or not the notice landed.
Re-raiseThe original send failure still propagates so retry/DLQ machinery keeps its contract.

Configuration Options

gateway.yaml

KeyTypeDefaultDescription
notify_on_undeliveredboolfalseOpt-in. When true, the router surfaces permanent failures.
undelivered_template`str \null`nullPlain-text notice sent on the same channel. null uses the built-in default. ${VAR} substitution applies.

Built-in default notice: ⚠️ Your request was processed but the reply couldn't be delivered.

DeliveryRouter kwargs (Python)

KwargTypeDefaultDescription
notify_on_undeliveredboolFalseSame semantics as the YAML key.
undelivered_templateOptional[str]NoneOverrides the notice text. None uses DeliveryRouter.DEFAULT_UNDELIVERED_TEMPLATE.
python
from praisonai_bot.bots.delivery import DeliveryRouter

router = DeliveryRouter(
    botos,
    notify_on_undelivered=True,
    undelivered_template="⚠️ We couldn't deliver that reply.",
)

MessageUndeliveredInput fields

FieldTypeDescription
platformstrPlatform id ("telegram", "slack", …).
contentstrThe original outbound content that failed to deliver.
channel_idstrChannel the reply was targeted at.
errorstrFormatted as `”

: <message>”. | | notice_delivered | bool | Whether the plain-text notice succeeded on the same channel. | | session_id, cwd, event_name, timestamp, agent_name | — | Inherited HookInput` fields. |


Common Patterns

Mirror every undelivered reply to a home channel.

python
from praisonaiagents import Agent
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult

registry = HookRegistry()

@registry.on(HookEvent.MESSAGE_UNDELIVERED)
def mirror_home(event_data):
    print(f"[home] {event_data.platform}:{event_data.channel_id} -> {event_data.content}")
    return HookResult.allow()

agent = Agent(name="mirror", instructions="Forward failures.", hooks=registry)

Alert only when even the fallback notice failed to land.

python
from praisonaiagents import Agent
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult

registry = HookRegistry()

@registry.on(HookEvent.MESSAGE_UNDELIVERED)
def page_on_total_loss(event_data):
    if not event_data.notice_delivered:
        print(f"[PAGE] total delivery loss on {event_data.platform}:{event_data.channel_id}")
    return HookResult.allow()

agent = Agent(name="pager", instructions="Escalate hard failures.", hooks=registry)

Best Practices

A one-liner slips through where a rich reply could not

The original reply may have failed for length or rich-media reasons. A short plain-text notice often lands on the same channel, so the user still knows something happened.

Handler errors never break delivery

Any exception in your MESSAGE_UNDELIVERED handler is swallowed. The router still re-raises the original send failure, so retry and DLQ machinery keep their contract.

It composes with the dead-target registry

The notice and hook fire on the same permanent-failure branch that marks a target dead. Subsequent sends short-circuit at the registry, so the notice does not repeat. See Dead-Target Registry.

The notice shares the platform rate-limit bucket

The fallback notice goes through the same adapter and shares the platform rate-limit bucket. See Bot Rate Limiting.


Related