Skip to content

Commit aa71f7f

Browse files
VACIncclawsweeper[bot]Takhoffman
authored
Fix Telegram stop debounce bypass (#83248)
Summary: - The PR adds a generic inbound debounce `cancelKey`, uses Telegram stop-like controls to cancel same-chat pen ... buffers and bypass debounce, and adds focused Telegram regression coverage plus updated channel test mocks. - Reproducibility: yes. by source inspection: current main enqueues Telegram text through inbound debounce bef ... nly has flush semantics for pending keyed work. I did not run a live Telegram repro in this read-only pass. Automerge notes: - PR branch already contained follow-up commit before automerge: Fix Telegram stop debounce bypass Validation: - ClawSweeper review passed for head 19245a3. - Required merge gates passed before the squash merge. Prepared head SHA: 19245a3 Review: #83248 (comment) Co-authored-by: VACInc <[email protected]> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <[email protected]>
1 parent d85a7c6 commit aa71f7f

8 files changed

Lines changed: 364 additions & 4 deletions

File tree

extensions/feishu/src/monitor.reaction.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,7 @@ describe("Feishu inbound debounce regressions", () => {
710710
params.onError?.(new Error("dispatch failed"), [item]);
711711
},
712712
flushKey: async () => {},
713+
cancelKey: () => false,
713714
}),
714715
}),
715716
);

extensions/feishu/src/monitor.test-mocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function createFeishuRuntimeMockModule(): {
3434
createInboundDebouncer: () => ({
3535
enqueue: async () => {},
3636
flushKey: async () => {},
37+
cancelKey: () => false,
3738
}),
3839
},
3940
text: {

extensions/feishu/src/test-support/lifecycle-test-support.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ function createImmediateInboundDebounce() {
8888
}
8989
},
9090
flushKey: async () => {},
91+
cancelKey: () => false,
9192
}),
9293
};
9394
}

extensions/msteams/src/monitor-handler.feedback-authz.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ function createRuntimeStub(readAllowFromStore: ReturnType<typeof vi.fn>): Plugin
4747
resolveInboundDebounceMs: () => 0,
4848
createInboundDebouncer: () => ({
4949
enqueue: async () => {},
50+
flushKey: async () => {},
51+
cancelKey: () => false,
5052
}),
5153
},
5254
pairing: {

extensions/msteams/src/monitor-handler.file-consent.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ function createRuntimeStub(stateDir?: string): PluginRuntime {
3939
resolveInboundDebounceMs: () => 0,
4040
createInboundDebouncer: () => ({
4141
enqueue: async () => {},
42+
flushKey: async () => {},
43+
cancelKey: () => false,
4244
}),
4345
},
4446
},

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "openclaw/plugin-sdk/channel-inbound-debounce";
1515
import { resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth-native";
1616
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
17+
import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
1718
import { buildCommandsMessagePaginated } from "openclaw/plugin-sdk/command-status";
1819
import type { DmPolicy, OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
1920
import type {
@@ -1506,6 +1507,7 @@ export const registerTelegramHandlers = ({
15061507
isForum: boolean;
15071508
resolvedThreadId?: number;
15081509
dmThreadId?: number;
1510+
dmPolicy: DmPolicy;
15091511
storeAllowFrom: string[];
15101512
senderId: string;
15111513
effectiveGroupAllow: NormalizedAllowFrom;
@@ -1524,6 +1526,7 @@ export const registerTelegramHandlers = ({
15241526
isForum,
15251527
resolvedThreadId,
15261528
dmThreadId,
1529+
dmPolicy,
15271530
storeAllowFrom,
15281531
senderId,
15291532
effectiveGroupAllow,
@@ -1535,11 +1538,39 @@ export const registerTelegramHandlers = ({
15351538
promptContextMinTimestampMs,
15361539
} = params;
15371540

1541+
const messageText = getTelegramTextParts(msg).text;
1542+
const botUsername = ctx.me?.username;
1543+
const isAbortControlMessage = isAbortRequestText(messageText, { botUsername });
1544+
let abortControlAuthorized: Promise<boolean> | undefined;
1545+
const isAuthorizedAbortControlMessage = () => {
1546+
if (!isAbortControlMessage || !senderId) {
1547+
return Promise.resolve(false);
1548+
}
1549+
abortControlAuthorized ??= resolveTelegramCommandIngressAuthorization({
1550+
accountId,
1551+
cfg,
1552+
dmPolicy,
1553+
isGroup,
1554+
chatId,
1555+
resolvedThreadId,
1556+
senderId,
1557+
effectiveDmAllow,
1558+
effectiveGroupAllow,
1559+
ownerAccess: { ownerList: [], senderIsOwner: false },
1560+
eventKind: "message",
1561+
allowTextCommands: true,
1562+
hasControlCommand: true,
1563+
modeWhenAccessGroupsOff: "allow",
1564+
includeDmAllowForGroupCommands: false,
1565+
}).then((gate) => gate.authorized);
1566+
return abortControlAuthorized;
1567+
};
1568+
15381569
// Text fragment handling - Telegram splits long pastes into multiple inbound messages (~4096 chars).
15391570
// We buffer “near-limit” messages and append immediately-following parts.
15401571
const text = typeof msg.text === "string" ? msg.text : undefined;
15411572
const isCommandLike = (text ?? "").trim().startsWith("/");
1542-
if (text && !isCommandLike) {
1573+
if (text && !isCommandLike && !isAbortControlMessage) {
15431574
const nowMs = Date.now();
15441575
const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
15451576
// Use resolvedThreadId for forum groups, dmThreadId for DM topics
@@ -1602,6 +1633,15 @@ export const registerTelegramHandlers = ({
16021633
scheduleTextFragmentFlush(entry);
16031634
return;
16041635
}
1636+
} else if (text && isAbortControlMessage && (await isAuthorizedAbortControlMessage())) {
1637+
const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
1638+
const threadId = resolvedThreadId ?? dmThreadId;
1639+
const key = `text:${chatId}:${threadId ?? "main"}:${senderId}`;
1640+
const existing = textFragmentBuffer.get(key);
1641+
if (existing) {
1642+
clearTimeout(existing.timer);
1643+
textFragmentBuffer.delete(key);
1644+
}
16051645
}
16061646

16071647
// Media group handling - buffer multi-image messages
@@ -1743,15 +1783,27 @@ export const registerTelegramHandlers = ({
17431783
debounceLane,
17441784
})
17451785
: null;
1786+
if (senderId && (await isAuthorizedAbortControlMessage())) {
1787+
for (const lane of ["default", "forward"] as const) {
1788+
inboundDebouncer.cancelKey(
1789+
buildTelegramInboundDebounceKey({
1790+
accountId,
1791+
conversationKey,
1792+
senderId,
1793+
debounceLane: lane,
1794+
}),
1795+
);
1796+
}
1797+
}
17461798
await inboundDebouncer.enqueue({
17471799
ctx,
17481800
msg,
17491801
allMedia,
17501802
storeAllowFrom,
17511803
receivedAtMs: Date.now(),
1752-
debounceKey,
1804+
debounceKey: isAbortControlMessage ? null : debounceKey,
17531805
debounceLane,
1754-
botUsername: ctx.me?.username,
1806+
botUsername,
17551807
...promptContextBoundaryOptions(promptContextMinTimestampMs),
17561808
});
17571809
};
@@ -2656,6 +2708,7 @@ export const registerTelegramHandlers = ({
26562708
isForum: event.isForum,
26572709
resolvedThreadId,
26582710
dmThreadId,
2711+
dmPolicy,
26592712
storeAllowFrom,
26602713
senderId: event.senderId,
26612714
effectiveGroupAllow,

0 commit comments

Comments
 (0)