/** * Repair Goose conversations from h5_conversation_messages when agent sync * left empty placeholders (portal direct chat → goosed escalation). */ import { extractConversationMessageText } from './conversation-memory.mjs'; export function parseStoredConversationRow(row) { if (!row) return null; const rawJson = row.raw_json ?? row.rawJson; if (typeof rawJson === 'string' && rawJson.trim()) { try { const parsed = JSON.parse(rawJson); if (parsed && typeof parsed === 'object') { return { ...parsed, metadata: { userVisible: true, agentVisible: true, ...(parsed.metadata && typeof parsed.metadata === 'object' ? parsed.metadata : {}), }, }; } } catch { // fall through to text reconstruction } } const text = String(row.text ?? '').trim(); if (!text) return null; const messageKey = String(row.message_key ?? row.messageKey ?? '').trim(); const createdMs = Number(row.created_at ?? row.createdAt ?? 0) || Date.now(); return { id: messageKey || undefined, role: String(row.role ?? 'user').trim() || 'user', created: Math.floor(createdMs / 1000), content: [{ type: 'text', text }], metadata: { userVisible: true, agentVisible: true, ...(String(row.role ?? '') === 'assistant' ? { source: 'conversation-db-repair' } : {}), }, }; } export function countNonEmptyConversationMessages(messages) { if (!Array.isArray(messages)) return 0; return messages.filter((message) => { if (message?.metadata?.userVisible === false) return false; return Boolean(extractConversationMessageText(message)); }).length; } export function buildConversationFromDbRows(dbRows) { if (!Array.isArray(dbRows) || dbRows.length === 0) return []; const byKey = new Map(); for (const row of dbRows) { const parsed = parseStoredConversationRow(row); if (!parsed) continue; const key = String(row.message_key ?? row.messageKey ?? parsed.id ?? '').trim() || `${parsed.role}:${extractConversationMessageText(parsed).slice(0, 48)}`; byKey.set(key, { message: parsed, sequenceNo: Number(row.sequence_no ?? row.sequenceNo ?? 0), createdAt: Number(row.created_at ?? row.createdAt ?? 0), }); } return [...byKey.values()] .sort((left, right) => { if (left.sequenceNo !== right.sequenceNo) return left.sequenceNo - right.sequenceNo; if (left.createdAt !== right.createdAt) return left.createdAt - right.createdAt; return String(left.message.id ?? '').localeCompare(String(right.message.id ?? '')); }) .map((entry) => entry.message); } export function shouldRepairConversationFromDb(gooseMessages, dbRows) { const dbBuilt = buildConversationFromDbRows(dbRows); const dbCount = countNonEmptyConversationMessages(dbBuilt); if (dbCount === 0) return false; const gooseVisible = Array.isArray(gooseMessages) ? gooseMessages.filter((message) => message?.metadata?.userVisible !== false) : []; const gooseNonEmpty = countNonEmptyConversationMessages(gooseVisible); const gooseEmptyVisible = gooseVisible.some( (message) => !extractConversationMessageText(message), ); if (gooseNonEmpty === 0) return true; if (dbCount > gooseNonEmpty) return true; if (gooseEmptyVisible && dbCount >= gooseNonEmpty) return true; return false; } export function repairConversationFromDbRows(gooseMessages, dbRows) { const dbBuilt = buildConversationFromDbRows(dbRows); if (!shouldRepairConversationFromDb(gooseMessages, dbRows)) { return Array.isArray(gooseMessages) ? gooseMessages : []; } if (dbBuilt.length === 0) return Array.isArray(gooseMessages) ? gooseMessages : []; return dbBuilt; } export async function loadSessionConversationRows(pool, sessionId, userId) { if (!pool || !sessionId || !userId) return []; const [rows] = await pool.query( `SELECT message_key, sequence_no, role, text, raw_json, created_at FROM h5_conversation_messages WHERE agent_session_id = ? AND user_id = ? ORDER BY sequence_no ASC, created_at ASC`, [sessionId, userId], ); return rows; } export async function repairSessionConversationFromDb(pool, gooseSession, sessionId, userId) { if (!gooseSession || !pool || !sessionId || !userId) return gooseSession; const dbRows = await loadSessionConversationRows(pool, sessionId, userId); const conversation = Array.isArray(gooseSession.conversation) ? gooseSession.conversation : []; if (!shouldRepairConversationFromDb(conversation, dbRows)) return gooseSession; return { ...gooseSession, conversation: repairConversationFromDbRows(conversation, dbRows), }; }