import { createClient } from "rivetkit/client"; import type { AgentType, AddRepoInput, AppConfig, CreateHandoffInput, HandoffRecord, HandoffSummary, HandoffWorkbenchChangeModelInput, HandoffWorkbenchCreateHandoffInput, HandoffWorkbenchCreateHandoffResponse, HandoffWorkbenchDiffInput, HandoffWorkbenchRenameInput, HandoffWorkbenchRenameSessionInput, HandoffWorkbenchSelectInput, HandoffWorkbenchSetSessionUnreadInput, HandoffWorkbenchSendMessageInput, HandoffWorkbenchSnapshot, HandoffWorkbenchTabInput, HandoffWorkbenchUpdateDraftInput, HistoryEvent, HistoryQueryInput, ProviderId, RepoOverview, RepoStackActionInput, RepoStackActionResult, RepoRecord, SwitchResult } from "@sandbox-agent/factory-shared"; import { sandboxInstanceKey, workspaceKey } from "./keys.js"; export type HandoffAction = "push" | "sync" | "merge" | "archive" | "kill"; type RivetMetadataResponse = { runtime?: string; actorNames?: Record; clientEndpoint?: string; clientNamespace?: string; clientToken?: string; }; export interface SandboxSessionRecord { id: string; agent: string; agentSessionId: string; lastConnectionId: string; createdAt: number; destroyedAt?: number; status?: "running" | "idle" | "error"; } export interface SandboxSessionEventRecord { id: string; eventIndex: number; sessionId: string; createdAt: number; connectionId: string; sender: "client" | "agent"; payload: unknown; } interface WorkspaceHandle { addRepo(input: AddRepoInput): Promise; listRepos(input: { workspaceId: string }): Promise; createHandoff(input: CreateHandoffInput): Promise; listHandoffs(input: { workspaceId: string; repoId?: string }): Promise; getRepoOverview(input: { workspaceId: string; repoId: string }): Promise; runRepoStackAction(input: RepoStackActionInput): Promise; history(input: HistoryQueryInput): Promise; switchHandoff(handoffId: string): Promise; getHandoff(input: { workspaceId: string; handoffId: string }): Promise; attachHandoff(input: { workspaceId: string; handoffId: string; reason?: string }): Promise<{ target: string; sessionId: string | null }>; pushHandoff(input: { workspaceId: string; handoffId: string; reason?: string }): Promise; syncHandoff(input: { workspaceId: string; handoffId: string; reason?: string }): Promise; mergeHandoff(input: { workspaceId: string; handoffId: string; reason?: string }): Promise; archiveHandoff(input: { workspaceId: string; handoffId: string; reason?: string }): Promise; killHandoff(input: { workspaceId: string; handoffId: string; reason?: string }): Promise; useWorkspace(input: { workspaceId: string }): Promise<{ workspaceId: string }>; getWorkbench(input: { workspaceId: string }): Promise; createWorkbenchHandoff(input: HandoffWorkbenchCreateHandoffInput): Promise; markWorkbenchUnread(input: HandoffWorkbenchSelectInput): Promise; renameWorkbenchHandoff(input: HandoffWorkbenchRenameInput): Promise; renameWorkbenchBranch(input: HandoffWorkbenchRenameInput): Promise; createWorkbenchSession(input: HandoffWorkbenchSelectInput & { model?: string }): Promise<{ tabId: string }>; renameWorkbenchSession(input: HandoffWorkbenchRenameSessionInput): Promise; setWorkbenchSessionUnread(input: HandoffWorkbenchSetSessionUnreadInput): Promise; updateWorkbenchDraft(input: HandoffWorkbenchUpdateDraftInput): Promise; changeWorkbenchModel(input: HandoffWorkbenchChangeModelInput): Promise; sendWorkbenchMessage(input: HandoffWorkbenchSendMessageInput): Promise; stopWorkbenchSession(input: HandoffWorkbenchTabInput): Promise; closeWorkbenchSession(input: HandoffWorkbenchTabInput): Promise; publishWorkbenchPr(input: HandoffWorkbenchSelectInput): Promise; revertWorkbenchFile(input: HandoffWorkbenchDiffInput): Promise; } interface SandboxInstanceHandle { createSession(input: { prompt: string; cwd?: string; agent?: AgentType | "opencode" }): Promise<{ id: string | null; status: "running" | "idle" | "error"; error?: string }>; listSessions(input?: { cursor?: string; limit?: number }): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }>; listSessionEvents(input: { sessionId: string; cursor?: string; limit?: number }): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }>; sendPrompt(input: { sessionId: string; prompt: string; notification?: boolean }): Promise; sessionStatus(input: { sessionId: string }): Promise<{ id: string; status: "running" | "idle" | "error" }>; providerState(): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }>; } interface RivetClient { workspace: { getOrCreate(key?: string | string[], opts?: { createWithInput?: unknown }): WorkspaceHandle; }; sandboxInstance: { getOrCreate(key?: string | string[], opts?: { createWithInput?: unknown }): SandboxInstanceHandle; }; } export interface BackendClientOptions { endpoint: string; defaultWorkspaceId?: string; } export interface BackendMetadata { runtime?: string; actorNames?: Record; clientEndpoint?: string; clientNamespace?: string; clientToken?: string; } export interface BackendClient { addRepo(workspaceId: string, remoteUrl: string): Promise; listRepos(workspaceId: string): Promise; createHandoff(input: CreateHandoffInput): Promise; listHandoffs(workspaceId: string, repoId?: string): Promise; getRepoOverview(workspaceId: string, repoId: string): Promise; runRepoStackAction(input: RepoStackActionInput): Promise; getHandoff(workspaceId: string, handoffId: string): Promise; listHistory(input: HistoryQueryInput): Promise; switchHandoff(workspaceId: string, handoffId: string): Promise; attachHandoff(workspaceId: string, handoffId: string): Promise<{ target: string; sessionId: string | null }>; runAction(workspaceId: string, handoffId: string, action: HandoffAction): Promise; createSandboxSession(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; prompt: string; cwd?: string; agent?: AgentType | "opencode"; }): Promise<{ id: string; status: "running" | "idle" | "error" }>; listSandboxSessions( workspaceId: string, providerId: ProviderId, sandboxId: string, input?: { cursor?: string; limit?: number } ): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }>; listSandboxSessionEvents( workspaceId: string, providerId: ProviderId, sandboxId: string, input: { sessionId: string; cursor?: string; limit?: number } ): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }>; sendSandboxPrompt(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; sessionId: string; prompt: string; notification?: boolean; }): Promise; sandboxSessionStatus( workspaceId: string, providerId: ProviderId, sandboxId: string, sessionId: string ): Promise<{ id: string; status: "running" | "idle" | "error" }>; sandboxProviderState( workspaceId: string, providerId: ProviderId, sandboxId: string ): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }>; getWorkbench(workspaceId: string): Promise; subscribeWorkbench(workspaceId: string, listener: () => void): () => void; createWorkbenchHandoff( workspaceId: string, input: HandoffWorkbenchCreateHandoffInput ): Promise; markWorkbenchUnread(workspaceId: string, input: HandoffWorkbenchSelectInput): Promise; renameWorkbenchHandoff(workspaceId: string, input: HandoffWorkbenchRenameInput): Promise; renameWorkbenchBranch(workspaceId: string, input: HandoffWorkbenchRenameInput): Promise; createWorkbenchSession( workspaceId: string, input: HandoffWorkbenchSelectInput & { model?: string } ): Promise<{ tabId: string }>; renameWorkbenchSession(workspaceId: string, input: HandoffWorkbenchRenameSessionInput): Promise; setWorkbenchSessionUnread( workspaceId: string, input: HandoffWorkbenchSetSessionUnreadInput ): Promise; updateWorkbenchDraft(workspaceId: string, input: HandoffWorkbenchUpdateDraftInput): Promise; changeWorkbenchModel(workspaceId: string, input: HandoffWorkbenchChangeModelInput): Promise; sendWorkbenchMessage(workspaceId: string, input: HandoffWorkbenchSendMessageInput): Promise; stopWorkbenchSession(workspaceId: string, input: HandoffWorkbenchTabInput): Promise; closeWorkbenchSession(workspaceId: string, input: HandoffWorkbenchTabInput): Promise; publishWorkbenchPr(workspaceId: string, input: HandoffWorkbenchSelectInput): Promise; revertWorkbenchFile(workspaceId: string, input: HandoffWorkbenchDiffInput): Promise; health(): Promise<{ ok: true }>; useWorkspace(workspaceId: string): Promise<{ workspaceId: string }>; } export function rivetEndpoint(config: AppConfig): string { return `http://${config.backend.host}:${config.backend.port}/api/rivet`; } export function createBackendClientFromConfig(config: AppConfig): BackendClient { return createBackendClient({ endpoint: rivetEndpoint(config), defaultWorkspaceId: config.workspace.default }); } function isLoopbackHost(hostname: string): boolean { const h = hostname.toLowerCase(); return h === "127.0.0.1" || h === "localhost" || h === "0.0.0.0" || h === "::1"; } function rewriteLoopbackClientEndpoint(clientEndpoint: string, fallbackOrigin: string): string { const clientUrl = new URL(clientEndpoint); if (!isLoopbackHost(clientUrl.hostname)) { return clientUrl.toString().replace(/\/$/, ""); } const originUrl = new URL(fallbackOrigin); // Keep the manager port from clientEndpoint; only rewrite host/protocol to match the origin. clientUrl.hostname = originUrl.hostname; clientUrl.protocol = originUrl.protocol; return clientUrl.toString().replace(/\/$/, ""); } async function fetchJsonWithTimeout(url: string, timeoutMs: number): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(url, { signal: controller.signal }); if (!res.ok) { throw new Error(`request failed: ${res.status} ${res.statusText}`); } return (await res.json()) as unknown; } finally { clearTimeout(timeout); } } async function fetchMetadataWithRetry( endpoint: string, namespace: string | undefined, opts: { timeoutMs: number; requestTimeoutMs: number } ): Promise { const base = new URL(endpoint); base.pathname = base.pathname.replace(/\/$/, "") + "/metadata"; if (namespace) { base.searchParams.set("namespace", namespace); } const start = Date.now(); let delayMs = 250; // Keep this bounded: callers (UI/CLI) should not hang forever if the backend is down. for (;;) { try { const json = await fetchJsonWithTimeout(base.toString(), opts.requestTimeoutMs); if (!json || typeof json !== "object") return {}; const data = json as Record; return { runtime: typeof data.runtime === "string" ? data.runtime : undefined, actorNames: data.actorNames && typeof data.actorNames === "object" ? (data.actorNames as Record) : undefined, clientEndpoint: typeof data.clientEndpoint === "string" ? data.clientEndpoint : undefined, clientNamespace: typeof data.clientNamespace === "string" ? data.clientNamespace : undefined, clientToken: typeof data.clientToken === "string" ? data.clientToken : undefined, }; } catch (err) { if (Date.now() - start > opts.timeoutMs) { throw err; } await new Promise((r) => setTimeout(r, delayMs)); delayMs = Math.min(delayMs * 2, 2_000); } } } export async function readBackendMetadata(input: { endpoint: string; namespace?: string; timeoutMs?: number; }): Promise { const base = new URL(input.endpoint); base.pathname = base.pathname.replace(/\/$/, "") + "/metadata"; if (input.namespace) { base.searchParams.set("namespace", input.namespace); } const json = await fetchJsonWithTimeout(base.toString(), input.timeoutMs ?? 4_000); if (!json || typeof json !== "object") { return {}; } const data = json as Record; return { runtime: typeof data.runtime === "string" ? data.runtime : undefined, actorNames: data.actorNames && typeof data.actorNames === "object" ? (data.actorNames as Record) : undefined, clientEndpoint: typeof data.clientEndpoint === "string" ? data.clientEndpoint : undefined, clientNamespace: typeof data.clientNamespace === "string" ? data.clientNamespace : undefined, clientToken: typeof data.clientToken === "string" ? data.clientToken : undefined, }; } export async function checkBackendHealth(input: { endpoint: string; namespace?: string; timeoutMs?: number; }): Promise { try { const metadata = await readBackendMetadata(input); return metadata.runtime === "rivetkit" && Boolean(metadata.actorNames); } catch { return false; } } async function probeMetadataEndpoint( endpoint: string, namespace: string | undefined, timeoutMs: number ): Promise { try { const base = new URL(endpoint); base.pathname = base.pathname.replace(/\/$/, "") + "/metadata"; if (namespace) { base.searchParams.set("namespace", namespace); } await fetchJsonWithTimeout(base.toString(), timeoutMs); return true; } catch { return false; } } export function createBackendClient(options: BackendClientOptions): BackendClient { let clientPromise: Promise | null = null; const workbenchSubscriptions = new Map< string, { listeners: Set<() => void>; disposeConnPromise: Promise<(() => Promise) | null> | null; } >(); const getClient = async (): Promise => { if (clientPromise) { return clientPromise; } clientPromise = (async () => { // Use the serverless /metadata endpoint to discover the manager endpoint. // If the server reports a loopback clientEndpoint (127.0.0.1), rewrite to the same host // as the configured endpoint so remote browsers/clients can connect. const configured = new URL(options.endpoint); const configuredOrigin = `${configured.protocol}//${configured.host}`; const initialNamespace = undefined; const metadata = await fetchMetadataWithRetry(options.endpoint, initialNamespace, { timeoutMs: 30_000, requestTimeoutMs: 8_000 }); // Candidate endpoint: manager endpoint if provided, otherwise stick to the configured endpoint. const candidateEndpoint = metadata.clientEndpoint ? rewriteLoopbackClientEndpoint(metadata.clientEndpoint, configuredOrigin) : options.endpoint; // If the manager port isn't reachable from this client (common behind reverse proxies), // fall back to the configured serverless endpoint to avoid hanging requests. const shouldUseCandidate = metadata.clientEndpoint ? await probeMetadataEndpoint(candidateEndpoint, metadata.clientNamespace, 1_500) : true; const resolvedEndpoint = shouldUseCandidate ? candidateEndpoint : options.endpoint; return createClient({ endpoint: resolvedEndpoint, namespace: metadata.clientNamespace, token: metadata.clientToken, // Prevent rivetkit from overriding back to a loopback endpoint (or to an unreachable manager). disableMetadataLookup: true, }) as unknown as RivetClient; })(); return clientPromise; }; const workspace = async (workspaceId: string): Promise => (await getClient()).workspace.getOrCreate(workspaceKey(workspaceId), { createWithInput: workspaceId }); const sandboxByKey = async ( workspaceId: string, providerId: ProviderId, sandboxId: string ): Promise => { const client = await getClient(); return (client as any).sandboxInstance.get(sandboxInstanceKey(workspaceId, providerId, sandboxId)); }; function isActorNotFoundError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); return message.includes("Actor not found"); } const sandboxByActorIdFromHandoff = async ( workspaceId: string, providerId: ProviderId, sandboxId: string ): Promise => { const ws = await workspace(workspaceId); const rows = await ws.listHandoffs({ workspaceId }); const candidates = [...rows].sort((a, b) => b.updatedAt - a.updatedAt); for (const row of candidates) { try { const detail = await ws.getHandoff({ workspaceId, handoffId: row.handoffId }); if (detail.providerId !== providerId) { continue; } const sandbox = detail.sandboxes.find((sb) => sb.sandboxId === sandboxId && sb.providerId === providerId && typeof (sb as any).sandboxActorId === "string" && (sb as any).sandboxActorId.length > 0 ) as ({ sandboxActorId?: string } | undefined); if (sandbox?.sandboxActorId) { const client = await getClient(); return (client as any).sandboxInstance.getForId(sandbox.sandboxActorId); } } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!isActorNotFoundError(error) && !message.includes("Unknown handoff")) { throw error; } // Best effort fallback path; ignore missing handoff actors here. } } return null; }; const withSandboxHandle = async ( workspaceId: string, providerId: ProviderId, sandboxId: string, run: (handle: SandboxInstanceHandle) => Promise ): Promise => { const handle = await sandboxByKey(workspaceId, providerId, sandboxId); try { return await run(handle); } catch (error) { if (!isActorNotFoundError(error)) { throw error; } const fallback = await sandboxByActorIdFromHandoff(workspaceId, providerId, sandboxId); if (!fallback) { throw error; } return await run(fallback); } }; const subscribeWorkbench = (workspaceId: string, listener: () => void): (() => void) => { let entry = workbenchSubscriptions.get(workspaceId); if (!entry) { entry = { listeners: new Set(), disposeConnPromise: null, }; workbenchSubscriptions.set(workspaceId, entry); } entry.listeners.add(listener); if (!entry.disposeConnPromise) { entry.disposeConnPromise = (async () => { const handle = await workspace(workspaceId); const conn = (handle as any).connect(); const unsubscribeEvent = conn.on("workbenchUpdated", () => { const current = workbenchSubscriptions.get(workspaceId); if (!current) { return; } for (const currentListener of [...current.listeners]) { currentListener(); } }); const unsubscribeError = conn.onError(() => {}); return async () => { unsubscribeEvent(); unsubscribeError(); await conn.dispose(); }; })().catch(() => null); } return () => { const current = workbenchSubscriptions.get(workspaceId); if (!current) { return; } current.listeners.delete(listener); if (current.listeners.size > 0) { return; } workbenchSubscriptions.delete(workspaceId); void current.disposeConnPromise?.then(async (disposeConn) => { await disposeConn?.(); }); }; }; return { async addRepo(workspaceId: string, remoteUrl: string): Promise { return (await workspace(workspaceId)).addRepo({ workspaceId, remoteUrl }); }, async listRepos(workspaceId: string): Promise { return (await workspace(workspaceId)).listRepos({ workspaceId }); }, async createHandoff(input: CreateHandoffInput): Promise { return (await workspace(input.workspaceId)).createHandoff(input); }, async listHandoffs(workspaceId: string, repoId?: string): Promise { return (await workspace(workspaceId)).listHandoffs({ workspaceId, repoId }); }, async getRepoOverview(workspaceId: string, repoId: string): Promise { return (await workspace(workspaceId)).getRepoOverview({ workspaceId, repoId }); }, async runRepoStackAction(input: RepoStackActionInput): Promise { return (await workspace(input.workspaceId)).runRepoStackAction(input); }, async getHandoff(workspaceId: string, handoffId: string): Promise { return (await workspace(workspaceId)).getHandoff({ workspaceId, handoffId }); }, async listHistory(input: HistoryQueryInput): Promise { return (await workspace(input.workspaceId)).history(input); }, async switchHandoff(workspaceId: string, handoffId: string): Promise { return (await workspace(workspaceId)).switchHandoff(handoffId); }, async attachHandoff(workspaceId: string, handoffId: string): Promise<{ target: string; sessionId: string | null }> { return (await workspace(workspaceId)).attachHandoff({ workspaceId, handoffId, reason: "cli.attach" }); }, async runAction(workspaceId: string, handoffId: string, action: HandoffAction): Promise { if (action === "push") { await (await workspace(workspaceId)).pushHandoff({ workspaceId, handoffId, reason: "cli.push" }); return; } if (action === "sync") { await (await workspace(workspaceId)).syncHandoff({ workspaceId, handoffId, reason: "cli.sync" }); return; } if (action === "merge") { await (await workspace(workspaceId)).mergeHandoff({ workspaceId, handoffId, reason: "cli.merge" }); return; } if (action === "archive") { await (await workspace(workspaceId)).archiveHandoff({ workspaceId, handoffId, reason: "cli.archive" }); return; } await (await workspace(workspaceId)).killHandoff({ workspaceId, handoffId, reason: "cli.kill" }); }, async createSandboxSession(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; prompt: string; cwd?: string; agent?: AgentType | "opencode"; }): Promise<{ id: string; status: "running" | "idle" | "error" }> { const created = await withSandboxHandle( input.workspaceId, input.providerId, input.sandboxId, async (handle) => handle.createSession({ prompt: input.prompt, cwd: input.cwd, agent: input.agent }) ); if (!created.id) { throw new Error(created.error ?? "sandbox session creation failed"); } return { id: created.id, status: created.status }; }, async listSandboxSessions( workspaceId: string, providerId: ProviderId, sandboxId: string, input?: { cursor?: string; limit?: number } ): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }> { return await withSandboxHandle( workspaceId, providerId, sandboxId, async (handle) => handle.listSessions(input ?? {}) ); }, async listSandboxSessionEvents( workspaceId: string, providerId: ProviderId, sandboxId: string, input: { sessionId: string; cursor?: string; limit?: number } ): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }> { return await withSandboxHandle( workspaceId, providerId, sandboxId, async (handle) => handle.listSessionEvents(input) ); }, async sendSandboxPrompt(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; sessionId: string; prompt: string; notification?: boolean; }): Promise { await withSandboxHandle( input.workspaceId, input.providerId, input.sandboxId, async (handle) => handle.sendPrompt({ sessionId: input.sessionId, prompt: input.prompt, notification: input.notification }) ); }, async sandboxSessionStatus( workspaceId: string, providerId: ProviderId, sandboxId: string, sessionId: string ): Promise<{ id: string; status: "running" | "idle" | "error" }> { return await withSandboxHandle( workspaceId, providerId, sandboxId, async (handle) => handle.sessionStatus({ sessionId }) ); }, async sandboxProviderState( workspaceId: string, providerId: ProviderId, sandboxId: string ): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }> { return await withSandboxHandle( workspaceId, providerId, sandboxId, async (handle) => handle.providerState() ); }, async getWorkbench(workspaceId: string): Promise { return (await workspace(workspaceId)).getWorkbench({ workspaceId }); }, subscribeWorkbench(workspaceId: string, listener: () => void): () => void { return subscribeWorkbench(workspaceId, listener); }, async createWorkbenchHandoff( workspaceId: string, input: HandoffWorkbenchCreateHandoffInput ): Promise { return (await workspace(workspaceId)).createWorkbenchHandoff(input); }, async markWorkbenchUnread(workspaceId: string, input: HandoffWorkbenchSelectInput): Promise { await (await workspace(workspaceId)).markWorkbenchUnread(input); }, async renameWorkbenchHandoff(workspaceId: string, input: HandoffWorkbenchRenameInput): Promise { await (await workspace(workspaceId)).renameWorkbenchHandoff(input); }, async renameWorkbenchBranch(workspaceId: string, input: HandoffWorkbenchRenameInput): Promise { await (await workspace(workspaceId)).renameWorkbenchBranch(input); }, async createWorkbenchSession( workspaceId: string, input: HandoffWorkbenchSelectInput & { model?: string } ): Promise<{ tabId: string }> { return await (await workspace(workspaceId)).createWorkbenchSession(input); }, async renameWorkbenchSession( workspaceId: string, input: HandoffWorkbenchRenameSessionInput ): Promise { await (await workspace(workspaceId)).renameWorkbenchSession(input); }, async setWorkbenchSessionUnread( workspaceId: string, input: HandoffWorkbenchSetSessionUnreadInput ): Promise { await (await workspace(workspaceId)).setWorkbenchSessionUnread(input); }, async updateWorkbenchDraft( workspaceId: string, input: HandoffWorkbenchUpdateDraftInput ): Promise { await (await workspace(workspaceId)).updateWorkbenchDraft(input); }, async changeWorkbenchModel( workspaceId: string, input: HandoffWorkbenchChangeModelInput ): Promise { await (await workspace(workspaceId)).changeWorkbenchModel(input); }, async sendWorkbenchMessage( workspaceId: string, input: HandoffWorkbenchSendMessageInput ): Promise { await (await workspace(workspaceId)).sendWorkbenchMessage(input); }, async stopWorkbenchSession(workspaceId: string, input: HandoffWorkbenchTabInput): Promise { await (await workspace(workspaceId)).stopWorkbenchSession(input); }, async closeWorkbenchSession(workspaceId: string, input: HandoffWorkbenchTabInput): Promise { await (await workspace(workspaceId)).closeWorkbenchSession(input); }, async publishWorkbenchPr(workspaceId: string, input: HandoffWorkbenchSelectInput): Promise { await (await workspace(workspaceId)).publishWorkbenchPr(input); }, async revertWorkbenchFile(workspaceId: string, input: HandoffWorkbenchDiffInput): Promise { await (await workspace(workspaceId)).revertWorkbenchFile(input); }, async health(): Promise<{ ok: true }> { const workspaceId = options.defaultWorkspaceId; if (!workspaceId) { throw new Error("Backend client default workspace is required for health checks"); } await (await workspace(workspaceId)).useWorkspace({ workspaceId }); return { ok: true }; }, async useWorkspace(workspaceId: string): Promise<{ workspaceId: string }> { return (await workspace(workspaceId)).useWorkspace({ workspaceId }); } }; }