import * as _anthropic_ai_claude_agent_sdk from '@anthropic-ai/claude-agent-sdk'; import * as z from 'zod'; import { z as z$1 } from 'zod'; import { EventEmitter } from 'node:events'; import { Socket } from 'socket.io-client'; import { ExpoPushMessage } from 'expo-server-sdk'; declare const sessionTurnEndStatusSchema: z.ZodEnum<{ completed: "completed"; failed: "failed"; cancelled: "cancelled"; }>; type SessionTurnEndStatus = z.infer; declare const sessionEnvelopeSchema: z.ZodObject<{ id: z.ZodString; time: z.ZodNumber; role: z.ZodEnum<{ user: "user"; agent: "agent"; }>; turn: z.ZodOptional; subagent: z.ZodOptional; claudeUuid: z.ZodOptional; codexItemId: z.ZodOptional; streamKey: z.ZodOptional; usage: z.ZodOptional; cache_read_input_tokens: z.ZodOptional; }, z.core.$strip>>; ev: z.ZodDiscriminatedUnion<[z.ZodObject<{ t: z.ZodLiteral<"text">; text: z.ZodString; thinking: z.ZodOptional; actualModel: z.ZodOptional; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"service">; text: z.ZodString; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"tool-call-start">; call: z.ZodString; name: z.ZodString; title: z.ZodString; description: z.ZodString; args: z.ZodRecord; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"tool-call-end">; call: z.ZodString; result: z.ZodOptional; isError: z.ZodOptional; stats: z.ZodOptional; totalTokens: z.ZodOptional; durationMs: z.ZodOptional; toolStats: z.ZodOptional>; }, z.core.$strip>>; }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"file">; ref: z.ZodString; name: z.ZodString; size: z.ZodNumber; mimeType: z.ZodOptional; image: z.ZodOptional>; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"turn-start">; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"start">; title: z.ZodOptional; description: z.ZodOptional; subagentType: z.ZodOptional; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"turn-end">; status: z.ZodEnum<{ completed: "completed"; failed: "failed"; cancelled: "cancelled"; }>; error: z.ZodOptional; costUsd: z.ZodOptional; durationMs: z.ZodOptional; numTurns: z.ZodOptional; usage: z.ZodOptional; cache_read_input_tokens: z.ZodOptional; }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"stop">; status: z.ZodOptional>; result: z.ZodOptional; }, z.core.$strip>>; usage: z.ZodOptional; totalTokens: z.ZodOptional; durationMs: z.ZodOptional; }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"progress">; toolUses: z.ZodNumber; lastTool: z.ZodOptional; totalTokens: z.ZodOptional; durationMs: z.ZodOptional; summary: z.ZodOptional; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"queue-cancel">; targetLocalKeys: z.ZodArray; reason: z.ZodOptional; }, z.core.$strip>], "t">; }, z.core.$strip>; type SessionEnvelope = z.infer; declare const sessionStreamFrameSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ t: z.ZodLiteral<"block-start">; mid: z.ZodString; idx: z.ZodNumber; kind: z.ZodEnum<{ text: "text"; thinking: "thinking"; }>; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"block-delta">; mid: z.ZodString; idx: z.ZodNumber; text: z.ZodString; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"block-end">; mid: z.ZodString; idx: z.ZodNumber; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"progress">; inputTokens: z.ZodOptional; cacheTokens: z.ZodOptional; thinkingTokens: z.ZodOptional; outputTokens: z.ZodOptional; status: z.ZodOptional>; }, z.core.$strip>, z.ZodObject<{ t: z.ZodLiteral<"turn-end">; }, z.core.$strip>], "t">; type SessionStreamFrame = z.infer; /** Runtime context occupancy, never cumulative billing. null after compaction is unknown. */ declare const ContextUsageSchema: z$1.ZodObject<{ source: z$1.ZodLiteral<"pi">; tokens: z$1.ZodNullable; contextWindow: z$1.ZodNumber; updatedAt: z$1.ZodNumber; }, z$1.core.$strip>; type ContextUsage = z$1.infer; declare const CLAUDE_AUTH_PROBE_VERSION: 1; type ClaudeAuthStatus = 'ok' | 'not-logged-in' | 'unknown' | 'error' | 'claude-missing'; type ClaudeAuthDiagnosis = 'keychain-empty-item' | 'store-divergence' | 'no-credentials' | 'credentials-rejected' | 'sdk-binary-missing' | 'probe-timeout' | 'probe-crash'; type ClaudeAuthLineage = 'launchd' | 'inherited-env' | 'other'; type ClaudeCredentialStore = 'auto' | 'file'; interface ClaudeAuthState$1 { probeVersion: typeof CLAUDE_AUTH_PROBE_VERSION; daemonPid: number; status: ClaudeAuthStatus; authMethod?: string; subscriptionType?: string; diagnosis?: ClaudeAuthDiagnosis; detail?: string; repairable?: 'delete-empty-keychain-item'; context: { platform: string; lineage: ClaudeAuthLineage; credentialStore: ClaudeCredentialStore; }; checkedAt: number; } /** * Simplified schema that only validates fields actually used in the codebase * while preserving all other fields through passthrough() */ declare const UsageSchema: z$1.ZodObject<{ input_tokens: z$1.ZodNumber; cache_creation_input_tokens: z$1.ZodOptional; cache_read_input_tokens: z$1.ZodOptional; output_tokens: z$1.ZodNumber; service_tier: z$1.ZodOptional; }, z$1.core.$loose>; declare const RawJSONLinesSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ type: z$1.ZodLiteral<"user">; isSidechain: z$1.ZodOptional; isMeta: z$1.ZodOptional; uuid: z$1.ZodString; message: z$1.ZodObject<{ content: z$1.ZodUnion; }, z$1.core.$loose>; }, z$1.core.$loose>, z$1.ZodObject<{ uuid: z$1.ZodString; type: z$1.ZodLiteral<"assistant">; message: z$1.ZodOptional; cache_read_input_tokens: z$1.ZodOptional; output_tokens: z$1.ZodNumber; service_tier: z$1.ZodOptional; }, z$1.core.$loose>>; model: z$1.ZodOptional; }, z$1.core.$loose>>; }, z$1.core.$loose>, z$1.ZodObject<{ type: z$1.ZodLiteral<"summary">; summary: z$1.ZodString; leafUuid: z$1.ZodString; }, z$1.core.$loose>, z$1.ZodObject<{ type: z$1.ZodLiteral<"system">; uuid: z$1.ZodString; }, z$1.core.$loose>, z$1.ZodObject<{ type: z$1.ZodLiteral<"result">; subtype: z$1.ZodOptional; is_error: z$1.ZodOptional; result: z$1.ZodOptional; total_cost_usd: z$1.ZodOptional; duration_ms: z$1.ZodOptional; num_turns: z$1.ZodOptional; usage: z$1.ZodOptional; cache_read_input_tokens: z$1.ZodOptional; output_tokens: z$1.ZodNumber; service_tier: z$1.ZodOptional; }, z$1.core.$loose>>; }, z$1.core.$loose>], "type">; type RawJSONLines = z$1.infer; /** * Minimal persistence functions for happy CLI * * Handles settings and private key storage in ~/.happy/ or local .happy/ */ declare const SandboxConfigSchema: z.ZodObject<{ enabled: z.ZodDefault; workspaceRoot: z.ZodOptional; sessionIsolation: z.ZodDefault>; customWritePaths: z.ZodDefault>; denyReadPaths: z.ZodDefault>; extraWritePaths: z.ZodDefault>; denyWritePaths: z.ZodDefault>; networkMode: z.ZodDefault>; allowedDomains: z.ZodDefault>; deniedDomains: z.ZodDefault>; allowLocalBinding: z.ZodDefault; }, z.core.$strip>; type SandboxConfig = z.infer; type Credentials = { token: string; /** Relay that issued this token. Absent only on credentials written by older CLIs. */ authServerUrl?: string; encryption: { type: 'legacy'; secret: Uint8Array; } | { type: 'dataKey'; publicKey: Uint8Array; machineKey: Uint8Array; }; }; /** * Permission mode type - includes both Claude and Codex modes * Must match MessageMetaSchema.permissionMode enum values * * Claude modes: default, acceptEdits, bypassPermissions, plan * Codex modes: read-only, safe-yolo, yolo * * When calling Claude SDK, Codex modes are mapped at the SDK boundary: * - yolo → bypassPermissions * - safe-yolo → default * - read-only → default */ type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'read-only' | 'safe-yolo' | 'yolo'; /** * Usage data type from Claude */ type Usage = z$1.infer; /** * Session information */ type Session = { id: string; seq: number; encryptionKey: Uint8Array; encryptionVariant: 'legacy' | 'dataKey'; metadata: Metadata; metadataVersion: number; agentState: AgentState$1 | null; agentStateVersion: number; }; /** * Machine metadata - static information (rarely changes) */ declare const MachineMetadataSchema: z$1.ZodObject<{ teamsVersion: z$1.ZodOptional; teamLaunchVersion: z$1.ZodOptional; host: z$1.ZodString; platform: z$1.ZodString; happyCliVersion: z$1.ZodString; homeDir: z$1.ZodString; happyHomeDir: z$1.ZodString; happyLibDir: z$1.ZodString; cliAvailability: z$1.ZodOptional; detectedAt: z$1.ZodNumber; }, z$1.core.$strip>>; resumeSupport: z$1.ZodOptional>; }, z$1.core.$strip>; type MachineMetadata = z$1.infer; declare const CliUpdateStateSchema: z$1.ZodObject<{ currentVersion: z$1.ZodString; recommendedVersion: z$1.ZodNullable; minimumVersion: z$1.ZodNullable; status: z$1.ZodEnum<{ current: "current"; available: "available"; required: "required"; }>; autoUpdateVersion: z$1.ZodOptional>; retrySupported: z$1.ZodOptional; manualUpdateSupported: z$1.ZodOptional; checkedAt: z$1.ZodNumber; handoverHold: z$1.ZodOptional>>; autoUpdate: z$1.ZodOptional; state: z$1.ZodUnion, z$1.ZodString]>; version: z$1.ZodNullable; detail: z$1.ZodOptional; at: z$1.ZodNumber; }, z$1.core.$strip>>>; }, z$1.core.$strip>; type CliUpdateState = z$1.infer; /** * Daemon state - dynamic runtime information (frequently updated) */ declare const DaemonStateSchema: z$1.ZodObject<{ agentVersions: z$1.ZodOptional; latest: z$1.ZodNullable; status: z$1.ZodString; }, z$1.core.$strip>>; }, z$1.core.$strip>>; agentVersionEpoch: z$1.ZodOptional; status: z$1.ZodUnion, z$1.ZodString]>; pid: z$1.ZodOptional; httpPort: z$1.ZodOptional; startedAt: z$1.ZodOptional; shutdownRequestedAt: z$1.ZodOptional; shutdownSource: z$1.ZodOptional, z$1.ZodString]>>; webTerminals: z$1.ZodOptional; tags: z$1.ZodOptional>; cwd: z$1.ZodOptional; createdAt: z$1.ZodOptional; activityAt: z$1.ZodOptional; agentState: z$1.ZodOptional>; agentKind: z$1.ZodOptional; agentObservedAt: z$1.ZodOptional; mirrorSessionId: z$1.ZodOptional; restoredAt: z$1.ZodOptional; manual: z$1.ZodOptional; attachTmux: z$1.ZodOptional; }, z$1.core.$strip>>; }, z$1.core.$strip>>; closedTerminals: z$1.ZodOptional; cwd: z$1.ZodOptional; mirrorSessionId: z$1.ZodOptional; claudeSessionId: z$1.ZodOptional; reason: z$1.ZodOptional>; tags: z$1.ZodOptional>; manual: z$1.ZodOptional; attachTmux: z$1.ZodOptional; closedAt: z$1.ZodNumber; }, z$1.core.$strip>>>; terminalRestore: z$1.ZodOptional>; tmuxSessions: z$1.ZodOptional; }, z$1.core.$strip>>; claudeHistory: z$1.ZodOptional>; codexHistory: z$1.ZodOptional>; cliUpdate: z$1.ZodOptional; minimumVersion: z$1.ZodNullable; status: z$1.ZodEnum<{ current: "current"; available: "available"; required: "required"; }>; autoUpdateVersion: z$1.ZodOptional>; retrySupported: z$1.ZodOptional; manualUpdateSupported: z$1.ZodOptional; checkedAt: z$1.ZodNumber; handoverHold: z$1.ZodOptional>>; autoUpdate: z$1.ZodOptional; state: z$1.ZodUnion, z$1.ZodString]>; version: z$1.ZodNullable; detail: z$1.ZodOptional; at: z$1.ZodNumber; }, z$1.core.$strip>>>; }, z$1.core.$strip>>; claudeAuth: z$1.ZodOptional, z$1.ZodString]>; authMethod: z$1.ZodOptional; subscriptionType: z$1.ZodOptional; diagnosis: z$1.ZodOptional; detail: z$1.ZodOptional; repairable: z$1.ZodOptional; context: z$1.ZodObject<{ platform: z$1.ZodString; lineage: z$1.ZodString; credentialStore: z$1.ZodString; }, z$1.core.$strip>; checkedAt: z$1.ZodNumber; }, z$1.core.$strip>>; }, z$1.core.$strip>; type DaemonState = z$1.infer; type ClaudeAuthState = ClaudeAuthState$1; type Machine = { id: string; encryptionKey: Uint8Array; encryptionVariant: 'legacy' | 'dataKey'; metadata: MachineMetadata; metadataVersion: number; daemonState: DaemonState | null; daemonStateVersion: number; }; declare const UserMessageSchema: z$1.ZodObject<{ role: z$1.ZodLiteral<"user">; content: z$1.ZodObject<{ type: z$1.ZodLiteral<"text">; text: z$1.ZodString; }, z$1.core.$strip>; localKey: z$1.ZodOptional; meta: z$1.ZodOptional; permissionMode: z$1.ZodOptional>; model: z$1.ZodOptional>; fallbackModel: z$1.ZodOptional>; customSystemPrompt: z$1.ZodOptional>; appendSystemPrompt: z$1.ZodOptional>; allowedTools: z$1.ZodOptional>>; disallowedTools: z$1.ZodOptional>>; effort: z$1.ZodOptional>; delivery: z$1.ZodOptional>; }, z$1.core.$strip>>; }, z$1.core.$strip>; type UserMessage = z$1.infer; /** * File event message — sent by the app as a session envelope before the text message. * Contains a ref pointing to the encrypted blob on the server. */ declare const FileEventMessageSchema: z$1.ZodObject<{ role: z$1.ZodLiteral<"session">; content: z$1.ZodObject<{ type: z$1.ZodLiteral<"session">; data: z$1.ZodObject<{ id: z$1.ZodString; time: z$1.ZodNumber; role: z$1.ZodLiteral<"user">; ev: z$1.ZodObject<{ t: z$1.ZodLiteral<"file">; ref: z$1.ZodString; name: z$1.ZodString; size: z$1.ZodNumber; mimeType: z$1.ZodOptional; image: z$1.ZodOptional; }, z$1.core.$strip>>; }, z$1.core.$strip>; }, z$1.core.$strip>; }, z$1.core.$strip>; }, z$1.core.$strip>; type FileEventMessage = z$1.infer; type Metadata = { /** Durable Teams spawn correlation; never contains credentials. */ teamOperationId?: string; /** * ACP session config option value (normalized for UI metadata consumers). */ models?: Array<{ code: string; value: string; description?: string | null; resolvedModel?: string; reasoningEfforts?: string[]; defaultReasoningEffort?: string; }>; currentModelCode?: string; /** Resolved SDK model when no per-message model override was supplied. */ defaultModelCode?: string; operatingModes?: Array<{ code: string; value: string; description?: string | null; }>; currentOperatingModeCode?: string; thoughtLevels?: Array<{ code: string; value: string; description?: string | null; }>; currentThoughtLevelCode?: string; path: string; host: string; version?: string; name?: string; os?: string; summary?: { text: string; updatedAt: number; }; machineId?: string; claudeSessionId?: string; /** * Terminal mirror (B-105): the vh web terminal this shadow session mirrors * (flavor === 'terminal-mirror'). Lets the web link mirror ↔ terminal both * ways. Absent on every other session; old clients ignore it. */ terminalId?: string; codexThreadId?: string; tools?: string[]; slashCommands?: string[]; mcpServers?: Array<{ name: string; status: string; }>; skills?: string[]; /** Attachment content blocks this daemon can forward to the active SDK. */ attachmentKinds?: string[]; /** Pending user messages can be canceled by stable transport id. */ queueCancellation?: boolean; /** Explicit feature negotiation; old CLIs omit this field. */ capabilities?: string[]; homeDir: string; happyHomeDir: string; happyLibDir: string; happyToolsDir: string; startedFromDaemon?: boolean; hostPid?: number; startedBy?: 'daemon' | 'terminal'; lifecycleState?: 'running' | 'archiveRequested' | 'archived' | string; lifecycleStateSince?: number; archivedBy?: string; archiveReason?: string; flavor?: string; sandbox?: SandboxConfig | null; dangerouslySkipPermissions?: boolean | null; /** * Effective permission mode the running Claude process enforces right now * (SDK vocabulary: default | acceptEdits | bypassPermissions | plan). The * CLI is the single source of truth: it writes this at start, on every * message/RPC/plan-approval change. Web displays it verbatim; old clients * ignore it and old CLIs never write it (capability claude-live-permission-v2). */ permissionMode?: string; /** Lineage for sessions created via the fork / duplicate flow. */ parentSessionId?: string; forkedFromMessageId?: string; /** * B-290: the Claude conversation this session was imported from (the * on-disk transcript written by claude CLI / desktop / claude.ai that was * copied by `claude-fork-session`). Lets the import picker hide originals * that already have a copy here. Absent on every other session. */ importedFromClaudeSessionId?: string; /** * B-464: the Codex thread this session was imported from (the rollout * written by the codex TUI / `codex exec` / Codex desktop that the wrapper * forked at start). `codexThreadId` is the fork. Lets the import picker hide * originals that already have a copy here. Absent on every other session. */ importedFromCodexThreadId?: string; /** * B-051: session variant. 'assistant' marks the machine's meta-agent * (dispatcher / voice assistant) session — fixed cwd ~/.happy/assistant, * singleton tag, assistant MCP tool surface. Absent on normal sessions; * old clients ignore the field (plain TS metadata, no zod). */ variant?: 'assistant' | string; /** * User-visible session tags (web renders them as chips; `#tag` search). * Optional only — never write an empty array. B-091: sessions dispatched BY * the assistant (HAPPY_SPAWNED_BY=assistant) are born with ['assistant'] so * they're recognizable in every list; old web clients just render one more * chip (harmless). */ tags?: string[]; /** * Task Board V2: latest LLM analysis of this session (boardAnalyzer). * Rides the normal metadata sync to every device; absent until the * daemon-local `boardLlm` opt-in produces a first verdict. */ board?: { /** board task (KV vh.board-tasks.v1) this session was classified under */ taskId?: string; attention?: 'none' | 'review' | 'blocked'; /** one-line Chinese progress note */ progress?: string; analyzedAt: number; }; }; type AgentState$1 = { contextUsage?: ContextUsage | null; controlledByUser?: boolean | null | undefined; requests?: { [id: string]: { tool: string; arguments: any; createdAt: number; kind?: 'tool' | 'elicitation' | 'user_dialog'; permissionSuggestions?: _anthropic_ai_claude_agent_sdk.PermissionUpdate[]; }; }; completedRequests?: { [id: string]: { tool: string; arguments: any; createdAt: number; completedAt: number; status: 'canceled' | 'denied' | 'approved'; reason?: string; mode?: PermissionMode; decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'; allowedTools?: string[]; }; }; }; /** * Common RPC types and interfaces for both session and machine clients */ /** * Generic RPC handler function type * @template TRequest - The request data type * @template TResponse - The response data type */ type RpcHandler = (data: TRequest) => TResponse | Promise; /** * RPC request data from server */ interface RpcRequest { method: string; params: string; } /** * Configuration for RPC handler manager */ interface RpcHandlerConfig { scopePrefix: string; encryptionKey: Uint8Array; encryptionVariant: 'legacy' | 'dataKey'; logger?: (message: string, data?: any) => void; } /** * Generic RPC handler manager for session and machine clients * Manages RPC method registration, encryption/decryption, and handler execution */ declare class RpcHandlerManager { private handlers; private readonly scopePrefix; private readonly encryptionKey; private readonly encryptionVariant; private readonly logger; private sockets; constructor(config: RpcHandlerConfig); /** * Register an RPC handler for a specific method * @param method - The method name (without prefix) * @param handler - The handler function */ registerHandler(method: string, handler: RpcHandler): void; unregisterHandler(method: string): void; /** * Handle an incoming RPC request * @param request - The RPC request data * @param callback - The response callback */ handleRequest(request: RpcRequest): Promise; onSocketConnect(socket: Socket): void; /** * Attach a candidate transport and wait until the server confirms every * method before a release handover closes the old transport. */ onSocketConnectAndWait(socket: Socket, timeoutMs?: number): Promise; onSocketDisconnect(socket?: Socket): void; /** * Get the number of registered handlers */ getHandlerCount(): number; /** * Check if a handler is registered * @param method - The method name (without prefix) */ hasHandler(method: string): boolean; /** * Clear all handlers */ clearHandlers(): void; /** * Get the prefixed method name * @param method - The method name */ private getPrefixedMethod; } /** * ACP (Agent Communication Protocol) message data types. * This is the unified format for all agent messages - CLI adapts each provider's format to ACP. */ type ACPMessageData = { type: 'message'; message: string; } | { type: 'reasoning'; message: string; } | { type: 'thinking'; text: string; } | { type: 'tool-call'; callId: string; name: string; input: unknown; id: string; } | { type: 'tool-result'; callId: string; output: unknown; id: string; isError?: boolean; } | { type: 'file-edit'; description: string; filePath: string; diff?: string; oldContent?: string; newContent?: string; id: string; } | { type: 'terminal-output'; data: string; callId: string; } | { type: 'task_started'; id: string; } | { type: 'task_complete'; id: string; } | { type: 'turn_aborted'; id: string; } | { type: 'permission-request'; permissionId: string; toolName: string; description: string; options?: unknown; } | { type: 'token_count'; [key: string]: unknown; }; declare class ApiSessionClient extends EventEmitter { private readonly token; readonly sessionId: string; private metadata; private metadataVersion; private agentState; private agentStateVersion; private socket; private relaySocket; private pendingMessages; private pendingMessageCallback; private pendingFileEvents; private pendingFileEventCallback; private blobKey; /** * In-flight attachment download promises that belong to the *current* * (not-yet-drained) batch. Each promise resolves to the decoded blob (or * null on failure), so per-message ownership is intrinsic — there is no * shared push-array between batches that a late download could leak into. */ private pendingDownloads; readonly rpcHandlerManager: RpcHandlerManager; private agentStateLock; private metadataLock; private encryptionKey; private encryptionVariant; private reconnectInterval; private ignoreArchiveSignal; private skipInitialMessages; private claudeSessionProtocolState; private readonly seenClaudeUsageEvents; private claudeUsageTotals; private lastSeq; private pendingOutbox; private readonly sendSync; private readonly receiveSync; private readonly directInboundLocalIds; private readonly routedInboundLocalIds; private handoverInFlight; /** B-265 reconnect: the message cursor was seeded from the server, so * nothing before it is history to replay — but socket fast-path routing * must wait for the first fetch, otherwise a message that lands between * the seed and the first fetch is routed twice (socket + fetch). */ private awaitingInitialFetch; constructor(token: string, session: Session, opts?: { initialSeq?: number; }); private createControlSocket; private startReleaseHandover; private releaseHandover; onUserMessage(callback: (data: UserMessage) => void): void; onFileEvent(callback: (data: FileEventMessage) => void): void; /** * Derive (and cache) the blob decryption key for this session. * Legacy sessions use deriveKey(masterSecret, 'Happy Blobs', ['master']). * DataKey sessions use deriveKey(dataKey, 'Happy Blobs', ['session']). */ getBlobKey(): Promise; /** * Download an encrypted attachment blob via the request-download flow: * POST /request-download → { downloadUrl } → GET downloadUrl. Local mode * downloadUrl points back at our server (Bearer required); S3 mode is a * presigned URL that does not accept extra headers. */ downloadAttachment(ref: string): Promise; /** * Download and decrypt an attachment blob. * Returns the decrypted binary data or null if decryption fails. */ downloadAndDecryptAttachment(ref: string): Promise; /** * Track an attachment download whose promise resolves to the decoded blob * (or null on failure). The download stays in the current batch until the * next drainAttachmentsForUserMessage call swaps the bucket out — file * events that arrive after the swap go into a fresh bucket bound to the * next user-text message. */ trackAttachmentDownload(promise: Promise<{ data: Uint8Array; mimeType: string; name: string; } | null>): void; /** * Atomically claim every download started before this call, wait for them * to resolve, and return the successful ones. The swap-then-await order * guarantees that a late-arriving file event cannot leak into this batch. */ drainAttachmentsForUserMessage(): Promise>; private authHeaders; private routeIncomingMessage; private fetchMessages; private static readonly MAX_OUTBOX_BATCH_SIZE; private flushOutbox; private rememberRoutedInbound; private rememberDirectInbound; private connectSessionRelay; private pushCommittedMessagesToRelay; private enqueueMessage; /** B-332: emit a queue-cancel tombstone, mirroring the web's own signal. */ sendQueueCancelReason(localKeys: string[], reason: 'cleared' | 'aborted' | 'restarted'): void; /** * B-332 site ③: find queued user input this wrapper destroyed (it was still * in the previous wrapper's in-memory queue when it died) and tell the web. * * Walks NEWEST → OLDEST via `before_seq`, decrypting each page, and stops at * the first turn-end. The runner calls this on EVERY reconnect (runClaude / * runCodex), seeded or not: a cursor seeded at the server's latest seq skips * the undelivered tail just as thoroughly as skipExistingMessages does — * and the daemon's resume/restart always seeds, so gating on the skip path * would miss the common case. Best-effort: a failed scan must not take a * session down. Needs no socket; POSTs through the same session-message * channel so the web receives it identically to a cancel-button tombstone. */ cancelUndeliveredQueuedInputs(): Promise; /** * Send a message to session * @param body - Message body (can be MessageContent or raw content for agent messages) */ sendClaudeSessionMessage(body: RawJSONLines, opts?: { /** Terminal mirror (B-105): deterministic per-envelope localId. The * index argument is REQUIRED in the id — one transcript line maps to * 0..N envelopes and a per-line-only id would make the server's * unique constraint swallow every envelope after the first. */ localIdFor?: (envelopeIndex: number) => string; }): void; closeClaudeSessionTurn(status?: SessionTurnEndStatus, meta?: { error?: string; }): void; sendCodexMessage(body: any): void; private enqueueSessionProtocolEnvelope; sendSessionProtocolMessage(envelope: SessionEnvelope, localId?: string): void; /** * Send a generic agent message to the session using ACP (Agent Communication Protocol) format. * Works for any agent type (Gemini, Codex, Claude, etc.) - CLI normalizes to unified ACP format. * * @param provider - The agent provider sending the message (e.g., 'gemini', 'codex', 'claude') * @param body - The message payload (type: 'message' | 'reasoning' | 'tool-call' | 'tool-result') */ sendAgentMessage(provider: 'gemini' | 'codex' | 'claude' | 'opencode' | 'openclaw', body: ACPMessageData): void; sendSessionEvent(event: { type: 'switch'; mode: 'local' | 'remote'; } | { type: 'message'; message: string; kind?: string; } | { type: 'permission-mode-changed'; mode: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'; } | { type: 'ready'; }, id?: string): void; /** * Send a ping message to keep the connection alive */ /** B-466: turn edges (and a periodic lease) reported to the local daemon so * auto-update waits for "no turn in flight" instead of "no sessions". */ private readonly turnReporter; keepAlive(thinking: boolean, mode: 'local' | 'remote'): void; /** * B-309: push one live stream frame (thinking/text delta or progress) to * this account's web clients. * * Encrypted with the SESSION key and relayed without being stored — * clipboard-push's shape, for clipboard-push's reasons: the relay must not * be able to read thinking text, and the session id is stamped by the * server from the authenticated connection rather than trusted from here. * * `volatile` on purpose. A draft that misses the wire while disconnected * is worth nothing — the persisted message is still coming, and replaying * stale deltas after a reconnect would paint text that has already landed. */ sendStreamFrame(frame: SessionStreamFrame): void; /** * Send session death message */ sendSessionDeath(): void; /** * Push text to the clipboard of every web client the user has open. * Encrypted with the SESSION key (same primitive the message stream uses, * proven to interop with the web's SessionEncryption.decryptRaw); the * server relays it to the user's web clients without reading it. * Returns delivery info for the MCP tool to report back to the model. */ pushClipboard(text: string): { delivered: boolean; truncated: boolean; totalBytes: number; }; /** * B-131: ask every web client the user has open to preview a file. * * Only the PATH travels — encrypted with the session key exactly like * clipboard-push. The web client then reads the file over the existing * machine-level fs-read RPC. Two reasons: it adds no new file access * (fs-read is already exposed) and it keeps the relay payload tiny, so * large files / images / PDFs ride the existing chunked read instead of * the 1MB relay cap. * * The path is encrypted rather than sent in the clear because every other * payload on this channel is (clipboard, fs RPC) — and a path leaks * project/client names and directory structure. */ pushFilePreview(path: string, mode?: 'file' | 'diff'): { delivered: boolean; error?: string; }; /** * Send usage data to the server */ sendUsageData(usage: Usage, model?: string, eventId?: string): void; /** Save a provider's cumulative session snapshot under one upsert key. */ sendAgentUsageSnapshot(agent: string, rawUsage: unknown): boolean; /** * Returns the latest session metadata known to the client. */ getMetadata(): Metadata | null; /** * Returns the latest agent state known to the client (requests, * controlledByUser, etc.). Used by the notification producer to classify * turn-end events. */ getAgentState(): AgentState$1 | null; /** * Update session metadata * @param handler - Handler function that returns the updated metadata */ suppressNextArchiveSignal(): void; skipExistingMessages(): void; updateMetadata(handler: (metadata: Metadata) => Metadata): void; /** * Update session agent state * @param handler - Handler function that returns the updated agent state */ updateAgentState(handler: (metadata: AgentState$1) => AgentState$1): void; /** * Wait for socket buffer to flush */ flush(): Promise; close(): Promise; private startSmartReconnect; } /** * The backends the daemon can spawn as a remote session. * * One list, four consumers: the `spawn --agent` CLI validator, the daemon's * `/spawn-session` zod enum, the machine RPC `SpawnSessionOptions` type and * the daemon's argv switch in `daemon/run.ts`. Before this module each site * carried its own copy and adding a backend meant finding all of them (B-306 * found the CLI one missing). Import from here; do not re-list. */ declare const SPAWN_AGENTS: readonly ["claude", "codex", "gemini", "openclaw", "pi"]; type SpawnAgent = (typeof SPAWN_AGENTS)[number]; interface SpawnSessionOptions { /** Optional initial model; absent preserves the runner default. */ model?: string; machineId?: string; directory: string; sessionId?: string; approvedNewDirectoryCreation?: boolean; agent?: SpawnAgent; environmentVariables?: Record; token?: string; /** * If set, the daemon spawns the agent with `--resume ` so the new * Happy session continues from an existing Claude conversation file. * Used by the session fork / duplicate flow: the fork RPC produces a * new Claude JSONL on disk, the spawn RPC then attaches a fresh Happy * session to it. */ resumeClaudeSessionId?: string; /** * If set, the daemon spawns Codex with `--resume ` so a fresh Happy * session attaches to a forked Codex app-server thread. */ resumeCodexThreadId?: string; /** Happy session id this fork was branched from (lineage). */ parentSessionId?: string; /** Happy message id used as the rewind point (only set for "duplicate"). */ forkedFromMessageId?: string; /** * B-290: source Claude conversation id of an imported transcript (the * original that `resumeClaudeSessionId` is a copy of). Recorded in session * metadata so the import picker hides it. Old daemons ignore the field. */ importedFromClaudeSessionId?: string; /** * B-464: Codex thread (written by the codex TUI / exec / desktop app) to * import: the spawned wrapper forks it through the app-server, continues * on the fork and records the original as `importedFromCodexThreadId`. * Mutually exclusive with `resumeCodexThreadId`. Old daemons ignore it. */ importCodexThreadId?: string; /** * B-294: title to stamp on the imported session's metadata (the source * conversation's summary / first prompt). Sanitized daemon-side. */ importTitle?: string; /** * B-051: spawn the machine's assistant (meta-agent) session. The daemon * forces cwd to ~/.happy/assistant (the passed `directory` is ignored), * bootstraps that home on first use, injects HAPPY_SESSION_VARIANT=assistant * into the spawned CLI, and enforces a per-machine singleton. */ variant?: 'assistant'; /** * B-051: assistant only — stop any live assistant process, purge its * persisted sessions.json entry, and spawn a brand-new assistant session * instead of returning/re-attaching to the existing one. Old daemons * simply ignore the field (compatible). */ forceNew?: boolean; /** * Permission mode forwarded to the spawned CLI as `--permission-mode ` * (allowlist-validated daemon-side; invalid values are ignored and logged). * Used by the assistant "skip permission approvals" setting: off → the web * sends 'default' so tool use requires approval instead of the fork's yolo * default. Absent → behavior unchanged. Old daemons ignore the field. */ permissionMode?: string; /** * B-069: spawn-origin tag ('assistant' = the assistant's session_spawn * tool requested this spawn). Recorded on the daemon's TrackedSession and * exported to the spawned CLI as HAPPY_SPAWNED_BY; drives the * daemon → assistant 主动汇报 sink. Old clients never send it (compatible). */ spawnedBy?: string; } type SpawnSessionResult = { type: 'success'; sessionId: string; } | { type: 'requestToApproveDirectoryCreation'; directory: string; } | { type: 'error'; errorMessage: string; }; /** Agent-specific visible-screen adapters. Unknown is deliberately not idle. */ type CodingAgentKind = 'claude' | 'codex' | 'pi'; /** * Coarse state of the agent (Claude Code) inside a web terminal, surfaced in * the sidebar. Optional everywhere: probing is best-effort and the field is * simply omitted when detection fails or times out. * - working: Claude Code is actively running a turn. * - needs_input: a permission / choice / plan-approval dialog is waiting. * - idle: Claude Code is up but sitting at its input box. * - shell: plain shell, no agent TUI detected. */ type AgentState = 'working' | 'needs_input' | 'idle' | 'shell'; /** One terminal in the cross-device list — the shape `list-terminals` returns * AND the shape pushed through daemonState.webTerminals (identical on * purpose: poll and push describe the same thing). */ interface TerminalListItem { id: string; title?: string; /** Always present in new daemon output; optional for old persisted state. */ tags?: string[]; cwd?: string; createdAt?: number; activityAt?: number; agentState?: AgentState; agentKind?: CodingAgentKind; agentObservedAt?: number; /** Mirror reconcile (design v3): stricter "claude is really here" gate (its * own TUI footer/dialog present) — daemon-internal, NOT pushed to web and * NOT part of the list signature. Only this may trigger mirror adopt (which * re-opens the B-107 input gate); `agentState` alone would false-positive * on a bare `node` process. */ claudeConfident?: boolean; /** Terminal mirror (B-105): shadow session id of the hand-typed claude * running inside this terminal (set via the daemon's mirror resolver). * The web shows the xterm ↔ structured toggle when present. */ mirrorSessionId?: string; /** B-150: this terminal was AUTO-RESTORED after a restart (ms epoch) — the * tmux session and the processes inside it are new, only the directory and * the claude conversation carried over. The web badges it until the user * opens it once; the mark is daemon-local and never persisted. */ restoredAt?: number; /** B-265: `@vh_title_manual` is set — carried into close records so a * restore can put the flag back (old webs ignore it). */ manual?: boolean; /** B-273: this terminal was opened to attach the user's tmux session of * this NAME (`@vh_attach`). Carried into close records so a manual * restore re-attaches; old webs ignore it. */ attachTmux?: string; /** B-287: the pane's real geometry from the same list-sessions read. * Daemon-internal (not in the signature): it feeds the persisted snapshot * and close records so a cold restore recreates the session at the size * it last had. */ paneCols?: number; paneRows?: number; } interface CLIAvailability { claude: boolean; codex: boolean; gemini: boolean; openclaw: boolean; /** * pi is spawnable only through the pi-acp adapter (`very-happy pi`), so this * is "both `pi` and `pi-acp` resolve on PATH". Older daemons never send the * field; the Web launcher treats absence as unavailable (unlike the other * agents), because such a daemon cannot spawn pi at all. */ pi: boolean; detectedAt: number; } /** * WebSocket client for machine/daemon communication with Happy server * Similar to ApiSessionClient but for machine-scoped connections */ type MachineRpcHandlers = { spawnSession: (options: SpawnSessionOptions) => Promise; resumeSession?: (sessionId: string, options?: { model?: string; permissionMode?: string; }) => Promise; /** B-264: stop a live-but-broken wrapper and relaunch it on the current CLI. */ restartSession?: (sessionId: string, options?: { model?: string; permissionMode?: string; }) => Promise; stopSession: (sessionId: string) => boolean; listTrackedSessionIds?: () => string[]; requestShutdown: () => void; }; declare class ApiMachineClient { private token; private machine; private socket; private relaySocket; private relayAssignment; private relaySwitchTracker; private relayRefreshInFlight; private keepAliveInterval; private lastKnownCLIAvailability; private lastKnownResumeSupport; /** Wall-clock of the last CLI availability probe. Initialised as "already * stale" so the FIRST keep-alive tick always probes and populates the * machine metadata that a connecting/reconnecting web client reads. */ private lastCliProbeAt; /** Stable across socket reconnects; a new client/run invalidates old version checks. */ private readonly agentVersionEpoch; private cliUpdateState; private cliUpdatePushChain; private claudeAuthState; private rpcHandlerManager; private resumeSessionHandler; private restartSessionHandler; private stopSessionHandler; private listTrackedSessionIds; private reconnectInterval; private handoverInFlight; private encTerminals; /** B-327: an auto-update swaps the process that owns these terminals, so it * waits until there are none. */ hasLiveTerminals(): boolean; private webTerminal; /** * The CLI availability last advertised in machine metadata (re-probed by * the keep-alive). Null until the first keep-alive tick; the daemon falls * back to its startup probe then. */ getCLIAvailability(): CLIAvailability | null; /** * Title a web terminal from inside it (`very-happy mcp` change_title → * daemon /terminal-title → here). Same tmux truthfulness as the * `set-terminal-title` RPC. */ setTerminalTitle(terminalId: string, title: string, ifAbsent: boolean): boolean; /** * Push text to the clipboard of every web client the user has open * (terminal-path claude → `very-happy mcp` → daemon /clipboard → here). * Encrypted with the per-machine key; the server relays without reading. */ pushClipboard(text: string, terminalId?: string): { delivered: boolean; truncated: boolean; totalBytes: number; error?: string; }; /** Queue a terminal preview with the machine key. Delivery does not prove a browser opened it. */ pushFilePreview(terminalId: string, path: string, mode?: 'file' | 'diff'): { delivered: boolean; error?: string; }; /** Encrypt one base64 terminal payload with the per-machine key (same scheme * as the live output stream) → base64 ciphertext. Used for both live output * and the open-terminal snapshot/replay payloads. */ private encTerminalData; constructor(token: string, machine: Machine); setCliUpdateRequestHandler(handler: (version: unknown) => Promise<{ accepted: true; } | { error: string; }>): void; setCliUpdateRetryHandler(handler: (version: unknown) => Promise<{ accepted: true; } | { error: string; }>): void; /** Cache the latest relay policy locally and publish it now or on the next * socket connect. This makes startup/offline races harmless. */ setCliUpdateState(state: CliUpdateState | null): void; /** * B-276: publish the daemon-context Claude auth preflight into daemonState * (same serialized push chain as the CLI update policy). Resolves true when * the server acknowledged the write with our value in it, false otherwise * (rate-limit `result:'error'` is swallowed by updateDaemonState, so the * caller checks the echoed state and re-sends on the next probe). */ setClaudeAuthState(state: ClaudeAuthState): Promise; /** B-276 machine RPCs. Responses always carry `claudeAuth` so the web can refresh in place. */ setClaudeAuthHandlers(handlers: { probe: () => Promise; repair: (action: string) => Promise<{ ok: true; claudeAuth: ClaudeAuthState; } | { error: string; claudeAuth: ClaudeAuthState | null; }>; setStore: (store: 'auto' | 'file') => Promise; }): void; setRPCHandlers({ spawnSession, resumeSession, restartSession, stopSession, listTrackedSessionIds, requestShutdown }: MachineRpcHandlers): void; private syncRestartSessionRpcRegistration; private syncResumeSessionRpcRegistration; /** Every pushed list also flows to the mirror manager (claude-exit * detection via pane observation). Set by the daemon at startup. */ private mirrorListObserver; /** B-107: gate for mirror-terminal-send — set with the rest of the * mirror integration; absent (daemon still starting) means refuse. */ private mirrorInputAllowed; /** B-150: one line per daemon start, only when something was restored or * deliberately skipped. Same channel the terminal notifications use, so it * lands wherever the user already routed those (webhook / inbox). */ private wireAutoRestoreReport; setMirrorIntegration(integration: { resolveMirrorSessionId: (terminalId: string) => string | undefined; onTerminalClosed: (terminalId: string) => void; onTerminalList: (terminals: TerminalListItem[]) => void; /** design v3: unconditional per-tick reconcile (self-heals lost bindings * on an unchanged-signature list). */ onTerminalListTick: (terminals: TerminalListItem[]) => void; isMirrorInputAllowed: (terminalId: string) => boolean; }): void; /** Mirror bindings changed → re-derive and (on diff) push the list now. */ requestTerminalListRefresh(): void; private latestTerminalList; private terminalPushChain; /** Push the tracked terminal list into daemonState.webTerminals (server * persists + broadcasts `update-machine`). Skipped while disconnected — * the connect handler re-ships a full snapshot anyway. */ private pushTerminalList; /** * Update machine metadata * Currently unused, changes from the mobile client are more likely * for example to set a custom name. */ updateMachineMetadata(handler: (metadata: MachineMetadata | null) => MachineMetadata): Promise; /** * Update daemon state (runtime info) - similar to session updateAgentState * Simplified without lock - relies on backoff for retry */ updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise; connect(): void; private createControlSocket; private activateControlSocket; private bindControlDataHandlers; private startReleaseHandover; private releaseHandover; private startKeepAlive; private bindRpcRequestHandler; private bindMachineCommandHandlers; private unbindMachineCommandHandlers; private bindRealtimeHandlers; /** Reconcile commands missed while the daemon was offline. The server * returns only ids owned by this account and durably archived in the DB. */ private reconcileArchivedSessions; private refreshRelayConnection; private startSmartReconnect; private stopKeepAlive; shutdown(): void; } interface PushToken { id: string; token: string; createdAt: number; updatedAt: number; } type SessionNotificationKind = 'done' | 'permission' | 'question'; declare class PushNotificationClient { private readonly token; private readonly baseUrl; private readonly expo; constructor(token: string, baseUrl?: string); /** * Fetch all push tokens for the authenticated user. * Retries up to 3 times with exponential backoff on transient errors. */ fetchPushTokens(): Promise; /** * Send push notification via Expo Push API with retry * @param messages - Array of push messages to send */ sendPushNotifications(messages: ExpoPushMessage[]): Promise; /** * Send a push notification to all registered devices for the user * @param title - Notification title * @param body - Notification body * @param data - Additional data to send with the notification */ sendToAllDevices(title: string, body?: string, data?: Record): void; /** * Routes session-event pushes through the server so it can apply * presence-based suppression (active desktop/web, mobile foreground). * Falls back to direct Expo send only when sessionId is missing — that * shouldn't happen for session notifications but guards against regressions. */ sendSessionNotification(params: { kind: SessionNotificationKind; metadata: Metadata | null | undefined; data?: Record; }): void; } /** * Produces account-encrypted notifications on session/agent events and posts * them to the server feed. Designed to be non-blocking and best-effort: * a failed POST is logged and dropped, never propagated to the session loop. * * Deduplication: each notification carries a `repeatKey` of the form * `${sessionId}:${notifType}`. The server keeps only the latest entry per * repeatKey, and we also suppress locally-identical repeats so we don't * spam the feed when nothing meaningful changed. */ declare class NotificationProducer { private readonly token; private readonly accountBoxPubKey; private readonly sessionId; private readonly getMetadata; /** Last repeatKey we successfully (or attempted to) emit, used for local dedup. */ private readonly lastEmittedAt; constructor(opts: { credential: Credentials; sessionId: string; getMetadata: () => Metadata | null; }); /** * A new non-empty permission request was added to agentState.requests. */ permissionRequest(toolName: string): void; /** * A turn finished and Claude produced a reply (had assistant output during * the turn). thinking went true → false with new assistant message. */ replyDone(snippet?: string): void; /** * A turn ended with the session idle and waiting for user input: * no pending permission requests, not controlled by user, not thinking. */ inputNeeded(): void; /** * The session errored. */ error(message?: string): void; /** * Build, encrypt, dedup and POST a notification. Best-effort, fire-and-forget. */ private emit; private post; /** Short, human-friendly label for the project/session for use in titles. */ private projectLabel; private clip; } /** * Reconnect-in-place helpers (B-265). A resumed process reattaches to an * EXISTING happy session (HAPPY_RECONNECT_*). Two truths must not be mixed up: * * - the server owns the conversation-level metadata (summary, tags, * claudeSessionId, board, …) and the message `seq`; * - the new process owns its own identity (pid, version, capabilities, …). * * Everything here is pure so the merge rules are unit-tested; the network * side is `api.getSession` and the wiring lives in runClaude / runCodex. */ interface ServerSessionSnapshot { seq: number; metadata: Metadata; metadataVersion: number; agentState: AgentState$1 | null; agentStateVersion: number; } declare class ApiClient { static create(credential: Credentials): Promise; private readonly credential; private readonly pushClient; private constructor(); /** * Create a new session or load existing one with the given tag */ getOrCreateSession(opts: { tag: string; metadata: Metadata; state: AgentState$1 | null; }): Promise; /** * Register or update machine with the server * Returns the current machine state from the server with decrypted metadata and daemonState */ getOrCreateMachine(opts: { machineId: string; metadata: MachineMetadata; daemonState?: DaemonState; }): Promise; sessionSyncClient(session: Session, opts?: { initialSeq?: number; }): ApiSessionClient; machineSyncClient(machine: Machine): ApiMachineClient; push(): PushNotificationClient; /** * Create an account-encrypted notification producer bound to a session. * The producer derives the account box public key from the stored * credentials (no raw secret seed needed for the dataKey credential shape) * and posts encrypted notifications to the server feed. Best-effort: * failures never propagate into the session loop. */ notificationProducer(sessionId: string, getMetadata: () => Metadata | null): NotificationProducer; /** * Read a single value from the account KV store (same HTTP endpoint the * web app uses). Returns null when the key does not exist or on ANY * failure — callers (boardAnalyzer) treat KV as best-effort context. */ kvGet(key: string): Promise<{ key: string; value: string; version: number; } | null>; /** * Register a vendor API token with the server * The token is sent as a JSON string - server handles encryption */ registerVendorToken(vendor: 'openai' | 'anthropic' | 'gemini', apiKey: any): Promise; /** * Get vendor API token from the server * Returns the token if it exists, null otherwise */ getVendorToken(vendor: 'openai' | 'anthropic' | 'gemini'): Promise; /** * Mark a session as inactive on the server (active=false). Does NOT * change `lifecycleState`, so the session remains visible in the app * and resumable — same effect as the in-app "Archive" button hitting * the /archive endpoint, but without the extra metadata. * * Used during graceful shutdown (Ctrl-C / SIGTERM) as a synchronous * fallback for the socket-based session-end signal: even if the * socket emit doesn't drain before the process exits, the HTTP * response confirms the deactivate landed. */ deactivateSession(sessionId: string): Promise; /** * B-265: one session by id — the message cursor (`seq`), metadata and * agent state with their versions, decrypted with the session key the * reconnecting process already holds. `unsupported` = the server has no * such route (or the row is gone): callers fall back to the legacy * skip-all-history reconnect. Transient failures are retried a few times * first, because falling back silently discards the very message a restore * is meant to deliver. */ getSession(sessionId: string, encryptionKey: Uint8Array, encryptionVariant: 'legacy' | 'dataKey', opts?: { attempts?: number; delaysMs?: number[]; timeoutMs?: number; }): Promise<{ ok: true; session: ServerSessionSnapshot; } | { ok: false; reason: 'unsupported' | 'unavailable'; }>; /** Clear the server-owned archive tombstone before an intentional resume. * Older servers return 404 because the endpoint does not exist; that is a * compatible no-op because those servers also have no durable tombstone. */ reactivateSession(sessionId: string): Promise; } declare class Logger { readonly logFilePath: string; private dangerouslyUnencryptedServerLoggingUrl; private dangerouslyUnencryptedServerLoggingToken; constructor(logFilePath?: string); localTimezoneTimestamp(): string; debug(message: string, ...args: unknown[]): void; debugLargeJson(message: string, object: unknown, maxStringLength?: number, maxArrayLength?: number): void; info(message: string, ...args: unknown[]): void; infoDeveloper(message: string, ...args: unknown[]): void; warn(message: string, ...args: unknown[]): void; getLogPath(): string; private logToConsole; private sendToRemoteServer; private logToFile; } declare let logger: Logger; declare class Configuration { readonly serverUrl: string; readonly webappUrl: string; readonly isDaemonProcess: boolean; readonly happyHomeDir: string; readonly logsDir: string; readonly settingsFile: string; readonly privateKeyFile: string; readonly daemonStateFile: string; readonly daemonLockFile: string; readonly sessionsFile: string; readonly currentCliVersion: string; readonly isExperimentalEnabled: boolean; readonly disableCaffeinate: boolean; constructor(); } declare const configuration: Configuration; export { ApiClient, ApiSessionClient, RawJSONLinesSchema, configuration, logger }; export type { RawJSONLines };