feat(cli): add paseo import --provider <name> <id> to resume existing sessions (#632)
Surfaces the daemon's existing resume-from-persistence capability through a new top-level CLI command so users can pull existing Claude Code, Codex, or OpenCode sessions into Paseo without losing history. Adds a new additive `import_agent_request` WebSocket message; no existing schemas change. Refs #611, #237, #492; partially addresses #268.
This commit is contained in:
@@ -22,6 +22,7 @@ import { addInspectOptions, runInspectCommand } from "./commands/agent/inspect.j
|
|||||||
import { addWaitOptions, runWaitCommand } from "./commands/agent/wait.js";
|
import { addWaitOptions, runWaitCommand } from "./commands/agent/wait.js";
|
||||||
import { addArchiveOptions, runArchiveCommand } from "./commands/agent/archive.js";
|
import { addArchiveOptions, runArchiveCommand } from "./commands/agent/archive.js";
|
||||||
import { addAttachOptions, runAttachCommand } from "./commands/agent/attach.js";
|
import { addAttachOptions, runAttachCommand } from "./commands/agent/attach.js";
|
||||||
|
import { addImportOptions, runImportCommand } from "./commands/agent/import.js";
|
||||||
import { withOutput } from "./output/index.js";
|
import { withOutput } from "./output/index.js";
|
||||||
import { onboardCommand } from "./commands/onboard.js";
|
import { onboardCommand } from "./commands/onboard.js";
|
||||||
import {
|
import {
|
||||||
@@ -60,6 +61,10 @@ export function createCli(): Command {
|
|||||||
withOutput(runRunCommand),
|
withOutput(runRunCommand),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
addJsonAndDaemonHostOptions(addImportOptions(program.command("import"))).action(
|
||||||
|
withOutput(runImportCommand),
|
||||||
|
);
|
||||||
|
|
||||||
addDaemonHostOption(addAttachOptions(program.command("attach"))).action(runAttachCommand);
|
addDaemonHostOption(addAttachOptions(program.command("attach"))).action(runAttachCommand);
|
||||||
|
|
||||||
addDaemonHostOption(addLogsOptions(program.command("logs"))).action(runLogsCommand);
|
addDaemonHostOption(addLogsOptions(program.command("logs"))).action(runLogsCommand);
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||||
|
import { collectMultiple } from "../../utils/command-options.js";
|
||||||
|
import type { CommandError, CommandOptions, SingleResult } from "../../output/index.js";
|
||||||
|
import { agentRunSchema, type AgentRunResult } from "./run.js";
|
||||||
|
import type { AgentSnapshotPayload } from "@getpaseo/server";
|
||||||
|
|
||||||
|
const IMPORT_PROVIDERS = new Set(["claude", "codex", "opencode", "acp"]);
|
||||||
|
|
||||||
|
export function addImportOptions(cmd: Command): Command {
|
||||||
|
return cmd
|
||||||
|
.description("Import an existing provider session as a Paseo agent")
|
||||||
|
.argument("<id>", "Provider session/thread ID to import")
|
||||||
|
.requiredOption("--provider <provider>", "Agent provider: claude, codex, opencode, or acp")
|
||||||
|
.option("--cwd <path>", "Working directory for providers that require it")
|
||||||
|
.option(
|
||||||
|
"--label <key=value>",
|
||||||
|
"Add label(s) to the agent (can be used multiple times)",
|
||||||
|
collectMultiple,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentImportOptions extends CommandOptions {
|
||||||
|
provider?: string;
|
||||||
|
cwd?: string;
|
||||||
|
label?: string[];
|
||||||
|
host?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AgentImportCommandResult = SingleResult<AgentRunResult>;
|
||||||
|
|
||||||
|
function toImportResult(agent: AgentSnapshotPayload): AgentRunResult {
|
||||||
|
return {
|
||||||
|
agentId: agent.id,
|
||||||
|
status: agent.status === "running" ? "running" : "created",
|
||||||
|
provider: agent.provider,
|
||||||
|
cwd: agent.cwd,
|
||||||
|
title: agent.title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseImportProvider(provider: string | undefined): string {
|
||||||
|
const normalizedProvider = provider?.trim();
|
||||||
|
if (!normalizedProvider) {
|
||||||
|
throw {
|
||||||
|
code: "MISSING_PROVIDER",
|
||||||
|
message: "Provider is required",
|
||||||
|
details: "Usage: paseo import --provider <provider> <id>",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IMPORT_PROVIDERS.has(normalizedProvider)) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_PROVIDER",
|
||||||
|
message: `Unsupported provider: ${normalizedProvider}`,
|
||||||
|
details: "Supported providers: claude, codex, opencode, acp",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseImportLabels(labelFlags: string[] | undefined): Record<string, string> {
|
||||||
|
const labels: Record<string, string> = {};
|
||||||
|
if (!labelFlags) {
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const labelFlag of labelFlags) {
|
||||||
|
const eqIndex = labelFlag.indexOf("=");
|
||||||
|
if (eqIndex === -1) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_LABEL",
|
||||||
|
message: `Invalid label format: ${labelFlag}`,
|
||||||
|
details: "Labels must be in key=value format",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = labelFlag.slice(0, eqIndex).trim();
|
||||||
|
if (!key) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_LABEL",
|
||||||
|
message: `Invalid label format: ${labelFlag}`,
|
||||||
|
details: "Labels must include a non-empty key in key=value format",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
labels[key] = labelFlag.slice(eqIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectToDaemonOrThrow(
|
||||||
|
hostOption: string | undefined,
|
||||||
|
host: string,
|
||||||
|
): Promise<Awaited<ReturnType<typeof connectToDaemon>>> {
|
||||||
|
try {
|
||||||
|
return await connectToDaemon({ host: hostOption });
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw {
|
||||||
|
code: "DAEMON_NOT_RUNNING",
|
||||||
|
message: `Cannot connect to daemon at ${host}: ${message}`,
|
||||||
|
details: "Start the daemon with: paseo daemon start",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runImportCommand(
|
||||||
|
sessionIdArg: string,
|
||||||
|
options: AgentImportOptions,
|
||||||
|
_command: Command,
|
||||||
|
): Promise<AgentImportCommandResult> {
|
||||||
|
const host = getDaemonHost({ host: options.host as string | undefined });
|
||||||
|
const sessionId = sessionIdArg.trim();
|
||||||
|
if (!sessionId) {
|
||||||
|
throw {
|
||||||
|
code: "MISSING_SESSION_ID",
|
||||||
|
message: "Session ID is required",
|
||||||
|
details: "Usage: paseo import --provider <provider> <id>",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = parseImportProvider(options.provider);
|
||||||
|
const cwd = options.cwd?.trim();
|
||||||
|
if (options.cwd !== undefined && !cwd) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_CWD",
|
||||||
|
message: "--cwd cannot be empty",
|
||||||
|
details: "Provide a working directory path or omit --cwd",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = parseImportLabels(options.label);
|
||||||
|
const client = await connectToDaemonOrThrow(options.host as string | undefined, host);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = await client.importAgent({
|
||||||
|
provider,
|
||||||
|
sessionId,
|
||||||
|
...(cwd ? { cwd } : {}),
|
||||||
|
...(Object.keys(labels).length > 0 ? { labels } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "single",
|
||||||
|
data: toImportResult(agent),
|
||||||
|
schema: agentRunSchema,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
await client.close().catch(() => {});
|
||||||
|
|
||||||
|
if (err && typeof err === "object" && "code" in err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw {
|
||||||
|
code: "AGENT_IMPORT_FAILED",
|
||||||
|
message: `Failed to import agent: ${message}`,
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { addInspectOptions, runInspectCommand } from "./inspect.js";
|
|||||||
import { addWaitOptions, runWaitCommand } from "./wait.js";
|
import { addWaitOptions, runWaitCommand } from "./wait.js";
|
||||||
import { addAttachOptions, runAttachCommand } from "./attach.js";
|
import { addAttachOptions, runAttachCommand } from "./attach.js";
|
||||||
import { addReloadOptions, runReloadCommand } from "./reload.js";
|
import { addReloadOptions, runReloadCommand } from "./reload.js";
|
||||||
|
import { addImportOptions, runImportCommand } from "./import.js";
|
||||||
import { runUpdateCommand } from "./update.js";
|
import { runUpdateCommand } from "./update.js";
|
||||||
import { withOutput } from "../../output/index.js";
|
import { withOutput } from "../../output/index.js";
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +30,10 @@ export function createAgentCommand(): Command {
|
|||||||
withOutput(runRunCommand),
|
withOutput(runRunCommand),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
addJsonAndDaemonHostOptions(addImportOptions(agent.command("import"))).action(
|
||||||
|
withOutput(runImportCommand),
|
||||||
|
);
|
||||||
|
|
||||||
addDaemonHostOption(addAttachOptions(agent.command("attach"))).action(runAttachCommand);
|
addDaemonHostOption(addAttachOptions(agent.command("attach"))).action(runAttachCommand);
|
||||||
|
|
||||||
addDaemonHostOption(addLogsOptions(agent.command("logs"))).action(runLogsCommand);
|
addDaemonHostOption(addLogsOptions(agent.command("logs"))).action(runLogsCommand);
|
||||||
|
|||||||
@@ -122,6 +122,13 @@ const perfNow: () => number =
|
|||||||
? () => performance.now()
|
? () => performance.now()
|
||||||
: () => Date.now();
|
: () => Date.now();
|
||||||
|
|
||||||
|
export interface ImportAgentInput {
|
||||||
|
provider: AgentProvider;
|
||||||
|
sessionId: string;
|
||||||
|
cwd?: string;
|
||||||
|
labels?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
DaemonTransport,
|
DaemonTransport,
|
||||||
DaemonTransportFactory,
|
DaemonTransportFactory,
|
||||||
@@ -1763,6 +1770,47 @@ export class DaemonClient {
|
|||||||
return status.agent;
|
return status.agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async importAgent(input: ImportAgentInput): Promise<AgentSnapshotPayload> {
|
||||||
|
const requestId = this.createRequestId();
|
||||||
|
const message = SessionInboundMessageSchema.parse({
|
||||||
|
type: "import_agent_request",
|
||||||
|
requestId,
|
||||||
|
provider: input.provider,
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
...(input.cwd ? { cwd: input.cwd } : {}),
|
||||||
|
...(input.labels && Object.keys(input.labels).length > 0 ? { labels: input.labels } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await this.sendRequest({
|
||||||
|
requestId,
|
||||||
|
message,
|
||||||
|
timeout: 15000,
|
||||||
|
options: { skipQueue: true },
|
||||||
|
select: (msg) => {
|
||||||
|
if (msg.type !== "status") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const resumed = AgentResumedStatusPayloadSchema.safeParse(msg.payload);
|
||||||
|
if (resumed.success && resumed.data.requestId === requestId) {
|
||||||
|
return resumed.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const failed = AgentCreateFailedStatusPayloadSchema.safeParse(msg.payload);
|
||||||
|
if (failed.success && failed.data.requestId === requestId) {
|
||||||
|
return failed.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status.status === "agent_create_failed") {
|
||||||
|
throw new Error(status.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.agent;
|
||||||
|
}
|
||||||
|
|
||||||
async refreshAgent(agentId: string, requestId?: string): Promise<AgentRefreshedStatusPayload> {
|
async refreshAgent(agentId: string, requestId?: string): Promise<AgentRefreshedStatusPayload> {
|
||||||
const resolvedRequestId = this.createRequestId(requestId);
|
const resolvedRequestId = this.createRequestId(requestId);
|
||||||
const message = SessionInboundMessageSchema.parse({
|
const message = SessionInboundMessageSchema.parse({
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
|
PersistedAgentDescriptor,
|
||||||
} from "./agent-sdk-types.js";
|
} from "./agent-sdk-types.js";
|
||||||
import type { ProviderDefinition } from "./provider-registry.js";
|
import type { ProviderDefinition } from "./provider-registry.js";
|
||||||
|
|
||||||
@@ -55,6 +56,30 @@ function createFeature(args: { id: string; label: string; value: boolean }): Age
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPersistedDescriptor(args: {
|
||||||
|
cwd: string;
|
||||||
|
sessionId: string;
|
||||||
|
nativeHandle?: string;
|
||||||
|
}): PersistedAgentDescriptor {
|
||||||
|
return {
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: args.sessionId,
|
||||||
|
cwd: args.cwd,
|
||||||
|
title: null,
|
||||||
|
lastActivityAt: new Date("2026-01-01T00:00:00Z"),
|
||||||
|
persistence: {
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: args.sessionId,
|
||||||
|
nativeHandle: args.nativeHandle,
|
||||||
|
metadata: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: args.cwd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeline: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class TestAgentClient implements AgentClient {
|
class TestAgentClient implements AgentClient {
|
||||||
readonly provider = "codex" as const;
|
readonly provider = "codex" as const;
|
||||||
readonly capabilities = TEST_CAPABILITIES;
|
readonly capabilities = TEST_CAPABILITIES;
|
||||||
@@ -1088,6 +1113,48 @@ test("resumeAgentFromPersistence keeps metadata config, applies overrides, and p
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("findPersistedAgent returns matching descriptors by session id or native handle", async () => {
|
||||||
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-find-persisted-"));
|
||||||
|
const storagePath = join(workdir, "agents");
|
||||||
|
const storage = new AgentStorage(storagePath, logger);
|
||||||
|
|
||||||
|
const descriptors: PersistedAgentDescriptor[] = [
|
||||||
|
createPersistedDescriptor({
|
||||||
|
cwd: workdir,
|
||||||
|
sessionId: "session-direct",
|
||||||
|
nativeHandle: "native-direct",
|
||||||
|
}),
|
||||||
|
createPersistedDescriptor({
|
||||||
|
cwd: workdir,
|
||||||
|
sessionId: "session-other",
|
||||||
|
nativeHandle: "native-match",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
class PersistedAgentsClient extends TestAgentClient {
|
||||||
|
lastLimit: number | undefined;
|
||||||
|
|
||||||
|
override async listPersistedAgents(options?: { limit?: number }) {
|
||||||
|
this.lastLimit = options?.limit;
|
||||||
|
return descriptors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new PersistedAgentsClient();
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: {
|
||||||
|
codex: client,
|
||||||
|
},
|
||||||
|
registry: storage,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(manager.findPersistedAgent("codex", "session-direct")).resolves.toBe(descriptors[0]);
|
||||||
|
await expect(manager.findPersistedAgent("codex", "native-match")).resolves.toBe(descriptors[1]);
|
||||||
|
await expect(manager.findPersistedAgent("codex", "missing")).resolves.toBeNull();
|
||||||
|
expect(client.lastLimit).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
test("reloadAgentSession passes daemon launch env through the provider launch context", async () => {
|
test("reloadAgentSession passes daemon launch env through the provider launch context", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-context-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-context-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
|
|||||||
@@ -606,6 +606,25 @@ export class AgentManager {
|
|||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findPersistedAgent(
|
||||||
|
provider: AgentProvider,
|
||||||
|
sessionId: string,
|
||||||
|
): Promise<PersistedAgentDescriptor | null> {
|
||||||
|
const client = this.requireClient(provider);
|
||||||
|
if (!client.listPersistedAgents) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptors = await client.listPersistedAgents({ limit: 200 });
|
||||||
|
return (
|
||||||
|
descriptors.find((descriptor) => {
|
||||||
|
return (
|
||||||
|
descriptor.sessionId === sessionId || descriptor.persistence.nativeHandle === sessionId
|
||||||
|
);
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async listProviderAvailability(): Promise<ProviderAvailability[]> {
|
async listProviderAvailability(): Promise<ProviderAvailability[]> {
|
||||||
const checks = Array.from(this.clients.keys()).map(async (provider) => {
|
const checks = Array.from(this.clients.keys()).map(async (provider) => {
|
||||||
const client = this.clients.get(provider);
|
const client = this.clients.get(provider);
|
||||||
|
|||||||
@@ -758,6 +758,45 @@ class VoiceFeatureUnavailableError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BuildImportPersistenceHandleInput {
|
||||||
|
provider: AgentProvider;
|
||||||
|
sessionId: string;
|
||||||
|
cwd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildImportPersistenceHandle(
|
||||||
|
input: BuildImportPersistenceHandleInput,
|
||||||
|
): AgentPersistenceHandle {
|
||||||
|
const cwd = input.cwd ?? process.cwd();
|
||||||
|
return {
|
||||||
|
provider: input.provider,
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
nativeHandle: input.sessionId,
|
||||||
|
metadata: {
|
||||||
|
provider: input.provider,
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyImportCwdOverride(
|
||||||
|
handle: AgentPersistenceHandle,
|
||||||
|
cwd: string | undefined,
|
||||||
|
): AgentPersistenceHandle {
|
||||||
|
if (!cwd) {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...handle,
|
||||||
|
metadata: {
|
||||||
|
...handle.metadata,
|
||||||
|
provider: handle.provider,
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function convertPCMToWavBuffer(
|
function convertPCMToWavBuffer(
|
||||||
pcmBuffer: Buffer,
|
pcmBuffer: Buffer,
|
||||||
sampleRate: number,
|
sampleRate: number,
|
||||||
@@ -1846,6 +1885,8 @@ export class Session {
|
|||||||
return this.handleCreateAgentRequest(msg);
|
return this.handleCreateAgentRequest(msg);
|
||||||
case "resume_agent_request":
|
case "resume_agent_request":
|
||||||
return this.handleResumeAgentRequest(msg);
|
return this.handleResumeAgentRequest(msg);
|
||||||
|
case "import_agent_request":
|
||||||
|
return this.handleImportAgentRequest(msg);
|
||||||
case "refresh_agent_request":
|
case "refresh_agent_request":
|
||||||
return this.handleRefreshAgentRequest(msg);
|
return this.handleRefreshAgentRequest(msg);
|
||||||
case "cancel_agent_request":
|
case "cancel_agent_request":
|
||||||
@@ -3251,6 +3292,72 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleImportAgentRequest(
|
||||||
|
msg: Extract<SessionInboundMessage, { type: "import_agent_request" }>,
|
||||||
|
): Promise<void> {
|
||||||
|
const { provider, sessionId, cwd, labels, requestId } = msg;
|
||||||
|
this.sessionLogger.info({ sessionId, provider }, `Importing agent ${sessionId} (${provider})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const descriptor = await this.agentManager.findPersistedAgent(provider, sessionId);
|
||||||
|
if (!descriptor && provider === "opencode" && !cwd) {
|
||||||
|
throw new Error(
|
||||||
|
"OpenCode sessions require --cwd when the session cannot be found in persisted agents",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = descriptor
|
||||||
|
? applyImportCwdOverride(descriptor.persistence, cwd)
|
||||||
|
: buildImportPersistenceHandle({ provider, sessionId, cwd });
|
||||||
|
const overrides = cwd ? ({ cwd } satisfies Partial<AgentSessionConfig>) : undefined;
|
||||||
|
|
||||||
|
await this.unarchiveAgentByHandle(handle);
|
||||||
|
const snapshot = await this.agentManager.resumeAgentFromPersistence(
|
||||||
|
handle,
|
||||||
|
overrides,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
labels,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
|
||||||
|
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
|
||||||
|
await this.forwardAgentUpdate(snapshot);
|
||||||
|
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
|
||||||
|
const agentPayload = await this.buildAgentPayload(snapshot);
|
||||||
|
this.emit({
|
||||||
|
type: "status",
|
||||||
|
payload: {
|
||||||
|
status: "agent_resumed",
|
||||||
|
agentId: snapshot.id,
|
||||||
|
requestId,
|
||||||
|
timelineSize,
|
||||||
|
agent: agentPayload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
this.sessionLogger.error({ err: error }, "Failed to import agent");
|
||||||
|
this.emit({
|
||||||
|
type: "status",
|
||||||
|
payload: {
|
||||||
|
status: "agent_create_failed",
|
||||||
|
requestId,
|
||||||
|
error: message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.emit({
|
||||||
|
type: "activity_log",
|
||||||
|
payload: {
|
||||||
|
id: uuidv4(),
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: "error",
|
||||||
|
content: `Failed to import agent: ${message}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async handleRefreshAgentRequest(
|
private async handleRefreshAgentRequest(
|
||||||
msg: Extract<SessionInboundMessage, { type: "refresh_agent_request" }>,
|
msg: Extract<SessionInboundMessage, { type: "refresh_agent_request" }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
@@ -1010,6 +1010,15 @@ export const ResumeAgentRequestMessageSchema = z.object({
|
|||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ImportAgentRequestMessageSchema = z.object({
|
||||||
|
type: z.literal("import_agent_request"),
|
||||||
|
provider: AgentProviderSchema,
|
||||||
|
sessionId: z.string(),
|
||||||
|
cwd: z.string().optional(),
|
||||||
|
labels: z.record(z.string()).optional(),
|
||||||
|
requestId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const RefreshAgentRequestMessageSchema = z.object({
|
export const RefreshAgentRequestMessageSchema = z.object({
|
||||||
type: z.literal("refresh_agent_request"),
|
type: z.literal("refresh_agent_request"),
|
||||||
agentId: z.string(),
|
agentId: z.string(),
|
||||||
@@ -1652,6 +1661,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
RefreshProvidersSnapshotRequestMessageSchema,
|
RefreshProvidersSnapshotRequestMessageSchema,
|
||||||
ProviderDiagnosticRequestMessageSchema,
|
ProviderDiagnosticRequestMessageSchema,
|
||||||
ResumeAgentRequestMessageSchema,
|
ResumeAgentRequestMessageSchema,
|
||||||
|
ImportAgentRequestMessageSchema,
|
||||||
RefreshAgentRequestMessageSchema,
|
RefreshAgentRequestMessageSchema,
|
||||||
CancelAgentRequestMessageSchema,
|
CancelAgentRequestMessageSchema,
|
||||||
ShutdownServerRequestMessageSchema,
|
ShutdownServerRequestMessageSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user