From bf8f2510c852e8e0dfce08c0336eefdb4da99aad Mon Sep 17 00:00:00 2001 From: qer Date: Sun, 14 Jun 2026 14:52:12 +0800 Subject: [PATCH] fix(acp): make ACP/Kimi sessions importable again (#1510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ACP-based providers (Kimi and other custom ACP agents, Copilot, Cursor) were absent from the import session picker. Two causes, both in the import path: 1. Never queried. The import refactor (51d1d007) gated provider listing on capabilities.supportsSessionListing and set it for claude/codex/opencode/pi but not for ACP providers, so agent-manager skipped every ACP provider in listImportableSessions — the picker showed 'no sessions' even though listImportableSessions is fully implemented. Set supportsSessionListing on DEFAULT_ACP_CAPABILITIES and COPILOT_CAPABILITIES. Agents that don't support session/list still return an empty list at the runtime probe, so this only controls whether the daemon queries them. 2. Ignored cwd. listImportableSessions never forwarded the requested cwd to the agent's session/list call, so agents returned globally-recent sessions that the import limit could truncate before the current directory's sessions were reached. Forward options.cwd as the session/list cwd filter (with the pagination cursor) so the agent filters by directory at the source. --- .../server/agent/providers/acp-agent.test.ts | 107 ++++++++++++++++++ .../src/server/agent/providers/acp-agent.ts | 12 +- .../agent/providers/copilot-acp-agent.ts | 1 + 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 8b42e12c4..cf5e58036 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -31,12 +31,14 @@ import { import { COPILOT_ALLOW_ALL_MODE_ID, COPILOT_MODES, + CopilotACPAgentClient, beforeCopilotModeWriter, transformCopilotConfigOptions, transformCopilotModeId, transformCopilotSessionResponse, writeCopilotProviderMode, } from "./copilot-acp-agent.js"; +import { GenericACPAgentClient } from "./generic-acp-agent.js"; import { transformPiModels } from "./pi/agent.js"; import type { AgentStreamEvent } from "../agent-sdk-types.js"; import { createTestLogger } from "../../../test-utils/test-logger.js"; @@ -1362,6 +1364,111 @@ describe("ACPAgentClient listModes", () => { }); }); +describe("ACPAgentClient listImportableSessions", () => { + function makeClient(args: { listSessions: ReturnType; supportsList?: boolean }) { + class TestACPAgentClient extends ACPAgentClient { + protected override async spawnProcess(): Promise { + return { + child: { kill: vi.fn(), exitCode: 0, signalCode: null, once: vi.fn() }, + connection: { listSessions: args.listSessions }, + initialize: { + agentCapabilities: + args.supportsList === false ? {} : { sessionCapabilities: { list: {} } }, + }, + } as unknown as SpawnedACPProcess; + } + + protected override async closeProbe(): Promise {} + } + + return new TestACPAgentClient({ + provider: "kimi", + logger: createTestLogger(), + defaultCommand: ["kimi", "acp"], + defaultModes: [], + }); + } + + test("forwards the requested cwd to session/list so the agent filters by directory", async () => { + const listSessions = vi.fn().mockResolvedValue({ + sessions: [ + { + sessionId: "session-1", + cwd: "/Users/moonshot", + title: "细致查看一下本仓库内容", + updatedAt: "2026-06-13T00:00:00.000Z", + }, + ], + nextCursor: null, + }); + + const client = makeClient({ listSessions }); + const result = await client.listImportableSessions({ cwd: "/Users/moonshot", limit: 20 }); + + expect(listSessions).toHaveBeenCalledWith({ cwd: "/Users/moonshot" }); + expect(result).toEqual([ + { + providerHandleId: "session-1", + cwd: "/Users/moonshot", + title: "细致查看一下本仓库内容", + firstPromptPreview: null, + lastPromptPreview: null, + lastActivityAt: new Date("2026-06-13T00:00:00.000Z"), + }, + ]); + }); + + test("omits cwd from session/list when none is requested", async () => { + const listSessions = vi.fn().mockResolvedValue({ sessions: [], nextCursor: null }); + const client = makeClient({ listSessions }); + + await client.listImportableSessions({ limit: 20 }); + + expect(listSessions).toHaveBeenCalledWith({}); + }); + + test("forwards cwd alongside the pagination cursor across pages", async () => { + const listSessions = vi + .fn() + .mockResolvedValueOnce({ + sessions: [{ sessionId: "s1", cwd: "/Users/moonshot", title: null, updatedAt: null }], + nextCursor: "cursor-2", + }) + .mockResolvedValueOnce({ + sessions: [{ sessionId: "s2", cwd: "/Users/moonshot", title: null, updatedAt: null }], + nextCursor: null, + }); + + const client = makeClient({ listSessions }); + await client.listImportableSessions({ cwd: "/Users/moonshot" }); + + expect(listSessions).toHaveBeenNthCalledWith(1, { cwd: "/Users/moonshot" }); + expect(listSessions).toHaveBeenNthCalledWith(2, { + cursor: "cursor-2", + cwd: "/Users/moonshot", + }); + }); +}); + +describe("ACP providers advertise session listing", () => { + // The daemon's agent-manager only queries providers whose + // capabilities.supportsSessionListing is true. Without it, ACP providers + // (Kimi and other custom ACP agents, Copilot) are skipped and import shows + // nothing even though listImportableSessions is implemented. + test("generic ACP clients (e.g. Kimi) report supportsSessionListing", () => { + const client = new GenericACPAgentClient({ + logger: createTestLogger(), + command: ["kimi", "acp"], + }); + expect(client.capabilities.supportsSessionListing).toBe(true); + }); + + test("Copilot ACP client reports supportsSessionListing", () => { + const client = new CopilotACPAgentClient({ logger: createTestLogger() }); + expect(client.capabilities.supportsSessionListing).toBe(true); + }); +}); + describe("transformPiModels", () => { test("keeps slash-free labels unchanged", () => { expect( diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index da8a4783f..e22281f5b 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -186,6 +186,10 @@ function resolveTerminalCommand( export const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: true, + // ACP agents can list prior sessions via `session/list`. The runtime probe in + // listImportableSessions returns nothing for agents that don't advertise the + // capability, so enabling this here only makes the daemon query them. + supportsSessionListing: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true, @@ -758,7 +762,13 @@ export class ACPAgentClient implements AgentClient { let cursor: string | null | undefined; for (;;) { const page: ListSessionsResponse = await this.runACPRequest(() => - probe.connection.listSessions(cursor ? { cursor } : {}), + probe.connection.listSessions({ + ...(cursor ? { cursor } : {}), + // Filter by working directory at the source. Without this the agent + // returns globally-recent sessions, which the `limit` below can + // truncate before the current directory's sessions are reached. + ...(options?.cwd ? { cwd: options.cwd } : {}), + }), ); for (const session of page.sessions) { sessions.push({ diff --git a/packages/server/src/server/agent/providers/copilot-acp-agent.ts b/packages/server/src/server/agent/providers/copilot-acp-agent.ts index d6a8fadf9..1d493b623 100644 --- a/packages/server/src/server/agent/providers/copilot-acp-agent.ts +++ b/packages/server/src/server/agent/providers/copilot-acp-agent.ts @@ -26,6 +26,7 @@ import { const COPILOT_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: true, + supportsSessionListing: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true,