/** * Chat Finish sync invariants (regression guard — do not simplify away). * * Context (2026-06): Finish could arrive before Goose persisted assistant turns. * Blind replace with the server snapshot wiped the live chat and exposed agent-only * prefixes (routing hints / skill prefaces) when displayText was missing. * * Required behaviour: * - merge local streamed messages with server snapshot (never blind replace) * - retry sync while server has fewer messages than the local view * - user-facing text must strip agent-only prefixes when displayText is absent * * Tests: chat-finish-sync.test.mjs, conversation-display.test.mjs * Verify: scripts/verify-chat-finish-sync.mjs (source guards) * Docs: docs/regression-guards/mindspace-publish-and-chat-finish.md */ /** @typedef {{ id?: string; role?: string; metadata?: Record; content?: unknown[] }} ChatMessage */ /** * Merge an authoritative server conversation snapshot into the currently * displayed messages without erasing what the user can already see. * * @param {ChatMessage[]} current * @param {ChatMessage[]} incoming */ export function mergeConversationSnapshot(current, incoming) { if (!Array.isArray(incoming) || incoming.length === 0) { return Array.isArray(current) ? current : []; } const base = Array.isArray(current) ? current : []; const incomingIds = new Set(incoming.map((message) => message?.id).filter(Boolean)); const localOnly = base.filter((message) => message?.id && !incomingIds.has(message.id)); return localOnly.length ? [...incoming, ...localOnly] : incoming; } /** * Finish-time sync: server may lag behind the streamed UI — keep the local tail. * * @param {ChatMessage[]} localMessages * @param {ChatMessage[]} serverMessages */ export function mergeSessionMessagesAfterFinish(localMessages, serverMessages) { return mergeConversationSnapshot(localMessages, serverMessages); }