mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(discord): normalize tagged reasoning in user-visible replies
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discord/reply normalization parity: strip `<think>/<final>` reasoning tags from final/block outbound payload text before preview-finalization and delivery so user-visible replies stay aligned with cleaned final-answer policy even when transcripts contain raw tagged content. (#38291)
|
||||
- Onboarding/headless Linux daemon probe hardening: treat `systemctl --user is-enabled` probe failures as non-fatal during daemon install flow so onboarding no longer crashes on SSH/headless VPS environments before showing install guidance. (#37297) Thanks @acarbajal-web.
|
||||
- Memory/QMD mcporter Windows spawn hardening: when `mcporter.cmd` launch fails with `spawn EINVAL`, retry via bare `mcporter` shell resolution so QMD recall can continue instead of falling back to builtin memory search. (#27402) Thanks @i0ivi0i.
|
||||
- Tools/web_search Brave language-code validation: align `search_lang` handling with Brave-supported codes (including `zh-hans`, `zh-hant`, `en-gb`, and `pt-br`), map common alias inputs (`zh`, `ja`) to valid Brave values, and reject unsupported codes before upstream requests to prevent 422 failures. (#37260) Thanks @heyanming.
|
||||
|
||||
@@ -46,4 +46,24 @@ describe("readLatestAssistantReply", () => {
|
||||
|
||||
expect(result).toBe("older output");
|
||||
});
|
||||
|
||||
it("normalizes reasoning tags when reading from transcript history", async () => {
|
||||
callGatewayMock.mockResolvedValue({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "<think>private reasoning</think><final>Clean answer</final>",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await readLatestAssistantReply({ sessionKey: "agent:main:child" });
|
||||
|
||||
expect(result).toBe("Clean answer");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -527,6 +527,24 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
expect(editMessageDiscord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("strips reasoning tags from final payload delivery", async () => {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.dispatcher.sendFinalReply({
|
||||
text: "<think>internal chain of thought</think><final>Visible answer</final>",
|
||||
});
|
||||
return { queuedFinal: true, counts: { final: 1, tool: 0, block: 0 } };
|
||||
});
|
||||
|
||||
await processStreamOffDiscordMessage();
|
||||
|
||||
expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
|
||||
expect(deliverDiscordReply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
replies: [expect.objectContaining({ text: "Visible answer" })],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("delivers non-reasoning block payloads to Discord", async () => {
|
||||
mockDispatchSingleBlockReply({ text: "hello from block stream" });
|
||||
await processStreamOffDiscordMessage();
|
||||
|
||||
@@ -592,6 +592,17 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
await draftStream.flush();
|
||||
};
|
||||
|
||||
const sanitizeVisibleReplyText = (text?: string) => {
|
||||
if (typeof text !== "string") {
|
||||
return text;
|
||||
}
|
||||
const cleaned = stripReasoningTagsFromText(text, { mode: "strict", trim: "both" });
|
||||
if (cleaned.startsWith("Reasoning:\n")) {
|
||||
return "";
|
||||
}
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
// When draft streaming is active, suppress block streaming to avoid double-streaming.
|
||||
const disableBlockStreamingForDraft = draftStream ? true : undefined;
|
||||
|
||||
@@ -609,10 +620,15 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
// Reasoning/thinking payloads should not be delivered to Discord.
|
||||
return;
|
||||
}
|
||||
const visiblePayload =
|
||||
typeof payload.text === "string"
|
||||
? { ...payload, text: sanitizeVisibleReplyText(payload.text) }
|
||||
: payload;
|
||||
if (draftStream && isFinal) {
|
||||
await flushDraft();
|
||||
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
const finalText = payload.text;
|
||||
const hasMedia =
|
||||
Boolean(visiblePayload.mediaUrl) || (visiblePayload.mediaUrls?.length ?? 0) > 0;
|
||||
const finalText = visiblePayload.text;
|
||||
const previewFinalText = resolvePreviewFinalText(finalText);
|
||||
const previewMessageId = draftStream.messageId();
|
||||
|
||||
@@ -622,7 +638,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
!hasMedia &&
|
||||
typeof previewFinalText === "string" &&
|
||||
typeof previewMessageId === "string" &&
|
||||
!payload.isError;
|
||||
!visiblePayload.isError;
|
||||
|
||||
if (canFinalizeViaPreviewEdit) {
|
||||
await draftStream.stop();
|
||||
@@ -657,7 +673,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
typeof messageIdAfterStop === "string" &&
|
||||
typeof previewFinalText === "string" &&
|
||||
!hasMedia &&
|
||||
!payload.isError
|
||||
!visiblePayload.isError
|
||||
) {
|
||||
try {
|
||||
await editMessageDiscord(
|
||||
@@ -688,7 +704,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
|
||||
const replyToId = replyReference.use();
|
||||
await deliverDiscordReply({
|
||||
replies: [payload],
|
||||
replies: [visiblePayload],
|
||||
target: deliverTarget,
|
||||
token,
|
||||
accountId,
|
||||
|
||||
Reference in New Issue
Block a user