diff --git a/assistant/src/daemon/handlers/sessions.ts b/assistant/src/daemon/handlers/sessions.ts index 58bb59f52f6..98be1e24308 100644 --- a/assistant/src/daemon/handlers/sessions.ts +++ b/assistant/src/daemon/handlers/sessions.ts @@ -740,10 +740,12 @@ export function handleHistoryRequest( // Apply text truncation when maxTextChars is set let wasTruncated = false; + let textWasTruncated = false; let text = m.text; if (msg.maxTextChars !== undefined && text.length > msg.maxTextChars) { text = text.slice(0, msg.maxTextChars) + ' \u2026 [truncated]'; wasTruncated = true; + textWasTruncated = true; } // Apply tool result truncation when maxToolResultChars is set @@ -764,8 +766,8 @@ export function handleHistoryRequest( timestamp: m.timestamp, ...(truncatedToolCalls.length > 0 ? { toolCalls: truncatedToolCalls, toolCallsBeforeText: m.toolCallsBeforeText } : {}), ...(attachments ? { attachments } : {}), - ...(!wasTruncated && m.textSegments.length > 0 ? { textSegments: m.textSegments } : {}), - ...(!wasTruncated && m.contentOrder.length > 0 ? { contentOrder: m.contentOrder } : {}), + ...(!textWasTruncated && m.textSegments.length > 0 ? { textSegments: m.textSegments } : {}), + ...(!textWasTruncated && m.contentOrder.length > 0 ? { contentOrder: m.contentOrder } : {}), ...(filteredSurfaces.length > 0 ? { surfaces: filteredSurfaces } : {}), ...(m.subagentNotification ? { subagentNotification: m.subagentNotification } : {}), ...(wasTruncated ? { wasTruncated: true } : {}), @@ -933,7 +935,7 @@ export function handleMessageContentRequest( // following user message rather than inline with the assistant message. // This mirrors the mergeToolResults logic used by handleHistoryRequest. if (dbMessage.role === 'assistant' && mergedToolCalls.some((tc) => tc.result === undefined)) { - const nextMsg = conversationStore.getNextMessage(msg.sessionId, dbMessage.createdAt); + const nextMsg = conversationStore.getNextMessage(msg.sessionId, dbMessage.createdAt, dbMessage.id); if (nextMsg && nextMsg.role === 'user') { try { const nextContent = JSON.parse(nextMsg.content); diff --git a/assistant/src/memory/conversation-store.ts b/assistant/src/memory/conversation-store.ts index cbb17b3b2a4..ece69f8e6e8 100644 --- a/assistant/src/memory/conversation-store.ts +++ b/assistant/src/memory/conversation-store.ts @@ -1,4 +1,4 @@ -import { and, asc, count, desc, eq, gt, inArray, isNull, lt, lte, ne, sql } from 'drizzle-orm'; +import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, lte, ne, sql } from 'drizzle-orm'; import { v4 as uuid } from 'uuid'; import { z } from 'zod'; @@ -339,17 +339,20 @@ export function getMessageById(messageId: string, conversationId?: string): Mess } /** - * Get the next message in a conversation after a given message (by timestamp). - * Used for legacy tool_result merging in the rehydrate endpoint. + * Get the next message in a conversation after a given message. + * Uses gte + ne(id) instead of gt on timestamp so that messages sharing the + * same millisecond (common in legacy conversations where an assistant turn and + * the following user tool_result are saved in the same tick) are not skipped. */ -export function getNextMessage(conversationId: string, afterTimestamp: number): MessageRow | null { +export function getNextMessage(conversationId: string, afterTimestamp: number, excludeMessageId: string): MessageRow | null { const db = getDb(); const row = db .select() .from(messages) .where(and( eq(messages.conversationId, conversationId), - gt(messages.createdAt, afterTimestamp), + gte(messages.createdAt, afterTimestamp), + ne(messages.id, excludeMessageId), )) .orderBy(asc(messages.createdAt)) .limit(1)