← Back to PRs

#11653: fix(telegram): retry without message_thread_id on stale forum thread error

by liuxiaopai-ai open 2026-02-08 03:45 View on GitHub →
channel: telegram stale
## Summary Fixes #11620 — The `message` tool fails with `400: Bad Request: message thread not found` when sending to a Telegram chat where Forum/Topics mode was previously enabled and then disabled. ## Root Cause When Forum/Topics mode is toggled off in a Telegram chat, the `message_thread_id` persisted in the session store (`lastThreadId` via `deliveryContextFromSession()`) becomes stale. The outbound send path unconditionally includes this stale thread ID in API calls: 1. `resolveSessionDeliveryTarget()` reads `lastThreadId` from session store 2. `deliverOutboundPayloads()` passes it to the Telegram outbound adapter 3. `sendMessageTelegram()` builds `{ message_thread_id: <stale_id> }` and includes it in the API call 4. Telegram rejects with `400: Bad Request: message thread not found` This affects all send types: `sendMessage`, `sendPhoto`, `sendVideo`, `sendAnimation`, `sendDocument`, `sendAudio`, `sendVoice`. Notably, **agent auto-replies work fine** because the inbound path reads `is_forum` from each incoming message and only builds `threadSpec` when the chat is actually a forum. But the `message` tool's outbound path trusts the session store blindly. ## Fix Added automatic retry logic in `sendMessageTelegram()`: when the Telegram API returns `400: Bad Request: message thread not found` **and** the request included a `message_thread_id`, the function retries once with the thread parameter removed. This ensures messages still deliver even when the session state contains an outdated thread ID. The fix is applied at the `sendMessageTelegram` level so all send types benefit from the same retry behavior without modifying each individual call site. ## Changes | File | Change | |------|--------| | `src/telegram/send.ts` | Add `THREAD_NOT_FOUND_RE` regex, `isThreadNotFoundError()` check, and try/catch retry wrapper around send logic (+30 lines) | | `src/telegram/send.thread-not-found-retry.test.ts` | 4 new unit tests | ## Tests 4 unit tests covering: - **Text message retry**: sends with `message_thread_id` → gets "thread not found" → retries without it → succeeds - **No retry for unrelated errors**: `chat not found` is not retried (only called once) - **No retry without thread ID**: if no `message_thread_id` was set, the error propagates normally - **Media retry**: `sendPhoto` with thread ID → "thread not found" → retries without thread → succeeds ``` ✓ src/telegram/send.thread-not-found-retry.test.ts (4 tests) 63ms Test Files 1 passed (1) Tests 4 passed (4) ``` ## Design Decision This retry-at-send approach was chosen over alternatives because: - **Minimal blast radius**: only affects sends that actually fail with this specific error - **No session store migration needed**: stale `lastThreadId` values self-heal on next send - **Covers all send types**: text, photo, video, audio, document, animation, voice - **Safe**: the retry is bounded (once only) and only triggered for the exact error pattern <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR adds a targeted retry in `src/telegram/send.ts` to handle Telegram’s `400: Bad Request: message thread not found` error when a stale `message_thread_id` is included (e.g., after Forum/Topics mode is disabled). It detects that specific error and retries once with the thread parameter removed, and introduces a new Vitest suite (`src/telegram/send.thread-not-found-retry.test.ts`) to validate retry behavior for text and media sends. Overall the approach is consistent with the existing outbound send flow (centralized in `sendMessageTelegram()` so all send types benefit) and keeps the retry bounded in intent; however, the current implementation uses recursion without a hard guard, and the retry can still re-derive a thread id from `to` via `parseTelegramTarget()`, which undermines the “retry without thread” guarantee in some inputs. <h3>Confidence Score: 3/5</h3> - Reasonably safe change, but retry logic has a clear edge case that can cause repeated retries/recursion in some inputs. - The fix is narrowly scoped and covered by unit tests, but `sendMessageTelegram()` retries by recursively calling itself without an explicit single-attempt guard and may reintroduce `message_thread_id` via `parseTelegramTarget(to)` on the retry call. Those issues can lead to repeated failures or unbounded recursion in the exact scenario this PR is meant to harden. - src/telegram/send.ts; src/telegram/send.thread-not-found-retry.test.ts <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs