Skip to content

feat(telegram): handle plugin approval events in TelegramExecApprovalHandler#57340

Closed
nk3750 wants to merge 6 commits into
openclaw:mainfrom
nk3750:fix/telegram-plugin-approval-handler
Closed

feat(telegram): handle plugin approval events in TelegramExecApprovalHandler#57340
nk3750 wants to merge 6 commits into
openclaw:mainfrom
nk3750:fix/telegram-plugin-approval-handler

Conversation

@nk3750

@nk3750 nk3750 commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #57339 — adds full plugin approval support to Telegram, bringing it to parity with Discord. Tested end-to-end: approval prompt appears, button click resolves immediately, tool proceeds.

Commit 1: Handler subscription (exec-approvals-handler.ts)

TelegramExecApprovalHandler now handles plugin.approval.requested and plugin.approval.resolved gateway events.

  • Subscribe to plugin.approval.requested and plugin.approval.resolved in handleGatewayEvent
  • Accept ExecApprovalRequest | PluginApprovalRequest in handleRequested, shouldHandle, and helpers
  • Use buildPluginApprovalPendingReplyPayload for plugin approval messages
  • Accept ExecApprovalResolved | PluginApprovalResolved in handleResolved

Commit 2: Direct callback resolution (bot-handlers.runtime.ts)

Resolve approval callbacks directly via a lazily-initialized gateway client instead of routing through processMessage (which deadlocks because the session pipeline is blocked waiting for the approval decision).

  • Parse approval ID and decision from callback data
  • Call plugin.approval.resolve or exec.approval.resolve via gateway client
  • Return early — never falls through to processMessage

Commit 3: Bypass sequentializer (sequential-key.ts)

The @grammyjs/runner sequentializer processes all updates per chat sequentially. When the agent turn is waiting for approval, the callback_query is queued behind it — deadlock at the runner level.

Approval callback_queries now get a separate sequential key (telegram:{chatId}:approval), allowing them to be processed in parallel with the blocked agent turn. Same pattern as abort requests (telegram:{chatId}:control).

Commit 4: Dedup + param fix (exec-approvals-handler.ts, bot-handlers.runtime.ts)

  • For plugin approvals, skip handler delivery when the target matches the source chat (the inline session prompt already covers it). Handles telegram: prefix normalization.
  • Remove invalid resolvedBy property from plugin.approval.resolve call (gateway only accepts { id, decision }).

Commit 5: Review feedback (bot-handlers.media.ts, bot-handlers.runtime.ts, exec-approvals-handler.ts)

  • APPROVE_CALLBACK_DATA_RE now matches always alias (used by "Allow Always" button to fit Telegram's 64-byte callback_data limit)
  • parseApprovalCallbackDecision correctly maps alwaysallow-always (was allow-once)
  • Type cast replaced with minimal shim in resolveRequestSessionTarget

Commit 6: Tests (exec-approvals-handler.test.ts)

7 new plugin approval tests, all 3 existing exec approval tests remain green (10/10).

Test plan

Unit tests (10/10 passing)

  • Exec: sends approval to originating topic when target=channel
  • Exec: falls back to approver DMs when channel routing unavailable
  • Exec: clears buttons on resolved
  • Plugin: sends prompt with buttons to approver DM (different chat from source)
  • Plugin: dedup skips delivery when source matches target (same DM)
  • Plugin: delivers to approver DM when source is a different chat
  • Plugin: exec approvals NOT deduped (dedup only applies to plugin)
  • Plugin: clears buttons on plugin.approval.resolved
  • Plugin: plugin.approval.requested routed via handleGatewayEvent
  • Plugin: plugin.approval.resolved routed via handleGatewayEvent

Manual e2e (Telegram DM with ClawClip plugin)

  • Handler receives plugin.approval.requested events
  • Handler sends approval prompt with buttons to Telegram
  • Sequentializer allows callback_query through during active approval wait
  • Clicking "Allow Once" resolves the approval and tool proceeds
  • Single approval prompt (no duplicates in DM conversations)
  • No crash or gateway rejection on resolve call
  • Audit log correctly records the decision
  • All changes typecheck clean (0 new errors)
  • Verify group chat → DM forwarding (handler sends to approver DM when source is a group)

@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram size: S labels Mar 29, 2026
@greptile-apps

greptile-apps Bot commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends TelegramExecApprovalHandler to handle plugin.approval.requested and plugin.approval.resolved gateway events, mirroring the existing behavior in the Discord handler. The implementation is clean: union types widen the relevant method signatures, buildPluginApprovalPendingReplyPayload is used for plugin requests, and event dispatch is handled with matching else if branches in handleGatewayEvent.

Key changes:

  • handleGatewayEvent now dispatches plugin.approval.requestedhandleRequested and plugin.approval.resolvedhandleResolved
  • handleRequested uses request.id.startsWith(\"plugin:\") (a well-established codebase convention, used in at least 4 other places) to select between buildPluginApprovalPendingReplyPayload and buildExecApprovalPendingReplyPayload
  • resolveExecApprovalSessionTarget is called with an as ExecApprovalRequest cast for PluginApprovalRequest; this is runtime-safe because the function only reads sessionKey and agentId from the request, both present in PluginApprovalRequestPayload, but the cast is worth noting
  • expiresAtMs, agentId, sessionKey, and all turnSource* fields are present on PluginApprovalRequest / PluginApprovalRequestPayload, so all downstream logic works correctly without special-casing

Confidence Score: 5/5

Safe to merge — no functional defects found; only a minor type-cast style note.

The implementation is functionally correct. All fields accessed on PluginApprovalRequest and PluginApprovalRequestPayload (expiresAtMs, sessionKey, agentId, turnSource*) are confirmed to exist. The startsWith("plugin:") discrimination is an established codebase convention. The resolveExecApprovalSessionTarget cast is safe at runtime. The only remaining note is a P2 style suggestion about the type assertion, which does not affect correctness.

No files require special attention.

Important Files Changed

Filename Overview
extensions/telegram/src/exec-approvals-handler.ts Extends handler to subscribe to plugin approval gateway events, widening method signatures to accept union types and dispatching to the correct payload builder. Functionally correct; one minor type assertion worth noting.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/telegram/src/exec-approvals-handler.ts
Line: 128

Comment:
**Type assertion bypasses TS safety on the request parameter**

`resolveExecApprovalSessionTarget` is typed to accept `ExecApprovalRequest`, but receives a `PluginApprovalRequest` via a cast. This is safe today because the function only reads `.request.sessionKey` and `.request.agentId` — fields present in both payload types — but the cast will silently suppress any future type errors if the function's internal field access ever broadens.

Consider expanding `resolveExecApprovalSessionTarget` upstream to accept a shared interface, or extracting a minimal compatible shim object so TypeScript continues to guard this call site.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(telegram): handle plugin approval e..." | Re-trigger Greptile

Comment thread extensions/telegram/src/exec-approvals-handler.ts Outdated
@vincentkoc

Copy link
Copy Markdown
Member

Thank you for putting this up.

This looks directionally correct and matches the Discord-side behavior, but I am not merging it blind without the Telegram regression coverage.

What still needs to happen before landing:

  • add focused tests for plugin.approval.requested and plugin.approval.resolved through the Telegram approvals handler
  • keep exec approvals green in the same test surface

The remaining review note about the cast is not the blocker here; the missing test coverage is.

Issue thread for the bug: #57339

@vincentkoc vincentkoc self-assigned this Mar 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/openclaw/openclaw/blob/e9061c7f2220ef7285b0e8dc0395e58b39a9c4d8/extensions/telegram/src/bot-handlers.runtime.ts#L1367
P1 Badge Route always approval callbacks through direct resolver

The new direct-resolution flow only runs inside if (isApprovalCallback), but that predicate comes from APPROVE_CALLBACK_DATA_RE, which matches allow-once|allow-always|deny while Telegram approval buttons emit /approve <id> always for the “Allow Always” action (extensions/telegram/src/approval-buttons.ts). As a result, “Allow Always” callbacks skip resolveApprovalDirect and fall back to processMessage; for plugin approvals this re-enters the session pipeline path that this change explicitly says deadlocks, so the callback can hang instead of resolving.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 381167dce1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/telegram/src/bot-handlers.runtime.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 078dabebdc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/telegram/src/bot-handlers.runtime.ts
EronFan pushed a commit to EronFan/openclaw-fork that referenced this pull request Mar 30, 2026
Approval callback_queries must bypass the per-chat sequential queue.
The agent turn waiting for approval holds the chat lock; queuing
the callback behind it causes a deadlock.

Changes:
- sequential-key.ts: approval callback_queries now route to
  a separate ':approval' key, running in parallel with the
  main agent pipeline (same pattern as ':control')
- bot-handlers.media.ts: accept 'always' as a valid decision
  value alongside 'allow-always' in APPROVE_CALLBACK_DATA_RE

This is part 2 of the Telegram plugin approval fix (Issue 1
and the sequentializer bypass). Part 1 (handler plugin.approval.*
support) is addressed separately in PR openclaw#57340.

Fixes openclaw#57339 (partial).
@nk3750

nk3750 commented Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for putting this up.

This looks directionally correct and matches the Discord-side behavior, but I am not merging it blind without the Telegram regression coverage.

What still needs to happen before landing:

  • add focused tests for plugin.approval.requested and plugin.approval.resolved through the Telegram approvals handler
  • keep exec approvals green in the same test surface

The remaining review note about the cast is not the blocker here; the missing test coverage is.

Issue thread for the bug: #57339

Added in 078dabe — 7 new plugin approval tests, all 3 existing exec approval tests remain green (10/10 passing).

Tests cover:

  • plugin.approval.requested → handler sends prompt with buttons to approver DM
  • plugin.approval.resolved → handler clears buttons
  • Both events routed correctly via handleGatewayEvent
  • Plugin approval dedup: skips delivery when source matches target (same DM)
  • Exec approvals NOT deduped (dedup only applies to plugin approvals)
  • Delivery to different approver DM when source chat differs
 Test Files  1 passed (1)
      Tests  10 passed (10)

nk3750 and others added 3 commits March 30, 2026 11:34
…Handler

TelegramExecApprovalHandler only listened for exec.approval.requested
and exec.approval.resolved gateway events, silently ignoring plugin
approval events. This caused plugin approval buttons on Telegram to
deadlock — the approval prompt rendered inline in the session, and
clicking the button routed through the same blocked session pipeline.

The Discord handler (DiscordExecApprovalHandler) already supports both
event types. This commit brings Telegram to parity by:

- Subscribing to plugin.approval.requested and plugin.approval.resolved
- Accepting ExecApprovalRequest | PluginApprovalRequest in handleRequested
- Using buildPluginApprovalPendingReplyPayload for plugin approval messages
- Accepting ExecApprovalResolved | PluginApprovalResolved in handleResolved

The existing button builder (buildTelegramExecApprovalButtons) and
callback_data parser already handle plugin:<uuid> approval IDs correctly.
…y client

When a user clicks an approval button on Telegram, the callback_query is
processed by the Grammy bot's update pipeline. However, the Grammy runner
sequentializes all updates per chat — and the agent turn that triggered
the approval is still "processing" (waiting for the approval decision).
This means the callback_query update is queued behind the blocked agent
turn, creating a deadlock at the runner level.

This commit adds direct approval resolution in the callback handler:
when an approval callback is detected, it resolves the approval via a
lazily-initialized gateway client (calling plugin.approval.resolve or
exec.approval.resolve), bypassing processMessage entirely.

NOTE: This alone does not fully fix the deadlock — the Grammy runner
sequentializer blocks the callback_query handler from executing at all.
A pre-sequentializer middleware is needed to intercept approval
callback_queries before they enter the per-chat sequential queue.
This is noted as a TODO for follow-up.
@vincentkoc vincentkoc force-pushed the fix/telegram-plugin-approval-handler branch from 078dabe to c75b5a2 Compare March 30, 2026 02:35
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Mar 30, 2026
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime labels Mar 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 896fff44c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/telegram/src/bot-handlers.runtime.ts Outdated
@vincentkoc vincentkoc force-pushed the fix/telegram-plugin-approval-handler branch from b993dd3 to 59d6886 Compare March 30, 2026 04:02
@openclaw-barnacle openclaw-barnacle Bot removed the docs Improvements or additions to documentation label Mar 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 768ea99ad3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1457 to +1461
// Plugin approvals block the session pipeline while waiting for a decision, so routing
// through processMessage would deadlock. Instead, call the gateway resolve method directly.
const parsed = parseApprovalCallbackDecision(data);
if (parsed) {
const ok = await resolveApprovalDirect(runtimeCfg, parsed.id, parsed.decision, senderId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route approval callbacks to a non-chat sequential key

This new direct-resolve path still depends on the callback handler running, but callback updates are still sequentialized on the same telegram:<chatId>[:topic] key as the in-flight turn waiting for approval (I checked extensions/telegram/src/sequential-key.ts). In same-chat approval flows (for example DM or channel-target approvals), the callback query stays queued behind the blocked turn, so resolveApprovalDirect(...) is never reached and the approval can deadlock until expiry.

Useful? React with 👍 / 👎.

Comment on lines +539 to +543
gatewayRuntimeHoisted.createOperatorApprovalsGatewayClient.mockResolvedValue({
connect: gatewayRuntimeHoisted.connectSpy,
request: gatewayRuntimeHoisted.requestSpy,
start: gatewayRuntimeHoisted.startSpy,
stop: gatewayRuntimeHoisted.stopSpy,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve onHello handshake in approval gateway test mock

The beforeEach override replaces the hoisted createOperatorApprovalsGatewayClient mock with a plain mockResolvedValue(...), which drops the start() behavior that triggers onHelloOk. Since ensureApprovalGatewayClient now waits on that hello signal before resolving approvals, callback-approval tests can hang indefinitely on first use instead of reaching assertions.

Useful? React with 👍 / 👎.

@steipete

Copy link
Copy Markdown
Contributor

Closing this as implemented after Codex review.

Current main already implements Telegram plugin approval handling through the newer native approval runtime and shared callback resolver, so PR #57340 is superseded.

What I checked:

  • Telegram approval runtime now handles plugin approvals: telegramApprovalNativeRuntime is registered for both exec and plugin approval events and builds plugin approval payloads; the Telegram approval capability loads that runtime for native delivery. (extensions/telegram/src/approval-handler.runtime.ts:94, 5dfc1b90e176)
  • Telegram approval capability is wired into the plugin surface: The Telegram approval native capability advertises eventKinds: ["exec", "plugin"] and loads approval-handler.runtime.ts, so plugin approval events are handled on current main even though the old TelegramExecApprovalHandler file no longer exists. (extensions/telegram/src/approval-native.ts:120, 5dfc1b90e176)
  • Plugin approval callbacks resolve directly from Telegram: The callback handler parses /approve payloads, recognizes plugin: approval ids, authorizes Telegram approvers, and resolves them through resolveTelegramExecApproval with plugin fallback enabled. (extensions/telegram/src/bot-handlers.runtime.ts:1432, 5dfc1b90e176)
  • Deadlock avoidance is already present: Approval callback queries are assigned a separate sequentializer lane (telegram:{chatId}:approval), which is the main concurrency fix described in the PR body. (extensions/telegram/src/sequential-key.ts:90, 5dfc1b90e176)
  • Current tests cover the landed behavior: Mainline tests cover plugin callback resolution from Telegram and plugin approval event subscription/finalization in the shared approval runtime. (extensions/telegram/src/bot.test.ts:556, 5dfc1b90e176)
  • Shipped in changelog: The changelog records the Telegram approval callback deadlock fix under release 2026.4.12, indicating this behavior was already shipped. (CHANGELOG.md:748, 5dfc1b90e176)

So I’m closing this as already implemented rather than keeping a duplicate issue open.

Review notes: reviewed against 5dfc1b90e176; fix evidence: release v2026.4.12, commit 5dfc1b90e176.

@steipete steipete closed this Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui channel: telegram Channel integration: telegram gateway Gateway runtime size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram: plugin approval buttons don't work (handler missing plugin.approval.* events)

3 participants