Skip to content

Delivery-failure retry loop: wake the agent to fix and resend undelivered outbound messages#14

Merged
dimavrem22 merged 1 commit into
mainfrom
feat/delivery-failure-retry-loop
Jul 10, 2026
Merged

Delivery-failure retry loop: wake the agent to fix and resend undelivered outbound messages#14
dimavrem22 merged 1 commit into
mainfrom
feat/delivery-failure-retry-loop

Conversation

@dimavrem22

Copy link
Copy Markdown
Contributor

Mirrors inkbox-ai/hermes-agent-plugin#48 for the Codex bridge — the same feature, ported to the gateway/session architecture.

What

An outbound message can die two ways, and until now the bridge only half-handled it:

  • Rejected at send time — the server's outbound content policy 422s the send (markdown artifacts, emoji overload…), the recipient opted out, the address is bad, or the body is too long. Recovered once inside the session, with no shared budget.
  • Failed after acceptance — the carrier flags it (e.g. error 40002) or the receiving mail server bounces it, reported later via text.delivery_failed / imessage.delivery_failed / message.bounced / message.failed webhooks. Woke the agent with no cap at all.

Both surfaces now feed one delivery-failure loop: the agent is woken in the same contact session with the exact error (rule/code/detail) plus its own undelivered body, and instructed to fix and resend via its Inkbox tools (or send a compliant alternative, or stop with [SILENT]).

Guardrails (feature behavior)

  • Hard cap: 3 total sends per logical reply. Failures 1 and 2 wake the agent; failure 3 goes quiet with a loud log line. The budget is shared across both failure surfaces (keyed by conversation + recipient, merged by max) — the synchronous send-rejection path records into the same gateway store via a new on_send_failure callback, gated by the cap.
  • Budget resets on a fresh inbound, a delivered receipt (text.delivered / imessage.delivered), or a 30-minute TTL.
  • Webhook replays are deduped per failed message.
  • Mail subscriptions now include message.bounced / message.failed, and the phone/iMessage lines subscribe to their delivery lifecycle — the failure handlers already existed but never fired without the subscription.

Testing

  • 16 unit tests (tests/test_delivery_failure_retry.py): cap semantics, budget sharing across the sync + webhook surfaces, resets (inbound/delivered/TTL), replay dedup, and all three channels.
  • Two live tests (real-model leg): test_sms_retry_after_carrier_delivery_failure (forged, org-key-signed text.delivery_failed webhook posted to the gateway's local listener) and test_sms_retry_after_internal_spam_block (emoji-overload bait) — both assert the loop engaged via the gateway-log wake fingerprint. Because Codex wakes through run_consult (the reply isn't auto-sent), a delivered follow-up is a best-effort signal rather than the assertion.
  • Live-suite conversation-health reset guardrail (tests/live/conftest.py) plus per-send body diversification so the shared 10DLC pool's duplicate_body / unanswered_limit rules stop blocking the driver on no-reply tests (voice "call me" → the agent calls back, never texts).

Requires inkbox>=0.4.20 (ships blocked_spam_filter); the floor is raised across package metadata, the setup wizard, and its tests. Version bump 0.1.3 → 0.1.4.

Mirrors inkbox-ai/hermes-agent-plugin#48 for the Codex bridge.

An outbound message can die two ways and until now the loop only half
handled it: rejected synchronously at send time (server content policy,
opt-out, bad address, too-long) recovered once inside the session, while
an accepted-then-failed message (carrier rejection, mail bounce) reported
by a lifecycle webhook woke the agent with NO cap at all. Both surfaces
now feed one budget:

- Hard cap: OUTBOUND_FAILURE_MAX_ATTEMPTS (3) total sends per logical
  reply. Failures 1 and 2 wake the agent with the exact error + the
  undelivered body; failure 3 goes quiet with a loud log line.
- The budget is shared across both surfaces (keyed by conversation +
  recipient, merged by max) — the sync path now records into the same
  gateway store via on_send_failure, gated by the cap.
- Reset on a fresh inbound, a delivered receipt (text/imessage.delivered),
  or a 30-minute TTL. Webhook replays stay deduped per failed message.
- Per-channel fix-it guidance + attempt count in the wake-up prompt, and
  an explicit [SILENT] escape.

Also: subscribe the mailbox to message.bounced/message.failed and the
phone/iMessage lines to their delivery lifecycle events — the failure
handlers already existed but never fired without the subscription.

Testing: tests/test_delivery_failure_retry.py (16 unit tests) covers the
cap, budget sharing across sync + webhook, resets (inbound/delivered/TTL),
replay dedup, and all three channels. Two live tests (real leg) exercise
both paths: a forged, org-key-signed text.delivery_failed webhook posted
to the gateway's local listener, and an emoji-overload bait that trips the
server's content filter — both assert the loop engaged via the gateway
log. Live suite gains a conversation-health reset guardrail (conftest.py)
plus per-send body diversification so the shared 10DLC pool's
duplicate_body / unanswered_limit rules stop blocking the driver.

Requires inkbox>=0.4.20 (ships the blocked_spam_filter SmsDeliveryStatus
variant); floor raised across package metadata, the setup wizard, and its
tests. Bump 0.1.3 -> 0.1.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dimavrem22 dimavrem22 merged commit 0a7835b into main Jul 10, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant