diff --git a/packages/server/src/server/agent/import-sessions.test.ts b/packages/server/src/server/agent/import-sessions.test.ts index d8125454d..8e82d7355 100644 --- a/packages/server/src/server/agent/import-sessions.test.ts +++ b/packages/server/src/server/agent/import-sessions.test.ts @@ -212,7 +212,7 @@ test("listImportableProviderSessions filters, sorts, limits, and projects import }); expect(listImportablePersistedAgents).toHaveBeenCalledWith({ - limit: 200, + limit: 2, providerFilter: new Set(["codex"]), cwd, }); diff --git a/packages/server/src/server/agent/import-sessions.ts b/packages/server/src/server/agent/import-sessions.ts index eba9172ca..7f5922a84 100644 --- a/packages/server/src/server/agent/import-sessions.ts +++ b/packages/server/src/server/agent/import-sessions.ts @@ -109,7 +109,7 @@ export async function listImportableProviderSessions( const importedHandles = await collectImportedProviderSessionHandles(agentManager, agentStorage); const descriptors = await agentManager.listImportablePersistedAgents({ - limit: 200, + limit, providerFilter, cwd: request.cwd, }); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index ffa0b66d2..6f2b84006 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -2638,12 +2638,13 @@ describe("Codex app-server provider", () => { }); describe("Codex persisted sessions", () => { - test("listPersistedAgents returns only sessions whose cwd matches the requested cwd", async () => { + test("listPersistedAgents uses thread list metadata without hydrating thread history", async () => { const allThreads = [ { id: "thread-a1", cwd: "/workspace/project-a", preview: "First A session", + name: "Named first A session", createdAt: 1000, updatedAt: 2000, }, @@ -2662,11 +2663,12 @@ describe("Codex persisted sessions", () => { updatedAt: 4000, }, ]; + const calls: Array<{ method: string; params?: unknown }> = []; const fakeClient = { - request: async (method: string) => { + request: async (method: string, params?: unknown) => { + calls.push({ method, params }); if (method === "thread/list") return { data: allThreads }; - if (method === "thread/read") return { thread: { turns: [] } }; return {}; }, notify: () => {}, @@ -2693,5 +2695,16 @@ describe("Codex persisted sessions", () => { expect(descriptors.map((d) => d.sessionId).sort()).toEqual(["thread-a1", "thread-a2"]); expect(descriptors.every((d) => d.cwd === "/workspace/project-a")).toBe(true); + expect(descriptors[0]).toEqual( + expect.objectContaining({ + sessionId: "thread-a1", + title: "Named first A session", + timeline: [{ type: "user_message", text: "First A session" }], + }), + ); + expect(calls).toEqual([ + { method: "initialize", params: expect.any(Object) }, + { method: "thread/list", params: { limit: 50, cwd: "/workspace/project-a" } }, + ]); }); }); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 78609f13f..d00fd6411 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -831,6 +831,11 @@ function filterCodexThreadsByCwd( return threads.filter((thread) => typeof thread.cwd === "string" && matchesCwd(thread.cwd)); } +function buildCodexThreadListTimeline(thread: Record): AgentTimelineItem[] { + const preview = typeof thread.preview === "string" ? thread.preview.trim() : ""; + return preview ? [{ type: "user_message", text: preview }] : []; +} + export function toAgentUsage(tokenUsage: unknown): AgentUsage | undefined { const usage = toObjectRecord(tokenUsage); if (!usage) return undefined; @@ -5486,58 +5491,48 @@ export class CodexAppServerAgentClient implements AgentClient { client.notify("initialized", {}); const limit = options?.limit ?? 20; - // thread/list returns the cheap `cwd` field. When the caller supplied - // a cwd hint we filter here so the per-thread `thread/read includeTurns` - // hydration below only runs for matching threads. Fetch a wider window - // when filtering since most threads will be from other cwds. + // thread/list returns the cheap `cwd` field. Fetch a wider window when + // filtering since most threads will be from other cwds, then keep the + // local realpath-aware filter for symlink-equivalent workspace paths. const listLimit = options?.cwd ? Math.max(limit, 50) : limit; - const response = toObjectRecord(await client.request("thread/list", { limit: listLimit })); - const allThreads = Array.isArray(response?.data) ? response.data.filter(isRecord) : []; - const threads = filterCodexThreadsByCwd(allThreads, options?.cwd); - const descriptors: PersistedAgentDescriptor[] = await Promise.all( - threads.slice(0, limit).map(async (thread) => { - const threadId = typeof thread.id === "string" ? thread.id : ""; - const cwd = typeof thread.cwd === "string" ? thread.cwd : process.cwd(); - const title = typeof thread.preview === "string" ? thread.preview : null; - let timeline: PersistedTimelineEntry[] = []; - - try { - timeline = await loadCodexThreadHistoryTimeline({ - threadId, - cwd, - requestThread: (threadIdToRead) => { - return readCodexThread(client, threadIdToRead); - }, - }); - } catch { - timeline = []; - } - - return { - provider: CODEX_PROVIDER, - sessionId: threadId, - cwd, - title, - lastActivityAt: new Date( - ((typeof thread.updatedAt === "number" ? thread.updatedAt : undefined) ?? - (typeof thread.createdAt === "number" ? thread.createdAt : undefined) ?? - 0) * 1000, - ), - persistence: { - provider: CODEX_PROVIDER, - sessionId: threadId, - nativeHandle: threadId, - metadata: { - provider: CODEX_PROVIDER, - cwd, - title, - threadId, - }, - }, - timeline: timeline.map((entry) => entry.item), - }; + const response = toObjectRecord( + await client.request("thread/list", { + limit: listLimit, + ...(options?.cwd ? { cwd: options.cwd } : {}), }), ); + const allThreads = Array.isArray(response?.data) ? response.data.filter(isRecord) : []; + const threads = filterCodexThreadsByCwd(allThreads, options?.cwd); + const descriptors: PersistedAgentDescriptor[] = threads.slice(0, limit).map((thread) => { + const threadId = typeof thread.id === "string" ? thread.id : ""; + const cwd = typeof thread.cwd === "string" ? thread.cwd : process.cwd(); + const preview = typeof thread.preview === "string" ? thread.preview : null; + const title = typeof thread.name === "string" && thread.name.trim() ? thread.name : preview; + + return { + provider: CODEX_PROVIDER, + sessionId: threadId, + cwd, + title, + lastActivityAt: new Date( + ((typeof thread.updatedAt === "number" ? thread.updatedAt : undefined) ?? + (typeof thread.createdAt === "number" ? thread.createdAt : undefined) ?? + 0) * 1000, + ), + persistence: { + provider: CODEX_PROVIDER, + sessionId: threadId, + nativeHandle: threadId, + metadata: { + provider: CODEX_PROVIDER, + cwd, + title, + threadId, + }, + }, + timeline: buildCodexThreadListTimeline(thread), + }; + }); return descriptors; } finally {