chore: Centralize conversation-history message ordering (#39599)

Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
This commit is contained in:
n8n-cat-bot[bot] 2026-09-25 11:51:21 +00:00 committed by GitHub
parent 29b9421518
commit ea66d19c5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 41 additions and 11 deletions

View File

@ -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' });

View File

@ -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<Map<string, InstanceAiMessage>> {
const byThread = new Map<string, InstanceAiMessage>();
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<InstanceAiMessage>, order: 'ASC' | 'DESC') {
return qb.orderBy('m.createdAt', order).addOrderBy('m.id', order);
}
private threads(): SelectQueryBuilder<InstanceAiThread> {
return this.dataSource.createQueryBuilder(InstanceAiThread, 't');
}