diff --git a/packages/cli/src/cli-surface.test.ts b/packages/cli/src/cli-surface.test.ts index 676f79234d..eb5e5b5272 100644 --- a/packages/cli/src/cli-surface.test.ts +++ b/packages/cli/src/cli-surface.test.ts @@ -46,6 +46,11 @@ describe("canonical CLI surface", () => { expect(help).toContain("--forge "); }); + it("offers provider mode selection when importing an agent", () => { + const importCommand = createCli().commands.find((command) => command.name() === "import"); + expect(importCommand?.helpInformation()).toContain("--mode "); + }); + it("uses background for execution and reserves detach for ownership", () => { const run = createCli().commands.find((command) => command.name() === "run"); expect(run?.helpInformation()).toContain("--background"); diff --git a/packages/cli/src/commands/agent/import.test.ts b/packages/cli/src/commands/agent/import.test.ts index 761b2a0f1e..5fda254bd1 100644 --- a/packages/cli/src/commands/agent/import.test.ts +++ b/packages/cli/src/commands/agent/import.test.ts @@ -57,4 +57,32 @@ describe("resolveImportCwd", () => { }); expect(result.data.provider).toBe("pi"); }); + + it("passes the requested mode to the import request", async () => { + importAgent.mockResolvedValueOnce({ + id: "agent-2", + status: "idle", + provider: "claude", + cwd: "/tmp/project", + title: "Imported Claude session", + }); + + await runImportCommand( + "claude-session-1", + { + daemonTarget: { kind: "endpoint", host: "example.test:12345" }, + provider: "claude", + cwd: "/tmp/project", + mode: "auto", + }, + {} as never, + ); + + expect(importAgent).toHaveBeenCalledWith({ + provider: "claude", + sessionId: "claude-session-1", + cwd: "/tmp/project", + modeId: "auto", + }); + }); }); diff --git a/packages/cli/src/commands/agent/import.ts b/packages/cli/src/commands/agent/import.ts index fa42b6860a..09ba64ba03 100644 --- a/packages/cli/src/commands/agent/import.ts +++ b/packages/cli/src/commands/agent/import.ts @@ -11,6 +11,7 @@ export function addImportOptions(cmd: Command): Command { .argument("", "Provider session/thread ID to import") .requiredOption("--provider ", "Agent provider id") .option("--cwd ", "Working directory for providers that require it") + .option("--mode ", "Provider-specific mode (e.g., plan, default, bypass)") .option( "--label ", "Add label(s) to the agent (can be used multiple times)", @@ -22,6 +23,7 @@ export function addImportOptions(cmd: Command): Command { export interface AgentImportOptions extends CommandOptions { provider?: string; cwd?: string; + mode?: string; label?: string[]; host?: string; } @@ -119,6 +121,7 @@ export async function runImportCommand( provider, sessionId, cwd, + ...(options.mode ? { modeId: options.mode } : {}), ...(Object.keys(labels).length > 0 ? { labels } : {}), }); diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index bf41e06961..c97cc85a8c 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -4717,6 +4717,7 @@ test("imports an agent by provider handle id", async () => { providerId: "custom-codex", providerHandleId: "thread-1", cwd: "/tmp/repo", + modeId: "full-access", }); expect(mock.sent).toHaveLength(1); @@ -4729,6 +4730,7 @@ test("imports an agent by provider handle id", async () => { providerHandleId?: string; sessionId?: string; cwd?: string; + modeId?: string; }; }; expect(request.message).toMatchObject({ @@ -4736,6 +4738,7 @@ test("imports an agent by provider handle id", async () => { providerId: "custom-codex", providerHandleId: "thread-1", cwd: "/tmp/repo", + modeId: "full-access", }); expect(request.message).not.toHaveProperty("sessionId"); diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 042541f5ca..40377c2042 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -192,6 +192,7 @@ interface ImportAgentInputBase { cwd?: string; workspaceId?: string; labels?: Record; + modeId?: string; } export type ImportAgentInput = @@ -2969,6 +2970,7 @@ export class DaemonClient { ...(input.cwd ? { cwd: input.cwd } : {}), ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}), ...(input.labels && Object.keys(input.labels).length > 0 ? { labels: input.labels } : {}), + ...(input.modeId ? { modeId: input.modeId } : {}), }); const status = await this.sendRequest({ diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index b8d0e56cc2..ba8302d5f4 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -1763,6 +1763,7 @@ export const ImportAgentRequestMessageSchema = z.object({ cwd: z.string().optional(), workspaceId: z.string().optional(), labels: z.record(z.string(), z.string()).optional(), + modeId: z.string().optional(), requestId: z.string(), }); diff --git a/packages/protocol/src/messages.workspaces.test.ts b/packages/protocol/src/messages.workspaces.test.ts index 3cb9b0d5bd..1538dbb63c 100644 --- a/packages/protocol/src/messages.workspaces.test.ts +++ b/packages/protocol/src/messages.workspaces.test.ts @@ -411,6 +411,7 @@ describe("workspace message schemas", () => { providerId: "custom-codex", providerHandleId: "thread-1", cwd: "/tmp/repo", + modeId: "full-access", }); const legacyRequest = SessionInboundMessageSchema.parse({ type: "import_agent_request", @@ -426,6 +427,7 @@ describe("workspace message schemas", () => { providerId: "custom-codex", providerHandleId: "thread-1", cwd: "/tmp/repo", + modeId: "full-access", }); expect(legacyRequest).toEqual({ type: "import_agent_request", diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index f4d4069d26..77c036ad5c 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -4071,6 +4071,7 @@ test("importProviderSession imports the selected session without listing and pub class ImportClient extends TestAgentClient { listCalls = 0; importInput: unknown = null; + importConfig: AgentSessionConfig | null = null; importLaunchContext: AgentLaunchContext | undefined; async listImportableSessions() { @@ -4080,10 +4081,11 @@ test("importProviderSession imports the selected session without listing and pub async importSession(input: ImportProviderSessionInput, context: ImportProviderSessionContext) { this.importInput = input; + this.importConfig = context.storedConfig; this.importLaunchContext = context.launchContext; return { session, - config: { provider: "codex" as const, cwd: workdir }, + config: context.storedConfig, persistence: { provider: "codex" as const, sessionId: input.providerHandleId, @@ -4156,10 +4158,12 @@ test("importProviderSession imports the selected session without listing and pub providerHandleId: "thread-selected", cwd: workdir, workspaceId: "ws-imported", + modeId: "full-access", }); expect(client.listCalls).toBe(0); expect(client.importInput).toEqual({ providerHandleId: "thread-selected", cwd: workdir }); + expect(client.importConfig?.modeId).toBe("full-access"); expect(client.importLaunchContext).toEqual({ agentId: imported.id, env: { @@ -4168,6 +4172,7 @@ test("importProviderSession imports the selected session without listing and pub }, }); expect(imported.lifecycle).toBe("idle"); + expect(imported.config.modeId).toBe("full-access"); expect(imported.historyPrimed).toBe(true); expect(manager.getTimeline(imported.id)).toEqual([ { type: "user_message", text: "Trace provider imports" }, @@ -8146,6 +8151,7 @@ test("unarchiveSnapshot unarchives native provider storage before clearing archi const unarchived = await manager.unarchiveSnapshot(agent.id, { workspaceId: "ws-restored", labels: { [PARENT_AGENT_ID_LABEL]: null, source: "reimport" }, + modeId: "full-access", }); const stored = await storage.get(agent.id); @@ -8156,6 +8162,7 @@ test("unarchiveSnapshot unarchives native provider storage before clearing archi expect(stored?.archivedAt).toBeNull(); expect(stored?.workspaceId).toBe("ws-restored"); expect(stored?.labels).toEqual({ retained: "yes", source: "reimport" }); + expect(stored?.config?.modeId).toBe("full-access"); }); test("unarchiveSnapshotByHandle unarchives native provider storage for the matched snapshot", async () => { diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 70b4e62dbc..b88f2abdcc 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -1357,6 +1357,7 @@ export class AgentManager { cwd: string; workspaceId: string; labels?: Record; + modeId?: string; }): Promise { return this.trackAgentRegistrationOperation(this.importProviderSessionInternal(input)); } @@ -1367,6 +1368,7 @@ export class AgentManager { cwd: string; workspaceId: string; labels?: Record; + modeId?: string; }): Promise { this.assertAcceptingAgentRegistrations(); const resolvedAgentId = validateAgentId(this.idFactory(), "importProviderSession"); @@ -1381,6 +1383,7 @@ export class AgentManager { { provider: input.provider, cwd: input.cwd, + modeId: input.modeId, }, resolvedAgentId, ); @@ -2143,7 +2146,7 @@ export class AgentManager { async unarchiveSnapshot( agentId: string, - updates?: { workspaceId?: string; labels?: AgentLabelPatch }, + updates?: { workspaceId?: string; labels?: AgentLabelPatch; modeId?: string }, ): Promise { const registry = this.requireRegistry(); const record = await registry.get(agentId); @@ -2159,6 +2162,7 @@ export class AgentManager { ...record, ...(updates?.workspaceId ? { workspaceId: updates.workspaceId } : {}), ...(updates?.labels ? { labels: applyLabelPatch(record.labels, updates.labels) } : {}), + ...(updates?.modeId ? { config: { ...record.config, modeId: updates.modeId } } : {}), archivedAt: null, updatedAt: new Date().toISOString(), }); diff --git a/packages/server/src/server/agent/agent-prompt.ts b/packages/server/src/server/agent/agent-prompt.ts index 8e222304e9..cb78ba230c 100644 --- a/packages/server/src/server/agent/agent-prompt.ts +++ b/packages/server/src/server/agent/agent-prompt.ts @@ -198,7 +198,7 @@ export async function unarchiveAgentState( _agentStorage: AgentStorage, agentManager: AgentUnarchiveController, agentId: string, - updates?: { workspaceId?: string; labels?: Record }, + updates?: { workspaceId?: string; labels?: Record; modeId?: string }, ): Promise { const unarchived = await agentManager.unarchiveSnapshot(agentId, updates); if (!unarchived) return false; diff --git a/packages/server/src/server/agent/import-sessions.test.ts b/packages/server/src/server/agent/import-sessions.test.ts index 9190e6942b..c1e60d5515 100644 --- a/packages/server/src/server/agent/import-sessions.test.ts +++ b/packages/server/src/server/agent/import-sessions.test.ts @@ -558,11 +558,13 @@ test("normalizeImportAgentRequest accepts new and legacy import handle shapes", requestId: "new-shape", providerId: "custom-codex", providerHandleId: "thread-1", + modeId: "full-access", }), ).toEqual({ requestId: "new-shape", provider: "custom-codex", providerHandleId: "thread-1", + modeId: "full-access", }); expect( @@ -633,7 +635,11 @@ class ProviderImportHarness { }, unarchiveSnapshot: async ( agentId: string, - updates?: { workspaceId?: string; labels?: Record }, + updates?: { + workspaceId?: string; + labels?: Record; + modeId?: string; + }, ) => { if (this.unarchiveWait) { await this.unarchiveWait; @@ -654,6 +660,7 @@ class ProviderImportHarness { ...record, workspaceId: updates?.workspaceId ?? record.workspaceId, labels, + config: updates?.modeId ? { ...record.config, modeId: updates.modeId } : record.config, archivedAt: null, }); return true; @@ -735,7 +742,12 @@ class ProviderImportHarness { }; } - import(input: { providerHandleId: string; cwd?: string; labels?: Record }) { + import(input: { + providerHandleId: string; + cwd?: string; + labels?: Record; + modeId?: string; + }) { return importProviderSession({ request: { requestId: "import-thread", @@ -743,6 +755,7 @@ class ProviderImportHarness { providerHandleId: input.providerHandleId, cwd: input.cwd, labels: input.labels, + modeId: input.modeId, }, workspaceProvisioning: createImportWorkspace("ws-restored"), agentManager: this.manager, @@ -752,7 +765,7 @@ class ProviderImportHarness { } } -test("importProviderSession uses the provider import path with the requested labels", async () => { +test("importProviderSession uses the provider import path with the requested labels and mode", async () => { const harness = await ProviderImportHarness.create(); harness.timeline = [ { type: "user_message", text: "Trace recent provider sessions" }, @@ -763,6 +776,7 @@ test("importProviderSession uses the provider import path with the requested lab providerHandleId: "thread-imported", cwd: "/tmp/imported-agent", labels: { source: "import" }, + modeId: "full-access", }); expect(harness.freshImports).toEqual([ @@ -772,6 +786,7 @@ test("importProviderSession uses the provider import path with the requested lab cwd: "/tmp/imported-agent", workspaceId: "ws-restored", labels: { source: "import" }, + modeId: "full-access", }, ]); expect(result).toEqual({ @@ -813,6 +828,7 @@ test("importProviderSession restores an archived session as the same standalone providerHandleId: "thread-archived", cwd: harness.snapshot.cwd, labels: { source: "reimport" }, + modeId: "full-access", }); expect(result).toEqual({ @@ -824,6 +840,7 @@ test("importProviderSession restores an archived session as the same standalone id: harness.snapshot.id, workspaceId: "ws-restored", labels: { existing: "label", source: "reimport" }, + config: { modeId: "full-access" }, archivedAt: null, }); expect((await harness.storage.get(harness.snapshot.id))?.labels).not.toHaveProperty( diff --git a/packages/server/src/server/agent/import-sessions.ts b/packages/server/src/server/agent/import-sessions.ts index 492667ab88..d385890533 100644 --- a/packages/server/src/server/agent/import-sessions.ts +++ b/packages/server/src/server/agent/import-sessions.ts @@ -48,6 +48,7 @@ export interface NormalizedImportAgentRequest { cwd?: string; workspaceId?: string; labels?: Record; + modeId?: string; requestId: string; } @@ -112,6 +113,7 @@ export function normalizeImportAgentRequest( cwd: msg.cwd, workspaceId: msg.workspaceId, labels: msg.labels, + ...(msg.modeId ? { modeId: msg.modeId } : {}), requestId: msg.requestId, }; } @@ -230,6 +232,7 @@ async function importProviderSessionNow( await unarchiveAgentState(input.agentStorage, input.agentManager, archivedRecord.id, { workspaceId, labels: Object.keys(labelPatch).length > 0 ? labelPatch : undefined, + modeId: input.request.modeId, }); try { const snapshot = await ensureAgentLoaded(archivedRecord.id, { @@ -253,6 +256,7 @@ async function importProviderSessionNow( cwd, workspaceId, labels, + ...(input.request.modeId ? { modeId: input.request.modeId } : {}), }); await unarchiveAgentState(input.agentStorage, input.agentManager, snapshot.id);