import type { z } from "zod"; import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities"; import type { AgentAttentionNotificationPayload } from "@getpaseo/protocol/agent-attention-notification"; import { AgentCreateFailedStatusPayloadSchema, AgentCreatedStatusPayloadSchema, AgentRefreshedStatusPayloadSchema, AgentResumedStatusPayloadSchema, CheckoutRenameBranchResponseSchema, parseServerInfoStatusPayload, RenameTerminalResponseSchema, RestartRequestedStatusPayloadSchema, ShutdownRequestedStatusPayloadSchema, DaemonUpdateResponseSchema, SessionInboundMessageSchema, type ServerInfoStatusPayload, } from "@getpaseo/protocol/messages"; import { validateWSOutboundMessage } from "@getpaseo/protocol/validation/ws-outbound"; import type { AgentStreamEventPayload, AgentSnapshotPayload, ProjectPlacementPayload, AgentPermissionResolvedMessage, CreateAgentRequestMessage, CreatePaseoWorktreeRequest, FileDownloadTokenResponse, FileUploadResponse, FileExplorerResponse, FileVersion, FileWriteResult, FetchAgentTimelineResponseMessage, AgentForkContextResponseMessage, GitSetupOptions, CheckoutStatusResponse, CheckoutCommit, ParsedDiffFile, CheckoutCommitResponse, CheckoutMergeResponse, CheckoutMergeFromBaseResponse, CheckoutPullResponse, CheckoutPushResponse, CheckoutRefreshResponse, CheckoutPrCreateResponse, CheckoutPrMergeResponse, CheckoutPrMergeMethod, CheckoutForgeSetAutoMergeResponse, CheckoutGithubSetAutoMergeResponse, CheckoutForgeGetCheckDetailsResponse, CheckoutGithubGetCheckDetailsResponse, CheckoutPrStatusResponse, PullRequestTimelineResponse, CheckoutSwitchBranchResponse, StashSaveResponse, StashPopResponse, StashListResponse, ValidateBranchResponse, BranchSuggestionsResponse, ForgeSearchResponse, ForgeSearchRequest, GitHubSearchResponse, GitHubSearchRequest, DirectorySuggestionsResponse, PaseoWorktreeListResponse, PaseoWorktreeArchiveResponse, ProjectIconSource, ProjectIconResponse, ProjectIconGetResponse, ProjectAddResponse, ProjectCreateDirectoryResponse, OpenProjectResponseMessage, WorkspaceGithubSearchRepositoriesResponse, ProjectGithubCloneProtocol, ProjectGithubCloneResponse, ArchiveWorkspaceResponseMessage, WorkspaceSetupStatusResponseMessage, ListCommandsResponse, ListProviderFeaturesResponseMessage, ListProviderModelsResponseMessage, ListProviderModesResponseMessage, ListAvailableProvidersResponse, GetProvidersSnapshotResponseMessage, RefreshProvidersSnapshotResponseMessage, ProviderDiagnosticResponseMessage, ProviderUsageListResponseMessage, DaemonGetStatusResponse, DaemonGetPairingOfferResponse, DiagnosticsResponse, AgentRewindResponseMessage, ListTerminalsResponse, CreateTerminalResponse, SubscribeTerminalResponse, SubscribeTerminalRequest, CloseItemsResponse, KillTerminalResponse, CaptureTerminalResponse, TerminalInput, SessionInboundMessage, SessionOutboundMessage, SendAgentMessageRequest, PaseoConfigRaw, PaseoConfigRevision, WorkspaceCreateRequest, WorkspaceRecoveryState, } from "@getpaseo/protocol/messages"; import type { AgentPermissionRequest, AgentPermissionResponse, AgentPersistenceHandle, AgentProviderNotice, AgentProvider, AgentSessionConfig, } from "@getpaseo/protocol/agent-types"; import type { MutableDaemonConfig, MutableDaemonConfigPatch } from "@getpaseo/protocol/messages"; import { isRelayClientWebSocketUrl } from "@getpaseo/protocol/daemon-endpoints"; import { terminalSubscriptionKey } from "@getpaseo/protocol/terminal-subscription-key"; import { asUint8Array, decodeFileTransferFrame, encodeFileTransferFrame, decodeTerminalStreamFrame, FileTransferOpcode, TerminalStreamOpcode, type FileTransferFrame, } from "@getpaseo/protocol/binary-frames/index"; import { createRelayE2eeTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeTransportClose, describeTransportError, type DaemonTransport, type DaemonTransportFactory, type WebSocketFactory, } from "./daemon-client-transport.js"; import { DaemonClientRuntimeMetrics } from "./daemon-client-runtime-metrics.js"; import { normalizeListProviderModelsPayload, normalizeProviderSnapshotUpdateMessage, normalizeProvidersSnapshotPayload, } from "./compat/normalize-provider-models.js"; import { TerminalStreamRouter, type TerminalStreamEvent } from "./terminal-stream-router.js"; import type { BrowserAutomationExecuteRequest, BrowserAutomationExecuteResponse, } from "@getpaseo/protocol/browser-automation/rpc-schemas"; export interface Logger { debug(obj: object, msg?: string): void; info(obj: object, msg?: string): void; warn(obj: object, msg?: string): void; error(obj: object, msg?: string): void; } const consoleLogger: Logger = { debug: () => {}, info: (obj, msg) => console.log(msg, obj), warn: (obj, msg) => console.warn(msg, obj), error: (obj, msg) => console.error(msg, obj), }; const perfNow: () => number = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now(); const PROJECT_GITHUB_CLONE_TIMEOUT_MS = 5 * 60 * 1000; interface ImportAgentInputBase { cwd?: string; workspaceId?: string; labels?: Record; } export type ImportAgentInput = | (ImportAgentInputBase & { providerId: string; providerHandleId: string; }) | (ImportAgentInputBase & { provider: AgentProvider; sessionId: string; }); function normalizePassword(value: string | undefined): string | null { if (typeof value !== "string") { return null; } return value.length > 0 ? value : null; } function extractCorrelatedResponseIdentity(input: unknown): CorrelatedResponseIdentity | null { if (!input || typeof input !== "object") { return null; } const envelope = input as { type?: unknown; message?: unknown }; if (envelope.type !== "session" || !envelope.message || typeof envelope.message !== "object") { return null; } const message = envelope.message as { type?: unknown; payload?: unknown }; if ( typeof message.type !== "string" || !( message.type === "rpc_error" || message.type.endsWith("_response") || message.type.endsWith(".response") || message.type.endsWith("/response") ) ) { return null; } if (!message.payload || typeof message.payload !== "object") { return null; } const payload = message.payload as { requestId?: unknown }; if (typeof payload.requestId !== "string") { return null; } return { requestId: payload.requestId, responseType: message.type, }; } export type { DaemonTransport, DaemonTransportFactory, WebSocketFactory, WebSocketLike, } from "./daemon-client-transport.js"; export type { TerminalStreamEvent }; export type ConnectionState = | { status: "idle" } | { status: "connecting"; attempt: number } | { status: "connected" } | { status: "disconnected"; reason?: string } | { status: "disposed" }; export type DaemonEvent = | { type: "agent_update"; agentId: string; payload: Extract["payload"]; } | { type: "workspace_update"; workspaceId: string; payload: Extract["payload"]; } | { type: "project.update"; payload: Extract["payload"]; } | { type: "workspace_setup_progress"; workspaceId: string; payload: Extract["payload"]; } | { type: "agent_stream"; agentId: string; event: AgentStreamEventPayload; timestamp: string; seq?: number; epoch?: string; } | { type: "status"; payload: { status: string } & Record } | { type: "agent_deleted"; agentId: string } | { type: "agent_permission_request"; agentId: string; request: AgentPermissionRequest; } | { type: "agent_permission_resolved"; agentId: string; requestId: string; resolution: AgentPermissionResponse; } | { type: "providers_snapshot_update"; payload: Extract["payload"]; } | { type: "error"; message: string }; export type DaemonEventHandler = (event: DaemonEvent) => void; export type BrowserAutomationExecuteRequestMessage = BrowserAutomationExecuteRequest; export type BrowserAutomationExecuteResponseMessage = BrowserAutomationExecuteResponse; export interface DaemonClientConfig { url: string; clientId: string; clientType?: "mobile" | "browser" | "cli" | "mcp"; appVersion?: string; runtimeGeneration?: number | null; password?: string; authHeader?: string; suppressSendErrors?: boolean; transportFactory?: DaemonTransportFactory; webSocketFactory?: WebSocketFactory; logger?: Logger; connectTimeoutMs?: number; e2ee?: { enabled?: boolean; daemonPublicKeyB64?: string; }; reconnect?: { enabled?: boolean; baseDelayMs?: number; maxDelayMs?: number; }; runtimeMetricsIntervalMs?: number; runtimeMetricsWindowMs?: number; capabilities?: Partial>; } export interface SendMessageOptions { messageId?: string; images?: Array<{ data: string; mimeType: string }>; attachments?: SendAgentMessageRequest["attachments"]; } export interface AgentAttentionRequiredNotification { agentId: string; reason: "finished" | "error" | "permission"; timestamp: string; shouldNotify: boolean; notification?: AgentAttentionNotificationPayload; } type AgentConfigOverrides = Partial>; export interface CreateAgentRequestOptions extends AgentConfigOverrides { config?: AgentSessionConfig; provider?: AgentProvider; cwd?: string; env?: CreateAgentRequestMessage["env"]; workspaceId?: string; callerAgentId?: string; initialPrompt?: string; clientMessageId?: string; outputSchema?: Record; images?: CreateAgentRequestMessage["images"]; attachments?: CreateAgentRequestMessage["attachments"]; git?: GitSetupOptions; worktree?: CreateAgentRequestMessage["worktree"]; autoArchive?: CreateAgentRequestMessage["autoArchive"]; // COMPAT(createAgentWorktree): low-level old callers may still send the // create-agent worktree field. Added in v0.2.0; remove after 2027-01-17. worktreeName?: string; requestId?: string; labels?: Record; } export interface CreatePaseoWorktreeInput extends Pick< CreatePaseoWorktreeRequest, | "cwd" | "projectId" | "worktreeSlug" | "firstAgentContext" | "refName" | "action" | "checkoutSource" | "githubPrNumber" > {} type CheckoutStatusPayload = CheckoutStatusResponse["payload"]; type SubscribeCheckoutDiffPayload = Extract< SessionOutboundMessage, { type: "subscribe_checkout_diff_response" } >["payload"]; type CheckoutDiffPayload = Omit; type CheckoutCommitPayload = CheckoutCommitResponse["payload"]; type CheckoutMergePayload = CheckoutMergeResponse["payload"]; type CheckoutMergeFromBasePayload = CheckoutMergeFromBaseResponse["payload"]; type CheckoutPullPayload = CheckoutPullResponse["payload"]; type CheckoutPushPayload = CheckoutPushResponse["payload"]; type CheckoutRefreshPayload = CheckoutRefreshResponse["payload"]; type CheckoutPrCreatePayload = CheckoutPrCreateResponse["payload"]; type CheckoutPrMergePayload = CheckoutPrMergeResponse["payload"]; type CheckoutForgeSetAutoMergePayload = CheckoutForgeSetAutoMergeResponse["payload"]; type CheckoutGithubSetAutoMergePayload = CheckoutGithubSetAutoMergeResponse["payload"]; type CheckoutForgeGetCheckDetailsPayload = CheckoutForgeGetCheckDetailsResponse["payload"]; type CheckoutGithubGetCheckDetailsPayload = CheckoutGithubGetCheckDetailsResponse["payload"]; type CheckoutPrStatusPayload = CheckoutPrStatusResponse["payload"]; type PullRequestTimelinePayload = PullRequestTimelineResponse["payload"]; type CheckoutSwitchBranchPayload = CheckoutSwitchBranchResponse["payload"]; export type RenameBranchResult = z.infer["payload"]; type StashSavePayload = StashSaveResponse["payload"]; type StashPopPayload = StashPopResponse["payload"]; type StashListPayload = StashListResponse["payload"]; type ValidateBranchPayload = ValidateBranchResponse["payload"]; type BranchSuggestionsPayload = BranchSuggestionsResponse["payload"]; type ForgeSearchPayload = ForgeSearchResponse["payload"]; type GitHubSearchPayload = GitHubSearchResponse["payload"]; type DirectorySuggestionsPayload = DirectorySuggestionsResponse["payload"]; type PaseoWorktreeListPayload = PaseoWorktreeListResponse["payload"]; type PaseoWorktreeArchivePayload = PaseoWorktreeArchiveResponse["payload"]; type CreatePaseoWorktreePayload = Extract< SessionOutboundMessage, { type: "create_paseo_worktree_response" } >["payload"]; type WorkspaceCreatePayload = Extract< SessionOutboundMessage, { type: "workspace.create.response" } >["payload"]; type FileExplorerPayload = FileExplorerResponse["payload"]; export type FileExplorerDirectoryPayload = NonNullable; type LegacyFileExplorerFilePayload = NonNullable; export interface FileReadResult { bytes: Uint8Array; mime: string; size: number; path: string; kind: LegacyFileExplorerFilePayload["kind"]; modifiedAt: string; revision?: string; } export interface FileUploadInput { fileName: string; mimeType: string; bytes: Uint8Array | ArrayBuffer; modifiedAt?: string; requestId?: string; chunkSize?: number; } export type FileUploadResult = FileUploadResponse["payload"]; type FileDownloadTokenPayload = FileDownloadTokenResponse["payload"]; type ListProviderFeaturesPayload = ListProviderFeaturesResponseMessage["payload"]; type ListProviderModelsPayload = ListProviderModelsResponseMessage["payload"]; type ListProviderModesPayload = ListProviderModesResponseMessage["payload"]; type ListAvailableProvidersPayload = ListAvailableProvidersResponse["payload"]; type GetProvidersSnapshotPayload = GetProvidersSnapshotResponseMessage["payload"]; type RefreshProvidersSnapshotPayload = RefreshProvidersSnapshotResponseMessage["payload"]; type ProviderDiagnosticPayload = ProviderDiagnosticResponseMessage["payload"]; type ProviderUsageListPayload = ProviderUsageListResponseMessage["payload"]; type DaemonStatusPayload = DaemonGetStatusResponse["payload"]; type DaemonPairingOfferPayload = DaemonGetPairingOfferResponse["payload"]; type DiagnosticsPayload = DiagnosticsResponse["payload"]; type ReadProjectConfigPayload = Extract< SessionOutboundMessage, { type: "read_project_config_response" } >["payload"]; type WriteProjectConfigPayload = Extract< SessionOutboundMessage, { type: "write_project_config_response" } >["payload"]; type ListCommandsPayload = ListCommandsResponse["payload"]; type ListCommandsDraftConfig = Pick< AgentSessionConfig, "provider" | "cwd" | "modeId" | "model" | "thinkingOptionId" | "featureValues" >; export interface WriteProjectConfigInput { repoRoot: string; config: PaseoConfigRaw; expectedRevision: PaseoConfigRevision | null; requestId?: string; } interface ListCommandsOptions { agentId: string; requestId?: string; draftConfig?: ListCommandsDraftConfig; } type LegacyListCommandsOptions = Omit; type SetVoiceModePayload = Extract< SessionOutboundMessage, { type: "set_voice_mode_response" } >["payload"]; type DictationFinishAcceptedPayload = Extract< SessionOutboundMessage, { type: "dictation_stream_finish_accepted" } >["payload"]; type AgentPermissionResolvedPayload = AgentPermissionResolvedMessage["payload"]; type ListTerminalsPayload = ListTerminalsResponse["payload"]; type CreateTerminalPayload = CreateTerminalResponse["payload"]; export type RenameTerminalResult = z.infer["payload"]; type SubscribeTerminalPayload = SubscribeTerminalResponse["payload"]; type CloseItemsPayload = CloseItemsResponse["payload"]; type KillTerminalPayload = KillTerminalResponse["payload"]; type CaptureTerminalPayload = CaptureTerminalResponse["payload"]; type ChatCreatePayload = Extract< SessionOutboundMessage, { type: "chat/create/response" } >["payload"]; type ChatListPayload = Extract["payload"]; type ChatInspectPayload = Extract< SessionOutboundMessage, { type: "chat/inspect/response" } >["payload"]; type ChatDeletePayload = Extract< SessionOutboundMessage, { type: "chat/delete/response" } >["payload"]; type ChatPostPayload = Extract["payload"]; type ChatReadPayload = Extract["payload"]; type ChatWaitPayload = Extract["payload"]; type LoopRunPayload = Extract["payload"]; type LoopListPayload = Extract["payload"]; type LoopInspectPayload = Extract< SessionOutboundMessage, { type: "loop/inspect/response" } >["payload"]; type LoopLogsPayload = Extract["payload"]; type LoopStopPayload = Extract["payload"]; type ScheduleCreatePayload = Extract< SessionOutboundMessage, { type: "schedule/create/response" } >["payload"]; type ScheduleListPayload = Extract< SessionOutboundMessage, { type: "schedule/list/response" } >["payload"]; type ScheduleInspectPayload = Extract< SessionOutboundMessage, { type: "schedule/inspect/response" } >["payload"]; type ScheduleLogsPayload = Extract< SessionOutboundMessage, { type: "schedule/logs/response" } >["payload"]; type SchedulePausePayload = Extract< SessionOutboundMessage, { type: "schedule/pause/response" } >["payload"]; type ScheduleResumePayload = Extract< SessionOutboundMessage, { type: "schedule/resume/response" } >["payload"]; type ScheduleDeletePayload = Extract< SessionOutboundMessage, { type: "schedule/delete/response" } >["payload"]; type ScheduleRunOncePayload = Extract< SessionOutboundMessage, { type: "schedule/run-once/response" } >["payload"]; type ScheduleUpdatePayload = Extract< SessionOutboundMessage, { type: "schedule/update/response" } >["payload"]; export type FetchAgentTimelinePayload = FetchAgentTimelineResponseMessage["payload"]; export type AgentForkContextPayload = AgentForkContextResponseMessage["payload"]; export type FetchAgentTimelineDirection = FetchAgentTimelinePayload["direction"]; export type FetchAgentTimelineProjection = FetchAgentTimelinePayload["projection"]; export type FetchAgentTimelineCursor = NonNullable; export interface FetchAgentOptions { agentId: string; requestId?: string; timeout?: number; } type LegacyFetchAgentOptions = Omit; export interface FetchAgentTimelineOptions { direction?: FetchAgentTimelineDirection; cursor?: FetchAgentTimelineCursor; limit?: number; projection?: FetchAgentTimelineProjection; requestId?: string; timeout?: number; } export type ProviderSubagentListPayload = Extract< SessionOutboundMessage, { type: "agent.provider_subagents.list.response" } >["payload"]; export type ProviderSubagentTimelinePayload = Extract< SessionOutboundMessage, { type: "agent.provider_subagents.timeline.get.response" } >["payload"]; export interface FetchProviderSubagentTimelineOptions { direction?: ProviderSubagentTimelinePayload["direction"]; cursor?: FetchAgentTimelineCursor; limit?: number; requestId?: string; timeout?: number; } // COMPAT(daemon-client-object-options): added in v0.1.102; remove after // 2026-12-29 once SDK callers have migrated to object parameters. function normalizeFetchAgentOptions( input: FetchAgentOptions | string, legacyOptions?: LegacyFetchAgentOptions | string, ): FetchAgentOptions { if (typeof input !== "string") { return input; } if (typeof legacyOptions === "string") { return { agentId: input, requestId: legacyOptions }; } return { agentId: input, ...legacyOptions }; } function normalizeListCommandsOptions( input: ListCommandsOptions | string, legacyOptions?: LegacyListCommandsOptions | string, ): ListCommandsOptions { if (typeof input !== "string") { return input; } if (typeof legacyOptions === "string") { return { agentId: input, requestId: legacyOptions }; } return { agentId: input, ...legacyOptions }; } export interface AgentForkContextOptions { boundaryCursor?: FetchAgentTimelineCursor; boundaryMessageId?: string; requestId?: string; } type AgentRefreshedStatusPayload = z.infer; type RestartRequestedStatusPayload = z.infer; type ShutdownRequestedStatusPayload = z.infer; export interface ShutdownServerOptions { requestId?: string; timeout?: number; } export interface DaemonStatusOptions { requestId?: string; timeout?: number; } export interface DaemonPairingOfferOptions { requestId?: string; timeout?: number; } type DaemonUpdateResponse = z.infer; type FetchAgentsPayload = Extract< SessionOutboundMessage, { type: "fetch_agents_response" } >["payload"]; type FetchAgentsRequest = Extract; export type FetchAgentsOptions = Omit & { requestId?: string; timeout?: number; }; export type FetchAgentsEntry = FetchAgentsPayload["entries"][number]; export type FetchAgentsPageInfo = FetchAgentsPayload["pageInfo"]; type FetchAgentHistoryPayload = Extract< SessionOutboundMessage, { type: "fetch_agent_history_response" } >["payload"]; type FetchAgentHistoryRequest = Extract< SessionInboundMessage, { type: "fetch_agent_history_request" } >; export type FetchAgentHistoryOptions = Omit & { requestId?: string; }; export type FetchAgentHistoryEntry = FetchAgentHistoryPayload["entries"][number]; export type FetchAgentHistoryPageInfo = FetchAgentHistoryPayload["pageInfo"]; type FetchRecentProviderSessionsPayload = Extract< SessionOutboundMessage, { type: "fetch_recent_provider_sessions_response" } >["payload"]; type FetchRecentProviderSessionsRequest = Extract< SessionInboundMessage, { type: "fetch_recent_provider_sessions_request" } >; export type FetchRecentProviderSessionsOptions = Omit< FetchRecentProviderSessionsRequest, "type" | "requestId" > & { requestId?: string; }; export type FetchRecentProviderSessionEntry = FetchRecentProviderSessionsPayload["entries"][number]; type FetchWorkspacesPayload = Extract< SessionOutboundMessage, { type: "fetch_workspaces_response" } >["payload"]; type FetchWorkspacesRequest = Extract; export type FetchWorkspacesOptions = Omit & { requestId?: string; }; export type FetchWorkspacesEntry = FetchWorkspacesPayload["entries"][number]; export type FetchWorkspacesPageInfo = FetchWorkspacesPayload["pageInfo"]; export type ProjectListPayload = Extract< SessionOutboundMessage, { type: "project.list.response" } >["payload"]; export interface CreateChatRoomOptions { name: string; purpose?: string | null; requestId?: string; } export interface InspectChatRoomOptions { room: string; requestId?: string; } export interface DeleteChatRoomOptions { room: string; requestId?: string; } export interface PostChatMessageOptions { room: string; body: string; authorAgentId?: string; replyToMessageId?: string | null; requestId?: string; } export interface ReadChatMessagesOptions { room: string; limit?: number; since?: string; authorAgentId?: string; requestId?: string; timeout?: number; } export interface WaitForChatMessagesOptions { room: string; afterMessageId?: string | null; timeoutMs?: number; requestId?: string; } export interface RunLoopOptions { prompt: string; cwd: string; provider?: string; model?: string; modeId?: string; verifierProvider?: string; verifierModel?: string; verifierModeId?: string; verifyPrompt?: string | null; verifyChecks?: string[]; name?: string | null; sleepMs?: number; maxIterations?: number; maxTimeMs?: number; requestId?: string; } export interface InspectLoopOptions { id: string; requestId?: string; } export interface LoopLogsOptions { id: string; afterSeq?: number; requestId?: string; } export interface StopLoopOptions { id: string; requestId?: string; } export interface CreateScheduleOptions { prompt: string; name?: string | null; cadence: { type: "cron"; expression: string; timezone?: string; }; target: | { type: "self"; agentId: string; } | { type: "agent"; agentId: string; } | { type: "new-agent"; config: { provider: AgentProvider; cwd: string; modeId?: string; model?: string; thinkingOptionId?: string; archiveOnFinish?: boolean; isolation?: "local" | "worktree"; title?: string | null; approvalPolicy?: string; sandboxMode?: string; networkAccess?: boolean; webSearch?: boolean; extra?: AgentSessionConfig["extra"]; systemPrompt?: string; mcpServers?: AgentSessionConfig["mcpServers"]; }; }; maxRuns?: number; expiresAt?: string; runOnCreate?: boolean; requestId?: string; } export interface InspectScheduleOptions { id: string; requestId?: string; } export interface UpdateScheduleNewAgentConfig { provider?: string; model?: string | null; modeId?: string | null; thinkingOptionId?: string | null; archiveOnFinish?: boolean; isolation?: "local" | "worktree"; cwd?: string; } export interface UpdateScheduleOptions { id: string; name?: string | null; prompt?: string; cadence?: { type: "cron"; expression: string; timezone?: string; }; newAgentConfig?: UpdateScheduleNewAgentConfig; maxRuns?: number | null; expiresAt?: string | null; requestId?: string; } export interface RenameBranchInput { cwd: string; branch: string; requestId?: string; } export interface RenameTerminalInput { terminalId: string; title: string; requestId?: string; } type OpenProjectPayload = OpenProjectResponseMessage["payload"]; type ProjectAddPayload = ProjectAddResponse["payload"]; export type ProjectCreateDirectoryPayload = ProjectCreateDirectoryResponse["payload"]; export type WorkspaceGithubSearchRepositoriesPayload = WorkspaceGithubSearchRepositoriesResponse["payload"]; type ProjectGithubClonePayload = ProjectGithubCloneResponse["payload"]; type ArchiveWorkspacePayload = ArchiveWorkspaceResponseMessage["payload"]; type WorkspaceSetupStatusPayload = WorkspaceSetupStatusResponseMessage["payload"]; export interface FetchAgentResult { agent: AgentSnapshotPayload; project: ProjectPlacementPayload | null; } export interface WaitForFinishResult { status: "idle" | "error" | "permission" | "timeout"; final: AgentSnapshotPayload | null; error: string | null; lastMessage: string | null; } interface Waiter { predicate: (msg: SessionOutboundMessage) => T | null; resolve(value: T): void; reject(error: Error): void; timeoutHandle: ReturnType | null; requestId?: string; } interface WaitHandle { promise: Promise; cancel: (error: Error) => void; } interface WaitOptions { skipQueue?: boolean; requestId?: string; } interface CorrelatedResponseIdentity { requestId: string; responseType?: string; } interface PendingBinaryFileRead { cwd: string; path: string; } interface BinaryFileTransferState extends PendingBinaryFileRead { mime: string; size: number; encoding: Extract< FileTransferFrame, { opcode: typeof FileTransferOpcode.FileBegin } >["metadata"]["encoding"]; modifiedAt: string; revision?: string; chunks: Uint8Array[]; } type RpcWaitResult = { kind: "ok"; value: T } | { kind: "error"; error: DaemonRpcError }; type GetDaemonConfigResponse = Extract< SessionOutboundMessage, { type: "get_daemon_config_response" } >; type SetDaemonConfigResponse = Extract< SessionOutboundMessage, { type: "set_daemon_config_response" } >; type CorrelatedResponseMessage = | Extract | GetDaemonConfigResponse | SetDaemonConfigResponse; type CorrelatedResponseType = CorrelatedResponseMessage["type"]; type CorrelatedResponsePayload = Extract< CorrelatedResponseMessage, { type: TType } >["payload"]; class DaemonRpcError extends Error { readonly requestId: string; readonly requestType?: string; readonly code?: string; constructor(params: { requestId: string; error: string; requestType?: string; code?: string }) { const parts = [params.error]; if (params.requestType) parts.push(`requestType=${params.requestType}`); if (params.code) parts.push(`code=${params.code}`); super(parts.join(" ")); this.name = "DaemonRpcError"; this.requestId = params.requestId; this.requestType = params.requestType; this.code = params.code; } } class DaemonProtocolError extends Error { readonly requestId: string; readonly responseType?: string; readonly code = "invalid_response"; constructor(identity: CorrelatedResponseIdentity) { const responseLabel = identity.responseType ?? "unknown response"; super(`Response validation failed for ${responseLabel}`); this.name = "DaemonProtocolError"; this.requestId = identity.requestId; this.responseType = identity.responseType; } } class PingTimeoutError extends Error { constructor(readonly timeoutMs: number) { super(`Ping timed out (${timeoutMs}ms)`); this.name = "PingTimeoutError"; } } function toTimeoutError(error: unknown, label: string, timeoutMs: number): Error { if (error instanceof PingTimeoutError) { return new Error(`${label} timed out (${timeoutMs}ms)`); } return error instanceof Error ? error : new Error(String(error)); } const DEFAULT_RECONNECT_BASE_DELAY_MS = 1500; const DEFAULT_RECONNECT_MAX_DELAY_MS = 30000; const DEFAULT_SESSION_RPC_TIMEOUT_MS = 60_000; const DEFAULT_CONNECT_TIMEOUT_MS = 15_000; const DEFAULT_LIVENESS_TIMEOUT_MS = 5000; const LIVENESS_HEARTBEAT_INTERVAL_MS = 10_000; const LIVENESS_HEARTBEAT_TIMEOUT_MS = 15_000; const LIVENESS_FAILURE_RECONNECT_THRESHOLD = 2; /** Default timeout for waiting for connection before sending queued messages */ const DEFAULT_SEND_QUEUE_TIMEOUT_MS = DEFAULT_SESSION_RPC_TIMEOUT_MS; const DEFAULT_DICTATION_FINISH_ACCEPT_TIMEOUT_MS = DEFAULT_SESSION_RPC_TIMEOUT_MS; const DEFAULT_DICTATION_FINISH_FALLBACK_TIMEOUT_MS = 5 * 60 * 1000; const DEFAULT_DICTATION_FINISH_TIMEOUT_GRACE_MS = 5000; function isWaiterTimeoutError(error: unknown): boolean { return error instanceof Error && error.message.startsWith("Timeout waiting for message"); } function normalizeClientId(value: unknown): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function decodeBase64ToBytes(base64: string): Uint8Array { const binary = globalThis.atob(base64); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes; } function legacyExplorerFileToBytes(file: LegacyFileExplorerFilePayload): FileReadResult { let bytes: Uint8Array; if (file.encoding === "base64" && file.content) { bytes = decodeBase64ToBytes(file.content); } else if (file.encoding === "utf-8" && file.content) { bytes = new TextEncoder().encode(file.content); } else { bytes = new Uint8Array(); } return { bytes, mime: file.mimeType ?? "application/octet-stream", size: file.size, path: file.path, kind: file.kind, modifiedAt: file.modifiedAt, revision: file.revision, }; } function binaryFileKind(mime: string, encoding: string): FileReadResult["kind"] { if (mime.startsWith("image/")) { return "image"; } if (encoding === "utf-8" || mime.startsWith("text/") || mime === "application/json") { return "text"; } return "binary"; } function concatByteChunks(chunks: Uint8Array[], size: number): Uint8Array { const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } return bytes; } function hashForLog(value: string): string { let hash = 0; for (let index = 0; index < value.length; index += 1) { hash = (hash * 31 + value.charCodeAt(index)) | 0; } return `h_${Math.abs(hash).toString(16)}`; } function toReasonCode(reason: string | null | undefined): string | null { if (!reason) { return null; } const normalized = reason.toLowerCase(); if (normalized.includes("timed out")) { return "connect_timeout"; } if (normalized.includes("disposed")) { return "disposed"; } if (normalized.includes("client closed")) { return "client_closed"; } if (normalized.includes("transport")) { return "transport_error"; } if (normalized.includes("failed to connect")) { return "connect_failed"; } return "unknown"; } interface PendingSend { message: SessionInboundMessage; resolve: () => void; reject: (error: Error) => void; timeoutHandle: ReturnType; } interface PingProbe { promise: Promise; resolve: (value: number) => void; reject: (error: Error) => void; timeoutHandle: ReturnType; startedAt: number; // Whether a timeout on this ping should be recorded as a liveness failure. Only the // heartbeat sets this; a latency measurement never drives teardown, even when a // heartbeat tick shares (dedupes onto) an in-flight measurement ping. drivesLivenessFailure: boolean; } export class DaemonClient { private transport: DaemonTransport | null = null; private transportCleanup: Array<() => void> = []; private rawMessageListeners: Set<(message: SessionOutboundMessage) => void> = new Set(); private messageHandlers: Map< SessionOutboundMessage["type"], Set<(message: SessionOutboundMessage) => void> > = new Map(); private eventListeners: Set = new Set(); private waiters: Set> = new Set(); private checkoutStatusInFlight: Map> = new Map(); private connectionListeners: Set<(status: ConnectionState) => void> = new Set(); private reconnectTimeout: ReturnType | null = null; private connectTimeout: ReturnType | null = null; private pendingGenericTransportErrorTimeout: ReturnType | null = null; private reconnectAttempt = 0; private shouldReconnect = true; private connectPromise: Promise | null = null; private connectResolve: (() => void) | null = null; private connectReject: ((error: Error) => void) | null = null; private lastErrorValue: string | null = null; private connectionState: ConnectionState = { status: "idle" }; private checkoutDiffSubscriptions = new Map< string, { cwd: string; compare: { mode: "uncommitted" | "base"; baseRef?: string; ignoreWhitespace?: boolean }; } >(); private terminalDirectorySubscriptions = new Map(); private fileSubscriptions = new Map< string, { cwd: string; path: string; onUpdate: (version: FileVersion) => void } >(); private readonly terminalStreams = new TerminalStreamRouter(); private pendingBinaryFileReads = new Map(); private activeBinaryFileTransfers = new Map(); private completedBinaryFileReads = new Map(); private logger: Logger; private pendingSendQueue: PendingSend[] = []; private readonly logConnectionPath: "direct" | "relay"; private readonly logServerId: string | null; private readonly logClientIdHash: string; private readonly logGeneration: number | null; private lastServerInfoMessage: ServerInfoStatusPayload | null = null; private runtimeMetricsInterval: ReturnType | null = null; private runtimeMetrics: DaemonClientRuntimeMetrics | null = null; private pingProbe: PingProbe | null = null; private livenessHeartbeatTimer: ReturnType | null = null; private lastLivenessRttMs: number | null = null; private consecutiveLivenessFailures = 0; constructor(private config: DaemonClientConfig) { this.logger = config.logger ?? consoleLogger; this.logConnectionPath = isRelayClientWebSocketUrl(this.config.url) ? "relay" : "direct"; let parsedUrlForLog: URL | null = null; try { parsedUrlForLog = new URL(this.config.url); } catch { parsedUrlForLog = null; } const parsedServerIdForLog = normalizeClientId(parsedUrlForLog?.searchParams.get("serverId")); this.logServerId = parsedServerIdForLog ?? parsedUrlForLog?.host ?? null; const resolvedClientId = normalizeClientId(this.config.clientId); if (!resolvedClientId) { throw new Error("Daemon client requires a non-empty clientId"); } this.config.clientId = resolvedClientId; this.logClientIdHash = hashForLog(resolvedClientId); this.logGeneration = typeof this.config.runtimeGeneration === "number" && Number.isFinite(this.config.runtimeGeneration) ? this.config.runtimeGeneration : null; const runtimeMetricsIntervalMs = typeof config.runtimeMetricsIntervalMs === "number" && config.runtimeMetricsIntervalMs > 0 ? config.runtimeMetricsIntervalMs : 0; if (runtimeMetricsIntervalMs > 0) { const runtimeMetricsWindowMs = typeof config.runtimeMetricsWindowMs === "number" && config.runtimeMetricsWindowMs > 0 ? Math.max(config.runtimeMetricsWindowMs, runtimeMetricsIntervalMs) : undefined; this.runtimeMetrics = new DaemonClientRuntimeMetrics( this.logger, { connectionPath: this.logConnectionPath, serverId: this.logServerId, getConnectionStatus: () => this.connectionState.status, }, runtimeMetricsWindowMs ? { windowMs: runtimeMetricsWindowMs } : undefined, ); this.runtimeMetricsInterval = setInterval(() => { this.runtimeMetrics?.flush(); }, runtimeMetricsIntervalMs); } } // ============================================================================ // Connection // ============================================================================ async connect(): Promise { if (this.connectionState.status === "disposed") { throw new Error("Daemon client is disposed"); } if (this.connectionState.status === "connected") { return; } if (this.connectPromise) { return this.connectPromise; } this.shouldReconnect = true; this.connectPromise = new Promise((resolve, reject) => { this.connectResolve = resolve; this.connectReject = reject; this.attemptConnect(); }); return this.connectPromise; } private attemptConnect(): void { if (this.connectionState.status === "disposed") { this.rejectConnect(new Error("Daemon client is disposed")); return; } if (!this.shouldReconnect) { this.rejectConnect(new Error("Daemon client is closed")); return; } if (this.connectionState.status === "connecting") { return; } const headers: Record = {}; const password = normalizePassword(this.config.password); if (password) { headers.Authorization = `Bearer ${password}`; } else if (this.config.authHeader) { headers.Authorization = this.config.authHeader; } const protocols = password ? [`paseo.bearer.${password}`] : undefined; try { // Reconnect can overlap with browser close/error delivery ordering. // Always dispose previous transport before constructing the next one. this.disposeTransport(); const baseTransportFactory = this.config.transportFactory ?? createWebSocketTransportFactory(this.config.webSocketFactory ?? defaultWebSocketFactory); const shouldUseRelayE2ee = this.config.e2ee?.enabled === true && isRelayClientWebSocketUrl(this.config.url); let transportFactory = baseTransportFactory; if (shouldUseRelayE2ee) { const daemonPublicKeyB64 = this.config.e2ee?.daemonPublicKeyB64; if (!daemonPublicKeyB64) { throw new Error("daemonPublicKeyB64 is required for relay E2EE"); } transportFactory = createRelayE2eeTransportFactory({ baseFactory: baseTransportFactory, daemonPublicKeyB64, logger: this.logger, }); } const transportUrl = this.resolveTransportUrlForAttempt(); const transport = transportFactory({ url: transportUrl, headers, ...(protocols ? { protocols } : {}), }); this.transport = transport; this.lastServerInfoMessage = null; this.updateConnectionState( { status: "connecting", attempt: this.reconnectAttempt, }, { event: "CONNECT_REQUEST" }, ); this.resetConnectTimeout(); const timeoutMs = Math.max(1, this.config.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS); this.connectTimeout = setTimeout(() => { if (this.connectionState.status !== "connecting") { return; } this.lastErrorValue = "Connection timed out"; this.disposeTransport(1001, "Connection timed out"); this.scheduleReconnect({ reason: "Connection timed out", event: "CONNECT_TIMEOUT", reasonCode: "connect_timeout", }); }, timeoutMs); this.transportCleanup = [ transport.onOpen(() => { if (this.pendingGenericTransportErrorTimeout) { clearTimeout(this.pendingGenericTransportErrorTimeout); this.pendingGenericTransportErrorTimeout = null; } this.lastErrorValue = null; this.sendHelloMessage(); }), transport.onClose((event) => { this.resetConnectTimeout(); if (this.pendingGenericTransportErrorTimeout) { clearTimeout(this.pendingGenericTransportErrorTimeout); this.pendingGenericTransportErrorTimeout = null; } const reason = describeTransportClose(event); if (reason) { this.lastErrorValue = reason; } this.scheduleReconnect({ reason, event: "TRANSPORT_CLOSE", reasonCode: "transport_closed", }); }), transport.onError((event) => { this.resetConnectTimeout(); const reason = describeTransportError(event); const isGeneric = reason === "Transport error"; // Browser WebSocket.onerror often provides no useful details and is followed // by a close event (often with code 1006). Prefer surfacing the close details // instead of immediately disconnecting with a generic "Transport error". if (isGeneric) { this.lastErrorValue ??= reason; if (!this.pendingGenericTransportErrorTimeout) { this.pendingGenericTransportErrorTimeout = setTimeout(() => { this.pendingGenericTransportErrorTimeout = null; if ( this.connectionState.status === "connected" || this.connectionState.status === "connecting" ) { this.lastErrorValue = reason; this.scheduleReconnect({ reason, event: "TRANSPORT_ERROR", reasonCode: "transport_error", }); } }, 250); } return; } if (this.pendingGenericTransportErrorTimeout) { clearTimeout(this.pendingGenericTransportErrorTimeout); this.pendingGenericTransportErrorTimeout = null; } this.lastErrorValue = reason; this.scheduleReconnect({ reason, event: "TRANSPORT_ERROR", reasonCode: "transport_error", }); }), transport.onMessage((data) => this.handleTransportMessage(data)), ]; } catch (error) { this.resetConnectTimeout(); const message = error instanceof Error ? error.message : "Failed to connect"; this.lastErrorValue = message; this.scheduleReconnect({ reason: message, event: "CONNECT_FAILED", reasonCode: "connect_failed", }); this.rejectConnect(error instanceof Error ? error : new Error(message)); } } private resolveConnect(): void { if (this.connectResolve) { this.connectResolve(); } this.connectPromise = null; this.connectResolve = null; this.connectReject = null; } private rejectConnect(error: Error): void { if (this.connectReject) { this.connectReject(error); } this.connectPromise = null; this.connectResolve = null; this.connectReject = null; } async close(): Promise { if (this.connectionState.status === "disposed") { return; } this.shouldReconnect = false; this.connectPromise = null; this.connectResolve = null; this.connectReject = null; if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } this.resetConnectTimeout(); this.disposeTransport(1000, "Client closed"); this.clearWaiters(new Error("Daemon client closed")); this.rejectPendingSendQueue(new Error("Daemon client closed")); this.rejectPingProbe(new Error("Daemon client closed")); this.terminalStreams.clearSlots(); this.fileSubscriptions.clear(); this.lastServerInfoMessage = null; if (this.runtimeMetricsInterval) { clearInterval(this.runtimeMetricsInterval); this.runtimeMetricsInterval = null; this.runtimeMetrics?.flush({ final: true }); this.runtimeMetrics = null; } this.updateConnectionState( { status: "disposed" }, { event: "DISPOSE", reason: "Client closed", reasonCode: "disposed" }, ); } ensureConnected(): void { if (this.connectionState.status === "disposed") { return; } if (!this.shouldReconnect) { this.shouldReconnect = true; } if ( this.connectionState.status === "connected" || this.connectionState.status === "connecting" ) { return; } void this.connect(); } getConnectionState(): ConnectionState { return this.connectionState; } subscribeConnectionStatus(listener: (status: ConnectionState) => void): () => void { this.connectionListeners.add(listener); listener(this.connectionState); return () => { this.connectionListeners.delete(listener); }; } get isConnected(): boolean { return this.connectionState.status === "connected"; } get isConnecting(): boolean { return this.connectionState.status === "connecting"; } get lastError(): string | null { return this.lastErrorValue; } getLastLivenessRttMs(): number | null { return this.lastLivenessRttMs; } // ============================================================================ // Message Subscription // ============================================================================ subscribe(handler: DaemonEventHandler): () => void { this.eventListeners.add(handler); return () => this.eventListeners.delete(handler); } subscribeRawMessages(handler: (message: SessionOutboundMessage) => void): () => void { this.rawMessageListeners.add(handler); return () => { this.rawMessageListeners.delete(handler); }; } on( type: TType, handler: (message: Extract) => void, ): () => void; on(handler: DaemonEventHandler): () => void; on( arg1: SessionOutboundMessage["type"] | DaemonEventHandler, arg2?: (message: SessionOutboundMessage) => void, ): () => void { if (typeof arg1 === "function") { return this.subscribe(arg1); } const type = arg1; const handler = arg2!; if (!this.messageHandlers.has(type)) { this.messageHandlers.set(type, new Set()); } this.messageHandlers.get(type)!.add(handler); return () => { const handlers = this.messageHandlers.get(type); if (!handlers) { return; } handlers.delete(handler); if (handlers.size === 0) { this.messageHandlers.delete(type); } }; } onAgentAttentionRequired( handler: (notification: AgentAttentionRequiredNotification) => void, ): () => void { const unsubscribeLegacy = this.on("agent_stream", (message) => { if (message.payload.event.type !== "attention_required") { return; } const event = message.payload.event; handler({ agentId: message.payload.agentId, reason: event.reason, timestamp: event.timestamp, shouldNotify: event.shouldNotify, ...(event.notification ? { notification: event.notification } : {}), }); }); const unsubscribeDedicated = this.on("agent_attention_required", (message) => { handler(message.payload); }); return () => { unsubscribeLegacy(); unsubscribeDedicated(); }; } // ============================================================================ // Core Send Helpers // ============================================================================ /** * Send a session message. For fire-and-forget messages (heartbeats, etc.), * failures are suppressed if `suppressSendErrors` is configured. * For RPC methods that wait for responses, use `sendSessionMessageOrThrow` instead. */ private sendSessionMessage(message: SessionInboundMessage): void { if (!this.transport || this.connectionState.status !== "connected") { if (this.config.suppressSendErrors) { return; } throw new Error(`Transport not connected (status: ${this.connectionState.status})`); } const payload = SessionInboundMessageSchema.parse(message); try { this.transport.send(JSON.stringify({ type: "session", message: payload })); } catch (error) { if (this.config.suppressSendErrors) { return; } throw error instanceof Error ? error : new Error(String(error)); } } private sendBinaryFrame(frame: Uint8Array): void { if (!this.transport || this.connectionState.status !== "connected") { if (this.config.suppressSendErrors) { return; } throw new Error(`Transport not connected (status: ${this.connectionState.status})`); } try { this.transport.send(frame); } catch (error) { if (this.config.suppressSendErrors) { return; } throw error instanceof Error ? error : new Error(String(error)); } } /** * Send a session message for RPC methods that create waiters. * If the connection is still being established ("connecting"), the message * is queued and will be sent once connected (or rejected after timeout). * This prevents waiters from hanging forever when called during connection. */ private sendSessionMessageOrThrow(message: SessionInboundMessage): Promise { const status = this.connectionState.status; // If connected, send immediately if (this.transport && status === "connected") { const payload = SessionInboundMessageSchema.parse(message); this.transport.send(JSON.stringify({ type: "session", message: payload })); return Promise.resolve(); } // If connecting, queue the message to be sent once connected if (status === "connecting") { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { // Remove from queue const idx = this.pendingSendQueue.findIndex((p) => p.resolve === resolve); if (idx !== -1) { this.pendingSendQueue.splice(idx, 1); } reject(new Error(`Timed out waiting for connection to send message`)); }, DEFAULT_SEND_QUEUE_TIMEOUT_MS); this.pendingSendQueue.push({ message, resolve, reject, timeoutHandle }); }); } // Not connected and not connecting - fail immediately return Promise.reject(new Error(`Transport not connected (status: ${status})`)); } /** * Flush pending send queue - called when connection is established. */ private flushPendingSendQueue(): void { const queue = this.pendingSendQueue; this.pendingSendQueue = []; for (const pending of queue) { clearTimeout(pending.timeoutHandle); try { if (this.transport && this.connectionState.status === "connected") { const payload = SessionInboundMessageSchema.parse(pending.message); this.transport.send(JSON.stringify({ type: "session", message: payload })); pending.resolve(); } else { pending.reject(new Error("Connection lost before message could be sent")); } } catch (error) { pending.reject(error instanceof Error ? error : new Error(String(error))); } } } /** * Reject all pending sends - called when connection fails or is closed. */ private rejectPendingSendQueue(error: Error): void { const queue = this.pendingSendQueue; this.pendingSendQueue = []; for (const pending of queue) { clearTimeout(pending.timeoutHandle); pending.reject(error); } } private async sendRequest(params: { requestId: string; message: SessionInboundMessage; timeout?: number; select: (msg: SessionOutboundMessage) => T | null; options?: { skipQueue?: boolean }; }): Promise { const timeout = params.timeout ?? DEFAULT_SESSION_RPC_TIMEOUT_MS; const { promise, cancel } = this.waitForWithCancel>( (msg) => { if (msg.type === "rpc_error" && msg.payload.requestId === params.requestId) { return { kind: "error", error: new DaemonRpcError({ requestId: msg.payload.requestId, error: msg.payload.error, requestType: msg.payload.requestType, code: msg.payload.code, }), }; } const value = params.select(msg); if (value === null) { return null; } return { kind: "ok", value }; }, timeout, { ...params.options, requestId: params.requestId }, ); try { await this.sendSessionMessageOrThrow(params.message); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); cancel(err); void promise.catch(() => undefined); throw err; } const result = await promise; if (result.kind === "error") { throw result.error; } return result.value; } private async sendCorrelatedRequest< TResponseType extends CorrelatedResponseType, TResult = CorrelatedResponsePayload, >(params: { requestId: string; message: SessionInboundMessage; timeout?: number; responseType: TResponseType; options?: { skipQueue?: boolean }; selectPayload?: (payload: CorrelatedResponsePayload) => TResult | null; }): Promise { return this.sendRequest({ requestId: params.requestId, message: params.message, timeout: params.timeout, options: params.options, select: (msg) => { const correlated = msg as CorrelatedResponseMessage; if (correlated.type !== params.responseType) { return null; } const payload = correlated.payload as unknown as CorrelatedResponsePayload; if (payload.requestId !== params.requestId) { return null; } if (!params.selectPayload) { return payload as TResult; } return params.selectPayload(payload); }, }); } private sendCorrelatedSessionRequest< TResponseType extends CorrelatedResponseType, TResult = CorrelatedResponsePayload, >(params: { requestId?: string; message: { type: SessionInboundMessage["type"] } & Record; responseType: TResponseType; timeout?: number; selectPayload?: (payload: CorrelatedResponsePayload) => TResult | null; }): Promise { const resolvedRequestId = this.createRequestId(params.requestId); const message = SessionInboundMessageSchema.parse({ ...params.message, requestId: resolvedRequestId, }); return this.sendCorrelatedRequest({ requestId: resolvedRequestId, message, responseType: params.responseType, timeout: params.timeout, options: { skipQueue: true }, ...(params.selectPayload ? { selectPayload: params.selectPayload } : {}), }); } private sendNamespacedCorrelatedSessionRequest< TResponseType extends CorrelatedResponseType, TResult = CorrelatedResponsePayload, >(params: { requestId?: string; message: { type: Extract } & Record< string, unknown >; timeout?: number; selectPayload?: (payload: CorrelatedResponsePayload) => TResult | null; }): Promise { const responseType = params.message.type.replace(/\.request$/, ".response") as TResponseType; return this.sendCorrelatedSessionRequest({ ...params, responseType, }); } private sendSessionMessageStrict(message: SessionInboundMessage): void { if (!this.transport || this.connectionState.status !== "connected") { throw new Error("Transport not connected"); } const payload = SessionInboundMessageSchema.parse(message); try { this.transport.send(JSON.stringify({ type: "session", message: payload })); } catch (error) { throw error instanceof Error ? error : new Error(String(error)); } } async clearAgentAttention(agentId: string | string[]): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "clear_agent_attention", agentId, requestId, }); await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "clear_agent_attention_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); } async clearWorkspaceAttention(workspaceId: string | string[]): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "workspace.clear_attention.request", workspaceId, requestId, }); const response = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "workspace.clear_attention.response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!response.success) { throw new Error(response.error ?? "Failed to clear workspace attention"); } } sendHeartbeat(params: { deviceType: "web" | "mobile"; focusedAgentId: string | null; focusedTerminalId?: string | null; lastActivityAt: string; appVisible: boolean; appVisibilityChangedAt?: string; }): void { this.sendSessionMessage({ type: "client_heartbeat", deviceType: params.deviceType, focusedAgentId: params.focusedAgentId, focusedTerminalId: params.focusedTerminalId ?? null, lastActivityAt: params.lastActivityAt, appVisible: params.appVisible, appVisibilityChangedAt: params.appVisibilityChangedAt, }); } registerPushToken(token: string): void { this.sendSessionMessage({ type: "register_push_token", token, }); } async ping(params?: { requestId?: string; timeoutMs?: number }): Promise<{ requestId: string; clientSentAt: number; serverReceivedAt: number; serverSentAt: number; rttMs: number; }> { const requestId = params?.requestId ?? `ping-${Date.now()}-${Math.random().toString(36).slice(2)}`; const clientSentAt = Date.now(); const payload = await this.sendRequest({ requestId, message: { type: "ping", requestId, clientSentAt }, timeout: params?.timeoutMs ?? 5000, select: (msg) => { if (msg.type !== "pong") return null; if (msg.payload.requestId !== requestId) return null; if (typeof msg.payload.serverReceivedAt !== "number") return null; if (typeof msg.payload.serverSentAt !== "number") return null; return msg.payload; }, }); return { requestId, clientSentAt, serverReceivedAt: payload.serverReceivedAt, serverSentAt: payload.serverSentAt, rttMs: Date.now() - clientSentAt, }; } measureLatency(params?: { timeoutMs?: number }): Promise { const timeoutMs = Math.max(1, params?.timeoutMs ?? DEFAULT_LIVENESS_TIMEOUT_MS); return this.sendPingAwaitRtt({ timeoutMs, drivesLivenessFailure: false }).catch((error) => { throw toTimeoutError(error, "Latency measurement", timeoutMs); }); } private async livenessPing(params?: { timeoutMs?: number }): Promise { const timeoutMs = Math.max(1, params?.timeoutMs ?? DEFAULT_LIVENESS_TIMEOUT_MS); try { const rttMs = await this.sendPingAwaitRtt({ timeoutMs, drivesLivenessFailure: true }); this.lastLivenessRttMs = rttMs; return rttMs; } catch (error) { throw toTimeoutError(error, "Liveness check", timeoutMs); } } private sendPingAwaitRtt(params: { timeoutMs: number; drivesLivenessFailure: boolean; }): Promise { if (this.connectionState.status !== "connected" || !this.transport) { return Promise.reject( new Error(`Transport not connected (status: ${this.connectionState.status})`), ); } if (this.pingProbe) { return this.pingProbe.promise; } const startedAt = perfNow(); const timeoutMs = params.timeoutMs; let resolveProbe: ((value: number) => void) | null = null; let rejectProbe: ((error: Error) => void) | null = null; const promise = new Promise((resolve, reject) => { resolveProbe = resolve; rejectProbe = reject; }); const probe: PingProbe = { promise, resolve: (value) => resolveProbe?.(value), reject: (error) => rejectProbe?.(error), timeoutHandle: setTimeout(() => { if (this.pingProbe !== probe) { return; } this.pingProbe = null; const error = new PingTimeoutError(timeoutMs); probe.reject(error); if (probe.drivesLivenessFailure) { this.recordLivenessFailure(toTimeoutError(error, "Liveness check", timeoutMs)); } }, timeoutMs), startedAt, drivesLivenessFailure: params.drivesLivenessFailure, }; this.pingProbe = probe; try { this.transport.send(JSON.stringify({ type: "ping" })); } catch (error) { this.clearPingProbe(); const sendError = error instanceof Error ? error : new Error(String(error)); if (probe.drivesLivenessFailure) { this.recordLivenessFailure(sendError); } return Promise.reject(sendError); } return promise; } private startLivenessHeartbeat(): void { this.stopLivenessHeartbeat(); this.lastLivenessRttMs = null; this.scheduleNextLivenessHeartbeat(); } private stopLivenessHeartbeat(): void { if (!this.livenessHeartbeatTimer) { return; } clearTimeout(this.livenessHeartbeatTimer); this.livenessHeartbeatTimer = null; } private scheduleNextLivenessHeartbeat(): void { if (this.connectionState.status !== "connected" || this.livenessHeartbeatTimer) { return; } this.livenessHeartbeatTimer = setTimeout(() => { this.livenessHeartbeatTimer = null; this.livenessPing({ timeoutMs: LIVENESS_HEARTBEAT_TIMEOUT_MS }) .catch(() => {}) .finally(() => { this.scheduleNextLivenessHeartbeat(); }); }, LIVENESS_HEARTBEAT_INTERVAL_MS); } // ============================================================================ // Agent RPCs (requestId-correlated) // ============================================================================ async fetchAgents(options?: FetchAgentsOptions): Promise { const resolvedRequestId = this.createRequestId(options?.requestId); const message = SessionInboundMessageSchema.parse({ type: "fetch_agents_request", requestId: resolvedRequestId, ...(options?.scope ? { scope: options.scope } : {}), ...(options?.filter ? { filter: options.filter } : {}), ...(options?.sort ? { sort: options.sort } : {}), ...(options?.page ? { page: options.page } : {}), ...(options?.subscribe ? { subscribe: options.subscribe } : {}), }); return this.sendRequest({ requestId: resolvedRequestId, message, timeout: options?.timeout, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "fetch_agents_response") { return null; } if (msg.payload.requestId !== resolvedRequestId) { return null; } return msg.payload; }, }); } async fetchAgentHistory(options?: FetchAgentHistoryOptions): Promise { const resolvedRequestId = this.createRequestId(options?.requestId); const message = SessionInboundMessageSchema.parse({ type: "fetch_agent_history_request", requestId: resolvedRequestId, ...(options?.filter ? { filter: options.filter } : {}), ...(options?.sort ? { sort: options.sort } : {}), ...(options?.page ? { page: options.page } : {}), }); return this.sendRequest({ requestId: resolvedRequestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "fetch_agent_history_response") { return null; } if (msg.payload.requestId !== resolvedRequestId) { return null; } return msg.payload; }, }); } async fetchRecentProviderSessions( options?: FetchRecentProviderSessionsOptions, ): Promise { const resolvedRequestId = this.createRequestId(options?.requestId); const message = SessionInboundMessageSchema.parse({ type: "fetch_recent_provider_sessions_request", requestId: resolvedRequestId, ...(options?.cwd ? { cwd: options.cwd } : {}), ...(options?.providers ? { providers: options.providers } : {}), ...(options?.since ? { since: options.since } : {}), ...(options?.limit ? { limit: options.limit } : {}), }); return this.sendRequest({ requestId: resolvedRequestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "fetch_recent_provider_sessions_response") { return null; } if (msg.payload.requestId !== resolvedRequestId) { return null; } return msg.payload; }, }); } async fetchWorkspaces(options?: FetchWorkspacesOptions): Promise { const resolvedRequestId = this.createRequestId(options?.requestId); const message = SessionInboundMessageSchema.parse({ type: "fetch_workspaces_request", requestId: resolvedRequestId, ...(options?.filter ? { filter: options.filter } : {}), ...(options?.sort ? { sort: options.sort } : {}), ...(options?.page ? { page: options.page } : {}), ...(options?.subscribe ? { subscribe: options.subscribe } : {}), }); return this.sendRequest({ requestId: resolvedRequestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "fetch_workspaces_response") { return null; } if (msg.payload.requestId !== resolvedRequestId) { return null; } return msg.payload; }, }); } async listProjects(requestId?: string): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "project.list.request", requestId: resolvedRequestId, }); return this.sendRequest({ requestId: resolvedRequestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "project.list.response") return null; if (msg.payload.requestId !== resolvedRequestId) return null; return msg.payload; }, }); } async openProject(cwd: string, requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "open_project_request", cwd, }, responseType: "open_project_response", }); } async addProject(cwd: string, requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "project.add.request", cwd, }, responseType: "project.add.response", }); } async createProjectDirectory( input: { parentPath: string; name: string }, requestId?: string, ): Promise { return this.sendNamespacedCorrelatedSessionRequest<"project.create_directory.response">({ requestId, message: { type: "project.create_directory.request", parentPath: input.parentPath, name: input.name, }, }); } async searchGithubRepositories( input: { query: string; limit?: number }, requestId?: string, ): Promise { return this.sendNamespacedCorrelatedSessionRequest<"workspace.github.search_repositories.response">( { requestId, message: { type: "workspace.github.search_repositories.request", query: input.query, limit: input.limit, }, }, ); } async cloneGithubProject( input: { repo: string; targetDirectory: string; cloneProtocol?: ProjectGithubCloneProtocol }, requestId?: string, ): Promise { const message = { type: "project.github.clone.request", repo: input.repo, targetDirectory: input.targetDirectory, ...(input.cloneProtocol ? { cloneProtocol: input.cloneProtocol } : {}), } as const; return this.sendNamespacedCorrelatedSessionRequest<"project.github.clone.response">({ requestId, message, timeout: PROJECT_GITHUB_CLONE_TIMEOUT_MS, }); } async startWorkspaceScript( workspaceId: string, scriptName: string, requestId?: string, ): Promise< Extract["payload"] > { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "start_workspace_script_request", workspaceId, scriptName, }, responseType: "start_workspace_script_response", }); } async listWorkspaceScripts( workspaceId: string, requestId?: string, ): Promise< Extract["payload"] > { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "workspace.script.list.request", workspaceId }, responseType: "workspace.script.list.response", }); } async startWorkspaceScriptWithStatus( workspaceId: string, scriptName: string, requestId?: string, ): Promise< Extract["payload"] > { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "workspace.script.start.request", workspaceId, scriptName }, responseType: "workspace.script.start.response", }); } async stopWorkspaceScript( workspaceId: string, scriptName: string, requestId?: string, ): Promise< Extract["payload"] > { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "workspace.script.stop.request", workspaceId, scriptName }, responseType: "workspace.script.stop.response", }); } async archiveWorkspace( workspaceId: string, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "archive_workspace_request", workspaceId, }, responseType: "archive_workspace_response", }); } async fetchWorkspaceSetupStatus( workspaceId: string, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "workspace_setup_status_request", workspaceId, }, responseType: "workspace_setup_status_response", }); } async fetchAgent(options: FetchAgentOptions): Promise; async fetchAgent(agentId: string, requestId?: string): Promise; async fetchAgent( agentId: string, options?: LegacyFetchAgentOptions, ): Promise; async fetchAgent( input: FetchAgentOptions | string, legacyOptions?: LegacyFetchAgentOptions | string, ): Promise { const options = normalizeFetchAgentOptions(input, legacyOptions); const resolvedRequestId = this.createRequestId(options.requestId); const message = SessionInboundMessageSchema.parse({ type: "fetch_agent_request", requestId: resolvedRequestId, agentId: options.agentId, }); const payload = await this.sendRequest({ requestId: resolvedRequestId, message, timeout: options.timeout, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "fetch_agent_response") { return null; } if (msg.payload.requestId !== resolvedRequestId) { return null; } return msg.payload; }, }); if (payload.error) { throw new Error(payload.error); } if (!payload.agent) { return null; } return { agent: payload.agent, project: payload.project ?? null }; } private resubscribeCheckoutDiffSubscriptions(): void { if (this.checkoutDiffSubscriptions.size === 0) { return; } for (const [subscriptionId, subscription] of this.checkoutDiffSubscriptions) { const message = SessionInboundMessageSchema.parse({ type: "subscribe_checkout_diff_request", subscriptionId, cwd: subscription.cwd, compare: subscription.compare, requestId: this.createRequestId(), }); this.sendSessionMessage(message); } } private resubscribeTerminalDirectorySubscriptions(): void { if (this.terminalDirectorySubscriptions.size === 0) { return; } for (const subscription of this.terminalDirectorySubscriptions.values()) { this.sendSessionMessage({ type: "subscribe_terminals_request", cwd: subscription.cwd, ...(subscription.workspaceId !== undefined ? { workspaceId: subscription.workspaceId } : {}), }); } } private resubscribeFileSubscriptions(): void { for (const [subscriptionId, subscription] of this.fileSubscriptions) { void this.sendCorrelatedSessionRequest({ message: { type: "fs.file.subscribe.request", cwd: subscription.cwd, path: subscription.path, subscriptionId, }, responseType: "fs.file.subscribe.response", }) .then((payload) => subscription.onUpdate(payload.initial)) .catch(() => undefined); } } // ============================================================================ // Agent Lifecycle // ============================================================================ async createAgent(options: CreateAgentRequestOptions): Promise { const requestId = this.createRequestId(options.requestId); const config = resolveAgentConfig(options); const message = SessionInboundMessageSchema.parse({ type: "create_agent_request", requestId, config, ...(options.env ? { env: options.env } : {}), ...(options.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}), ...(options.callerAgentId !== undefined ? { callerAgentId: options.callerAgentId } : {}), ...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}), ...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}), ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}), ...(options.images && options.images.length > 0 ? { images: options.images } : {}), ...(options.attachments && options.attachments.length > 0 ? { attachments: options.attachments } : {}), ...(options.git ? { git: options.git } : {}), ...(options.worktree ? { worktree: options.worktree } : {}), ...(options.autoArchive !== undefined ? { autoArchive: options.autoArchive } : {}), ...(options.worktreeName ? { worktreeName: options.worktreeName } : {}), ...(options.labels && Object.keys(options.labels).length > 0 ? { labels: options.labels } : {}), }); const status = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "status") { return null; } const created = AgentCreatedStatusPayloadSchema.safeParse(msg.payload); if (created.success && created.data.requestId === requestId) { return created.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 deleteAgent(agentId: string): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "delete_agent_request", agentId, requestId, }); await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "agent_deleted") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); } async archiveAgent(agentId: string): Promise<{ archivedAt: string }> { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "archive_agent_request", agentId, requestId, }); const result = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "agent_archived") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); return { archivedAt: result.archivedAt }; } async detachAgent(agentId: string): Promise { const payload = await this.sendNamespacedCorrelatedSessionRequest<"agent.detach.response">({ message: { type: "agent.detach.request", agentId, }, }); if (!payload.accepted) { throw new Error(payload.error ?? "detachAgent rejected"); } } async updateAgent( agentId: string, updates: { name?: string; labels?: Record }, ): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "update_agent_request", agentId, ...(updates.name !== undefined ? { name: updates.name } : {}), ...(updates.labels && Object.keys(updates.labels).length > 0 ? { labels: updates.labels } : {}), requestId, }); const payload = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "update_agent_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!payload.accepted) { throw new Error(payload.error ?? "updateAgent rejected"); } } async renameProject( projectId: string, customName: string | null, requestId?: string, ): Promise<{ customName: string | null }> { const payload = await this.sendCorrelatedSessionRequest({ requestId, message: { type: "project.rename.request", projectId, customName, }, responseType: "project.rename.response", }); if (!payload.accepted) { throw new Error(payload.error ?? "renameProject rejected"); } return { customName: payload.customName }; } async setProjectIcon( projectId: string, source: ProjectIconSource, requestId?: string, ): Promise { const payload = await this.sendNamespacedCorrelatedSessionRequest<"project.icon.set.response">({ requestId, message: { type: "project.icon.set.request", projectId, source }, }); if (!payload.accepted) throw new Error(payload.error ?? "setProjectIcon rejected"); } async removeProject( projectId: string, requestId?: string, ): Promise<{ removedWorkspaceIds: string[] }> { const payload = await this.sendNamespacedCorrelatedSessionRequest<"project.remove.response">({ requestId, message: { type: "project.remove.request", projectId, }, }); if (!payload.accepted) { throw new Error(payload.error ?? "removeProject rejected"); } return { removedWorkspaceIds: payload.removedWorkspaceIds }; } async setWorkspaceTitle( workspaceId: string, title: string | null, requestId?: string, ): Promise<{ title: string | null }> { const payload = await this.sendCorrelatedSessionRequest({ requestId, message: { type: "workspace.title.set.request", workspaceId, title, }, responseType: "workspace.title.set.response", }); if (!payload.accepted) { throw new Error(payload.error ?? "setWorkspaceTitle rejected"); } return { title: payload.title }; } async setWorkspacePinned( workspaceId: string, pinned: boolean, requestId?: string, ): Promise<{ pinnedAt: string | null }> { const payload = await this.sendCorrelatedSessionRequest({ requestId, message: { type: "workspace.pin.set.request", workspaceId, pinned, }, responseType: "workspace.pin.set.response", }); if (!payload.accepted) { throw new Error(payload.error ?? "setWorkspacePinned rejected"); } return { pinnedAt: payload.pinnedAt }; } async inspectWorkspaceRecovery( workspaceId: string, requestId?: string, ): Promise { const payload = await this.sendNamespacedCorrelatedSessionRequest<"workspace.recovery.inspect.response">({ requestId, message: { type: "workspace.recovery.inspect.request", workspaceId, }, }); return payload.state; } async restoreWorkspace(workspaceId: string, requestId?: string): Promise { const payload = await this.sendNamespacedCorrelatedSessionRequest<"workspace.recovery.restore.response">({ requestId, message: { type: "workspace.recovery.restore.request", workspaceId, }, timeout: 150_000, }); if (!payload.accepted) { throw new Error(payload.error ?? "Workspace recovery was rejected by the host"); } } async resumeAgent( handle: AgentPersistenceHandle, overrides?: Partial, ): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "resume_agent_request", requestId, handle, ...(overrides ? { overrides } : {}), }); const status = await this.sendRequest({ requestId, message, 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; } return null; }, }); return status.agent; } async importAgent(input: ImportAgentInput): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "import_agent_request", requestId, ...("providerId" in input ? { providerId: input.providerId, providerHandleId: input.providerHandleId } : { provider: input.provider, sessionId: input.sessionId }), ...(input.cwd ? { cwd: input.cwd } : {}), ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}), ...(input.labels && Object.keys(input.labels).length > 0 ? { labels: input.labels } : {}), }); const status = await this.sendRequest({ requestId, message, 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 { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "refresh_agent_request", agentId, requestId: resolvedRequestId, }); return this.sendRequest({ requestId: resolvedRequestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "status") { return null; } const refreshed = AgentRefreshedStatusPayloadSchema.safeParse(msg.payload); if (refreshed.success && refreshed.data.requestId === resolvedRequestId) { return refreshed.data; } return null; }, }); } async fetchAgentTimeline( agentId: string, options: FetchAgentTimelineOptions = {}, ): Promise { const resolvedRequestId = this.createRequestId(options.requestId); const message = SessionInboundMessageSchema.parse({ type: "fetch_agent_timeline_request", agentId, requestId: resolvedRequestId, ...(options.direction ? { direction: options.direction } : {}), ...(options.cursor ? { cursor: options.cursor } : {}), ...(typeof options.limit === "number" ? { limit: options.limit } : {}), ...(options.projection ? { projection: options.projection } : {}), }); const payload = await this.sendRequest({ requestId: resolvedRequestId, message, timeout: options.timeout, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "fetch_agent_timeline_response") { return null; } if (msg.payload.requestId !== resolvedRequestId) { return null; } return msg.payload; }, }); if (payload.error) { throw new Error(payload.error); } return payload; } async listProviderSubagents( parentAgentId: string, options: { requestId?: string; timeout?: number } = {}, ): Promise { const requestId = this.createRequestId(options.requestId); const message = SessionInboundMessageSchema.parse({ type: "agent.provider_subagents.list.request", parentAgentId, requestId, }); const payload = await this.sendRequest({ requestId, message, timeout: options.timeout, options: { skipQueue: true }, select: (response) => response.type === "agent.provider_subagents.list.response" && response.payload.requestId === requestId ? response.payload : null, }); if (payload.error) { throw new Error(payload.error); } return payload; } async fetchProviderSubagentTimeline( parentAgentId: string, subagentId: string, options: FetchProviderSubagentTimelineOptions = {}, ): Promise { const requestId = this.createRequestId(options.requestId); const message = SessionInboundMessageSchema.parse({ type: "agent.provider_subagents.timeline.get.request", parentAgentId, subagentId, requestId, ...(options.direction ? { direction: options.direction } : {}), ...(options.cursor ? { cursor: options.cursor } : {}), ...(typeof options.limit === "number" ? { limit: options.limit } : {}), }); const payload = await this.sendRequest({ requestId, message, timeout: options.timeout, options: { skipQueue: true }, select: (response) => response.type === "agent.provider_subagents.timeline.get.response" && response.payload.requestId === requestId ? response.payload : null, }); if (payload.error) { throw new Error(payload.error); } return payload; } async setAgentTimelineSubscription(agentIds: string[]): Promise { // COMPAT(selectiveAgentTimeline): added in v0.1.106. Old daemons keep their // legacy global stream and do not understand this RPC. Remove after // 2027-01-12 once the supported daemon floor is >= v0.1.106. if (!this.lastServerInfoMessage?.features?.selectiveAgentTimeline) { return; } const requestId = this.createRequestId(); const normalizedAgentIds = [...new Set(agentIds)].sort(); const message = SessionInboundMessageSchema.parse({ type: "agent.timeline.set_subscription.request", agentIds: normalizedAgentIds, requestId, }); await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (response) => { if (response.type !== "agent.timeline.set_subscription.response") { return null; } return response.payload.requestId === requestId ? response.payload : null; }, }); } async buildAgentForkContext( agentId: string, options: AgentForkContextOptions = {}, ): Promise { const resolvedRequestId = this.createRequestId(options.requestId); const message = SessionInboundMessageSchema.parse({ type: "agent.fork_context.request", agentId, requestId: resolvedRequestId, ...(options.boundaryCursor ? { boundaryCursor: options.boundaryCursor } : {}), ...(options.boundaryMessageId ? { boundaryMessageId: options.boundaryMessageId } : {}), }); const payload = await this.sendRequest({ requestId: resolvedRequestId, message, timeout: 15000, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "agent.fork_context.response") { return null; } if (msg.payload.requestId !== resolvedRequestId) { return null; } return msg.payload; }, }); if (payload.error) { throw new Error(payload.error); } return payload; } // ============================================================================ // Agent Interaction // ============================================================================ async sendAgentMessage( agentId: string, text: string, options?: SendMessageOptions, ): Promise { const requestId = this.createRequestId(); const messageId = options?.messageId ?? crypto.randomUUID(); const message = SessionInboundMessageSchema.parse({ type: "send_agent_message_request", requestId, agentId, text, ...(messageId ? { messageId } : {}), ...(options?.images ? { images: options.images } : {}), ...(options?.attachments ? { attachments: options.attachments } : {}), }); const payload = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "send_agent_message_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!payload.accepted) { throw new Error(payload.error ?? "sendAgentMessage rejected"); } } async sendMessage(agentId: string, text: string, options?: SendMessageOptions): Promise { await this.sendAgentMessage(agentId, text, options); } async rewindAgent( agentId: string, messageId: string, mode: "conversation" | "files" | "both", ): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "agent.rewind.request", requestId, agentId, messageId, mode, }); const payload = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "agent.rewind.response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!payload.ok) { throw new Error(payload.error ?? "Agent rewind failed"); } return payload; } async cancelAgent(agentId: string): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "cancel_agent_request", agentId, requestId, }); const payload = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "cancel_agent_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (payload.error) { throw new Error(payload.error); } } async setAgentMode(agentId: string, modeId: string): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "set_agent_mode_request", agentId, modeId, requestId, }); const payload = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "set_agent_mode_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!payload.accepted) { throw new Error(payload.error ?? "setAgentMode rejected"); } return payload.notice ?? null; } async setAgentModel(agentId: string, modelId: string | null): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "set_agent_model_request", agentId, modelId, requestId, }); const payload = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "set_agent_model_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!payload.accepted) { throw new Error(payload.error ?? "setAgentModel rejected"); } } async setAgentFeature(agentId: string, featureId: string, value: unknown): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "set_agent_feature_request", agentId, featureId, value, requestId, }); const payload = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "set_agent_feature_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!payload.accepted) { throw new Error(payload.error ?? "setAgentFeature rejected"); } } async setAgentThinkingOption( agentId: string, thinkingOptionId: string | null, ): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "set_agent_thinking_request", agentId, thinkingOptionId, requestId, }); const payload = await this.sendRequest({ requestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "set_agent_thinking_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!payload.accepted) { throw new Error(payload.error ?? "setAgentThinkingOption rejected"); } return payload.notice ?? null; } async restartServer(reason?: string, requestId?: string): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "restart_server_request", ...(reason && reason.trim().length > 0 ? { reason } : {}), requestId: resolvedRequestId, }); return this.sendRequest({ requestId: resolvedRequestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "status") { return null; } const restarted = RestartRequestedStatusPayloadSchema.safeParse(msg.payload); if (!restarted.success) { return null; } if (restarted.data.requestId !== resolvedRequestId) { return null; } return restarted.data; }, }); } async shutdownServer(options?: ShutdownServerOptions): Promise { const resolvedRequestId = this.createRequestId(options?.requestId); const message = SessionInboundMessageSchema.parse({ type: "shutdown_server_request", requestId: resolvedRequestId, }); return this.sendRequest({ requestId: resolvedRequestId, message, timeout: options?.timeout, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "status") { return null; } const shutdown = ShutdownRequestedStatusPayloadSchema.safeParse(msg.payload); if (!shutdown.success) { return null; } if (shutdown.data.requestId !== resolvedRequestId) { return null; } return shutdown.data; }, }); } async updateDaemon(requestId?: string): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "daemon.update.request", requestId: resolvedRequestId, }); return this.sendRequest({ requestId: resolvedRequestId, message, timeout: 300_000, // 5 minutes — npm update can be slow on remote machines options: { skipQueue: true }, select: (msg) => { const parsed = DaemonUpdateResponseSchema.safeParse(msg); if (!parsed.success) { return null; } if (parsed.data.payload.requestId !== resolvedRequestId) { return null; } return parsed.data.payload; }, }); } // ============================================================================ // Audio / Voice // ============================================================================ async setVoiceMode(enabled: boolean, agentId?: string): Promise { const requestId = this.createRequestId(); const message = SessionInboundMessageSchema.parse({ type: "set_voice_mode", enabled, ...(agentId ? { agentId } : {}), requestId, }); const response = await this.sendRequest({ requestId, message, select: (msg) => { if (msg.type !== "set_voice_mode_response") { return null; } if (msg.payload.requestId !== requestId) { return null; } return msg.payload; }, }); if (!response.accepted) { const codeSuffix = typeof response.reasonCode === "string" && response.reasonCode.trim().length > 0 ? ` (${response.reasonCode})` : ""; throw new Error((response.error ?? "Failed to set voice mode") + codeSuffix); } return response; } async sendVoiceAudioChunk(audio: string, format: string, isLast = false): Promise { this.sendSessionMessage({ type: "voice_audio_chunk", audio, format, isLast }); } async startDictationStream(dictationId: string, format: string): Promise { const ack = this.waitForWithCancel( (msg) => { if (msg.type !== "dictation_stream_ack") { return null; } if (msg.payload.dictationId !== dictationId) { return null; } if (msg.payload.ackSeq !== -1) { return null; } return msg.payload; }, 30000, { skipQueue: true }, ); const ackPromise = ack.promise.then(() => undefined); const streamError = this.waitForWithCancel( (msg) => { if (msg.type !== "dictation_stream_error") { return null; } if (msg.payload.dictationId !== dictationId) { return null; } return msg.payload; }, 30000, { skipQueue: true }, ); const errorPromise = streamError.promise.then((payload) => { throw new Error(payload.error); }); const cleanupError = new Error("Cancelled dictation start waiter"); try { this.sendSessionMessageStrict({ type: "dictation_stream_start", dictationId, format }); await Promise.race([ackPromise, errorPromise]); } finally { ack.cancel(cleanupError); streamError.cancel(cleanupError); void ackPromise.catch(() => undefined); void errorPromise.catch(() => undefined); } } sendDictationStreamChunk(dictationId: string, seq: number, audio: string, format: string): void { this.sendSessionMessageStrict({ type: "dictation_stream_chunk", dictationId, seq, audio, format, }); } async finishDictationStream( dictationId: string, finalSeq: number, ): Promise<{ dictationId: string; text: string }> { const final = this.waitForWithCancel( (msg) => { if (msg.type !== "dictation_stream_final") { return null; } if (msg.payload.dictationId !== dictationId) { return null; } return msg.payload; }, 0, { skipQueue: true }, ); const streamError = this.waitForWithCancel( (msg) => { if (msg.type !== "dictation_stream_error") { return null; } if (msg.payload.dictationId !== dictationId) { return null; } return msg.payload; }, 0, { skipQueue: true }, ); const finishAccepted = this.waitForWithCancel( (msg) => { if (msg.type !== "dictation_stream_finish_accepted") { return null; } if (msg.payload.dictationId !== dictationId) { return null; } return msg.payload; }, DEFAULT_DICTATION_FINISH_ACCEPT_TIMEOUT_MS, { skipQueue: true }, ); const finalPromise = final.promise; const errorPromise = streamError.promise.then((payload) => { throw new Error(payload.error); }); const finishAcceptedPromise = finishAccepted.promise; const finalOutcomePromise = finalPromise.then((payload) => ({ kind: "final" as const, payload, })); const errorOutcomePromise = errorPromise.then( () => ({ kind: "error" as const, error: new Error("Unexpected dictation stream error state"), }), (error) => ({ kind: "error" as const, error: error instanceof Error ? error : new Error(String(error)), }), ); const finishAcceptedOutcomePromise = finishAcceptedPromise.then( (payload) => ({ kind: "accepted" as const, payload }), (error) => { if (isWaiterTimeoutError(error)) { return { kind: "accepted_timeout" as const }; } return { kind: "accepted_error" as const, error: error instanceof Error ? error : new Error(String(error)), }; }, ); const waitForFinalResult = async ( timeoutMs: number, ): Promise<{ dictationId: string; text: string }> => { if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { const outcome = await Promise.race([finalOutcomePromise, errorOutcomePromise]); if (outcome.kind === "error") { throw outcome.error; } return outcome.payload; } let timeoutHandle: ReturnType | null = null; const timeoutPromise = new Promise<{ kind: "timeout" }>((resolve) => { timeoutHandle = setTimeout(() => resolve({ kind: "timeout" }), timeoutMs); }); const outcome = await Promise.race([ finalOutcomePromise, errorOutcomePromise, timeoutPromise, ]); if (timeoutHandle) { clearTimeout(timeoutHandle); } if (outcome.kind === "timeout") { throw new Error(`Timeout waiting for dictation finalization (${timeoutMs}ms)`); } if (outcome.kind === "error") { throw outcome.error; } return outcome.payload; }; const cleanupError = new Error("Cancelled dictation finish waiter"); try { this.sendSessionMessageStrict({ type: "dictation_stream_finish", dictationId, finalSeq }); const firstOutcome = await Promise.race([ finalOutcomePromise, errorOutcomePromise, finishAcceptedOutcomePromise, ]); if (firstOutcome.kind === "final") { return firstOutcome.payload; } if (firstOutcome.kind === "error") { throw firstOutcome.error; } if (firstOutcome.kind === "accepted") { return await waitForFinalResult( firstOutcome.payload.timeoutMs + DEFAULT_DICTATION_FINISH_TIMEOUT_GRACE_MS, ); } return await waitForFinalResult(DEFAULT_DICTATION_FINISH_FALLBACK_TIMEOUT_MS); } finally { final.cancel(cleanupError); streamError.cancel(cleanupError); finishAccepted.cancel(cleanupError); void finalPromise.catch(() => undefined); void errorPromise.catch(() => undefined); void finishAcceptedPromise.catch(() => undefined); } } cancelDictationStream(dictationId: string): void { this.sendSessionMessageStrict({ type: "dictation_stream_cancel", dictationId }); } async abortRequest(): Promise { this.sendSessionMessage({ type: "abort_request" }); } async audioPlayed(id: string): Promise { this.sendSessionMessage({ type: "audio_played", id }); } // ============================================================================ // Git Operations // ============================================================================ async getCheckoutStatus( cwd: string, options?: { requestId?: string }, ): Promise { const requestId = options?.requestId; if (!requestId) { const existing = this.checkoutStatusInFlight.get(cwd); if (existing) { return existing; } } const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "checkout_status_request", cwd, requestId: resolvedRequestId, }); const responsePromise = this.sendRequest({ requestId: resolvedRequestId, message, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "checkout_status_response") { return null; } if (msg.payload.requestId !== resolvedRequestId) { return null; } return msg.payload; }, }); if (!requestId) { this.checkoutStatusInFlight.set(cwd, responsePromise); void responsePromise .finally(() => { if (this.checkoutStatusInFlight.get(cwd) === responsePromise) { this.checkoutStatusInFlight.delete(cwd); } }) .catch(() => undefined); } return responsePromise; } private normalizeCheckoutDiffCompare(compare: { mode: "uncommitted" | "base"; baseRef?: string; ignoreWhitespace?: boolean; }): { mode: "uncommitted" | "base"; baseRef?: string; ignoreWhitespace?: boolean } { if (compare.mode === "uncommitted") { return compare.ignoreWhitespace === true ? { mode: "uncommitted", ignoreWhitespace: true } : { mode: "uncommitted" }; } const trimmedBaseRef = compare.baseRef?.trim(); if (!trimmedBaseRef) { return compare.ignoreWhitespace === true ? { mode: "base", ignoreWhitespace: true } : { mode: "base" }; } return compare.ignoreWhitespace === true ? { mode: "base", baseRef: trimmedBaseRef, ignoreWhitespace: true } : { mode: "base", baseRef: trimmedBaseRef }; } async getCheckoutDiff( cwd: string, compare: { mode: "uncommitted" | "base"; baseRef?: string; ignoreWhitespace?: boolean }, requestId?: string, ): Promise { const oneShotSubscriptionId = `oneshot-checkout-diff:${crypto.randomUUID()}`; try { const payload = await this.subscribeCheckoutDiff(cwd, compare, { subscriptionId: oneShotSubscriptionId, requestId, }); return { cwd: payload.cwd, files: payload.files, error: payload.error, diffTooLarge: payload.diffTooLarge, requestId: payload.requestId, }; } finally { try { this.unsubscribeCheckoutDiff(oneShotSubscriptionId); } catch { // Ignore disconnect races during one-shot cleanup. } } } async subscribeCheckoutDiff( cwd: string, compare: { mode: "uncommitted" | "base"; baseRef?: string; ignoreWhitespace?: boolean }, options?: { subscriptionId?: string; requestId?: string }, ): Promise { const subscriptionId = options?.subscriptionId ?? crypto.randomUUID(); const normalizedCompare = this.normalizeCheckoutDiffCompare(compare); const previousSubscription = this.checkoutDiffSubscriptions.get(subscriptionId) ?? null; this.checkoutDiffSubscriptions.set(subscriptionId, { cwd, compare: normalizedCompare, }); const resolvedRequestId = this.createRequestId(options?.requestId); const message = SessionInboundMessageSchema.parse({ type: "subscribe_checkout_diff_request", subscriptionId, cwd, compare: normalizedCompare, requestId: resolvedRequestId, }); try { return await this.sendCorrelatedRequest({ requestId: resolvedRequestId, message, responseType: "subscribe_checkout_diff_response", options: { skipQueue: true }, selectPayload: (payload) => { if (payload.subscriptionId !== subscriptionId) { return null; } return payload; }, }); } catch (error) { if (previousSubscription) { this.checkoutDiffSubscriptions.set(subscriptionId, previousSubscription); } else { this.checkoutDiffSubscriptions.delete(subscriptionId); } throw error; } } unsubscribeCheckoutDiff(subscriptionId: string): void { this.checkoutDiffSubscriptions.delete(subscriptionId); this.sendSessionMessage({ type: "unsubscribe_checkout_diff_request", subscriptionId, }); } async checkoutCommit( cwd: string, input: { message?: string; addAll?: boolean }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_commit_request", cwd, message: input.message, addAll: input.addAll, }, responseType: "checkout_commit_response", }); } async checkoutMerge( cwd: string, input: { baseRef?: string; strategy?: "merge" | "squash"; requireCleanTarget?: boolean }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_merge_request", cwd, baseRef: input.baseRef, strategy: input.strategy, requireCleanTarget: input.requireCleanTarget, }, responseType: "checkout_merge_response", }); } async checkoutMergeFromBase( cwd: string, input: { baseRef?: string; requireCleanTarget?: boolean }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_merge_from_base_request", cwd, baseRef: input.baseRef, requireCleanTarget: input.requireCleanTarget, }, responseType: "checkout_merge_from_base_response", }); } async checkoutPull(cwd: string, requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_pull_request", cwd, }, responseType: "checkout_pull_response", }); } async checkoutPush(cwd: string, requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_push_request", cwd, }, responseType: "checkout_push_response", }); } async checkoutRefresh(cwd: string, requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout.refresh.request", cwd, }, responseType: "checkout.refresh.response", }); } async listCheckoutCommits( cwd: string, requestId?: string, ): Promise<{ baseRef: string | null; commits: CheckoutCommit[] }> { const payload = await this.sendNamespacedCorrelatedSessionRequest<"checkout.commits.list.response">({ requestId, message: { type: "checkout.commits.list.request", cwd, }, timeout: 60000, }); if (payload.error) { throw new Error(payload.error.message); } return { baseRef: payload.baseRef, commits: payload.commits }; } async getCommitFileDiff( cwd: string, sha: string, path: string, requestId?: string, ): Promise<{ file: ParsedDiffFile | null }> { const payload = await this.sendNamespacedCorrelatedSessionRequest<"checkout.commits.file_diff.response">({ requestId, message: { type: "checkout.commits.file_diff.request", cwd, sha, path, }, timeout: 60000, }); if (payload.error) { throw new Error(payload.error.message); } return { file: payload.file }; } async checkoutPrCreate( cwd: string, input: { title?: string; body?: string; baseRef?: string }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_pr_create_request", cwd, title: input.title, body: input.body, baseRef: input.baseRef, }, responseType: "checkout_pr_create_response", }); } async checkoutPrMerge( cwd: string, input: { method: CheckoutPrMergeMethod }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_pr_merge_request", cwd, mergeMethod: input.method, }, responseType: "checkout_pr_merge_response", }); } async checkoutForgeSetAutoMerge( cwd: string, input: { enabled: true; method: CheckoutPrMergeMethod } | { enabled: false }, requestId?: string, ): Promise { return this.sendNamespacedCorrelatedSessionRequest<"checkout.forge.set_auto_merge.response">({ requestId, message: { type: "checkout.forge.set_auto_merge.request", cwd, enabled: input.enabled, ...(input.enabled ? { mergeMethod: input.method } : {}), }, timeout: 60000, }); } async checkoutGithubSetAutoMerge( cwd: string, input: { enabled: true; method: CheckoutPrMergeMethod } | { enabled: false }, requestId?: string, ): Promise { return this.sendNamespacedCorrelatedSessionRequest<"checkout.github.set_auto_merge.response">({ requestId, message: { type: "checkout.github.set_auto_merge.request", cwd, enabled: input.enabled, ...(input.enabled ? { mergeMethod: input.method } : {}), }, }); } async checkoutForgeGetCheckDetails( input: { cwd: string; repoOwner?: string; repoName?: string; checkRunId?: number; workflowRunId?: number; changeRequestNumber?: number; }, requestId?: string, ): Promise { return this.sendNamespacedCorrelatedSessionRequest<"checkout.forge.get_check_details.response">( { requestId, message: { type: "checkout.forge.get_check_details.request", cwd: input.cwd, repoOwner: input.repoOwner, repoName: input.repoName, checkRunId: input.checkRunId, workflowRunId: input.workflowRunId, changeRequestNumber: input.changeRequestNumber, }, timeout: 60000, }, ); } async checkoutGithubGetCheckDetails( input: { cwd: string; repoOwner?: string; repoName?: string; checkRunId?: number; workflowRunId?: number; }, requestId?: string, ): Promise { return this.sendNamespacedCorrelatedSessionRequest<"checkout.github.get_check_details.response">( { requestId, message: { type: "checkout.github.get_check_details.request", cwd: input.cwd, repoOwner: input.repoOwner, repoName: input.repoName, checkRunId: input.checkRunId, workflowRunId: input.workflowRunId, }, }, ); } async checkoutPrStatus(cwd: string, requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_pr_status_request", cwd, }, responseType: "checkout_pr_status_response", }); } async pullRequestTimeline( input: { cwd: string; prNumber: number; repoOwner: string; repoName: string }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "pull_request_timeline_request", cwd: input.cwd, prNumber: input.prNumber, repoOwner: input.repoOwner, repoName: input.repoName, }, responseType: "pull_request_timeline_response", }); } async checkoutSwitchBranch( cwd: string, branch: string, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "checkout_switch_branch_request", cwd, branch, }, responseType: "checkout_switch_branch_response", }); } async renameBranch(input: RenameBranchInput): Promise { return this.sendCorrelatedSessionRequest({ requestId: input.requestId, message: { type: "checkout.rename_branch.request", cwd: input.cwd, branch: input.branch, }, responseType: "checkout.rename_branch.response", }); } async stashSave( cwd: string, options?: { branch?: string }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "stash_save_request", cwd, branch: options?.branch, }, responseType: "stash_save_response", }); } async stashPop(cwd: string, stashIndex: number, requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "stash_pop_request", cwd, stashIndex, }, responseType: "stash_pop_response", }); } async stashList( cwd: string, options?: { paseoOnly?: boolean }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "stash_list_request", cwd, paseoOnly: options?.paseoOnly, }, responseType: "stash_list_response", }); } async getPaseoWorktreeList( input: { cwd?: string; repoRoot?: string }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "paseo_worktree_list_request", cwd: input.cwd, repoRoot: input.repoRoot, }, responseType: "paseo_worktree_list_response", }); } async archivePaseoWorktree( input: { worktreePath?: string; repoRoot?: string; branchName?: string; workspaceId?: string; scope?: "workspace" | "worktree"; }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "paseo_worktree_archive_request", worktreePath: input.worktreePath, repoRoot: input.repoRoot, branchName: input.branchName, ...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}), ...(input.scope !== undefined ? { scope: input.scope } : {}), }, responseType: "paseo_worktree_archive_response", }); } async createPaseoWorktree( input: CreatePaseoWorktreeInput, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "create_paseo_worktree_request", cwd: input.cwd, ...(input.projectId !== undefined ? { projectId: input.projectId } : {}), worktreeSlug: input.worktreeSlug, ...(input.firstAgentContext !== undefined ? { firstAgentContext: input.firstAgentContext } : {}), ...(input.refName !== undefined ? { refName: input.refName } : {}), ...(input.action !== undefined ? { action: input.action } : {}), ...(input.checkoutSource !== undefined ? { checkoutSource: input.checkoutSource } : {}), ...(input.githubPrNumber !== undefined ? { githubPrNumber: input.githubPrNumber } : {}), }, responseType: "create_paseo_worktree_response", }); } async createWorkspace( input: { source: WorkspaceCreateRequest["source"]; title?: string; firstAgentContext?: WorkspaceCreateRequest["firstAgentContext"]; }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "workspace.create.request", source: input.source, ...(input.title !== undefined ? { title: input.title } : {}), ...(input.firstAgentContext !== undefined ? { firstAgentContext: input.firstAgentContext } : {}), }, responseType: "workspace.create.response", }); } async validateBranch( options: { cwd: string; branchName: string }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "validate_branch_request", cwd: options.cwd, branchName: options.branchName, }, responseType: "validate_branch_response", }); } async getBranchSuggestions( options: { cwd: string; query?: string; limit?: number }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "branch_suggestions_request", cwd: options.cwd, query: options.query, limit: options.limit, }, responseType: "branch_suggestions_response", }); } async searchForge( options: { cwd: string; query: string; limit?: number; kinds?: ForgeSearchRequest["kinds"] }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "forge.search.request", cwd: options.cwd, query: options.query, limit: options.limit, kinds: options.kinds, }, responseType: "forge.search.response", timeout: 15000, }); } async searchGitHub( options: { cwd: string; query: string; limit?: number; kinds?: GitHubSearchRequest["kinds"] }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "github_search_request", cwd: options.cwd, query: options.query, limit: options.limit, kinds: options.kinds, }, responseType: "github_search_response", }); } async getDirectorySuggestions( options: { query: string; limit?: number; cwd?: string; includeFiles?: boolean; includeDirectories?: boolean; matchMode?: "fuzzy" | "suffix"; }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "directory_suggestions_request", query: options.query, cwd: options.cwd, includeFiles: options.includeFiles, includeDirectories: options.includeDirectories, matchMode: options.matchMode, limit: options.limit, }, responseType: "directory_suggestions_response", // Home-tree scans on large home dirs can take several seconds; don't cut // the suggestion request off early (it would surface as an empty list). }); } // ============================================================================ // File Explorer // ============================================================================ private async requestFileExplorer( cwd: string, path: string, mode: "list" | "file", requestId?: string, acceptBinary = false, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "file_explorer_request", cwd, path, mode, ...(acceptBinary ? { acceptBinary: true } : {}), }, responseType: "file_explorer_response", }); } async listDirectory( cwd: string, path: string, requestId?: string, ): Promise { const payload = await this.requestFileExplorer(cwd, path, "list", requestId); if (payload.error) { throw new Error(payload.error); } if (!payload.directory) { throw new Error("Directory listing unavailable."); } return payload.directory; } async readFile(cwd: string, path: string, requestId?: string): Promise { const resolvedRequestId = this.createRequestId(requestId); this.pendingBinaryFileReads.set(resolvedRequestId, { cwd, path }); try { const payload = await this.requestFileExplorer(cwd, path, "file", resolvedRequestId, true); if (payload.error) { throw new Error(payload.error); } const binaryResult = this.completedBinaryFileReads.get(resolvedRequestId); if (binaryResult) { this.completedBinaryFileReads.delete(resolvedRequestId); return binaryResult; } if (!payload.file) { throw new Error("File unavailable."); } return legacyExplorerFileToBytes(payload.file); } finally { this.pendingBinaryFileReads.delete(resolvedRequestId); this.activeBinaryFileTransfers.delete(resolvedRequestId); } } async subscribeFile( input: { cwd: string; path: string }, onUpdate: (version: FileVersion) => void, ): Promise<{ initial: FileVersion; unsubscribe: () => void }> { const subscriptionId = this.createRequestId(); this.fileSubscriptions.set(subscriptionId, { ...input, onUpdate }); try { const payload = await this.sendCorrelatedSessionRequest({ message: { type: "fs.file.subscribe.request", cwd: input.cwd, path: input.path, subscriptionId, }, responseType: "fs.file.subscribe.response", }); return { initial: payload.initial, unsubscribe: () => { if (!this.fileSubscriptions.delete(subscriptionId)) return; void this.sendCorrelatedSessionRequest({ message: { type: "fs.file.unsubscribe.request", subscriptionId }, responseType: "fs.file.unsubscribe.response", }).catch(() => undefined); }, }; } catch (error) { this.fileSubscriptions.delete(subscriptionId); throw error; } } async writeFile(input: { cwd: string; path: string; content: string; expectedModifiedAt: string; expectedRevision?: string; }): Promise { const payload = await this.sendCorrelatedSessionRequest({ message: { type: "fs.file.write.request", ...input }, responseType: "fs.file.write.response", }); return payload.result; } async uploadFile(input: FileUploadInput): Promise { const bytes = asUint8Array(input.bytes); if (!bytes) { throw new Error("File bytes are required."); } const resolvedRequestId = this.createRequestId(input.requestId); const modifiedAt = input.modifiedAt ?? new Date().toISOString(); const responsePromise = this.sendCorrelatedRequest({ requestId: resolvedRequestId, message: { type: "file.upload.request", fileName: input.fileName, mimeType: input.mimeType, size: bytes.byteLength, modifiedAt, requestId: resolvedRequestId, }, responseType: "file.upload.response", options: { skipQueue: true }, }); this.sendBinaryFrame( encodeFileTransferFrame({ opcode: FileTransferOpcode.FileBegin, requestId: resolvedRequestId, metadata: { mime: input.mimeType, size: bytes.byteLength, encoding: "binary", modifiedAt, fileName: input.fileName, }, }), ); const chunkSize = input.chunkSize ?? 1024 * 1024; for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) { this.sendBinaryFrame( encodeFileTransferFrame({ opcode: FileTransferOpcode.FileChunk, requestId: resolvedRequestId, payload: bytes.subarray(offset, Math.min(offset + chunkSize, bytes.byteLength)), }), ); } this.sendBinaryFrame( encodeFileTransferFrame({ opcode: FileTransferOpcode.FileEnd, requestId: resolvedRequestId, }), ); return responsePromise; } async requestDownloadToken( cwd: string, path: string, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "file_download_token_request", cwd, path, }, responseType: "file_download_token_response", }); } async requestProjectIcon( cwd: string, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "project_icon_request", cwd, }, responseType: "project_icon_response", }); } async getProjectIcon( projectId: string, requestId?: string, ): Promise { return this.sendNamespacedCorrelatedSessionRequest<"project.icon.get.response">({ requestId, message: { type: "project.icon.get.request", projectId }, }); } // ============================================================================ // Provider Models / Commands // ============================================================================ async listProviderModels( provider: AgentProvider, options?: { cwd?: string; requestId?: string }, ): Promise { const payload = await this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "list_provider_models_request", provider, cwd: options?.cwd, }, responseType: "list_provider_models_response", // Provider SDK cold starts (especially model discovery) can exceed 60s. timeout: 90000, }); return normalizeListProviderModelsPayload(payload); } async listProviderModes( provider: AgentProvider, options?: { cwd?: string; requestId?: string }, ): Promise { return this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "list_provider_modes_request", provider, cwd: options?.cwd, }, responseType: "list_provider_modes_response", timeout: 90000, }); } async listProviderFeatures( draftConfig: ListCommandsDraftConfig, options?: { requestId?: string }, ): Promise { return this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "list_provider_features_request", draftConfig, }, responseType: "list_provider_features_response", timeout: 90000, }); } async listAvailableProviders(options?: { requestId?: string; }): Promise { return this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "list_available_providers_request", }, responseType: "list_available_providers_response", }); } async getProvidersSnapshot(options?: { cwd?: string; requestId?: string; }): Promise { const payload = await this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "get_providers_snapshot_request", cwd: options?.cwd, }, responseType: "get_providers_snapshot_response", }); return normalizeProvidersSnapshotPayload(payload); } async getDaemonConfig( requestId?: string, ): Promise<{ requestId: string; config: MutableDaemonConfig }> { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "get_daemon_config_request", }, responseType: "get_daemon_config_response", }); } async getDaemonStatus(options?: DaemonStatusOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "daemon.get_status.request", }, responseType: "daemon.get_status.response", timeout: options?.timeout, }); } async connectHub(hubUrl: string, token: string, requestId?: string) { this.requireHubRelationshipSupport(); return this.sendCorrelatedSessionRequest({ requestId, message: { type: "hub.management.daemon.connect.request", hubUrl, token }, responseType: "hub.management.daemon.connect.response", }); } async getHubStatus(requestId?: string) { this.requireHubRelationshipSupport(); return this.sendCorrelatedSessionRequest({ requestId, message: { type: "hub.management.daemon.get_status.request" }, responseType: "hub.management.daemon.get_status.response", }); } async disconnectHub(force = false, requestId?: string) { this.requireHubRelationshipSupport(); return this.sendCorrelatedSessionRequest({ requestId, message: { type: "hub.management.daemon.disconnect.request", force }, responseType: "hub.management.daemon.disconnect.response", }); } async getDaemonPairingOffer( options?: DaemonPairingOfferOptions, ): Promise { return this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "daemon.get_pairing_offer.request", }, responseType: "daemon.get_pairing_offer.response", timeout: options?.timeout, }); } async collectDiagnostics(requestId?: string): Promise { return this.sendNamespacedCorrelatedSessionRequest({ requestId, message: { type: "diagnostics.request", }, }); } async patchDaemonConfig( config: MutableDaemonConfigPatch, requestId?: string, ): Promise<{ requestId: string; config: MutableDaemonConfig }> { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "set_daemon_config_request", config, }, responseType: "set_daemon_config_response", }); } sendBrowserAutomationExecuteResponse(response: BrowserAutomationExecuteResponse): void { this.sendSessionMessageStrict(response); } async readProjectConfig(repoRoot: string, requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "read_project_config_request", repoRoot, }, responseType: "read_project_config_response", }); } async writeProjectConfig(input: WriteProjectConfigInput): Promise { return this.sendCorrelatedSessionRequest({ requestId: input.requestId, message: { type: "write_project_config_request", repoRoot: input.repoRoot, config: input.config, expectedRevision: input.expectedRevision, }, responseType: "write_project_config_response", }); } async refreshProvidersSnapshot(options?: { cwd?: string; providers?: AgentProvider[]; requestId?: string; }): Promise { return this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "refresh_providers_snapshot_request", cwd: options?.cwd, providers: options?.providers, }, responseType: "refresh_providers_snapshot_response", timeout: 120000, }); } async getProviderDiagnostic( provider: AgentProvider, options?: { requestId?: string }, ): Promise { return this.sendCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "provider_diagnostic_request", provider, }, responseType: "provider_diagnostic_response", timeout: 180000, }); } async listProviderUsage(options?: { requestId?: string }): Promise { return this.sendNamespacedCorrelatedSessionRequest({ requestId: options?.requestId, message: { type: "provider.usage.list.request", }, }); } async listCommands(options: ListCommandsOptions): Promise; async listCommands(agentId: string, requestId?: string): Promise; async listCommands( agentId: string, options?: LegacyListCommandsOptions, ): Promise; async listCommands( input: ListCommandsOptions | string, legacyOptions?: LegacyListCommandsOptions | string, ): Promise { const options = normalizeListCommandsOptions(input, legacyOptions); return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "list_commands_request", agentId: options.agentId, ...(options.draftConfig ? { draftConfig: options.draftConfig } : {}), }, responseType: "list_commands_response", }); } // ============================================================================ // Permissions // ============================================================================ async respondToPermission( agentId: string, requestId: string, response: AgentPermissionResponse, ): Promise { this.sendSessionMessage({ type: "agent_permission_response", agentId, requestId, response, }); } async respondToPermissionAndWait( agentId: string, requestId: string, response: AgentPermissionResponse, timeout = 15000, ): Promise { const message = SessionInboundMessageSchema.parse({ type: "agent_permission_response", agentId, requestId, response, }); return this.sendRequest({ requestId, message, timeout, options: { skipQueue: true }, select: (msg) => { if (msg.type !== "agent_permission_resolved") { return null; } if (msg.payload.requestId !== requestId) { return null; } if (msg.payload.agentId !== agentId) { return null; } return msg.payload; }, }); } // ============================================================================ // Waiting / Streaming Helpers // ============================================================================ async waitForAgentUpsert( agentId: string, predicate: (snapshot: AgentSnapshotPayload) => boolean, timeout = 60000, ): Promise { const deadline = Date.now() + timeout; const remainingTimeoutMs = () => Math.max(1, deadline - Date.now()); const timeoutError = () => new Error(`Timed out waiting for agent ${agentId}`); const fetchAgentWithinDeadline = () => this.fetchAgent({ agentId, timeout: remainingTimeoutMs() }).catch(() => null); const initialResult = await fetchAgentWithinDeadline(); if (initialResult && predicate(initialResult.agent)) { return initialResult.agent; } if (Date.now() >= deadline) { throw timeoutError(); } return await new Promise((resolve, reject) => { let settled = false; let pollInFlight = false; let pollTimer: ReturnType | null = null; let timeoutTimer: ReturnType | null = null; let unsubscribe: (() => void) | null = null; const finish = ( result: { kind: "ok"; snapshot: AgentSnapshotPayload } | { kind: "error"; error: Error }, ) => { if (settled) { return; } settled = true; if (timeoutTimer) { clearTimeout(timeoutTimer); timeoutTimer = null; } if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } if (unsubscribe) { unsubscribe(); unsubscribe = null; } if (result.kind === "ok") { resolve(result.snapshot); return; } reject(result.error); }; const maybeResolve = (snapshot: AgentSnapshotPayload | null) => { if (!snapshot) { return false; } if (!predicate(snapshot)) { return false; } finish({ kind: "ok", snapshot }); return true; }; const poll = async () => { if (settled || pollInFlight) { return; } pollInFlight = true; try { const result = await fetchAgentWithinDeadline(); maybeResolve(result?.agent ?? null); } finally { pollInFlight = false; } }; unsubscribe = this.on("agent_update", (message) => { if (settled) { return; } if (message.payload.kind !== "upsert") { return; } const snapshot = message.payload.agent; if (snapshot.id !== agentId) { return; } maybeResolve(snapshot); }); const remaining = Math.max(1, deadline - Date.now()); timeoutTimer = setTimeout(() => { finish({ kind: "error", error: timeoutError(), }); }, remaining); pollTimer = setInterval(() => { void poll(); }, 250); void poll(); }); } async waitForFinish(agentId: string, timeout = 60000): Promise { const requestId = this.createRequestId(); const hasTimeout = Number.isFinite(timeout) && timeout > 0; const message = SessionInboundMessageSchema.parse({ type: "wait_for_finish_request", requestId, agentId, ...(hasTimeout ? { timeoutMs: timeout } : {}), }); const payload = await this.sendCorrelatedRequest({ requestId, message, responseType: "wait_for_finish_response", timeout: hasTimeout ? timeout + 5000 : 0, options: { skipQueue: true }, }); return { status: payload.status, final: payload.final, error: payload.error, lastMessage: payload.lastMessage, }; } // ============================================================================ // Terminals // ============================================================================ subscribeTerminals(input: { cwd: string; workspaceId?: string }): void { this.terminalDirectorySubscriptions.set(terminalSubscriptionKey(input.cwd, input.workspaceId), { cwd: input.cwd, workspaceId: input.workspaceId, }); if (!this.transport || this.connectionState.status !== "connected") { return; } this.sendSessionMessage({ type: "subscribe_terminals_request", cwd: input.cwd, ...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}), }); } unsubscribeTerminals(input: { cwd: string; workspaceId?: string }): void { this.terminalDirectorySubscriptions.delete( terminalSubscriptionKey(input.cwd, input.workspaceId), ); if (!this.transport || this.connectionState.status !== "connected") { return; } this.sendSessionMessage({ type: "unsubscribe_terminals_request", cwd: input.cwd, ...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}), }); } async listTerminals( cwd?: string, requestId?: string, options?: { workspaceId?: string }, ): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "list_terminals_request", ...(cwd === undefined ? {} : { cwd }), ...(options?.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}), requestId: resolvedRequestId, }); return this.sendCorrelatedRequest({ requestId: resolvedRequestId, message, responseType: "list_terminals_response", options: { skipQueue: true }, }); } async createTerminal( cwd: string, name?: string, requestId?: string, options?: { agentId?: string; command?: string; args?: string[]; workspaceId?: string; size?: { rows: number; cols: number }; }, ): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "create_terminal_request", cwd, name, agentId: options?.agentId, command: options?.command, args: options?.args, ...(options?.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}), ...(options?.size !== undefined ? { size: options.size } : {}), requestId: resolvedRequestId, }); return this.sendCorrelatedRequest({ requestId: resolvedRequestId, message, responseType: "create_terminal_response", options: { skipQueue: true }, }); } async renameTerminal(input: RenameTerminalInput): Promise { return this.sendCorrelatedSessionRequest({ requestId: input.requestId, message: { type: "terminal.rename.request", terminalId: input.terminalId, title: input.title, }, responseType: "terminal.rename.response", }); } async subscribeTerminal( terminalId: string, optionsOrRequestId?: | { restore?: SubscribeTerminalRequest["restore"]; requestId?: string } | string, ): Promise { const restore = typeof optionsOrRequestId === "object" ? optionsOrRequestId.restore : undefined; const requestId = typeof optionsOrRequestId === "object" ? optionsOrRequestId.requestId : optionsOrRequestId; const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "subscribe_terminal_request", terminalId, requestId: resolvedRequestId, ...(restore ? { restore } : {}), }); const payload = await this.sendCorrelatedRequest({ requestId: resolvedRequestId, message, responseType: "subscribe_terminal_response", options: { skipQueue: true }, }); if (payload.error === null) { this.terminalStreams.setSlot(terminalId, payload.slot); } return payload; } unsubscribeTerminal(terminalId: string): void { this.terminalStreams.removeTerminal(terminalId); this.sendSessionMessage({ type: "unsubscribe_terminal_request", terminalId, }); } sendTerminalInput(terminalId: string, message: TerminalInput["message"]): void { const frame = this.terminalStreams.encodeInput(terminalId, message); if (frame) { this.sendBinaryFrame(frame); return; } this.sendSessionMessage({ type: "terminal_input", terminalId, message, }); } async killTerminal(terminalId: string, requestId?: string): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "kill_terminal_request", terminalId, requestId: resolvedRequestId, }); return this.sendCorrelatedRequest({ requestId: resolvedRequestId, message, responseType: "kill_terminal_response", options: { skipQueue: true }, }); } async closeItems( input: { agentIds?: string[]; terminalIds?: string[] }, requestId?: string, ): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "close_items_request", agentIds: input.agentIds ?? [], terminalIds: input.terminalIds ?? [], requestId: resolvedRequestId, }); return this.sendCorrelatedRequest({ requestId: resolvedRequestId, message, responseType: "close_items_response", options: { skipQueue: true }, }); } async captureTerminal( terminalId: string, options?: { start?: number; end?: number; stripAnsi?: boolean }, requestId?: string, ): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "capture_terminal_request", terminalId, ...(options?.start === undefined ? {} : { start: options.start }), ...(options?.end === undefined ? {} : { end: options.end }), ...(options?.stripAnsi === undefined ? {} : { stripAnsi: options.stripAnsi }), requestId: resolvedRequestId, }); return this.sendCorrelatedRequest({ requestId: resolvedRequestId, message, responseType: "capture_terminal_response", options: { skipQueue: true }, }); } async createChatRoom(options: CreateChatRoomOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "chat/create", name: options.name, ...(options.purpose ? { purpose: options.purpose } : {}), }, responseType: "chat/create/response", }); } async listChatRooms(requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "chat/list", }, responseType: "chat/list/response", }); } async inspectChatRoom(options: InspectChatRoomOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "chat/inspect", room: options.room, }, responseType: "chat/inspect/response", }); } async deleteChatRoom(options: DeleteChatRoomOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "chat/delete", room: options.room, }, responseType: "chat/delete/response", }); } async postChatMessage(options: PostChatMessageOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "chat/post", room: options.room, body: options.body, ...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}), ...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}), }, responseType: "chat/post/response", }); } async readChatMessages(options: ReadChatMessagesOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "chat/read", room: options.room, ...(typeof options.limit === "number" ? { limit: options.limit } : {}), ...(options.since ? { since: options.since } : {}), ...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}), }, responseType: "chat/read/response", timeout: options.timeout, }); } async waitForChatMessages(options: WaitForChatMessagesOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "chat/wait", room: options.room, ...(options.afterMessageId ? { afterMessageId: options.afterMessageId } : {}), ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}), }, responseType: "chat/wait/response", timeout: (options.timeoutMs ?? 0) + 10000, }); } async scheduleCreate(options: CreateScheduleOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "schedule/create", prompt: options.prompt, cadence: options.cadence, target: options.target, ...(options.name ? { name: options.name } : {}), ...(typeof options.maxRuns === "number" ? { maxRuns: options.maxRuns } : {}), ...(options.expiresAt ? { expiresAt: options.expiresAt } : {}), ...(typeof options.runOnCreate === "boolean" ? { runOnCreate: options.runOnCreate } : {}), }, responseType: "schedule/create/response", }); } async scheduleList(requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "schedule/list", }, responseType: "schedule/list/response", }); } async scheduleInspect(options: InspectScheduleOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "schedule/inspect", scheduleId: options.id, }, responseType: "schedule/inspect/response", }); } async scheduleLogs(options: InspectScheduleOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "schedule/logs", scheduleId: options.id, }, responseType: "schedule/logs/response", }); } async schedulePause(options: InspectScheduleOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "schedule/pause", scheduleId: options.id, }, responseType: "schedule/pause/response", }); } async scheduleResume(options: InspectScheduleOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "schedule/resume", scheduleId: options.id, }, responseType: "schedule/resume/response", }); } async scheduleDelete(options: InspectScheduleOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "schedule/delete", scheduleId: options.id, }, responseType: "schedule/delete/response", }); } async scheduleRunOnce(options: InspectScheduleOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "schedule/run-once", scheduleId: options.id, }, responseType: "schedule/run-once/response", }); } async scheduleUpdate(options: UpdateScheduleOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "schedule/update", scheduleId: options.id, ...(options.name !== undefined ? { name: options.name } : {}), ...(options.prompt !== undefined ? { prompt: options.prompt } : {}), ...(options.cadence !== undefined ? { cadence: options.cadence } : {}), ...(options.newAgentConfig !== undefined ? { newAgentConfig: options.newAgentConfig } : {}), ...(options.maxRuns !== undefined ? { maxRuns: options.maxRuns } : {}), ...(options.expiresAt !== undefined ? { expiresAt: options.expiresAt } : {}), }, responseType: "schedule/update/response", }); } async loopRun(options: RunLoopOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, message: { type: "loop/run", prompt: options.prompt, cwd: options.cwd, ...(options.provider ? { provider: options.provider } : {}), ...(options.model ? { model: options.model } : {}), ...(options.modeId ? { modeId: options.modeId } : {}), ...(options.verifierProvider ? { verifierProvider: options.verifierProvider } : {}), ...(options.verifierModel ? { verifierModel: options.verifierModel } : {}), ...(options.verifierModeId ? { verifierModeId: options.verifierModeId } : {}), ...(options.verifyPrompt ? { verifyPrompt: options.verifyPrompt } : {}), ...(options.verifyChecks && options.verifyChecks.length > 0 ? { verifyChecks: options.verifyChecks } : {}), ...(options.name ? { name: options.name } : {}), ...(typeof options.sleepMs === "number" ? { sleepMs: options.sleepMs } : {}), ...(typeof options.maxIterations === "number" ? { maxIterations: options.maxIterations } : {}), ...(typeof options.maxTimeMs === "number" ? { maxTimeMs: options.maxTimeMs } : {}), }, responseType: "loop/run/response", }); } async loopList(requestId?: string): Promise { return this.sendCorrelatedSessionRequest({ requestId, message: { type: "loop/list", }, responseType: "loop/list/response", }); } async loopInspect(options: string | InspectLoopOptions): Promise { const normalized = typeof options === "string" ? { id: options } : options; return this.sendCorrelatedSessionRequest({ requestId: normalized.requestId, message: { type: "loop/inspect", id: normalized.id, }, responseType: "loop/inspect/response", }); } async loopLogs(options: string | LoopLogsOptions, afterSeq?: number): Promise { const normalized = typeof options === "string" ? { id: options, afterSeq } : options; return this.sendCorrelatedSessionRequest({ requestId: normalized.requestId, message: { type: "loop/logs", id: normalized.id, ...(typeof normalized.afterSeq === "number" ? { afterSeq: normalized.afterSeq } : {}), }, responseType: "loop/logs/response", }); } async loopStop(options: string | StopLoopOptions): Promise { const normalized = typeof options === "string" ? { id: options } : options; return this.sendCorrelatedSessionRequest({ requestId: normalized.requestId, message: { type: "loop/stop", id: normalized.id, }, responseType: "loop/stop/response", }); } onTerminalStreamEvent(handler: (event: TerminalStreamEvent) => void): () => void { return this.terminalStreams.onEvent(handler); } async waitForTerminalStreamEvent( predicate: (event: TerminalStreamEvent) => boolean, timeout = 5000, ): Promise { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { unsubscribe(); reject(new Error(`Timeout waiting for terminal stream event (${timeout}ms)`)); }, timeout); const unsubscribe = this.onTerminalStreamEvent((event) => { if (!predicate(event)) { return; } clearTimeout(timeoutHandle); unsubscribe(); resolve(event); }); }); } // ============================================================================ // Internals // ============================================================================ private createRequestId(requestId?: string): string { return requestId ?? crypto.randomUUID(); } getLastServerInfoMessage(): ServerInfoStatusPayload | null { return this.lastServerInfoMessage; } private requireHubRelationshipSupport(): void { // COMPAT(hubRelationship): added in v0.1.X, drop the gate when floor >= v0.1.X. if (this.lastServerInfoMessage?.features?.hubRelationship !== true) { throw new Error("Update the host to use Hub relationship management."); } } private resolveTransportUrlForAttempt(): string { return this.config.url; } private sendHelloMessage(): void { if (!this.transport) { this.scheduleReconnect({ reason: "Transport unavailable before hello", event: "HELLO_TRANSPORT_MISSING", reasonCode: "transport_error", }); return; } try { this.transport.send( JSON.stringify({ type: "hello", clientId: this.config.clientId, clientType: this.config.clientType ?? "cli", protocolVersion: 1, capabilities: { [CLIENT_CAPS.customModeIcons]: true, [CLIENT_CAPS.reasoningMergeEnum]: true, [CLIENT_CAPS.terminalReflowableSnapshot]: true, [CLIENT_CAPS.providerSubagents]: true, [CLIENT_CAPS.projectUpdates]: true, ...this.config.capabilities, }, ...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}), }), ); } catch (error) { const message = error instanceof Error ? error.message : "Failed to send hello message"; this.lastErrorValue = message; this.scheduleReconnect({ reason: message, event: "HELLO_SEND_FAILED", reasonCode: "transport_error", }); } } private disposeTransport(code = 1001, reason = "Reconnecting"): void { this.stopLivenessHeartbeat(); this.cleanupTransport(); if (this.transport) { try { this.transport.close(code, reason); } catch { // no-op } this.transport = null; } } private cleanupTransport(): void { this.resetConnectTimeout(); if (this.pendingGenericTransportErrorTimeout) { clearTimeout(this.pendingGenericTransportErrorTimeout); this.pendingGenericTransportErrorTimeout = null; } for (const cleanup of this.transportCleanup) { try { cleanup(); } catch { // no-op } } this.transportCleanup = []; } private resetConnectTimeout(): void { if (!this.connectTimeout) { return; } clearTimeout(this.connectTimeout); this.connectTimeout = null; } private handleTransportMessage(data: unknown): void { const rawData = data && typeof data === "object" && "data" in data ? (data as { data: unknown }).data : data; if ( typeof Blob !== "undefined" && rawData instanceof Blob && typeof rawData.arrayBuffer === "function" ) { void rawData .arrayBuffer() .then((buffer) => { this.handleTransportMessage(buffer); return; }) .catch(() => { // Ignore failed blob decoding and allow reconnect logic to recover. }); return; } const rawBytes = asUint8Array(rawData); if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) { return; } const payload = decodeMessageData(rawData); if (!payload) { return; } this.handleJsonPayload(payload, rawBytes?.byteLength); } private handleJsonPayload(payload: string, rawBytesLength: number | undefined): void { const bytes = rawBytesLength ?? payload.length; const startMs = perfNow(); let parsedJson: unknown; try { parsedJson = JSON.parse(payload); } catch { return; } const parsed = validateWSOutboundMessage(parsedJson); if (!parsed.success) { const responseIdentity = extractCorrelatedResponseIdentity(parsedJson); const envelopeType = parsedJson != null && typeof parsedJson === "object" && "type" in parsedJson && typeof parsedJson.type === "string" ? parsedJson.type : "unknown"; const msgType = responseIdentity?.responseType ?? envelopeType; this.logger.warn({ msgType, error: parsed.error.message }, "Message validation failed"); if (responseIdentity) { this.rejectWaitersForRequestId( responseIdentity.requestId, new DaemonProtocolError(responseIdentity), ); } return; } this.consecutiveLivenessFailures = 0; if (parsed.data.type === "pong") { this.resolvePingProbe(); this.runtimeMetrics?.recordMessage("pong", bytes, perfNow() - startMs); return; } this.handleSessionMessage(parsed.data.message); const msgType = parsed.data.message.type; this.runtimeMetrics?.recordMessage(msgType, bytes, perfNow() - startMs); if (parsed.data.message.type === "agent_stream") { this.runtimeMetrics?.recordAgentStream(parsed.data.message.payload); } } private tryHandleBinaryFrame(rawBytes: Uint8Array): boolean { const fileFrame = decodeFileTransferFrame(rawBytes); if (fileFrame) { this.consecutiveLivenessFailures = 0; this.handleFileTransferFrame(fileFrame); this.runtimeMetrics?.recordBinaryFrame("other", rawBytes.byteLength, 0); return true; } const frame = decodeTerminalStreamFrame(rawBytes); if (!frame) { return false; } this.consecutiveLivenessFailures = 0; const binaryStartMs = perfNow(); this.terminalStreams.handleFrame(frame); let frameKind: "output" | "snapshot" | "other" = "other"; if (frame.opcode === TerminalStreamOpcode.Output) { frameKind = "output"; } else if (frame.opcode === TerminalStreamOpcode.Snapshot) { frameKind = "snapshot"; } else if (frame.opcode === TerminalStreamOpcode.Restore) { frameKind = "output"; } this.runtimeMetrics?.recordBinaryFrame( frameKind, rawBytes.byteLength, perfNow() - binaryStartMs, ); return true; } private handleFileTransferFrame(frame: FileTransferFrame): void { if (frame.opcode === FileTransferOpcode.FileBegin) { const pending = this.pendingBinaryFileReads.get(frame.requestId); if (!pending) { return; } this.activeBinaryFileTransfers.set(frame.requestId, { ...pending, mime: frame.metadata.mime, size: frame.metadata.size, encoding: frame.metadata.encoding, modifiedAt: frame.metadata.modifiedAt, revision: frame.metadata.revision, chunks: [], }); return; } const transfer = this.activeBinaryFileTransfers.get(frame.requestId); if (!transfer) { return; } if (frame.opcode === FileTransferOpcode.FileChunk) { transfer.chunks.push(frame.payload); return; } const bytes = concatByteChunks(transfer.chunks, transfer.size); this.activeBinaryFileTransfers.delete(frame.requestId); this.completedBinaryFileReads.set(frame.requestId, { bytes, mime: transfer.mime, size: transfer.size, path: transfer.path, kind: binaryFileKind(transfer.mime, transfer.encoding), modifiedAt: transfer.modifiedAt, revision: transfer.revision, }); this.handleSessionMessage({ type: "file_explorer_response", payload: { cwd: transfer.cwd, path: transfer.path, mode: "file", directory: null, file: null, error: null, requestId: frame.requestId, }, }); } private updateConnectionState( next: ConnectionState, metadata?: { event: string; reason?: string; reasonCode?: string }, ): void { const previous = this.connectionState; this.connectionState = next; const reasonFromNext = next.status === "disconnected" && typeof next.reason === "string" ? next.reason : null; const reason = metadata?.reason ?? reasonFromNext; const reasonCode = metadata?.reasonCode ?? toReasonCode(reason); this.logger.debug( { serverId: this.logServerId, clientIdHash: this.logClientIdHash, from: previous.status, to: next.status, event: metadata?.event ?? "STATE_UPDATE", connectionPath: this.logConnectionPath, generation: this.logGeneration, reasonCode, reason, }, "DaemonClientTransition", ); for (const listener of this.connectionListeners) { try { listener(next); } catch { // no-op } } } setReconnectEnabled(enabled: boolean): void { this.config = { ...this.config, reconnect: { ...this.config.reconnect, enabled } }; } private scheduleReconnect(input?: { reason?: string; event?: string; reasonCode?: string; }): void { if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } const wasDisposed = this.connectionState.status === "disposed"; const reason = input?.reason; if (typeof reason === "string" && reason.trim().length > 0) { this.lastErrorValue = reason.trim(); } // Clear all pending waiters and queued sends since the connection was lost // and responses from the previous connection will never arrive. this.clearWaiters(new Error(reason ?? "Connection lost")); this.rejectPendingSendQueue(new Error(reason ?? "Connection lost")); this.rejectPingProbe(new Error(reason ?? "Connection lost")); this.terminalStreams.clearSlots(); this.lastServerInfoMessage = null; if (wasDisposed) { this.rejectConnect(new Error(reason ?? "Daemon client is disposed")); return; } this.emitDisconnectedStateForReconnect(reason, input); if (!this.shouldReconnect || this.config.reconnect?.enabled === false) { this.rejectConnect(new Error(reason ?? "Transport disconnected before connect")); return; } this.armReconnectTimer(); } private emitDisconnectedStateForReconnect( reason: string | undefined, input: { reason?: string; event?: string; reasonCode?: string } | undefined, ): void { this.updateConnectionState( { status: "disconnected", ...(reason ? { reason } : {}), }, { event: input?.event ?? "TRANSPORT_CLOSE", ...(reason ? { reason } : {}), ...(input?.reasonCode ? { reasonCode: input.reasonCode } : {}), }, ); } private armReconnectTimer(): void { const attempt = this.reconnectAttempt; const baseDelay = this.config.reconnect?.baseDelayMs ?? DEFAULT_RECONNECT_BASE_DELAY_MS; const maxDelay = this.config.reconnect?.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS; const delay = Math.min(baseDelay * 2 ** attempt, maxDelay); this.reconnectAttempt = attempt + 1; this.reconnectTimeout = setTimeout(() => { this.reconnectTimeout = null; if (!this.shouldReconnect) { return; } this.attemptConnect(); }, delay); } private resolvePingProbe(): void { const probe = this.pingProbe; if (!probe) { return; } this.pingProbe = null; clearTimeout(probe.timeoutHandle); probe.resolve(perfNow() - probe.startedAt); } private clearPingProbe(): void { const probe = this.pingProbe; if (!probe) { return; } this.pingProbe = null; clearTimeout(probe.timeoutHandle); } private rejectPingProbe(error: Error): void { const probe = this.pingProbe; if (!probe) { return; } this.pingProbe = null; clearTimeout(probe.timeoutHandle); probe.reject(error); } private recordLivenessFailure(error: Error): void { this.consecutiveLivenessFailures += 1; if (this.consecutiveLivenessFailures < LIVENESS_FAILURE_RECONNECT_THRESHOLD) { return; } this.consecutiveLivenessFailures = 0; this.lastErrorValue = error.message; this.disposeTransport(1001, "Liveness check timed out"); this.scheduleReconnect({ reason: error.message, event: "LIVENESS_TIMEOUT", reasonCode: "liveness_timeout", }); } private handleSessionMessage(msg: SessionOutboundMessage): void { const consumerMessage = normalizeProviderSnapshotUpdateMessage(msg); if (consumerMessage.type === "status") { const serverInfo = parseServerInfoStatusPayload(consumerMessage.payload); if (serverInfo) { this.lastServerInfoMessage = serverInfo; if (this.connectionState.status === "connecting") { this.resetConnectTimeout(); this.reconnectAttempt = 0; this.updateConnectionState({ status: "connected" }, { event: "HELLO_SERVER_INFO" }); this.startLivenessHeartbeat(); this.resubscribeCheckoutDiffSubscriptions(); this.resubscribeTerminalDirectorySubscriptions(); this.resubscribeFileSubscriptions(); this.flushPendingSendQueue(); this.resolveConnect(); } } } if (consumerMessage.type === "terminal_stream_exit") { this.terminalStreams.removeTerminal(consumerMessage.payload.terminalId); } if (consumerMessage.type === "fs.file.update") { this.fileSubscriptions .get(consumerMessage.payload.subscriptionId) ?.onUpdate(consumerMessage.payload.version); } if (this.rawMessageListeners.size > 0) { for (const handler of this.rawMessageListeners) { try { handler(consumerMessage); } catch { // no-op } } } const handlers = this.messageHandlers.get(consumerMessage.type); if (handlers) { for (const handler of handlers) { try { handler(consumerMessage); } catch { // no-op } } } const event = this.toEvent(consumerMessage); if (event) { for (const handler of this.eventListeners) { handler(event); } } this.resolveWaiters(consumerMessage); } private resolveWaiters(msg: SessionOutboundMessage): void { for (const waiter of Array.from(this.waiters)) { const result = waiter.predicate(msg); if (result !== null) { this.waiters.delete(waiter); if (waiter.timeoutHandle) { clearTimeout(waiter.timeoutHandle); } waiter.resolve(result); } } } private rejectWaitersForRequestId(requestId: string, error: Error): void { for (const waiter of Array.from(this.waiters)) { if (waiter.requestId !== requestId) { continue; } this.waiters.delete(waiter); if (waiter.timeoutHandle) { clearTimeout(waiter.timeoutHandle); } waiter.reject(error); } } private clearWaiters(error: Error): void { for (const waiter of Array.from(this.waiters)) { if (waiter.timeoutHandle) { clearTimeout(waiter.timeoutHandle); } waiter.reject(error); } this.waiters.clear(); } private toEvent(msg: SessionOutboundMessage): DaemonEvent | null { switch (msg.type) { case "agent_update": return { type: "agent_update", agentId: msg.payload.kind === "upsert" ? msg.payload.agent.id : msg.payload.agentId, payload: msg.payload, }; case "workspace_update": return { type: "workspace_update", workspaceId: msg.payload.kind === "upsert" ? msg.payload.workspace.id : msg.payload.id, payload: msg.payload, }; case "project.update": return { type: "project.update", payload: msg.payload }; case "workspace_setup_progress": return { type: "workspace_setup_progress", workspaceId: msg.payload.workspaceId, payload: msg.payload, }; case "agent_stream": return { type: "agent_stream", agentId: msg.payload.agentId, event: msg.payload.event, timestamp: msg.payload.timestamp, ...(typeof msg.payload.seq === "number" ? { seq: msg.payload.seq } : {}), ...(typeof msg.payload.epoch === "string" ? { epoch: msg.payload.epoch } : {}), }; case "status": return { type: "status", payload: msg.payload }; case "agent_deleted": return { type: "agent_deleted", agentId: msg.payload.agentId }; case "agent_permission_request": return { type: "agent_permission_request", agentId: msg.payload.agentId, request: msg.payload.request, }; case "agent_permission_resolved": return { type: "agent_permission_resolved", agentId: msg.payload.agentId, requestId: msg.payload.requestId, resolution: msg.payload.resolution, }; case "providers_snapshot_update": return { type: "providers_snapshot_update", payload: msg.payload, }; default: return null; } } private waitForWithCancel( predicate: (msg: SessionOutboundMessage) => T | null, timeout = 30000, options?: WaitOptions, ): WaitHandle { // Capture stack trace at call site, not inside setTimeout const timeoutError = new Error(`Timeout waiting for message (${timeout}ms)`); let waiter: Waiter | null = null; let settled = false; let rejectFn: ((error: Error) => void) | null = null; const promise = new Promise((resolve, reject) => { const wrappedResolve = (value: T) => { if (settled) return; settled = true; resolve(value); }; const wrappedReject = (error: Error) => { if (settled) return; settled = true; reject(error); }; rejectFn = wrappedReject; const timeoutHandle = timeout > 0 ? setTimeout(() => { if (waiter) { this.waiters.delete(waiter); } wrappedReject(timeoutError); }, timeout) : null; waiter = { predicate, resolve: wrappedResolve, reject: wrappedReject, timeoutHandle, requestId: options?.requestId, }; this.waiters.add(waiter); }); const cancel = (error: Error) => { if (settled) { return; } if (waiter) { this.waiters.delete(waiter); if (waiter.timeoutHandle) { clearTimeout(waiter.timeoutHandle); } } if (rejectFn) { rejectFn(error); return; } // Extremely unlikely: cancel called before the Promise executor ran. queueMicrotask(() => { if (!settled && rejectFn) { rejectFn(error); } }); }; return { promise, cancel }; } } function resolveAgentConfig(options: CreateAgentRequestOptions): AgentSessionConfig { const { config, provider, cwd, env: _env, workspaceId: _workspaceId, initialPrompt: _initialPrompt, images: _images, git: _git, worktreeName: _worktreeName, requestId: _requestId, labels: _labels, ...overrides } = options; const baseConfig: Partial = { ...(provider ? { provider } : {}), ...(cwd ? { cwd } : {}), ...overrides, }; const merged = config ? { ...baseConfig, ...config } : baseConfig; if (!merged.provider || !merged.cwd) { throw new Error("createAgent requires provider and cwd"); } return { ...merged, provider: merged.provider, cwd: merged.cwd, }; }