diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 4e26e753c5..24acc43bb2 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -5381,7 +5381,39 @@ test("fetches scoped recent provider sessions", async () => { }); }); -test("imports an agent by provider handle id", async () => { +test.each([undefined, false])( + "rejects explicit import modes on hosts without support (%s)", + async (supported) => { + const transport = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_import_mode_compat", + logger: createMockLogger(), + reconnect: { enabled: false }, + transportFactory: () => transport.transport, + }); + clients.push(client); + const connecting = client.connect(); + transport.triggerOpen({ features: { importSessionMode: supported } }); + await connecting; + + const importing = client.importAgent({ + provider: "claude", + sessionId: "native-session", + modeId: "auto", + }); + void importing.catch(() => undefined); + expect(transport.sent).toHaveLength(0); + await expect(importing).rejects.toThrow( + "Update the host to select a mode when importing an agent.", + ); + }, +); + +test.each([ + ["full-access", true], + [undefined, undefined], +] as const)("imports an agent by provider handle id with mode %s", async (modeId, supported) => { const logger = createMockLogger(); const mock = createMockTransport(); @@ -5395,14 +5427,14 @@ test("imports an agent by provider handle id", async () => { clients.push(client); const connectPromise = client.connect(); - mock.triggerOpen(); + mock.triggerOpen({ features: { importSessionMode: supported } }); await connectPromise; const promise = client.importAgent({ providerId: "custom-codex", providerHandleId: "thread-1", cwd: "/tmp/repo", - modeId: "full-access", + modeId, }); expect(mock.sent).toHaveLength(1); @@ -5423,8 +5455,8 @@ test("imports an agent by provider handle id", async () => { providerId: "custom-codex", providerHandleId: "thread-1", cwd: "/tmp/repo", - modeId: "full-access", }); + expect(request.message.modeId).toBe(modeId); expect(request.message).not.toHaveProperty("sessionId"); mock.triggerMessage( diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index e027777c57..b3d0e75a71 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -3211,6 +3211,10 @@ export class DaemonClient { } async importAgent(input: ImportAgentInput): Promise { + // COMPAT(importSessionMode): added after v0.11.4, remove gate after 2027-04-16. + if (input.modeId && this.lastServerInfoMessage?.features?.importSessionMode !== true) { + throw new Error("Update the host to select a mode when importing an agent."); + } const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "import_agent_request", diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 93431cfe9e..5edd278ecb 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -3715,6 +3715,8 @@ export const ServerInfoStatusPayloadSchema = z hopliteProvider: z.boolean().optional(), // COMPAT(importSessionWorkspaceTarget): added in v0.1.110, remove gate after 2027-01-16. importSessionWorkspaceTarget: z.boolean().optional(), + // COMPAT(importSessionMode): added after v0.11.4, remove gate after 2027-04-16. + importSessionMode: z.boolean().optional(), // COMPAT(importSessionSearch): added in v0.8.0, remove gate after 2027-03-02. importSessionSearch: z.boolean().optional(), // COMPAT(forgeProviders): added in v0.2.0-beta.1. Drop the gate after diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index e833a07ce3..d282675963 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -1918,6 +1918,8 @@ export class VoiceAssistantWebSocketServer { hopliteProvider: this.providerSnapshotManager.supportsConfigRequiredProvider("hoplite"), // COMPAT(importSessionWorkspaceTarget): added in v0.1.110, remove gate after 2027-01-16. importSessionWorkspaceTarget: true, + // COMPAT(importSessionMode): added after v0.11.4, remove gate after 2027-04-16. + importSessionMode: true, // COMPAT(importSessionSearch): added in v0.7.3, remove gate after 2027-03-02. importSessionSearch: true, // COMPAT(forgeProviders): added in v0.2.0-beta.1. Drop the gate after