From 228b9d74c4b06406c4b95f3863cb7b714c66e354 Mon Sep 17 00:00:00 2001 From: Ani Tunturi Date: Tue, 17 Mar 2026 13:43:25 -0400 Subject: [PATCH] fix(bot,matrix): use resolved conversation key for audio storage Replace hardcoded 'default' conversation ID with convKey in core bot path and chatId in adapter-internal audio calls. Prevents mapping inconsistencies in per-chat/per-channel routing. --- src/channels/matrix/adapter.ts | 17 ++++++++++------- src/core/bot.ts | 32 +++++--------------------------- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/src/channels/matrix/adapter.ts b/src/channels/matrix/adapter.ts index 2512ea0..3c9fb40 100644 --- a/src/channels/matrix/adapter.ts +++ b/src/channels/matrix/adapter.ts @@ -227,8 +227,11 @@ export class MatrixAdapter implements ChannelAdapter { if (!this.client) throw new Error("Matrix client not initialized"); const { chatId, text } = msg; - const { plain, html } = formatMatrixHTML(text); - const htmlBody = (msg.htmlPrefix || '') + html; + // If parseMode is HTML, text is already formatted — skip markdown conversion + const { plain, html } = msg.parseMode === 'HTML' + ? { plain: text.replace(/<[^>]+>/g, ''), html: text } + : formatMatrixHTML(text); + const htmlBody = html; const content = { msgtype: MsgType.Text, @@ -273,11 +276,11 @@ export class MatrixAdapter implements ChannelAdapter { return true; // 'all' } - async editMessage(chatId: string, messageId: string, text: string, htmlPrefix?: string): Promise { + async editMessage(chatId: string, messageId: string, text: string): Promise { if (!this.client) throw new Error("Matrix client not initialized"); const { plain, html } = formatMatrixHTML(text); - const htmlBody = (htmlPrefix || '') + html; + const htmlBody = html; const prefixedPlain = this.config.messagePrefix ? `${this.config.messagePrefix}\n\n${plain}` : plain; const prefixedHtml = this.config.messagePrefix ? `${this.config.messagePrefix}

${htmlBody}` : htmlBody; @@ -1454,7 +1457,7 @@ export class MatrixAdapter implements ChannelAdapter { if (kind === 'audio') { this.ourAudioEvents.add(eventId); if (caption) { - this.storage.storeAudioMessage(eventId, 'default', chatId, caption); + this.storage.storeAudioMessage(eventId, chatId, chatId, caption); } const reactionContent: ReactionEventContent = { "m.relates_to": { @@ -1485,7 +1488,7 @@ export class MatrixAdapter implements ChannelAdapter { const audioEventId = await this.uploadAndSendAudio(roomId, audioData); if (audioEventId) { // Store mapping so 🎤 on the regenerated audio works too - this.storage.storeAudioMessage(audioEventId, "default", roomId, text); + this.storage.storeAudioMessage(audioEventId, roomId, roomId, text); } return audioEventId; } catch (err) { @@ -1516,7 +1519,7 @@ export class MatrixAdapter implements ChannelAdapter { const audioEventId = await this.uploadAndSendAudio(chatId, audioData); if (audioEventId) { // Store for 🎤 regeneration - this.storage.storeAudioMessage(audioEventId, "default", chatId, text); + this.storage.storeAudioMessage(audioEventId, chatId, chatId, text); } } catch (err) { log.error("TTS failed (non-fatal):", err); diff --git a/src/core/bot.ts b/src/core/bot.ts index b74b6cd..0544362 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -13,7 +13,7 @@ import { extname, resolve, join } from 'node:path'; import type { ChannelAdapter } from '../channels/types.js'; import type { BotConfig, InboundMessage, TriggerContext, TriggerType, StreamMsg } from './types.js'; import { formatApiErrorForUser } from './errors.js'; -import { formatToolCallDisplay, formatReasoningDisplay, formatQuestionsForChannel, formatReasoningAsCodeBlock } from './display.js'; +import { formatToolCallDisplay, formatReasoningDisplay, formatQuestionsForChannel } from './display.js'; import type { AgentSession } from './interfaces.js'; import { Store } from './store.js'; import { getPendingApprovals, rejectApproval, cancelRuns, cancelConversation, recoverOrphanedConversationApproval, getLatestRunError, getAgentModel, updateAgentModel, isRecoverableConversationId, recoverPendingApprovalsForAgent } from '../tools/letta-api.js'; @@ -1880,42 +1880,20 @@ export class LettaBot implements AgentSession { await new Promise(resolve => setTimeout(resolve, waitMs)); } - // Determine if reasoning should be shown for this room - const chatId = msg.chatId; - const noReasoningRooms = this.config.display?.noReasoningRooms || []; - const reasoningRooms = this.config.display?.reasoningRooms; - const shouldShowReasoning = this.config.display?.showReasoning && - !noReasoningRooms.includes(chatId) && - (!reasoningRooms || reasoningRooms.length === 0 || reasoningRooms.includes(chatId)); - - // Build reasoning HTML prefix if available (injected into formatted_body only) - let reasoningHtmlPrefix: string | undefined; - if (collectedReasoning.trim() && shouldShowReasoning) { - const reasoningBlock = formatReasoningAsCodeBlock( - collectedReasoning, - adapter.id, - this.config.display?.reasoningMaxChars - ); - if (reasoningBlock) { - reasoningHtmlPrefix = reasoningBlock.text; - log.info(`Reasoning block generated (${reasoningHtmlPrefix.length} chars) for ${chatId}`); - } - } - const finalResponse = this.prefixResponse(response); try { if (messageId) { - await adapter.editMessage(msg.chatId, messageId, finalResponse, reasoningHtmlPrefix); + await adapter.editMessage(msg.chatId, messageId, finalResponse); } else { - await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId, htmlPrefix: reasoningHtmlPrefix }); + await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId }); } sentAnyMessage = true; this.store.resetRecoveryAttempts(); } catch (sendErr) { log.warn('Final message delivery failed:', sendErr instanceof Error ? sendErr.message : sendErr); try { - const result = await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId, htmlPrefix: reasoningHtmlPrefix }); + const result = await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId }); messageId = result.messageId ?? null; sentAnyMessage = true; this.store.resetRecoveryAttempts(); @@ -1931,7 +1909,7 @@ export class LettaBot implements AgentSession { // 🎤 on bot's TEXT message (tap to regenerate TTS audio) adapter.addReaction?.(msg.chatId, messageId, '🎤').catch(() => {}); // Store raw text — adapter's TTS layer will clean it at synthesis time - adapter.storeAudioMessage?.(messageId, 'default', msg.chatId, response); + adapter.storeAudioMessage?.(messageId, convKey, msg.chatId, response); // Generate TTS audio only in response to voice input if (msg.isVoiceInput) { adapter.sendAudio?.(msg.chatId, response).catch((err) => {