fix(core): Correct Agent session pagination on SQLite (#39802)

This commit is contained in:
Michael Drury 2026-09-29 11:57:20 +00:00 committed by GitHub
parent 8d4111dff8
commit 1882cff76b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 4 deletions

View File

@ -146,6 +146,11 @@ export class AgentExecutionThreadRepository extends BaseRepository<AgentExecutio
cursor?: string,
filters: AgentSessionQueryFilters = {},
): Promise<AgentExecutionThreadPage> {
// SQLite timestamps can omit milliseconds, so compare them in one format.
const updatedAt =
this.manager.connection.options.type === 'postgres'
? 'thread.updatedAt'
: "STRFTIME('%Y-%m-%d %H:%M:%f', thread.updatedAt)";
const query = this.createQueryBuilder('thread')
.where('thread.projectId = :projectId', { projectId })
.andWhere('thread.agentId = :agentId', { agentId })
@ -157,9 +162,9 @@ export class AgentExecutionThreadRepository extends BaseRepository<AgentExecutio
.take(limit + 1);
if (cursor) {
query.andWhere('thread.updatedAt < :cursor', { cursor: new Date(cursor) });
query.andWhere(`${updatedAt} < :cursor`, { cursor: new Date(cursor) });
}
this.applyListFilters(query, filters, userId);
this.applyListFilters(query, filters, userId, updatedAt);
const threads = await query.getMany();
const hasMore = threads.length > limit;
if (hasMore) threads.pop();
@ -174,17 +179,18 @@ export class AgentExecutionThreadRepository extends BaseRepository<AgentExecutio
query: SelectQueryBuilder<AgentExecutionThread>,
filters: AgentSessionQueryFilters,
userId: string,
updatedAt: string,
) {
if (filters.scope === 'mine') {
query.andWhere('thread.ownerId = :userId', { userId });
}
if (filters.updatedAfter) {
query.andWhere('thread.updatedAt >= :updatedAfter', {
query.andWhere(`${updatedAt} >= :updatedAfter`, {
updatedAfter: filters.updatedAfter,
});
}
if (filters.updatedBefore) {
query.andWhere('thread.updatedAt <= :updatedBefore', {
query.andWhere(`${updatedAt} <= :updatedBefore`, {
updatedBefore: filters.updatedBefore,
});
}

View File

@ -2304,6 +2304,56 @@ describe('AgentExecutionRepository', () => {
return await repository.save(execution);
};
it('paginates sessions with whole-second and millisecond timestamps', async () => {
const newest = await createThread();
const middle = await createThread();
const oldest = await createThread();
for (const [id, timestamp] of [
[newest.id, '2026-01-03 00:00:01'],
[middle.id, '2026-01-03 00:00:00.500'],
[oldest.id, '2026-01-03 00:00:00'],
]) {
await threadRepo
.createQueryBuilder()
.update()
.set({ updatedAt: () => `'${timestamp}'` })
.where('id = :id', { id })
.execute();
}
const first = await threadRepo.findByProjectIdPaginated(projectId, agentId, viewerId, 1);
const second = await threadRepo.findByProjectIdPaginated(
projectId,
agentId,
viewerId,
1,
first.nextCursor ?? undefined,
);
const third = await threadRepo.findByProjectIdPaginated(
projectId,
agentId,
viewerId,
1,
second.nextCursor ?? undefined,
);
const after = await threadRepo.findByProjectIdPaginated(
projectId,
agentId,
viewerId,
10,
undefined,
{
updatedAfter: new Date('2026-01-03T00:00:00Z'),
},
);
expect(first.threads.map(({ id }) => id)).toEqual([newest.id]);
expect(second.threads.map(({ id }) => id)).toEqual([middle.id]);
expect(third.threads.map(({ id }) => id)).toEqual([oldest.id]);
expect(third.nextCursor).toBeNull();
expect(after.threads.map(({ id }) => id)).toEqual([newest.id, middle.id, oldest.id]);
});
it('filters private sessions before pagination and preserves their owner on reuse', async () => {
const owner = await createMember();
const other = await createAdmin();