mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(telegram): prevent false empty replies after message-tool delivery (#115588)
* fix(telegram): suppress message-tool-only reply fallbacks Honor message-tool-only delivery when a non-silent Telegram reply is skipped in both ordinary group dispatch and native slash-command turns. Keep the existing visible fallback for genuinely empty native replies and cover both dispatch surfaces with regression tests. Fixes #90091 * fix(telegram): preserve genuine delivery failure fallbacks Suppress only non-silent skips owned by message-tool-only delivery. Preserve visible fallbacks when the same turn also encounters a real dispatch or final-delivery failure, with an explicit regression for the mixed failure path. * fix(telegram): preserve native message-tool delivery errors Track genuine native slash delivery failures so message-tool-only skip suppression never hides a failed response. Cover the exact mixed empty-skip and real-error path with a regression while preserving true-empty and intentionally cancelled fallbacks. * test(telegram): preserve synchronous native delivery callbacks
This commit is contained in:
committed by
GitHub
parent
98dae674cb
commit
104322c5cc
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
Reference in New Issue
Block a user