From ea66d19c5e7e13258bd21cf4bea517604c84efc6 Mon Sep 17 00:00:00 2001 From: "n8n-cat-bot[bot]" <283985454+n8n-cat-bot[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:51:21 +0000 Subject: [PATCH] chore: Centralize conversation-history message ordering (#39599) Co-authored-by: n8n-cat-bot[bot] --- ...ion-history.repository.integration.test.ts | 34 +++++++++++++++++++ ...ance-ai-conversation-history.repository.ts | 18 ++++------ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/modules/instance-ai/repositories/__tests__/instance-ai-conversation-history.repository.integration.test.ts b/packages/cli/src/modules/instance-ai/repositories/__tests__/instance-ai-conversation-history.repository.integration.test.ts index 13c36153519..1574700b7de 100644 --- a/packages/cli/src/modules/instance-ai/repositories/__tests__/instance-ai-conversation-history.repository.integration.test.ts +++ b/packages/cli/src/modules/instance-ai/repositories/__tests__/instance-ai-conversation-history.repository.integration.test.ts @@ -427,6 +427,40 @@ describe('InstanceAiConversationHistoryRepository', () => { }); }); + it('uses the same id tie-break for search, opening messages, and windows', async () => { + const threadId = await createThread({ withOpeningMessage: false }); + const createdAt = at(9000); + for (const id of ['msg-bbb', 'msg-ccc', 'msg-aaa']) { + await createMessage({ + threadId, + id, + role: 'user', + content: userContent('deploy'), + createdAt, + }); + } + + const matches = await repository.findSearchMatchRows([threadId], 'deploy', 2); + const first = await repository.findFirstUserMessages([threadId]); + const tail = await repository.getConversationWindow({ + threadId, + before: 2, + after: 0, + project: (row) => row, + }); + const head = await repository.getConversationWindow({ + threadId, + before: 0, + after: 2, + project: (row) => row, + }); + + expect(matches.get(threadId)?.map((row) => row.id)).toEqual(['msg-ccc', 'msg-bbb']); + expect(first.get(threadId)?.id).toBe('msg-aaa'); + expect(tail.rows.map((row) => row.id)).toEqual(['msg-bbb', 'msg-ccc']); + expect(head.rows.map((row) => row.id)).toEqual(['msg-aaa', 'msg-bbb']); + }); + describe('findMessageInThread', () => { it('finds a conversation row and ignores rows of other threads', async () => { const threadId = await createThread({ title: 'Anchors' }); diff --git a/packages/cli/src/modules/instance-ai/repositories/instance-ai-conversation-history.repository.ts b/packages/cli/src/modules/instance-ai/repositories/instance-ai-conversation-history.repository.ts index e0090acd3aa..9834f8ca9b2 100644 --- a/packages/cli/src/modules/instance-ai/repositories/instance-ai-conversation-history.repository.ts +++ b/packages/cli/src/modules/instance-ai/repositories/instance-ai-conversation-history.repository.ts @@ -123,14 +123,12 @@ export class InstanceAiConversationHistoryRepository { if (maxRowsPerThread <= 0) return byThread; for (const threadId of threadIds) { - const rows = await this.messages() + const rows = await this.orderMessages(this.messages(), 'DESC') .where('m.threadId = :threadId', { threadId }) .andWhere(buildMessageMatchCondition('m'), { pattern: buildSearchLikePattern(query), askUserMarker: ASK_USER_CONTENT_MARKER, }) - .orderBy('m.createdAt', 'DESC') - .addOrderBy('m.id', 'DESC') .take(maxRowsPerThread) .getMany(); byThread.set(threadId, rows); @@ -146,11 +144,9 @@ export class InstanceAiConversationHistoryRepository { async findFirstUserMessages(threadIds: string[]): Promise> { const byThread = new Map(); for (const threadId of threadIds) { - const row = await this.messages() + const row = await this.orderMessages(this.messages(), 'ASC') .where('m.threadId = :threadId', { threadId }) .andWhere("m.role = 'user'") - .orderBy('m.createdAt', 'ASC') - .addOrderBy('m.id', 'ASC') .take(1) .getOne(); if (row) byThread.set(threadId, row); @@ -309,11 +305,7 @@ export class InstanceAiConversationHistoryRepository { const sqlOrder = direction === 'older' ? 'DESC' : 'ASC'; const fetchLimit = limit * WINDOW_OVERFETCH_FACTOR + 1; - const fetched = await qb - .orderBy('m.createdAt', sqlOrder) - .addOrderBy('m.id', sqlOrder) - .take(fetchLimit) - .getMany(); + const fetched = await this.orderMessages(qb, sqlOrder).take(fetchLimit).getMany(); const rows: T[] = []; let moreVisibleFetched = false; @@ -341,6 +333,10 @@ export class InstanceAiConversationHistoryRepository { .andWhere(buildVisibleRowCondition('m'), VISIBLE_ROW_MARKERS); } + private orderMessages(qb: SelectQueryBuilder, order: 'ASC' | 'DESC') { + return qb.orderBy('m.createdAt', order).addOrderBy('m.id', order); + } + private threads(): SelectQueryBuilder { return this.dataSource.createQueryBuilder(InstanceAiThread, 't'); }