From 175c42eacd7eb3d4849629b33669570b543b01c1 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sun, 10 May 2026 08:20:38 +0530 Subject: [PATCH] fix(telegram): tighten select callback handling --- .../telegram/src/bot-handlers.runtime.ts | 55 +++++++++++++----- .../src/bot.create-telegram-bot.test.ts | 16 +++--- src/agents/internal-events.ts | 16 +----- ...ded-helpers.sanitizeuserfacingtext.test.ts | 20 ------- src/agents/subagent-announce-delivery.test.ts | 57 ------------------- src/agents/subagent-announce-delivery.ts | 6 +- 6 files changed, 52 insertions(+), 118 deletions(-) diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index 3a6fdfbff1b8..fb94fe4f81ee 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -240,15 +240,40 @@ export const registerTelegramHandlers = ({ }; const MULTI_SELECT_PREFIX = "OC_MULTI|"; + const MULTI_SELECT_TOGGLE_PREFIX = `${MULTI_SELECT_PREFIX}toggle|`; const SELECT_PREFIX = "OC_SELECT|"; const SELECTED_PREFIX = "✅ "; + type TelegramManagedSelectCallback = + | { type: "multi-toggle"; value: string } + | { type: "multi-clear" } + | { type: "multi-submit" } + | { type: "select"; value: string }; + type TelegramCallbackButton = { text: string; callback_data: string; style?: "danger" | "success" | "primary"; }; + const parseTelegramManagedSelectCallback = ( + data: string, + ): TelegramManagedSelectCallback | undefined => { + if (data.startsWith(MULTI_SELECT_TOGGLE_PREFIX)) { + return { type: "multi-toggle", value: data.slice(MULTI_SELECT_TOGGLE_PREFIX.length) }; + } + if (data === `${MULTI_SELECT_PREFIX}clear`) { + return { type: "multi-clear" }; + } + if (data === `${MULTI_SELECT_PREFIX}submit`) { + return { type: "multi-submit" }; + } + if (data.startsWith(SELECT_PREFIX)) { + return { type: "select", value: data.slice(SELECT_PREFIX.length) }; + } + return undefined; + }; + const cloneInlineKeyboardButtons = (message: Message): TelegramCallbackButton[][] => { const rows = (message as { reply_markup?: { inline_keyboard?: unknown } }).reply_markup ?.inline_keyboard; @@ -292,15 +317,14 @@ export const registerTelegramHandlers = ({ const isSelectedMultiButton = (button: TelegramCallbackButton): boolean => /^✅\s*/.test(button.text); const isMultiToggleButton = (button: TelegramCallbackButton): boolean => - typeof button.callback_data === "string" && - button.callback_data.startsWith(`${MULTI_SELECT_PREFIX}toggle|`); + button.callback_data.startsWith(MULTI_SELECT_TOGGLE_PREFIX); const resolveMultiSelectedValues = (buttons: TelegramCallbackButton[][]): string[] => buttons.flatMap((row) => row.flatMap((button) => { if (!isMultiToggleButton(button) || !isSelectedMultiButton(button)) { return []; } - return [button.callback_data.slice(`${MULTI_SELECT_PREFIX}toggle|`.length)]; + return [button.callback_data.slice(MULTI_SELECT_TOGGLE_PREFIX.length)]; }), ); const updateMultiSelectKeyboard = ( @@ -313,7 +337,7 @@ export const registerTelegramHandlers = ({ if (!isMultiToggleButton(button)) { return button; } - const buttonValue = button.callback_data.slice(`${MULTI_SELECT_PREFIX}toggle|`.length); + const buttonValue = button.callback_data.slice(MULTI_SELECT_TOGGLE_PREFIX.length); const baseText = stripMultiSelectPrefix(button.text); const selected = action === "clear" @@ -1694,10 +1718,17 @@ export const registerTelegramHandlers = ({ return; } - if (data.startsWith(MULTI_SELECT_PREFIX)) { - const [, action, value = ""] = data.split("|"); - if (action === "toggle" || action === "clear") { - const buttons = updateMultiSelectKeyboard(callbackMessage, action, value); + const managedSelectCallback = parseTelegramManagedSelectCallback(data); + if (managedSelectCallback) { + if ( + managedSelectCallback.type === "multi-toggle" || + managedSelectCallback.type === "multi-clear" + ) { + const buttons = updateMultiSelectKeyboard( + callbackMessage, + managedSelectCallback.type === "multi-clear" ? "clear" : "toggle", + managedSelectCallback.type === "multi-toggle" ? managedSelectCallback.value : "", + ); if (buttons.length > 0) { try { await editCallbackButtons(buttons); @@ -1709,7 +1740,8 @@ export const registerTelegramHandlers = ({ } return; } - if (action === "submit") { + + if (managedSelectCallback.type === "multi-submit") { const selected = resolveMultiSelectedValues(cloneInlineKeyboardButtons(callbackMessage)); const synthetic = buildCallbackSyntheticTextContext({ ctx, @@ -1724,10 +1756,7 @@ export const registerTelegramHandlers = ({ }); return; } - } - if (data.startsWith(SELECT_PREFIX)) { - const value = data.slice(SELECT_PREFIX.length); try { await clearCallbackButtons(); } catch (editErr) { @@ -1743,7 +1772,7 @@ export const registerTelegramHandlers = ({ ctx, callbackMessage, callback, - text: `Single-select submitted: ${value}`, + text: `Single-select submitted: ${managedSelectCallback.value}`, isForum, }); await processMessageWithReplyChain(synthetic.ctx, synthetic.message, [], storeAllowFrom, { diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index 1ad1a30413b1..b22330150426 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -764,14 +764,14 @@ describe("createTelegramBot", () => { await callbackHandler({ callbackQuery: { id: "cbq-multi-toggle-1", - data: "OC_MULTI|toggle|red", + data: "OC_MULTI|toggle|env|prod", from: { id: 9, first_name: "Ada", username: "ada_bot" }, message: { chat: { id: 1234, type: "private" }, date: 1736380800, message_id: 10, reply_markup: { - inline_keyboard: [[{ text: "Red", callback_data: "OC_MULTI|toggle|red" }]], + inline_keyboard: [[{ text: "Prod", callback_data: "OC_MULTI|toggle|env|prod" }]], }, }, }, @@ -781,7 +781,7 @@ describe("createTelegramBot", () => { expect(editMessageReplyMarkupSpy).toHaveBeenCalledWith(1234, 10, { reply_markup: { - inline_keyboard: [[{ text: "✅ Red", callback_data: "OC_MULTI|toggle|red" }]], + inline_keyboard: [[{ text: "✅ Prod", callback_data: "OC_MULTI|toggle|env|prod" }]], }, }); expect(replySpy).not.toHaveBeenCalled(); @@ -808,7 +808,7 @@ describe("createTelegramBot", () => { message_id: 10, reply_markup: { inline_keyboard: [ - [{ text: "✅ Red", callback_data: "OC_MULTI|toggle|red" }], + [{ text: "✅ Prod", callback_data: "OC_MULTI|toggle|env|prod" }], [{ text: "Blue", callback_data: "OC_MULTI|toggle|blue" }], ], }, @@ -819,7 +819,7 @@ describe("createTelegramBot", () => { }); expect(replySpy).toHaveBeenCalledTimes(1); - expect(replySpy.mock.calls[0][0].Body).toContain("Multi-select submitted: red"); + expect(replySpy.mock.calls[0][0].Body).toContain("Multi-select submitted: env|prod"); }); it("submits OC_SELECT values as a synthetic inbound message and clears buttons", async () => { @@ -834,14 +834,14 @@ describe("createTelegramBot", () => { await callbackHandler({ callbackQuery: { id: "cbq-select-1", - data: "OC_SELECT|alpha", + data: "OC_SELECT|env|canary", from: { id: 9, first_name: "Ada", username: "ada_bot" }, message: { chat: { id: 1234, type: "private" }, date: 1736380800, message_id: 10, reply_markup: { - inline_keyboard: [[{ text: "Alpha", callback_data: "OC_SELECT|alpha" }]], + inline_keyboard: [[{ text: "Canary", callback_data: "OC_SELECT|env|canary" }]], }, }, }, @@ -853,7 +853,7 @@ describe("createTelegramBot", () => { reply_markup: { inline_keyboard: [] }, }); expect(replySpy).toHaveBeenCalledTimes(1); - expect(replySpy.mock.calls[0][0].Body).toContain("Single-select submitted: alpha"); + expect(replySpy.mock.calls[0][0].Body).toContain("Single-select submitted: env|canary"); }); it("preserves native command source for prefixed callback_query payloads", async () => { diff --git a/src/agents/internal-events.ts b/src/agents/internal-events.ts index 13e60b0237ef..6ecb489e138e 100644 --- a/src/agents/internal-events.ts +++ b/src/agents/internal-events.ts @@ -25,8 +25,6 @@ type AgentTaskCompletionInternalEvent = { replyInstruction: string; }; -const MAX_CHILD_RESULT_PROMPT_CHARS = 4_000; - export type AgentInternalEvent = AgentTaskCompletionInternalEvent; export { INTERNAL_RUNTIME_CONTEXT_BEGIN, INTERNAL_RUNTIME_CONTEXT_END }; @@ -43,23 +41,11 @@ function sanitizeMultilineField(value: string, fallback: string): string { return sanitized || fallback; } -function truncateChildResultForPrompt(value: string): string { - if (value.length <= MAX_CHILD_RESULT_PROMPT_CHARS) { - return value; - } - return [ - value.slice(0, MAX_CHILD_RESULT_PROMPT_CHARS).trimEnd(), - "", - `[child result truncated: ${value.length - MAX_CHILD_RESULT_PROMPT_CHARS} additional characters omitted]`, - ].join("\n"); -} - function formatChildResultDataBlock(value: string): string { - const safeValue = truncateChildResultForPrompt(value); return ( wrapPromptDataBlock({ label: "Child result", - text: safeValue, + text: value, }) || "Child result: (no output)" ); } diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts index 24e4bf822f36..05236f98f5fc 100644 --- a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts @@ -362,26 +362,6 @@ describe("sanitizeUserFacingText", () => { ); }); - it("bounds large child completion results before injecting internal context", () => { - const internal = formatAgentInternalEventsForPrompt([ - { - type: "task_completion", - source: "subagent", - childSessionKey: "agent:main:subagent:test", - childSessionId: "sess_1", - announceType: "subagent task", - taskLabel: "Investigate issue", - status: "ok", - statusLabel: "completed successfully", - result: "x".repeat(6_000), - replyInstruction: "Reply to the user in your own words.", - }, - ]); - - expect(internal).toContain("[child result truncated: 2000 additional characters omitted]"); - expect(internal.length).toBeLessThan(5_000); - }); - it("does not strip inline delimiter mentions that are not standalone marker lines", () => { const input = `Note: ${INTERNAL_RUNTIME_CONTEXT_BEGIN} appears inline and should stay.`; expect(sanitizeUserFacingText(input)).toBe(input); diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts index eac0682a64a2..9f1968545cac 100644 --- a/src/agents/subagent-announce-delivery.test.ts +++ b/src/agents/subagent-announce-delivery.test.ts @@ -1211,63 +1211,6 @@ describe("deliverSubagentAnnouncement completion delivery", () => { expect(sendMessage).not.toHaveBeenCalled(); }); - it("reports subagent group completions that miss required message-tool delivery", async () => { - const callGateway = createGatewayMock({ - result: { - payloads: [ - { - text: "Child result that must not be raw-sent.", - }, - ], - }, - }); - const sendMessage = createSendMessageMock(); - const result = await deliverSlackChannelAnnouncement({ - callGateway, - sendMessage, - sessionId: "requester-session-channel", - isActive: false, - expectsCompletionMessage: true, - directIdempotencyKey: "announce-channel-subagent-message-tool", - sourceTool: "subagent_announce", - internalEvents: [ - { - type: "task_completion", - source: "subagent", - childSessionKey: "agent:openclaw:subagent:child-123", - childSessionId: "child-123", - announceType: "subagent task", - taskLabel: "channel completion smoke", - status: "ok", - statusLabel: "completed successfully", - result: "Raw child result that should stay internal.", - replyInstruction: "Let the requester/orchestrator deliver the final response.", - }, - ], - }); - - expect(result).toEqual( - expect.objectContaining({ - delivered: false, - path: "direct", - error: "completion agent did not deliver through the message tool", - }), - ); - expect(callGateway).toHaveBeenCalledWith( - expect.objectContaining({ - method: "agent", - params: expect.objectContaining({ - deliver: false, - channel: "slack", - accountId: "acct-1", - to: "channel:C123", - threadId: undefined, - }), - }), - ); - expect(sendMessage).not.toHaveBeenCalled(); - }); - it("does not fallback for generated media group completions when message tool evidence exists", async () => { const callGateway = createGatewayMock({ result: { diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts index 529922cdf758..f253566a5c27 100644 --- a/src/agents/subagent-announce-delivery.ts +++ b/src/agents/subagent-announce-delivery.ts @@ -56,11 +56,7 @@ import type { SpawnSubagentMode } from "./subagent-spawn.types.js"; const DEFAULT_SUBAGENT_ANNOUNCE_TIMEOUT_MS = 120_000; const MAX_TIMER_SAFE_TIMEOUT_MS = 2_147_000_000; -const AGENT_MEDIATED_COMPLETION_TOOLS = new Set([ - "music_generate", - "video_generate", - "subagent_announce", -]); +const AGENT_MEDIATED_COMPLETION_TOOLS = new Set(["music_generate", "video_generate"]); type SubagentAnnounceDeliveryDeps = { callGateway: typeof callGateway;