From 8c30aa70a0a4e6ef86b7a3e464ff26f2acd770c8 Mon Sep 17 00:00:00 2001 From: Ian Hobson Date: Wed, 26 Aug 2026 15:13:48 +0200 Subject: [PATCH 1/2] feat: Speed up session listing by using a database query Listing sessions via the scan-and-repair path and then filtering by cwd in the client can take a long time. By comparison using a cwd-filtered DB query is much faster (e.g. 40ms vs ~30s on my machine with a few hundred sessions). --- src/CodexAcpClient.ts | 30 ++-------- .../CodexACPAgent/list-sessions.test.ts | 57 ++++--------------- 2 files changed, 15 insertions(+), 72 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 68b74e00..b95b5ddd 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -1083,14 +1083,7 @@ export class CodexAcpClient { "appServer", "unknown", ]; - const requestedCwd = request.cwd?.trim() ?? null; - const filterByCwd = (thread: Thread): boolean => { - if (!requestedCwd) return true; - if (isAbsolutePathLike(requestedCwd)) { - return arePathsEqual(thread.cwd, requestedCwd); - } - return arePathBasenamesEqual(thread.cwd, requestedCwd); - }; + const requestedCwd = request.cwd?.trim() || null; const preferredProvider = this.getModelProvider(); const modelProviders = preferredProvider ? [preferredProvider] : []; @@ -1098,6 +1091,8 @@ export class CodexAcpClient { cursor: request.cursor ?? null, modelProviders: modelProviders, sourceKinds: sourceKinds, + cwd: requestedCwd, + useStateDbOnly: true, }); const mapThreadToSession = (thread: Thread) => ({ @@ -1107,25 +1102,8 @@ export class CodexAcpClient { updatedAt: new Date(thread.updatedAt * 1000).toISOString(), }); - if (listResponse.data.length === 0) { - const diagnostics = await this.runSessionListDiagnostics(); - logger.log("Session list diagnostics", diagnostics); - } - - let sessions = listResponse.data.map(mapThreadToSession); - if (requestedCwd) { - const filtered = listResponse.data - .filter(filterByCwd) - .map(mapThreadToSession); - if (filtered.length > 0 || isAbsolutePathLike(requestedCwd)) { - sessions = filtered; - } else { - logger.log("Ignoring non-absolute cwd filter for session/list", {cwd: requestedCwd}); - } - } - return { - sessions, + sessions: listResponse.data.map(mapThreadToSession), nextCursor: listResponse.nextCursor ?? null, }; } diff --git a/src/__tests__/CodexACPAgent/list-sessions.test.ts b/src/__tests__/CodexACPAgent/list-sessions.test.ts index 1c6f9573..bb9bf196 100644 --- a/src/__tests__/CodexACPAgent/list-sessions.test.ts +++ b/src/__tests__/CodexACPAgent/list-sessions.test.ts @@ -37,34 +37,8 @@ describe("CodexACPAgent - list sessions", () => { name: null, turns: [], }; - const threadB: Thread = { - id: "sess-2", - sessionId: "sess-2", - parentThreadId: null, - threadSource: null, - forkedFromId: null, - preview: "Other session", - ephemeral: false, - modelProvider: "openai", - createdAt: 300, - updatedAt: 400, - recencyAt: null, - status: { type: "idle" }, - path: null, - cwd: "/repo/other", - cliVersion: "0.0.0", - section: null, - sectionEnteredAt: null, - source: "cli", - agentNickname: null, - agentRole: null, - gitInfo: null, - name: null, - turns: [], - }; - codexAppServerClient.threadList = vi.fn().mockResolvedValue({ - data: [threadA, threadB], + data: [threadA], nextCursor: "next-cursor", }); codexAppServerClient.threadLoadedList = vi.fn().mockResolvedValue({ @@ -87,13 +61,15 @@ describe("CodexACPAgent - list sessions", () => { "appServer", "unknown", ], + cwd: "/repo/project", + useStateDbOnly: true, })); await expect(JSON.stringify(response, null, 2)).toMatchFileSnapshot( "data/list-sessions.json" ); }); - it("normalizes Windows cwd filters before comparing absolute paths", async () => { + it("forwards absolute Windows cwd filters to the app server", async () => { const fixture = createCodexMockTestFixture(); const codexAcpAgent = fixture.getCodexAcpAgent(); const codexAcpClient = fixture.getCodexAcpClient(); @@ -115,7 +91,7 @@ describe("CodexACPAgent - list sessions", () => { recencyAt: null, status: { type: "idle" }, path: null, - cwd: "D:\\workspace\\sample-project\\", + cwd: "d:/workspace/sample-project", cliVersion: "0.0.0", section: null, sectionEnteredAt: null, @@ -126,16 +102,8 @@ describe("CodexACPAgent - list sessions", () => { name: null, turns: [], }; - const otherThread: Thread = { - ...matchingThread, - id: "sess-other", - sessionId: "sess-other", - preview: "Other session", - cwd: "D:\\workspace\\other-project", - }; - codexAppServerClient.threadList = vi.fn().mockResolvedValue({ - data: [matchingThread, otherThread], + data: [matchingThread], nextCursor: null, }); @@ -146,17 +114,14 @@ describe("CodexACPAgent - list sessions", () => { expect(response.sessions).toEqual([{ sessionId: "sess-win", - cwd: "D:\\workspace\\sample-project\\", + cwd: "d:/workspace/sample-project", title: "Windows session", updatedAt: "1970-01-01T00:03:20.000Z", }]); - - const basenameResponse = await codexAcpAgent.listSessions({ - cwd: "sample-project", - cursor: null, - }); - - expect(basenameResponse.sessions.map(session => session.sessionId)).toEqual(["sess-win"]); + expect(codexAppServerClient.threadList).toHaveBeenCalledWith(expect.objectContaining({ + cwd: "d:/workspace/sample-project", + useStateDbOnly: true, + })); }); it("should prefer the explicit thread name as the session title", async () => { From ea565cc9c1c4a87a56da5db6173acd5c7986fbb4 Mon Sep 17 00:00:00 2001 From: Ian Hobson Date: Fri, 28 Aug 2026 13:37:42 +0200 Subject: [PATCH 2/2] test: Add a test for cwd-filtered pagination --- .../CodexACPAgent/list-sessions.test.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/__tests__/CodexACPAgent/list-sessions.test.ts b/src/__tests__/CodexACPAgent/list-sessions.test.ts index bb9bf196..54704fed 100644 --- a/src/__tests__/CodexACPAgent/list-sessions.test.ts +++ b/src/__tests__/CodexACPAgent/list-sessions.test.ts @@ -69,6 +69,85 @@ describe("CodexACPAgent - list sessions", () => { ); }); + it("preserves cwd filtering across pagination", async () => { + const fixture = createCodexMockTestFixture(); + const codexAcpAgent = fixture.getCodexAcpAgent(); + const codexAcpClient = fixture.getCodexAcpClient(); + const codexAppServerClient = fixture.getCodexAppServerClient(); + const cwd = "/repo/project"; + + codexAcpClient.authRequired = vi.fn().mockResolvedValue(false); + + const makeThread = (id: string, updatedAt: number, threadCwd = cwd): Thread => ({ + id, + sessionId: id, + parentThreadId: null, + threadSource: null, + forkedFromId: null, + preview: `${id} session`, + ephemeral: false, + modelProvider: "openai", + createdAt: updatedAt - 100, + updatedAt, + recencyAt: null, + status: { type: "idle" }, + path: null, + cwd: threadCwd, + cliVersion: "0.0.0", + section: null, + sectionEnteredAt: null, + source: "cli", + agentNickname: null, + agentRole: null, + gitInfo: null, + name: null, + turns: [], + }); + + codexAppServerClient.threadList = vi.fn().mockImplementation(({ cursor, cwd: requestedCwd }) => { + if (requestedCwd !== cwd) { + return { + data: [makeThread("unrelated", 400, "/repo/other")], + nextCursor: "global-page-2", + }; + } + if (cursor === null) { + return { data: [makeThread("page-1", 300)], nextCursor: "page-2" }; + } + if (cursor === "page-2") { + return { data: [makeThread("page-2", 200)], nextCursor: null }; + } + throw new Error(`Unexpected project cursor: ${cursor}`); + }); + + const firstResponse = await codexAcpAgent.listSessions({ cwd, cursor: null }); + const secondResponse = await codexAcpAgent.listSessions({ + cwd, + cursor: firstResponse.nextCursor ?? null, + }); + + expect(firstResponse.sessions).toEqual([expect.objectContaining({ + sessionId: "page-1", + cwd, + })]); + expect(firstResponse.nextCursor).toBe("page-2"); + expect(secondResponse.sessions).toEqual([expect.objectContaining({ + sessionId: "page-2", + cwd, + })]); + expect(secondResponse.nextCursor).toBeNull(); + expect(codexAppServerClient.threadList).toHaveBeenNthCalledWith(1, expect.objectContaining({ + cwd, + cursor: null, + useStateDbOnly: true, + })); + expect(codexAppServerClient.threadList).toHaveBeenNthCalledWith(2, expect.objectContaining({ + cwd, + cursor: "page-2", + useStateDbOnly: true, + })); + }); + it("forwards absolute Windows cwd filters to the app server", async () => { const fixture = createCodexMockTestFixture(); const codexAcpAgent = fixture.getCodexAcpAgent();