godcrm/backend/utils/bubblePagination.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

127 lines
3.7 KiB
JavaScript

/**
* Bubble-based pagination utility for chat messages.
*
* A "bubble" represents what the user sees in the UI:
* - 1 user message (role='user') = 1 bubble
* - A contiguous sequence of assistant messages (thinking, tool_call,
* tool_result, text) between two user messages = 1 bubble
* - Standalone assistant text = 1 bubble
* - System messages (role='system') = don't count
*/
export const BUBBLE_PAGE_SIZE = 50;
/**
* Count how many bubbles are in a message array.
* Messages can be in any order (ASC or DESC) — the function normalises to ASC
* internally so the grouping logic is consistent.
*
* @param {Array} messages - Array of message objects with at least a `role` field.
* @returns {number} The number of visible bubbles.
*/
export function countBubbles(messages) {
if (!messages || messages.length === 0) return 0;
let bubbles = 0;
let inAssistantGroup = false;
for (const msg of messages) {
const role = msg.role;
// System messages are invisible — skip
if (role === 'system') continue;
if (role === 'user') {
// If we were accumulating an assistant group, close it first
if (inAssistantGroup) {
inAssistantGroup = false;
}
bubbles += 1;
} else {
// assistant / tool_call / tool_result / thinking — all part of assistant group
if (!inAssistantGroup) {
bubbles += 1;
inAssistantGroup = true;
}
// else: still inside the same assistant bubble, don't count again
}
}
return bubbles;
}
/**
* Count bubbles in a message array and return pagination info.
*
* @param {Array} messages - Raw messages from DB, ordered by created_at DESC, id DESC (newest first).
* @param {number} bubbleLimit - How many bubbles to return (default 50).
* @returns {{
* messages: Array,
* hasMore: boolean,
* nextCursor: number|null,
* bubbleCount: number
* }}
*/
export function paginateByBubbles(messages, bubbleLimit = BUBBLE_PAGE_SIZE) {
if (!messages || messages.length === 0) {
return { messages: [], hasMore: false, nextCursor: null, bubbleCount: 0 };
}
// Messages arrive DESC (newest first).
// We walk from index 0 (newest) towards the end (oldest), collecting
// bubbles until we reach bubbleLimit.
let bubbleCount = 0;
let inAssistantGroup = false;
let cutoffIndex = messages.length; // will hold the exclusive end of collected messages
for (let i = 0; i < messages.length; i++) {
const role = messages[i].role;
// System messages are invisible — always include but don't count
if (role === 'system') continue;
if (role === 'user') {
// Close any open assistant group first
if (inAssistantGroup) {
inAssistantGroup = false;
}
// This user message is a new bubble
if (bubbleCount >= bubbleLimit) {
// We've already collected enough bubbles; stop here
cutoffIndex = i;
break;
}
bubbleCount += 1;
} else {
// assistant / tool / thinking
if (!inAssistantGroup) {
// Starting a new assistant group — that's a new bubble
if (bubbleCount >= bubbleLimit) {
cutoffIndex = i;
break;
}
bubbleCount += 1;
inAssistantGroup = true;
}
// If already in a group, it's still the same bubble
}
}
const collected = messages.slice(0, cutoffIndex);
const hasMore = cutoffIndex < messages.length;
// Reverse to ASC for display (chronological order)
collected.reverse();
// nextCursor = ID of the oldest message we're returning (first after reverse)
const nextCursor = hasMore && collected.length > 0 ? collected[0].id : null;
return {
messages: collected,
hasMore,
nextCursor,
bubbleCount,
};
}