fix(cli): support mode when importing agents

This commit is contained in:
Keith Kowal
2026-09-11 14:57:01 -07:00
parent fa93c4290e
commit 89cb1bf34d
12 changed files with 82 additions and 6 deletions
+5
View File
@@ -46,6 +46,11 @@ describe("canonical CLI surface", () => {
expect(help).toContain("--forge <forge>");
});
it("offers provider mode selection when importing an agent", () => {
const importCommand = createCli().commands.find((command) => command.name() === "import");
expect(importCommand?.helpInformation()).toContain("--mode <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");
@@ -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",
});
});
});
@@ -11,6 +11,7 @@ export function addImportOptions(cmd: Command): Command {
.argument("<id>", "Provider session/thread ID to import")
.requiredOption("--provider <provider>", "Agent provider id")
.option("--cwd <path>", "Working directory for providers that require it")
.option("--mode <mode>", "Provider-specific mode (e.g., plan, default, bypass)")
.option(
"--label <key=value>",
"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 } : {}),
});
@@ -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");
+2
View File
@@ -192,6 +192,7 @@ interface ImportAgentInputBase {
cwd?: string;
workspaceId?: string;
labels?: Record<string, string>;
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({
+1
View File
@@ -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(),
});
@@ -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",
@@ -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 () => {
@@ -1357,6 +1357,7 @@ export class AgentManager {
cwd: string;
workspaceId: string;
labels?: Record<string, string>;
modeId?: string;
}): Promise<ManagedAgent> {
return this.trackAgentRegistrationOperation(this.importProviderSessionInternal(input));
}
@@ -1367,6 +1368,7 @@ export class AgentManager {
cwd: string;
workspaceId: string;
labels?: Record<string, string>;
modeId?: string;
}): Promise<ManagedAgent> {
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<boolean> {
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(),
});
@@ -198,7 +198,7 @@ export async function unarchiveAgentState(
_agentStorage: AgentStorage,
agentManager: AgentUnarchiveController,
agentId: string,
updates?: { workspaceId?: string; labels?: Record<string, string | null> },
updates?: { workspaceId?: string; labels?: Record<string, string | null>; modeId?: string },
): Promise<boolean> {
const unarchived = await agentManager.unarchiveSnapshot(agentId, updates);
if (!unarchived) return false;
@@ -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<string, string | null> },
updates?: {
workspaceId?: string;
labels?: Record<string, string | null>;
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<string, string> }) {
import(input: {
providerHandleId: string;
cwd?: string;
labels?: Record<string, string>;
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(
@@ -48,6 +48,7 @@ export interface NormalizedImportAgentRequest {
cwd?: string;
workspaceId?: string;
labels?: Record<string, string>;
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);