diff --git a/extensions/telegram/src/bot-message-dispatch.fallback-topic-media.test.ts b/extensions/telegram/src/bot-message-dispatch.fallback-topic-media.test.ts index 221c29ea48bc..120cbd4488a6 100644 --- a/extensions/telegram/src/bot-message-dispatch.fallback-topic-media.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.fallback-topic-media.test.ts @@ -124,6 +124,68 @@ describeTelegramDispatch("dispatchTelegramMessage fallback-topic-media", () => { expect(deliverReplies).not.toHaveBeenCalled(); }); + it("does not emit an empty-response fallback for message-tool-only delivery skips", async () => { + dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => { + dispatcherOptions.onSkip?.({}, { kind: "final", reason: "empty" }); + return { + queuedFinal: false, + counts: { block: 0, final: 0, tool: 0 }, + sourceReplyDeliveryMode: "message_tool_only", + }; + }); + + await dispatchWithContext({ + context: createContext({ + chatId: -1001234, + isGroup: true, + ctxPayload: { + SessionKey: "agent:test:telegram:group:-1001234", + ChatType: "group", + } as TelegramMessageContext["ctxPayload"], + primaryCtx: { + message: { chat: { id: -1001234, type: "supergroup" } }, + } as TelegramMessageContext["primaryCtx"], + msg: { + chat: { id: -1001234, type: "supergroup" }, + message_id: 456, + } as TelegramMessageContext["msg"], + threadSpec: { id: undefined, scope: "none" }, + replyThreadId: undefined, + }), + streamMode: "off", + }); + + expect(deliverReplies).not.toHaveBeenCalled(); + }); + + it("retains the failure fallback when message-tool-only delivery also fails", async () => { + dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => { + dispatcherOptions.onSkip?.({}, { kind: "final", reason: "empty" }); + await dispatcherOptions.onError?.(new Error("Telegram final delivery failed"), { + kind: "final", + }); + return { + queuedFinal: false, + counts: { block: 0, final: 0, tool: 0 }, + sourceReplyDeliveryMode: "message_tool_only", + }; + }); + + await dispatchWithContext({ + context: createContext({ + ctxPayload: createDirectSessionPayload(), + }), + streamMode: "off", + }); + + expect(deliverReplies).toHaveBeenCalledOnce(); + expect(deliverReplies).toHaveBeenCalledWith( + expect.objectContaining({ + replies: [{ text: "No response generated. Please try again." }], + }), + ); + }); + it("does not emit a silent-reply fallback for no-response group turns", async () => { dispatchReplyWithBufferedBlockDispatcher.mockResolvedValue({ queuedFinal: false, diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index 207212c4ec76..bd58d34b3dfc 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -489,8 +489,8 @@ export const dispatchTelegramMessage = async ({ !suppressFailureFallback && !progress.finalAnswerDelivered() && (state.dispatchError || - deliverySummary.skippedNonSilent > 0 || - deliverySummary.failedNonSilent > 0); + deliverySummary.failedNonSilent > 0 || + (deliverySummary.skippedNonSilent > 0 && !state.suppressSilentReplyFallback)); if (shouldSendFailureFallback) { const fallbackText = state.dispatchError ? "Something went wrong while processing your request. Please try again." diff --git a/extensions/telegram/src/bot-native-commands.session-meta.test.ts b/extensions/telegram/src/bot-native-commands.session-meta.test.ts index 47e32c4bc221..d240eefa9651 100644 --- a/extensions/telegram/src/bot-native-commands.session-meta.test.ts +++ b/extensions/telegram/src/bot-native-commands.session-meta.test.ts @@ -1354,6 +1354,58 @@ describe("registerTelegramNativeCommands — session metadata", () => { expect(deliveryMocks.deliverReplies).not.toHaveBeenCalled(); }); + it("does not emit the empty fallback for a message-tool-only native reply", async () => { + dispatchChannelInboundTurnMock.mockImplementationOnce(async (plan) => { + plan.dispatcherOptions?.onSkip?.({}, { kind: "final", reason: "empty" }); + return { + admission: { kind: "dispatch" }, + dispatched: true, + ctxPayload: plan.ctxPayload, + routeSessionKey: plan.route.sessionKey, + dispatchResult: { + queuedFinal: false, + counts: { block: 0, final: 0, tool: 0 }, + sourceReplyDeliveryMode: "message_tool_only", + }, + }; + }); + const { handler } = registerAndResolveStatusHandler({ cfg: {} }); + + await handler(createTelegramPrivateCommandContext()); + + expect(deliveryMocks.deliverReplies).not.toHaveBeenCalled(); + }); + + it("retains the native fallback when message-tool-only delivery also fails", async () => { + dispatchChannelInboundTurnMock.mockImplementationOnce(async (plan) => { + plan.dispatcherOptions?.onSkip?.({}, { kind: "final", reason: "empty" }); + plan.delivery.onError?.(new Error("Telegram final delivery failed"), { + kind: "final", + }); + return { + admission: { kind: "dispatch" }, + dispatched: true, + ctxPayload: plan.ctxPayload, + routeSessionKey: plan.route.sessionKey, + dispatchResult: { + queuedFinal: false, + counts: { block: 0, final: 0, tool: 0 }, + sourceReplyDeliveryMode: "message_tool_only", + }, + }; + }); + const { handler } = registerAndResolveStatusHandler({ cfg: {} }); + + await handler(createTelegramPrivateCommandContext()); + + expect(deliveryMocks.deliverReplies).toHaveBeenCalledOnce(); + expect(deliveryMocks.deliverReplies).toHaveBeenCalledWith( + expect.objectContaining({ + replies: [{ text: "No response generated. Please try again." }], + }), + ); + }); + it("retains the empty fallback for a true non-silent metadata-only native reply", async () => { dispatchChannelInboundTurnMock.mockImplementationOnce(async (plan) => { plan.dispatcherOptions?.onSkip?.({}, { kind: "final", reason: "empty" }); diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index f18a0e8c2a4e..2f37207e69e7 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -1650,6 +1650,7 @@ export const registerTelegramNativeCommands = ({ delivered: false, intentionallySuppressed: false, skippedNonSilent: 0, + failedNonSilent: 0, }; const { deliverReplies } = await loadTelegramNativeCommandDeliveryRuntime(); @@ -1735,6 +1736,7 @@ export const registerTelegramNativeCommands = ({ } }, onError: (err, info) => { + deliveryState.failedNonSilent += 1; runtime.error?.(danger(`telegram slash ${info.kind} reply failed: ${String(err)}`)); }, }, @@ -1743,14 +1745,17 @@ export const registerTelegramNativeCommands = ({ disableBlockStreaming, }, }; - await ( + const turnResult = await ( telegramDeps.dispatchChannelInboundTurn ?? defaultTelegramNativeCommandDeps.dispatchChannelInboundTurn )(turnPlan); if ( !deliveryState.delivered && !deliveryState.intentionallySuppressed && - deliveryState.skippedNonSilent > 0 + deliveryState.skippedNonSilent > 0 && + (!turnResult.dispatched || + turnResult.dispatchResult.sourceReplyDeliveryMode !== "message_tool_only" || + deliveryState.failedNonSilent > 0) ) { await deliverReplies({ replies: [{ text: EMPTY_RESPONSE_FALLBACK }],