mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix: suppress reasoning payloads from generic channel dispatch path
When reasoningLevel is 'on', reasoning content was being sent as a visible message to WhatsApp and other non-Telegram channels via two paths: 1. Block reply: emitted via onBlockReply in handleMessageEnd 2. Final payloads: added to replyItems in buildEmbeddedRunPayloads Telegram has its own dispatch path (bot-message-dispatch.ts) that splits reasoning into a dedicated lane and handles suppression. The generic dispatch-from-config.ts path used by WhatsApp, web, etc. had no such filtering. Fix: - Add isReasoning?: boolean flag to ReplyPayload - Tag reasoning payloads at both emission points - Filter isReasoning payloads in dispatch-from-config.ts for both block reply and final reply paths Telegram is unaffected: it uses its own deliver callback that detects reasoning via the 'Reasoning:\n' prefix and routes to a separate lane. Fixes #24954
This commit is contained in:
@@ -187,7 +187,7 @@ export function buildEmbeddedRunPayloads(params: {
|
||||
? formatReasoningMessage(extractAssistantThinking(params.lastAssistant))
|
||||
: "";
|
||||
if (reasoningText) {
|
||||
replyItems.push({ text: reasoningText });
|
||||
replyItems.push({ text: reasoningText, isReasoning: true });
|
||||
}
|
||||
|
||||
const fallbackAnswerText = params.lastAssistant ? extractAssistantText(params.lastAssistant) : "";
|
||||
|
||||
@@ -339,7 +339,7 @@ export function handleMessageEnd(
|
||||
return;
|
||||
}
|
||||
ctx.state.lastReasoningSent = formattedReasoning;
|
||||
void onBlockReply?.({ text: formattedReasoning });
|
||||
void onBlockReply?.({ text: formattedReasoning, isReasoning: true });
|
||||
};
|
||||
|
||||
if (shouldEmitReasoningBeforeAnswer) {
|
||||
|
||||
@@ -538,4 +538,47 @@ describe("dispatchReplyFromConfig", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses isReasoning payloads from final replies (WhatsApp channel)", async () => {
|
||||
setNoAbort();
|
||||
const dispatcher = createDispatcher();
|
||||
const ctx = buildTestCtx({ Provider: "whatsapp" });
|
||||
const replyResolver = async () =>
|
||||
[
|
||||
{ text: "Reasoning:\n_thinking..._", isReasoning: true },
|
||||
{ text: "The answer is 42" },
|
||||
] satisfies ReplyPayload[];
|
||||
await dispatchReplyFromConfig({ ctx, cfg: emptyConfig, dispatcher, replyResolver });
|
||||
const finalCalls = (dispatcher.sendFinalReply as ReturnType<typeof vi.fn>).mock.calls;
|
||||
expect(finalCalls).toHaveLength(1);
|
||||
expect(finalCalls[0][0]).toMatchObject({ text: "The answer is 42" });
|
||||
});
|
||||
|
||||
it("suppresses isReasoning payloads from block replies (generic dispatch path)", async () => {
|
||||
setNoAbort();
|
||||
const dispatcher = createDispatcher();
|
||||
const ctx = buildTestCtx({ Provider: "whatsapp" });
|
||||
const blockReplySentTexts: string[] = [];
|
||||
const replyResolver = async (
|
||||
_ctx: MsgContext,
|
||||
opts?: GetReplyOptions,
|
||||
): Promise<ReplyPayload> => {
|
||||
// Simulate block reply with reasoning payload
|
||||
await opts?.onBlockReply?.({ text: "Reasoning:\n_thinking..._", isReasoning: true });
|
||||
await opts?.onBlockReply?.({ text: "The answer is 42" });
|
||||
return { text: "The answer is 42" };
|
||||
};
|
||||
// Capture what actually gets dispatched as block replies
|
||||
(dispatcher.sendBlockReply as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
(payload: ReplyPayload) => {
|
||||
if (payload.text) {
|
||||
blockReplySentTexts.push(payload.text);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
await dispatchReplyFromConfig({ ctx, cfg: emptyConfig, dispatcher, replyResolver });
|
||||
expect(blockReplySentTexts).not.toContain("Reasoning:\n_thinking..._");
|
||||
expect(blockReplySentTexts).toContain("The answer is 42");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -363,6 +363,12 @@ export async function dispatchReplyFromConfig(params: {
|
||||
},
|
||||
onBlockReply: (payload: ReplyPayload, context) => {
|
||||
const run = async () => {
|
||||
// Suppress reasoning payloads — channels using this generic dispatch
|
||||
// path (WhatsApp, web, etc.) do not have a dedicated reasoning lane.
|
||||
// Telegram has its own dispatch path that handles reasoning splitting.
|
||||
if (payload.isReasoning) {
|
||||
return;
|
||||
}
|
||||
// Accumulate block text for TTS generation after streaming
|
||||
if (payload.text) {
|
||||
if (accumulatedBlockText.length > 0) {
|
||||
@@ -396,6 +402,11 @@ export async function dispatchReplyFromConfig(params: {
|
||||
let queuedFinal = false;
|
||||
let routedFinalCount = 0;
|
||||
for (const reply of replies) {
|
||||
// Suppress reasoning payloads from channel delivery — channels using this
|
||||
// generic dispatch path do not have a dedicated reasoning lane.
|
||||
if (reply.isReasoning) {
|
||||
continue;
|
||||
}
|
||||
const ttsReply = await maybeApplyTtsToPayload({
|
||||
payload: reply,
|
||||
cfg,
|
||||
|
||||
@@ -66,6 +66,9 @@ export type ReplyPayload = {
|
||||
/** Send audio as voice message (bubble) instead of audio file. Defaults to false. */
|
||||
audioAsVoice?: boolean;
|
||||
isError?: boolean;
|
||||
/** Marks this payload as a reasoning/thinking block. Channels that do not
|
||||
* have a dedicated reasoning lane (e.g. WhatsApp, web) should suppress it. */
|
||||
isReasoning?: boolean;
|
||||
/** Channel-specific payload data (per-channel envelope). */
|
||||
channelData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user