diff --git a/packages/cli/src/modules/agents/repositories/agent-execution-thread.repository.ts b/packages/cli/src/modules/agents/repositories/agent-execution-thread.repository.ts index 45a332336f4..e071c479b3d 100644 --- a/packages/cli/src/modules/agents/repositories/agent-execution-thread.repository.ts +++ b/packages/cli/src/modules/agents/repositories/agent-execution-thread.repository.ts @@ -146,6 +146,11 @@ export class AgentExecutionThreadRepository extends BaseRepository { + // 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 limit; if (hasMore) threads.pop(); @@ -174,17 +179,18 @@ export class AgentExecutionThreadRepository extends BaseRepository, 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, }); } diff --git a/packages/cli/test/integration/database/repositories/agent-execution.repository.test.ts b/packages/cli/test/integration/database/repositories/agent-execution.repository.test.ts index 3ffa0d40f5a..ff0c6cee58f 100644 --- a/packages/cli/test/integration/database/repositories/agent-execution.repository.test.ts +++ b/packages/cli/test/integration/database/repositories/agent-execution.repository.test.ts @@ -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();