Skip to content

fix(notifications): deliver Telegram frames when the paired chat is not a forum - #3897

Merged
Yeachan-Heo merged 3 commits into
Yeachan-Heo:devfrom
yazzang-homelab:fix/telegram-private-chat-topic-refusal
Aug 6, 2026
Merged

fix(notifications): deliver Telegram frames when the paired chat is not a forum#3897
Yeachan-Heo merged 3 commits into
Yeachan-Heo:devfrom
yazzang-homelab:fix/telegram-private-chat-topic-refusal

Conversation

@yazzang-homelab

Copy link
Copy Markdown
Contributor

What

Fix Telegram notification frames being dropped in a paired private chat whose bot has no Threaded Mode.

  • isThreadedModeCapabilityRefusal now recognizes Telegram's real reply for that chat: Bad Request: the chat is not a forum.
  • The suppressed/rejected verdict for createForumTopic moved from caller-local flags into typed errors (TopicCreationSuppressed, TopicCreationRejected), so every awaiter of the shared in-flight creation classifies the same rejection identically.
  • A confirmed capability refusal is latched, so later frames stop re-issuing an already rejected createForumTopic once per message.

Why

TopicRegistry.getOrCreateTopic de-duplicates concurrent creations behind one in-flight promise. Only the caller that actually ran the create callback set creationSuppressed/creationRejected; any frame that joined the shared promise saw both flags false, fell through to the fail-closed branch, and rethrew:

notifications: Telegram topic creation failed: Error: createForumTopic: invalid message_thread_id
notifications daemon: handleSessionMessage failed

That frame is then dropped instead of being flat-delivered to the private chat — observed 10 times in one day on a live install (gjc 0.12.12), and the reason identity headers published after /resume (the renamed session) and pending asks never arrive.

Verified against the live Bot API for a paired private chat:

{"ok": false, "error_code": 400, "description": "Bad Request: the chat is not a forum"}

That description matched none of the refusal patterns, so the deterministic flat-delivery path was never taken and the daemon retried the rejected create for every single frame.

Testing

  • New regression test private chat without Threaded Mode: concurrent frames all deliver flat in packages/coding-agent/test/notifications-telegram-daemon.test.ts: with a slow non-forum rejection, a concurrent identity_header + action_needed pair must both deliver flat, a following context_update must deliver, and only one createForumTopic attempt may be made. It fails on dev with the exact production error and passes with this change.
  • bun test packages/coding-agent/test/notifications-telegram-daemon.test.ts — 553 pass, 0 fail.
  • bun test scripts/telegram-daemon-generation-guard.test.ts — 48 pass; scripts/telegram-daemon-generation-guard.ts --validate-current-tree and the base/head run (v43 no protected changes) are clean.
  • bun --cwd=packages/coding-agent run check — clean.

GJC verdict

gajae.pr-review-verdict.v1 needs-human sha256:0afa50eaef4d1db8ba9bab0dfa31de4abe1756e3 reviewer:human evidence:local bun test packages/coding-agent/test/notifications-telegram-daemon.test.ts

  • Target branch is dev
  • bun check passes
  • Tested locally
  • CHANGELOG updated (if user-facing)
  • Verdict above matches the exact PR head, not an earlier commit

…ot a forum

A paired private chat whose bot has no Threaded Mode answers
createForumTopic with "Bad Request: the chat is not a forum". That
description was not a recognized capability refusal, and the
suppressed/rejected verdict lived in caller-local flags while
getOrCreateTopic shares one in-flight creation, so every awaiter that
joined the shared promise rethrew and dropped its frame - identity
headers published after /resume, asks, and context updates never
reached the chat.

The rejection now travels as typed errors so every awaiter classifies it
identically, the non-forum description counts as a capability refusal,
and a confirmed refusal is latched so later frames stop re-issuing a
rejected createForumTopic per message.

Lore-id: 7c3a1f95
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: concurrent identity/ask frames against a non-forum private chat deliver flat with a single createForumTopic attempt
Not-tested: live Telegram bot with Threaded Mode enabled mid-run (latch requires daemon restart)
@yazzang-homelab
yazzang-homelab force-pushed the fix/telegram-private-chat-topic-refusal branch from 0afa50e to e926f2e Compare August 5, 2026 23:24
@yazzang-homelab

Copy link
Copy Markdown
Contributor Author

Rebased onto current dev (11e48d5bc, #3901) — the previous head was CONFLICTING/DIRTY on the CHANGELOG ## [Unreleased] section. New head e926f2ed5; rebase was CHANGELOG-only, no code merges, and the diff is otherwise unchanged.

Re-verified on the new base: bun test packages/coding-agent/test/notifications-telegram-daemon.test.ts → 553 pass, 0 fail; bun scripts/telegram-daemon-generation-guard.tsv43 no protected changes (no generation bump owed).

@Yeachan-Heo
Yeachan-Heo merged commit 85e1e6b into Yeachan-Heo:dev Aug 6, 2026
22 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.

2 participants