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.
This commit is contained in:
Ani Tunturi
2026-03-17 13:43:25 -04:00
committed by Ani
parent 9af2d2625f
commit 228b9d74c4
2 changed files with 15 additions and 34 deletions

View File

@@ -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<void> {
async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
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}<br><br>${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);

View File

@@ -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) => {