Files
memind/conversation-repair.mjs

132 lines
4.8 KiB
JavaScript

/**
* 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 filterUserVisibleConversation(messages) {
if (!Array.isArray(messages)) return [];
return messages.filter((message) => message?.metadata?.userVisible !== false);
}
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),
};
}