openclaw - ✅(Solved) Fix [Bug]: Telegram final reply stuck in pendingFinalDelivery with attempt=1, lastError=null, no sendMessage [1 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#84238Fetched 2026-05-20 03:42:18
View on GitHub
Comments
2
Participants
3
Timeline
8
Reactions
1
Author
Timeline (top)
labeled ×5commented ×2cross-referenced ×1

On OpenClaw 2026.5.18, a normal Telegram DM turn can finish successfully, persist pendingFinalDelivery: true, record one delivery attempt with pendingFinalDeliveryLastError: null, but never actually produce a Telegram sendMessage ok / outbound delivery log and never clear the pending final.

This is distinct from #83184, which is about the heartbeat path not clearing pending fields after a successful heartbeat send. In this case, the user-message final reply is generated and persisted, OpenClaw records an attempt, but the delivery path has no observable send success and no recorded failure.

Error Message

The final reply was persisted and one attempt was recorded, but no Telegram send success was observed and no error was recorded:

Root Cause

This failure mode is difficult to detect because pendingFinalDeliveryLastError remains null, so ordinary health checks treat the system as healthy.

Fix Action

Fix / Workaround

  • Add an explicit state machine such as queued -> sending -> sent | failed.
  • Record the adapter/channel send result durably, including Telegram message id when available.
  • Record a failure when the dispatch path exits without invoking the adapter.
  • Clear pending only after confirmed send success.
  • Add an idempotency key such as sessionId + pendingFinalDeliveryIntentId to prevent duplicate sends if both the built-in delivery path and a watchdog/retry worker race.
  • Consider a small built-in retry worker for stale pending finals.

PR fix notes

PR #84382: Fix pending final delivery without send proof

Description (problem / solution / changelog)

Summary

  • Treat routed final replies with no visible outbound delivery result as failed instead of successful.
  • Preserve pending final delivery state after failed final dispatch and record a non-null retry error.
  • Keep restart recovery from marking deliver: false pending-final resume requests as clean delivery success.

Root cause

Final reply state could be persisted before channel delivery, while route-level suppressed/no-result sends were reported as success. Restart recovery also recorded a clean pending-final attempt for a gateway resume request that explicitly used deliver: false, leaving pendingFinalDeliveryLastError null even though channel delivery had not been proven.

Linked Issue

Fixes #84238.

Why This Is Safe

The change only affects runtime accounting after a visible final reply has no routed or queued send proof. Successful deliveries still clear pending-final metadata, and pre-send policy suppression for silent/empty/reasoning replies remains unchanged.

Security / Runtime Controls

No auth, allowlist, send-policy, prompt, credential, or gateway permission controls changed. The behavior is enforced in runtime delivery accounting, not by prompt text.

Verification

  • CI=true pnpm test src/auto-reply/reply/route-reply.test.ts src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts src/agents/main-session-restart-recovery.test.ts
  • git diff --check
  • CI=true pnpm check:changed

Behavior addressed: pending final delivery no longer treats a no-result routed send or deliver: false resume as clean success. Real environment tested: local macOS checkout with mocked channel delivery regression coverage. Exact steps or command run after this patch: CI=true pnpm test src/auto-reply/reply/route-reply.test.ts src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts src/agents/main-session-restart-recovery.test.ts Evidence after fix: route reply returns failure for visible replies with no outbound result; final dispatch preserves pending-final state and writes a non-null last error; restart recovery persists a non-null pending-delivery note. Observed result after fix: focused tests passed, git diff --check passed, and CI=true pnpm check:changed passed. What was not tested: live Telegram DM delivery with real credentials; broader outbox retry-worker design remains unchanged.

Out Of Scope

  • Adding a new pending-final retry worker.
  • Reworking the outbound durable queue state machine.
  • Live Telegram credentialed E2E validation.

Made with Cursor

Changed files

  • src/agents/main-session-restart-recovery.test.ts (modified, +3/-1)
  • src/agents/main-session-restart-recovery.ts (modified, +2/-1)
  • src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts (modified, +5/-1)
  • src/auto-reply/reply/dispatch-from-config.ts (modified, +34/-0)
  • src/auto-reply/reply/route-reply.test.ts (modified, +45/-1)
  • src/auto-reply/reply/route-reply.ts (modified, +9/-0)

Code Example

2026-05-20 01:14:15 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: pending age 3s < 120s; skipped
2026-05-20 01:15:17 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: pending age 33s < 120s; skipped
2026-05-20 01:16:18 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: pending age 94s < 120s; skipped
2026-05-20 01:17:19 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: resending pending final via telegram/main_local_tg/<redacted-user-id>
2026-05-20 01:17:22 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: resend ok messageId=unknown
2026-05-20 01:17:23 pending-final watchdog: complete candidates=1 changed=1 dry_run=False

---

{
  "status": "done",
  "pendingFinalDelivery": true,
  "pendingFinalDeliveryAttemptCount": 1,
  "pendingFinalDeliveryLastError": null,
  "pendingFinalDeliveryTextLen": 419,
  "deliveryContext": {
    "channel": "telegram",
    "to": "telegram:<redacted-user-id>",
    "accountId": "main_local_tg"
  }
}

---

pendingFinalDeliveryAttemptCount = 1
pendingFinalDeliveryLastError = null
no matching Telegram sendMessage ok
pendingFinalDelivery remains true
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

On OpenClaw 2026.5.18, a normal Telegram DM turn can finish successfully, persist pendingFinalDelivery: true, record one delivery attempt with pendingFinalDeliveryLastError: null, but never actually produce a Telegram sendMessage ok / outbound delivery log and never clear the pending final.

This is distinct from #83184, which is about the heartbeat path not clearing pending fields after a successful heartbeat send. In this case, the user-message final reply is generated and persisted, OpenClaw records an attempt, but the delivery path has no observable send success and no recorded failure.

Impact

  • The model generated the final reply.
  • Telegram API/provider was not obviously down.
  • The session reached status: done.
  • The final answer stayed stuck in pendingFinalDelivery.
  • The user saw no reply until an out-of-band local watchdog resent the pending final.

This failure mode is difficult to detect because pendingFinalDeliveryLastError remains null, so ordinary health checks treat the system as healthy.

Observed evidence

Deployment:

  • OpenClaw version: 2026.5.18
  • OS: macOS
  • Install method: npm/CLI under launchd
  • Channel: Telegram DM
  • Gateway/provider were otherwise healthy

Timeline, local time:

  • 00:45:36: Telegram inbound message received.
  • Around 00:47:54: agent run completed and session reached status: done.
  • The session store contained a complete final reply in pending final delivery:
    • pendingFinalDelivery: true
    • pendingFinalDeliveryTextLen: 419
    • pendingFinalDeliveryAttemptCount: 1
    • pendingFinalDeliveryLastError: null
    • deliveryContext.channel: telegram
    • deliveryContext.accountId: main_local_tg
    • deliveryContext.to: telegram:<redacted-user-id>
  • Between 00:47 and 00:52, gateway logs had no matching Telegram outbound sendMessage ok for that final answer.
  • At 01:14 / 01:15 / 01:16, an external watchdog observed the pending final but skipped it because it was younger than the local 120s threshold.
  • At 01:17:19, the watchdog resent the pending final via Telegram and then cleared the pending fields successfully.

Relevant local watchdog log excerpt:

2026-05-20 01:14:15 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: pending age 3s < 120s; skipped
2026-05-20 01:15:17 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: pending age 33s < 120s; skipped
2026-05-20 01:16:18 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: pending age 94s < 120s; skipped
2026-05-20 01:17:19 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: resending pending final via telegram/main_local_tg/<redacted-user-id>
2026-05-20 01:17:22 pending-final watchdog: agent:main:telegram:direct:<redacted-user-id>: resend ok messageId=unknown
2026-05-20 01:17:23 pending-final watchdog: complete candidates=1 changed=1 dry_run=False

Session-store snapshots showed the stuck state before recovery:

{
  "status": "done",
  "pendingFinalDelivery": true,
  "pendingFinalDeliveryAttemptCount": 1,
  "pendingFinalDeliveryLastError": null,
  "pendingFinalDeliveryTextLen": 419,
  "deliveryContext": {
    "channel": "telegram",
    "to": "telegram:<redacted-user-id>",
    "accountId": "main_local_tg"
  }
}

After watchdog recovery, the same session no longer had pending final fields set.

Expected behavior

Final delivery should behave like a reliable outbox:

  1. Persist final reply as pending/outbox entry.
  2. Attempt channel delivery.
  3. If Telegram delivery succeeds, record message id / success and clear pending.
  4. If delivery fails or no adapter send occurs, record a non-null pendingFinalDeliveryLastError.
  5. Leave enough durable state for retry workers/watchdogs to know whether the message was sent, failed, or still pending.

Actual behavior

The final reply was persisted and one attempt was recorded, but no Telegram send success was observed and no error was recorded:

pendingFinalDeliveryAttemptCount = 1
pendingFinalDeliveryLastError = null
no matching Telegram sendMessage ok
pendingFinalDelivery remains true

Suggested fix

Treat pendingFinalDelivery as a durable outbox rather than a best-effort callback:

  • Add an explicit state machine such as queued -> sending -> sent | failed.
  • Record the adapter/channel send result durably, including Telegram message id when available.
  • Record a failure when the dispatch path exits without invoking the adapter.
  • Clear pending only after confirmed send success.
  • Add an idempotency key such as sessionId + pendingFinalDeliveryIntentId to prevent duplicate sends if both the built-in delivery path and a watchdog/retry worker race.
  • Consider a small built-in retry worker for stale pending finals.

Related issues

  • #83184: heartbeat-driven replies leave pendingFinalDelivery stuck after successful heartbeat delivery.
  • #80715: Slack replies silently dropped after being composed.
  • #78532: delivery success reported when no adapter was invoked.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Final delivery should behave like a reliable outbox:

  1. Persist final reply as pending/outbox entry.
  2. Attempt channel delivery.
  3. If Telegram delivery succeeds, record message id / success and clear pending.
  4. If delivery fails or no adapter send occurs, record a non-null pendingFinalDeliveryLastError.
  5. Leave enough durable state for retry workers/watchdogs to know whether the message was sent, failed, or still pending.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Telegram final reply stuck in pendingFinalDelivery with attempt=1, lastError=null, no sendMessage [1 pull requests, 2 comments, 3 participants]