fix(telegram): avoid fallback after message tool send (#78726) (thank… · openclaw/openclaw@447182a
neeravmakwan
·
2026-05-07
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
6 | 6 | |
7 | 7 | ### Changes |
8 | 8 | |
| 9 | +- Telegram: treat successful same-chat `message` tool outbound sends during an inbound telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback (#78685). Thanks @neeravmakwana. |
9 | 10 | - Docs/iMessage: deprecate BlueBubbles for new OpenClaw setups, document the upstream server-release rationale, and point new iMessage deployments toward the native `imsg` path while keeping BlueBubbles as a supported legacy fallback. |
10 | 11 | - Discord/streaming: default Discord replies to progress draft previews so tool/work activity appears in one edited Discord message unless `channels.discord.streaming.mode` is set to `off`. |
11 | 12 | - Plugins/install: add `npm-pack:<path.tgz>` installs so local npm pack artifacts run through the same managed npm-root install, lockfile verification, dependency scan, and install-record path as registry npm plugins. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2,6 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
2 | 2 | import { captureEnv } from "openclaw/plugin-sdk/test-env"; |
3 | 3 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
4 | 4 | import { handleTelegramAction, telegramActionRuntime } from "./action-runtime.js"; |
| 5 | +import { beginTelegramInboundTurnDeliveryCorrelation } from "./inbound-turn-delivery.js"; |
5 | 6 | |
6 | 7 | const originalTelegramActionRuntime = { ...telegramActionRuntime }; |
7 | 8 | const reactMessageTelegram = vi.fn(async () => ({ ok: true })); |
@@ -357,6 +358,27 @@ describe("handleTelegramAction", () => {
|
357 | 358 | }); |
358 | 359 | }); |
359 | 360 | |
| 361 | +it("marks the matching inbound turn delivered after a successful send", async () => { |
| 362 | +let count = 0; |
| 363 | +const end = beginTelegramInboundTurnDeliveryCorrelation("telegram-session", { |
| 364 | +outboundTo: "@testchannel", |
| 365 | +markInboundTurnDelivered: () => { |
| 366 | +count += 1; |
| 367 | +}, |
| 368 | +}); |
| 369 | +await handleTelegramAction( |
| 370 | +{ |
| 371 | +action: "sendMessage", |
| 372 | +to: "@testchannel", |
| 373 | +content: "Hello, Telegram!", |
| 374 | +}, |
| 375 | +telegramConfig(), |
| 376 | +{ sessionKey: "telegram-session" }, |
| 377 | +); |
| 378 | +expect(count).toBe(1); |
| 379 | +end(); |
| 380 | +}); |
| 381 | + |
360 | 382 | it("accepts shared send action aliases", async () => { |
361 | 383 | await handleTelegramAction( |
362 | 384 | { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -19,6 +19,7 @@ import {
|
19 | 19 | import type { MessagePresentation } from "openclaw/plugin-sdk/interactive-runtime"; |
20 | 20 | import { createTelegramActionGate, resolveTelegramPollActionGateState } from "./accounts.js"; |
21 | 21 | import { resolveTelegramInlineButtons } from "./button-types.js"; |
| 22 | +import { notifyTelegramInboundTurnOutboundSuccess } from "./inbound-turn-delivery.js"; |
22 | 23 | import { |
23 | 24 | resolveTelegramInlineButtonsScope, |
24 | 25 | resolveTelegramTargetChatType, |
@@ -228,6 +229,7 @@ export async function handleTelegramAction(
|
228 | 229 | options?: { |
229 | 230 | mediaLocalRoots?: readonly string[]; |
230 | 231 | mediaReadFile?: (filePath: string) => Promise<Buffer>; |
| 232 | +sessionKey?: string | null; |
231 | 233 | }, |
232 | 234 | ): Promise<AgentToolResult<unknown>> { |
233 | 235 | const { action, accountId } = { |
@@ -394,6 +396,11 @@ export async function handleTelegramAction(
|
394 | 396 | readBooleanParam(params, "asDocument") ?? |
395 | 397 | false, |
396 | 398 | }); |
| 399 | +notifyTelegramInboundTurnOutboundSuccess({ |
| 400 | +sessionKey: options?.sessionKey ?? undefined, |
| 401 | + to, |
| 402 | + accountId, |
| 403 | +}); |
397 | 404 | await maybePinTelegramActionSend({ |
398 | 405 | args: params, |
399 | 406 | cfg, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -80,6 +80,7 @@ import {
|
80 | 80 | } from "./error-policy.js"; |
81 | 81 | import { shouldSuppressLocalTelegramExecApprovalPrompt } from "./exec-approvals.js"; |
82 | 82 | import { markdownToTelegramChunks, renderTelegramHtmlText } from "./format.js"; |
| 83 | +import { beginTelegramInboundTurnDeliveryCorrelation } from "./inbound-turn-delivery.js"; |
83 | 84 | import { |
84 | 85 | createLaneDeliveryStateTracker, |
85 | 86 | createLaneTextDeliverer, |
@@ -684,6 +685,14 @@ export const dispatchTelegramMessage = async ({
|
684 | 685 | ? ctxPayload.ReplyToQuoteEntities |
685 | 686 | : undefined; |
686 | 687 | const deliveryState = createLaneDeliveryStateTracker(); |
| 688 | +const endTelegramInboundTurnDeliveryCorrelation = beginTelegramInboundTurnDeliveryCorrelation( |
| 689 | +ctxPayload.SessionKey, |
| 690 | +{ |
| 691 | +outboundTo: String(chatId), |
| 692 | +outboundAccountId: route.accountId, |
| 693 | +markInboundTurnDelivered: () => deliveryState.markDelivered(), |
| 694 | +}, |
| 695 | +); |
687 | 696 | const clearGroupHistory = () => { |
688 | 697 | if (isGroup && historyKey) { |
689 | 698 | clearHistoryEntriesIfEnabled({ |
@@ -1354,6 +1363,7 @@ export const dispatchTelegramMessage = async ({
|
1354 | 1363 | } finally { |
1355 | 1364 | dispatchWasSuperseded = isDispatchSuperseded(); |
1356 | 1365 | releaseReplyFence(); |
| 1366 | +endTelegramInboundTurnDeliveryCorrelation(); |
1357 | 1367 | } |
1358 | 1368 | if (dispatchWasSuperseded) { |
1359 | 1369 | if (statusReactionController) { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -43,6 +43,7 @@ describe("telegramMessageActions", () => {
|
43 | 43 | cfg: {} as never, |
44 | 44 | accountId: "default", |
45 | 45 | mediaLocalRoots: [], |
| 46 | +sessionKey: "telegram-session", |
46 | 47 | } as never); |
47 | 48 | |
48 | 49 | expect(handleTelegramActionMock).toHaveBeenCalledWith( |
@@ -62,6 +63,7 @@ describe("telegramMessageActions", () => {
|
62 | 63 | expect.anything(), |
63 | 64 | expect.objectContaining({ |
64 | 65 | mediaLocalRoots: [], |
| 66 | +sessionKey: "telegram-session", |
65 | 67 | }), |
66 | 68 | ); |
67 | 69 | }); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -177,7 +177,15 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
177 | 177 | extractToolSend: ({ args }) => { |
178 | 178 | return extractToolSend(args, "sendMessage"); |
179 | 179 | }, |
180 | | -handleAction: async ({ action, params, cfg, accountId, mediaLocalRoots, toolContext }) => { |
| 180 | +handleAction: async ({ |
| 181 | + action, |
| 182 | + params, |
| 183 | + cfg, |
| 184 | + accountId, |
| 185 | + mediaLocalRoots, |
| 186 | + sessionKey, |
| 187 | + toolContext, |
| 188 | +}) => { |
181 | 189 | const telegramAction = resolveTelegramMessageActionName(action); |
182 | 190 | if (!telegramAction) { |
183 | 191 | throw new Error(`Unsupported Telegram action: ${action}`); |
@@ -194,7 +202,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
194 | 202 | : {}), |
195 | 203 | }, |
196 | 204 | cfg, |
197 | | -{ mediaLocalRoots }, |
| 205 | +{ mediaLocalRoots, sessionKey }, |
198 | 206 | ); |
199 | 207 | }, |
200 | 208 | }; |
| Original file line number | Diff line number | Diff line change |
|---|
|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { |
| 3 | +beginTelegramInboundTurnDeliveryCorrelation, |
| 4 | +notifyTelegramInboundTurnOutboundSuccess, |
| 5 | +} from "./inbound-turn-delivery.js"; |
| 6 | + |
| 7 | +describe("telegram inbound turn delivery", () => { |
| 8 | +it("marks delivered once for a matching outbound send then clears correlation", () => { |
| 9 | +let count = 0; |
| 10 | +const end = beginTelegramInboundTurnDeliveryCorrelation("sess:z", { |
| 11 | +outboundTo: "999", |
| 12 | +outboundAccountId: "a1", |
| 13 | +markInboundTurnDelivered: () => { |
| 14 | +count += 1; |
| 15 | +}, |
| 16 | +}); |
| 17 | +notifyTelegramInboundTurnOutboundSuccess({ |
| 18 | +sessionKey: "sess:z", |
| 19 | +to: "999", |
| 20 | +accountId: "a1", |
| 21 | +}); |
| 22 | +expect(count).toBe(1); |
| 23 | +end(); |
| 24 | +notifyTelegramInboundTurnOutboundSuccess({ |
| 25 | +sessionKey: "sess:z", |
| 26 | +to: "999", |
| 27 | +accountId: "a1", |
| 28 | +}); |
| 29 | +expect(count).toBe(1); |
| 30 | +}); |
| 31 | + |
| 32 | +it("ignores outbound sends to another destination", () => { |
| 33 | +let count = 0; |
| 34 | +const end = beginTelegramInboundTurnDeliveryCorrelation("sess:y", { |
| 35 | +outboundTo: "1", |
| 36 | +markInboundTurnDelivered: () => { |
| 37 | +count += 1; |
| 38 | +}, |
| 39 | +}); |
| 40 | +notifyTelegramInboundTurnOutboundSuccess({ |
| 41 | +sessionKey: "sess:y", |
| 42 | +to: "2", |
| 43 | +accountId: undefined, |
| 44 | +}); |
| 45 | +expect(count).toBe(0); |
| 46 | +end(); |
| 47 | +}); |
| 48 | +}); |
| Original file line number | Diff line number | Diff line change |
|---|
|
| 1 | +export type TelegramInboundTurnDeliveryEnd = () => void; |
| 2 | + |
| 3 | +type ActiveTurn = { |
| 4 | +outboundTo: string; |
| 5 | +outboundAccountId?: string; |
| 6 | +markInboundTurnDelivered: () => void; |
| 7 | +}; |
| 8 | + |
| 9 | +const registry = new Map<string, ActiveTurn>(); |
| 10 | + |
| 11 | +export function beginTelegramInboundTurnDeliveryCorrelation( |
| 12 | +sessionKey: string | undefined, |
| 13 | +turn: ActiveTurn, |
| 14 | +): TelegramInboundTurnDeliveryEnd { |
| 15 | +const key = sessionKey?.trim(); |
| 16 | +if (!key) { |
| 17 | +return () => {}; |
| 18 | +} |
| 19 | +registry.set(key, turn); |
| 20 | +return () => { |
| 21 | +registry.delete(key); |
| 22 | +}; |
| 23 | +} |
| 24 | + |
| 25 | +export function notifyTelegramInboundTurnOutboundSuccess(params: { |
| 26 | +sessionKey: string | undefined; |
| 27 | +to: string; |
| 28 | +accountId?: string | null; |
| 29 | +}): void { |
| 30 | +const key = params.sessionKey?.trim(); |
| 31 | +if (!key) { |
| 32 | +return; |
| 33 | +} |
| 34 | +const turn = registry.get(key); |
| 35 | +if (!turn || turn.outboundTo !== params.to) { |
| 36 | +return; |
| 37 | +} |
| 38 | +if (turn.outboundAccountId && params.accountId && params.accountId !== turn.outboundAccountId) { |
| 39 | +return; |
| 40 | +} |
| 41 | +turn.markInboundTurnDelivered(); |
| 42 | +} |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。