openclaw - ✅(Solved) Fix Telegram: plugin approval callback_query deadlocked by Grammy sequentializer [1 pull requests, 1 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#64977Fetched 2026-04-12 13:26:08
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
closed ×1cross-referenced ×1

Root Cause

getTelegramSequentialKey() in extensions/telegram/src/sequential-key.ts returns the same key for:

  • The user message that starts the agent turn (telegram:<chatId>)
  • The approval callback_query from clicking the button (telegram:<chatId>)

The @grammyjs/runner sequentializer processes updates with the same key sequentially. When the agent turn is blocked on plugin.approval.waitDecision, the callback_query is queued behind it. The callback can't run because the lane is held; the lane can't release because it's waiting for the callback.

Abort requests already have a fix for this exact patterngetTelegramSequentialKey returns telegram:<chatId>:control for abort messages (line 38-46), giving them a separate lane. Approval callbacks need the same treatment.

Fix Action

Fix / Workaround

This is a sequentializer deadlock in the Grammy runner. It was not fixed by #57838 (channel approval refactor). That PR fixed handler subscription and routing — the callback resolution code is correct — but the callback_query never reaches the handler because it's queued behind the blocked agent turn.

  • #57339 — original bug report (closed as resolved by #57838, but sequentializer issue persists)
  • #57340 — our PR with the sequentializer fix (commit 3) that was not merged

PR fix notes

PR #64979: fix(telegram): bypass sequentializer for approval callback_queries

Description (problem / solution / changelog)

Summary

Fixes #64977 — plugin approval buttons on Telegram deadlock because the Grammy sequentializer queues the approval callback_query behind the blocked agent turn.

One file changed, one pattern followed. Same fix as abort requests (telegram:<chatId>:control) and btw requests (telegram:<chatId>:btw).

Root Cause

getTelegramSequentialKey() returns the same key (telegram:<chatId>) for both:

  • The user message that starts the agent turn (which blocks on plugin.approval.waitDecision)
  • The approval callback_query from clicking the inline button

The @grammyjs/runner sequentializer processes same-key updates sequentially. Deadlock: the callback can't run because the lane is held, and the lane can't release because it's waiting for the callback.

Fix

Detect approval callback_query updates via parseExecApprovalCommandText(data) and return a separate sequential key: telegram:<chatId>:approval.

This is the same pattern already used for:

  • Abort requests → telegram:<chatId>:control (line 40-44)
  • Btw requests → telegram:<chatId>:btw (line 46-54)

Before / After (gateway logs, same machine, same guardrail)

Before — approval times out after 300s, resolve arrives too late:

plugin.approval.waitDecision 299961ms  ✓  (timed out)
plugin.approval.resolve 0ms  ✗  errorCode=INVALID_REQUEST errorMessage=unknown or expired approval id
plugin.approval.resolve 1ms  ✗  errorCode=INVALID_REQUEST errorMessage=unknown or expired approval id

After — approval resolves in 3s:

plugin.approval.waitDecision 3147ms  ✓  (resolved by button click)
plugin.approval.resolve 564ms  ✓

Audit log confirms: decision: "approval_required" at 19:22:32, resolution: "allow-once", userResponse: "approved" at 19:22:35.

Changes

FileChange
extensions/telegram/src/sequential-key.tsAdd data?: string to callback_query context type. Detect approval callbacks via parseExecApprovalCommandText and return telegram:<chatId>:approval key.
extensions/telegram/src/sequential-key.test.ts4 new tests: plugin approval, exec approval, allow-always variant, non-approval callback (no false positives). All 23 tests pass.

Test plan

  • pnpm test -- extensions/telegram/src/sequential-key.test.ts — 23/23 pass (19 existing + 4 new)
  • Plugin approval callback → telegram:<chatId>:approval key
  • Exec approval callback → telegram:<chatId>:approval key
  • always alias callback → telegram:<chatId>:approval key
  • Non-approval callback → normal telegram:<chatId> key (no false positives)
  • Live e2e: ClawLens guardrail require_approval → Telegram button click → resolves in 3s (was 300s timeout)

Environment

  • OpenClaw: 2026.4.10 (44e5b62) base, tested on 2026.4.11-beta.1 (faf9fe7)
  • Plugin: ClawLens (returns requireApproval from before_tool_call guardrails)
  • Channel: Telegram DM

🤖 Generated with Claude Code

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/telegram/src/sequential-key.test.ts (modified, +44/-0)
  • extensions/telegram/src/sequential-key.ts (modified, +9/-1)

Code Example

2026-04-11T11:55:05.276 [ws] ⇄ res ✓ plugin.approval.waitDecision 299961ms conn=6a5f...
2026-04-11T11:55:12.827 [ws] ⇄ res ✗ plugin.approval.resolve 0ms errorCode=INVALID_REQUEST
    errorMessage=unknown or expired approval id conn=a4a6...
2026-04-11T11:55:13.819 [ws] ⇄ res ✗ plugin.approval.resolve 1ms errorCode=INVALID_REQUEST
    errorMessage=unknown or expired approval id conn=c197...

---

{"timestamp":"2026-04-11T18:50:05.191Z","decision":"approval_required",
 "params":{"guardrailId":"gr_e75ea794f9b1","guardrailAction":"require_approval",
 "identityKey":"https://apnews.com"}}

{"timestamp":"2026-04-11T18:55:05.278Z","decision":"block",
 "userResponse":"denied","params":{"guardrailId":"gr_e75ea794f9b1","resolution":"timeout"}}

---

// After the abort request check (line ~46), before the general path:
const callbackData = ctx.update?.callback_query?.data;
if (callbackData && parseExecApprovalCommandText(callbackData) !== null) {
  if (typeof chatId === "number") {
    return `telegram:${chatId}:approval`;
  }
  return "telegram:approval";
}
RAW_BUFFERClick to expand / collapse

Bug Description

Plugin approval buttons on Telegram appear correctly but clicking them has no effect. The approval times out after 300s. The button clicks are processed after the timeout, arriving at the gateway too late.

This is a sequentializer deadlock in the Grammy runner. It was not fixed by #57838 (channel approval refactor). That PR fixed handler subscription and routing — the callback resolution code is correct — but the callback_query never reaches the handler because it's queued behind the blocked agent turn.

Root Cause

getTelegramSequentialKey() in extensions/telegram/src/sequential-key.ts returns the same key for:

  • The user message that starts the agent turn (telegram:<chatId>)
  • The approval callback_query from clicking the button (telegram:<chatId>)

The @grammyjs/runner sequentializer processes updates with the same key sequentially. When the agent turn is blocked on plugin.approval.waitDecision, the callback_query is queued behind it. The callback can't run because the lane is held; the lane can't release because it's waiting for the callback.

Abort requests already have a fix for this exact patterngetTelegramSequentialKey returns telegram:<chatId>:control for abort messages (line 38-46), giving them a separate lane. Approval callbacks need the same treatment.

Evidence (OpenClaw 2026.4.10, post #57838)

Gateway logs — approval times out, then resolve attempts arrive late

2026-04-11T11:55:05.276 [ws] ⇄ res ✓ plugin.approval.waitDecision 299961ms conn=6a5f...
2026-04-11T11:55:12.827 [ws] ⇄ res ✗ plugin.approval.resolve 0ms errorCode=INVALID_REQUEST
    errorMessage=unknown or expired approval id conn=a4a6...
2026-04-11T11:55:13.819 [ws] ⇄ res ✗ plugin.approval.resolve 1ms errorCode=INVALID_REQUEST
    errorMessage=unknown or expired approval id conn=c197...

waitDecision blocks for exactly 300s (the plugin's timeoutMs), then times out. The resolve calls arrive 7-8 seconds after expiry — the button clicks were queued behind the blocked turn in the sequentializer and only processed after the timeout released the lane. By then the approval record is gone.

Audit log — confirms timeout

{"timestamp":"2026-04-11T18:50:05.191Z","decision":"approval_required",
 "params":{"guardrailId":"gr_e75ea794f9b1","guardrailAction":"require_approval",
 "identityKey":"https://apnews.com"}}

{"timestamp":"2026-04-11T18:55:05.278Z","decision":"block",
 "userResponse":"denied","params":{"guardrailId":"gr_e75ea794f9b1","resolution":"timeout"}}

Approval requested at 18:50:05, timed out at 18:55:05 — exactly 5 minutes. Button clicks never reached the handler during that window.

Proposed Fix

In extensions/telegram/src/sequential-key.ts, detect approval callback_queries and return a separate sequential key, same pattern as abort requests:

// After the abort request check (line ~46), before the general path:
const callbackData = ctx.update?.callback_query?.data;
if (callbackData && parseExecApprovalCommandText(callbackData) !== null) {
  if (typeof chatId === "number") {
    return `telegram:${chatId}:approval`;
  }
  return "telegram:approval";
}

This gives approval callbacks their own lane so they bypass the blocked agent turn, exactly like telegram:<chatId>:control does for abort requests.

This was commit 3 in #57340 (which addressed the same bug). The handler and routing fixes from that PR were superseded by #57838, but the sequentializer bypass was not included in the upstream refactor.

Steps to Reproduce

  1. Install a plugin that returns requireApproval from before_tool_call
  2. Trigger a tool call that hits the approval guardrail via Telegram
  3. Approval prompt appears with buttons
  4. Click "Allow Once"
  5. Nothing happens — agent hangs for 300s then times out

Environment

  • OpenClaw: 2026.4.10 (44e5b62) — includes #57838
  • Channel: Telegram (Bot API, Grammy + @grammyjs/runner)
  • Plugin: ClawLens (returns requireApproval from before_tool_call guardrails)

Related

  • #57339 — original bug report (closed as resolved by #57838, but sequentializer issue persists)
  • #57340 — our PR with the sequentializer fix (commit 3) that was not merged

extent analysis

TL;DR

Modify the getTelegramSequentialKey function to return a separate key for approval callback queries, allowing them to bypass the blocked agent turn.

Guidance

  • Identify the getTelegramSequentialKey function in extensions/telegram/src/sequential-key.ts and modify it to detect approval callback queries.
  • Add a conditional statement to return a separate sequential key, such as telegram:${chatId}:approval, for approval callback queries.
  • Verify that the modified function returns the correct sequential key for approval callback queries.
  • Test the changes by reproducing the steps to reproduce the bug and verifying that the approval prompt responds correctly.

Example

const callbackData = ctx.update?.callback_query?.data;
if (callbackData && parseExecApprovalCommandText(callbackData) !== null) {
  if (typeof chatId === "number") {
    return `telegram:${chatId}:approval`;
  }
  return "telegram:approval";
}

Notes

The proposed fix is based on the pattern used for abort requests, which return a separate sequential key telegram:<chatId>:control. This change should allow approval callbacks to bypass the blocked agent turn and respond correctly.

Recommendation

Apply the workaround by modifying the getTelegramSequentialKey function to return a separate key for approval callback queries. This change should resolve the sequentializer deadlock issue and allow the approval prompt to respond correctly.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING