import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { afterEach, expect, test, vi } from "vitest"; import { createPaseoApi, createPaseoClient } from "./index.js"; import { DaemonClient } from "./daemon-client.js"; import type { PaseoAgent, PaseoClient, PaseoWorkspace } from "./index.js"; type FakeWebSocketHandler = (...args: unknown[]) => void; class FakeWebSocket { static instances: FakeWebSocket[] = []; readyState = 1; sent: Array = []; onopen: FakeWebSocketHandler | null = null; onmessage: FakeWebSocketHandler | null = null; onclose: FakeWebSocketHandler | null = null; onerror: FakeWebSocketHandler | null = null; constructor( readonly url: string, readonly protocols?: string | string[], ) { FakeWebSocket.instances.push(this); } send(data: string | ArrayBuffer | Uint8Array): void { this.sent.push(data); const frame = JSON.parse(String(data)); if (frame.message?.type === "subscription.release.request") this.message( sessionMessage({ type: "subscription.release.response", payload: { requestId: frame.message.requestId, subscriptionId: frame.message.subscriptionId, }, }), ); } close(): void { this.readyState = 3; } open(): void { this.onopen?.(); } message(data: string): void { this.onmessage?.({ data }); } } afterEach(() => { vi.unstubAllGlobals(); FakeWebSocket.instances.length = 0; }); function sessionMessage(message: object): string { return JSON.stringify({ type: "session", message, }); } function parseSentSessionMessage(data: string | ArrayBuffer | Uint8Array | undefined): { type: string; requestId?: string; agentId?: string; workspaceId?: string; provider?: string; providers?: string[]; cwd?: string; config?: unknown; draftConfig?: unknown; filter?: unknown; page?: unknown; sync?: unknown; text?: string; } { if (typeof data !== "string") { throw new Error("Expected string WebSocket frame"); } const parsed = JSON.parse(data); return parsed.message; } function parseSentFrame( data: string | ArrayBuffer | Uint8Array | undefined, ): Record { if (typeof data !== "string") { throw new Error("Expected string WebSocket frame"); } return JSON.parse(data); } async function connectClient( features: Record = { providerUsageList: true, providersSnapshotCwd: true, ownedSubscriptions: true, }, ): Promise<{ client: PaseoClient; ws: FakeWebSocket }> { const client = createPaseoClient({ url: "ws://daemon.test", webSocketFactory: (url) => new FakeWebSocket(url), reconnect: { enabled: false }, }); const connectPromise = client.connect(); const ws = FakeWebSocket.instances[0]; ws.open(); const hello = parseSentFrame(ws.sent.at(-1)); expect(hello).toMatchObject({ type: "hello", clientType: "cli", protocolVersion: 1, }); expect(hello.clientId).toEqual(expect.stringMatching(/^paseo-sdk-/)); ws.message( sessionMessage({ type: "status", payload: { status: "server_info", serverId: "srv_sdk_test", hostname: null, version: null, features, }, }), ); await connectPromise; return { client, ws }; } function acknowledgeObservation(ws: FakeWebSocket, subscriptionId: string): void { const request = parseSentSessionMessage(ws.sent.at(-1)); ws.message( sessionMessage({ type: request.type.replace(/([._])request$/, "$1response"), payload: { requestId: request.requestId, subscriptionId, entries: [], pageInfo: { nextCursor: null, prevCursor: null, hasMore: false }, }, }), ); } async function observeAgents(client: PaseoClient, ws: FakeWebSocket): Promise { const ready = client.agents.list({ subscribe: {} }); acknowledgeObservation(ws, "agents-sdk"); return (await ready).subscriptionId!; } function createWorkspace(input: Partial = {}): PaseoWorkspace { return { id: "workspace_sdk", projectId: "project_sdk", projectDisplayName: "SDK", projectRootPath: "/repo/sdk", workspaceDirectory: "/repo/sdk", projectKind: "git", workspaceKind: "directory", name: "sdk", archivingAt: null, status: "done", statusEnteredAt: null, activityAt: "2026-05-16T00:00:00.000Z", scripts: [], gitRuntime: null, githubRuntime: null, ...input, }; } function createAgent(input: Partial = {}): PaseoAgent { return { id: "agent_sdk", provider: "codex", cwd: "/repo/sdk", model: null, createdAt: "2026-05-16T00:00:00.000Z", updatedAt: "2026-05-16T00:00:00.000Z", lastUserMessageAt: null, status: "idle", capabilities: { supportsStreaming: true, supportsSessionPersistence: true, supportsDynamicModes: false, supportsMcpServers: true, supportsReasoningStream: true, supportsRewindBoth: false, supportsRewindConversation: false, supportsRewindFiles: false, supportsToolInvocations: true, }, currentModeId: null, availableModes: [], pendingPermissions: [], persistence: null, title: null, labels: {}, archivedAt: null, ...input, }; } test("createPaseoClient exposes workspace list through the daemon client", async () => { const { client, ws } = await connectClient(); const listPromise = client.workspaces.list({ filter: { query: "sdk" }, page: { limit: 10 }, }); const request = parseSentSessionMessage(ws.sent.at(-1)); expect(request).toMatchObject({ type: "fetch_workspaces_request", filter: { query: "sdk" }, page: { limit: 10 }, }); ws.message( sessionMessage({ type: "fetch_workspaces_response", payload: { requestId: request.requestId, entries: [], pageInfo: { nextCursor: null, prevCursor: null, hasMore: false, }, }, }), ); await expect(listPromise).resolves.toEqual({ requestId: request.requestId, entries: [], emptyProjects: [], pageInfo: { nextCursor: null, prevCursor: null, hasMore: false, }, }); expect(client.getConnectionState()).toEqual({ status: "connected" }); await client.close(); }); test("createPaseoApi borrows daemon capabilities without exposing connection ownership", () => { const daemonClient = new DaemonClient({ url: "ws://daemon.test", clientId: "borrowed-api", reconnect: { enabled: false }, }); const paseo = createPaseoApi(daemonClient); expect(Object.keys(paseo).sort()).toEqual([ "agents", "config", "dispose", "hosts", "observeEvents", "projects", "providers", "terminals", "workspaces", ]); expect("connect" in paseo).toBe(false); expect("close" in paseo).toBe(false); expect("skills" in paseo.agents).toBe(false); }); test("agent handles send permission responses for their agent", async () => { const { client, ws } = await connectClient(); await client.agents.ref("agent_sdk").respondToPermission({ requestId: "permission-request", response: { behavior: "deny", selectedActionId: "deny-once", message: "Not approved", interrupt: true, }, }); expect(parseSentFrame(ws.sent.at(-1))).toEqual({ type: "session", message: { type: "agent_permission_response", agentId: "agent_sdk", requestId: "permission-request", response: { behavior: "deny", selectedActionId: "deny-once", message: "Not approved", interrupt: true, }, }, }); await client.close(); }); test("project actions list registered projects through the existing RPC", async () => { const { client, ws } = await connectClient(); const listPromise = client.projects.list({ requestId: "projects-list-request", sync: { generation: "daemon-generation", afterSeq: 7 }, }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "project.list.request", requestId: "projects-list-request", sync: { generation: "daemon-generation", afterSeq: 7 }, }); ws.message( sessionMessage({ type: "project.list.response", payload: { requestId: "projects-list-request", projects: [ { projectId: "project_sdk", projectKey: "sdk", projectDisplayName: "SDK", projectCustomName: null, projectCustomIconRevision: null, projectIconRevision: "icon-revision", projectRootPath: "/repo/sdk", projectKind: "git", syncSeq: 8, }, ], sync: { generation: "daemon-generation", headSeq: 8, mode: "changes", removals: [], }, }, }), ); await expect(listPromise).resolves.toEqual({ requestId: "projects-list-request", projects: [ { projectId: "project_sdk", projectKey: "sdk", projectDisplayName: "SDK", projectCustomName: null, projectCustomIconRevision: null, projectIconRevision: "icon-revision", projectRootPath: "/repo/sdk", projectKind: "git", syncSeq: 8, }, ], sync: { generation: "daemon-generation", headSeq: 8, mode: "changes", removals: [], }, }); await client.close(); }); test("project actions subscribe to existing project updates", async () => { const { client, ws } = await connectClient(); const updates: string[] = []; const unsubscribe = client.projects.subscribe((update) => { updates.push(update.kind === "upsert" ? update.project.projectDisplayName : update.projectId); }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "session.events.set_subscription.request", events: ["project.update"], }); acknowledgeObservation(ws, "projects-sdk"); ws.message( sessionMessage({ type: "project.update", payload: { subscriptionId: "projects-sdk", kind: "upsert", project: { projectId: "project_sdk", projectKey: "sdk", projectDisplayName: "Renamed SDK", projectCustomName: "Renamed SDK", projectCustomIconRevision: null, projectIconRevision: "icon-revision", projectRootPath: "/repo/sdk", projectKind: "git", syncSeq: 9, }, generation: "daemon-generation", seq: 9, }, }), ); ws.message( sessionMessage({ type: "project.update", payload: { subscriptionId: "projects-sdk", kind: "remove", projectId: "project_removed", generation: "daemon-generation", seq: 10, }, }), ); expect(updates).toEqual(["Renamed SDK", "project_removed"]); unsubscribe(); ws.message( sessionMessage({ type: "project.update", payload: { subscriptionId: "projects-sdk", kind: "remove", projectId: "project_after_unsubscribe", }, }), ); expect(updates).toEqual(["Renamed SDK", "project_removed"]); await client.close(); }); test("agent actions list the daemon directory without exposing the low-level client", async () => { const { client, ws } = await connectClient(); const listedAgent = createAgent({ title: "Planner" }); const listPromise = client.agents.list({ filter: { includeArchived: false }, page: { limit: 10 }, }); const request = parseSentSessionMessage(ws.sent.at(-1)); expect(request).toMatchObject({ type: "fetch_agents_request", filter: { includeArchived: false }, page: { limit: 10 }, }); ws.message( sessionMessage({ type: "fetch_agents_response", payload: { requestId: request.requestId, entries: [ { agent: listedAgent, project: { projectKey: "project_sdk", projectName: "SDK", workspaceName: "main", checkout: { cwd: "/repo/sdk", isGit: false, currentBranch: null, remoteUrl: null, isPaseoOwnedWorktree: false, mainRepoRoot: null, }, }, }, ], pageInfo: { nextCursor: null, prevCursor: null, hasMore: false, }, }, }), ); await expect(listPromise).resolves.toMatchObject({ entries: [{ agent: { id: "agent_sdk", title: "Planner" } }], }); await client.close(); }); test("workspace handles keep identity and refresh snapshots through existing driver calls", async () => { const { client, ws } = await connectClient(); const openedWorkspace = createWorkspace(); const openPromise = client.workspaces.open("/repo/sdk", "open-workspace-request"); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "open_project_request", cwd: "/repo/sdk", }); ws.message( sessionMessage({ type: "open_project_response", payload: { requestId: "open-workspace-request", workspace: openedWorkspace, error: null, }, }), ); const workspace = await openPromise; expect(workspace.id).toBe("workspace_sdk"); expect(workspace.projectId).toBe("project_sdk"); expect(workspace.directory).toBe("/repo/sdk"); expect(workspace.current()).toEqual(openedWorkspace); const refreshedWorkspace = createWorkspace({ name: "sdk refreshed" }); const refetchPromise = workspace.refresh({ requestId: "workspace-refetch-request" }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "fetch_workspaces_request", requestId: "workspace-refetch-request", page: { limit: 200 }, }); ws.message( sessionMessage({ type: "fetch_workspaces_response", payload: { requestId: "workspace-refetch-request", entries: [], pageInfo: { nextCursor: "workspace-page-2", prevCursor: null, hasMore: true, }, }, }), ); await new Promise((resolve) => setTimeout(resolve, 0)); const secondPageRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(secondPageRequest).toMatchObject({ type: "fetch_workspaces_request", page: { limit: 200, cursor: "workspace-page-2" }, }); ws.message( sessionMessage({ type: "fetch_workspaces_response", payload: { requestId: secondPageRequest.requestId, entries: [refreshedWorkspace], pageInfo: { nextCursor: null, prevCursor: null, hasMore: false, }, }, }), ); await expect(refetchPromise).resolves.toEqual(refreshedWorkspace); expect(workspace.current()).toEqual(refreshedWorkspace); const directoryReady = client.workspaces.list({ subscribe: {} }); acknowledgeObservation(ws, "workspaces-sdk"); await directoryReady; const updates: string[] = []; const unsubscribe = workspace.subscribe((update) => { if (update.kind === "upsert") { updates.push(update.workspace.name); } }); const pushedWorkspace = createWorkspace({ name: "sdk pushed" }); ws.message( sessionMessage({ type: "workspace_update", payload: { subscriptionId: "workspaces-sdk", kind: "upsert", workspace: pushedWorkspace }, }), ); expect(updates).toEqual(["sdk pushed"]); expect(workspace.current()).toEqual(pushedWorkspace); const titlePromise = workspace.setTitle("SDK review", "workspace-title-request"); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "workspace.title.set.request", requestId: "workspace-title-request", workspaceId: "workspace_sdk", title: "SDK review", }); ws.message( sessionMessage({ type: "workspace.title.set.response", payload: { requestId: "workspace-title-request", workspaceId: "workspace_sdk", accepted: true, title: "SDK review", error: null, }, }), ); await expect(titlePromise).resolves.toEqual({ title: "SDK review" }); unsubscribe(); ws.message( sessionMessage({ type: "workspace_update", payload: { subscriptionId: "workspaces-sdk", kind: "upsert", workspace: createWorkspace({ name: "sdk after unsubscribe" }), }, }), ); expect(updates).toEqual(["sdk pushed"]); await client.close(); }); test("plugin-shaped PR workspace create and agent create use the existing daemon RPCs", async () => { const { client, ws } = await connectClient(); const createdWorkspace = createWorkspace({ id: "workspace_fresh", name: "Issue 42" }); const workspacePromise = client.workspaces.create({ source: { kind: "worktree", cwd: "/repo/sdk", action: "checkout", checkoutSource: { kind: "change_request", forge: "github", number: 42 }, }, title: "Issue 42", }); const workspaceRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(workspaceRequest).toMatchObject({ type: "workspace.create.request", source: { kind: "worktree", cwd: "/repo/sdk", action: "checkout", checkoutSource: { kind: "change_request", forge: "github", number: 42 }, }, title: "Issue 42", }); ws.message( sessionMessage({ type: "workspace.create.response", payload: { requestId: workspaceRequest.requestId, workspace: createdWorkspace, setupTerminalId: null, error: null, }, }), ); const workspace = await workspacePromise; const agentPromise = workspace.agents.create({ config: { provider: "codex/gpt-5.4" }, parent: client.agents.ref("parent_sdk"), prompt: "Implement issue 42.", }); const agentRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(agentRequest).toMatchObject({ type: "create_agent_request", config: { provider: "codex", model: "gpt-5.4", cwd: "/repo/sdk" }, workspaceId: "workspace_fresh", callerAgentId: "parent_sdk", initialPrompt: "Implement issue 42.", }); ws.message( sessionMessage({ type: "status", payload: { status: "agent_created", requestId: agentRequest.requestId, agentId: "agent_sdk", agent: createAgent({ workspaceId: "workspace_fresh" }), }, }), ); const agent = await agentPromise; expect(agent.workspaceId).toBe("workspace_fresh"); expect(agent.cwd).toBe("/repo/sdk"); await client.close(); }); test("agents.create forwards internal on the request, outside the agent config", async () => { const { client, ws } = await connectClient({ providerUsageList: true, providersSnapshotCwd: true, ownedSubscriptions: true, backgroundWorkspaces: true, }); const createPromise = client.agents.create({ config: { provider: "codex/gpt-5.4" }, cwd: "/repo/sdk", title: "Summary helper", background: true, autoArchive: true, }); const request = parseSentSessionMessage(ws.sent.at(-1)); expect(request).toMatchObject({ type: "create_agent_request", config: { provider: "codex", model: "gpt-5.4", cwd: "/repo/sdk", title: "Summary helper" }, background: true, autoArchive: true, }); expect(request.config).not.toHaveProperty("internal"); ws.message( sessionMessage({ type: "status", payload: { status: "agent_created", requestId: request.requestId, agentId: "agent_helper", agent: createAgent({ id: "agent_helper" }), }, }), ); const agent = await createPromise; expect(agent.id).toBe("agent_helper"); await client.close(); }); test("agents.create with internal fails before sending on a host without internal agents", async () => { const { client, ws } = await connectClient(); const sentBefore = ws.sent.length; await expect( client.agents.create({ config: { provider: "codex/gpt-5.4" }, cwd: "/repo/sdk", background: true, }), ).rejects.toThrow("Update the host to use background workspaces."); expect(ws.sent).toHaveLength(sentBefore); await client.close(); }); test("agent handles delegate create, send, timeline refetch, archive, and local updates", async () => { const { client, ws } = await connectClient(); const createdAgent = createAgent(); const createPromise = client.agents.create({ config: { provider: "codex/gpt-5.4" }, cwd: "/repo/sdk", prompt: "ship it", }); const createRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(createRequest).toMatchObject({ type: "create_agent_request", config: { provider: "codex", model: "gpt-5.4", cwd: "/repo/sdk", }, initialPrompt: "ship it", }); ws.message( sessionMessage({ type: "status", payload: { status: "agent_created", requestId: createRequest.requestId, agentId: "agent_sdk", agent: createdAgent, }, }), ); const agent = await createPromise; expect(agent.id).toBe("agent_sdk"); expect(agent.current()).toEqual(createdAgent); await observeAgents(client, ws); const updatedAgents: string[] = []; const unsubscribe = agent.subscribe((update) => { if (update.kind === "upsert") { updatedAgents.push(update.agent.title ?? ""); } }); const updatedAgent = createAgent({ title: "Updated" }); ws.message( sessionMessage({ type: "agent_update", payload: { subscriptionId: "agents-sdk", kind: "upsert", agent: updatedAgent, project: null }, }), ); expect(updatedAgents).toEqual(["Updated"]); expect(agent.current()).toEqual(updatedAgent); const sendPromise = agent.send("hello", { messageId: "message-sdk", activeTurnBehavior: "steer", }); const sendRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(sendRequest).toMatchObject({ type: "send_agent_message_request", agentId: "agent_sdk", text: "hello", messageId: "message-sdk", activeTurnBehavior: "steer", }); ws.message( sessionMessage({ type: "send_agent_message_response", payload: { requestId: sendRequest.requestId, agentId: "agent_sdk", accepted: true, error: null, }, }), ); await sendPromise; const runPromise = agent.run("finish the task", { messageId: "run-message-sdk", activeTurnBehavior: "interrupt", timeoutMs: 30_000, }); const runSendRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(runSendRequest).toMatchObject({ type: "send_agent_message_request", agentId: "agent_sdk", text: "finish the task", messageId: "run-message-sdk", activeTurnBehavior: "interrupt", }); ws.message( sessionMessage({ type: "send_agent_message_response", payload: { requestId: runSendRequest.requestId, agentId: "agent_sdk", accepted: true, error: null, }, }), ); await new Promise((resolve) => setTimeout(resolve, 0)); const waitRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(waitRequest).toMatchObject({ type: "wait_for_finish_request", agentId: "agent_sdk", timeoutMs: 30_000, }); const finishedAgent = createAgent({ title: "Finished" }); ws.message( sessionMessage({ type: "wait_for_finish_response", payload: { requestId: waitRequest.requestId, agentId: "agent_sdk", status: "idle", final: finishedAgent, error: null, lastMessage: "DONE", }, }), ); await expect(runPromise).resolves.toMatchObject({ status: "idle", lastMessage: "DONE" }); expect(agent.current()).toEqual(finishedAgent); const defaultWaitPromise = agent.waitForFinish(); const defaultWaitRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(defaultWaitRequest).toMatchObject({ type: "wait_for_finish_request", agentId: "agent_sdk", timeoutMs: 10 * 60_000, }); ws.message( sessionMessage({ type: "wait_for_finish_response", payload: { requestId: defaultWaitRequest.requestId, agentId: "agent_sdk", status: "idle", final: finishedAgent, error: null, lastMessage: "DONE", }, }), ); await expect(defaultWaitPromise).resolves.toMatchObject({ status: "idle" }); const timelineAgent = createAgent({ title: "Timeline" }); const timelinePromise = agent.timeline.refetch({ limit: 5 }); const timelineRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(timelineRequest).toMatchObject({ type: "fetch_agent_timeline_request", agentId: "agent_sdk", limit: 5, }); ws.message( sessionMessage({ type: "fetch_agent_timeline_response", payload: { requestId: timelineRequest.requestId, agentId: "agent_sdk", agent: timelineAgent, direction: "tail", projection: "projected", epoch: "epoch-sdk", reset: false, staleCursor: false, gap: false, window: { minSeq: 0, maxSeq: 0, nextSeq: 0, }, startCursor: null, endCursor: null, hasOlder: false, hasNewer: false, entries: [], error: null, }, }), ); await timelinePromise; expect(agent.current()).toEqual(timelineAgent); const appendPromise = agent.timeline.append({ type: "plugin", id: "review-1", kind: "review", version: 1, data: { status: "running" }, }); const appendRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(appendRequest).toMatchObject({ type: "agent.timeline.append.request", agentId: "agent_sdk", item: { id: "review-1", kind: "review" }, }); ws.message( sessionMessage({ type: "agent.timeline.append.response", payload: { requestId: appendRequest.requestId, seq: 8, epoch: "epoch-sdk" }, }), ); await expect(appendPromise).resolves.toEqual({ seq: 8, epoch: "epoch-sdk" }); const archivePromise = agent.archive(); const archiveRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(archiveRequest).toMatchObject({ type: "archive_agent_request", agentId: "agent_sdk", }); ws.message( sessionMessage({ type: "agent_archived", payload: { requestId: archiveRequest.requestId, agentId: "agent_sdk", archivedAt: "2026-05-16T01:00:00.000Z", }, }), ); await expect(archivePromise).resolves.toEqual({ archivedAt: "2026-05-16T01:00:00.000Z", }); expect(agent.current()?.archivedAt).toBe("2026-05-16T01:00:00.000Z"); expect(agent.archivedAt).toBe("2026-05-16T01:00:00.000Z"); unsubscribe(); await client.close(); }); test("agent handles list the session's own commands through the existing daemon RPC", async () => { const { client, ws } = await connectClient(); const agent = client.agents.ref("agent_sdk"); const commandsPromise = agent.commands({ requestId: "agent-commands-request" }); const request = parseSentSessionMessage(ws.sent.at(-1)); expect(request).toMatchObject({ type: "list_commands_request", agentId: "agent_sdk", requestId: "agent-commands-request", }); ws.message( sessionMessage({ type: "list_commands_response", payload: { requestId: "agent-commands-request", agentId: "agent_sdk", commands: [ { name: "brainstorming", description: "Turn an idea into a design", argumentHint: "[topic]", kind: "skill", }, { name: "usage", description: "Show usage", argumentHint: "", kind: "command", }, ], error: null, }, }), ); await expect(commandsPromise).resolves.toMatchObject({ agentId: "agent_sdk", error: null, commands: [ { name: "brainstorming", kind: "skill" }, { name: "usage", kind: "command" }, ], }); await client.close(); }); test("agent handles expose the observed snapshot through readonly properties", async () => { const { client, ws } = await connectClient(); const agent = client.agents.ref("agent_sdk"); expect(agent.capabilities).toBeNull(); expect(agent.availableModes).toBeNull(); expect(agent.pendingPermissions).toBeNull(); expect(agent.activeTurn).toBeNull(); expect(agent.lastUsage).toBeNull(); expect(agent.lastError).toBeNull(); expect(agent.features).toBeNull(); expect(agent.runtimeInfo).toBeNull(); expect(agent.archivedAt).toBeNull(); const observedAgent = createAgent({ capabilities: { supportsStreaming: false, supportsSessionPersistence: false, supportsDynamicModes: true, supportsMcpServers: false, supportsReasoningStream: false, supportsRewindBoth: true, supportsRewindConversation: true, supportsRewindFiles: true, supportsToolInvocations: false, }, availableModes: [{ id: "plan", label: "Plan" }], pendingPermissions: [{ id: "permission-sdk", provider: "codex", name: "shell", kind: "tool" }], activeTurn: { turnId: "turn-sdk", startedAt: "2026-05-16T00:05:00.000Z" }, lastUsage: { inputTokens: 120, outputTokens: 45, totalCostUsd: 0.02 }, lastError: "provider exited", features: [{ type: "toggle", id: "web-search", label: "Web search", value: true }], runtimeInfo: { provider: "codex", sessionId: "session-sdk", model: "gpt-5.4" }, archivedAt: "2026-05-16T02:00:00.000Z", }); await observeAgents(client, ws); const unsubscribe = agent.subscribe(() => {}); ws.message( sessionMessage({ type: "agent_update", payload: { subscriptionId: "agents-sdk", kind: "upsert", agent: observedAgent, project: null, }, }), ); expect(agent.capabilities).toEqual(observedAgent.capabilities); expect(agent.availableModes).toEqual([{ id: "plan", label: "Plan" }]); expect(agent.pendingPermissions).toEqual([ { id: "permission-sdk", provider: "codex", name: "shell", kind: "tool" }, ]); expect(agent.activeTurn).toEqual({ turnId: "turn-sdk", startedAt: "2026-05-16T00:05:00.000Z", }); expect(agent.lastUsage).toEqual({ inputTokens: 120, outputTokens: 45, totalCostUsd: 0.02 }); expect(agent.lastError).toBe("provider exited"); expect(agent.features).toEqual([ { type: "toggle", id: "web-search", label: "Web search", value: true }, ]); expect(agent.runtimeInfo).toEqual({ provider: "codex", sessionId: "session-sdk", model: "gpt-5.4", }); expect(agent.archivedAt).toBe("2026-05-16T02:00:00.000Z"); unsubscribe(); await client.close(); }); test("provider actions delegate to existing provider RPCs and local snapshot updates", async () => { const { client, ws } = await connectClient(); const modelsPromise = client.providers.listModels("codex", { cwd: "/repo/sdk", requestId: "provider-models-request", }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "list_provider_models_request", requestId: "provider-models-request", provider: "codex", cwd: "/repo/sdk", }); ws.message( sessionMessage({ type: "list_provider_models_response", payload: { requestId: "provider-models-request", provider: "codex", models: [{ provider: "codex", id: "gpt-5.4", label: "GPT-5.4" }], error: null, fetchedAt: "2026-05-16T00:00:00.000Z", }, }), ); await expect(modelsPromise).resolves.toMatchObject({ provider: "codex", models: [{ provider: "codex", id: "gpt-5.4", label: "GPT-5.4" }], }); const modesPromise = client.providers.listModes("codex", { cwd: "/repo/sdk", requestId: "provider-modes-request", }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "list_provider_modes_request", requestId: "provider-modes-request", provider: "codex", cwd: "/repo/sdk", }); ws.message( sessionMessage({ type: "list_provider_modes_response", payload: { requestId: "provider-modes-request", provider: "codex", modes: [{ id: "full-access", label: "Full Access" }], error: null, fetchedAt: "2026-05-16T00:00:00.000Z", }, }), ); await expect(modesPromise).resolves.toMatchObject({ provider: "codex", modes: [{ id: "full-access", label: "Full Access" }], }); const featuresPromise = client.providers.listFeatures( { provider: "codex/gpt-5.4", cwd: "/repo/sdk", modeId: "full-access", thinkingOptionId: "high", featureValues: { webSearch: true }, }, { requestId: "provider-features-request" }, ); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "list_provider_features_request", requestId: "provider-features-request", draftConfig: { provider: "codex", cwd: "/repo/sdk", modeId: "full-access", model: "gpt-5.4", thinkingOptionId: "high", featureValues: { webSearch: true }, }, }); ws.message( sessionMessage({ type: "list_provider_features_response", payload: { requestId: "provider-features-request", provider: "codex", features: [{ type: "toggle", id: "webSearch", label: "Web Search", value: true }], error: null, fetchedAt: "2026-05-16T00:00:00.000Z", }, }), ); await expect(featuresPromise).resolves.toMatchObject({ provider: "codex", features: [{ type: "toggle", id: "webSearch", label: "Web Search", value: true }], }); const availablePromise = client.providers.listAvailable({ requestId: "providers-available-request", }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "list_available_providers_request", requestId: "providers-available-request", }); ws.message( sessionMessage({ type: "list_available_providers_response", payload: { requestId: "providers-available-request", providers: [{ provider: "codex", available: true, error: null }], error: null, fetchedAt: "2026-05-16T00:00:00.000Z", }, }), ); await expect(availablePromise).resolves.toMatchObject({ providers: [{ provider: "codex", available: true, error: null }], }); const snapshotPromise = client.providers.snapshot({ cwd: "/repo/sdk", requestId: "providers-snapshot-request", }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "get_providers_snapshot_request", requestId: "providers-snapshot-request", cwd: "/repo/sdk", }); ws.message( sessionMessage({ type: "get_providers_snapshot_response", payload: { requestId: "providers-snapshot-request", entries: [{ provider: "codex", status: "ready", enabled: true }], generatedAt: "2026-05-16T00:00:00.000Z", }, }), ); await expect(snapshotPromise).resolves.toMatchObject({ entries: [{ provider: "codex", status: "ready", enabled: true }], }); const readyPromise = client.providers.waitForReady({ cwd: "/repo/sdk", timeoutMs: 5_000 }); acknowledgeObservation(ws, "provider-wait"); await new Promise((resolve) => setTimeout(resolve, 0)); const readyRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(readyRequest).toMatchObject({ type: "get_providers_snapshot_request", cwd: "/repo/sdk", }); ws.message( sessionMessage({ type: "get_providers_snapshot_response", payload: { requestId: readyRequest.requestId, cwd: "/repo/sdk", entries: [{ provider: "codex", status: "loading", enabled: true }], generatedAt: "2026-05-16T00:00:00.000Z", }, }), ); ws.message( sessionMessage({ type: "providers_snapshot_update", payload: { subscriptionId: "provider-wait", cwd: "/repo/other", entries: [{ provider: "codex", status: "ready", enabled: true }], generatedAt: "2026-05-16T00:00:30.000Z", }, }), ); ws.message( sessionMessage({ type: "providers_snapshot_update", payload: { subscriptionId: "provider-wait", cwd: "/repo/sdk", entries: [{ provider: "codex", status: "ready", enabled: true }], generatedAt: "2026-05-16T00:00:01.000Z", }, }), ); await expect(readyPromise).resolves.toMatchObject({ requestId: readyRequest.requestId, entries: [{ provider: "codex", status: "ready", enabled: true }], generatedAt: "2026-05-16T00:00:01.000Z", }); const canonicalReadyPromise = client.providers.waitForReady({ cwd: "/repo/./sdk", timeoutMs: 5_000, }); acknowledgeObservation(ws, "provider-canonical-wait"); await new Promise((resolve) => setTimeout(resolve, 0)); const canonicalReadyRequest = parseSentSessionMessage(ws.sent.at(-1)); ws.message( sessionMessage({ type: "providers_snapshot_update", payload: { subscriptionId: "provider-canonical-wait", cwd: "/repo/sdk", entries: [{ provider: "codex", status: "ready", enabled: true }], generatedAt: "2026-05-16T00:00:02.000Z", }, }), ); ws.message( sessionMessage({ type: "get_providers_snapshot_response", payload: { requestId: canonicalReadyRequest.requestId, cwd: "/repo/sdk", entries: [{ provider: "codex", status: "loading", enabled: true }], generatedAt: "2026-05-16T00:00:01.000Z", }, }), ); await expect(canonicalReadyPromise).resolves.toMatchObject({ requestId: canonicalReadyRequest.requestId, cwd: "/repo/sdk", entries: [{ provider: "codex", status: "ready", enabled: true }], }); const refreshPromise = client.providers.refresh({ cwd: "/repo/sdk", providers: ["codex"], requestId: "providers-refresh-request", }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "refresh_providers_snapshot_request", requestId: "providers-refresh-request", cwd: "/repo/sdk", providers: ["codex"], }); ws.message( sessionMessage({ type: "refresh_providers_snapshot_response", payload: { requestId: "providers-refresh-request", acknowledged: true, }, }), ); await expect(refreshPromise).resolves.toEqual({ requestId: "providers-refresh-request", acknowledged: true, }); const diagnosticPromise = client.providers.diagnostic("codex", { requestId: "provider-diagnostic-request", }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "provider_diagnostic_request", requestId: "provider-diagnostic-request", provider: "codex", }); ws.message( sessionMessage({ type: "provider_diagnostic_response", payload: { requestId: "provider-diagnostic-request", provider: "codex", diagnostic: "Codex is ready.", }, }), ); await expect(diagnosticPromise).resolves.toEqual({ requestId: "provider-diagnostic-request", provider: "codex", diagnostic: "Codex is ready.", }); const usagePromise = client.providers.listUsage({ requestId: "provider-usage-request", }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "provider.usage.list.request", requestId: "provider-usage-request", }); ws.message( sessionMessage({ type: "provider.usage.list.response", payload: { requestId: "provider-usage-request", fetchedAt: "2026-05-16T00:10:00.000Z", providers: [ { providerId: "codex", displayName: "Codex", status: "available", planLabel: "pro", windows: [ { id: "weekly", label: "Weekly limit", usedPct: 25, remainingPct: 75, resetsAt: "2026-05-20T00:00:00.000Z", tone: "ok", }, ], }, ], }, }), ); await expect(usagePromise).resolves.toEqual({ requestId: "provider-usage-request", fetchedAt: "2026-05-16T00:10:00.000Z", providers: [ { providerId: "codex", displayName: "Codex", status: "available", planLabel: "pro", windows: [ { id: "weekly", label: "Weekly limit", usedPct: 25, remainingPct: 75, resetsAt: "2026-05-20T00:00:00.000Z", tone: "ok", }, ], }, ], }); const snapshotUpdates: string[] = []; const snapshotModelDefaults: Array = []; const unsubscribe = client.providers.subscribe((update) => { snapshotUpdates.push(update.generatedAt); snapshotModelDefaults.push(update.entries[0]?.models?.[0]?.defaultThinkingOptionId); }); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "session.events.set_subscription.request", events: ["providers_snapshot_update"], }); acknowledgeObservation(ws, "provider-updates"); ws.message( sessionMessage({ type: "providers_snapshot_update", payload: { subscriptionId: "provider-updates", cwd: "/repo/sdk", entries: [ { provider: "codex", status: "ready", enabled: true, models: [ { provider: "codex", id: "gpt-5.5", label: "GPT-5.5", thinkingOptions: [{ id: "high", label: "High", isDefault: true }], }, ], }, ], generatedAt: "2026-05-16T01:00:00.000Z", }, }), ); expect(snapshotUpdates).toEqual(["2026-05-16T01:00:00.000Z"]); expect(snapshotModelDefaults).toEqual(["high"]); unsubscribe(); await client.close(); }); test("waitForReady reads an older host on the existing connection", async () => { const { client, ws } = await connectClient({}); const waiting = client.providers.waitForReady({ cwd: "/repo/./sdk" }); await expect .poll(() => parseSentSessionMessage(ws.sent.at(-1)).type) .toBe("get_providers_snapshot_request"); const request = parseSentSessionMessage(ws.sent.at(-1)); ws.message( sessionMessage({ type: "get_providers_snapshot_response", payload: { requestId: request.requestId, entries: [], generatedAt: "2026-09-11T00:00:00.000Z", }, }), ); await expect(waiting).resolves.toMatchObject({ entries: [] }); expect(FakeWebSocket.instances).toHaveLength(1); await client.close(); }); test("provider usage requires the advertised host capability", async () => { const { client, ws } = await connectClient({}); const sentBeforeUsage = ws.sent.length; await expect(client.providers.listUsage()).rejects.toThrow( "Update the host to list provider usage.", ); expect(ws.sent).toHaveLength(sentBeforeUsage); await client.close(); }); test("config actions delegate to existing daemon config RPCs", async () => { const { client, ws } = await connectClient(); const getPromise = client.config.get("config-get-request"); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "get_daemon_config_request", requestId: "config-get-request", }); ws.message( sessionMessage({ type: "get_daemon_config_response", payload: { requestId: "config-get-request", config: { mcp: { injectIntoAgents: true }, providers: {}, autoArchiveAfterMerge: false, }, }, }), ); await expect(getPromise).resolves.toEqual({ requestId: "config-get-request", config: { mcp: { injectIntoAgents: true }, providers: {}, browserTools: { enabled: false }, metadataGeneration: { providers: [] }, autoArchiveAfterMerge: false, enableTerminalAgentHooks: false, appendSystemPrompt: "", }, }); const patchPromise = client.config.patch( { providers: { codex: { enabled: false, }, }, }, "config-patch-request", ); expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({ type: "set_daemon_config_request", requestId: "config-patch-request", config: { providers: { codex: { enabled: false, }, }, }, }); ws.message( sessionMessage({ type: "set_daemon_config_response", payload: { requestId: "config-patch-request", config: { mcp: { injectIntoAgents: true }, providers: { codex: { enabled: false, }, }, autoArchiveAfterMerge: false, }, }, }), ); await expect(patchPromise).resolves.toEqual({ requestId: "config-patch-request", config: { mcp: { injectIntoAgents: true }, providers: { codex: { enabled: false, }, }, browserTools: { enabled: false }, metadataGeneration: { providers: [] }, autoArchiveAfterMerge: false, enableTerminalAgentHooks: false, appendSystemPrompt: "", }, }); await client.close(); }); test("agent config maps provider/model and provider-native options to the daemon", async () => { const { client, ws } = await connectClient(); const createdAgent = createAgent({ model: "gpt-5.4", currentModeId: "full-access", }); const createPromise = client.agents.create({ config: { provider: "codex/gpt-5.4", modeId: "full-access", thinkingOptionId: "high", featureValues: { webSearch: true }, options: { approval_policy: "never", sandbox_mode: "workspace-write", sandbox_workspace_write: { writable_roots: ["/repo/sdk"], network_access: false, }, }, }, cwd: "/repo/sdk", prompt: "use configured provider", }); const request = parseSentSessionMessage(ws.sent.at(-1)); expect(request).toMatchObject({ type: "create_agent_request", config: { provider: "codex", cwd: "/repo/sdk", model: "gpt-5.4", modeId: "full-access", thinkingOptionId: "high", featureValues: { webSearch: true }, providerOptions: { approval_policy: "never", sandbox_mode: "workspace-write", sandbox_workspace_write: { writable_roots: ["/repo/sdk"], network_access: false, }, }, }, initialPrompt: "use configured provider", }); ws.message( sessionMessage({ type: "status", payload: { status: "agent_created", requestId: request.requestId, agentId: "agent_sdk", agent: createdAgent, }, }), ); const agent = await createPromise; expect(agent.current()).toEqual(createdAgent); await client.close(); }); test("agent config requires provider/model syntax", async () => { const { client } = await connectClient(); await expect( client.agents.create({ config: { provider: "codex" }, cwd: "/repo/sdk", }), ).rejects.toThrow('Expected config.provider in "provider/model" format'); await client.close(); }); test("canceled timeline handles and captured state are collectible while their API stays alive", async () => { const source = ` import assert from "node:assert/strict"; import { createPaseoApi } from ${JSON.stringify(new URL("./index.ts", import.meta.url).href)}; import { DaemonClient } from ${JSON.stringify(new URL("./daemon-client.ts", import.meta.url).href)}; const drivers = [], apis = []; let errors = 0; async function batch(mode) { const driver = new DaemonClient({ url: "ws://127.0.0.1:1/ws", clientId: mode, reconnect: { enabled: false } }); const scope = new AbortController(); const api = createPaseoApi(driver, { signal: scope.signal }); drivers.push(driver); apis.push(api); const refs = [], ready = []; for (let i = 0; i < 100; i++) { const state = { bytes: new Uint8Array(64 * 1024) }; const handle = api.agents.ref("unconnected-agent").timeline.subscribe(update => { if (update.event.type === "error") errors++; return state.bytes[0]; }); refs.push({ handle: new WeakRef(handle), state: new WeakRef(state) }); ready.push(handle.ready.catch(() => {})); if (mode === "callable") handle(); if (mode === "release") await handle.release(); } if (mode === "scope-abort") scope.abort(); if (mode === "client-close") await driver.close(); if (mode !== "active") await Promise.all(ready); return refs; } const groups = {}; for (const mode of ["callable", "release", "scope-abort", "client-close", "active"]) groups[mode] = await batch(mode); async function collect() { for (let i = 0; i < 12; i++) { await new Promise(r => setImmediate(r)); global.gc(); } } const count = refs => ({ handles: refs.filter(r => r.handle.deref()).length, states: refs.filter(r => r.state.deref()).length }); await collect(); const live = Object.fromEntries(Object.entries(groups).map(([mode, refs]) => [mode, count(refs)])); console.log(JSON.stringify({ phase: "API alive", live, errors })); for (const api of apis) await api.dispose(); await collect(); const disposed = Object.fromEntries(Object.entries(groups).map(([mode, refs]) => [mode, count(refs)])); console.log(JSON.stringify({ phase: "API disposed", disposed })); for (const driver of drivers) await driver.close(); for (const mode of ["callable", "release", "scope-abort", "client-close"]) assert.deepEqual(live[mode], { handles: 0, states: 0 }, mode); assert.deepEqual(live.active, { handles: 100, states: 100 }, "active ownership is retained"); assert.equal(errors, 100, "client shutdown reports failure; explicit cancellation does not"); for (const result of Object.values(disposed)) assert.deepEqual(result, { handles: 0, states: 0 }); `; const env = Object.fromEntries( Object.entries(process.env).filter(([key]) => !/^(PASEO_|EXPO_|E2E_|AGENT_BROWSER_)/.test(key)), ); const result = await promisify(execFile)( process.execPath, ["--expose-gc", "--import", "tsx", "--input-type=module", "-e", source], { env, timeout: 15000 }, ); expect(result.stdout).toContain('"phase":"API alive"'); }, 20000); test("qualified parents preserve foreign identity without resolving an equal local agent ID", async () => { const { client, ws } = await connectClient(); const localParent = client.agents.ref("same-parent"); expect(localParent.address).toEqual({ serverId: "srv_sdk_test", agentId: "same-parent" }); const creating = client.agents.create({ config: { provider: "codex/gpt-5.4" }, cwd: "/repo/sdk", parent: { serverId: "srv_parent_elsewhere", agentId: "same-parent" }, labels: { task: "preserved" }, }); const request = parseSentSessionMessage(ws.sent.at(-1)); expect(request).not.toHaveProperty("callerAgentId"); expect(request).toMatchObject({ labels: { task: "preserved", "paseo.parent-agent-id": "same-parent", "paseo.parent-server-id": "srv_parent_elsewhere", }, }); ws.message( sessionMessage({ type: "status", payload: { status: "agent_created", requestId: request.requestId, agentId: "agent_sdk", agent: createAgent(), }, }), ); await creating; await client.close(); expect(localParent.address).toEqual({ serverId: "srv_sdk_test", agentId: "same-parent" }); });