#19991: feat(telegram): callback direct mode with dedupe, button state, and tap intercept
channel: telegram
size: XL
Cluster:
Telegram Inline Button Enhancements
## Summary
Adds a new `callback` config block under `channels.telegram` for fine-grained control over Telegram callback query handling.
### New features
- **`callback.enabled`** — enables direct callback mode (callback queries handled natively instead of being forwarded as synthetic messages)
- **`callback.tapIntercept`** — middleware-level early intercept with ack support (`ackText`, `ackAlert`)
- **`callback.forwardUnhandled`** — controls whether unmatched callbacks fall through to message processing
- **`callback.dedupeWindowMs`** — deduplicates rapid repeated clicks within a time window (capped at 60s)
- **`callback.buttonStateMode`** — visual feedback after click: `off` / `mark-clicked` (✅) / `disable-clicked` (sentinel)
### Fixes included
- **C1**: Fixed double `answerCallbackQuery` when `tapIntercept=true` + `ackText` configured — ack now happens in the middleware with the correct params, handler skips if already answered
- **M1**: `forwardUnhandled=false` now has schema validation requiring `enabled=true`; JSDoc clarified
- **N2**: Incoming clicks on `disable-clicked` buttons (sentinel prefix `ocb:clicked`) are detected and early-returned
- **R2**: `dedupeWindowMs` capped at 60,000ms to prevent memory accumulation
### Test coverage
- tap intercept + ackText: verifies single answerCallbackQuery call with correct params
- disable-sentinel detection: verifies early return without processMessage
- All `buttonStateMode` enum values tested (`off`, `mark-clicked`, `disable-clicked`)
- Regression: legacy mode (`enabled=false`) still forwards callbacks as messages
### Config example
```json5
{
channels: {
telegram: {
callback: {
enabled: true,
tapIntercept: true,
ackText: "Got it!",
forwardUnhandled: false,
dedupeWindowMs: 3000,
buttonStateMode: "disable-clicked"
}
}
}
}
```
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Adds a new `callback` config block under `channels.telegram` for fine-grained control over Telegram callback query handling, including direct callback mode, tap intercept middleware, dedupe, and post-click button state editing.
- **Config & Schema**: New `TelegramCallbackConfig` type and Zod schema with validation (e.g., `forwardUnhandled=false` requires `enabled=true`). Well-structured with proper `strict()` and `superRefine`.
- **Tap Intercept**: Middleware in `bot.ts` answers callback queries early to prevent Telegram retries; the downstream handler skips its own ack when tap intercept is on. However, the middleware doesn't distinguish disabled sentinel buttons from regular callbacks, so `ackText` is shown for already-disabled buttons.
- **Dedupe**: `callbackDedupeCache` with lazy cleanup on each callback and 60s cap. Correctly gated behind `callbackDirectEnabled`.
- **Button State Mode**: `buildEditedCallbackKeyboard` treats `mark-clicked` and `disable-clicked` identically (both replace callback_data with sentinel and keep only the clicked button). The `mode` parameter is `void`-ed. The JSDoc documents different behaviors for each mode that don't match the implementation.
- **Model buttons**: Removes checkmark (✓) from current model display — `currentModel` is no longer used in `buildModelsKeyboard` but remains in the `ModelsKeyboardParams` type and is still passed at the call site (dead parameter).
- **Tests**: Comprehensive coverage for new features including tap intercept + ackText, sentinel detection, all `buttonStateMode` values, dedupe, and legacy mode regression.
<h3>Confidence Score: 3/5</h3>
- Mostly safe to merge but has a UX-level logic issue with tap intercept acking disabled buttons, and a config behavior mismatch between mark-clicked and disable-clicked modes.
- The PR is well-structured with good test coverage and clear config validation. Score of 3 reflects two issues: (1) the tap intercept middleware doesn't filter out sentinel/disabled callbacks, causing ackText to show on disabled buttons when both features are combined, and (2) the mark-clicked vs disable-clicked config options are documented as distinct behaviors but implemented identically, which could confuse users.
- `src/telegram/bot.ts` (tap intercept middleware doesn't filter sentinel callbacks), `src/telegram/bot-handlers.ts` (buildEditedCallbackKeyboard ignores mode parameter)
<sub>Last reviewed commit: f980f11</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#14549: feat(telegram): add support for URL-type inline buttons
by kokosthief · 2026-02-12
77.8%
#9265: Feature: Telegram Inline Button Support for Exec Approvals
by vishaltandale00 · 2026-02-05
77.6%
#19213: Telegram: preserve DM topic thread in direct replies
by Kemalau · 2026-02-17
76.4%
#22095: feat: add Telegram inline buttons to exec approval requests
by AIflow-Labs · 2026-02-20
75.8%
#16102: Fix: Telegram Inline Button Support for Exec Approvals (builds on #...
by RoguePhoenix117 · 2026-02-14
75.8%
#21346: [AI-assisted] Telegram: add reaction state machine with fallback an...
by Archie818 · 2026-02-19
75.5%
#15864: feat: add deliverOnlyToolMessages config for clean messaging channe...
by gandalf-the-engineer · 2026-02-14
75.5%
#17953: fix(telegram): prevent silent message loss and duplicate messages i...
by zuyan9 · 2026-02-16
75.4%
#19829: fix(telegram): fall back to default scope for array capabilities wi...
by NewdlDewdl · 2026-02-18
75.2%
#22434: feat(telegram): support sending original quality images
by godenjan · 2026-02-21
75.2%