@astralform/js 7.5.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/utils.ts","../src/rate-limit.ts","../src/streaming.ts","../src/types.ts","../src/client.ts","../src/storage.ts","../src/tools.ts","../src/protocol-registry.ts","../src/translate.ts","../src/session.ts","../src/restore-plan.ts","../src/stream-manager.ts","../src/replay.ts","../src/embedded-resource.ts"],"sourcesContent":["// Core classes\nexport { AstralformClient, parseVoicePolishFrame } from \"./client.js\";\nexport { ChatSession, CONVERSATION_PAGE_SIZE } from \"./session.js\";\nexport { ToolRegistry, type ToolHandler } from \"./tools.js\";\nexport { InMemoryStorage, type ChatStorage } from \"./storage.js\";\n\n// Errors\nexport {\n AstralformError,\n AuthenticationError,\n RateLimitError,\n LLMNotConfiguredError,\n ServerError,\n ConnectionError,\n StreamAbortedError,\n} from \"./errors.js\";\nexport type { RateLimitErrorDetails } from \"./errors.js\";\n\n// Utilities\nexport { generateId } from \"./utils.js\";\n\n// Streaming\nexport { streamJobSSE } from \"./streaming.js\";\n\n// Stream manager\nexport { StreamManager } from \"./stream-manager.js\";\nexport type {\n StreamState,\n SendOptions,\n StreamManagerEvent,\n} from \"./stream-manager.js\";\n\n// Delta translator (shared between session and replay)\nexport { translateDelta } from \"./translate.js\";\n\n// Event replay (for restoring conversations from persisted events)\nexport { mapSseToChat, replayEvents } from \"./replay.js\";\nexport type { RawSseEvent } from \"./replay.js\";\n\n// Embedded resource detection (protocol-agnostic UI surface helper)\nexport {\n isEmbeddedResource,\n parseEmbeddedResource,\n} from \"./embedded-resource.js\";\nexport type { EmbeddedResource } from \"./embedded-resource.js\";\n\n// Protocol adapter registry — consumers register framework-specific\n// renderers for embedded resource MIME types.\nexport { ProtocolRegistry } from \"./protocol-registry.js\";\nexport type { ProtocolAdapter } from \"./protocol-registry.js\";\n\n// Event type constants\nexport { ChatEventType } from \"./types.js\";\nexport type { ChatEventTypeValue } from \"./types.js\";\n\n// High-level ChatEvent (SDK → consumer)\nexport type { ChatEvent, BlockDeltaPayload, TurnUsage } from \"./types.js\";\n\n// Custom event payload catalog\nexport type {\n AgentIdentity,\n TaskStatus,\n TodoItem,\n TodoUpdatePayload,\n PlanUpdatePayload,\n NoteUpdatePayload,\n TitleGeneratedPayload,\n SubagentStartPayload,\n SubagentStopPayload,\n ContextWarningPayload,\n ContextUpdatePayload,\n MemoryRecord,\n MemoryRecallPayload,\n MemoryUpdatePayload,\n DesktopStreamPayload,\n AttachmentStagedPayload,\n WorkspaceReadyPayload,\n AssetCreatedPayload,\n ToolApprovalRequestedPayload,\n ToolApprovalGrantedPayload,\n ToolPermissionDeniedPayload,\n ToolHarnessWarningPayload,\n UserUnavailablePayload,\n PromptSuggestionPayload,\n} from \"./custom-events.js\";\n\n// Wire protocol types (for consumers that want to parse the raw SSE\n// data themselves or write their own transport adapter)\nexport type {\n WireEvent,\n WireMessageStart,\n WireMessageStop,\n WireBlockStart,\n WireBlockDelta,\n WireBlockStop,\n WireStallWarning,\n WireRetryEvent,\n WireErrorEvent,\n WireKeepalive,\n WireCustomEvent,\n WireBlockKind,\n WireBlockStatus,\n WireStopReason,\n WireBlockDeltaPayload,\n WireTextDelta,\n WireThinkingDelta,\n WireSignatureDelta,\n WireInputDelta,\n WireInputArgDelta,\n WireOutputDelta,\n WireStatusDelta,\n} from \"./types.js\";\n\n// Config + domain models\nexport type {\n AstralformConfig,\n AstralformApiKeyConfig,\n AstralformUserTokenConfig,\n Conversation,\n Message,\n AgentStatus,\n AgentCapability,\n UIComponentsConfig,\n AgentInfo,\n SkillInfo,\n ModelOption,\n ModelChoiceOptions,\n TeamSummary,\n TeamAgentSummary,\n} from \"./types.js\";\n\n// Request / response types\nexport type {\n ChatStreamRequest,\n CodeProject,\n AvailableRepository,\n AvailableRepositories,\n EffortRung,\n ReasoningEffort,\n ThinkingDescriptor,\n ThinkingRungOption,\n ToolResultRequest,\n ToolResult,\n ToolDefinition,\n ToolApprovalRequest,\n ToolApprovalDecision,\n ToolApprovalScope,\n ToolGrant,\n MyToolGrantsPage,\n ToolCallRequest,\n JobCreateResponse,\n JobStatus,\n JobSummary,\n ActiveJob,\n FeedbackRequest,\n FeedbackResponse,\n ConversationAsset,\n StreamJobSSEOptions,\n ChatStreamEvent,\n ConversationEvent,\n VoiceConfig,\n VoiceLLMMode,\n VoicePolishEvent,\n VoicePolishMode,\n VoicePolishRequest,\n VoiceTranscribeOptions,\n VoiceTranscript,\n} from \"./types.js\";\nexport { VOICE_POLISH_MODES, isVoiceLLMMode, isVoicePolishMode } from \"./types.js\";\n","export interface RateLimitErrorDetails {\n retryAfterSec?: number;\n resetAt?: number;\n scope?: string;\n policyId?: string;\n limit?: number;\n remaining?: number;\n requestId?: string;\n}\n\nexport class AstralformError extends Error {\n constructor(\n message: string,\n public code: string,\n ) {\n super(message);\n this.name = \"AstralformError\";\n }\n}\n\nexport class AuthenticationError extends AstralformError {\n constructor(message = \"Invalid or missing API key\") {\n super(message, \"authentication_error\");\n this.name = \"AuthenticationError\";\n }\n}\n\nexport class RateLimitError extends AstralformError {\n declare readonly retryAfterSec?: number;\n declare readonly resetAt?: number;\n declare readonly scope?: string;\n declare readonly policyId?: string;\n declare readonly limit?: number;\n declare readonly remaining?: number;\n declare readonly requestId?: string;\n\n constructor(\n message = \"Rate limit exceeded\",\n details: RateLimitErrorDetails = {},\n ) {\n super(message, \"rate_limit_error\");\n this.name = \"RateLimitError\";\n Object.assign(this, details);\n }\n}\n\nexport class LLMNotConfiguredError extends AstralformError {\n constructor(message = \"LLM provider not configured for this agent\") {\n super(message, \"llm_not_configured\");\n this.name = \"LLMNotConfiguredError\";\n }\n}\n\nexport class ServerError extends AstralformError {\n /**\n * The HTTP status, when this came from a response.\n *\n * Every status except 401 and 429 collapses into this one class, so without\n * it a caller cannot tell \"the thing is already gone\" (404) from \"the server\n * broke\" (500) or \"this is not yours\" (403) — the message is the only other\n * signal and it is prose. `deleteConversation` is the case that forced it:\n * it read EVERY failure as already-deleted and dropped the conversation\n * locally regardless, so a 500 looked exactly like success and the row came\n * back on the next device.\n *\n * Undefined when a ServerError is constructed without a response.\n */\n declare readonly status?: number;\n\n constructor(message = \"Internal server error\", status?: number) {\n super(message, \"server_error\");\n this.name = \"ServerError\";\n if (status !== undefined) Object.assign(this, { status });\n }\n}\n\nexport class ConnectionError extends AstralformError {\n constructor(message = \"Failed to connect to server\") {\n super(message, \"connection_error\");\n this.name = \"ConnectionError\";\n }\n}\n\nexport class StreamAbortedError extends AstralformError {\n constructor(message = \"Stream was aborted\") {\n super(message, \"stream_aborted\");\n this.name = \"StreamAbortedError\";\n }\n}\n","export function sanitizeErrorText(text: string): string {\n return text.slice(0, 500).replace(/Bearer\\s+\\S+/gi, \"Bearer [REDACTED]\");\n}\n\nexport function generateId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Convert a snake_case string to camelCase.\n */\nfunction snakeToCamel(str: string): string {\n return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());\n}\n\n/**\n * Transform all keys of an object from snake_case to camelCase.\n * Unknown fields pass through by default — a field the API adds and this\n * function does not name is still delivered to the consumer, just under its\n * camelCase name. Only values that need derivation (defaults, coercion,\n * filtering, nesting) should keep hand-mapping.\n *\n * @example\n * ```ts\n * const raw = { message_count: 5, created_at: \"2026-01-01T00:00:00Z\" };\n * const result = camelizeKeys(raw);\n * // → { messageCount: 5, createdAt: \"2026-01-01T00:00:00Z\" }\n * ```\n */\nexport function camelizeKeys<T>(\n obj: Record<string, unknown>,\n): T {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n result[snakeToCamel(key)] = obj[key];\n }\n return result as T;\n}\n","import { RateLimitError, type RateLimitErrorDetails } from \"./errors.js\";\nimport { sanitizeErrorText } from \"./utils.js\";\n\nconst DEFAULT_MESSAGE = \"Rate limit exceeded\";\n\nfunction parseNumber(value: unknown): number | undefined {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed) {\n return undefined;\n }\n const parsed = Number(trimmed);\n if (Number.isFinite(parsed)) {\n return parsed;\n }\n }\n return undefined;\n}\n\nfunction parseString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction parseJsonObject(rawText: string): Record<string, unknown> | undefined {\n if (!rawText) {\n return undefined;\n }\n try {\n const parsed: unknown = JSON.parse(rawText);\n if (parsed && typeof parsed === \"object\") {\n return parsed as Record<string, unknown>;\n }\n } catch {\n // Ignore parse errors and fall back to text-only handling.\n }\n return undefined;\n}\n\nfunction parseRetryAfterHeader(value: string | null): number | undefined {\n if (!value) {\n return undefined;\n }\n\n const numeric = parseNumber(value);\n if (numeric !== undefined) {\n return Math.max(0, Math.ceil(numeric));\n }\n\n const asDate = Date.parse(value);\n if (Number.isFinite(asDate)) {\n const diffMs = asDate - Date.now();\n return Math.max(0, Math.ceil(diffMs / 1000));\n }\n\n return undefined;\n}\n\nfunction parseResetTimestamp(value: unknown): number | undefined {\n const numeric = parseNumber(value);\n if (numeric !== undefined) {\n if (numeric > 1_000_000_000_000) {\n return Math.floor(numeric);\n }\n return Math.floor(numeric * 1000);\n }\n\n const asString = parseString(value);\n if (!asString) {\n return undefined;\n }\n\n const asDate = Date.parse(asString);\n if (Number.isFinite(asDate)) {\n return asDate;\n }\n return undefined;\n}\n\nfunction pickFirst<T>(\n payload: Record<string, unknown>,\n keys: string[],\n parser: (value: unknown) => T | undefined,\n): T | undefined {\n for (const key of keys) {\n const parsed = parser(payload[key]);\n if (parsed !== undefined) {\n return parsed;\n }\n }\n return undefined;\n}\n\nfunction buildRateLimitDetails(\n payload: Record<string, unknown>,\n headers?: Headers,\n): RateLimitErrorDetails {\n const headerRetryAfter = headers\n ? parseRetryAfterHeader(headers.get(\"retry-after\"))\n : undefined;\n const bodyRetryAfter = pickFirst(\n payload,\n [\"retry_after\", \"retryAfter\", \"retry_after_sec\", \"retryAfterSec\"],\n parseNumber,\n );\n\n const retryAfterSec = bodyRetryAfter ?? headerRetryAfter;\n\n const headerReset = headers\n ? parseResetTimestamp(\n headers.get(\"x-ratelimit-reset\") ?? headers.get(\"x-ratelimit-reset-at\"),\n )\n : undefined;\n const bodyReset = parseResetTimestamp(\n payload.reset_at ?? payload.resetAt ?? payload.reset,\n );\n\n const resetAt =\n bodyReset ??\n headerReset ??\n (retryAfterSec !== undefined\n ? Date.now() + retryAfterSec * 1000\n : undefined);\n\n const limit =\n pickFirst(payload, [\"limit\", \"rate_limit\", \"max\"], parseNumber) ??\n (headers ? parseNumber(headers.get(\"x-ratelimit-limit\")) : undefined);\n\n const remaining =\n pickFirst(payload, [\"remaining\", \"rate_limit_remaining\"], parseNumber) ??\n (headers ? parseNumber(headers.get(\"x-ratelimit-remaining\")) : undefined);\n\n const scope =\n pickFirst(payload, [\"scope\", \"limit_scope\"], parseString) ??\n (headers ? parseString(headers.get(\"x-ratelimit-scope\")) : undefined);\n\n const policyId =\n pickFirst(payload, [\"policy_id\", \"policyId\", \"policy\"], parseString) ??\n (headers\n ? parseString(\n headers.get(\"x-ratelimit-policy\") ??\n headers.get(\"x-ratelimit-policy-id\"),\n )\n : undefined);\n\n const requestId =\n pickFirst(payload, [\"request_id\", \"requestId\"], parseString) ??\n (headers\n ? parseString(\n headers.get(\"x-request-id\") ?? headers.get(\"x-correlation-id\"),\n )\n : undefined);\n\n return {\n retryAfterSec,\n resetAt,\n scope,\n policyId,\n limit,\n remaining,\n requestId,\n };\n}\n\nexport function createRateLimitErrorFromPayload(\n payload: Record<string, unknown>,\n fallbackMessage = DEFAULT_MESSAGE,\n): RateLimitError {\n const message = pickFirst(\n payload,\n [\"message\", \"error_description\"],\n parseString,\n );\n return new RateLimitError(\n message ?? fallbackMessage,\n buildRateLimitDetails(payload),\n );\n}\n\nexport function createRateLimitErrorFromHttp(\n response: Response,\n rawText: string,\n): RateLimitError {\n const payload = parseJsonObject(rawText) ?? {};\n const details = buildRateLimitDetails(payload, response.headers);\n\n const sanitizedText = sanitizeErrorText(rawText);\n const message =\n pickFirst(payload, [\"message\", \"error_description\"], parseString) ??\n (sanitizedText || DEFAULT_MESSAGE);\n\n return new RateLimitError(message, details);\n}\n","import {\n AuthenticationError,\n ConnectionError,\n ServerError,\n StreamAbortedError,\n} from \"./errors.js\";\nimport { createRateLimitErrorFromHttp } from \"./rate-limit.js\";\nimport type { ChatStreamEvent, StreamJobSSEOptions } from \"./types.js\";\nimport { sanitizeErrorText } from \"./utils.js\";\n\n/**\n * SSE stream reader. GET for the job event stream (the default); POST with a\n * JSON body for the voice polish stream. The frame parser is the same.\n */\nexport async function* streamJobSSE(\n options: StreamJobSSEOptions,\n): AsyncGenerator<ChatStreamEvent> {\n const { url, headers, signal, fetchFn, method = \"GET\", body } = options;\n\n let response: Response;\n try {\n response = await fetchFn(url, {\n method,\n headers,\n body,\n signal,\n });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n throw new StreamAbortedError();\n }\n throw new ConnectionError(\n err instanceof Error ? err.message : \"Failed to connect\",\n );\n }\n\n if (!response.ok) {\n const rawText = await response.text().catch(() => \"\");\n const text = rawText ? sanitizeErrorText(rawText) : \"\";\n switch (response.status) {\n case 401:\n throw new AuthenticationError();\n case 429:\n throw createRateLimitErrorFromHttp(response, rawText);\n default:\n throw new ServerError(text || `HTTP ${response.status}`, response.status);\n }\n }\n\n if (!response.body) {\n throw new ConnectionError(\"Response body is null\");\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let currentEvent = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"event: \")) {\n currentEvent = line.slice(7).trim();\n } else if (line.startsWith(\"data: \")) {\n const data = line.slice(6);\n if (data === \"[DONE]\") {\n return;\n }\n yield { event: currentEvent || \"message\", data };\n }\n if (line === \"\") {\n currentEvent = \"\";\n }\n }\n }\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n throw new StreamAbortedError();\n }\n throw err;\n } finally {\n reader.releaseLock();\n }\n}\n","// =============================================================================\n// Astralform SDK v2 type definitions\n// =============================================================================\n//\n// Wire protocol types mirror the Pydantic models in\n// `backend/src/stream/protocol.py` 1:1. The SDK forwards typed events to\n// the consumer; block construction happens on the consumer side.\n//\n// Custom event payloads live in `custom-events.ts`.\n\nimport type { AgentIdentity, MemoryRecord, TodoItem } from \"./custom-events.js\";\n\n// Re-export so consumers can import from types.ts or custom-events.ts\nexport type { AgentIdentity, MemoryRecord, TodoItem };\n\n// --- Configuration ---\n//\n// The SDK supports two authentication modes. TypeScript narrows the union by\n// property presence, so consumers can write:\n//\n// new AstralformClient({ apiKey, userId }) // API-key mode\n// new AstralformClient({ accessToken, agentId }) // user-token mode\n//\n// Pick based on who the caller represents:\n//\n// * API-key mode — A customer's backend (B2B2C). Agent scoping is baked\n// into the key; the end user is named per request via `X-End-User-ID`.\n//\n// * User-token mode — An app acting on behalf of an Astralform account\n// holder (AstralChat, future 3rd-party integrations). The OIDC access\n// token is issued by the Astralform Identity Provider (our OAuth 2.1\n// Server at auth.astralform.ai); agent scoping comes from the\n// `X-Agent-ID` header.\n\ninterface AstralformBaseConfig {\n /** Override the default API origin. Defaults to https://api.astralform.ai. */\n baseURL?: string;\n /** Supply a custom fetch (SSR, testing, custom interceptors). */\n fetch?: typeof globalThis.fetch;\n /**\n * Abort a REST request — connect, headers, AND body read — after this many\n * milliseconds. Defaults to 30_000. Does not apply to `uploadFile` (large\n * files on slow uplinks) or to SSE streaming, which is long-lived by design\n * and carries its own `AbortSignal`.\n *\n * Note that unlike axios and friends, `0` does NOT mean \"no timeout\": any\n * non-positive or non-finite value falls back to the default. REST calls\n * cannot opt out of the deadline — an unbounded REST request is the bug\n * this exists to prevent, and it fails silently (a stalled body strands the\n * caller forever with nothing to react to).\n */\n timeoutMs?: number;\n}\n\nexport interface AstralformApiKeyConfig extends AstralformBaseConfig {\n /** Agent API key (`sk_live_...` or `sk_test_...`). */\n apiKey: string;\n /**\n * The customer's own identifier for the end user making this call.\n * Sent as the `X-End-User-ID` header. Required in API-key mode.\n */\n userId: string;\n}\n\nexport interface AstralformUserTokenConfig extends AstralformBaseConfig {\n /**\n * OIDC access token issued by the Astralform Identity Provider.\n * Expect this to be short-lived; use `client.updateAccessToken()` after\n * refreshing to hot-swap without re-instantiating the client.\n */\n accessToken: string;\n /**\n * Active agent context. Sent as the `X-Agent-ID` header. The backend\n * verifies the token's developer has access to this agent on every\n * request; switching agents is a local `updateAgentId()` call.\n *\n * Optional so a pre-pick client (right after login, before the user has\n * chosen a team/agent) can still call account-scoped discovery routes\n * like `listTeams()` / `listAgents(teamId)`. Agent-scoped calls\n * (conversations, messages, chat) will error out until one is set.\n */\n agentId?: string;\n /**\n * Optional end-user override — lets a developer acting under a user\n * token impersonate a downstream end-user identity for testing\n * purposes. When set, sent alongside `X-Agent-ID` as `X-End-User-ID`\n * so memory, rate limits, and conversations scope to the specified\n * end-user rather than the developer themselves.\n *\n * Use `client.updateEndUserId()` to rotate at runtime.\n */\n endUserId?: string;\n}\n\nexport type AstralformConfig =\n AstralformApiKeyConfig | AstralformUserTokenConfig;\n\n// --- Event type constants (SDK public) ---\n//\n// These are the high-level ChatEvent kinds the SDK emits to its\n// consumer. The raw wire events below are translated into these\n// kinds at the session boundary.\n\nexport const ChatEventType = {\n // Connection lifecycle (SDK-local, not wire)\n Connected: \"connected\",\n Disconnected: \"disconnected\",\n\n // Turn lifecycle\n MessageStart: \"message_start\",\n MessageStop: \"message_stop\",\n\n // Block lifecycle\n BlockStart: \"block_start\",\n BlockDelta: \"block_delta\",\n BlockStop: \"block_stop\",\n\n // Reliability\n Stall: \"stall\",\n Retry: \"retry\",\n Error: \"error\",\n Keepalive: \"keepalive\",\n\n // Conversation-level (typed custom events)\n UserMessage: \"user_message\",\n TitleGenerated: \"title_generated\",\n TodoUpdate: \"todo_update\",\n PlanUpdate: \"plan_update\",\n NoteUpdate: \"note_update\",\n ContextUpdate: \"context_update\",\n SubagentStart: \"subagent_start\",\n SubagentStop: \"subagent_stop\",\n ContextWarning: \"context_warning\",\n MemoryRecall: \"memory_recall\",\n MemoryUpdate: \"memory_update\",\n DesktopStream: \"desktop_stream\",\n AttachmentStaged: \"attachment_staged\",\n WorkspaceReady: \"workspace_ready\",\n AssetCreated: \"asset_created\",\n ToolApprovalRequested: \"tool_approval_requested\",\n ToolApprovalGranted: \"tool_approval_granted\",\n ToolPermissionDenied: \"tool_permission_denied\",\n ToolHarnessWarning: \"tool_harness_warning\",\n UserUnavailable: \"user_unavailable\",\n PromptSuggestion: \"prompt_suggestion\",\n StateChanged: \"state_changed\",\n\n // Generic fallthrough for unknown custom events\n Custom: \"custom\",\n} as const;\n\nexport type ChatEventTypeValue =\n (typeof ChatEventType)[keyof typeof ChatEventType];\n\n// =============================================================================\n// Wire protocol (matches backend Pydantic models 1:1, snake_case)\n// =============================================================================\n\nexport type WireBlockKind = \"text\" | \"thinking\" | \"tool_use\";\n\nexport type WireBlockStatus =\n | \"streaming\"\n | \"awaiting_client_result\"\n | \"ok\"\n | \"error\"\n | \"denied\"\n | \"cancelled\";\n\nexport type WireStopReason =\n \"end_turn\" | \"tool_use\" | \"max_tokens\" | \"context_overflow\" | \"error\";\n\n// --- BlockDelta payloads (discriminated on `channel`) ---\n\nexport interface WireTextDelta {\n channel: \"text\";\n text: string;\n}\n\nexport interface WireThinkingDelta {\n channel: \"thinking\";\n text: string;\n}\n\nexport interface WireSignatureDelta {\n channel: \"signature\";\n signature: string;\n}\n\nexport interface WireInputDelta {\n channel: \"input\";\n partial_json: string;\n}\n\nexport interface WireInputArgDelta {\n channel: \"input_arg\";\n arg_name: string;\n text: string;\n}\n\nexport interface WireOutputDelta {\n channel: \"output\";\n stream: \"stdout\" | \"stderr\" | \"progress\";\n chunk: string;\n}\n\nexport interface WireStatusDelta {\n channel: \"status\";\n status:\n \"executing\" | \"awaiting_client_result\" | \"awaiting_approval\" | \"denied\";\n note?: string;\n}\n\nexport type WireBlockDeltaPayload =\n | WireTextDelta\n | WireThinkingDelta\n | WireSignatureDelta\n | WireInputDelta\n | WireInputArgDelta\n | WireOutputDelta\n | WireStatusDelta;\n\n// --- Top-level wire events ---\n\ninterface WireEnvelope {\n seq: number;\n ts: number;\n job_id: string;\n}\n\nexport interface WireMessageStart extends WireEnvelope {\n type: \"message_start\";\n turn_id: string;\n model: string;\n agent_name?: string | null;\n agent_display_name?: string | null;\n agent_avatar_url?: string | null;\n}\n\nexport interface WireBlockStart extends WireEnvelope {\n type: \"block_start\";\n turn_id: string;\n path: number[];\n parent_path?: number[] | null;\n kind: WireBlockKind;\n metadata: Record<string, unknown>;\n}\n\nexport interface WireBlockDelta extends WireEnvelope {\n type: \"block_delta\";\n turn_id: string;\n path: number[];\n delta: WireBlockDeltaPayload;\n}\n\nexport interface WireBlockStop extends WireEnvelope {\n type: \"block_stop\";\n turn_id: string;\n path: number[];\n status: WireBlockStatus;\n final: Record<string, unknown>;\n}\n\nexport interface WireMessageStop extends WireEnvelope {\n type: \"message_stop\";\n turn_id: string;\n stop_reason: WireStopReason;\n usage: {\n input_tokens?: number;\n output_tokens?: number;\n cached_tokens?: number;\n cache_creation_tokens?: number;\n };\n ttfb_ms?: number | null;\n total_ms: number;\n stall_count: number;\n}\n\nexport interface WireStallWarning extends WireEnvelope {\n type: \"stall\";\n since_last_event_ms: number;\n stall_count: number;\n}\n\nexport interface WireRetryEvent extends WireEnvelope {\n type: \"retry\";\n attempt: number;\n reason: string;\n backoff_ms: number;\n strategy?: string | null;\n max_attempts?: number | null;\n context_recovery?: Record<string, unknown> | null;\n}\n\nexport interface WireErrorEvent extends WireEnvelope {\n type: \"error\";\n code: string;\n message: string;\n block_path?: number[] | null;\n // Rate limit fields (carried when code == \"rate_limit_exceeded\")\n retry_after?: number;\n retry_after_sec?: number;\n reset_at?: number | string;\n scope?: string;\n policy_id?: string;\n limit?: number;\n remaining?: number;\n request_id?: string;\n}\n\nexport interface WireKeepalive extends WireEnvelope {\n type: \"keepalive\";\n since_last_event_ms: number;\n}\n\nexport interface WireCustomEvent extends WireEnvelope {\n type: \"custom\";\n name: string;\n data: Record<string, unknown>;\n}\n\nexport type WireEvent =\n | WireMessageStart\n | WireBlockStart\n | WireBlockDelta\n | WireBlockStop\n | WireMessageStop\n | WireStallWarning\n | WireRetryEvent\n | WireErrorEvent\n | WireKeepalive\n | WireCustomEvent;\n\n// =============================================================================\n// ChatEvent — high-level SDK events (camelCase, emitted to consumers)\n// =============================================================================\n\nexport interface TurnUsage {\n inputTokens: number;\n outputTokens: number;\n cachedTokens: number;\n /** Tokens written to the model's prompt cache (wire: `cache_creation_tokens`). */\n cacheCreationTokens: number;\n}\n\nexport type BlockDeltaPayload =\n | { channel: \"text\"; text: string }\n | { channel: \"thinking\"; text: string }\n | { channel: \"signature\"; signature: string }\n | { channel: \"input\"; partialJson: string }\n | { channel: \"inputArg\"; argName: string; text: string }\n | {\n channel: \"output\";\n stream: \"stdout\" | \"stderr\" | \"progress\";\n chunk: string;\n }\n | {\n channel: \"status\";\n status:\n \"executing\" | \"awaiting_client_result\" | \"awaiting_approval\" | \"denied\";\n note?: string;\n };\n\nexport type ChatEvent =\n // Connection lifecycle\n | { type: \"connected\" }\n | { type: \"disconnected\" }\n\n // Turn lifecycle\n | {\n type: \"message_start\";\n turnId: string;\n model: string;\n agentName?: string | null;\n agentDisplayName?: string | null;\n agentAvatarUrl?: string | null;\n }\n | {\n type: \"message_stop\";\n turnId: string;\n jobId: string;\n stopReason: WireStopReason;\n usage: TurnUsage;\n ttfbMs?: number | null;\n totalMs: number;\n stallCount: number;\n }\n\n // Block lifecycle\n | {\n type: \"block_start\";\n turnId: string;\n path: number[];\n parentPath?: number[] | null;\n kind: WireBlockKind;\n metadata: Record<string, unknown>;\n }\n | {\n type: \"block_delta\";\n turnId: string;\n path: number[];\n delta: BlockDeltaPayload;\n }\n | {\n type: \"block_stop\";\n turnId: string;\n path: number[];\n status: WireBlockStatus;\n final: Record<string, unknown>;\n }\n\n // Reliability\n | {\n type: \"stall\";\n sinceLastEventMs: number;\n stallCount: number;\n }\n | {\n type: \"retry\";\n attempt: number;\n reason: string;\n backoffMs: number;\n strategy?: string | null;\n maxAttempts?: number | null;\n contextRecovery?: Record<string, unknown> | null;\n }\n | {\n type: \"error\";\n code: string;\n message: string;\n blockPath: number[] | null;\n }\n | {\n type: \"keepalive\";\n sinceLastEventMs: number;\n }\n\n // Conversation-level — typed custom events\n | {\n type: \"user_message\";\n content: string;\n createdAt?: number;\n id?: string;\n /** This message was steered into a run already in flight — it started\n * no turn of its own. Consumers that badge a steer (a \"waiting to be\n * read\" notice) key off this; without it a replayed steer is\n * indistinguishable from an ordinary prompt. */\n steer?: boolean;\n }\n | { type: \"title_generated\"; title: string }\n | { type: \"todo_update\"; todos: TodoItem[] }\n /** The conversation's plan, as markdown, after the agent wrote or revised it.\n * Carries the FULL body rather than a diff: the backend replaces the plan\n * wholesale (`write_plan` — \"Replaces any existing plan\"), so a consumer that\n * merged deltas would drift from the stored document. */\n | { type: \"plan_update\"; plan: string }\n /** Names of the conversation's notes, after one was written or deleted. Names\n * only — bodies are unbounded and read on demand. */\n | { type: \"note_update\"; notes: string[] }\n | {\n type: \"context_update\";\n context: Record<string, unknown>;\n phase?: string | null;\n updatedAt?: number | null;\n }\n | {\n type: \"subagent_start\";\n agent: AgentIdentity;\n taskCallId?: string | null;\n }\n | {\n type: \"subagent_stop\";\n agent: AgentIdentity;\n taskCallId?: string | null;\n }\n | {\n type: \"context_warning\";\n /** Known values: \"info\" | \"warning\" | \"critical\". Typed as string for forward compat. */\n severity: string;\n utilizationPct: number;\n remainingTokens: number;\n windowTokens: number;\n inputTokens: number;\n message: string;\n }\n | { type: \"memory_recall\"; memories: MemoryRecord[] }\n | {\n type: \"memory_update\";\n /** Known values: \"created\" | \"updated\" | \"deleted\". */\n action: string;\n memoryId?: string | null;\n key?: string | null;\n namespace?: string | null;\n }\n | { type: \"desktop_stream\"; url: string; sandboxId?: string | null }\n | {\n type: \"attachment_staged\";\n attachmentId: string;\n filename: string;\n contentType?: string | null;\n sizeBytes?: number | null;\n }\n | {\n type: \"workspace_ready\";\n sandboxId: string;\n workspacePath?: string | null;\n }\n | {\n type: \"asset_created\";\n assetId: string;\n filename: string;\n url?: string | null;\n contentType?: string | null;\n }\n | {\n type: \"tool_approval_requested\";\n toolName: string;\n callId: string;\n arguments: Record<string, unknown>;\n riskLevel?: string | null;\n reason?: string | null;\n }\n | {\n type: \"tool_approval_granted\";\n toolName: string;\n callId: string;\n }\n | {\n type: \"tool_permission_denied\";\n toolName: string;\n callId: string;\n reason?: string | null;\n /** Known values: \"hook\" | \"rule\" | \"user\" | \"timeout\" | \"circuit_breaker\". */\n deniedBy?: string | null;\n }\n | {\n type: \"tool_harness_warning\";\n toolName: string;\n callId: string;\n message?: string | null;\n details?: Record<string, unknown> | null;\n }\n | {\n type: \"user_unavailable\";\n consecutiveTimeouts: number;\n toolName?: string | null;\n }\n | {\n type: \"prompt_suggestion\";\n suggestions: string[];\n }\n | {\n type: \"state_changed\";\n /**\n * Job lifecycle state. Known values from the backend today include\n * \"queued\" | \"running\" | \"waiting_for_tool\" | \"completed\" | \"failed\".\n * Typed as string for forward compat.\n */\n state: string;\n }\n\n // Generic fallthrough for unknown custom events\n | {\n type: \"custom\";\n name: string;\n data: Record<string, unknown>;\n };\n\n// =============================================================================\n// Domain models (unchanged from previous SDK version)\n// =============================================================================\n\nexport interface Conversation {\n id: string;\n title: string;\n messageCount: number;\n createdAt: string;\n updatedAt: string;\n /**\n * The project this task belongs to (`owner/repo`), or `null` for an ordinary\n * conversation. Set on the first turn and immutable after. Absent (rather than\n * null) from an Astralform older than 0.69.46.\n */\n repository?: string | null;\n}\n\nexport interface Message {\n id: string;\n conversationId: string;\n role: \"user\" | \"assistant\" | \"system\";\n content: string;\n parentId?: string;\n status: \"sending\" | \"streaming\" | \"complete\" | \"error\";\n createdAt: string;\n toolCalls?: ToolCallRequest[];\n}\n\nexport interface UIComponentsConfig {\n enabled: boolean;\n /** Protocol slug (e.g. \"a2ui\"). Null when disabled. */\n protocol: string | null;\n /** MIME type to match against embedded resources (e.g. \"application/json+a2ui\"). */\n mimeType: string | null;\n}\n\n/** One capability the agent either has or does not, right now. */\nexport interface AgentCapability {\n /** Stable key, e.g. `\"image\"`, `\"web\"`, `\"skills\"`. */\n key: string;\n enabled: boolean;\n}\n\nexport interface AgentStatus {\n isReady: boolean;\n llmConfigured: boolean;\n llmProvider?: string;\n llmModel?: string;\n message: string;\n uiComponents: UIComponentsConfig;\n /**\n * What the agent can do right now, so a client can offer a capability only\n * where it will actually work rather than failing on send.\n *\n * Empty when the backend does not report it (older servers) or when the agent\n * is not ready — treat an ABSENT key as \"not reported\", never as disabled.\n * Only capabilities with a real per-agent gate appear; always-on ones do not,\n * because a constant tells a client nothing.\n */\n capabilities: AgentCapability[];\n}\n\nexport interface AgentInfo {\n name: string;\n displayName: string;\n description: string;\n isOrchestrator: boolean;\n isEnabled: boolean;\n avatarUrl?: string;\n /**\n * Whether this agent's tasks can name a repository — the workspace has GitHub\n * connected and enabled here. A client shows Projects and Tasks on it.\n *\n * Derived by the server from the connector, so it cannot go stale the way the\n * retired `mode` toggle could. It gates a SURFACE, not an ability: naming a\n * repository is optional on every task, and a task that names none is an\n * ordinary chat. Absent on Astralform older than 0.69.50 — fall back to `mode`\n * there.\n *\n * It is a property of the WORKSPACE, not of a persona: `GET /v1/agents` selects\n * the workspace row itself and returns exactly one entry, so read\n * `agents[0].codeProjectsEnabled`. The workspace picker (`listAgents`) does not\n * carry it, so a client learns this after opening an agent.\n */\n codeProjectsEnabled?: boolean;\n /**\n * @deprecated Removed in the next Astralform release. There is no agent mode —\n * a repository belongs to the TASK, so one agent answers general questions and\n * works in repositories from the same list. Read {@link codeProjectsEnabled}.\n *\n * Still reported for one release, as the STORED value of the retired column, so\n * clients built before the change keep behaving exactly as they did. Do not\n * treat it as an alias for `codeProjectsEnabled`: an agent that never had the\n * toggle set still reports `\"chat\"` while its tasks can bind perfectly well.\n */\n mode?: \"chat\" | \"code\";\n}\n\n// --- Team / Agent discovery (OIDC user-token surface) ---\n\nexport interface TeamSummary {\n id: string;\n name: string;\n slug: string;\n isDefault: boolean;\n /** Caller's role in this team (e.g. \"owner\", \"admin\", \"member\"). */\n role: string;\n}\n\n/**\n * A team-level agent (formerly \"project\") — the workspace a user opens to\n * chat. Distinct from `AgentInfo`, which describes the AI personas available\n * INSIDE an agent workspace (orchestrator + specialists).\n */\nexport interface TeamAgentSummary {\n id: string;\n name: string;\n /** Human-readable label for pickers, when set (wire: `display_name`). Falls back to `name`. */\n displayName?: string | null;\n teamId: string;\n createdAt: string;\n updatedAt: string;\n /** Agent avatar URL, when the team has set one (wire: `avatar_url`). */\n avatarUrl?: string | null;\n}\n\nexport interface SkillInfo {\n name: string;\n displayName: string;\n description: string;\n isEnabled: boolean;\n}\n\n// TodoItem is imported from custom-events.ts and re-exported above.\n\n// =============================================================================\n// Job and request types\n// =============================================================================\n\nexport interface JobCreateResponse {\n job_id: string;\n conversation_id: string;\n message_id: string;\n status: string;\n}\n\nexport interface JobStatus {\n jobId: string;\n status: string;\n createdAt: string | null;\n startedAt: string | null;\n completedAt: string | null;\n errorMessage: string | null;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface ActiveJob {\n jobId: string | null;\n status: string;\n}\n\nexport interface JobSummary {\n jobId: string;\n status: string;\n replacesJobId: string | null;\n responseContent: Record<string, unknown> | null;\n metrics: Record<string, unknown> | null;\n createdAt: string | null;\n}\n\nexport interface FeedbackRequest {\n /** 1 for thumbs up, -1 for thumbs down. */\n rating: 1 | -1;\n comment?: string | null;\n}\n\nexport interface FeedbackResponse {\n id: string;\n jobId: string;\n rating: number;\n comment: string | null;\n createdAt: string;\n}\n\n/**\n * One rung on a model's thinking ladder.\n *\n * These strings are not ours to choose: they are verbatim what OpenAI,\n * DeepSeek and Z.AI each enumerate when rejecting an invalid\n * `reasoning_effort`, so renaming one means sending a value the provider 400s\n * on. A given model accepts a SUBSET — read it from\n * {@link ModelOption.thinkingControl}, never assume all seven.\n *\n * `\"none\"` is a real off on the providers that list it, and is distinct from\n * omitting the effort entirely: omitting means \"use the model's own default\",\n * which on some models still reasons.\n */\nexport type EffortRung =\n | \"none\"\n | \"minimal\"\n | \"low\"\n | \"medium\"\n | \"high\"\n | \"xhigh\"\n | \"max\";\n\n/**\n * A reasoning effort a caller may request. Widened from `low | medium | high`\n * once the providers were probed and shown to expose seven values.\n */\nexport type ReasoningEffort = EffortRung;\n\n/** One selectable rung, with the label every client should display for it. */\nexport interface ThinkingRungOption {\n id: EffortRung;\n /** Server-owned, so two clients cannot word the same rung differently. */\n label: string;\n}\n\n/**\n * A model's thinking control, from `GET /v1/models`.\n *\n * `ladder` runs least-effort-first. That order is the menu order and the basis\n * of any proportional indicator — a rung's INDEX is its strength. It contains a\n * `none` rung if and only if the model can genuinely stop reasoning, so an Off\n * is an ordinary rung rather than something a client synthesizes.\n *\n * `default` is the rung a run uses when the caller picks nothing, and is\n * nullable: for the OpenAI-style family the server sends no effort at all, so\n * the choice belongs to the provider and naming a rung would be a guess.\n */\nexport interface ThinkingDescriptor {\n ladder: ThinkingRungOption[];\n default: EffortRung | null;\n}\n\n/**\n * The per-request model choice (client-side model selection). `provider` and\n * `model` are paired — send both or neither; when omitted, the server reuses the\n * conversation's last model or a connected-provider default.\n */\nexport interface ModelChoiceOptions {\n provider?: string;\n model?: string;\n reasoningEffort?: ReasoningEffort;\n temperature?: number;\n}\n\nexport interface ChatStreamRequest {\n message?: string;\n conversation_id?: string;\n /**\n * Which project (GitHub repository, `owner/repo`) this task belongs to.\n *\n * Optional on every agent. The first turn that names one binds the task, and a\n * later turn may omit it or repeat the same value; a DIFFERENT value is refused\n * (409), not ignored — a task is bound to one repository for life, so a\n * different repository means a new task. A task that never names one is an\n * ordinary chat; since Astralform 0.69.50 there is no agent mode that requires\n * one.\n * Astralform >= 0.69.46.\n */\n repository?: string;\n mcp_manifest?: ToolDefinition[];\n enabled_mcp?: string[];\n continue_from_message?: string;\n resend_from?: string;\n upload_ids?: string[];\n agent_name?: string;\n plan_mode?: boolean;\n image_mode?: boolean;\n video_mode?: boolean;\n /**\n * Start a durable long-horizon goal for this run (goal mode). The backend mints\n * an agent_goal from this text and drives the run under a budget until the\n * objective is genuinely complete. A blank/omitted value runs a normal turn.\n */\n goal?: string;\n /**\n * Per-request model choice (client-side model selection), wire shape.\n * `provider` and `model` are paired; omit to reuse the thread's last model.\n */\n provider?: string;\n model?: string;\n reasoning_effort?: ReasoningEffort;\n temperature?: number;\n}\n\n/** One repository an app user works with on an agent. */\nexport interface CodeProject {\n repoFullName: string;\n addedAt: string;\n}\n\n/** A repository the workspace's GitHub installations cover. */\nexport interface AvailableRepository {\n fullName: string;\n private: boolean;\n}\n\n/**\n * What an app user may add, and why the list might be empty.\n *\n * `state` separates the three empty cases a picker must not conflate:\n * `ok` (connected, covers nothing new), `not_installed` (no installation to ask)\n * and `unavailable` (GitHub could not be reached — try again, do not tell the\n * user they have no repositories). `totalCount` is the installations' raw total\n * BEFORE already-added projects are subtracted, so it is not the list's length.\n */\nexport interface AvailableRepositories {\n state: \"ok\" | \"unavailable\" | \"not_installed\";\n repositories: AvailableRepository[];\n totalCount: number;\n /**\n * At least one of the workspace's GitHub installations could not be\n * enumerated, so the list is missing whatever that one covers. It is NOT\n * pagination — there is no next page to ask for. A picker should say some\n * repositories may be missing rather than present the list as complete.\n */\n partial: boolean;\n}\n\nexport interface ToolResultRequest {\n conversation_id: string;\n message_id: string;\n tool_results: ToolResult[];\n}\n\nexport interface ToolResult {\n call_id: string;\n tool_name: string;\n result: string;\n is_error: boolean;\n}\n\nexport type ToolApprovalDecision = \"allow\" | \"deny\";\nexport type ToolApprovalScope = \"once\" | \"conversation\" | \"always\";\n\nexport interface ToolApprovalRequest {\n job_id: string;\n call_id: string;\n decision: ToolApprovalDecision;\n scope: ToolApprovalScope;\n}\n\n/**\n * A remembered tool-permission grant belonging to the current end user.\n * Only `conversation`/`always` grants are ever stored (`once` is consumed at\n * approval time and never persisted).\n */\nexport interface ToolGrant {\n id: string;\n toolName: string;\n decision: ToolApprovalDecision;\n scope: Exclude<ToolApprovalScope, \"once\">;\n /** Set for `conversation`-scoped grants; `null` for `always`. */\n conversationId: string | null;\n createdAt: string;\n}\n\n/** A page of the current end user's own tool grants. */\nexport interface MyToolGrantsPage {\n grants: ToolGrant[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n}\n\nexport interface ToolCallRequest {\n callId: string;\n toolName: string;\n displayName?: string;\n description?: string;\n arguments: Record<string, unknown>;\n isClientTool: boolean;\n toolCategory?: string;\n iconUrl?: string;\n}\n\n// =============================================================================\n// SSE transport / raw parsing\n// =============================================================================\n\nexport interface StreamJobSSEOptions {\n url: string;\n headers: Record<string, string>;\n signal?: AbortSignal;\n fetchFn: typeof globalThis.fetch;\n /** Request method; defaults to GET (the job event stream). */\n method?: \"GET\" | \"POST\";\n /** A JSON-serialised body for POST streams (the voice polish stream). */\n body?: string;\n}\n\n// =============================================================================\n// Voice input\n// =============================================================================\n\n/**\n * The styles a transcript can be shaped into, as the server names them.\n * `raw` never calls the model — the transcript is used as recognized.\n */\nexport const VOICE_POLISH_MODES = [\"raw\", \"light\", \"structured\", \"formal\"] as const;\n\nexport type VoicePolishMode = (typeof VOICE_POLISH_MODES)[number];\n\n/** A mode that calls the polish model — every mode except `raw`. */\nexport type VoiceLLMMode = Exclude<VoicePolishMode, \"raw\">;\n\n/** Whether `value` is one of the four modes this SDK version knows. */\nexport function isVoicePolishMode(value: unknown): value is VoicePolishMode {\n return (\n typeof value === \"string\" && (VOICE_POLISH_MODES as readonly string[]).includes(value)\n );\n}\n\n/**\n * Whether `mode` may be passed to `streamVoicePolish` — the check to make on\n * `VoiceConfig.defaultMode`, which can be `raw`.\n */\nexport function isVoiceLLMMode(mode: VoicePolishMode): mode is VoiceLLMMode {\n return mode !== \"raw\";\n}\n\n/**\n * What a client needs to run the microphone for an agent, from\n * `GET /v1/voice/config`. Deliberately carries no provider or model names.\n */\nexport interface VoiceConfig {\n enabled: boolean;\n /**\n * The styles the client may request, as the server names them. Kept as\n * plain strings so a mode this SDK version does not know still reaches a\n * picker; `isVoicePolishMode` narrows one.\n */\n modes: string[];\n /**\n * The style to use when the user has not picked one. Falls back to\n * `structured` when the server names a mode this SDK does not know. Can be\n * `raw`, which `streamVoicePolish` refuses — check it with `isVoiceLLMMode`\n * (or compare against `\"raw\"`) before polishing.\n */\n defaultMode: VoicePolishMode;\n /** In tap-to-talk mode, the pause that ends a recording. */\n silenceAutoStopSeconds: number;\n /** Send the message as soon as the result is ready. */\n autoSend: boolean;\n maxRecordingSeconds: number;\n /**\n * Whether the configured recognizer emits live partial transcripts. Batch\n * (Whisper-style) providers do not; they transcribe on stop.\n */\n supportsStreaming: boolean;\n /**\n * The project vocabulary, for display. The server applies it to every\n * transcription and polish itself; `transcribeVoice({ hotwords })` and\n * `VoicePolishRequest.hotwords` carry only the user's own words, which the\n * server merges after these.\n */\n hotwords: string[];\n}\n\n/** One recording turned into text, from `POST /v1/voice/transcriptions`. */\nexport interface VoiceTranscript {\n text: string;\n language: string | null;\n /** Length of the submitted audio, when the WAV header said so. */\n durationMs: number | null;\n /** Time the provider took. */\n asrMs: number;\n}\n\nexport interface VoiceTranscribeOptions {\n /** File name sent with the recording; defaults to `recording.wav`. */\n filename?: string;\n /**\n * The user's own vocabulary — not the project's, which the server already\n * holds and merges in first. Sent comma-separated, so a word cannot itself\n * contain a comma.\n */\n hotwords?: string[];\n /** ISO 639-1 hint; omit for auto-detection. */\n language?: string;\n /**\n * Abort the upload. No client-side deadline applies to this call (a\n * recording can run to `maxRecordingSeconds` and the upload with it), so\n * this is the only way to give up on a stalled one; the promise rejects\n * with the abort reason (the runtime's `AbortError`, or what was passed to\n * `abort(reason)`).\n */\n signal?: AbortSignal;\n}\n\nexport interface VoicePolishRequest {\n text: string;\n /** One of the LLM modes; `raw` is refused by the server. */\n mode: VoiceLLMMode;\n /** The user's own vocabulary; the server merges it after the project's. */\n hotwords?: string[];\n}\n\n/**\n * One frame of the polish stream. `delta` text arrives in order; `done.text`\n * is authoritative (the cleaned full output, which can differ from the\n * concatenated deltas). On `error` keep the raw transcript; `partial` is what\n * was streamed before the failure.\n */\nexport type VoicePolishEvent =\n | { type: \"delta\"; text: string }\n | { type: \"done\"; text: string; polishMs: number }\n | { type: \"error\"; reason: string; partial: string; detail?: string };\n\nexport interface ChatStreamEvent {\n event: string;\n data: string;\n}\n\nexport interface ConversationEvent {\n seq: number;\n event: string;\n data: Record<string, unknown>;\n}\n\n// =============================================================================\n// Send / session options\n// =============================================================================\n\nexport interface SendOptions extends ModelChoiceOptions {\n /**\n * Which conversation this turn belongs to. Defaults to the session's current\n * one.\n *\n * ⚠️ BEHAVIOR CHANGE: passing this now RELOCATES the session. A value\n * different from the current one sets `session.conversationId`, DISCARDS\n * `session.messages` (the old conversation's list is not that conversation's\n * history), and invalidates any `loadConversation` still in flight.\n * Previously it addressed a single turn elsewhere and left the session where\n * it was. After a successful send the list holds only that turn, so load the\n * conversation if you need its history.\n *\n * The change is deliberate: `onSessionEvent` tags every emitted event with\n * `session.conversationId`, so a turn sent elsewhere without moving the\n * pointer streamed its events back under the wrong conversation. Addressing\n * one turn away from the session is not something the session can honestly\n * represent while it has a single pointer, so the send now moves it.\n */\n conversationId?: string;\n enabledClientTools?: string[];\n uploadIds?: string[];\n agentName?: string;\n planMode?: boolean;\n /**\n * Attach the image-generation tool to this turn.\n *\n * Off by default and per-message, because generating costs the developer real\n * money at a third-party provider — the agent does not get to decide on its\n * own that a picture would be nice. The tool is not attached at all unless\n * this is set, so an agent with image generation configured still cannot\n * generate one on an ordinary turn.\n *\n * Gate the affordance on `AgentStatus.capabilities` (Astralform ≥ 0.61.0) —\n * an agent with no provider configured will simply have no tool to call.\n * The `image_mode` field itself is accepted from Astralform ≥ 0.59.0.\n */\n imageMode?: boolean;\n /**\n * Put this turn in video mode, attaching the video-generation tool.\n *\n * Off by default and per-message, for a sharper reason than images: a clip\n * occupies one shared GPU for minutes, during which image generation on the\n * same host cannot run at all. The tool is not attached unless this is set.\n *\n * Mutually exclusive with `imageMode` at the composer level — which is why a\n * video turn also gets the image tool server-side: the user cannot select\n * both, so the agent must be able to produce its own first frame.\n *\n * `generate_video` animates an EXISTING image; it cannot start from text, and\n * the clip is silent. Gate the affordance on `AgentStatus.capabilities`\n * (`video`), the same way image mode does.\n */\n videoMode?: boolean;\n /**\n * Start a durable long-horizon goal for this run (goal mode). The text becomes\n * the goal's objective; the backend keeps the agent working under a budget until\n * it's complete. Omit for a normal turn.\n */\n goal?: string;\n /**\n * The project this task belongs to (`owner/repo`), when it has one.\n *\n * Send it on the turn that STARTS a task; the binding is write-once, so a\n * later turn may omit it or repeat the same value, and a DIFFERENT value is\n * refused (409) rather than ignored. Omit it entirely and the task is an\n * ordinary chat — since Astralform 0.69.50 no agent requires one, and a first\n * turn without it is no longer a 400. Astralform >= 0.69.46.\n */\n repository?: string;\n}\n\n/**\n * A selectable model for one of the team's connected providers, from\n * `GET /v1/models`. Backs the client-side model picker.\n */\nexport interface ModelOption {\n provider: string;\n providerDisplay: string;\n model: string;\n thinking: boolean;\n tools: boolean;\n vision: boolean;\n /**\n * The model's thinking control, or absent when it has none.\n *\n * Its ABSENCE is the signal to render no control — that replaces a separate\n * `supportsEffort` flag which had to be kept consistent with the level list\n * beside it. Distinct from `thinking` above, which says only whether the\n * model reasons at all: a model can reason with no control a client can\n * drive.\n */\n thinkingControl?: ThinkingDescriptor;\n /** the provider's brand mark (picker tiles); null until the backend rollout / for icon-less providers */\n iconUrl?: string | null;\n /** when the CALLER last ran this model (picker \"Recent\" ordering); null for a never-used model */\n lastUsedAt?: string | null;\n /** how many times the caller has run this model; null alongside lastUsedAt */\n useCount?: number | null;\n /**\n * The model's context window in tokens, as the serving provider reports it\n * — which is not always the model's headline number (a provider may serve\n * a smaller window than the model supports). Null when the backend does not\n * state one.\n */\n contextWindow?: number | null;\n}\n\n// =============================================================================\n// Conversation assets\n// =============================================================================\n\nexport interface ConversationAsset {\n id: string;\n kind: \"upload\" | \"output\";\n originalName: string;\n mediaType: string;\n sizeBytes: number;\n workspacePath?: string;\n sourceMessageId?: string;\n agentName?: string;\n /**\n * Freshly-signed download/preview URL. Minted per response by the backend\n * (private `workspaces` bucket), so it reflects the current signature rather\n * than a link baked in when the asset was created. May be absent if signing\n * failed or the asset has no stored object.\n */\n url?: string;\n /**\n * The asset's PERMANENT address. Authorization is resolved per request\n * against the caller's live session, so unlike `url` it never expires — but\n * it needs an `Authorization` header, which a browser will not attach to an\n * `<img src>`. Store and link this one; render from `url`.\n */\n contentUrl?: string;\n /**\n * A still to show for an asset the browser cannot draw from `url` alone.\n * Present only for video: an `<img src>` pointed at an mp4 renders nothing,\n * so a video row would otherwise have no thumbnail. Signed and expiring\n * exactly like `url` — display, not identity, so never store it.\n */\n posterUrl?: string;\n createdAt: string;\n}\n","import { AuthenticationError, ConnectionError, ServerError } from \"./errors.js\";\nimport { createRateLimitErrorFromHttp } from \"./rate-limit.js\";\nimport { streamJobSSE } from \"./streaming.js\";\nimport { VOICE_POLISH_MODES, isVoicePolishMode } from \"./types.js\";\nimport { camelizeKeys, sanitizeErrorText } from \"./utils.js\";\nimport type {\n ActiveJob,\n AgentInfo,\n AvailableRepositories,\n CodeProject,\n AstralformApiKeyConfig,\n AstralformConfig,\n ChatStreamEvent,\n ChatStreamRequest,\n ConversationAsset,\n ConversationEvent,\n Conversation,\n FeedbackRequest,\n FeedbackResponse,\n JobCreateResponse,\n JobStatus,\n JobSummary,\n Message,\n ModelOption,\n MyToolGrantsPage,\n AgentStatus,\n TeamAgentSummary,\n SkillInfo,\n TeamSummary,\n ToolApprovalRequest,\n ToolResultRequest,\n VoiceConfig,\n VoicePolishEvent,\n VoicePolishRequest,\n VoiceTranscribeOptions,\n VoiceTranscript,\n} from \"./types.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.astralform.ai\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nfunction validateBaseURL(url: string): string {\n const cleaned = url.replace(/\\/+$/, \"\");\n try {\n const parsed = new URL(cleaned);\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `Invalid baseURL protocol \"${parsed.protocol}\" - only http: and https: are allowed`,\n );\n }\n return parsed.origin + parsed.pathname.replace(/\\/+$/, \"\");\n } catch (err) {\n if (err instanceof Error && err.message.includes(\"Invalid baseURL\")) {\n throw err;\n }\n throw new Error(`Invalid baseURL: \"${cleaned}\" is not a valid URL`);\n }\n}\n\nfunction isApiKeyConfig(\n config: AstralformConfig,\n): config is AstralformApiKeyConfig {\n return \"apiKey\" in config;\n}\n\n/** Discriminates between the two auth modes the client supports. */\ntype AuthMode =\n | { kind: \"api_key\"; apiKey: string; userId: string }\n | {\n kind: \"user_token\";\n accessToken: string;\n /** Null until the user picks an agent; account-scoped calls still work. */\n agentId: string | null;\n /** Optional end-user override. When present, sent as X-End-User-ID. */\n endUserId: string | null;\n };\n\nexport class AstralformClient {\n private readonly baseURL: string;\n private readonly fetchFn: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n /**\n * Auth state is mutable so callers can rotate access tokens or switch\n * agent context without re-instantiating the client. API-key mode is\n * effectively immutable in practice but uses the same shape for uniformity.\n */\n private auth: AuthMode;\n\n constructor(config: AstralformConfig) {\n if (isApiKeyConfig(config)) {\n if (!config.apiKey || typeof config.apiKey !== \"string\") {\n throw new Error(\"apiKey is required and must be a non-empty string\");\n }\n if (!config.userId || typeof config.userId !== \"string\") {\n throw new Error(\"userId is required in API-key mode\");\n }\n this.auth = {\n kind: \"api_key\",\n apiKey: config.apiKey,\n userId: config.userId,\n };\n } else {\n if (!config.accessToken || typeof config.accessToken !== \"string\") {\n throw new Error(\n \"accessToken is required and must be a non-empty string in user-token mode\",\n );\n }\n // agentId is optional — a pre-pick client (right after login) can\n // still hit account-scoped routes like listTeams(). Agent-scoped\n // routes will 4xx until one is set via updateAgentId().\n const agentId =\n typeof config.agentId === \"string\" && config.agentId.length > 0\n ? config.agentId\n : null;\n this.auth = {\n kind: \"user_token\",\n accessToken: config.accessToken,\n agentId,\n endUserId:\n typeof config.endUserId === \"string\" && config.endUserId.length > 0\n ? config.endUserId\n : null,\n };\n }\n\n this.baseURL = validateBaseURL(config.baseURL ?? DEFAULT_BASE_URL);\n this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);\n this.timeoutMs =\n typeof config.timeoutMs === \"number\" &&\n Number.isFinite(config.timeoutMs) &&\n config.timeoutMs > 0\n ? config.timeoutMs\n : DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Replace the current OIDC access token without reconstructing the client.\n * Use after refreshing via the host app's own token manager.\n * Throws if the client was created in API-key mode.\n */\n updateAccessToken(accessToken: string): void {\n if (this.auth.kind !== \"user_token\") {\n throw new Error(\"updateAccessToken is only valid in user-token mode\");\n }\n if (!accessToken || typeof accessToken !== \"string\") {\n throw new Error(\"accessToken must be a non-empty string\");\n }\n this.auth = { ...this.auth, accessToken };\n }\n\n /**\n * Swap the active agent for a user-token client. The backend verifies the\n * current developer has access to the new agent; a 403 comes back if not.\n */\n updateAgentId(agentId: string): void {\n if (this.auth.kind !== \"user_token\") {\n throw new Error(\"updateAgentId is only valid in user-token mode\");\n }\n if (!agentId || typeof agentId !== \"string\") {\n throw new Error(\"agentId must be a non-empty string\");\n }\n this.auth = { ...this.auth, agentId };\n }\n\n /**\n * Set (or clear) the end-user override for user-token mode.\n *\n * Pass `null` or an empty string to clear — subsequent requests go\n * back to scoping against the developer's own identity. Throws if\n * called in API-key mode, where end-user context already travels via\n * the constructor's `userId` field.\n */\n updateEndUserId(endUserId: string | null): void {\n if (this.auth.kind !== \"user_token\") {\n throw new Error(\"updateEndUserId is only valid in user-token mode\");\n }\n const normalized =\n typeof endUserId === \"string\" && endUserId.length > 0 ? endUserId : null;\n this.auth = { ...this.auth, endUserId: normalized };\n }\n\n /** Current end-user override in user-token mode, or `null` if unset. */\n get endUserId(): string | null {\n return this.auth.kind === \"user_token\" ? this.auth.endUserId : null;\n }\n\n /**\n * Active agent for user-token mode, or `null` if pre-pick (client\n * was constructed without one). For API-key mode the agent is baked\n * into the key, so this getter returns `null` there too — use\n * `authMode` to disambiguate.\n */\n get agentId(): string | null {\n return this.auth.kind === \"user_token\" ? this.auth.agentId : null;\n }\n\n /** Which auth mode this client was constructed with. */\n get authMode(): \"api_key\" | \"user_token\" {\n return this.auth.kind;\n }\n\n /**\n * Authorization + identity headers for the current auth mode, without\n * `Content-Type`. Suitable for JSON requests (paired with the JSON header\n * in the `headers` getter) and for multipart uploads where the browser\n * must set its own `Content-Type` boundary.\n */\n private get authHeaders(): Record<string, string> {\n if (this.auth.kind === \"api_key\") {\n return {\n Authorization: `Bearer ${this.auth.apiKey}`,\n \"X-End-User-ID\": this.auth.userId,\n };\n }\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.auth.accessToken}`,\n };\n if (this.auth.agentId) {\n headers[\"X-Agent-ID\"] = this.auth.agentId;\n }\n if (this.auth.endUserId) {\n headers[\"X-End-User-ID\"] = this.auth.endUserId;\n }\n return headers;\n }\n\n private get headers(): Record<string, string> {\n return {\n ...this.authHeaders,\n \"Content-Type\": \"application/json\",\n };\n }\n\n /**\n * Run one REST exchange under a single deadline covering connect, headers,\n * AND the body read. The body read is the part that matters: `json()` used\n * to sit outside every guard, so a response whose headers arrived but whose\n * body stalled hung forever — silently stranding callers that await it\n * (a stalled `getMessages` used to leave `StreamManager.restore()` parked\n * before it ever fetched the events it renders from).\n *\n * The controller is created per request and is deliberately NOT the\n * session's — that one means \"the user cancelled this turn\" and is null\n * outside a live turn. Aborting frees the socket; the race guarantees a\n * rejection even when an injected `fetch` ignores the signal.\n */\n private async withDeadline<T>(\n run: (signal: AbortSignal) => Promise<T>,\n ): Promise<T> {\n const controller = new AbortController();\n const timedOut = () =>\n new ConnectionError(`Request timed out after ${this.timeoutMs}ms`);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n reject(timedOut());\n }, this.timeoutMs);\n });\n try {\n // Promise.race subscribes to both inputs, so the loser's later\n // rejection (the abort landing after the deadline won) counts as\n // handled and can't surface as an unhandled rejection.\n return await Promise.race([run(controller.signal), deadline]);\n } catch (err) {\n // A signal-honouring fetch rejects with AbortError before the race\n // settles — normalize it to the same timeout error either way.\n if (controller.signal.aborted) throw timedOut();\n throw err;\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async request(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<Response> {\n return this.withDeadline((signal) => this.send(method, path, body, signal));\n }\n\n /** Fetch + status handling. Always called inside `withDeadline`. */\n private async send(\n method: string,\n path: string,\n body: unknown,\n signal: AbortSignal,\n ): Promise<Response> {\n const response = await this.fetchFn(`${this.baseURL}${path}`, {\n method,\n headers: this.headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n signal,\n }).catch((err) => {\n throw new ConnectionError(\n err instanceof Error ? err.message : \"Failed to connect\",\n );\n });\n await this.handleError(response);\n return response;\n }\n\n // DO NOT refactor these back into `request()` + `.json()`. Parsing the body\n // INSIDE the raced callback is the entire fix: `json()` outside the deadline\n // is the original bug (headers arrive, body stalls, caller hangs forever).\n // `request()` survives for `del()`, which never reads the body.\n\n async get<T>(path: string): Promise<T> {\n return this.withDeadline(async (signal) => {\n const response = await this.send(\"GET\", path, undefined, signal);\n return (await response.json()) as T;\n });\n }\n\n async post<T>(path: string, body: unknown): Promise<T> {\n return this.withDeadline(async (signal) => {\n const response = await this.send(\"POST\", path, body, signal);\n return (await response.json()) as T;\n });\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n return this.withDeadline(async (signal) => {\n const response = await this.send(\"PATCH\", path, body, signal);\n return (await response.json()) as T;\n });\n }\n\n private async del(path: string): Promise<void> {\n await this.request(\"DELETE\", path);\n }\n\n private async handleError(response: Response): Promise<void> {\n if (response.ok) return;\n const text = await response.text().catch(() => \"\");\n switch (response.status) {\n case 401:\n throw new AuthenticationError();\n case 429:\n throw createRateLimitErrorFromHttp(response, text);\n default: {\n const safeText = text ? sanitizeErrorText(text) : \"\";\n // Status carried alongside the message: callers that need to tell one\n // failure from another (a 404 meaning \"already gone\" from a 500 meaning\n // \"we don't know\") were left parsing prose otherwise.\n throw new ServerError(\n safeText || `HTTP ${response.status}`,\n response.status,\n );\n }\n }\n }\n\n // --- REST Methods ---\n\n async getHealth(): Promise<{\n status: string;\n version: string;\n ollama_connected: boolean;\n }> {\n return this.get(\"/v1/health\");\n }\n\n // Agent readiness check, scoped to the client's active agent via X-Agent-ID.\n async getAgentStatus(): Promise<AgentStatus> {\n const raw = await this.get<{\n is_ready: boolean;\n llm_configured: boolean;\n llm_provider?: string;\n llm_model?: string;\n message: string;\n ui_components?: {\n enabled?: boolean;\n protocol?: string | null;\n mime_type?: string | null;\n };\n capabilities?: Array<{ key?: string; enabled?: boolean }>;\n }>(\"/v1/agent/status\");\n const ui = raw.ui_components ?? {};\n return {\n isReady: raw.is_ready,\n llmConfigured: raw.llm_configured,\n llmProvider: raw.llm_provider,\n llmModel: raw.llm_model,\n message: raw.message,\n // Defaults to [] rather than undefined so a caller can iterate without a\n // guard, and so a server that predates the field behaves as \"reports\n // nothing\" instead of throwing. Entries missing a key are dropped: a\n // capability with no name cannot be matched against and would render as\n // an unlabelled row.\n capabilities: (raw.capabilities ?? []).flatMap((c) =>\n typeof c?.key === \"string\" && c.key.length > 0\n ? [{ key: c.key, enabled: Boolean(c.enabled) }]\n : [],\n ),\n uiComponents: {\n enabled: Boolean(ui.enabled),\n protocol: ui.protocol ?? null,\n mimeType: ui.mime_type ?? null,\n },\n };\n }\n\n /**\n * A page of conversations, newest-updated first.\n *\n * `options.repository` narrows to one project's tasks (`owner/repo`) — the same\n * paging applies within the filter, so a client showing tasks per project pages\n * each project separately. Tasks that named no repository fall outside every\n * such filter; list them with no filter at all.\n */\n async getConversations(\n limit = 50,\n offset = 0,\n options?: { repository?: string },\n ): Promise<Conversation[]> {\n const safeLimit = Math.max(1, Math.min(200, Math.floor(Number(limit))));\n const safeOffset = Math.max(0, Math.floor(Number(offset)));\n const filter = options?.repository\n ? `&repository=${encodeURIComponent(options.repository)}`\n : \"\";\n const raw = await this.get<\n {\n id: string;\n title: string;\n message_count: number;\n created_at: string;\n updated_at: string;\n repository?: string | null;\n }[]\n >(`/v1/conversations?limit=${safeLimit}&offset=${safeOffset}${filter}`);\n return raw.map((c) => camelizeKeys<Conversation>(c as unknown as Record<string, unknown>));\n }\n\n async getMessages(conversationId: string): Promise<Message[]> {\n const raw = await this.get<\n {\n id: string;\n conversation_id: string;\n role: \"user\" | \"assistant\" | \"system\";\n content: string;\n parent_id?: string;\n created_at: string;\n }[]\n >(`/v1/conversations/${encodeURIComponent(conversationId)}/messages`);\n return raw.map((m) => ({\n id: m.id,\n conversationId: m.conversation_id,\n role: m.role,\n content: m.content,\n parentId: m.parent_id,\n status: \"complete\" as const,\n createdAt: m.created_at,\n }));\n }\n\n /**\n * Replace the title the server generated from the conversation's first turn.\n *\n * The server does NOT bump `updated_at` for a rename — conversations list\n * newest-updated first, and relabelling one is not activity — so the\n * timestamp coming back is the original. Callers should merge the response\n * rather than stamp their own, or they reintroduce the reordering the\n * server deliberately avoids.\n */\n async renameConversation(id: string, title: string): Promise<Conversation> {\n const c = await this.patch<{\n id: string;\n title: string;\n message_count: number;\n created_at: string;\n updated_at: string;\n }>(`/v1/conversations/${encodeURIComponent(id)}`, { title });\n return camelizeKeys<Conversation>(c as unknown as Record<string, unknown>);\n }\n\n async deleteConversation(id: string): Promise<void> {\n await this.del(`/v1/conversations/${encodeURIComponent(id)}`);\n }\n\n /**\n * List the AI personas (sub-agents) available INSIDE the client's active\n * agent workspace — orchestrator + specialists, addressed per message via\n * `ChatStreamRequest.agent_name`. Not to be confused with `listAgents()`,\n * which enumerates the team-level agents a signed-in user can open.\n */\n async getAgents(): Promise<AgentInfo[]> {\n const raw = await this.get<\n {\n name: string;\n display_name: string;\n description: string;\n is_orchestrator: boolean;\n is_enabled: boolean;\n avatar_url?: string;\n code_projects_enabled?: boolean;\n mode?: \"chat\" | \"code\";\n }[]\n >(\"/v1/agents\");\n return raw.map((a) => camelizeKeys<AgentInfo>(a as unknown as Record<string, unknown>));\n }\n\n /**\n * List the models the caller may pick this turn — expanded from the curated\n * catalog of the providers the team has connected (client-side model\n * selection). Backs the composer's model picker. Scoped to the active agent\n * via X-Agent-ID, same as {@link getAgentStatus}.\n */\n async getModels(): Promise<ModelOption[]> {\n const raw = await this.get<Record<string, unknown>[]>(\"/v1/models\");\n // camelizeKeys, not a hand-written map: the previous version listed the\n // fields it knew and silently dropped the rest, so `effort_levels` was\n // emitted by the server for months and never reached a caller. Structural\n // mapping means the next field the API adds arrives on its own.\n //\n // Shallow is correct here rather than a limitation. `thinkingControl`'s\n // nested keys (`ladder`, `id`, `label`, `default`) are all single words, so\n // there is nothing to convert inside it — and recursing would rewrite keys\n // inside arbitrary JSON payloads elsewhere in the SDK, which is a worse\n // failure than the one it would prevent. `client.test.ts` pins the key set.\n //\n // Fields whose absence carries no signal — iconUrl/lastUsedAt/useCount and\n // contextWindow — normalize to null, so \"no data\" is one value rather than\n // undefined-vs-null ambiguity for consumers. thinkingControl deliberately\n // does NOT: there, absence IS the signal, meaning \"no control to render\".\n // That split is the rule to apply to the next optional field added here.\n return raw.map((m) => {\n const option = camelizeKeys<ModelOption>(m);\n return {\n ...option,\n iconUrl: option.iconUrl ?? null,\n lastUsedAt: option.lastUsedAt ?? null,\n useCount: option.useCount ?? null,\n contextWindow: option.contextWindow ?? null,\n };\n });\n }\n\n async getSkills(): Promise<SkillInfo[]> {\n const raw = await this.get<\n {\n name: string;\n display_name: string;\n description: string;\n is_enabled: boolean;\n }[]\n >(\"/v1/skills\");\n return raw.map((s) => camelizeKeys<SkillInfo>(s as unknown as Record<string, unknown>));\n }\n\n async getConversationEvents(\n conversationId: string,\n jobId?: string,\n ): Promise<ConversationEvent[]> {\n let url = `/v1/conversations/${encodeURIComponent(conversationId)}/events`;\n if (jobId) url += `?job_id=${encodeURIComponent(jobId)}`;\n return this.get(url);\n }\n\n async submitToolResult(request: ToolResultRequest): Promise<void> {\n await this.post(\"/v1/tool-result\", request);\n }\n\n async submitToolApproval(request: ToolApprovalRequest): Promise<void> {\n await this.post(\"/v1/tool-approval\", request);\n }\n\n // --- End-user tool-permission self-service ---\n\n /**\n * List the current end user's own remembered tool-permission grants.\n * Only `conversation`/`always` grants exist (`once` is never persisted).\n * Paginated via `limit` (default 100, max 200) / `offset`; `total` lets you\n * page through all of them.\n */\n async getMyToolPermissions(options?: {\n limit?: number;\n offset?: number;\n }): Promise<MyToolGrantsPage> {\n const params = new URLSearchParams();\n if (options?.limit != null) {\n const safeLimit = Math.max(\n 1,\n Math.min(200, Math.floor(Number(options.limit))),\n );\n params.set(\"limit\", String(safeLimit));\n }\n if (options?.offset != null) {\n const safeOffset = Math.max(0, Math.floor(Number(options.offset)));\n params.set(\"offset\", String(safeOffset));\n }\n const qs = params.toString();\n const raw = await this.get<{\n grants: {\n id: string;\n tool_name: string;\n decision: \"allow\" | \"deny\";\n scope: \"conversation\" | \"always\";\n conversation_id: string | null;\n created_at: string;\n }[];\n total: number;\n limit: number;\n offset: number;\n }>(`/v1/me/tool-permissions${qs ? `?${qs}` : \"\"}`);\n return {\n grants: raw.grants.map((g) => ({\n id: g.id,\n toolName: g.tool_name,\n decision: g.decision,\n scope: g.scope,\n conversationId: g.conversation_id,\n createdAt: g.created_at,\n })),\n total: raw.total,\n limit: raw.limit,\n offset: raw.offset,\n };\n }\n\n /**\n * Revoke one of the current end user's remembered grants by id. The agent\n * will ask again the next time that tool is used.\n */\n async revokeToolPermission(id: string): Promise<void> {\n await this.del(`/v1/me/tool-permissions/${encodeURIComponent(id)}`);\n }\n\n // --- Conversation Assets ---\n\n private mapAsset(raw: Record<string, unknown>): ConversationAsset {\n return {\n id: raw.id as string,\n kind: raw.kind as \"upload\" | \"output\",\n originalName: raw.original_name as string,\n mediaType: raw.media_type as string,\n sizeBytes: raw.size_bytes as number,\n workspacePath: raw.workspace_path as string | undefined,\n sourceMessageId: raw.source_message_id as string | undefined,\n agentName: raw.agent_name as string | undefined,\n // The API serializes an unsigned asset as `url: null`; normalize to\n // undefined so it matches the `url?: string` type and consumers that\n // check `!== undefined` never receive a null.\n url: (raw.url as string | null) ?? undefined,\n // This mapping is an ALLOWLIST — a field the API returns and this\n // function does not name is dropped silently, and no type error says so.\n // `content_url` shipped that way and was invisible to every consumer.\n contentUrl: (raw.content_url as string | null) ?? undefined,\n posterUrl: (raw.poster_url as string | null) ?? undefined,\n createdAt: raw.created_at as string,\n };\n }\n\n async uploadFile(\n conversationId: string,\n file: Blob,\n filename?: string,\n ): Promise<ConversationAsset> {\n const formData = new FormData();\n formData.append(\"file\", file, filename);\n\n const response = await this.fetchFn(\n `${this.baseURL}/v1/conversations/${encodeURIComponent(conversationId)}/uploads`,\n {\n method: \"POST\",\n headers: this.authHeaders,\n body: formData,\n },\n ).catch((err) => {\n throw new ConnectionError(\n err instanceof Error ? err.message : \"Failed to connect\",\n );\n });\n await this.handleError(response);\n const raw = await response.json();\n return this.mapAsset(raw as Record<string, unknown>);\n }\n\n // --- Voice input ---\n\n /** The agent's voice-input defaults (`GET /v1/voice/config`). */\n async getVoiceConfig(): Promise<VoiceConfig> {\n const raw = await this.get<Record<string, unknown>>(\"/v1/voice/config\");\n return {\n enabled: Boolean(raw.enabled),\n modes: (raw.modes as string[] | undefined) ?? [...VOICE_POLISH_MODES],\n // A mode this SDK does not know must not reach a `switch` typed as\n // `VoicePolishMode`; `structured` is the server's own default.\n defaultMode: isVoicePolishMode(raw.default_mode) ? raw.default_mode : \"structured\",\n silenceAutoStopSeconds:\n (raw.silence_auto_stop_seconds as number | undefined) ?? 2,\n autoSend: (raw.auto_send as boolean | undefined) ?? true,\n maxRecordingSeconds: (raw.max_recording_seconds as number | undefined) ?? 300,\n supportsStreaming: Boolean(raw.supports_streaming),\n hotwords: (raw.hotwords as string[] | undefined) ?? [],\n };\n }\n\n /**\n * Transcribe one recording with the agent's configured speech-to-text\n * provider (`POST /v1/voice/transcriptions`). 16 kHz mono 16-bit WAV is the\n * reference format; anything the provider accepts works.\n *\n * Deliberately outside `withDeadline`: a recording can run to\n * `VoiceConfig.maxRecordingSeconds`, so the 30 s default would cut real\n * uploads off. Pass `options.signal` to give up on a stalled one; the\n * promise then rejects with the abort reason — the runtime's `AbortError`,\n * or whatever was passed to `abort(reason)`.\n */\n async transcribeVoice(\n audio: Blob,\n options: VoiceTranscribeOptions = {},\n ): Promise<VoiceTranscript> {\n const formData = new FormData();\n formData.append(\"file\", audio, options.filename ?? \"recording.wav\");\n if (options.hotwords?.length) {\n // The form field is a comma-separated string — the server splits it on\n // commas (and newlines), so this is the wire format, not a choice made\n // here. A word containing a comma arrives as two.\n formData.append(\"hotwords\", options.hotwords.join(\", \"));\n }\n if (options.language) {\n formData.append(\"language\", options.language);\n }\n const response = await this.fetchFn(`${this.baseURL}/v1/voice/transcriptions`, {\n method: \"POST\",\n headers: this.authHeaders,\n body: formData,\n signal: options.signal,\n }).catch((err) => {\n // Any abort is the caller's, not a failure — including `abort(reason)`,\n // which rejects with the caller's own error rather than an AbortError.\n if (options.signal?.aborted) {\n throw err;\n }\n throw new ConnectionError(\n err instanceof Error ? err.message : \"Failed to connect\",\n );\n });\n await this.handleError(response);\n const raw = (await response.json()) as Record<string, unknown>;\n return {\n text: (raw.text as string | undefined) ?? \"\",\n language: (raw.language as string | null | undefined) ?? null,\n durationMs: (raw.duration_ms as number | null | undefined) ?? null,\n asrMs: (raw.asr_ms as number | undefined) ?? 0,\n };\n }\n\n /**\n * Stream the LLM rewrite of a transcript (`POST /v1/voice/polish`) as typed\n * frames.\n *\n * Failures the server reports mid-stream arrive as an `error` frame, but\n * the iteration itself can reject: aborting `signal` closes the connection\n * (which cancels the model call upstream) and rejects with\n * `StreamAbortedError`; a non-2xx open rejects with `AuthenticationError`,\n * `RateLimitError` or `ServerError`; a network failure with\n * `ConnectionError`. Wrap the `for await` accordingly.\n */\n async *streamVoicePolish(\n request: VoicePolishRequest,\n options: { signal?: AbortSignal } = {},\n ): AsyncGenerator<VoicePolishEvent> {\n const frames = streamJobSSE({\n url: `${this.baseURL}/v1/voice/polish`,\n headers: { ...this.headers, Accept: \"text/event-stream\" },\n method: \"POST\",\n body: JSON.stringify({\n text: request.text,\n mode: request.mode,\n hotwords: request.hotwords ?? [],\n }),\n signal: options.signal,\n fetchFn: this.fetchFn,\n });\n for await (const frame of frames) {\n const event = parseVoicePolishFrame(frame);\n if (event) yield event;\n }\n }\n\n async listUploads(conversationId: string): Promise<ConversationAsset[]> {\n const raw = await this.get<Record<string, unknown>[]>(\n `/v1/conversations/${encodeURIComponent(conversationId)}/uploads`,\n );\n return raw.map((r) => this.mapAsset(r));\n }\n\n async listOutputs(conversationId: string): Promise<ConversationAsset[]> {\n const raw = await this.get<Record<string, unknown>[]>(\n `/v1/conversations/${encodeURIComponent(conversationId)}/outputs`,\n );\n return raw.map((r) => this.mapAsset(r));\n }\n\n // --- Account-scoped discovery (user-token mode) ---\n //\n // Lets a signed-in user pick which team/agent they want to act on.\n // Backend gates these on OIDC user context (no X-Agent-ID required) —\n // sending them in API-key mode yields 401.\n\n async listTeams(): Promise<TeamSummary[]> {\n const raw = await this.get<\n Array<{\n id: string;\n name: string;\n slug: string;\n is_default: boolean;\n role: string;\n }>\n >(\"/v1/teams\");\n return raw.map((t) => camelizeKeys<TeamSummary>(t as unknown as Record<string, unknown>));\n }\n\n /**\n * List the team-level agents (formerly \"projects\") the signed-in user can\n * open — the pickable workspaces under a team. Not to be confused with\n * `getAgents()`, which lists the AI personas inside the active agent.\n */\n async listAgents(teamId: string): Promise<TeamAgentSummary[]> {\n const raw = await this.get<\n Array<{\n id: string;\n name: string;\n display_name?: string | null;\n team_id: string;\n created_at: string;\n updated_at: string;\n avatar_url?: string | null;\n }>\n >(`/v1/teams/${encodeURIComponent(teamId)}/agents`);\n return raw.map((a) => camelizeKeys<TeamAgentSummary>(a as unknown as Record<string, unknown>));\n }\n\n // --- Projects: the repositories this app user works with ---\n\n /**\n * The projects (GitHub repositories) this app user works with, and what they\n * may add.\n *\n * A project list is per app user: the developer connects the workspace's GitHub\n * account, and each user curates their own list from what that connection\n * covers. There is no agent-level gate — an agent with no GitHub lists nothing,\n * reports `not_installed` from `available()`, and refuses `add()`. Read\n * {@link AgentInfo.codeProjectsEnabled} to decide whether to show the surface.\n */\n readonly code = {\n projects: {\n /** This user's projects on the active agent, oldest first. */\n list: async (): Promise<CodeProject[]> => {\n const raw = await this.get<{ repo_full_name: string; added_at: string }[]>(\n \"/v1/code/projects\",\n );\n return raw.map((p) => camelizeKeys<CodeProject>(p as unknown as Record<string, unknown>));\n },\n\n /**\n * What the workspace's GitHub installations cover, minus what this user\n * has already added. Read `state` before the list: an empty `repositories`\n * means something different in each of its three values.\n */\n available: async (): Promise<AvailableRepositories> => {\n const raw = await this.get<{\n state: \"ok\" | \"unavailable\" | \"not_installed\";\n repositories: { full_name: string; private: boolean }[];\n total_count: number;\n partial: boolean;\n }>(\"/v1/code/projects/available\");\n return {\n state: raw.state,\n repositories: (raw.repositories ?? []).map((r) => ({\n fullName: r.full_name,\n private: r.private,\n })),\n // Defaulted like its neighbours: the `unavailable` branch has nothing\n // to count, and an absent field behind a `number` type prints\n // \"undefined\" in a picker rather than a number.\n totalCount: raw.total_count ?? 0,\n partial: raw.partial ?? false,\n };\n },\n\n /**\n * Add a repository. The server checks it against the workspace's own\n * installations and answers a repository it cannot reach the same way it\n * answers one owned by someone else — deliberately, so this call cannot be\n * used to discover which organisations use Astralform.\n */\n add: async (repoFullName: string): Promise<CodeProject> => {\n const raw = await this.post<{ repo_full_name: string; added_at: string }>(\n \"/v1/code/projects\",\n { repo_full_name: repoFullName },\n );\n return camelizeKeys<CodeProject>(raw as unknown as Record<string, unknown>);\n },\n\n /**\n * Remove a project. Tasks already bound to that repository keep their\n * binding — they simply stop grouping under it.\n */\n remove: async (owner: string, repo: string): Promise<void> => {\n await this.del(\n `/v1/code/projects/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,\n );\n },\n },\n };\n\n // --- Jobs API ---\n\n async createJob(request: ChatStreamRequest): Promise<JobCreateResponse> {\n return this.post<JobCreateResponse>(\"/v1/jobs\", request);\n }\n\n async *streamJobEvents(\n jobId: string,\n afterSeq = -1,\n signal?: AbortSignal,\n ): AsyncGenerator<ChatStreamEvent> {\n const url = `${this.baseURL}/v1/jobs/${encodeURIComponent(jobId)}/events?after=${afterSeq}`;\n yield* streamJobSSE({\n url,\n headers: this.headers,\n signal,\n fetchFn: this.fetchFn,\n });\n }\n\n async cancelJob(jobId: string): Promise<void> {\n await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/cancel`, {});\n }\n\n async getJob(jobId: string): Promise<JobStatus> {\n const raw = await this.get<{\n job_id: string;\n status: string;\n created_at?: string | null;\n started_at?: string | null;\n completed_at?: string | null;\n error_message?: string | null;\n input_tokens?: number;\n output_tokens?: number;\n }>(`/v1/jobs/${encodeURIComponent(jobId)}`);\n return {\n jobId: raw.job_id,\n status: raw.status,\n createdAt: raw.created_at ?? null,\n startedAt: raw.started_at ?? null,\n completedAt: raw.completed_at ?? null,\n errorMessage: raw.error_message ?? null,\n inputTokens: raw.input_tokens ?? 0,\n outputTokens: raw.output_tokens ?? 0,\n };\n }\n\n async submitFeedback(\n jobId: string,\n request: FeedbackRequest,\n ): Promise<FeedbackResponse> {\n const body: { rating: 1 | -1; comment?: string } = {\n rating: request.rating,\n };\n if (request.comment != null) body.comment = request.comment;\n const raw = await this.post<{\n id: string;\n job_id: string;\n rating: number;\n comment: string | null;\n created_at: string;\n }>(`/v1/jobs/${encodeURIComponent(jobId)}/feedback`, body);\n return camelizeKeys<FeedbackResponse>(raw as unknown as Record<string, unknown>);\n }\n\n async getActiveJob(conversationId: string): Promise<ActiveJob> {\n const raw = await this.get<{\n job_id: string | null;\n status: string;\n }>(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);\n return {\n jobId: raw.job_id ?? null,\n status: raw.status,\n };\n }\n\n async listJobs(conversationId: string): Promise<JobSummary[]> {\n const raw = await this.get<\n {\n job_id: string;\n status: string;\n replaces_job_id?: string | null;\n response_content?: Record<string, unknown> | null;\n metrics?: Record<string, unknown> | null;\n created_at?: string | null;\n }[]\n >(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);\n return raw.map((j) => ({\n jobId: j.job_id,\n status: j.status,\n replacesJobId: j.replaces_job_id ?? null,\n responseContent: j.response_content ?? null,\n metrics: j.metrics ?? null,\n createdAt: j.created_at ?? null,\n }));\n }\n}\n\n/**\n * Decode one SSE frame of `POST /v1/voice/polish`; null for frames the\n * client does not act on (pings, unknown events).\n */\nexport function parseVoicePolishFrame(frame: {\n event: string;\n data: string;\n}): VoicePolishEvent | null {\n let payload: Record<string, unknown>;\n try {\n const parsed: unknown = JSON.parse(frame.data);\n // `null`, `true`, `42` and `[]` all parse; only a plain object carries\n // the fields read below (an array would synthesize an `error` event), and\n // a throw here would end the whole polish generator.\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return null;\n payload = parsed as Record<string, unknown>;\n } catch {\n return null;\n }\n switch (frame.event) {\n case \"delta\":\n return typeof payload.text === \"string\"\n ? { type: \"delta\", text: payload.text }\n : null;\n case \"done\":\n return typeof payload.text === \"string\"\n ? {\n type: \"done\",\n text: payload.text,\n polishMs: (payload.polish_ms as number | undefined) ?? 0,\n }\n : null;\n case \"error\":\n return {\n type: \"error\",\n reason: (payload.reason as string | undefined) ?? \"unknown\",\n partial: (payload.partial as string | undefined) ?? \"\",\n ...(typeof payload.detail === \"string\" ? { detail: payload.detail } : {}),\n };\n default:\n return null;\n }\n}\n","import type { Conversation, Message } from \"./types.js\";\n\nexport interface ChatStorage {\n fetchConversations(): Promise<Conversation[]>;\n fetchConversation(id: string): Promise<Conversation | null>;\n createConversation(id: string, title: string): Promise<Conversation>;\n updateConversationTitle(id: string, title: string): Promise<void>;\n deleteConversation(id: string): Promise<void>;\n fetchMessages(conversationId: string): Promise<Message[]>;\n addMessage(message: Message, conversationId: string): Promise<void>;\n updateMessageStatus(id: string, status: Message[\"status\"]): Promise<void>;\n deleteMessage(id: string): Promise<void>;\n}\n\nexport class InMemoryStorage implements ChatStorage {\n private conversations = new Map<string, Conversation>();\n private messages = new Map<string, Message[]>();\n\n async fetchConversations(): Promise<Conversation[]> {\n return Array.from(this.conversations.values()).sort(\n (a, b) =>\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),\n );\n }\n\n async fetchConversation(id: string): Promise<Conversation | null> {\n return this.conversations.get(id) ?? null;\n }\n\n async createConversation(id: string, title: string): Promise<Conversation> {\n const now = new Date().toISOString();\n const conversation: Conversation = {\n id,\n title,\n messageCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n this.conversations.set(id, conversation);\n this.messages.set(id, []);\n return conversation;\n }\n\n async updateConversationTitle(id: string, title: string): Promise<void> {\n const conv = this.conversations.get(id);\n if (conv) {\n conv.title = title;\n conv.updatedAt = new Date().toISOString();\n }\n }\n\n async deleteConversation(id: string): Promise<void> {\n this.conversations.delete(id);\n this.messages.delete(id);\n }\n\n async fetchMessages(conversationId: string): Promise<Message[]> {\n return this.messages.get(conversationId) ?? [];\n }\n\n async addMessage(message: Message, conversationId: string): Promise<void> {\n const msgs = this.messages.get(conversationId) ?? [];\n msgs.push(message);\n this.messages.set(conversationId, msgs);\n\n const conv = this.conversations.get(conversationId);\n if (conv) {\n conv.messageCount = msgs.length;\n conv.updatedAt = new Date().toISOString();\n }\n }\n\n async updateMessageStatus(\n id: string,\n status: Message[\"status\"],\n ): Promise<void> {\n for (const msgs of this.messages.values()) {\n const msg = msgs.find((m) => m.id === id);\n if (msg) {\n msg.status = status;\n return;\n }\n }\n }\n\n async deleteMessage(id: string): Promise<void> {\n for (const [convId, msgs] of this.messages.entries()) {\n const idx = msgs.findIndex((m) => m.id === id);\n if (idx !== -1) {\n msgs.splice(idx, 1);\n const conv = this.conversations.get(convId);\n if (conv) {\n conv.messageCount = msgs.length;\n }\n return;\n }\n }\n }\n}\n","import type { ToolCallRequest, ToolDefinition, ToolResult } from \"./types.js\";\n\nexport type ToolHandler = (args: Record<string, unknown>) => Promise<string>;\n\ninterface RegisteredTool {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n handler: ToolHandler;\n}\n\n/** Validates tool name: alphanumeric, hyphens, underscores, dots only */\nconst TOOL_NAME_PATTERN = /^[a-zA-Z0-9_.\\-]+$/;\n\n/** Strips prototype-polluting keys from an object */\nfunction sanitizeArgs(args: Record<string, unknown>): Record<string, unknown> {\n const clean: Record<string, unknown> = Object.create(null);\n for (const key of Object.keys(args)) {\n if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") {\n continue;\n }\n clean[key] = args[key];\n }\n return clean;\n}\n\nexport class ToolRegistry {\n private tools = new Map<string, RegisteredTool>();\n\n registerTool(\n name: string,\n description: string,\n inputSchema: Record<string, unknown>,\n handler: ToolHandler,\n ): void {\n if (!name || !TOOL_NAME_PATTERN.test(name)) {\n throw new Error(\n `Invalid tool name \"${name}\" - must match ${TOOL_NAME_PATTERN}`,\n );\n }\n if (name.length > 256) {\n throw new Error(\"Tool name must be 256 characters or fewer\");\n }\n this.tools.set(name, { name, description, inputSchema, handler });\n }\n\n unregisterTool(name: string): boolean {\n return this.tools.delete(name);\n }\n\n hasTool(name: string): boolean {\n return this.tools.has(name);\n }\n\n async executeTool(request: ToolCallRequest): Promise<ToolResult> {\n const tool = this.tools.get(request.toolName);\n if (!tool) {\n return {\n call_id: request.callId,\n tool_name: request.toolName,\n result: `Tool \"${request.toolName}\" not found`,\n is_error: true,\n };\n }\n\n try {\n const result = await tool.handler(sanitizeArgs(request.arguments));\n return {\n call_id: request.callId,\n tool_name: request.toolName,\n result,\n is_error: false,\n };\n } catch (err) {\n return {\n call_id: request.callId,\n tool_name: request.toolName,\n result: err instanceof Error ? err.message : String(err),\n is_error: true,\n };\n }\n }\n\n getManifest(): ToolDefinition[] {\n return Array.from(this.tools.values()).map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.inputSchema,\n }));\n }\n\n getToolNames(): string[] {\n return Array.from(this.tools.keys());\n }\n\n clear(): void {\n this.tools.clear();\n }\n}\n","// =============================================================================\n// Protocol adapter registry — pluggable UI protocol layer\n// =============================================================================\n//\n// The SDK is framework-agnostic: it never renders. But when the backend\n// emits an MCP-style embedded resource (A2UI today, other protocols\n// tomorrow) the consumer needs to hand the payload to a renderer. This\n// file defines the contract between those two sides:\n//\n// • `ProtocolAdapter` — an opaque, framework-specific handle that\n// claims a MIME type. The SDK stores it; the consumer narrows the\n// type when reading it back out.\n//\n// • `ProtocolRegistry` — a MIME-keyed map of adapters. One lives on\n// each `ChatSession` so its lifecycle matches the session: clearing\n// on disconnect, swapping on reconnect with a different agent.\n//\n// The consumer decides _whether_ to register an adapter — typically by\n// consulting `session.agentStatus.uiComponents` after `connect()`.\n// The SDK never auto-registers anything; it just stores what it's told.\n// =============================================================================\n\n/**\n * Minimal adapter contract. Frontends extend this with a `render()`\n * method (or equivalent) returning their framework's view type.\n */\nexport interface ProtocolAdapter {\n /** IANA-style MIME type this adapter handles (e.g. ``application/json+a2ui``). */\n readonly mimeType: string;\n}\n\n/**\n * MIME-keyed adapter map, generic on the adapter subtype so consumers\n * can register richer shapes without casting on every read.\n */\nexport class ProtocolRegistry<T extends ProtocolAdapter = ProtocolAdapter> {\n private adapters = new Map<string, T>();\n\n /** Register or replace the adapter for a MIME type. */\n register(adapter: T): void {\n this.adapters.set(adapter.mimeType, adapter);\n }\n\n /** Remove the adapter for a MIME type. No-op if not registered. */\n unregister(mimeType: string): void {\n this.adapters.delete(mimeType);\n }\n\n /** Returns the adapter for a MIME type, or ``null`` if none is registered. */\n get(mimeType: string): T | null {\n return this.adapters.get(mimeType) ?? null;\n }\n\n has(mimeType: string): boolean {\n return this.adapters.has(mimeType);\n }\n\n /** Drop every adapter. Called when a session disconnects. */\n clear(): void {\n this.adapters.clear();\n }\n\n listMimeTypes(): string[] {\n return Array.from(this.adapters.keys());\n }\n}\n","// =============================================================================\n// Wire → ChatEvent translation\n//\n// Single source of truth for the pure translation from backend wire types\n// (snake_case, mirroring Pydantic) to consumer-facing ChatEvents (camelCase).\n// Shared between the live session loop and the persisted-event replay path,\n// so adding a new event type only requires updating one place.\n// =============================================================================\n\nimport type {\n BlockDeltaPayload,\n ChatEvent,\n WireBlockDeltaPayload,\n WireEvent,\n} from \"./types.js\";\nimport type { MemoryRecord, TodoItem } from \"./custom-events.js\";\n\n// --- Delta channels ---\n\n/**\n * Translate a WireBlockDeltaPayload into the consumer-facing shape. Returns\n * ``null`` for unknown channels so forward-compatible backends can add new\n * ones without breaking old clients.\n */\nexport function translateDelta(\n wire: WireBlockDeltaPayload,\n): BlockDeltaPayload | null {\n switch (wire.channel) {\n case \"text\":\n return { channel: \"text\", text: wire.text };\n case \"thinking\":\n return { channel: \"thinking\", text: wire.text };\n case \"signature\":\n return { channel: \"signature\", signature: wire.signature };\n case \"input\":\n return { channel: \"input\", partialJson: wire.partial_json };\n case \"input_arg\":\n return {\n channel: \"inputArg\",\n argName: wire.arg_name,\n text: wire.text,\n };\n case \"output\":\n return { channel: \"output\", stream: wire.stream, chunk: wire.chunk };\n case \"status\":\n return {\n channel: \"status\",\n status: wire.status,\n note: wire.note,\n };\n default:\n return null;\n }\n}\n\n// --- Custom events ---\n\nfunction translateAgentIdentity(raw: Record<string, unknown>) {\n return {\n name: (raw.name as string) ?? \"\",\n displayName: (raw.display_name as string | null) ?? null,\n avatarUrl: (raw.avatar_url as string | null) ?? null,\n description: (raw.description as string | null) ?? null,\n };\n}\n\n/**\n * Translate a wire TodoItem (snake_case — `active_form`, `blocked_by`,\n * mirroring `backend/src/stream/protocol.py`) into the consumer-facing\n * camelCase shape. The backend emits todos exactly as the payload catalog\n * defines them, so the translation is the single place the casing is fixed;\n * consumers can rely on `todo.activeForm` / `todo.blockedBy` matching the\n * declared types.\n */\nfunction translateTodoItem(raw: Record<string, unknown>): TodoItem {\n return {\n id: (raw.id as number) ?? 0,\n subject: (raw.subject as string) ?? \"\",\n status: (raw.status as TodoItem[\"status\"]) ?? \"pending\",\n description: (raw.description as string | null) ?? null,\n activeForm: (raw.active_form as string | null) ?? null,\n owner: (raw.owner as string | null) ?? null,\n blockedBy: (raw.blocked_by as number[] | null) ?? null,\n blocks: (raw.blocks as number[] | null) ?? null,\n priority: (raw.priority as number | null) ?? null,\n };\n}\n\n/**\n * Translate a wire custom event (``{type: \"custom\", name, data}``) into a\n * typed ChatEvent. Unknown names fall through to the generic ``custom``\n * passthrough so consumers can still observe future backends.\n */\nexport function translateCustomEvent(\n name: string,\n data: Record<string, unknown>,\n): ChatEvent {\n switch (name) {\n case \"user_message\":\n return {\n type: \"user_message\",\n content: (data.content as string) ?? \"\",\n createdAt: data.created_at as number | undefined,\n };\n case \"title_generated\":\n return {\n type: \"title_generated\",\n title: (data.title as string) ?? \"\",\n };\n case \"todo_update\":\n return {\n type: \"todo_update\",\n todos: ((data.todos as unknown[]) ?? []).map((t) =>\n translateTodoItem(t as Record<string, unknown>),\n ),\n };\n case \"plan_update\":\n return {\n type: \"plan_update\",\n plan: (data.plan as string) ?? \"\",\n };\n case \"note_update\":\n return {\n type: \"note_update\",\n notes: (data.notes as string[]) ?? [],\n };\n case \"context_update\":\n return {\n type: \"context_update\",\n context: (data.context as Record<string, unknown>) ?? {},\n phase: (data.phase as string | null) ?? null,\n updatedAt: (data.updated_at as number | null) ?? null,\n };\n case \"subagent_start\":\n return {\n type: \"subagent_start\",\n agent: translateAgentIdentity(\n (data.agent as Record<string, unknown>) ?? {},\n ),\n taskCallId: (data.task_call_id as string | null) ?? null,\n };\n case \"subagent_stop\":\n return {\n type: \"subagent_stop\",\n agent: translateAgentIdentity(\n (data.agent as Record<string, unknown>) ?? {},\n ),\n taskCallId: (data.task_call_id as string | null) ?? null,\n };\n case \"context_warning\":\n return {\n type: \"context_warning\",\n severity: (data.severity as string) ?? \"warning\",\n utilizationPct: (data.utilization_pct as number) ?? 0,\n remainingTokens: (data.remaining_tokens as number) ?? 0,\n windowTokens: (data.window_tokens as number) ?? 0,\n inputTokens: (data.input_tokens as number) ?? 0,\n message: (data.message as string) ?? \"\",\n };\n case \"memory_recall\":\n return {\n type: \"memory_recall\",\n memories: (data.memories as MemoryRecord[] | undefined) ?? [],\n };\n case \"memory_update\":\n return {\n type: \"memory_update\",\n action: (data.action as string) ?? \"\",\n memoryId: (data.memory_id as string | null) ?? null,\n key: (data.key as string | null) ?? null,\n namespace: (data.namespace as string | null) ?? null,\n };\n case \"desktop_stream\":\n return {\n type: \"desktop_stream\",\n url: (data.url as string) ?? \"\",\n sandboxId: (data.sandbox_id as string | null) ?? null,\n };\n case \"attachment_staged\":\n return {\n type: \"attachment_staged\",\n attachmentId: (data.attachment_id as string) ?? \"\",\n filename: (data.filename as string) ?? \"\",\n contentType: (data.content_type as string | null) ?? null,\n sizeBytes: (data.size_bytes as number | null) ?? null,\n };\n case \"workspace_ready\":\n return {\n type: \"workspace_ready\",\n sandboxId: (data.sandbox_id as string) ?? \"\",\n workspacePath: (data.workspace_path as string | null) ?? null,\n };\n case \"asset_created\":\n return {\n type: \"asset_created\",\n assetId: (data.asset_id as string) ?? \"\",\n filename: (data.filename as string) ?? \"\",\n url: (data.url as string | null) ?? null,\n contentType: (data.content_type as string | null) ?? null,\n };\n case \"tool_approval_requested\":\n return {\n type: \"tool_approval_requested\",\n toolName: (data.tool_name as string) ?? \"\",\n callId: (data.call_id as string) ?? \"\",\n arguments: (data.arguments as Record<string, unknown>) ?? {},\n riskLevel: (data.risk_level as string | null) ?? null,\n reason: (data.reason as string | null) ?? null,\n };\n case \"tool_approval_granted\":\n return {\n type: \"tool_approval_granted\",\n toolName: (data.tool_name as string) ?? \"\",\n callId: (data.call_id as string) ?? \"\",\n };\n case \"tool_permission_denied\":\n return {\n type: \"tool_permission_denied\",\n toolName: (data.tool_name as string) ?? \"\",\n callId: (data.call_id as string) ?? \"\",\n reason: (data.reason as string | null) ?? null,\n deniedBy: (data.denied_by as string | null) ?? null,\n };\n case \"tool_harness_warning\":\n return {\n type: \"tool_harness_warning\",\n toolName: (data.tool_name as string) ?? \"\",\n callId: (data.call_id as string) ?? \"\",\n message: (data.message as string | null) ?? null,\n details: (data.details as Record<string, unknown> | null) ?? null,\n };\n case \"user_unavailable\":\n return {\n type: \"user_unavailable\",\n consecutiveTimeouts: (data.consecutive_timeouts as number) ?? 0,\n toolName: (data.tool_name as string | null) ?? null,\n };\n case \"prompt_suggestion\":\n return {\n type: \"prompt_suggestion\",\n suggestions: (data.suggestions as string[]) ?? [],\n };\n case \"state_changed\":\n return {\n type: \"state_changed\",\n state: (data.state as string) ?? \"\",\n };\n default:\n return { type: \"custom\", name, data };\n }\n}\n\n// --- Top-level wire events ---\n\n/**\n * Legacy-transport payload extractor for events that aren't wrapped in a\n * ``CustomEvent`` envelope. The backend's ``writer.emit(name, data)`` path\n * spreads the payload fields at the top level of the wire object (see\n * ``backend/src/jobs/suggestion_hook.py`` for ``prompt_suggestion``), so\n * the wire object itself *is* the ``data`` dict for ``translateCustomEvent``.\n * This helper isolates the unsafe cast to one place.\n */\nfunction legacyCustomEventData(wire: WireEvent): Record<string, unknown> {\n return wire as unknown as Record<string, unknown>;\n}\n\n/**\n * Translate a full WireEvent into its typed ChatEvent counterpart. Returns\n * ``null`` when the wire payload is malformed (e.g. unknown delta channel)\n * so the caller can skip it without crashing the stream.\n */\nexport function translateWireEvent(wire: WireEvent): ChatEvent | null {\n // Legacy transport: ``prompt_suggestion`` is emitted via the raw\n // ``writer.emit()`` path rather than wrapped in a CustomEvent envelope,\n // so its ``type`` field is the event name itself. Route it through the\n // custom-event translator to keep the mapping in one place.\n if ((wire as { type: string }).type === \"prompt_suggestion\") {\n return translateCustomEvent(\n \"prompt_suggestion\",\n legacyCustomEventData(wire),\n );\n }\n switch (wire.type) {\n case \"message_start\":\n return {\n type: \"message_start\",\n turnId: wire.turn_id,\n model: wire.model,\n agentName: wire.agent_name,\n agentDisplayName: wire.agent_display_name,\n agentAvatarUrl: wire.agent_avatar_url,\n };\n case \"block_start\":\n return {\n type: \"block_start\",\n turnId: wire.turn_id,\n path: wire.path,\n parentPath: wire.parent_path ?? null,\n kind: wire.kind,\n metadata: wire.metadata,\n };\n case \"block_delta\": {\n const delta = translateDelta(wire.delta);\n if (!delta) return null;\n return {\n type: \"block_delta\",\n turnId: wire.turn_id,\n path: wire.path,\n delta,\n };\n }\n case \"block_stop\":\n return {\n type: \"block_stop\",\n turnId: wire.turn_id,\n path: wire.path,\n status: wire.status,\n final: wire.final,\n };\n case \"message_stop\":\n return {\n type: \"message_stop\",\n turnId: wire.turn_id,\n jobId: wire.job_id,\n stopReason: wire.stop_reason,\n usage: {\n inputTokens: wire.usage.input_tokens ?? 0,\n outputTokens: wire.usage.output_tokens ?? 0,\n cachedTokens: wire.usage.cached_tokens ?? 0,\n cacheCreationTokens: wire.usage.cache_creation_tokens ?? 0,\n },\n ttfbMs: wire.ttfb_ms,\n totalMs: wire.total_ms,\n stallCount: wire.stall_count,\n };\n case \"stall\":\n return {\n type: \"stall\",\n sinceLastEventMs: wire.since_last_event_ms,\n stallCount: wire.stall_count,\n };\n case \"retry\":\n return {\n type: \"retry\",\n attempt: wire.attempt,\n reason: wire.reason,\n backoffMs: wire.backoff_ms,\n strategy: wire.strategy ?? null,\n maxAttempts: wire.max_attempts ?? null,\n contextRecovery: wire.context_recovery ?? null,\n };\n case \"error\":\n return {\n type: \"error\",\n code: wire.code,\n message: wire.message,\n blockPath: wire.block_path ?? null,\n };\n case \"keepalive\":\n return {\n type: \"keepalive\",\n sinceLastEventMs: wire.since_last_event_ms,\n };\n case \"custom\":\n return translateCustomEvent(wire.name, wire.data);\n default: {\n // Exhaustive guard — a new WireEvent variant should force this switch\n // to be updated at compile time.\n const _exhaustive: never = wire;\n void _exhaustive;\n return null;\n }\n }\n}\n","import { AstralformClient } from \"./client.js\";\nimport { InMemoryStorage, type ChatStorage } from \"./storage.js\";\nimport { ToolRegistry } from \"./tools.js\";\nimport { ProtocolRegistry } from \"./protocol-registry.js\";\nimport { translateWireEvent } from \"./translate.js\";\nimport type {\n AgentInfo,\n AstralformConfig,\n ChatEvent,\n ChatStreamRequest,\n Conversation,\n ConversationEvent,\n Message,\n AgentStatus,\n SendOptions,\n SkillInfo,\n ToolCallRequest,\n ToolResult,\n WireEvent,\n} from \"./types.js\";\nimport { generateId } from \"./utils.js\";\nimport {\n AuthenticationError,\n ConnectionError,\n RateLimitError,\n ServerError,\n} from \"./errors.js\";\n\ntype ChatEventHandler = (event: ChatEvent) => void;\n\n/**\n * Bounded auto-reconnect for a live SSE stream that drops mid-turn (worker\n * restart, network blip). We resume from ``lastSeq`` — the backend replays\n * missed events (``?after=seq``) and, for a job that already died, back-fills a\n * terminal event — so the UI recovers without a manual page refresh. Backoff is\n * exponential and capped: the six sleeps sum to 17.5s (0.5+1+2+4+5+5), which\n * comfortably covers a server restart without spinning forever if the job is\n * genuinely gone.\n *\n * That 17.5s is the BACKOFF total, not the time to give up. An attempt that\n * fails fast costs ~nothing, but one that stalls burns SSE_STALL_TIMEOUT_MS\n * before it even registers as a failure — so with all 7 attempts stalling the\n * worst case is 7 * 135s + 17.5s ≈ 16 minutes. That ceiling is accepted, not\n * accidental: an all-stalls sequence needs every attempt to die the rare QUIC\n * way (fail-fast is the common failure), and shrinking the attempt budget to\n * hold the old ~5.5-minute bound would cost real resilience on flaky networks\n * to improve a pathological case. A stalled stream is indistinguishable from\n * a slow one until the watchdog fires, and cutting that shorter risks killing\n * healthy long-running turns.\n */\nconst SSE_MAX_RECONNECTS = 6;\n\n/**\n * Max silence tolerated on an established SSE stream before we declare it a\n * zombie and reconnect. The backend emits a keepalive on a fixed cadence\n * (``subscribe.py``), moving from every 15s to every 45s to cut mobile radio\n * wake-ups, so a healthy stream never goes quiet this long. This\n * matters because some failures (notably HTTP/3 / QUIC connection deaths)\n * leave ``reader.read()`` pending forever — no bytes, no error, no FIN — and\n * without a watchdog the retry loop below never engages and the UI hangs on\n * \"working\" indefinitely.\n *\n * DEPENDS ON THE KEEPALIVE'S FRAMING, not just its interval. The backend\n * sends it as a typed wire event (``{\"event\": \"keepalive\", \"data\": ...}``), so\n * it reaches ``streamJobSSE``'s parser as a real ``data:`` line and resets the\n * timer below. An SSE-protocol comment (``: keepalive``) would be silently\n * swallowed by that parser — it only reacts to ``event:``/``data:`` — and this\n * watchdog would then fire on every healthy turn that thinks this long. If\n * the backend ever changes that framing, this constant has to change with\n * it.\n *\n * 3x the PLANNED 45s keepalive cadence, not the current 15s one: 135s\n * tolerates both, so this client can ship ahead of the backend flip without\n * ever racing the keepalive it depends on.\n */\nconst SSE_STALL_TIMEOUT_MS = 135_000;\n\n// Retries for the client-tool result POST itself, independent of the SSE\n// reconnect loop — reconnecting the *stream* can't recover a failed *result\n// submission*, and retrying the POST avoids re-executing a client tool.\nconst TOOL_RESULT_MAX_RETRIES = 3;\n\n/**\n * Conversations fetched per page, by ``connect`` and ``loadMoreConversations``\n * alike. The two must use the same size: the offset is derived from how many\n * rows the server has returned so far, so a first page of a different size\n * would leave the second page's offset pointing at the wrong row.\n */\nexport const CONVERSATION_PAGE_SIZE = 50;\n\nfunction sseReconnectDelayMs(attempt: number): number {\n return Math.min(500 * 2 ** (attempt - 1), 5000);\n}\n\nfunction pathEquals(a: number[], b: number[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * ChatSession — translates the backend wire protocol into typed ChatEvents\n * for consumers. Owns HTTP + SSE plumbing, conversation state, and the\n * client-tool round-trip. Does NOT own block construction — consumers\n * build their own block state from the typed events.\n */\nexport class ChatSession {\n readonly client: AstralformClient;\n readonly toolRegistry: ToolRegistry;\n readonly storage: ChatStorage;\n /**\n * Pluggable UI protocol adapters. Consumers register a framework-\n * specific adapter (e.g. React) for each MIME type they can render,\n * typically gated on ``session.agentStatus.uiComponents.protocol``.\n * ``ToolBlock``-style consumers look up the adapter for an incoming\n * embedded resource and hand off rendering.\n */\n readonly protocols = new ProtocolRegistry();\n\n // State\n conversationId: string | null = null;\n conversations: Conversation[] = [];\n /**\n * Whether another page of conversations may exist on the server.\n *\n * Inferred from the last page being full, since the list endpoint returns a\n * bare array with no total. A total that happens to be an exact multiple of\n * the page size therefore costs one extra empty request before this flips —\n * cheaper than adding a count query to every list call.\n */\n hasMoreConversations = false;\n /** True while ``loadMoreConversations`` is in flight. */\n isLoadingConversations = false;\n messages: Message[] = [];\n /**\n * Which conversation ``messages`` currently holds.\n *\n * Distinct from ``conversationId``, and the distinction is the point:\n * ``loadConversation`` moves the POINTER synchronously and installs the LIST\n * an await later, so for the whole duration of every load the two disagree.\n * Anything pairing a message with a conversation — ``regenerate`` above all —\n * has to read this one, or it will pair the previous conversation's last\n * message with the new conversation's id.\n */\n messagesConversationId: string | null = null;\n isStreaming = false;\n agentStatus: AgentStatus | null = null;\n agents: AgentInfo[] = [];\n skills: SkillInfo[] = [];\n enabledClientTools = new Set<string>();\n modelDisplayName: string | null = null;\n\n /**\n * Ids of conversations the SERVER has handed us, which is the paging offset.\n *\n * Deliberately not ``conversations.length``. That array also holds\n * conversations created locally and unshifted on top (``createNewConversation``,\n * and the auto-created conversation in ``consumeJobStream``), so using its\n * length as the offset would over-count and silently skip a row of real\n * history on the next page. Tracking ids rather than a counter also makes\n * deletion self-correcting: removing a server-sourced conversation shifts\n * every later page up by one, and dropping its id from this set is exactly\n * that shift — while deleting a purely local one correctly changes nothing.\n */\n private serverConversationIds = new Set<string>();\n\n /**\n * Bumped every time ``connect()`` re-seeds the conversation list.\n *\n * A ``loadMoreConversations`` request issued before a re-seed describes the\n * OLD paging state, so applying its response afterwards both appends the\n * wrong rows and corrupts the offset. Concretely: with 100 rows held, an\n * offset-100 response landing after a reconnect has reset to rows 0-49 would\n * append rows 100-149 — a 50-row hole — and leave the id set at 100, so every\n * later page re-requests offset 100 and never advances again. The generation\n * is captured before the await and rechecked after, so a superseded response\n * is discarded instead.\n */\n private conversationsGeneration = 0;\n\n /**\n * Bumped by every ``loadConversation`` call, so an out-of-order fetch can\n * tell it is no longer the newest one and drop its result. Separate from\n * ``conversationsGeneration``, which guards the conversation LIST.\n */\n private loadGeneration = 0;\n\n /**\n * Ids of locally-created user messages the server has not acknowledged yet.\n *\n * Arrival time cannot answer \"could the reply have included this?\" on its\n * own. Every `loadConversation` in `StreamManager` sits behind the\n * active-job probe, so the ORDINARY ordering is that a send lands BEFORE the\n * load starts — and an arrival-time rule drops exactly those, losing the\n * prompt the user just sent while its stream is still running. Membership\n * here is set by `send` and cleared when a server row turns up carrying the\n * same turn, so the keep-decision no longer depends on which side of the\n * fetch the push landed on.\n *\n * The value is `serverRowsKnown` as of the `message_stop` that proved the\n * row committed, or 0 until then — including after the job response, which\n * hands back the id but starts the loop as a background task and so proves\n * nothing about the row. That stamp is what separates \"the snapshot predates\n * the row\" from \"the server does not have this row\" — see\n * `loadConversation`.\n *\n * It is an ANNOTATION ON `this.messages`: reconciliation only ever consults\n * entries of that array, so an id whose message has left it is dead weight.\n * `setMessages` is the single place the array is replaced, and it prunes.\n */\n private pendingUserMessages = new Map<string, number>();\n\n /**\n * Bumped once per completed turn, at `message_stop`, so a fetch can record\n * what was proven when it was ISSUED. A row proven committed before the\n * fetch went out must appear in its snapshot; one proven after may\n * legitimately be missing.\n */\n private serverRowsKnown = 0;\n\n /**\n * Replace the message list, keeping `pendingUserMessages` an annotation on\n * it. Every removal from the array goes through here — `push` is the only\n * other mutation and it cannot orphan an id.\n */\n private setMessages(next: Message[]): void {\n this.messages = next;\n if (this.pendingUserMessages.size === 0) return;\n const present = new Set(next.map((m) => m.id));\n for (const id of this.pendingUserMessages.keys()) {\n if (!present.has(id)) this.pendingUserMessages.delete(id);\n }\n }\n\n // Minimal in-session accumulation for the assistant message record.\n // Only top-level ``text`` blocks contribute; subagent / tool output\n // is tracked by the consumer's own block store.\n private accumulatedText = \"\";\n private currentTextPath: number[] | null = null;\n\n private handlers: Set<ChatEventHandler> = new Set();\n private abortController: AbortController | null = null;\n\n constructor(config: AstralformConfig, storage?: ChatStorage) {\n this.client = new AstralformClient(config);\n this.toolRegistry = new ToolRegistry();\n this.storage = storage ?? new InMemoryStorage();\n }\n\n on(handler: ChatEventHandler): () => void {\n this.handlers.add(handler);\n return () => {\n this.handlers.delete(handler);\n };\n }\n\n private emit(event: ChatEvent): void {\n for (const handler of this.handlers) {\n try {\n handler(event);\n } catch {\n // Don't let handler errors crash the session\n }\n }\n }\n\n async connect(): Promise<void> {\n const [status, conversations, agents, skills] = await Promise.allSettled([\n this.client.getAgentStatus(),\n this.client.getConversations(CONVERSATION_PAGE_SIZE),\n this.client.getAgents().catch(() => [] as AgentInfo[]),\n this.client.getSkills().catch(() => [] as SkillInfo[]),\n ]);\n\n if (status.status === \"fulfilled\") {\n this.agentStatus = status.value;\n }\n if (conversations.status === \"fulfilled\") {\n // Reconnect re-seeds page 1, so reset the paging state with it rather\n // than letting a previous connection's offset carry over. Bumping the\n // generation here (and only here — a FAILED fetch leaves the list intact,\n // so an in-flight page is still valid against it) invalidates any page\n // request already in flight against the old offset.\n this.conversationsGeneration++;\n this.conversations = conversations.value;\n this.serverConversationIds = new Set(\n conversations.value.map((c) => c.id),\n );\n this.hasMoreConversations =\n conversations.value.length === CONVERSATION_PAGE_SIZE;\n }\n if (agents.status === \"fulfilled\") {\n this.agents = agents.value;\n }\n if (skills.status === \"fulfilled\") {\n this.skills = skills.value;\n }\n\n this.emit({ type: \"connected\" });\n }\n\n async send(content: string, options?: SendOptions): Promise<void> {\n if ((options?.provider == null) !== (options?.model == null)) {\n throw new Error(\n \"`provider` and `model` must be supplied together (client-side model selection).\",\n );\n }\n if (this.isStreaming) return;\n\n const conversationId =\n options?.conversationId ?? this.conversationId ?? undefined;\n // Captured so a send that never reaches the wire can put the list back —\n // see the put-back at the end of this method.\n let relocatedFrom: {\n messages: Message[];\n messagesId: string | null;\n conversationId: string | null;\n generation: number;\n target: string;\n } | null = null;\n\n // Sending to an explicit conversation makes it the session's — catch the\n // pointer up now rather than waiting for an in-flight `loadConversation`\n // to land. `onSessionEvent` tags every emitted event with this field, so\n // leaving it behind would file this send's own stream events under the\n // conversation the caller just addressed away from: the same mis-tagging\n // the restore guards close, re-entering through the send path.\n //\n // A pointer move IS an event a load in flight must lose to — otherwise it\n // passes its own guard and installs its list under a conversation the send\n // has since relocated to. Same rule in `createNewConversation` and\n // `deleteConversation`.\n if (conversationId) {\n // Only a MOVE invalidates a load in flight. Re-stating the conversation\n // the session is already on — the ordinary case, since the manager\n // passes the active id on every send — must leave a load for that same\n // conversation alone, or sending while its history is still arriving\n // would throw the history away.\n if (conversationId !== this.conversationId) {\n this.loadGeneration++;\n // Both pointers, snapshotted separately: they are deliberately\n // distinct everywhere else here, and during an in-flight\n // `loadConversation` they disagree — restoring one into both would\n // rewind `conversationId` to wherever the MESSAGES were rather than\n // where the session pointed. The generation is claimed here too, so\n // the put-back can tell whether it still owns the session.\n relocatedFrom = {\n messages: this.messages,\n messagesId: this.messagesConversationId,\n conversationId: this.conversationId,\n generation: this.loadGeneration,\n target: conversationId,\n };\n // Drop the old conversation's list with the pointer. Leaving it behind\n // is the same one-conversation's-messages-under-another's-id pairing\n // the load guard exists to prevent — and here nothing re-fetches, so\n // for a direct `ChatSession` caller (this is a documented public\n // option) it would simply persist. Through `StreamManager` the\n // in-flight restore reinstalls the right list.\n this.setMessages([]);\n // NOT `conversationId`. After this the list holds at most this one\n // turn, which is not that conversation's history — and\n // `StreamManager.regenerate` trusts this field, so claiming it would\n // let regenerate fire against a view missing everything before it.\n // `null` keeps the claim honest and the gate closed until a real load.\n this.messagesConversationId = null;\n }\n this.conversationId = conversationId;\n }\n\n const userMessage: Message = {\n id: generateId(),\n conversationId: conversationId ?? \"\",\n role: \"user\",\n content,\n status: \"complete\",\n createdAt: new Date().toISOString(),\n };\n if (conversationId) {\n // Best-effort, matching the sibling call in `consumeJobStream`. On the\n // relocation path the session is ALREADY moved and the list already\n // emptied by the time this runs, so an uncaught rejection escapes `send`\n // before the put-back at the bottom and strands it there — pointed at\n // the target with an empty list and `messagesConversationId` null, i.e.\n // `regenerate` gated off with nothing left to reopen it.\n await this.storage\n .addMessage(userMessage, conversationId)\n .catch(() => {});\n }\n this.messages.push(userMessage);\n // 0: no job response yet, so the server may not hold this row at all.\n //\n // Unconditional. Gated on `conversationId`, the auto-created-conversation\n // path (a direct `session.send(\"hi\")` with no conversation anywhere) was\n // the one branch outside this machinery: `consumeJobStream`'s\n // reconciliation keys off membership here, so that row kept its client id\n // even though the job response had just returned the server's — leaving it\n // unprotected by the merge below and feeding `resendFromCheckpoint` an id\n // the server never issued. The window where an entry would be wrong is\n // already excluded: `loadConversation` filters on `m.conversationId === id`\n // and this row's is `\"\"` until `consumeJobStream` backfills it, which it\n // does immediately before the reconciliation.\n this.pendingUserMessages.set(userMessage.id, 0);\n\n const request: ChatStreamRequest = {\n message: content,\n conversation_id: conversationId,\n mcp_manifest: this.toolRegistry.getManifest(),\n enabled_mcp: Array.from(\n options?.enabledClientTools ?? this.enabledClientTools,\n ),\n upload_ids: options?.uploadIds,\n agent_name: options?.agentName,\n plan_mode: options?.planMode,\n image_mode: options?.imageMode,\n video_mode: options?.videoMode,\n goal: options?.goal,\n // The project this task belongs to, when it has one. Write-once\n // server-side: sent on every turn, honoured on the first.\n repository: options?.repository,\n // Per-request model choice (client-side model selection).\n provider: options?.provider,\n model: options?.model,\n reasoning_effort: options?.reasoningEffort,\n temperature: options?.temperature,\n };\n\n // `processStream` never rejects — it catches and emits an `error` event —\n // so a thrown-exception guard here would be dead code. The signal has to\n // SURVIVE the turn, which `currentJobId` does not: `message_stop` nulls it,\n // so after any completed send it reads exactly as it did before, and a\n // put-back keyed on it would fire on the success path and revert a\n // relocation that worked.\n // Did THIS send reach the wire? Per-call, not a session counter: the\n // `isStreaming` bail above fires before `processStream` sets the flag,\n // with an `await storage.addMessage` in between, so two sends can\n // interleave and a shared counter would answer \"did ANY job get created\n // during my await\". `currentJobId` cannot serve either — `message_stop`\n // nulls it, so after a completed turn it reads as it did before the send.\n const wire: { reached: boolean } = { reached: false };\n await this.processStream(request, wire);\n\n // The relocation above emptied the list before anything was sent. If no job\n // was created there is no new conversation's history to replace it and\n // nothing that will re-fetch, so a direct `ChatSession` caller would be\n // left holding nothing at all. Put it back.\n // Guarded like every other post-await mutation in this file. `createJob`\n // can hang and then fail, and the session can have moved on meanwhile — an\n // unguarded put-back would rewind it to a conversation the user left, which\n // is the failure this whole change exists to prevent, arriving through the\n // one path that was still missing the check.\n // No job means nothing can ever acknowledge this message, so its entry\n // would sit at `knownAt === 0` forever and every later load would\n // re-append a message the server never received. Unconditional, because\n // the put-back below only runs when the send RELOCATED — not the ordinary\n // shape. Keyed on the counter, not the throw: a job that WAS created has\n // had `userMessage.id` replaced with the server's.\n if (!wire.reached) {\n this.pendingUserMessages.delete(userMessage.id);\n // The ROW too, not just its map entry. A load resolving between the push\n // and here has already merged this row into `messages` — correctly, for\n // the success case — and it survives with a client-minted id, which\n // `regenerate` then hands to `resend_from`. Nothing acknowledged it, and\n // `status` says \"complete\" — a claim a send that never reached the wire\n // has no business making.\n const at = this.messages.indexOf(userMessage);\n if (at !== -1) this.messages.splice(at, 1);\n // And the STORAGE copy. `addMessage` above runs before `createJob`, so\n // it has already landed on this path — and `loadConversation` falls back\n // to `storage.fetchMessages` whenever the API fetch rejects, which would\n // reinstall the phantom on any later offline load. Addressable because\n // the id is still the client's: the rename in `consumeJobStream` only\n // runs after `createJob` resolves, which by definition did not happen.\n if (conversationId) {\n await this.storage.deleteMessage(userMessage.id).catch(() => {});\n }\n }\n if (\n relocatedFrom &&\n wire.reached &&\n this.loadGeneration === relocatedFrom.generation\n ) {\n // A SUFFIX of the target conversation rather than its whole history,\n // but it is that conversation's — and `regenerate`, the only consumer of\n // this pairing, needs just the last user turn, whose id is now the\n // server's. Left `null`, nothing here would ever reopen the gate.\n // From the snapshot, not from `conversationId ?? null`: the relocation\n // only runs under `if (conversationId)`, so that fallback was dead and\n // read as though the id could be absent here.\n this.messagesConversationId = relocatedFrom.target;\n } else if (\n relocatedFrom &&\n !wire.reached &&\n this.loadGeneration === relocatedFrom.generation\n ) {\n // No pending-id snapshot to restore alongside. `POST /v1/jobs` always\n // returns `message_id` (required on both sides of the contract; the\n // backend mints it with `uuid4()` before validating the request), so\n // every id the relocation pruned had already been reconciled and carries\n // a `knownAt` at or below any later fetch's issue point — meaning\n // `loadConversation` would not re-append it even if it were restored.\n this.setMessages(relocatedFrom.messages);\n this.messagesConversationId = relocatedFrom.messagesId;\n this.conversationId = relocatedFrom.conversationId;\n }\n }\n\n async resendFromCheckpoint(\n messageId: string,\n newContent: string,\n ): Promise<void> {\n if (this.isStreaming) return;\n\n const request: ChatStreamRequest = {\n message: newContent,\n conversation_id: this.conversationId ?? undefined,\n resend_from: messageId,\n mcp_manifest: this.toolRegistry.getManifest(),\n enabled_mcp: Array.from(this.enabledClientTools),\n };\n\n await this.processStream(request);\n }\n\n private resetStreamingState(): void {\n this.accumulatedText = \"\";\n this.currentTextPath = null;\n }\n\n private async processStream(\n request: ChatStreamRequest,\n wire?: { reached: boolean },\n ): Promise<void> {\n this.isStreaming = true;\n this.resetStreamingState();\n const controller = new AbortController();\n this.abortController = controller;\n\n try {\n await this.consumeJobStream(request, wire);\n } catch (err) {\n if (!(err instanceof DOMException && err.name === \"AbortError\")) {\n this.emit({\n type: \"error\",\n code: \"connection_error\",\n message: err instanceof Error ? err.message : String(err),\n blockPath: null,\n });\n }\n } finally {\n // Only if this invocation still OWNS the turn. `detach()` cannot\n // interrupt a client tool — `executeClientTools` awaits arbitrary\n // consumer code with no signal — so this `finally` can run long after a\n // switch handed the session to a newer turn. Clearing unconditionally\n // then nulls THAT turn's flag and controller: its stream keeps pumping\n // while the session reports idle, `stop()` aborts nothing, and the next\n // switch neither parks its job nor stops it emitting under the new\n // conversation's id. The mirror of `ownsTurnState`, on the live side.\n if (this.abortController === controller) {\n this.isStreaming = false;\n this.abortController = null;\n }\n }\n }\n\n /** Last received sequence number for resumable reconnection */\n private lastSeq = -1;\n\n /**\n * Client-tool call_ids whose result was already submitted this turn. On a\n * reconnect the resumed stream can replay a tool request we already handled;\n * this dedups so each is executed + submitted at most once (but a request we\n * never submitted still runs). Cleared at the start of each turn.\n */\n private submittedToolCallIds = new Set<string>();\n\n /** Current job ID for cancellation */\n currentJobId: string | null = null;\n\n private async consumeJobStream(\n request: ChatStreamRequest,\n wire?: { reached: boolean },\n ): Promise<void> {\n const job = await this.client.createJob(request);\n if (wire) wire.reached = true;\n this.currentJobId = job.job_id;\n\n const conversationId = job.conversation_id;\n if (!this.conversationId) {\n this.conversationId = conversationId;\n // The list in hand is this conversation's — the backend just created it\n // around the turn being sent. Without this the pairing never becomes\n // valid and `regenerate` is a permanent no-op for a consumer that\n // reached a conversation this way.\n this.messagesConversationId = conversationId;\n }\n // Ensure the conversation exists in both the local array and\n // ChatStorage so title_generated, completeStream, and fallback\n // reload all work for backend-created conversations.\n if (!this.conversations.some((c) => c.id === conversationId)) {\n const now = new Date().toISOString();\n const conv = {\n id: conversationId,\n title: \"\",\n messageCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n this.conversations.unshift(conv);\n await this.storage.createConversation(conversationId, \"\").catch(() => {});\n }\n // Backfill the just-sent user message if send() ran before we knew the\n // conversation id (first turn of an auto-created conversation).\n const lastMsg = this.messages[this.messages.length - 1];\n if (lastMsg?.role === \"user\" && !lastMsg.conversationId) {\n lastMsg.conversationId = conversationId;\n await this.storage.addMessage(lastMsg, conversationId).catch(() => {});\n }\n const promptMessageId = job.message_id;\n // Reconcile the local id with the server's. `send` minted a client id that\n // the backend never sees, so nothing downstream could ever match the two —\n // which forced acknowledgement to be guessed from role+content, and no\n // content heuristic can tell a genuinely repeated prompt (\"continue\",\n // \"retry\" — the norm in an agent chat) from one the server already holds.\n // The job response carries the real id, so take it and the question\n // becomes exact.\n if (\n promptMessageId &&\n lastMsg?.role === \"user\" &&\n this.pendingUserMessages.has(lastMsg.id)\n ) {\n this.pendingUserMessages.delete(lastMsg.id);\n const clientMintedId = lastMsg.id;\n lastMsg.id = promptMessageId;\n // Still 0 — the id is now the server's, but the ROW is not proven\n // committed. `POST /v1/jobs` mints the id and starts the loop as a\n // BACKGROUND task (`start_job_task`) before returning, so the prompt is\n // persisted by the loop, not by the handler. `message_stop` is the\n // earliest point the row is certainly there.\n this.pendingUserMessages.set(promptMessageId, 0);\n // Write the rename THROUGH to storage. In memory it lands either way,\n // and with `InMemoryStorage` the stored copy is the same object by\n // reference — so it silently picks the new id up and the divergence is\n // invisible in tests. A `ChatStorage` that serializes on write\n // (IndexedDB, SQLite — the reason the interface is public) keeps the\n // client id forever, and `loadConversation`'s fallback to\n // `storage.fetchMessages` would then reinstate it, handing\n // `resend_from` an id the server never issued. No `updateMessage` on\n // the interface, so delete + re-add.\n if (conversationId && clientMintedId !== promptMessageId) {\n await this.storage.deleteMessage(clientMintedId).catch(() => {});\n await this.storage.addMessage(lastMsg, conversationId).catch(() => {});\n }\n // A snapshot can resolve after the backend commits this row but before\n // the job response arrives, and until it does the local copy carries a\n // client id the server never saw — so there was no id to match on and\n // `loadConversation` kept both. Renaming would then put two rows under\n // one id, the state the assistant row's own id exists to prevent, and\n // `planRestore`'s `byId` would resolve to the wrong one. The local copy\n // is the survivor: it is the newest message, so the tail is where it\n // belongs. Removing the other cannot orphan its map entry, since the\n // survivor now carries the same id.\n const dupe = this.messages.findIndex(\n (m) => m !== lastMsg && m.id === promptMessageId,\n );\n if (dupe !== -1) this.messages.splice(dupe, 1);\n }\n this.lastSeq = -1;\n this.submittedToolCallIds.clear();\n\n await this.consumeEventStream(\n job.job_id,\n conversationId,\n promptMessageId,\n true, // executeClientTools\n );\n }\n\n /**\n * Shared event consumption loop. Parses each wire event, updates\n * minimal session state, and emits typed ChatEvents to consumers.\n */\n private async consumeEventStream(\n jobId: string,\n conversationId: string,\n promptMessageId: string,\n executeClientTools: boolean,\n ): Promise<void> {\n // Capture the signal ONCE. detach()/disconnect() abort the controller and\n // then null it out synchronously, so re-reading this.abortController later\n // would lose the aborted state (?. → undefined → falsy) and the loop would\n // reconnect an unstoppable, signal-less stream. The AbortSignal stays valid\n // (and stays aborted) even after the controller is gone.\n const signal = this.abortController?.signal;\n\n for (let attempt = 0; ; attempt++) {\n // Bail BEFORE building the next attempt. An already-aborted signal never\n // dispatches `abort` again, so the listener below would silently miss it\n // and this attempt would go out after the caller disconnected — emitting\n // events, and potentially running a client tool, on a session it believes\n // is gone. Passing the outer signal straight to fetch used to make that\n // impossible (fetch checks `signal.aborted` up front); the per-attempt\n // controller removes that guarantee unless it is restored here.\n if (signal?.aborted) return;\n // Each attempt gets its own controller, linked to the session signal,\n // so the stall watchdog can kill a zombie connection without aborting\n // the whole session — the retry below then resumes from lastSeq.\n const attemptController = new AbortController();\n const linkAbort = () => attemptController.abort();\n signal?.addEventListener(\"abort\", linkAbort);\n // Client tools stay enabled across reconnects; re-seen tool requests are\n // deduped by submitted call_id in dispatchWireEvent, so a tool whose\n // result we never posted (drop before submit) still runs on resume.\n let sawTerminal: boolean;\n try {\n const stream = this.client.streamJobEvents(\n jobId,\n this.lastSeq,\n attemptController.signal,\n );\n sawTerminal = await this.pumpStream(\n stream,\n conversationId,\n promptMessageId,\n executeClientTools,\n () => attemptController.abort(),\n );\n } catch (err) {\n if (signal?.aborted) return; // user cancelled / detached\n // Auth failures and rate limits can't be fixed by reconnecting (and\n // hammering a 429 is harmful) — surface them immediately. Genuine\n // connectivity failures (incl. a 5xx from a restarting server) retry.\n if (\n err instanceof AuthenticationError ||\n err instanceof RateLimitError\n ) {\n throw err;\n }\n if (attempt >= SSE_MAX_RECONNECTS) throw err;\n await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);\n continue; // resume from lastSeq\n } finally {\n signal?.removeEventListener(\"abort\", linkAbort);\n }\n\n // A terminal event (message_stop / error) ends the turn — including the\n // backend's back-filled terminal for a job that died mid-stream.\n if (sawTerminal || signal?.aborted) return;\n\n // Stream ended WITHOUT a terminal event: the worker/connection dropped\n // mid-turn. Resume from lastSeq so the backend can replay missed events\n // (and back-fill a terminal for a dead job) rather than leave the UI\n // hanging on \"working\".\n if (attempt >= SSE_MAX_RECONNECTS) {\n throw new ConnectionError(\"Lost connection to the response stream.\");\n }\n await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);\n }\n }\n\n /**\n * Consume a single SSE stream to exhaustion. Returns whether a terminal\n * event (``message_stop`` / ``error``) was seen, so the caller can decide\n * whether an ended stream means \"turn done\" vs \"dropped, reconnect\".\n *\n * ``onStall`` aborts the per-attempt connection: if no event arrives within\n * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15-45s), the stream is a\n * zombie — ``reader.read()`` will never settle — so we kill the fetch and\n * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.\n */\n private async pumpStream(\n stream: AsyncGenerator<{ data: string }>,\n conversationId: string,\n promptMessageId: string,\n executeClientTools: boolean,\n onStall?: () => void,\n ): Promise<boolean> {\n let sawTerminal = false;\n const iterator = stream[Symbol.asyncIterator]();\n try {\n while (true) {\n const next = iterator.next();\n let stallTimer: ReturnType<typeof setTimeout> | undefined;\n const stall = new Promise<never>((_, reject) => {\n stallTimer = setTimeout(() => {\n // Reject BEFORE aborting, and the order is load-bearing. Aborting\n // first makes the pending read reject too (StreamAbortedError from\n // `streaming.ts`), and whichever settles first wins the race below\n // — so a stall could surface as `stream_aborted`. Both retry\n // identically, but the diagnostic would then contradict this very\n // comment. Rejecting first settles the race deterministically;\n // `reject` changes state synchronously, so the later abort is a\n // no-op for the race.\n reject(\n new ConnectionError(\n `Stream stalled: no events for ${SSE_STALL_TIMEOUT_MS}ms`,\n ),\n );\n // Then cancel the zombie fetch so the connection is released\n // instead of leaking one per reconnect.\n onStall?.();\n }, SSE_STALL_TIMEOUT_MS);\n });\n let result: IteratorResult<{ data: string }>;\n try {\n result = await Promise.race([next, stall]);\n } finally {\n clearTimeout(stallTimer);\n }\n if (result.done) break;\n const raw = result.value;\n let parsed: WireEvent;\n try {\n const data = JSON.parse(raw.data);\n if (\n typeof data !== \"object\" ||\n data === null ||\n typeof data.type !== \"string\"\n ) {\n // Legacy \"done\" sentinel — backend still emits it for subscribers.\n // Silently consume; the new protocol uses message_stop for turn end.\n if (typeof (data as { seq?: unknown })?.seq === \"number\") {\n this.lastSeq = (data as { seq: number }).seq;\n }\n continue;\n }\n parsed = data as WireEvent;\n if (typeof (data as Record<string, unknown>).seq === \"number\") {\n this.lastSeq = (data as Record<string, unknown>).seq as number;\n }\n } catch {\n continue;\n }\n\n if (parsed.type === \"message_stop\" || parsed.type === \"error\") {\n sawTerminal = true;\n }\n\n await this.dispatchWireEvent(\n parsed,\n conversationId,\n promptMessageId,\n executeClientTools,\n );\n }\n } finally {\n // Fire-and-forget: on the stall path the pending read may never settle\n // (e.g. a custom fetchFn that ignores the abort signal), and awaiting\n // this would re-hang the pump the watchdog just rescued.\n void iterator.return?.(undefined).catch(() => {});\n }\n return sawTerminal;\n }\n\n /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */\n private sleepUnlessAborted(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) return resolve();\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n\n /** POST a client-tool result, retrying transient failures a few times. */\n private async submitToolResultWithRetry(\n payload: Parameters<AstralformClient[\"submitToolResult\"]>[0],\n ): Promise<void> {\n const signal = this.abortController?.signal;\n for (let attempt = 0; ; attempt++) {\n try {\n await this.client.submitToolResult(payload);\n return;\n } catch (err) {\n if (signal?.aborted) throw err;\n if (\n err instanceof AuthenticationError ||\n err instanceof RateLimitError\n ) {\n throw err;\n }\n if (attempt >= TOOL_RESULT_MAX_RETRIES) throw err;\n await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);\n }\n }\n }\n\n private async dispatchWireEvent(\n wire: WireEvent,\n conversationId: string,\n promptMessageId: string,\n executeClientTools: boolean,\n ): Promise<void> {\n // Side effects that depend on mutable session state must run before the\n // ChatEvent is emitted so consumers see a consistent view.\n this.applyWireSideEffects(wire, conversationId, promptMessageId, true);\n\n const event = translateWireEvent(wire);\n if (event) {\n this.emit(event);\n }\n\n // Client tool round-trip — deferred to block_stop with\n // status=awaiting_client_result, where the parsed input is in final.input.\n if (\n executeClientTools &&\n wire.type === \"block_stop\" &&\n wire.status === \"awaiting_client_result\" &&\n wire.final?.call_id\n ) {\n const f = wire.final;\n const callId = (f.call_id as string) ?? \"\";\n // Dedup across reconnects: a resumed stream can replay a tool request we\n // already handled. Execute + submit each call_id at most once — but DO\n // run requests not yet submitted (e.g. the drop happened before we could\n // post the result), rather than skipping client tools wholesale.\n if (callId && !this.submittedToolCallIds.has(callId)) {\n const request: ToolCallRequest = {\n callId,\n toolName: (f.tool_name as string) ?? \"\",\n arguments: (f.input as Record<string, unknown>) ?? {},\n isClientTool: true,\n };\n const results = await this.executeClientTools([request]);\n // Retry the POST itself before giving up: reconnecting the SSE stream\n // can't recover a failed result submission, and retrying here avoids\n // re-executing the tool on a transient network blip.\n await this.submitToolResultWithRetry({\n conversation_id: conversationId,\n // The message that TRIGGERED the tool calls, which is what the\n // backend stores it against — the prompt id, correctly.\n message_id: promptMessageId,\n tool_results: results,\n });\n // Marked only after a successful submit, so a drop mid-POST re-runs it.\n this.submittedToolCallIds.add(callId);\n }\n }\n }\n\n /**\n * Synchronous replay of a single stored wire event — the side-effect +\n * translate + emit core of ``dispatchWireEvent`` without the (live-only)\n * client-tool round-trip. Called in a tight synchronous loop during history\n * restore so the consumer's per-event store writes batch into ONE render\n * instead of re-typing the whole conversation event by event.\n */\n private replayWireEvent(wire: WireEvent, conversationId: string): void {\n // `live: false` — a replay must never touch the streaming lifecycle. It\n // cannot be inferred from `promptMessageId` being empty, because\n // `reconnectToJob` passes empty too and IS live.\n this.applyWireSideEffects(wire, conversationId, \"\", false);\n const event = translateWireEvent(wire);\n if (event) {\n this.emit(event);\n }\n }\n\n /**\n * State mutations driven by wire events. Kept separate from translation so\n * the pure wire → ChatEvent mapping can live in translate.ts and be reused\n * by the replay path.\n *\n * ``promptMessageId`` is the id of the USER turn that started this job —\n * `POST /v1/jobs` returns it and the backend tags the prompt with it\n * (`HumanMessage(content=..., id=message_id)`), which is what lets a restore\n * pair a job with its prompt by id instead of by position. It was previously\n * documented here as the ASSISTANT's id and used as one; there is no\n * server-assigned assistant id on the wire, so that row gets a local one.\n * Empty in the reconnect and conversation-switch replay paths, where the\n * messages have already been loaded from REST and must not be re-pushed —\n * so it doubles as the \"is this a live send?\" gate.\n */\n private applyWireSideEffects(\n wire: WireEvent,\n conversationId: string,\n promptMessageId: string,\n live: boolean,\n ): void {\n // `accumulatedText`, `currentTextPath`, `isStreaming` and `currentJobId`\n // are ONE turn's state, and a restore replaying completed turns over a\n // running one must not touch any of it. `send` bails only on the manager's\n // `streaming`, so it runs during `restoring` and nothing bumps the\n // generation — the replay loop's `superseded()` therefore stays false and\n // the two overlap. A live event always owns this state; a replayed one\n // only when nothing is running.\n const ownsTurnState = live || !this.isStreaming;\n\n switch (wire.type) {\n case \"message_start\":\n // Reset per-turn accumulator so multi-turn replay doesn't concatenate\n // text from prior turns into the next assistant message.\n if (ownsTurnState) this.resetStreamingState();\n if (wire.model) {\n this.modelDisplayName = wire.model;\n }\n return;\n\n case \"block_start\":\n // Track the currently open top-level text block so we can accumulate\n // its content for the assistant Message record.\n if (\n ownsTurnState &&\n wire.kind === \"text\" &&\n (!wire.parent_path || wire.parent_path.length === 0)\n ) {\n this.currentTextPath = wire.path;\n }\n return;\n\n case \"block_delta\":\n if (\n ownsTurnState &&\n wire.delta.channel === \"text\" &&\n this.currentTextPath !== null &&\n pathEquals(this.currentTextPath, wire.path)\n ) {\n this.accumulatedText += wire.delta.text;\n }\n return;\n\n case \"block_stop\":\n if (\n ownsTurnState &&\n this.currentTextPath !== null &&\n pathEquals(this.currentTextPath, wire.path)\n ) {\n this.currentTextPath = null;\n }\n return;\n\n case \"message_stop\":\n // The turn ran to completion, so the backend has persisted its prompt.\n // This — not the job response — is the proof `loadConversation` needs\n // before it may read \"absent from the snapshot\" as \"the server does\n // not have it\".\n if (promptMessageId && this.pendingUserMessages.has(promptMessageId)) {\n this.pendingUserMessages.set(promptMessageId, ++this.serverRowsKnown);\n }\n // Only record the assistant message on a live send — a non-empty\n // prompt id IS that signal. Reconnect/replay paths load messages via\n // REST instead.\n if (promptMessageId) {\n const assistantMessage: Message = {\n // NOT `promptMessageId` — that is the USER turn's id, which\n // `consumeJobStream` stamps onto the user row. Sharing it puts two\n // rows under one id, and `pendingUserMessages` is id-keyed. No\n // server-assigned assistant id exists on the wire, so this row is\n // local until a REST load replaces it.\n id: generateId(),\n conversationId,\n role: \"assistant\",\n content: this.accumulatedText,\n status: \"complete\",\n createdAt: new Date().toISOString(),\n };\n this.messages.push(assistantMessage);\n this.storage\n .addMessage(assistantMessage, conversationId)\n .catch(() => {});\n }\n // Only the LIVE stream owns these. A restore replaying a completed\n // turn over a running one used to clear both: `currentJobId` null\n // means the job can no longer be cancelled by `stop()` or parked by\n // `detachStreamingTurn`, and `isStreaming` false means the next send\n // passes its own guard and opens a second stream sharing this one's\n // `abortController` and `lastSeq`.\n if (ownsTurnState) {\n this.isStreaming = false;\n this.currentJobId = null;\n }\n return;\n\n case \"custom\":\n if (wire.name === \"title_generated\") {\n const title = (wire.data.title as string) ?? \"\";\n if (this.conversationId && title) {\n const conv = this.conversations.find(\n (c) => c.id === this.conversationId,\n );\n if (conv) {\n conv.title = title;\n }\n this.storage\n .updateConversationTitle(this.conversationId, title)\n .catch(() => {});\n }\n }\n return;\n\n default:\n return;\n }\n }\n\n private async executeClientTools(\n toolCalls: ToolCallRequest[],\n ): Promise<ToolResult[]> {\n const results: ToolResult[] = [];\n for (const call of toolCalls) {\n const result = await this.toolRegistry.executeTool(call);\n results.push(result);\n }\n return results;\n }\n\n /**\n * Load conversation context (messages) without replaying events.\n * Used before reconnectToJob — SSE replay handles event replay.\n */\n async loadConversation(id: string): Promise<void> {\n // Claimed BEFORE the await, so the check below is \"am I still the newest\n // load?\" rather than \"does the session still point where I left it?\".\n const load = ++this.loadGeneration;\n this.conversationId = id;\n // NOT while a turn is live. `resetStreamingState` nulls `currentTextPath`,\n // and the live turn's `block_start` has already gone by — so every later\n // `block_delta` fails the path check, accumulates nothing, and\n // `message_stop` persists an EMPTY assistant row. Only reachable when a\n // send lands in a switch's probe window (the normal switch path detaches\n // first, which clears `isStreaming`), which is exactly the window this\n // change is about.\n if (!this.isStreaming) this.resetStreamingState();\n // Read BEFORE the fetch goes out: the server evaluates it later still, so\n // any row already proven committed at this point has to be in the reply.\n const rowsKnownAtIssue = this.serverRowsKnown;\n const messages = await this.client\n .getMessages(id)\n .catch(() => this.storage.fetchMessages(id));\n // Nothing serializes callers, and this fetch is not instant. Install these\n // unconditionally and the session holds ONE conversation's id beside\n // ANOTHER's messages — the pair `send` (which posts to `conversationId`)\n // and `regenerate` (which resends `messages`' last user turn) read\n // together.\n //\n // A monotonic token rather than `this.conversationId !== id`, because that\n // comparison is ABA-blind: on A -> B -> A with A's FIRST fetch slow, the id\n // is back to A by the time that fetch lands, so the check passes and it\n // clobbers the fresh messages the second A load already installed. The id\n // says where the session is, not which load last spoke.\n if (load !== this.loadGeneration) return;\n // The reply is a SNAPSHOT of the server's state. A user message the server\n // has not persisted yet is not in it, and assigning straight over\n // `this.messages` drops it with nothing to restore it — the SSE handler\n // only ever appends the assistant's reply, never the user turn again. So\n // the send is posted correctly and then vanishes, leaving `regenerate` to\n // pick the PREVIOUS turn as the last user message.\n //\n // Matched on ID, which `consumeJobStream` reconciles with the server's as\n // soon as the job response lands — so this is exact, not a heuristic over\n // role/content/arrival order.\n const pending = this.messages.filter(\n (m) => this.pendingUserMessages.has(m.id) && m.conversationId === id,\n );\n const stillPending = pending.filter((m) => {\n if (messages.some((f) => f.id === m.id)) return false;\n const knownAt = this.pendingUserMessages.get(m.id) ?? 0;\n // Absent from the snapshot has two causes needing opposite handling.\n // 0 means the turn has not completed, so the row may not be committed —\n // keep it, that is the race this set exists for. Otherwise the turn\n // finished before this fetch was issued, so the snapshot HAD to contain\n // it; missing means the server does not have it, and re-appending would\n // resurrect it at the tail on every later load.\n return knownAt === 0 || knownAt > rowsKnownAtIssue;\n });\n for (const m of pending) {\n if (!stillPending.includes(m)) this.pendingUserMessages.delete(m.id);\n }\n this.setMessages(\n stillPending.length ? [...messages, ...stillPending] : messages,\n );\n this.messagesConversationId = id;\n }\n\n /**\n * Reconnect to a running job's SSE stream (e.g. after page reload).\n * Replays all events from the beginning and continues live.\n */\n async reconnectToJob(jobId: string): Promise<void> {\n if (this.isStreaming) return;\n\n this.isStreaming = true;\n this.currentJobId = jobId;\n this.lastSeq = -1;\n this.submittedToolCallIds.clear();\n this.resetStreamingState();\n const controller = new AbortController();\n this.abortController = controller;\n\n try {\n await this.consumeEventStream(\n jobId,\n this.conversationId ?? \"\",\n \"\",\n false, // don't execute client tools on reconnect\n );\n } catch (err) {\n this.emit({\n type: \"error\",\n code: \"connection_error\",\n message: err instanceof Error ? err.message : String(err),\n blockPath: null,\n });\n } finally {\n // Only if this invocation still OWNS the turn. `detach()` cannot\n // interrupt a client tool — `executeClientTools` awaits arbitrary\n // consumer code with no signal — so this `finally` can run long after a\n // switch handed the session to a newer turn. Clearing unconditionally\n // then nulls THAT turn's flag and controller: its stream keeps pumping\n // while the session reports idle, `stop()` aborts nothing, and the next\n // switch neither parks its job nor stops it emitting under the new\n // conversation's id. The mirror of `ownsTurnState`, on the live side.\n if (this.abortController === controller) {\n this.isStreaming = false;\n this.abortController = null;\n }\n }\n }\n\n /** Detach from the SSE stream without cancelling the job. */\n detach(): void {\n this.abortController?.abort();\n this.abortController = null;\n this.isStreaming = false;\n this.resetStreamingState();\n this.emit({ type: \"disconnected\" });\n }\n\n /**\n * Cancel the running turn: stop the job server-side and tear the stream\n * down, WITHOUT ending the session. A turn-level cancel is not a\n * session-level teardown, so unlike `disconnect()` this leaves the protocol\n * registry alone — the SDK never auto-registers adapters, so clearing them\n * on a Stop press would silently kill embedded-resource rendering for the\n * rest of the session with nothing to re-register it.\n */\n cancelTurn(): void {\n if (this.currentJobId) {\n this.client.cancelJob(this.currentJobId).catch(() => {});\n }\n this.detach();\n // `detach()` does not do this, and a cancelled job's id must not linger —\n // a later `stop()` would re-issue `cancelJob` against it.\n this.currentJobId = null;\n // A cancelled turn never reaches `message_stop`, so nothing can ever prove\n // its prompt committed, and an entry left at `knownAt === 0` means every\n // later load re-appends the row — forever, since `setMessages`'s prune\n // only drops rows that LEFT the list. The server is authoritative from\n // here: if it holds the prompt a load returns it, and if it does not (the\n // loop runs as a background task, so a fast cancel can beat the write)\n // the row correctly disappears. NOT done in `detach()`, where the job\n // keeps running and will persist.\n for (const [id, knownAt] of this.pendingUserMessages) {\n if (knownAt === 0) this.pendingUserMessages.delete(id);\n }\n }\n\n /** Stop the job and end the session's activity (explicit user action). */\n disconnect(): void {\n this.cancelTurn();\n // Drop all protocol adapters — lifecycle tied to the session.\n this.protocols.clear();\n }\n\n /**\n * A pointer move happened above this layer, so any `loadConversation` in\n * flight must lose to it. `StreamManager` owns a pointer of its own and\n * moves it before this one; without this the two halves would gate on\n * counters that bump at different instants — `generation` synchronously in\n * `setActiveConversation`, `loadGeneration` only once the switch's own load\n * actually runs, which is behind the active-job probe.\n */\n invalidateLoadsInFlight(): void {\n this.loadGeneration++;\n }\n\n async createNewConversation(): Promise<string> {\n const id = generateId();\n // Witness captured before the await, like `deleteConversation`'s re-test of\n // its own pointer. `storage.createConversation` is a real round-trip for\n // any non-memory `ChatStorage`, and anything that moves the pointer during\n // it — a switch, a relocating send — bumps this.\n const load = this.loadGeneration;\n const conversation = await this.storage.createConversation(\n id,\n \"New Conversation\",\n );\n this.conversations.unshift(conversation);\n // Created and in the list either way; only the RELOCATION is conditional.\n // Without this the manager could decline its own pointer move while the\n // session had already taken this one — manager on B, session on the new\n // id with an empty list, which is sticky: `regenerate` gates on that\n // pairing and `switchTo` early-returns on B, so re-clicking B does\n // nothing and the user has to visit a third conversation and come back.\n if (load !== this.loadGeneration) return id;\n // A pointer move — same rule as `send`'s relocation. Invisible from\n // `restore`, which bails at its next `superseded()` check, while the\n // session already holds the mismatched pairing.\n this.loadGeneration++;\n this.conversationId = id;\n this.setMessages([]);\n // The empty list IS this conversation's list — say so, or every consumer\n // of the pairing (regenerate) stays blocked on the previous conversation.\n this.messagesConversationId = id;\n return id;\n }\n\n /**\n * Replay one completed turn's already-fetched events, synchronously.\n *\n * Fetching is the caller's job (``StreamManager.restore`` loads every turn's\n * events in parallel and the message list once), so this is pure replay: no\n * awaits, so the whole restore runs in a single synchronous pass and the\n * consumer batches it into one render.\n *\n * ``userMessageContent`` is the prompt that triggered this turn. It's emitted\n * as a synthetic ``user_message`` BEFORE any of the turn's events: user\n * prompts aren't persisted in ``job_events``, and some events precede\n * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),\n * so leading with the prompt keeps the turn in order.\n */\n replayTurn(\n id: string,\n events: ConversationEvent[],\n userMessageContent?: string,\n userMessageId?: string,\n isSteer = false,\n ): void {\n this.conversationId = id;\n // Same rule as `loadConversation`: not while a turn is live. Nulling\n // `currentTextPath` after the live turn's `block_start` has gone by makes\n // every later delta accumulate nothing, and its `message_stop` then\n // persists the REPLAYED turn's text as the assistant row.\n if (!this.isStreaming) this.resetStreamingState();\n\n if (userMessageContent) {\n this.emit({\n type: \"user_message\",\n content: userMessageContent,\n ...(userMessageId ? { id: userMessageId } : {}),\n ...(isSteer ? { steer: true } : {}),\n });\n }\n\n // The data payload is authoritative for `type` (matching how\n // replay.ts#mapSseToChat reads it), with the SSE event name as a fallback\n // for pre-v2 rows.\n for (const ev of events) {\n const type = (ev.data.type as string) || ev.event;\n if (!type || type === \"done\") continue;\n\n const wire = { ...ev.data, type } as unknown as WireEvent;\n try {\n this.replayWireEvent(wire, id);\n } catch {\n // Skip malformed replay events\n }\n }\n }\n\n /**\n * Load a conversation's messages and replay its persisted history.\n *\n * Convenience for plain-``ChatSession`` consumers (the documented\n * conversation-management API). ``StreamManager`` drives restore itself —\n * loading messages once and replaying each turn in parallel — and does NOT\n * call this; it's kept so direct-Session usage doesn't break.\n *\n * Without ``jobId`` it replays the whole conversation; with one, just that\n * job's events.\n */\n async switchConversation(id: string, jobId?: string): Promise<void> {\n // Load the messages through `loadConversation` rather than fetching and\n // assigning them here: one implementation of the token check and the\n // pending merge, so the two cannot drift apart. Still parallel — the two\n // fetches start together.\n // `loadConversation` claims its token synchronously, before its first\n // await, so reading `loadGeneration` straight after the call gives the\n // token THIS switch is operating under.\n const loading = this.loadConversation(id);\n const token = this.loadGeneration;\n const [loadResult, eventsResult] = await Promise.allSettled([\n loading,\n this.client.getConversationEvents(id, jobId),\n ]);\n // Guarding the messages alone was not enough — and left this in a worse\n // state than before. `replayTurn` opens by assigning `this.conversationId`\n // and then emits the whole turn, so a superseded call still did both of\n // the things this guard exists to stop: re-pointed the session at the\n // conversation the caller left, and poured its events out of the stream.\n // With the messages now correctly dropped, the id moved back alone —\n // leaving one conversation's messages under another's id, precisely the\n // pairing `loadConversation` documents itself as eliminating. Previously\n // both moved together: still wrong, but at least coherent.\n // Checked against the token, not against whether the load itself was\n // superseded: on a plain A -> B the load for A COMPLETES before B starts,\n // so it reports success, and only the events fetch is still open when B\n // supersedes. The token catches both — the load losing mid-flight, and\n // this whole call losing after it.\n if (token !== this.loadGeneration) return;\n // Ordered AFTER the token check, because a load can be both rejected AND\n // superseded: blanking then wipes the NEWER conversation's freshly\n // installed list and stamps it with this conversation's id — the same\n // mismatch this whole guard exists to prevent, arriving through the\n // failure path. A rejected load has already moved the pointer, so when\n // this call does still own the session the list must not be left behind.\n // Only reachable through a custom `ChatStorage` whose fallback throws\n // (`InMemoryStorage` never does), but `ChatStorage` is a public interface.\n if (loadResult.status === \"rejected\") {\n this.setMessages([]);\n this.messagesConversationId = id;\n }\n this.replayTurn(\n id,\n eventsResult.status === \"fulfilled\" ? eventsResult.value : [],\n );\n }\n\n /**\n * Append the next page of conversation history to ``conversations``.\n *\n * The list is ordered ``updated_at DESC`` and paged by offset, so a\n * conversation bumped to the top mid-scroll can surface again in a later\n * page; ids already held are dropped rather than duplicated. Returns only\n * the conversations actually appended, which may be empty even on a full\n * page. Rejects on network failure with ``hasMoreConversations`` still true,\n * so the caller can retry.\n *\n * KNOWN LIMITATION — offset paging is only stable while the prefix already\n * consumed stays put. The offset tracking here corrects for perturbations\n * THIS session causes (local unshifts, ``deleteConversation``), but not for\n * ones it never sees:\n *\n * - a conversation this session hasn't loaded yet is bumped to the top (a\n * headless routine or another device posting to it), pushing the whole\n * list down — it lands inside the consumed prefix, which no later offset\n * revisits;\n * - a conversation is deleted from another tab/device, shrinking the list so\n * the next offset lands one row too far in.\n *\n * Each perturbation costs at most one conversation off the sidebar, and only\n * until the next ``connect()`` — that re-seeds page 1 and resets the paging\n * state, so a reload or reconnect always recovers it. Nothing is lost\n * server-side. Both cases are pinned by tests in\n * ``tests/conversation-paging.test.ts``.\n *\n * Closing the gap properly needs a stable server cursor (keyset paging on\n * ``(updated_at, id)``) rather than a raw offset, which is a backend change —\n * tracking ids client-side cannot discover a row that moved into a region\n * already scanned.\n */\n async loadMoreConversations(): Promise<Conversation[]> {\n // Scroll handlers fire far faster than the request completes; without this\n // guard every frame would refetch the same offset.\n if (this.isLoadingConversations || !this.hasMoreConversations) return [];\n this.isLoadingConversations = true;\n const generation = this.conversationsGeneration;\n try {\n const page = await this.client.getConversations(\n CONVERSATION_PAGE_SIZE,\n this.serverConversationIds.size,\n );\n // A reconnect re-seeded the list while this was in flight, so this page\n // describes a paging state that no longer exists. Drop it untouched —\n // connect() has already set conversations/hasMore for the new state, and\n // the caller's next call pages from there.\n if (generation !== this.conversationsGeneration) return [];\n this.hasMoreConversations = page.length === CONVERSATION_PAGE_SIZE;\n const fresh = page.filter((c) => !this.serverConversationIds.has(c.id));\n for (const c of page) this.serverConversationIds.add(c.id);\n // A locally-created conversation can already sit in the array from its\n // unshift; count it toward the offset (the server did return it) but\n // don't append a second copy.\n const known = new Set(this.conversations.map((c) => c.id));\n const appended = fresh.filter((c) => !known.has(c.id));\n this.conversations.push(...appended);\n return appended;\n } finally {\n this.isLoadingConversations = false;\n }\n }\n\n /**\n * Rename a conversation, server first.\n *\n * Server first, like the delete below: a failed rename written locally would\n * leave the sidebar showing a title the server never accepted, and nothing\n * refetches a conversation that is already in the loaded list. (The delete\n * used to be the counter-example here — it dropped the row whatever the\n * server said, on the theory that a failed one was self-correcting. It was\n * not: the row stayed deleted locally and alive on the server.)\n *\n * Mirrors the `title_generated` path: the entry in `conversations` is\n * mutated in place, which is what every consumer of the list reads.\n */\n async renameConversation(id: string, title: string): Promise<void> {\n const updated = await this.client.renameConversation(id, title);\n const conv = this.conversations.find((c) => c.id === id);\n if (conv) {\n // The server's title, not the caller's — it trims before storing.\n conv.title = updated.title;\n }\n await this.storage.updateConversationTitle(id, updated.title);\n }\n\n async deleteConversation(id: string): Promise<void> {\n try {\n await this.client.deleteConversation(id);\n } catch (err) {\n // 404 only. The backend 404s a conversation that is already gone, and\n // that IS this delete succeeding — drop it locally and carry on.\n //\n // Everything else means the conversation still exists: 403 (not yours),\n // 401, 429, 5xx, or the request never landing at all. This used to\n // swallow all of them and delete locally anyway, so a failed delete was\n // indistinguishable from a successful one — the row vanished from the\n // sidebar, survived on the server, and came back on the next device or\n // the next reload. Rethrowing BEFORE the local delete is what makes the\n // two outcomes tellable apart; the caller decides what to show.\n if (!(err instanceof ServerError) || err.status !== 404) throw err;\n }\n await this.storage.deleteConversation(id);\n // Shrinks the paging offset iff the server had handed us this one — every\n // later page now shifts up by one, and without this the next page would\n // skip a conversation. A purely local conversation isn't in the set, so\n // deleting it correctly leaves the offset alone.\n //\n // ``Set.delete`` reports whether it was there, which is exactly the\n // \"was this server-sourced?\" test. When it was, any page ALREADY in flight\n // is now stale for the same reason as a reconnect: its offset was computed\n // pre-delete but the server evaluates the query post-delete, so it starts\n // one row late and would skip that row for good. The adjustment above fixes\n // future requests and cannot rescue an outstanding one — so invalidate it.\n if (this.serverConversationIds.delete(id)) {\n this.conversationsGeneration++;\n }\n this.conversations = this.conversations.filter((c) => c.id !== id);\n if (this.conversationId === id) {\n // Same pointer-move rule as `createNewConversation`. Worse here if\n // skipped: `session.messages` is public, so the reinstalled list is the\n // DELETED conversation's history rendered under a null id.\n this.loadGeneration++;\n this.conversationId = null;\n this.setMessages([]);\n this.messagesConversationId = null;\n }\n }\n\n toggleClientTool(name: string): boolean {\n if (this.enabledClientTools.has(name)) {\n this.enabledClientTools.delete(name);\n return false;\n }\n this.enabledClientTools.add(name);\n return true;\n }\n}\n","/**\n * Deciding which prompt started which turn, when restoring a conversation.\n *\n * Restore has to pair each completed job with the user message that triggered\n * it. It used to do that by POSITION — the N-th completed job to the N-th user\n * message — which is only sound while every user message starts exactly one\n * job. A mid-run steer (`POST /conversations/{id}/steer`) is a user message\n * that starts none, so it shifted every later turn onto the wrong prompt and\n * pushed the tail off the end of the loop entirely.\n *\n * The backend now stamps a turn's prompt with the same id the job records\n * (`job.message_id`), so the pairing can be exact. Four kinds of row show up in\n * the two lists, and the id is what tells them apart:\n *\n * | row | has message id | job links to it |\n * |----------------------|----------------|-----------------|\n * | current turn prompt | yes | yes |\n * | mid-run steer | yes | no |\n * | legacy prompt | no | n/a |\n * | goal-continuation | hidden from the message list entirely |\n *\n * Classification keys off whether a job's `message_id` actually MATCHES a\n * message — never off a field being present. Both are always populated in real\n * data: `jobs.message_id` is NOT NULL, and the messages endpoint substitutes a\n * positional index string when a row carries no id of its own. A presence check\n * therefore reads every pre-link conversation as linked and strips every prompt\n * bubble from it.\n *\n * So: a job whose id matches a message is a turn; a message no job claims is a\n * steer; a job past the cutover whose id matches nothing visible is a\n * continuation, replaying with no bubble (what the positional version did by\n * accident when it ran off the end of the message list).\n *\n * Jobs are the spine, so continuations keep their place in the transcript.\n * Steers are interleaved at the point they appear in the message list, which\n * puts them after the turn that was running when they were sent.\n *\n * A conversation can also be reopened while a turn is STILL RUNNING, and that\n * turn's prompt is paired here too (`runningJob`) even though its events are\n * not replayed from storage — they arrive on the live stream the caller\n * reconnects to. Only the pairing is special-cased; the walk treats it as an\n * ordinary turn.\n *\n * Jobs are the spine, which means a conversation can also have NO spine: every\n * job failed or was cancelled, or the only one is still running and the\n * active-job probe did not resolve it. The walk is then empty, and the prompts\n * are carried entirely by the message list — so they are emitted on their own\n * rather than dropped with the jobs that would have anchored them.\n *\n * A conversation predating the link has no `message_id` on any job; there is\n * nothing to pair with, and no backfill is possible — inferring which historic\n * prompt started which job is exactly the ambiguity the link removes, so\n * guessing would bake the bug into the data. Those fall back to the positional\n * walk, unchanged, and keep the old behaviour including its flaw.\n */\n\nexport interface RestoreJob {\n job_id: string;\n /** The prompt that started this turn. Absent on pre-link rows and on\n * goal-continuation jobs, whose seed is hidden from the message list. */\n message_id?: string | null;\n}\n\nexport interface RestoreMessage {\n /** Present once the backend tags prompts; absent on pre-link rows. */\n id?: string;\n content: string;\n}\n\nexport type ReplayStep =\n /** Replay a job's events, optionally preceded by the prompt bubble. */\n | { kind: \"turn\"; jobId: string; content?: string; messageId?: string }\n /** A user message that started no turn — render the bubble alone. */\n | { kind: \"steer\"; content: string; messageId?: string };\n\n/**\n * Order the replay: which jobs to play, with which prompts, and where the\n * steers go between them. Pure, so the ordering rules are testable without a\n * session, a network, or a fake event stream.\n */\nexport function planRestore(args: {\n completedJobs: RestoreJob[];\n /**\n * The turn that is still RUNNING, if one is. It joins the walk as an\n * ordinary turn so it is paired with its prompt by the same rules as any\n * other — the caller simply holds no events for it, because those are the\n * live stream it reconnects to, so the step renders the bubble alone.\n *\n * Without it a live turn's prompt is paired with nothing while still being\n * `claimed` (below), so it is neither a turn nor a steer and vanishes from\n * the restore entirely — leaving a conversation reopened mid-turn showing\n * the running turn's blocks under no prompt at all.\n */\n runningJob?: RestoreJob;\n /**\n * Message ids claimed by ANY job, not just completed ones. A steer is a\n * prompt no job started, so testing against completed jobs alone reads a\n * prompt whose turn is still RUNNING as a steer — and replays it as a\n * second, `steer`-flagged bubble on top of the one the live send already\n * rendered. Optional so callers that only have the completed set keep the\n * old behaviour.\n *\n * Ignored when the walk is EMPTY — see the branch below. Claiming prevents a\n * second copy of a prompt, and a walk with no turns in it emits no first\n * copy, so honouring the set there would drop a running turn's prompt rather\n * than de-duplicate it.\n */\n claimedMessageIds?: (string | null | undefined)[];\n userMessages: RestoreMessage[];\n}): ReplayStep[] {\n const { completedJobs, runningJob, userMessages } = args;\n // The running turn sits after every completed one, which is where it belongs\n // both chronologically and positionally: on a pre-link conversation the\n // fallback walk pairs it with `userMessages[completedJobs.length]`, the\n // prompt that follows the last completed turn's.\n const jobs = runningJob ? [...completedJobs, runningJob] : completedJobs;\n const claimed = new Set(\n (args.claimedMessageIds ?? jobs.map((j) => j.message_id)).filter(\n (id): id is string => !!id,\n ),\n );\n\n const byId = new Map<string, number>();\n userMessages.forEach((m, i) => {\n if (m.id) byId.set(m.id, i);\n });\n /** The message index this job's prompt lives at, if it is visible. */\n const linkOf = (j: RestoreJob): number | undefined =>\n j.message_id ? byId.get(j.message_id) : undefined;\n\n // `slice`, not `jobs`: the caller hands this a PORTION of the walk (the\n // pre-cutover head, or the whole list), and reusing the outer name for\n // sometimes-the-same list made `jobs` mean two things in one function.\n const positional = (\n slice: RestoreJob[],\n msgs: RestoreMessage[],\n ): ReplayStep[] =>\n slice.map((job, i) => ({\n kind: \"turn\" as const,\n jobId: job.job_id,\n content: msgs[i]?.content,\n messageId: msgs[i]?.id,\n }));\n\n // No turn to walk AT ALL, which is not the same as a conversation with no\n // history. Two ways to get here, and they are the whole reason this branch\n // exists:\n //\n // - every job FAILED or was CANCELLED — neither is `completed`, so neither\n // reaches `completedJobs`;\n // - the only job is still RUNNING and the active-job probe did not resolve\n // it, so the caller passes no `runningJob` (the Redis liveness key can\n // expire while the row is still `in_progress`).\n //\n // `positional` maps over JOBS, so it returns [] for an empty walk and the\n // whole transcript renders empty — the prompt the user actually sent\n // disappears, leaving the turn's blocks under nothing at all.\n //\n // `claimed` is deliberately IGNORED here, and that is what separates the two\n // cases above. Claiming exists to stop a prompt being drawn twice: once by\n // the turn that anchors it and once as a steer. An empty walk emits no turn\n // at all, so nothing can anchor anything and there is no second copy to\n // avoid — while a running job IS claimed (the caller claims every job it\n // replays, and it replays every job but the one arriving live), so filtering\n // on it here would drop exactly the running-but-unresolved prompt this branch\n // has to rescue.\n //\n // Nor does that re-open the double-bubble the claim set guards: the caller\n // only replays after announcing `restoring`, i.e. after telling the consumer\n // to clear, and bails on `turnStarted` / `viewTakenOverByLiveTurn` when a\n // send owns the view instead. So reaching here means the view is empty.\n //\n // Checked before `firstLinked`, because an empty list has no linked job by\n // definition and would otherwise fall into the pre-link branch below and\n // return [] from there.\n if (jobs.length === 0) {\n return userMessages.map((m) => ({\n kind: \"steer\" as const,\n content: m.content,\n messageId: m.id,\n }));\n }\n\n // Detection keys off whether a job's id actually MATCHES a message — never\n // off a field merely being present. Both fields are always populated in real\n // data (`jobs.message_id` is NOT NULL, and the messages endpoint substitutes\n // a positional index string when a row has no id of its own), so a\n // presence check silently classifies every pre-link conversation as linked\n // and strips every prompt bubble from it.\n const firstLinked = jobs.findIndex((j) => linkOf(j) !== undefined);\n if (firstLinked === -1) {\n // Nothing matches: the whole conversation predates the tagging.\n return positional(jobs, userMessages);\n }\n\n // The tagging started at a point in time, so a conversation spanning it\n // splits cleanly: everything before the first linked turn has no usable\n // link and falls back to position; everything after is exact.\n const cutover = linkOf(jobs[firstLinked]!)!;\n const steps: ReplayStep[] = positional(\n jobs.slice(0, firstLinked),\n userMessages.slice(0, cutover),\n );\n\n let cursor = cutover;\n const isSteer = (m: RestoreMessage | undefined): m is RestoreMessage =>\n !!m?.id && !claimed.has(m.id);\n\n /** Emit every steer sitting before `stopAt`, and advance past it. */\n const drainTo = (stopAt: number) => {\n while (cursor < stopAt) {\n const m = userMessages[cursor++];\n if (isSteer(m)) {\n steps.push({ kind: \"steer\", content: m.content, messageId: m.id });\n }\n }\n cursor = stopAt + 1;\n };\n\n for (const job of jobs.slice(firstLinked)) {\n const at = linkOf(job);\n if (at !== undefined) {\n drainTo(at);\n const prompt = userMessages[at]!;\n steps.push({\n kind: \"turn\",\n jobId: job.job_id,\n content: prompt.content,\n messageId: prompt.id,\n });\n continue;\n }\n // Past the cutover, a job whose id matches nothing visible is a goal\n // continuation: its seed is deliberately hidden from the message list, so\n // it replays its events with no bubble — which is what the positional\n // version did by accident when it ran off the end of the messages.\n steps.push({ kind: \"turn\", jobId: job.job_id });\n }\n\n // Steers sent during the final turn sit past every prompt.\n for (let i = cursor; i < userMessages.length; i++) {\n const m = userMessages[i];\n if (isSteer(m)) {\n steps.push({ kind: \"steer\", content: m.content, messageId: m.id });\n }\n }\n\n return steps;\n}\n","/**\n * StreamManager — high-level conversation lifecycle coordinator.\n *\n * Sits on top of ChatSession and manages the state machine for\n * multi-conversation SSE streaming. Framework-agnostic: emits\n * typed events to registered handlers. Block construction is NOT\n * the SDK's concern — consumers build their own block tree from\n * the forwarded ``ChatEvent`` instances.\n *\n * import { ChatSession, StreamManager } from \"@astralform/js\";\n * const session = new ChatSession({ ... });\n * const manager = new StreamManager(session);\n * manager.on((event) => {\n * if (event.type === \"event\") {\n * // event.event is a typed ChatEvent — dispatch to your reducer\n * }\n * });\n * await manager.send(\"Hello\");\n */\nimport { planRestore } from \"./restore-plan\";\n\nimport type { ChatEvent, ModelChoiceOptions } from \"./types.js\";\nimport { ChatEventType } from \"./types.js\";\nimport type { ChatSession } from \"./session.js\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type StreamState = \"idle\" | \"streaming\" | \"restoring\" | \"detached\";\n\nexport interface SendOptions extends ModelChoiceOptions {\n agentName?: string;\n uploadIds?: string[];\n planMode?: boolean;\n /**\n * Attach the image-generation tool to this turn. Per-message and off by\n * default — generating costs the developer real money at a third-party\n * provider. Gate the affordance on `AgentStatus.capabilities`.\n */\n imageMode?: boolean;\n /**\n * Attach the video-generation tool to this turn. Mutually exclusive with\n * `imageMode` at the composer level. See `SendOptions.videoMode` in types.ts\n * for the full rule — a clip animates an existing image, is silent, and holds\n * one shared GPU for minutes, so it is per-message and off by default.\n */\n videoMode?: boolean;\n /**\n * Start a durable long-horizon goal for this run (goal mode) — the text is the\n * goal objective the backend drives to completion. Omit for a normal turn.\n */\n goal?: string;\n /**\n * The project this task belongs to (`owner/repo`), when it has one.\n *\n * Write-once server-side, and the refusal is the part that matters: the FIRST\n * turn that names a repository binds the task, a later turn may omit it or\n * repeat the same value, and a DIFFERENT value is refused (409) rather than\n * ignored — silently acting on the wrong repository is the failure that rule\n * exists to prevent. Omit it entirely and the task is an ordinary chat: since\n * Astralform 0.69.50 there is no agent mode that requires one, so a first turn\n * without it is no longer a 400. Astralform >= 0.69.46.\n */\n repository?: string;\n}\n\nexport type StreamManagerEvent =\n | { type: \"stateChange\"; state: StreamState; conversationId: string | null }\n | { type: \"conversationChanged\"; conversationId: string | null }\n | {\n type: \"backgroundJobsChanged\";\n jobs: ReadonlyMap<string, string>;\n }\n | { type: \"event\"; conversationId: string | null; event: ChatEvent }\n | { type: \"versionsReady\"; conversationId: string; count: number }\n | {\n /**\n * A history replay ran to the end — emitted for EVERY restored\n * conversation, whatever its jobs' statuses. This is the signal to\n * rehydrate per-turn state (attachment chips, composer modes, goal\n * runs) from the jobs endpoint. It exists separately from\n * ``versionsReady`` because that one is completed-only by contract\n * (a version is an answer to switch to), and a conversation whose\n * only turns were stopped or failed still needs this pass — gating\n * rehydration on ``versionsReady`` left such conversations\n * permanently chip-less.\n */\n type: \"restoreSettled\";\n conversationId: string;\n };\n\ntype EventHandler = (event: StreamManagerEvent) => void;\n\n/** One turn as the conversation's job list describes it. */\ninterface RestoreJob {\n job_id: string;\n status: string;\n message_id?: string | null;\n metrics?: Record<string, unknown>;\n}\n\n// =============================================================================\n// StreamManager\n// =============================================================================\n\nexport class StreamManager {\n private session: ChatSession;\n private _state: StreamState = \"idle\";\n private _activeConversationId: string | null = null;\n private _backgroundJobs = new Map<string, string>();\n private handlers: EventHandler[] = [];\n private unsub: (() => void) | null = null;\n /**\n * Bumped every time the active conversation moves. An async sequence that\n * captures it can then tell, at each await boundary, whether it is still the\n * one the user is waiting on — see ``restore``.\n */\n private generation = 0;\n /**\n * Bumped every time a turn STARTS. `generation` does not move for a send\n * (only a pointer move does), and the streaming state returns to idle when a\n * turn ends — so neither can tell a restore that a turn ran inside one of\n * its awaits. This can.\n */\n private turnCounter = 0;\n /**\n * True while a `resync` is between its probe and its restore. Visibility and\n * focus listeners can both fire for one return, and two overlapping resyncs\n * would each detach the other's stream mid-flight — the second call must\n * find the flag set and leave the first to converge.\n */\n private _resyncing = false;\n\n constructor(session: ChatSession) {\n this.session = session;\n this.attach();\n }\n\n // ── Public state ──────────────────────────────────────────────\n\n get state(): StreamState {\n return this._state;\n }\n\n get activeConversationId(): string | null {\n return this._activeConversationId;\n }\n\n get backgroundJobs(): ReadonlyMap<string, string> {\n return this._backgroundJobs;\n }\n\n // ── Event subscription ────────────────────────────────────────\n\n on(handler: EventHandler): () => void {\n this.handlers.push(handler);\n return () => {\n this.handlers = this.handlers.filter((h) => h !== handler);\n };\n }\n\n private emit(event: StreamManagerEvent): void {\n for (const handler of this.handlers) {\n try {\n handler(event);\n } catch {\n // Don't let handler errors crash the manager\n }\n }\n }\n\n private setState(state: StreamState): void {\n this._state = state;\n this.emit({\n type: \"stateChange\",\n state,\n conversationId: this._activeConversationId,\n });\n }\n\n // ── Session event wiring ──────────────────────────────────────\n\n private attach(): void {\n this.unsub = this.session.on((event: ChatEvent) => {\n this.onSessionEvent(event);\n });\n }\n\n private onSessionEvent(event: ChatEvent): void {\n const convId = this.session.conversationId;\n\n // Forward every event to subscribers as a typed envelope\n this.emit({\n type: \"event\",\n conversationId: convId,\n event,\n });\n\n // Handle completion — message_stop is the terminal turn event.\n if (event.type === ChatEventType.MessageStop) {\n // `!isStreaming` discriminates the LIVE stop from one a restore is\n // replaying over a running turn. On the live path the side effects run\n // before this emit, so the flag is already false by the time a real stop\n // arrives; a replayed one leaves it true. Settling on a replayed stop\n // announces idle mid-turn, after which `finalizeStream` and the real\n // stop both no-op — the state `settleIdle` documents.\n if (this._state === \"streaming\" && !this.session.isStreaming) {\n this.setState(\"idle\");\n }\n }\n }\n\n // ── Send ──────────────────────────────────────────────────────\n\n async send(content: string, options?: SendOptions): Promise<void> {\n if ((options?.provider == null) !== (options?.model == null)) {\n throw new Error(\n \"`provider` and `model` must be supplied together (client-side model selection).\",\n );\n }\n if (this._state === \"streaming\") return;\n\n // Auto-create conversation if none active.\n //\n // Deliberately NOT guarded the way `createConversation` is, and the\n // asymmetry is the point: that one is a navigation, so it loses to a\n // switch that lands in its await. This one is not — `send` must have a\n // target to send AT, and declining the pointer move would post the user's\n // composed text into whichever conversation they clicked meanwhile.\n // So `send` wins, and the two halves still agree: `session.send` relocates\n // to the same id a moment later. What it costs is that the bump can\n // supersede a `switchTo` that landed inside the await — recoverable,\n // unlike the sticky case, since the send's own `setState`/`finalizeStream`\n // announce and `_activeConversationId` is no longer that conversation, so\n // re-clicking it works.\n let target = this._activeConversationId;\n if (!target) {\n target = await this.session.createNewConversation();\n // Captured, not re-read below: `setActiveConversation` emits\n // `conversationChanged` synchronously, and a handler routing on the\n // pointer can `switchTo` from inside it — so re-reading would post the\n // text the user composed here into whichever conversation they landed\n // on instead. Same door `setActiveConversation` returns its claimed\n // generation to close.\n this.setActiveConversation(target);\n }\n\n // Counted where the turn is ANNOUNCED, not where the method is entered:\n // `turnStarted` is the one signal that survives a state which never\n // changed, so a bump on a path that returns without starting anything\n // reads to an in-flight restore as a takeover it must yield to. Above,\n // `createNewConversation` can reject and leave exactly that.\n this.turnCounter++;\n this.setState(\"streaming\");\n\n try {\n // Spread, not a hand-copied allowlist: this forward used to name each\n // field, and a field added to the session's options but not here was\n // dropped silently with types that said otherwise — which is exactly how\n // `repository` was lost. The manager's `SendOptions` carries no key the\n // session does not accept, so the spread is equivalent today and cannot\n // drift tomorrow.\n await this.session.send(content, {\n ...options,\n conversationId: target ?? undefined,\n });\n } catch {\n // AbortError from detach is expected\n }\n\n this.finalizeStream();\n }\n\n // ── Regenerate ────────────────────────────────────────────────\n\n async regenerate(): Promise<void> {\n if (this._state === \"streaming\") return;\n // Unlike `send`, regenerate cannot be addressed: `resendFromCheckpoint`\n // takes no conversation override, and the message id comes from\n // `session.messages` — which a settling switch can leave holding the\n // PREVIOUS conversation's list. Pairing that id with any conversation is\n // incoherent, so the only correct move is not to act.\n //\n // Gated on which conversation the MESSAGES belong to, not on the session's\n // conversation pointer. Pointer equality is wrong in the widest case\n // rather than an edge: `loadConversation`\n // assigns the pointer SYNCHRONOUSLY and installs the messages only when the\n // fetch returns, so for the whole duration of every ordinary load the two\n // pointers already agree while `messages` still holds the previous\n // conversation's turns. Regenerating there resends the OLD conversation's\n // last message under the NEW conversation's id.\n //\n // `messagesConversationId` moves with the list itself, so it answers the\n // question that actually matters — are these messages this conversation's?\n // Returns silently, as this method already does for a streaming state and\n // an empty history.\n if (this.session.messagesConversationId !== this._activeConversationId) {\n return;\n }\n\n const userMsgs = this.session.messages.filter(\n (m: { role: string }) => m.role === \"user\",\n );\n const lastUserMsg = userMsgs[userMsgs.length - 1];\n if (!lastUserMsg) return;\n\n // Past BOTH silent returns above, for the reason given at `send`'s bump.\n // The first of them is not an edge during a restore but the ordinary case:\n // `loadConversation` moves the pointer synchronously and installs the list\n // only when its fetch returns, so for that whole window these two disagree\n // and this method returns having done nothing.\n this.turnCounter++;\n this.setState(\"streaming\");\n\n try {\n await this.session.resendFromCheckpoint(\n lastUserMsg.id,\n lastUserMsg.content,\n );\n } catch {\n // AbortError from detach is expected\n }\n\n this.finalizeStream();\n }\n\n // ── Switch conversation ───────────────────────────────────────\n\n /**\n * Switch the active conversation.\n *\n * ``opts.skipHistoryReplay`` is a fast path for consumers that CACHE a\n * restored conversation's rendered blocks: it moves the active pointer and\n * loads the message list (needed for send / regenerate context) but skips\n * the expensive event fetch + replay, and never enters the ``restoring``\n * state, so a consumer that clears its block view on ``restoring`` keeps\n * showing the cached history with no flash of a spinner.\n *\n * It still confirms there is no live job before skipping: the in-memory\n * background-job map is empty on a fresh instance (page reload) and blind to\n * jobs started in another tab/device, so the fast path always asks the server\n * (``getActiveJob``) and falls through to a full reconnect if one is running.\n * That one small request is the only cost it doesn't skip, so passing the\n * flag whenever you hold cached blocks is safe.\n */\n async switchTo(\n conversationId: string,\n opts?: { skipHistoryReplay?: boolean },\n ): Promise<void> {\n if (conversationId === this._activeConversationId) return;\n\n // Capture BEFORE the delete below: a background job THIS instance detached\n // must reconnect, so it can never take the cached fast path.\n const targetHadBackgroundJob = this._backgroundJobs.has(conversationId);\n\n // If streaming, detach (job keeps running in background)\n this.detachStreamingTurn();\n\n // Clear background job for target (we're viewing it now). Captured,\n // because the delete is a CLAIM that this switch will take the job over —\n // see the undo after `restore`.\n const parkedJobId = this._backgroundJobs.get(conversationId);\n if (parkedJobId !== undefined) {\n this._backgroundJobs.delete(conversationId);\n this.emit({\n type: \"backgroundJobsChanged\",\n jobs: this._backgroundJobs,\n });\n }\n\n // Captured from the call itself, not read back afterwards — see\n // `setActiveConversation`.\n const gen = this.setActiveConversation(conversationId);\n\n if (opts?.skipHistoryReplay && !targetHadBackgroundJob) {\n // Cached fast path — but a job started before this instance existed (page\n // reload) or in another tab/device won't be in _backgroundJobs, so confirm\n // with the server that nothing is live before skipping the reconnect.\n let activeJobId: string | null = null;\n try {\n activeJobId = (await this.session.client.getActiveJob(conversationId))\n .jobId;\n } catch {\n // Network error — treat as no active job (best-effort, matches restore()).\n }\n if (gen !== this.generation) return;\n if (!activeJobId) {\n // Consumer already holds the rendered blocks. Load the message list so\n // send/regenerate have their context, but skip the fetch + replay and\n // stay out of the ``restoring`` state.\n // `finally`, for the same reason `restore`'s caller has one:\n // `loadConversation` rejects when the API fetch and the\n // `storage.fetchMessages` fallback both fail, and `detachStreamingTurn`\n // above recorded `idle` WITHOUT announcing on the contract that the\n // caller announces. Returning through a throw leaves the consumer's\n // last `stateChange` at `streaming` with nothing able to clear it —\n // `finalizeStream` and the `message_stop` branch both act only on\n // `streaming`, so the composer stays disabled for the session.\n try {\n await this.session.loadConversation(conversationId);\n } finally {\n if (gen === this.generation) this.settleIdle();\n }\n return;\n }\n // A live job is running — fall through to a full restore(), which\n // reconnects to its stream.\n }\n\n let tookOver = false;\n try {\n await this.restore(conversationId, gen);\n tookOver = gen === this.generation;\n } finally {\n // In a `finally`, because `restore` awaits `loadConversation` unguarded\n // and that rejects when the API fetch AND the `storage.fetchMessages`\n // fallback both fail. `switchTo` then rejects and everything below would\n // be skipped — losing the parked job permanently and leaving `_state` at\n // `restoring` for good, since nothing else announces and neither the\n // next switch nor a send resets it. `switchConversation` already defends\n // this same case on the grounds that `ChatStorage` is public; this path\n // was the inconsistency.\n //\n // Deleting the entry above was a CLAIM that this switch would take the\n // job over. A superseded or throwing restore never does, so the job is\n // left running with no local record: the badge vanishes and\n // `deleteConversation` can no longer cancel it, because\n // `_backgroundJobs.get(id)` is undefined and `wasActive` is false so\n // neither cancel path fires. Not re-added when the switch that\n // superseded us landed here — it owns the display now.\n // Suppressed only when a NEWER switch landed on this same conversation\n // and now owns its display — not merely because the pointer still names\n // it, which is also true when `restore` threw and we are still here.\n const supersededOntoSame =\n gen !== this.generation &&\n this._activeConversationId === conversationId;\n // ...and not for a conversation that was DELETED while we were parked.\n // `deleteConversation` cancels the parked job by reading\n // `_backgroundJobs.get(id)`, which this switch had already emptied — so\n // it found nothing to cancel, and putting the entry back afterwards\n // leaves a running job nothing can stop plus a badge on a conversation\n // no longer in the list. Exactly the harm the cancel branch argues\n // against, reached through the delete path.\n const stillExists = this.session.conversations.some(\n (c) => c.id === conversationId,\n );\n if (\n parkedJobId !== undefined &&\n !tookOver &&\n !supersededOntoSame &&\n stillExists\n ) {\n this._backgroundJobs.set(conversationId, parkedJobId);\n this.emit({\n type: \"backgroundJobsChanged\",\n jobs: this._backgroundJobs,\n });\n }\n // A throw leaves `restoring` announced with no successor to clear it.\n // Only when we are still the current generation: if a newer switch\n // superseded us it owns the announcement.\n if (!tookOver && gen === this.generation && this._state === \"restoring\") {\n this.settleIdle();\n }\n }\n }\n\n // ── Resync after the page sat in the background ───────────────\n\n /**\n * Re-attach to whatever the server says is live for the ACTIVE conversation.\n *\n * The reconnect machinery inside ``consumeEventStream`` only runs while a\n * stream is being consumed — and a page suspended in the background (locked\n * phone, app switch, hidden tab) can outlive it: timers are throttled or\n * suspended, so the stall watchdog may never fire while hidden; the\n * reconnect budget (``SSE_MAX_RECONNECTS``) can burn out in fail-fast\n * attempts; and a 401 from a rotated access token ends the loop outright as\n * non-retryable. What is left is a manager that believes a turn is streaming\n * (or has given up on one that is still running) with nothing attached — and\n * no navigation will ever fix it, because ``switchTo`` early-returns on the\n * conversation it is already on.\n *\n * So the consumer calls this when the user COMES BACK\n * (``visibilitychange → visible``, window ``focus``). One ``getActiveJob``\n * probe, then:\n *\n * - attached to exactly the job the server calls live → healthy. The stall\n * watchdog owns zombie recovery from here, now that timers run again.\n * No-op.\n * - anything else — attached to a job the server no longer calls live,\n * attached to nothing while a job runs, or idle with a live job another\n * tab/device started — → detach and re-run ``restore``, the same path a\n * conversation reopen takes, with all of its supersession and takeover\n * guards inherited.\n *\n * Skipped while a restore is already in flight (it is converging on server\n * truth by itself) and while another resync holds the flag — see\n * ``_resyncing``.\n */\n async resync(): Promise<void> {\n const conversationId = this._activeConversationId;\n if (!conversationId) return;\n if (this._state === \"restoring\" || this._resyncing) return;\n this._resyncing = true;\n // Captured BEFORE the probe, with nothing awaiting between here and the\n // set: a switch moves the generation, a send or regenerate moves the turn\n // counter (never the generation — see `turnCounter`), and whichever lands\n // inside the probe's await owns everything after it. `turnStarted`, not\n // `viewTakenOverByLiveTurn`: the zombie case this method exists for IS\n // `_state === \"streaming\"` with nothing attached, so the streaming state\n // alone cannot mean \"a live send took over\" — but a turn that STARTED\n // since the capture unambiguously is one.\n const gen = this.generation;\n const turn = this.turnCounter;\n try {\n let activeJobId: string | null = null;\n try {\n activeJobId = (await this.session.client.getActiveJob(conversationId))\n .jobId;\n } catch {\n // Can't ask the server — the stall watchdog still owns recovery.\n return;\n }\n if (gen !== this.generation || this.turnStarted(turn)) return;\n // Is this manager attached to a turn at all? `_state` is the synchronous\n // authority; `session.isStreaming` covers the window where a send/restore\n // raised it behind an await (see `viewTakenOverByLiveTurn`).\n const attached = this._state === \"streaming\" || this.session.isStreaming;\n // Nothing attached and nothing live: the ordinary case for a focus event,\n // and the one that must cost nothing beyond the probe above.\n if (activeJobId === null && !attached) return;\n // Attached to exactly the job the server calls live: healthy. The stall\n // watchdog owns zombie recovery from here, now that timers run again.\n if (\n activeJobId !== null &&\n this.session.isStreaming &&\n this.session.currentJobId === activeJobId\n ) {\n return;\n }\n // Attached to a job the server no longer calls live, or attached to\n // nothing while one runs: rebuild from server truth. `session.detach()`\n // aborts whatever socket remains and emits `disconnected` (consumers\n // clear their streaming flags on it) WITHOUT `detachStreamingTurn`'s\n // parking bookkeeping — this job is not becoming a background job, it\n // is about to be re-attached or replayed by the restore below. Its\n // `currentJobId` is cleared for the same reason `detachStreamingTurn`\n // clears it: `detach()` deliberately leaves it, `stop()` has no state\n // guard, and the branches below where the restore does NOT reconnect\n // would leave it naming a job the server just said is not live.\n this.session.detach();\n this.session.currentJobId = null;\n // Un-own the old turn WITHOUT announcing — `restore` announces from\n // here. Same contract as `detachStreamingTurn`'s tail: had this left\n // `streaming` set, `restore`'s `settleIdle` would read it as a live\n // send's state and refuse to announce, stranding the state.\n this._state = \"idle\";\n try {\n await this.restore(conversationId, gen);\n } catch {\n // The `finally` below settles the announced state. Swallowed\n // deliberately: this method is documented to be wired straight into\n // visibility/focus listeners with no `.catch` of their own, and the\n // failure is recoverable — the settle re-enables the next\n // visibility/focus event to retry rather than locking it out.\n }\n } finally {\n this._resyncing = false;\n // `restore` can reject at `loadConversation`, having already announced\n // `restoring`. Only when still current: a newer switch owns the\n // announcement. Without this the state is unrecoverable — the guard at\n // the top of this method locks out every later resync, and `switchTo`\n // early-returns on this very conversation. Compared through an asserted\n // local because the entry guard above narrows `_state` to exclude\n // \"restoring\" for the rest of this function, and `restore()`'s writes\n // to it are invisible to that narrowing.\n const restoring = \"restoring\" as StreamState;\n if (gen === this.generation && this._state === restoring) {\n this.settleIdle();\n }\n }\n }\n\n // ── Create / rename / delete conversation ─────────────────────\n\n /**\n * Create a conversation and make it active.\n *\n * The returned id is NOT guaranteed to be the active conversation: if a\n * switch lands inside the storage round-trip, this declines the pointer move\n * so the newer one wins, and no `conversationChanged` fires for the new id.\n * A caller that routes on the return value should `switchTo(id)` rather than\n * assume it is current — that call is not a no-op in the declined case.\n */\n async createConversation(): Promise<string> {\n // BEFORE `createNewConversation`, which is itself the relocation — it sets\n // `session.conversationId` and empties `session.messages`. `detach()`\n // emits `disconnected`, and `onSessionEvent` tags every event from\n // `session.conversationId`, so tearing down afterwards labels the OLD\n // conversation's teardown with the NEW conversation's id — and does it\n // before `conversationChanged` has fired. `switchTo` detaches while the\n // pointer is still the old one; this is the parity that comment claimed.\n this.detachStreamingTurn();\n let id: string;\n try {\n id = await this.session.createNewConversation();\n } catch (err) {\n // `detachStreamingTurn` above tore the turn down and recorded `idle`\n // without announcing; a rejecting `storage.createConversation` (quota, a\n // network-backed store) would otherwise propagate past both settle\n // points below and leave the composer disabled for good. Same shape\n // `deleteConversation` uses: announce what happened, then rethrow.\n this.settleIdle();\n throw err;\n }\n // A `switchTo` landing inside that await claimed the newer generation, and\n // relocating over it would bump the generation out from under its restore:\n // the consumer sees `conversationChanged: B` followed by this one, and B\n // never restores. Last writer wins, everywhere.\n //\n // Read the SESSION's outcome rather than deriving a second opinion from\n // our own counter. The two are not equivalent in both directions:\n // `setActiveConversation` bumps both, so a superseded manager implies a\n // declining session — but `loadConversation` and a relocating `send` bump\n // `loadGeneration` ALONE, so the session can decline while our generation\n // is untouched and we would relocate over it. Either mismatch leaves the\n // manager and the session naming different conversations, which is worse\n // than either outcome alone: `switchTo` early-returns on the one it thinks\n // is active, so the user cannot click their way out.\n if (this.session.conversationId !== id) {\n // `detachStreamingTurn` above already parked the job, detached the\n // stream and recorded `idle` WITHOUT announcing, on the contract that\n // the caller announces. Every sibling path does — `settleIdle` below,\n // `deleteConversation`'s catch, `switchTo`'s successor restore — and\n // this one did not. When a `switchTo` caused the decline its restore\n // covers the gap, but `loadGeneration` also moves via the public\n // `loadConversation` / `switchConversation`, and then nothing announces\n // and the composer stays disabled after the turn was torn down.\n this.settleIdle();\n return id;\n }\n this.setActiveConversation(id);\n // Settle the state here. `setActiveConversation` bumps the generation, so\n // a restore this supersedes now returns WITHOUT emitting — including\n // without the `idle` that would have cleared a consumer's spinner. Unlike\n // `switchTo`, there is no successor restore to announce it instead, so\n // `_state` would sit at `restoring` on a brand-new empty conversation\n // until the next `send` or `stop` happened to clear it. Being a\n // generation-bumping origin means owning the announcement — but not over\n // a live turn: `createNewConversation` is awaited, and a send landing\n // inside that await owns the streaming state and will finalize it itself.\n this.settleIdle();\n return id;\n }\n\n /**\n * Rename a conversation. Purely a relabel — no active-conversation or\n * background-job bookkeeping to do, unlike delete, so this is a passthrough.\n */\n async renameConversation(id: string, title: string): Promise<void> {\n await this.session.renameConversation(id, title);\n }\n\n async deleteConversation(id: string): Promise<void> {\n // Evaluated up front, because the cancel below has to happen BEFORE the\n // delete: `session.deleteConversation` nulls `conversationId` and empties\n // `messages`, and `disconnect()` emits `disconnected`, which\n // `onSessionEvent` tags from that same field — so cancelling afterwards\n // labels the teardown `null`, before `conversationChanged` has fired. Same\n // ordering fault as `createConversation` had. It also stops the job a\n // round-trip sooner.\n const wasActive = this._activeConversationId === id;\n const cancelled = wasActive && this._state === \"streaming\";\n if (cancelled) {\n // NOT `disconnect()`: it ends in `protocols.clear()`. Deleting one\n // conversation is not a session teardown.\n this.session.cancelTurn();\n this._state = \"idle\"; // cancelled, not parked — recorded, not announced\n }\n try {\n await this.session.deleteConversation(id);\n } catch (err) {\n // The delete did NOT happen. `ChatSession.deleteConversation` rejects\n // either because the server refused it (anything but a 404, which means\n // already-gone) or because `ChatStorage` threw — and both reject BEFORE\n // it filters `conversations` or nulls the pointer. Relocating here would\n // announce a deletion that never occurred, against a session that still\n // lists the conversation and still holds its messages.\n //\n // But the cancel above already tore the stream down and recorded `idle`\n // WITHOUT announcing, so that much has to be announced or the composer\n // stays spinning on a conversation that is neither deleted nor\n // streaming. Announce, relocate nothing, and let the caller see the\n // failure — swallowing it reports success for work that did not happen.\n if (cancelled) this.setState(\"idle\");\n throw err;\n }\n // Re-tested, not `wasActive`: that was read before a real DELETE, and\n // relocating on it would overwrite a switch that landed during the\n // round-trip and bump the generation out from under its restore.\n // `wasActive` is still the right read for the CANCEL, which must happen\n // before the delete.\n if (this._activeConversationId === id) {\n // CANCEL rather than park. `detachStreamingTurn` is right when the user\n // navigates away — the turn keeps running and can be rejoined — but this\n // conversation is gone, so its output has nowhere to land. Parking it\n // would also re-add the very entry deleted below: consumers would render\n // a running-job indicator on a conversation no longer in the list, and\n // `switchTo` would compute `targetHadBackgroundJob` and force a full\n // restore of it.\n this.setActiveConversation(null);\n // Same reason as `createConversation`: this bumps the generation, so any\n // restore it supersedes goes quiet, and nothing else will announce.\n this.settleIdle();\n }\n // AFTER the branch above, so nothing can put the entry back.\n //\n // Emitted, or the consumer's last snapshot keeps a running-job badge on a\n // conversation no longer in the list — the same harm the `wasActive`\n // comment above argues against, on the branch that does not take that fix.\n // Cancelled too, for parity with the active branch above: the conversation\n // is gone either way, so a parked job left running bills tokens for output\n // with nowhere to land. Whether you happened to be watching it when you\n // pressed delete should not decide that.\n const parkedJobId = this._backgroundJobs.get(id);\n if (this._backgroundJobs.delete(id)) {\n if (parkedJobId) {\n this.session.client.cancelJob(parkedJobId).catch(() => {});\n }\n this.emit({ type: \"backgroundJobsChanged\", jobs: this._backgroundJobs });\n }\n }\n\n // ── Stop (explicit cancel) ────────────────────────────────────\n\n stop(): void {\n // `cancelTurn`, not `disconnect`: Stop ends the TURN. `disconnect` ends in\n // `protocols.clear()`, so routing Stop through it dropped every registered\n // `ProtocolAdapter` for the rest of the session — the same harm\n // `deleteConversation` avoids, on the path users actually press.\n this.session.cancelTurn();\n this.setState(\"idle\");\n }\n\n // ── Cleanup ───────────────────────────────────────────────────\n\n destroy(): void {\n if (this.unsub) {\n this.unsub();\n this.unsub = null;\n }\n this.handlers = [];\n }\n\n // ── Internal: helpers ──────────────────────────────────────────\n\n /**\n * Park a streaming turn as a background job and detach from its SSE stream.\n *\n * Every method that relocates the active conversation has to do this before\n * announcing a new state. Announcing `idle` while `session.isStreaming` is\n * still true is worse than announcing nothing: `manager.send` no longer bails\n * on the streaming state, calls `session.send`, and THAT bails on its own\n * `isStreaming` — so the message is never posted, no error is emitted, and\n * the composer looks ready the whole time.\n */\n private detachStreamingTurn(): void {\n if (this._state !== \"streaming\") return;\n const oldConvId = this._activeConversationId;\n const jobId = this.session.currentJobId;\n if (oldConvId && jobId) {\n this._backgroundJobs.set(oldConvId, jobId);\n this.emit({ type: \"backgroundJobsChanged\", jobs: this._backgroundJobs });\n }\n this.session.detach();\n // `detach()` deliberately leaves `currentJobId`, but the job is parked now\n // — it is no longer THIS pointer's turn. Left behind, `stop()` (which has\n // no state guard) calls `cancelTurn()` and cancels the PARKED\n // conversation's job, while `_backgroundJobs` still lists it and emits\n // nothing, so the badge outlives the job. `cancelTurn` documents this\n // exact hazard for its own path.\n this.session.currentJobId = null;\n // Record — without announcing — that the manager no longer owns a live\n // turn. The caller announces, and this is what lets it use `settleIdle()`:\n // a `streaming` state seen there afterwards belongs to a NEW send that\n // landed during the caller's own awaits, which owns its own announcement.\n this._state = \"idle\";\n }\n\n /**\n * Announce `idle` unless a turn is actually streaming.\n *\n * A `send` can land inside any of the switch paths — the fast path most\n * easily, since it deliberately stays out of `restoring` and so leaves the\n * composer live for the whole probe. `send` sets `streaming` and does not\n * bump the generation, so the path resumes, passes its supersession check,\n * and would announce a ready composer over a running stream. From there\n * `finalizeStream` and the `message_stop` branch both no-op (they only act\n * on `streaming`), so it stays `idle` for the whole turn — and the next send\n * reaches `session.send`, which bails on its own `isStreaming`: message\n * never posted, no error, composer ready throughout.\n */\n private settleIdle(): void {\n if (this._state === \"streaming\") return;\n this.setState(\"idle\");\n }\n\n private finalizeStream(): void {\n if (this._state === \"streaming\") {\n this.setState(\"idle\");\n }\n }\n\n // ── Internal: restore ─────────────────────────────────────────\n\n /**\n * A conversation's turns, oldest first.\n *\n * Its own method so ``restore`` can put the request on the wire beside the\n * probe and the message list while ``replayHistory``, which consumes it,\n * keeps owning the shape it reads.\n */\n private jobList(conversationId: string): Promise<RestoreJob[]> {\n return this.session.client.get<RestoreJob[]>(\n `/v1/conversations/${encodeURIComponent(conversationId)}/jobs`,\n );\n }\n\n private async restore(conversationId: string, gen: number): Promise<void> {\n /**\n * Has the user moved on since this restore started?\n *\n * A restore is a long chain of awaits — the active-job probe, the message\n * list, the job list, then every completed turn's events in parallel. That\n * last one is seconds for a large conversation, and clicks are not\n * serialized, so a switch routinely lands mid-chain. Everything after this\n * point either mutates session state the newer switch now owns\n * (``loadConversation``, ``replayTurn``, ``reconnectToJob``) or announces a\n * state the newer switch is responsible for (``setState``), so a superseded\n * restore must stop rather than finish.\n *\n * Left to run, it re-pointed the session at the conversation it was\n * replaying and poured that conversation's whole history out of the event\n * stream, which the consumer rendered into the one on screen.\n *\n * Stopping is safe with a consumer that caches restored blocks: the blocks\n * for this conversation never arrive, so its cache stays empty and the next\n * open takes the full path again rather than the skip-replay fast path.\n */\n const superseded = (): boolean => gen !== this.generation;\n\n // Before the announce and the probe, not after. A handler routing on\n // `conversationChanged` can call `switchTo` synchronously from inside\n // `setActiveConversation`'s emit, so this restore can already be\n // superseded on entry — and announcing here tags `restoring` with the\n // NEWER conversation's id and burns a `getActiveJob` round-trip for one\n // nobody is waiting on. Same re-entrancy door the per-turn check in the\n // replay loop exists for.\n if (superseded()) return;\n // Not over a live turn — the `restoring` analogue of `settleIdle`. The\n // fast path deliberately stays out of `restoring` and leaves the composer\n // live for the whole probe, so a `send` can already own the streaming\n // state by the time a non-null job sends us here. `switchTo`'s own doc\n // says that path exists for \"a consumer that clears its block view on\n // `restoring`\" — announcing it here makes that consumer wipe the turn\n // still streaming into it. The state recovers (the active-job branch\n // re-announces `streaming`); the rendered blocks do not.\n //\n // Captured rather than re-read, because it is also the answer to \"was the\n // consumer told to clear its block view?\", which is what decides whether\n // the history replay below repaints an emptied view or duplicates one that\n // was never emptied. The two questions have the same answer by contract —\n // `restoring` is the documented signal to clear — so they share the flag.\n // Captured with the same timing as `gen`: before anything can await.\n const turn = this.turnCounter;\n const announcedRestoring = !this.session.isStreaming;\n if (announcedRestoring) this.setState(\"restoring\");\n // Re-checked, because `setState` emits SYNCHRONOUSLY and a handler routing\n // on `stateChange` can `switchTo` from inside it — the same re-entrancy\n // door as the check above, one line later. It did not have to be guarded\n // while `loadConversation` sat behind the probe's await and its own\n // supersession check; issuing the load WITH the probe puts it back in this\n // emit's synchronous path, where an unguarded load runs AFTER the newer\n // switch's and so claims the newer token — leaving the session holding the\n // abandoned conversation's id and messages under a manager pointing at the\n // one the user chose.\n //\n // Returning having announced `restoring` is what the check after the probe\n // already did: the superseding switch announces and settles its own state.\n if (superseded()) return;\n\n // The three requests a restore opens with, issued TOGETHER. None is an\n // input to another — the active-job probe, the message list and the job\n // list are all addressed by `conversationId` alone — so serially they cost\n // the SUM of three round trips before a single event is asked for, and any\n // one that stalls blocks the two behind it. Fired together, the restore\n // waits for the slowest instead.\n //\n // What that does NOT change is the order the results are consumed in.\n // `loadConversation` still lands before the replay, because the replay\n // reads the list it installs; parallelising the fetch moves when the\n // request leaves, not when its effect is observed.\n //\n // The two supersession checks either side of `loadConversation` collapse\n // into the one after the joint await: a joint await is still a single\n // await boundary. What does NOT collapse with them is the check above,\n // which now covers the announcement rather than the probe — the load is\n // synchronous with the emit, so that is where the door it has to close\n // moved to. `loadConversation` moving the session pointer sooner is\n // otherwise safe for the reason it is safe at all: `setActiveConversation`\n // bumps the session's load token, so an ASYNCHRONOUS switch landing in this\n // window makes the load in flight lose and its snapshot is never installed.\n const probeRequest = this.session.client\n .getActiveJob(conversationId)\n // Network error — assume no active job.\n .catch(() => null);\n const loadRequest = this.session.loadConversation(conversationId);\n // Handed to `replayHistory` rather than joined here, so that a stalled job\n // list cannot hold up the checks below, and so that its failure still\n // arrives inside the try that already treats a failed history load as\n // non-blocking. The `catch` is only to mark the rejection handled for the\n // paths that reach neither (superseded, or a live turn holding the view) —\n // `replayHistory` awaits the original and handles it itself.\n //\n // Gated on `announcedRestoring`, which `replayHistory` is gated on too:\n // reopening a conversation whose turn is still live takes the reconnect\n // and never replays, so fetching the list there would buy a whole round\n // trip to throw away — a regression in the one thing this change is about.\n // The flag is known synchronously, so keeping it costs no serialisation.\n // The other two discard paths cannot be gated the same way: both are\n // answers that only exist after the await this request is racing.\n const jobsRequest = announcedRestoring\n ? this.jobList(conversationId)\n : null;\n void jobsRequest?.catch(() => {});\n\n const [probe] = await Promise.all([probeRequest, loadRequest]);\n const activeJobId = probe?.jobId ?? null;\n if (superseded()) return;\n\n // History first, in BOTH branches. A live turn used to skip it entirely and\n // reconnect to the running job alone — but a prompt is not in `job_events`\n // (it lives in the messages table, see below), and neither is any earlier\n // turn, so everything except the running turn's own blocks was missing for\n // as long as the turn lasted. Long tool calls made that a matter of\n // minutes, which is exactly when a user switches away and back.\n //\n // Two conditions, answering two different questions, because either one\n // alone replays over a view that still holds content:\n //\n // - `announcedRestoring` — did we tell the consumer to clear? If we never\n // did, it still holds the blocks it rendered and replaying appends to\n // them. This applies to the SETTLED path too, which used to replay\n // regardless: that case now shows no history until the next open, which\n // is the same trade the live path takes and for the same reason — the\n // view was never cleared, so neither outcome is coherent.\n // - has a live turn taken the view over SINCE? `send` bails only on\n // `streaming` and we sit in `restoring`, so nothing gates a send for\n // the whole restore: it goes through, clears nothing, and renders its\n // own optimistic prompt. Replaying then re-emits the prompt it just\n // drew and appends the history under it.\n //\n // The pair is not redundant: a stream that ENDS during the probe leaves\n // no live turn over a view that was never cleared.\n //\n // They are separate statements rather than one `&&` because the answers\n // differ in what they forbid. A takeover means NEITHER half below is ours,\n // so this returns rather than\n // short-circuiting the `&&` into the reconnect: replaying lands under the\n // bubble the send drew, and the reconnect opens the running turn's stream\n // under it — `reconnectToJob`'s own `isStreaming` bail cannot see a send\n // still inside `storage.addMessage`, for the same one-await reason\n // `viewTakenOverByLiveTurn` exists. Gated on `announcedRestoring` so the\n // reading is unambiguous (we set `restoring` ourselves, so anything else is\n // a send or regenerate) and the never-cleared path still falls through to\n // the reconnect exactly as before.\n if (announcedRestoring && this.viewTakenOverByLiveTurn()) return;\n // Tested on `jobsRequest` rather than `announcedRestoring`: the two are the\n // same condition by construction above, and this spelling is the one that\n // narrows the request away from null.\n if (\n jobsRequest &&\n !(await this.replayHistory(\n conversationId,\n gen,\n activeJobId,\n turn,\n jobsRequest,\n ))\n )\n return;\n\n if (activeJobId) {\n this.setState(\"streaming\");\n try {\n await this.session.reconnectToJob(activeJobId);\n } catch {\n // Stream ended or aborted\n }\n // A switch during the stream already detached it and parked the job in\n // ``_backgroundJobs``; the newer switch owns the state from there.\n if (superseded()) return;\n // Discriminated on the SESSION: `_state === \"streaming\"` is also what a\n // `send` landing during the probe sets, and `reconnectToJob` bails on\n // `isStreaming` without reconnecting anything — so announcing `idle` here\n // lands over a running turn (see `settleIdle` for why that is\n // unrecoverable). `settleIdle` itself does not fit; this branch sets\n // `streaming` itself, so its test cannot tell the two cases apart.\n if (this._state === \"streaming\" && !this.session.isStreaming) {\n this.setState(\"idle\");\n }\n } else {\n // Re-checked even though `replayHistory` reports supersession: it does\n // not run at all when we never announced `restoring`, and it swallows a\n // failure that may have left the chain part-way. Either way this\n // announcement belongs to whichever switch is current.\n if (superseded()) return;\n this.settleIdle();\n }\n }\n\n /**\n * Has a live turn taken the block view over?\n *\n * ``_state`` is the SYNCHRONOUS authority and ``session.isStreaming`` lags it\n * by an await: ``send`` sets ``_state = \"streaming\"`` before its first await,\n * while the session only raises its flag inside ``processStream``, behind the\n * ``storage.addMessage`` write. For that whole window a send is underway —\n * composer cleared, optimistic bubble drawn — and the session flag still\n * reads false. Reading both closes the window from either end, since\n * ``reconnectToJob`` is the mirror case: it raises the session flag without\n * ever moving ``_state``.\n */\n private viewTakenOverByLiveTurn(): boolean {\n return this._state === \"streaming\" || this.session.isStreaming;\n }\n\n /**\n * Has a turn STARTED since ``turn`` was captured?\n *\n * ``viewTakenOverByLiveTurn`` reads the current state, so it cannot see a\n * turn that both started and ENDED inside one of the restore's awaits — a\n * send that fails fast (auth, rate limit) resolves in about the time the job\n * list takes, and leaves `_state` back at idle with its blocks already\n * rendered. A monotonic count is the only thing that survives a state that\n * has returned to where it started.\n */\n private turnStarted(since: number): boolean {\n return this.turnCounter !== since;\n }\n\n /**\n * Replay a conversation's persisted history into the consumer's block view.\n *\n * Returns false when this restore lost the right to finish — a newer switch\n * superseded it, or a send took the view over — in which case the caller\n * must stop rather than finish. See ``restore``.\n *\n * ``activeJobId`` names the turn that is still running, if any. Its events\n * are NOT fetched here: they are the live stream the caller reconnects to\n * straight after. It is passed so ``planRestore`` can pair it with the prompt\n * that started it, which is emitted as a bubble with no events — the whole\n * reason a conversation reopened mid-turn now shows the message that started\n * that turn.\n *\n * ``jobsRequest`` is the job list already IN FLIGHT — issued by ``restore``\n * alongside the probe and the message list rather than fetched here, so the\n * three round trips overlap. It is awaited inside the try below, which is\n * what keeps a failed job list non-blocking exactly as it was when the fetch\n * lived here.\n */\n private async replayHistory(\n conversationId: string,\n gen: number,\n activeJobId: string | null,\n turn: number,\n jobsRequest: Promise<RestoreJob[]>,\n ): Promise<boolean> {\n // Three ways to lose the right to replay, checked at every await boundary\n // below because all of them arrive from outside this function while it\n // waits: a newer switch (the generation); a turn holding the view right\n // now; and a turn that has already come and gone inside one of these\n // awaits, which the state check cannot see because the state is back where\n // it started. Nothing gates `send` during a restore — see the caller. The\n // caller treats any of them as \"stop\": a turn that took over owns the\n // state, so reconnecting the one we were restoring would open a second\n // stream under it.\n const stopReplay = (): boolean =>\n gen !== this.generation ||\n this.viewTakenOverByLiveTurn() ||\n this.turnStarted(turn);\n try {\n const jobs = await jobsRequest;\n if (stopReplay()) return false;\n // Every job EXCEPT the one we are about to reconnect to. The probe and\n // this list now LEAVE together, but they are still answered\n // independently, so a turn that ENDS between the two replies comes back\n // settled here while `activeJobId` still names it — putting the same job\n // in the replay set AND `runningJob`, which `planRestore` walks twice and\n // `eventsByJobId` then replays twice, before the reconnect delivers it a\n // third time. The window is narrower than it was (it used to span\n // `loadConversation` too) but it does not close, so the exclusion stays.\n //\n // Excluding it settles both halves at once: the job we reconnect to is\n // never in the events wave and can only enter the plan as the running\n // turn, so the live stream is its single source either way — a reconnect\n // to a job that has just finished still drains its whole event log.\n //\n // This used to also require `status === \"completed\"`, which silently made\n // a FAILED turn unrecoverable. Its events are persisted exactly like any\n // other — `job_events` is the forensic record, and the history endpoint\n // returns a failed job's stream complete, terminal `error` event and all —\n // but restore never asked for them, so the whole turn vanished on reload:\n // the tool calls, their output, and the error that explains why it\n // stopped. A conversation whose ONLY job failed came back blank.\n //\n // Status is the wrong axis for this decision. What decides whether a job\n // belongs in the events wave is where its events COME FROM: the live\n // stream for the one being reconnected to, storage for every other. How a\n // turn ended says nothing about that, and a client that hides failed turns\n // does not make them not have happened — it just stops the user seeing\n // what the agent did before it stopped.\n const replayableJobs = jobs.filter(\n (j: { job_id: string }) => j.job_id !== activeJobId,\n );\n\n // User prompts aren't persisted in job_events — they live in the\n // messages table, so each turn has to be paired with the message that\n // started it. `job.message_id` is that link; planRestore also decides\n // where mid-run steers (user messages that start no job) and goal\n // continuations (jobs with no visible prompt) belong. See\n // restore-plan.ts for why pairing by index was wrong.\n const userMessages = this.session.messages.filter(\n (m) => m.role === \"user\",\n );\n // Matched against the job LIST rather than trusted from the probe: the\n // prompt pairing needs the running job's `message_id`, which only the\n // list carries. A probe id absent from the list (raced purge) leaves\n // `runningJob` undefined — and because `claimedMessageIds` is derived\n // from that same list, its prompt is then unclaimed and surfaces as a\n // steer bubble instead of vanishing.\n const runningJob = activeJobId\n ? jobs.find((j) => j.job_id === activeJobId)\n : undefined;\n const plan = planRestore({\n completedJobs: replayableJobs.map((j) => ({\n job_id: j.job_id,\n message_id: j.message_id,\n })),\n runningJob: runningJob && {\n job_id: runningJob.job_id,\n message_id: runningJob.message_id,\n },\n // EVERY job, which is the same set the replay walk gets. The claim set\n // answers one question — \"will some turn step already draw this prompt?\"\n // — so it is a RESTATEMENT of the walk, and the two drifting apart is\n // what produces either a missing bubble or a doubled one.\n //\n // Failed and cancelled were excluded here for exactly one reason: they\n // produced no turn step, which is the bug fixed above. Now that they do,\n // the exclusion has no case left to describe.\n //\n // Being precise about what this change does and does not do: it is not\n // load-bearing TODAY. `planRestore` anchors a prompt at the index its\n // job links to and advances the cursor past it, so a message claimed by\n // a job inside the walk is never offered to the steer branch anyway —\n // the two spellings agree on current inputs. It is here because the\n // invariant is what keeps them agreeing: narrow the walk again without\n // narrowing this, and the prompts of the jobs dropped from it go with\n // them, silently. Deriving both from `jobs` makes that impossible to\n // get half-right.\n claimedMessageIds: jobs.map((j) => j.message_id),\n userMessages: userMessages.map((m) => ({\n id: m.id,\n content: m.content,\n })),\n });\n\n // Fetch every turn's events up front, in PARALLEL. The backend strips\n // live-only deltas from this path, so each response is small; parallel\n // fetch collapses N serial round-trips into one wave. We still fetch\n // per job (not the whole conversation in one call) so superseded\n // regeneration versions stay available for version navigation — the\n // whole-conversation endpoint drops them.\n //\n // The running turn is absent by construction (excluded above): its events\n // are the live stream the caller reconnects to, and fetching them here\n // would replay every block it is about to receive again.\n const eventLists = await Promise.all(\n replayableJobs.map((job: { job_id: string }) =>\n this.session.client\n .getConversationEvents(conversationId, job.job_id)\n .catch(() => []),\n ),\n );\n // THE window. This wave is the slow part of a restore — the events of\n // every completed turn — and a click during it is the ordinary case,\n // not a rare one. The fetched events are discarded rather than\n // replayed: the replay below is what re-points the session and floods\n // the consumer.\n if (stopReplay()) return false;\n\n const eventsByJobId = new Map(\n replayableJobs.map((job, i) => [job.job_id, eventLists[i] ?? []]),\n );\n\n // Replay every step in one SYNCHRONOUS pass (no awaits between events\n // or turns), so the consumer batches the whole history into a single\n // render instead of re-typing it event by event. A steer replays as a\n // turn with no events: the bubble, and nothing after it.\n for (const step of plan) {\n // Checked per TURN, not just before the loop: \"synchronous\" bounds\n // out awaits, not re-entrancy. `replayTurn` emits through\n // `onSessionEvent` to every handler, and nothing in the `on()`\n // contract stops a handler driving the manager straight back —\n // `switchTo`, `createConversation` and `deleteConversation` all bump\n // the generation from inside this loop. Without this the remaining\n // turns keep pouring out, tagged with the abandoned conversation's\n // id, which is the leak this guard exists to close, reached through\n // the one door an await boundary does not cover.\n if (stopReplay()) return false;\n if (step.kind === \"steer\") {\n this.session.replayTurn(\n conversationId,\n [],\n step.content,\n step.messageId,\n true,\n );\n continue;\n }\n // The running turn resolves to no entry here by design, so this emits\n // its prompt bubble and nothing else — and does so BEFORE the caller\n // reconnects, which is what keeps the bubble above the agent header\n // the live stream's `message_start` is about to open.\n this.session.replayTurn(\n conversationId,\n eventsByJobId.get(step.jobId) ?? [],\n step.content,\n step.messageId,\n );\n }\n\n // Before the announcements, not only before `setState` below. The\n // loop's check runs at the TOP of each turn, so a handler that\n // navigates away while the LAST turn replays — or the only turn, for a\n // single-job conversation — exits the loop normally with no iteration\n // left to catch it, and this would fire for the abandoned\n // conversation.\n if (stopReplay()) return false;\n\n // Two announcements, split on purpose. ``restoreSettled`` fires for\n // every replay that ran to the end: it is the rehydration signal\n // (attachment chips, composer modes, goal runs), and a conversation\n // whose only turns were stopped or failed still needs that pass.\n this.emit({ type: \"restoreSettled\", conversationId });\n\n // COMPLETED only, deliberately narrower than the replay set. This drives\n // version navigation, and a version is an answer the user can switch to —\n // a failed turn produced none, so counting it would offer a version that\n // does not exist. Widening the replay set is about showing what happened;\n // this is about what can be navigated between.\n const versionCount = replayableJobs.filter(\n (j: { status: string }) => j.status === \"completed\",\n ).length;\n if (versionCount > 0) {\n this.emit({\n type: \"versionsReady\",\n conversationId,\n count: versionCount,\n });\n }\n } catch {\n // History load failed — non-blocking. A live turn still reconnects, and\n // a settled one still announces idle; the transcript is what is lost,\n // exactly as before this was hoisted out of the completed-only branch.\n }\n return !stopReplay();\n }\n\n // ── Internal: set active conversation ─────────────────────────\n\n private setActiveConversation(id: string | null): number {\n this._activeConversationId = id;\n // The session's load token moves at the same instant as this one. Both\n // halves of a create then consult a counter that has actually changed —\n // otherwise `switchTo` bumps `generation` synchronously while\n // `loadGeneration` waits on the active-job probe, and for that whole\n // window the manager sees itself superseded and the session does not.\n this.session.invalidateLoadsInFlight();\n // EVERY move of the pointer bumps the generation, not just `switchTo`:\n // creating a conversation and deleting the active one relocate the user\n // just as much, and an in-flight restore has to yield to those too.\n const claimed = ++this.generation;\n // Returned so callers capture the generation THIS move claimed, before the\n // emit below. A handler reacting to `conversationChanged` by calling back\n // into the manager — routing on the conversation pointer is the obvious\n // consumer shape — bumps again synchronously, so a caller reading\n // `this.generation` afterwards would capture the INNER value and never see\n // itself as superseded. Both switches would then run to completion and the\n // abandoned one would replay its whole history: the same re-entrancy door\n // the replay loop already guards against.\n this.emit({ type: \"conversationChanged\", conversationId: id });\n return claimed;\n }\n}\n","// =============================================================================\n// Event replay — translate persisted wire events into ChatEvent sequences\n//\n// The backend persists wire events in the `job_events` table. When a\n// consumer needs to restore a conversation (page refresh, conversation\n// switch), it fetches these raw events and replays them through the same\n// ChatEvent pipeline used during live streaming.\n//\n// All wire → ChatEvent translation lives in `translate.ts`. This file just\n// adapts the persisted envelope shape (`{seq, event, data}`) and handles\n// user-message interleaving, which the backend doesn't record.\n// =============================================================================\n\nimport { translateWireEvent } from \"./translate.js\";\nimport type { ChatEvent, WireEvent } from \"./types.js\";\n\n/**\n * Raw SSE event shape returned by GET /v1/conversations/{id}/events.\n * Mirrors what JobEventWriter persists to the job_events table.\n */\nexport interface RawSseEvent {\n seq: number;\n event: string;\n data: Record<string, unknown>;\n /** Epoch ms when the event was persisted (from job_events.created_at). */\n created_at?: number;\n}\n\n/**\n * Build a ``WireEvent`` from the persisted envelope. The data payload is\n * authoritative (and always carries ``type`` in practice); the SSE event\n * name is a fallback for older rows that pre-date the v2 protocol.\n */\nfunction toWireEvent(raw: RawSseEvent): WireEvent | null {\n const type = (raw.data.type as string) || raw.event;\n if (!type || type === \"done\") return null;\n return { ...raw.data, type } as unknown as WireEvent;\n}\n\n/**\n * Map a raw SSE event (persisted in job_events) into the SDK ChatEvent\n * format. Returns an array because some rows (malformed / ``done`` sentinels)\n * map to zero events.\n */\nexport function mapSseToChat(raw: RawSseEvent): ChatEvent[] {\n const wire = toWireEvent(raw);\n if (!wire) return [];\n const event = translateWireEvent(wire);\n return event ? [event] : [];\n}\n\n/**\n * Replay persisted SSE events through the provided handler, interleaving\n * user messages from session.messages at the START of each turn (user\n * messages aren't persisted in job_events).\n *\n * The turn boundary is a change of ``job_id``, NOT ``message_start`` or\n * ``message_stop``. A completed job maps to exactly one user turn — but a\n * single job can contain several ``message_start``/``message_stop`` pairs (a\n * tool-use loop: LLM call → tool result → LLM call again), so neither event\n * reliably delimits turns. And within a job some events precede the first\n * ``message_start`` (e.g. ``memory_recall``, emitted during prompt prep), so\n * gating the user block on ``message_start`` would replay them above the\n * user's own message. Keying off ``job_id`` injects the prompt once per job,\n * before its first event — matching how the restore path\n * (``stream-manager.ts#restore`` → ``session.ts#replayTurn``) leads each turn\n * with its own prompt.\n */\nexport function replayEvents(\n sseEvents: RawSseEvent[],\n userMessages: { role: string; content: string }[],\n handleEvent: (event: ChatEvent) => void,\n addBlock: (block: { type: \"user\"; id: string; content: string }) => void,\n): void {\n const userMsgs = userMessages.filter((m) => m.role === \"user\");\n let userIdx = 0;\n let currentJobId: string | null = null;\n\n for (const raw of sseEvents) {\n const type = (raw.data.type as string) || raw.event;\n if (!type || type === \"done\") continue;\n\n // A new job_id starts a new user turn — inject its prompt before the\n // job's first event. Events without a job_id stay within the current\n // turn (never a boundary), so a stray untagged event can't misfire.\n const jobId = (raw.data.job_id as string | undefined) ?? null;\n if (jobId !== null && jobId !== currentJobId) {\n currentJobId = jobId;\n if (userIdx < userMsgs.length) {\n addBlock({\n type: \"user\",\n id: `replay_user_${userIdx}`,\n content: userMsgs[userIdx]!.content,\n });\n userIdx++;\n }\n }\n\n for (const ce of mapSseToChat(raw)) {\n handleEvent(ce);\n }\n }\n}\n","// =============================================================================\n// Embedded Resource detection — protocol-agnostic\n// =============================================================================\n//\n// The backend can emit rich UI surfaces (A2UI, and future protocols) by\n// wrapping a tool result in an MCP-style embedded resource:\n//\n// {\n// \"_embedded_resource\": true,\n// \"mime_type\": \"application/json+a2ui\",\n// \"uri\": \"a2ui://surface/<id>\",\n// \"payload\": { ...protocol-specific... }\n// }\n//\n// The SDK stays protocol-agnostic: it exposes a detector/parser so\n// frontends can route the payload to a registered renderer for the\n// matching MIME type. The SDK itself never imports a renderer.\n// =============================================================================\n\n/**\n * Parsed shape of an MCP-style embedded resource, as emitted by the\n * backend's UI component tools (``render_surface``, ``update_surface``).\n */\nexport interface EmbeddedResource {\n /** IANA-style MIME type, e.g. \"application/json+a2ui\". */\n mimeType: string;\n /** Opaque URI (e.g. \"a2ui://surface/my-form\"). */\n uri: string;\n /** Protocol-specific payload — shape depends on ``mimeType``. */\n payload: Record<string, unknown>;\n}\n\n/**\n * Detect whether a value is an embedded resource wrapper. The check is\n * purposely loose — any object with ``_embedded_resource: true`` is\n * accepted, which matches the MCP convention and keeps the SDK\n * forward-compatible with future protocols.\n */\nexport function isEmbeddedResource(value: unknown): value is {\n _embedded_resource: true;\n mime_type?: string;\n uri?: string;\n payload?: Record<string, unknown>;\n} {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { _embedded_resource?: unknown })._embedded_resource === true\n );\n}\n\n/**\n * Parse an embedded resource from arbitrary tool output. Returns\n * ``null`` when the value isn't an embedded resource or is malformed.\n *\n * Accepts either an object (the preferred wire format) or a JSON\n * string containing one (defense in depth — some transport layers\n * historically serialized tool results before sending).\n */\nexport function parseEmbeddedResource(value: unknown): EmbeddedResource | null {\n let candidate: unknown = value;\n if (typeof candidate === \"string\") {\n // Only try JSON.parse if it plausibly looks like a JSON object\n // starting with `{`. Avoids pathological input.\n const trimmed = candidate.trim();\n if (!trimmed.startsWith(\"{\")) return null;\n try {\n candidate = JSON.parse(trimmed);\n } catch {\n return null;\n }\n }\n if (!isEmbeddedResource(candidate)) return null;\n const mimeType = candidate.mime_type;\n const uri = candidate.uri;\n const payload = candidate.payload;\n if (typeof mimeType !== \"string\" || !mimeType) return null;\n if (typeof uri !== \"string\" || !uri) return null;\n if (!payload || typeof payload !== \"object\") return null;\n return {\n mimeType,\n uri,\n payload: payload as Record<string, unknown>,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/utils.ts","../src/rate-limit.ts","../src/streaming.ts","../src/types.ts","../src/client.ts","../src/storage.ts","../src/tools.ts","../src/protocol-registry.ts","../src/translate.ts","../src/session.ts","../src/restore-plan.ts","../src/stream-manager.ts","../src/replay.ts","../src/embedded-resource.ts"],"sourcesContent":["// Core classes\nexport { AstralformClient, parseVoicePolishFrame } from \"./client.js\";\nexport { ChatSession, CONVERSATION_PAGE_SIZE } from \"./session.js\";\nexport { ToolRegistry, type ToolHandler } from \"./tools.js\";\nexport { InMemoryStorage, type ChatStorage } from \"./storage.js\";\n\n// Errors\nexport {\n AstralformError,\n AuthenticationError,\n RateLimitError,\n LLMNotConfiguredError,\n ServerError,\n ConnectionError,\n StreamAbortedError,\n} from \"./errors.js\";\nexport type { RateLimitErrorDetails } from \"./errors.js\";\n\n// Utilities\nexport { generateId } from \"./utils.js\";\n\n// Streaming\nexport { streamJobSSE } from \"./streaming.js\";\n\n// Stream manager\nexport { StreamManager } from \"./stream-manager.js\";\nexport type {\n StreamState,\n SendOptions,\n StreamManagerEvent,\n} from \"./stream-manager.js\";\n\n// Delta translator (shared between session and replay)\nexport { translateDelta } from \"./translate.js\";\n\n// Event replay (for restoring conversations from persisted events)\nexport { mapSseToChat, replayEvents } from \"./replay.js\";\nexport type { RawSseEvent } from \"./replay.js\";\n\n// Embedded resource detection (protocol-agnostic UI surface helper)\nexport {\n isEmbeddedResource,\n parseEmbeddedResource,\n} from \"./embedded-resource.js\";\nexport type { EmbeddedResource } from \"./embedded-resource.js\";\n\n// Protocol adapter registry — consumers register framework-specific\n// renderers for embedded resource MIME types.\nexport { ProtocolRegistry } from \"./protocol-registry.js\";\nexport type { ProtocolAdapter } from \"./protocol-registry.js\";\n\n// Event type constants\nexport { ChatEventType } from \"./types.js\";\nexport type { ChatEventTypeValue } from \"./types.js\";\n\n// High-level ChatEvent (SDK → consumer)\nexport type { ChatEvent, BlockDeltaPayload, TurnUsage } from \"./types.js\";\n\n// Custom event payload catalog\nexport type {\n AgentIdentity,\n TaskStatus,\n TodoItem,\n TodoUpdatePayload,\n PlanUpdatePayload,\n NoteUpdatePayload,\n TitleGeneratedPayload,\n SubagentStartPayload,\n SubagentStopPayload,\n ContextWarningPayload,\n ContextUpdatePayload,\n MemoryRecord,\n MemoryRecallPayload,\n MemoryUpdatePayload,\n DesktopStreamPayload,\n AttachmentStagedPayload,\n WorkspaceReadyPayload,\n AssetCreatedPayload,\n ToolApprovalRequestedPayload,\n ToolApprovalGrantedPayload,\n ToolPermissionDeniedPayload,\n ToolHarnessWarningPayload,\n UserUnavailablePayload,\n PromptSuggestionPayload,\n} from \"./custom-events.js\";\n\n// Wire protocol types (for consumers that want to parse the raw SSE\n// data themselves or write their own transport adapter)\nexport type {\n WireEvent,\n WireMessageStart,\n WireMessageStop,\n WireBlockStart,\n WireBlockDelta,\n WireBlockStop,\n WireStallWarning,\n WireRetryEvent,\n WireErrorEvent,\n WireKeepalive,\n WireCustomEvent,\n WireBlockKind,\n WireBlockStatus,\n WireStopReason,\n WireBlockDeltaPayload,\n WireTextDelta,\n WireThinkingDelta,\n WireSignatureDelta,\n WireInputDelta,\n WireInputArgDelta,\n WireOutputDelta,\n WireStatusDelta,\n} from \"./types.js\";\n\n// Config + domain models\nexport type {\n AstralformConfig,\n AstralformApiKeyConfig,\n AstralformUserTokenConfig,\n Conversation,\n Message,\n AgentStatus,\n AgentCapability,\n UIComponentsConfig,\n AgentInfo,\n SkillInfo,\n ModelOption,\n ModelChoiceOptions,\n TeamSummary,\n TeamAgentSummary,\n} from \"./types.js\";\n\n// Request / response types\nexport type {\n ChatStreamRequest,\n CodeProject,\n AvailableRepository,\n AvailableRepositories,\n EffortRung,\n ReasoningEffort,\n ThinkingDescriptor,\n ThinkingRungOption,\n ToolResultRequest,\n ToolResult,\n ToolDefinition,\n ToolApprovalRequest,\n ToolApprovalDecision,\n ToolApprovalScope,\n ToolGrant,\n MyToolGrantsPage,\n ToolCallRequest,\n JobCreateResponse,\n JobStatus,\n JobSummary,\n ActiveJob,\n FeedbackRequest,\n FeedbackResponse,\n ConversationAsset,\n StreamJobSSEOptions,\n ChatStreamEvent,\n ConversationEvent,\n VoiceConfig,\n VoiceLLMMode,\n VoicePolishEvent,\n VoicePolishMode,\n VoicePolishRequest,\n VoiceTranscribeOptions,\n VoiceTranscript,\n} from \"./types.js\";\nexport { VOICE_POLISH_MODES, isVoiceLLMMode, isVoicePolishMode } from \"./types.js\";\n","export interface RateLimitErrorDetails {\n retryAfterSec?: number;\n resetAt?: number;\n scope?: string;\n policyId?: string;\n limit?: number;\n remaining?: number;\n requestId?: string;\n}\n\nexport class AstralformError extends Error {\n constructor(\n message: string,\n public code: string,\n ) {\n super(message);\n this.name = \"AstralformError\";\n }\n}\n\nexport class AuthenticationError extends AstralformError {\n constructor(message = \"Invalid or missing API key\") {\n super(message, \"authentication_error\");\n this.name = \"AuthenticationError\";\n }\n}\n\nexport class RateLimitError extends AstralformError {\n declare readonly retryAfterSec?: number;\n declare readonly resetAt?: number;\n declare readonly scope?: string;\n declare readonly policyId?: string;\n declare readonly limit?: number;\n declare readonly remaining?: number;\n declare readonly requestId?: string;\n\n constructor(\n message = \"Rate limit exceeded\",\n details: RateLimitErrorDetails = {},\n ) {\n super(message, \"rate_limit_error\");\n this.name = \"RateLimitError\";\n Object.assign(this, details);\n }\n}\n\nexport class LLMNotConfiguredError extends AstralformError {\n constructor(message = \"LLM provider not configured for this agent\") {\n super(message, \"llm_not_configured\");\n this.name = \"LLMNotConfiguredError\";\n }\n}\n\nexport class ServerError extends AstralformError {\n /**\n * The HTTP status, when this came from a response.\n *\n * Every status except 401 and 429 collapses into this one class, so without\n * it a caller cannot tell \"the thing is already gone\" (404) from \"the server\n * broke\" (500) or \"this is not yours\" (403) — the message is the only other\n * signal and it is prose. `deleteConversation` is the case that forced it:\n * it read EVERY failure as already-deleted and dropped the conversation\n * locally regardless, so a 500 looked exactly like success and the row came\n * back on the next device.\n *\n * Undefined when a ServerError is constructed without a response.\n */\n declare readonly status?: number;\n\n constructor(message = \"Internal server error\", status?: number) {\n super(message, \"server_error\");\n this.name = \"ServerError\";\n if (status !== undefined) Object.assign(this, { status });\n }\n}\n\nexport class ConnectionError extends AstralformError {\n constructor(message = \"Failed to connect to server\") {\n super(message, \"connection_error\");\n this.name = \"ConnectionError\";\n }\n}\n\nexport class StreamAbortedError extends AstralformError {\n constructor(message = \"Stream was aborted\") {\n super(message, \"stream_aborted\");\n this.name = \"StreamAbortedError\";\n }\n}\n","export function sanitizeErrorText(text: string): string {\n return text.slice(0, 500).replace(/Bearer\\s+\\S+/gi, \"Bearer [REDACTED]\");\n}\n\nexport function generateId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Convert a snake_case string to camelCase.\n */\nfunction snakeToCamel(str: string): string {\n return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());\n}\n\n/**\n * Transform all keys of an object from snake_case to camelCase.\n * Unknown fields pass through by default — a field the API adds and this\n * function does not name is still delivered to the consumer, just under its\n * camelCase name. Only values that need derivation (defaults, coercion,\n * filtering, nesting) should keep hand-mapping.\n *\n * @example\n * ```ts\n * const raw = { message_count: 5, created_at: \"2026-01-01T00:00:00Z\" };\n * const result = camelizeKeys(raw);\n * // → { messageCount: 5, createdAt: \"2026-01-01T00:00:00Z\" }\n * ```\n */\nexport function camelizeKeys<T>(\n obj: Record<string, unknown>,\n): T {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n result[snakeToCamel(key)] = obj[key];\n }\n return result as T;\n}\n","import { RateLimitError, type RateLimitErrorDetails } from \"./errors.js\";\nimport { sanitizeErrorText } from \"./utils.js\";\n\nconst DEFAULT_MESSAGE = \"Rate limit exceeded\";\n\nfunction parseNumber(value: unknown): number | undefined {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed) {\n return undefined;\n }\n const parsed = Number(trimmed);\n if (Number.isFinite(parsed)) {\n return parsed;\n }\n }\n return undefined;\n}\n\nfunction parseString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction parseJsonObject(rawText: string): Record<string, unknown> | undefined {\n if (!rawText) {\n return undefined;\n }\n try {\n const parsed: unknown = JSON.parse(rawText);\n if (parsed && typeof parsed === \"object\") {\n return parsed as Record<string, unknown>;\n }\n } catch {\n // Ignore parse errors and fall back to text-only handling.\n }\n return undefined;\n}\n\nfunction parseRetryAfterHeader(value: string | null): number | undefined {\n if (!value) {\n return undefined;\n }\n\n const numeric = parseNumber(value);\n if (numeric !== undefined) {\n return Math.max(0, Math.ceil(numeric));\n }\n\n const asDate = Date.parse(value);\n if (Number.isFinite(asDate)) {\n const diffMs = asDate - Date.now();\n return Math.max(0, Math.ceil(diffMs / 1000));\n }\n\n return undefined;\n}\n\nfunction parseResetTimestamp(value: unknown): number | undefined {\n const numeric = parseNumber(value);\n if (numeric !== undefined) {\n if (numeric > 1_000_000_000_000) {\n return Math.floor(numeric);\n }\n return Math.floor(numeric * 1000);\n }\n\n const asString = parseString(value);\n if (!asString) {\n return undefined;\n }\n\n const asDate = Date.parse(asString);\n if (Number.isFinite(asDate)) {\n return asDate;\n }\n return undefined;\n}\n\nfunction pickFirst<T>(\n payload: Record<string, unknown>,\n keys: string[],\n parser: (value: unknown) => T | undefined,\n): T | undefined {\n for (const key of keys) {\n const parsed = parser(payload[key]);\n if (parsed !== undefined) {\n return parsed;\n }\n }\n return undefined;\n}\n\nfunction buildRateLimitDetails(\n payload: Record<string, unknown>,\n headers?: Headers,\n): RateLimitErrorDetails {\n const headerRetryAfter = headers\n ? parseRetryAfterHeader(headers.get(\"retry-after\"))\n : undefined;\n const bodyRetryAfter = pickFirst(\n payload,\n [\"retry_after\", \"retryAfter\", \"retry_after_sec\", \"retryAfterSec\"],\n parseNumber,\n );\n\n const retryAfterSec = bodyRetryAfter ?? headerRetryAfter;\n\n const headerReset = headers\n ? parseResetTimestamp(\n headers.get(\"x-ratelimit-reset\") ?? headers.get(\"x-ratelimit-reset-at\"),\n )\n : undefined;\n const bodyReset = parseResetTimestamp(\n payload.reset_at ?? payload.resetAt ?? payload.reset,\n );\n\n const resetAt =\n bodyReset ??\n headerReset ??\n (retryAfterSec !== undefined\n ? Date.now() + retryAfterSec * 1000\n : undefined);\n\n const limit =\n pickFirst(payload, [\"limit\", \"rate_limit\", \"max\"], parseNumber) ??\n (headers ? parseNumber(headers.get(\"x-ratelimit-limit\")) : undefined);\n\n const remaining =\n pickFirst(payload, [\"remaining\", \"rate_limit_remaining\"], parseNumber) ??\n (headers ? parseNumber(headers.get(\"x-ratelimit-remaining\")) : undefined);\n\n const scope =\n pickFirst(payload, [\"scope\", \"limit_scope\"], parseString) ??\n (headers ? parseString(headers.get(\"x-ratelimit-scope\")) : undefined);\n\n const policyId =\n pickFirst(payload, [\"policy_id\", \"policyId\", \"policy\"], parseString) ??\n (headers\n ? parseString(\n headers.get(\"x-ratelimit-policy\") ??\n headers.get(\"x-ratelimit-policy-id\"),\n )\n : undefined);\n\n const requestId =\n pickFirst(payload, [\"request_id\", \"requestId\"], parseString) ??\n (headers\n ? parseString(\n headers.get(\"x-request-id\") ?? headers.get(\"x-correlation-id\"),\n )\n : undefined);\n\n return {\n retryAfterSec,\n resetAt,\n scope,\n policyId,\n limit,\n remaining,\n requestId,\n };\n}\n\nexport function createRateLimitErrorFromPayload(\n payload: Record<string, unknown>,\n fallbackMessage = DEFAULT_MESSAGE,\n): RateLimitError {\n const message = pickFirst(\n payload,\n [\"message\", \"error_description\"],\n parseString,\n );\n return new RateLimitError(\n message ?? fallbackMessage,\n buildRateLimitDetails(payload),\n );\n}\n\nexport function createRateLimitErrorFromHttp(\n response: Response,\n rawText: string,\n): RateLimitError {\n const payload = parseJsonObject(rawText) ?? {};\n const details = buildRateLimitDetails(payload, response.headers);\n\n const sanitizedText = sanitizeErrorText(rawText);\n const message =\n pickFirst(payload, [\"message\", \"error_description\"], parseString) ??\n (sanitizedText || DEFAULT_MESSAGE);\n\n return new RateLimitError(message, details);\n}\n","import {\n AuthenticationError,\n ConnectionError,\n ServerError,\n StreamAbortedError,\n} from \"./errors.js\";\nimport { createRateLimitErrorFromHttp } from \"./rate-limit.js\";\nimport type { ChatStreamEvent, StreamJobSSEOptions } from \"./types.js\";\nimport { sanitizeErrorText } from \"./utils.js\";\n\n/**\n * SSE stream reader. GET for the job event stream (the default); POST with a\n * JSON body for the voice polish stream. The frame parser is the same.\n */\nexport async function* streamJobSSE(\n options: StreamJobSSEOptions,\n): AsyncGenerator<ChatStreamEvent> {\n const { url, headers, signal, fetchFn, method = \"GET\", body } = options;\n\n let response: Response;\n try {\n response = await fetchFn(url, {\n method,\n headers,\n body,\n signal,\n });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n throw new StreamAbortedError();\n }\n throw new ConnectionError(\n err instanceof Error ? err.message : \"Failed to connect\",\n );\n }\n\n if (!response.ok) {\n const rawText = await response.text().catch(() => \"\");\n const text = rawText ? sanitizeErrorText(rawText) : \"\";\n switch (response.status) {\n case 401:\n throw new AuthenticationError();\n case 429:\n throw createRateLimitErrorFromHttp(response, rawText);\n default:\n throw new ServerError(text || `HTTP ${response.status}`, response.status);\n }\n }\n\n if (!response.body) {\n throw new ConnectionError(\"Response body is null\");\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let currentEvent = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"event: \")) {\n currentEvent = line.slice(7).trim();\n } else if (line.startsWith(\"data: \")) {\n const data = line.slice(6);\n if (data === \"[DONE]\") {\n return;\n }\n yield { event: currentEvent || \"message\", data };\n }\n if (line === \"\") {\n currentEvent = \"\";\n }\n }\n }\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n throw new StreamAbortedError();\n }\n throw err;\n } finally {\n reader.releaseLock();\n }\n}\n","// =============================================================================\n// Astralform SDK v2 type definitions\n// =============================================================================\n//\n// Wire protocol types mirror the Pydantic models in\n// `backend/src/stream/protocol.py` 1:1. The SDK forwards typed events to\n// the consumer; block construction happens on the consumer side.\n//\n// Custom event payloads live in `custom-events.ts`.\n\nimport type { AgentIdentity, MemoryRecord, TodoItem } from \"./custom-events.js\";\n\n// Re-export so consumers can import from types.ts or custom-events.ts\nexport type { AgentIdentity, MemoryRecord, TodoItem };\n\n// --- Configuration ---\n//\n// The SDK supports two authentication modes. TypeScript narrows the union by\n// property presence, so consumers can write:\n//\n// new AstralformClient({ apiKey, userId }) // API-key mode\n// new AstralformClient({ accessToken, agentId }) // user-token mode\n//\n// Pick based on who the caller represents:\n//\n// * API-key mode — A customer's backend (B2B2C). Agent scoping is baked\n// into the key; the end user is named per request via `X-End-User-ID`.\n//\n// * User-token mode — An app acting on behalf of an Astralform account\n// holder (AstralChat, future 3rd-party integrations). The OIDC access\n// token is issued by the Astralform Identity Provider (our OAuth 2.1\n// Server at auth.astralform.ai); agent scoping comes from the\n// `X-Agent-ID` header.\n\ninterface AstralformBaseConfig {\n /** Override the default API origin. Defaults to https://api.astralform.ai. */\n baseURL?: string;\n /** Supply a custom fetch (SSR, testing, custom interceptors). */\n fetch?: typeof globalThis.fetch;\n /**\n * Abort a REST request — connect, headers, AND body read — after this many\n * milliseconds. Defaults to 30_000. Does not apply to `uploadFile` (large\n * files on slow uplinks) or to SSE streaming, which is long-lived by design\n * and carries its own `AbortSignal`.\n *\n * Note that unlike axios and friends, `0` does NOT mean \"no timeout\": any\n * non-positive or non-finite value falls back to the default. REST calls\n * cannot opt out of the deadline — an unbounded REST request is the bug\n * this exists to prevent, and it fails silently (a stalled body strands the\n * caller forever with nothing to react to).\n */\n timeoutMs?: number;\n}\n\nexport interface AstralformApiKeyConfig extends AstralformBaseConfig {\n /** Agent API key (`sk_live_...` or `sk_test_...`). */\n apiKey: string;\n /**\n * The customer's own identifier for the end user making this call.\n * Sent as the `X-End-User-ID` header. Required in API-key mode.\n */\n userId: string;\n}\n\nexport interface AstralformUserTokenConfig extends AstralformBaseConfig {\n /**\n * OIDC access token issued by the Astralform Identity Provider.\n * Expect this to be short-lived; use `client.updateAccessToken()` after\n * refreshing to hot-swap without re-instantiating the client.\n */\n accessToken: string;\n /**\n * Active agent context. Sent as the `X-Agent-ID` header. The backend\n * verifies the token's developer has access to this agent on every\n * request; switching agents is a local `updateAgentId()` call.\n *\n * Optional so a pre-pick client (right after login, before the user has\n * chosen a team/agent) can still call account-scoped discovery routes\n * like `listTeams()` / `listAgents(teamId)`. Agent-scoped calls\n * (conversations, messages, chat) will error out until one is set.\n */\n agentId?: string;\n /**\n * Optional end-user override — lets a developer acting under a user\n * token impersonate a downstream end-user identity for testing\n * purposes. When set, sent alongside `X-Agent-ID` as `X-End-User-ID`\n * so memory, rate limits, and conversations scope to the specified\n * end-user rather than the developer themselves.\n *\n * Use `client.updateEndUserId()` to rotate at runtime.\n */\n endUserId?: string;\n}\n\nexport type AstralformConfig =\n AstralformApiKeyConfig | AstralformUserTokenConfig;\n\n// --- Event type constants (SDK public) ---\n//\n// These are the high-level ChatEvent kinds the SDK emits to its\n// consumer. The raw wire events below are translated into these\n// kinds at the session boundary.\n\nexport const ChatEventType = {\n // Connection lifecycle (SDK-local, not wire)\n Connected: \"connected\",\n Disconnected: \"disconnected\",\n\n // Turn lifecycle\n MessageStart: \"message_start\",\n MessageStop: \"message_stop\",\n\n // Block lifecycle\n BlockStart: \"block_start\",\n BlockDelta: \"block_delta\",\n BlockStop: \"block_stop\",\n\n // Reliability\n Stall: \"stall\",\n Retry: \"retry\",\n Error: \"error\",\n Keepalive: \"keepalive\",\n\n // Conversation-level (typed custom events)\n UserMessage: \"user_message\",\n TitleGenerated: \"title_generated\",\n TodoUpdate: \"todo_update\",\n PlanUpdate: \"plan_update\",\n NoteUpdate: \"note_update\",\n ContextUpdate: \"context_update\",\n SubagentStart: \"subagent_start\",\n SubagentStop: \"subagent_stop\",\n ContextWarning: \"context_warning\",\n MemoryRecall: \"memory_recall\",\n MemoryUpdate: \"memory_update\",\n DesktopStream: \"desktop_stream\",\n AttachmentStaged: \"attachment_staged\",\n WorkspaceReady: \"workspace_ready\",\n AssetCreated: \"asset_created\",\n ToolApprovalRequested: \"tool_approval_requested\",\n ToolApprovalGranted: \"tool_approval_granted\",\n ToolPermissionDenied: \"tool_permission_denied\",\n ToolHarnessWarning: \"tool_harness_warning\",\n UserUnavailable: \"user_unavailable\",\n PromptSuggestion: \"prompt_suggestion\",\n StateChanged: \"state_changed\",\n\n // Generic fallthrough for unknown custom events\n Custom: \"custom\",\n} as const;\n\nexport type ChatEventTypeValue =\n (typeof ChatEventType)[keyof typeof ChatEventType];\n\n// =============================================================================\n// Wire protocol (matches backend Pydantic models 1:1, snake_case)\n// =============================================================================\n\nexport type WireBlockKind = \"text\" | \"thinking\" | \"tool_use\";\n\nexport type WireBlockStatus =\n | \"streaming\"\n | \"awaiting_client_result\"\n | \"ok\"\n | \"error\"\n | \"denied\"\n | \"cancelled\";\n\nexport type WireStopReason =\n \"end_turn\" | \"tool_use\" | \"max_tokens\" | \"context_overflow\" | \"error\";\n\n// --- BlockDelta payloads (discriminated on `channel`) ---\n\nexport interface WireTextDelta {\n channel: \"text\";\n text: string;\n}\n\nexport interface WireThinkingDelta {\n channel: \"thinking\";\n text: string;\n}\n\nexport interface WireSignatureDelta {\n channel: \"signature\";\n signature: string;\n}\n\nexport interface WireInputDelta {\n channel: \"input\";\n partial_json: string;\n}\n\nexport interface WireInputArgDelta {\n channel: \"input_arg\";\n arg_name: string;\n text: string;\n}\n\nexport interface WireOutputDelta {\n channel: \"output\";\n stream: \"stdout\" | \"stderr\" | \"progress\";\n chunk: string;\n}\n\nexport interface WireStatusDelta {\n channel: \"status\";\n status:\n \"executing\" | \"awaiting_client_result\" | \"awaiting_approval\" | \"denied\";\n note?: string;\n}\n\nexport type WireBlockDeltaPayload =\n | WireTextDelta\n | WireThinkingDelta\n | WireSignatureDelta\n | WireInputDelta\n | WireInputArgDelta\n | WireOutputDelta\n | WireStatusDelta;\n\n// --- Top-level wire events ---\n\ninterface WireEnvelope {\n seq: number;\n ts: number;\n job_id: string;\n}\n\nexport interface WireMessageStart extends WireEnvelope {\n type: \"message_start\";\n turn_id: string;\n model: string;\n agent_name?: string | null;\n agent_display_name?: string | null;\n agent_avatar_url?: string | null;\n}\n\nexport interface WireBlockStart extends WireEnvelope {\n type: \"block_start\";\n turn_id: string;\n path: number[];\n parent_path?: number[] | null;\n kind: WireBlockKind;\n metadata: Record<string, unknown>;\n}\n\nexport interface WireBlockDelta extends WireEnvelope {\n type: \"block_delta\";\n turn_id: string;\n path: number[];\n delta: WireBlockDeltaPayload;\n}\n\nexport interface WireBlockStop extends WireEnvelope {\n type: \"block_stop\";\n turn_id: string;\n path: number[];\n status: WireBlockStatus;\n final: Record<string, unknown>;\n}\n\nexport interface WireMessageStop extends WireEnvelope {\n type: \"message_stop\";\n turn_id: string;\n stop_reason: WireStopReason;\n usage: {\n input_tokens?: number;\n output_tokens?: number;\n cached_tokens?: number;\n cache_creation_tokens?: number;\n };\n ttfb_ms?: number | null;\n total_ms: number;\n stall_count: number;\n}\n\nexport interface WireStallWarning extends WireEnvelope {\n type: \"stall\";\n since_last_event_ms: number;\n stall_count: number;\n}\n\nexport interface WireRetryEvent extends WireEnvelope {\n type: \"retry\";\n attempt: number;\n reason: string;\n backoff_ms: number;\n strategy?: string | null;\n max_attempts?: number | null;\n context_recovery?: Record<string, unknown> | null;\n}\n\nexport interface WireErrorEvent extends WireEnvelope {\n type: \"error\";\n code: string;\n message: string;\n block_path?: number[] | null;\n // Rate limit fields (carried when code == \"rate_limit_exceeded\")\n retry_after?: number;\n retry_after_sec?: number;\n reset_at?: number | string;\n scope?: string;\n policy_id?: string;\n limit?: number;\n remaining?: number;\n request_id?: string;\n}\n\nexport interface WireKeepalive extends WireEnvelope {\n type: \"keepalive\";\n since_last_event_ms: number;\n}\n\nexport interface WireCustomEvent extends WireEnvelope {\n type: \"custom\";\n name: string;\n data: Record<string, unknown>;\n}\n\nexport type WireEvent =\n | WireMessageStart\n | WireBlockStart\n | WireBlockDelta\n | WireBlockStop\n | WireMessageStop\n | WireStallWarning\n | WireRetryEvent\n | WireErrorEvent\n | WireKeepalive\n | WireCustomEvent;\n\n// =============================================================================\n// ChatEvent — high-level SDK events (camelCase, emitted to consumers)\n// =============================================================================\n\nexport interface TurnUsage {\n inputTokens: number;\n outputTokens: number;\n cachedTokens: number;\n /** Tokens written to the model's prompt cache (wire: `cache_creation_tokens`). */\n cacheCreationTokens: number;\n}\n\nexport type BlockDeltaPayload =\n | { channel: \"text\"; text: string }\n | { channel: \"thinking\"; text: string }\n | { channel: \"signature\"; signature: string }\n | { channel: \"input\"; partialJson: string }\n | { channel: \"inputArg\"; argName: string; text: string }\n | {\n channel: \"output\";\n stream: \"stdout\" | \"stderr\" | \"progress\";\n chunk: string;\n }\n | {\n channel: \"status\";\n status:\n \"executing\" | \"awaiting_client_result\" | \"awaiting_approval\" | \"denied\";\n note?: string;\n };\n\nexport type ChatEvent =\n // Connection lifecycle\n | { type: \"connected\" }\n | { type: \"disconnected\" }\n\n // Turn lifecycle\n | {\n type: \"message_start\";\n turnId: string;\n model: string;\n agentName?: string | null;\n agentDisplayName?: string | null;\n agentAvatarUrl?: string | null;\n }\n | {\n type: \"message_stop\";\n turnId: string;\n jobId: string;\n stopReason: WireStopReason;\n usage: TurnUsage;\n ttfbMs?: number | null;\n totalMs: number;\n stallCount: number;\n }\n\n // Block lifecycle\n | {\n type: \"block_start\";\n turnId: string;\n path: number[];\n parentPath?: number[] | null;\n kind: WireBlockKind;\n metadata: Record<string, unknown>;\n }\n | {\n type: \"block_delta\";\n turnId: string;\n path: number[];\n delta: BlockDeltaPayload;\n }\n | {\n type: \"block_stop\";\n turnId: string;\n path: number[];\n status: WireBlockStatus;\n final: Record<string, unknown>;\n }\n\n // Reliability\n | {\n type: \"stall\";\n sinceLastEventMs: number;\n stallCount: number;\n }\n | {\n type: \"retry\";\n attempt: number;\n reason: string;\n backoffMs: number;\n strategy?: string | null;\n maxAttempts?: number | null;\n contextRecovery?: Record<string, unknown> | null;\n }\n | {\n type: \"error\";\n code: string;\n message: string;\n blockPath: number[] | null;\n }\n | {\n type: \"keepalive\";\n sinceLastEventMs: number;\n }\n\n // Conversation-level — typed custom events\n | {\n type: \"user_message\";\n content: string;\n createdAt?: number;\n id?: string;\n /** This message was steered into a run already in flight — it started\n * no turn of its own. Consumers that badge a steer (a \"waiting to be\n * read\" notice) key off this; without it a replayed steer is\n * indistinguishable from an ordinary prompt. */\n steer?: boolean;\n }\n | { type: \"title_generated\"; title: string }\n | { type: \"todo_update\"; todos: TodoItem[] }\n /** The conversation's plan, as markdown, after the agent wrote or revised it.\n * Carries the FULL body rather than a diff: the backend replaces the plan\n * wholesale (`write_plan` — \"Replaces any existing plan\"), so a consumer that\n * merged deltas would drift from the stored document. */\n | { type: \"plan_update\"; plan: string }\n /** Names of the conversation's notes, after one was written or deleted. Names\n * only — bodies are unbounded and read on demand. */\n | { type: \"note_update\"; notes: string[] }\n | {\n type: \"context_update\";\n context: Record<string, unknown>;\n phase?: string | null;\n updatedAt?: number | null;\n }\n | {\n type: \"subagent_start\";\n agent: AgentIdentity;\n taskCallId?: string | null;\n }\n | {\n type: \"subagent_stop\";\n agent: AgentIdentity;\n taskCallId?: string | null;\n }\n | {\n type: \"context_warning\";\n /** Known values: \"info\" | \"warning\" | \"critical\". Typed as string for forward compat. */\n severity: string;\n utilizationPct: number;\n remainingTokens: number;\n windowTokens: number;\n inputTokens: number;\n message: string;\n }\n | { type: \"memory_recall\"; memories: MemoryRecord[] }\n | {\n type: \"memory_update\";\n /** Known values: \"created\" | \"updated\" | \"deleted\". */\n action: string;\n memoryId?: string | null;\n key?: string | null;\n namespace?: string | null;\n }\n | { type: \"desktop_stream\"; url: string; sandboxId?: string | null }\n | {\n type: \"attachment_staged\";\n attachmentId: string;\n filename: string;\n contentType?: string | null;\n sizeBytes?: number | null;\n }\n | {\n type: \"workspace_ready\";\n sandboxId: string;\n workspacePath?: string | null;\n }\n | {\n type: \"asset_created\";\n assetId: string;\n filename: string;\n url?: string | null;\n contentType?: string | null;\n }\n | {\n type: \"tool_approval_requested\";\n toolName: string;\n callId: string;\n arguments: Record<string, unknown>;\n riskLevel?: string | null;\n reason?: string | null;\n }\n | {\n type: \"tool_approval_granted\";\n toolName: string;\n callId: string;\n }\n | {\n type: \"tool_permission_denied\";\n toolName: string;\n callId: string;\n reason?: string | null;\n /** Known values: \"hook\" | \"rule\" | \"user\" | \"timeout\" | \"circuit_breaker\". */\n deniedBy?: string | null;\n }\n | {\n type: \"tool_harness_warning\";\n toolName: string;\n callId: string;\n message?: string | null;\n details?: Record<string, unknown> | null;\n }\n | {\n type: \"user_unavailable\";\n consecutiveTimeouts: number;\n toolName?: string | null;\n }\n | {\n type: \"prompt_suggestion\";\n suggestions: string[];\n }\n | {\n type: \"state_changed\";\n /**\n * Job lifecycle state. Known values from the backend today include\n * \"queued\" | \"running\" | \"waiting_for_tool\" | \"completed\" | \"failed\".\n * Typed as string for forward compat.\n */\n state: string;\n }\n\n // Generic fallthrough for unknown custom events\n | {\n type: \"custom\";\n name: string;\n data: Record<string, unknown>;\n };\n\n// =============================================================================\n// Domain models (unchanged from previous SDK version)\n// =============================================================================\n\nexport interface Conversation {\n id: string;\n title: string;\n messageCount: number;\n createdAt: string;\n updatedAt: string;\n /**\n * The project this task belongs to (`owner/repo`), or `null` for an ordinary\n * conversation. Set on the first turn and immutable after. Absent (rather than\n * null) from an Astralform older than 0.69.46.\n */\n repository?: string | null;\n}\n\nexport interface Message {\n id: string;\n conversationId: string;\n role: \"user\" | \"assistant\" | \"system\";\n content: string;\n parentId?: string;\n status: \"sending\" | \"streaming\" | \"complete\" | \"error\";\n createdAt: string;\n toolCalls?: ToolCallRequest[];\n}\n\nexport interface UIComponentsConfig {\n enabled: boolean;\n /** Protocol slug (e.g. \"a2ui\"). Null when disabled. */\n protocol: string | null;\n /** MIME type to match against embedded resources (e.g. \"application/json+a2ui\"). */\n mimeType: string | null;\n}\n\n/** One capability the agent either has or does not, right now. */\nexport interface AgentCapability {\n /** Stable key, e.g. `\"image\"`, `\"web\"`, `\"skills\"`. */\n key: string;\n enabled: boolean;\n}\n\nexport interface AgentStatus {\n isReady: boolean;\n llmConfigured: boolean;\n llmProvider?: string;\n llmModel?: string;\n message: string;\n uiComponents: UIComponentsConfig;\n /**\n * What the agent can do right now, so a client can offer a capability only\n * where it will actually work rather than failing on send.\n *\n * Empty when the backend does not report it (older servers) or when the agent\n * is not ready — treat an ABSENT key as \"not reported\", never as disabled.\n * Only capabilities with a real per-agent gate appear; always-on ones do not,\n * because a constant tells a client nothing.\n */\n capabilities: AgentCapability[];\n}\n\nexport interface AgentInfo {\n name: string;\n displayName: string;\n description: string;\n isOrchestrator: boolean;\n isEnabled: boolean;\n avatarUrl?: string;\n /**\n * Whether this agent's tasks can name a repository — the workspace has GitHub\n * connected and enabled here. A client shows Projects and Tasks on it.\n *\n * Derived by the server from the connector, so it cannot go stale the way the\n * retired `mode` toggle could. It gates a SURFACE, not an ability: naming a\n * repository is optional on every task, and a task that names none is an\n * ordinary chat. Absent on Astralform older than 0.69.50, where it should be\n * read as false: those backends had an agent-level mode instead, and this\n * SDK no longer carries the field that expressed it.\n *\n * It is a property of the WORKSPACE, not of a persona: `GET /v1/agents` selects\n * the workspace row itself and returns exactly one entry, so read\n * `agents[0].codeProjectsEnabled`. The workspace picker (`listAgents`) does not\n * carry it, so a client learns this after opening an agent.\n */\n codeProjectsEnabled?: boolean;\n}\n\n// --- Team / Agent discovery (OIDC user-token surface) ---\n\nexport interface TeamSummary {\n id: string;\n name: string;\n slug: string;\n isDefault: boolean;\n /** Caller's role in this team (e.g. \"owner\", \"admin\", \"member\"). */\n role: string;\n}\n\n/**\n * A team-level agent (formerly \"project\") — the workspace a user opens to\n * chat. Distinct from `AgentInfo`, which describes the AI personas available\n * INSIDE an agent workspace (orchestrator + specialists).\n */\nexport interface TeamAgentSummary {\n id: string;\n name: string;\n /** Human-readable label for pickers, when set (wire: `display_name`). Falls back to `name`. */\n displayName?: string | null;\n teamId: string;\n createdAt: string;\n updatedAt: string;\n /** Agent avatar URL, when the team has set one (wire: `avatar_url`). */\n avatarUrl?: string | null;\n}\n\nexport interface SkillInfo {\n name: string;\n displayName: string;\n description: string;\n isEnabled: boolean;\n}\n\n// TodoItem is imported from custom-events.ts and re-exported above.\n\n// =============================================================================\n// Job and request types\n// =============================================================================\n\nexport interface JobCreateResponse {\n job_id: string;\n conversation_id: string;\n message_id: string;\n status: string;\n}\n\nexport interface JobStatus {\n jobId: string;\n status: string;\n createdAt: string | null;\n startedAt: string | null;\n completedAt: string | null;\n errorMessage: string | null;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface ActiveJob {\n jobId: string | null;\n status: string;\n}\n\nexport interface JobSummary {\n jobId: string;\n status: string;\n replacesJobId: string | null;\n responseContent: Record<string, unknown> | null;\n metrics: Record<string, unknown> | null;\n createdAt: string | null;\n}\n\nexport interface FeedbackRequest {\n /** 1 for thumbs up, -1 for thumbs down. */\n rating: 1 | -1;\n comment?: string | null;\n}\n\nexport interface FeedbackResponse {\n id: string;\n jobId: string;\n rating: number;\n comment: string | null;\n createdAt: string;\n}\n\n/**\n * One rung on a model's thinking ladder.\n *\n * These strings are not ours to choose: they are verbatim what OpenAI,\n * DeepSeek and Z.AI each enumerate when rejecting an invalid\n * `reasoning_effort`, so renaming one means sending a value the provider 400s\n * on. A given model accepts a SUBSET — read it from\n * {@link ModelOption.thinkingControl}, never assume all seven.\n *\n * `\"none\"` is a real off on the providers that list it, and is distinct from\n * omitting the effort entirely: omitting means \"use the model's own default\",\n * which on some models still reasons.\n */\nexport type EffortRung =\n | \"none\"\n | \"minimal\"\n | \"low\"\n | \"medium\"\n | \"high\"\n | \"xhigh\"\n | \"max\";\n\n/**\n * A reasoning effort a caller may request. Widened from `low | medium | high`\n * once the providers were probed and shown to expose seven values.\n */\nexport type ReasoningEffort = EffortRung;\n\n/** One selectable rung, with the label every client should display for it. */\nexport interface ThinkingRungOption {\n id: EffortRung;\n /** Server-owned, so two clients cannot word the same rung differently. */\n label: string;\n}\n\n/**\n * A model's thinking control, from `GET /v1/models`.\n *\n * `ladder` runs least-effort-first. That order is the menu order and the basis\n * of any proportional indicator — a rung's INDEX is its strength. It contains a\n * `none` rung if and only if the model can genuinely stop reasoning, so an Off\n * is an ordinary rung rather than something a client synthesizes.\n *\n * `default` is the rung a run uses when the caller picks nothing, and is\n * nullable: for the OpenAI-style family the server sends no effort at all, so\n * the choice belongs to the provider and naming a rung would be a guess.\n */\nexport interface ThinkingDescriptor {\n ladder: ThinkingRungOption[];\n default: EffortRung | null;\n}\n\n/**\n * The per-request model choice (client-side model selection). `provider` and\n * `model` are paired — send both or neither; when omitted, the server reuses the\n * conversation's last model or a connected-provider default.\n */\nexport interface ModelChoiceOptions {\n provider?: string;\n model?: string;\n reasoningEffort?: ReasoningEffort;\n temperature?: number;\n}\n\nexport interface ChatStreamRequest {\n message?: string;\n conversation_id?: string;\n /**\n * Which project (GitHub repository, `owner/repo`) this task belongs to.\n *\n * Optional on every agent. The first turn that names one binds the task, and a\n * later turn may omit it or repeat the same value; a DIFFERENT value is refused\n * (409), not ignored — a task is bound to one repository for life, so a\n * different repository means a new task. A task that never names one is an\n * ordinary chat; since Astralform 0.69.50 there is no agent mode that requires\n * one.\n * Astralform >= 0.69.46.\n */\n repository?: string;\n mcp_manifest?: ToolDefinition[];\n enabled_mcp?: string[];\n continue_from_message?: string;\n resend_from?: string;\n upload_ids?: string[];\n agent_name?: string;\n plan_mode?: boolean;\n image_mode?: boolean;\n video_mode?: boolean;\n /**\n * Start a durable long-horizon goal for this run (goal mode). The backend mints\n * an agent_goal from this text and drives the run under a budget until the\n * objective is genuinely complete. A blank/omitted value runs a normal turn.\n */\n goal?: string;\n /**\n * Per-request model choice (client-side model selection), wire shape.\n * `provider` and `model` are paired; omit to reuse the thread's last model.\n */\n provider?: string;\n model?: string;\n reasoning_effort?: ReasoningEffort;\n temperature?: number;\n}\n\n/** One repository an app user works with on an agent. */\nexport interface CodeProject {\n repoFullName: string;\n addedAt: string;\n}\n\n/** A repository the workspace's GitHub installations cover. */\nexport interface AvailableRepository {\n fullName: string;\n private: boolean;\n}\n\n/**\n * What an app user may add, and why the list might be empty.\n *\n * `state` separates the three empty cases a picker must not conflate:\n * `ok` (connected, covers nothing new), `not_installed` (no installation to ask)\n * and `unavailable` (GitHub could not be reached — try again, do not tell the\n * user they have no repositories). `totalCount` is the installations' raw total\n * BEFORE already-added projects are subtracted, so it is not the list's length.\n */\nexport interface AvailableRepositories {\n state: \"ok\" | \"unavailable\" | \"not_installed\";\n repositories: AvailableRepository[];\n totalCount: number;\n /**\n * At least one of the workspace's GitHub installations could not be\n * enumerated, so the list is missing whatever that one covers. It is NOT\n * pagination — there is no next page to ask for. A picker should say some\n * repositories may be missing rather than present the list as complete.\n */\n partial: boolean;\n}\n\nexport interface ToolResultRequest {\n conversation_id: string;\n message_id: string;\n tool_results: ToolResult[];\n}\n\nexport interface ToolResult {\n call_id: string;\n tool_name: string;\n result: string;\n is_error: boolean;\n}\n\nexport type ToolApprovalDecision = \"allow\" | \"deny\";\nexport type ToolApprovalScope = \"once\" | \"conversation\" | \"always\";\n\nexport interface ToolApprovalRequest {\n job_id: string;\n call_id: string;\n decision: ToolApprovalDecision;\n scope: ToolApprovalScope;\n}\n\n/**\n * A remembered tool-permission grant belonging to the current end user.\n * Only `conversation`/`always` grants are ever stored (`once` is consumed at\n * approval time and never persisted).\n */\nexport interface ToolGrant {\n id: string;\n toolName: string;\n decision: ToolApprovalDecision;\n scope: Exclude<ToolApprovalScope, \"once\">;\n /** Set for `conversation`-scoped grants; `null` for `always`. */\n conversationId: string | null;\n createdAt: string;\n}\n\n/** A page of the current end user's own tool grants. */\nexport interface MyToolGrantsPage {\n grants: ToolGrant[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n}\n\nexport interface ToolCallRequest {\n callId: string;\n toolName: string;\n displayName?: string;\n description?: string;\n arguments: Record<string, unknown>;\n isClientTool: boolean;\n toolCategory?: string;\n iconUrl?: string;\n}\n\n// =============================================================================\n// SSE transport / raw parsing\n// =============================================================================\n\nexport interface StreamJobSSEOptions {\n url: string;\n headers: Record<string, string>;\n signal?: AbortSignal;\n fetchFn: typeof globalThis.fetch;\n /** Request method; defaults to GET (the job event stream). */\n method?: \"GET\" | \"POST\";\n /** A JSON-serialised body for POST streams (the voice polish stream). */\n body?: string;\n}\n\n// =============================================================================\n// Voice input\n// =============================================================================\n\n/**\n * The styles a transcript can be shaped into, as the server names them.\n * `raw` never calls the model — the transcript is used as recognized.\n */\nexport const VOICE_POLISH_MODES = [\"raw\", \"light\", \"structured\", \"formal\"] as const;\n\nexport type VoicePolishMode = (typeof VOICE_POLISH_MODES)[number];\n\n/** A mode that calls the polish model — every mode except `raw`. */\nexport type VoiceLLMMode = Exclude<VoicePolishMode, \"raw\">;\n\n/** Whether `value` is one of the four modes this SDK version knows. */\nexport function isVoicePolishMode(value: unknown): value is VoicePolishMode {\n return (\n typeof value === \"string\" && (VOICE_POLISH_MODES as readonly string[]).includes(value)\n );\n}\n\n/**\n * Whether `mode` may be passed to `streamVoicePolish` — the check to make on\n * `VoiceConfig.defaultMode`, which can be `raw`.\n */\nexport function isVoiceLLMMode(mode: VoicePolishMode): mode is VoiceLLMMode {\n return mode !== \"raw\";\n}\n\n/**\n * What a client needs to run the microphone for an agent, from\n * `GET /v1/voice/config`. Deliberately carries no provider or model names.\n */\nexport interface VoiceConfig {\n enabled: boolean;\n /**\n * The styles the client may request, as the server names them. Kept as\n * plain strings so a mode this SDK version does not know still reaches a\n * picker; `isVoicePolishMode` narrows one.\n */\n modes: string[];\n /**\n * The style to use when the user has not picked one. Falls back to\n * `structured` when the server names a mode this SDK does not know. Can be\n * `raw`, which `streamVoicePolish` refuses — check it with `isVoiceLLMMode`\n * (or compare against `\"raw\"`) before polishing.\n */\n defaultMode: VoicePolishMode;\n /** In tap-to-talk mode, the pause that ends a recording. */\n silenceAutoStopSeconds: number;\n /** Send the message as soon as the result is ready. */\n autoSend: boolean;\n maxRecordingSeconds: number;\n /**\n * Whether the configured recognizer emits live partial transcripts. Batch\n * (Whisper-style) providers do not; they transcribe on stop.\n */\n supportsStreaming: boolean;\n /**\n * The project vocabulary, for display. The server applies it to every\n * transcription and polish itself; `transcribeVoice({ hotwords })` and\n * `VoicePolishRequest.hotwords` carry only the user's own words, which the\n * server merges after these.\n */\n hotwords: string[];\n}\n\n/** One recording turned into text, from `POST /v1/voice/transcriptions`. */\nexport interface VoiceTranscript {\n text: string;\n language: string | null;\n /** Length of the submitted audio, when the WAV header said so. */\n durationMs: number | null;\n /** Time the provider took. */\n asrMs: number;\n}\n\nexport interface VoiceTranscribeOptions {\n /** File name sent with the recording; defaults to `recording.wav`. */\n filename?: string;\n /**\n * The user's own vocabulary — not the project's, which the server already\n * holds and merges in first. Sent comma-separated, so a word cannot itself\n * contain a comma.\n */\n hotwords?: string[];\n /** ISO 639-1 hint; omit for auto-detection. */\n language?: string;\n /**\n * Abort the upload. No client-side deadline applies to this call (a\n * recording can run to `maxRecordingSeconds` and the upload with it), so\n * this is the only way to give up on a stalled one; the promise rejects\n * with the abort reason (the runtime's `AbortError`, or what was passed to\n * `abort(reason)`).\n */\n signal?: AbortSignal;\n}\n\nexport interface VoicePolishRequest {\n text: string;\n /** One of the LLM modes; `raw` is refused by the server. */\n mode: VoiceLLMMode;\n /** The user's own vocabulary; the server merges it after the project's. */\n hotwords?: string[];\n}\n\n/**\n * One frame of the polish stream. `delta` text arrives in order; `done.text`\n * is authoritative (the cleaned full output, which can differ from the\n * concatenated deltas). On `error` keep the raw transcript; `partial` is what\n * was streamed before the failure.\n */\nexport type VoicePolishEvent =\n | { type: \"delta\"; text: string }\n | { type: \"done\"; text: string; polishMs: number }\n | { type: \"error\"; reason: string; partial: string; detail?: string };\n\nexport interface ChatStreamEvent {\n event: string;\n data: string;\n}\n\nexport interface ConversationEvent {\n seq: number;\n event: string;\n data: Record<string, unknown>;\n}\n\n// =============================================================================\n// Send / session options\n// =============================================================================\n\nexport interface SendOptions extends ModelChoiceOptions {\n /**\n * Which conversation this turn belongs to. Defaults to the session's current\n * one.\n *\n * ⚠️ BEHAVIOR CHANGE: passing this now RELOCATES the session. A value\n * different from the current one sets `session.conversationId`, DISCARDS\n * `session.messages` (the old conversation's list is not that conversation's\n * history), and invalidates any `loadConversation` still in flight.\n * Previously it addressed a single turn elsewhere and left the session where\n * it was. After a successful send the list holds only that turn, so load the\n * conversation if you need its history.\n *\n * The change is deliberate: `onSessionEvent` tags every emitted event with\n * `session.conversationId`, so a turn sent elsewhere without moving the\n * pointer streamed its events back under the wrong conversation. Addressing\n * one turn away from the session is not something the session can honestly\n * represent while it has a single pointer, so the send now moves it.\n */\n conversationId?: string;\n enabledClientTools?: string[];\n uploadIds?: string[];\n agentName?: string;\n planMode?: boolean;\n /**\n * Attach the image-generation tool to this turn.\n *\n * Off by default and per-message, because generating costs the developer real\n * money at a third-party provider — the agent does not get to decide on its\n * own that a picture would be nice. The tool is not attached at all unless\n * this is set, so an agent with image generation configured still cannot\n * generate one on an ordinary turn.\n *\n * Gate the affordance on `AgentStatus.capabilities` (Astralform ≥ 0.61.0) —\n * an agent with no provider configured will simply have no tool to call.\n * The `image_mode` field itself is accepted from Astralform ≥ 0.59.0.\n */\n imageMode?: boolean;\n /**\n * Put this turn in video mode, attaching the video-generation tool.\n *\n * Off by default and per-message, for a sharper reason than images: a clip\n * occupies one shared GPU for minutes, during which image generation on the\n * same host cannot run at all. The tool is not attached unless this is set.\n *\n * Mutually exclusive with `imageMode` at the composer level — which is why a\n * video turn also gets the image tool server-side: the user cannot select\n * both, so the agent must be able to produce its own first frame.\n *\n * `generate_video` animates an EXISTING image; it cannot start from text, and\n * the clip is silent. Gate the affordance on `AgentStatus.capabilities`\n * (`video`), the same way image mode does.\n */\n videoMode?: boolean;\n /**\n * Start a durable long-horizon goal for this run (goal mode). The text becomes\n * the goal's objective; the backend keeps the agent working under a budget until\n * it's complete. Omit for a normal turn.\n */\n goal?: string;\n /**\n * The project this task belongs to (`owner/repo`), when it has one.\n *\n * Send it on the turn that STARTS a task; the binding is write-once, so a\n * later turn may omit it or repeat the same value, and a DIFFERENT value is\n * refused (409) rather than ignored. Omit it entirely and the task is an\n * ordinary chat — since Astralform 0.69.50 no agent requires one, and a first\n * turn without it is no longer a 400. Astralform >= 0.69.46.\n */\n repository?: string;\n}\n\n/**\n * A selectable model for one of the team's connected providers, from\n * `GET /v1/models`. Backs the client-side model picker.\n */\nexport interface ModelOption {\n provider: string;\n providerDisplay: string;\n model: string;\n thinking: boolean;\n tools: boolean;\n vision: boolean;\n /**\n * The model's thinking control, or absent when it has none.\n *\n * Its ABSENCE is the signal to render no control — that replaces a separate\n * `supportsEffort` flag which had to be kept consistent with the level list\n * beside it. Distinct from `thinking` above, which says only whether the\n * model reasons at all: a model can reason with no control a client can\n * drive.\n */\n thinkingControl?: ThinkingDescriptor;\n /** the provider's brand mark (picker tiles); null until the backend rollout / for icon-less providers */\n iconUrl?: string | null;\n /** when the CALLER last ran this model (picker \"Recent\" ordering); null for a never-used model */\n lastUsedAt?: string | null;\n /** how many times the caller has run this model; null alongside lastUsedAt */\n useCount?: number | null;\n /**\n * The model's context window in tokens, as the serving provider reports it\n * — which is not always the model's headline number (a provider may serve\n * a smaller window than the model supports). Null when the backend does not\n * state one.\n */\n contextWindow?: number | null;\n}\n\n// =============================================================================\n// Conversation assets\n// =============================================================================\n\nexport interface ConversationAsset {\n id: string;\n kind: \"upload\" | \"output\";\n originalName: string;\n mediaType: string;\n sizeBytes: number;\n workspacePath?: string;\n sourceMessageId?: string;\n agentName?: string;\n /**\n * Freshly-signed download/preview URL. Minted per response by the backend\n * (private `workspaces` bucket), so it reflects the current signature rather\n * than a link baked in when the asset was created. May be absent if signing\n * failed or the asset has no stored object.\n */\n url?: string;\n /**\n * The asset's PERMANENT address. Authorization is resolved per request\n * against the caller's live session, so unlike `url` it never expires — but\n * it needs an `Authorization` header, which a browser will not attach to an\n * `<img src>`. Store and link this one; render from `url`.\n */\n contentUrl?: string;\n /**\n * A still to show for an asset the browser cannot draw from `url` alone.\n * Present only for video: an `<img src>` pointed at an mp4 renders nothing,\n * so a video row would otherwise have no thumbnail. Signed and expiring\n * exactly like `url` — display, not identity, so never store it.\n */\n posterUrl?: string;\n createdAt: string;\n}\n","import { AuthenticationError, ConnectionError, ServerError } from \"./errors.js\";\nimport { createRateLimitErrorFromHttp } from \"./rate-limit.js\";\nimport { streamJobSSE } from \"./streaming.js\";\nimport { VOICE_POLISH_MODES, isVoicePolishMode } from \"./types.js\";\nimport { camelizeKeys, sanitizeErrorText } from \"./utils.js\";\nimport type {\n ActiveJob,\n AgentInfo,\n AvailableRepositories,\n CodeProject,\n AstralformApiKeyConfig,\n AstralformConfig,\n ChatStreamEvent,\n ChatStreamRequest,\n ConversationAsset,\n ConversationEvent,\n Conversation,\n FeedbackRequest,\n FeedbackResponse,\n JobCreateResponse,\n JobStatus,\n JobSummary,\n Message,\n ModelOption,\n MyToolGrantsPage,\n AgentStatus,\n TeamAgentSummary,\n SkillInfo,\n TeamSummary,\n ToolApprovalRequest,\n ToolResultRequest,\n VoiceConfig,\n VoicePolishEvent,\n VoicePolishRequest,\n VoiceTranscribeOptions,\n VoiceTranscript,\n} from \"./types.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.astralform.ai\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nfunction validateBaseURL(url: string): string {\n const cleaned = url.replace(/\\/+$/, \"\");\n try {\n const parsed = new URL(cleaned);\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `Invalid baseURL protocol \"${parsed.protocol}\" - only http: and https: are allowed`,\n );\n }\n return parsed.origin + parsed.pathname.replace(/\\/+$/, \"\");\n } catch (err) {\n if (err instanceof Error && err.message.includes(\"Invalid baseURL\")) {\n throw err;\n }\n throw new Error(`Invalid baseURL: \"${cleaned}\" is not a valid URL`);\n }\n}\n\nfunction isApiKeyConfig(\n config: AstralformConfig,\n): config is AstralformApiKeyConfig {\n return \"apiKey\" in config;\n}\n\n/** Discriminates between the two auth modes the client supports. */\ntype AuthMode =\n | { kind: \"api_key\"; apiKey: string; userId: string }\n | {\n kind: \"user_token\";\n accessToken: string;\n /** Null until the user picks an agent; account-scoped calls still work. */\n agentId: string | null;\n /** Optional end-user override. When present, sent as X-End-User-ID. */\n endUserId: string | null;\n };\n\nexport class AstralformClient {\n private readonly baseURL: string;\n private readonly fetchFn: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n /**\n * Auth state is mutable so callers can rotate access tokens or switch\n * agent context without re-instantiating the client. API-key mode is\n * effectively immutable in practice but uses the same shape for uniformity.\n */\n private auth: AuthMode;\n\n constructor(config: AstralformConfig) {\n if (isApiKeyConfig(config)) {\n if (!config.apiKey || typeof config.apiKey !== \"string\") {\n throw new Error(\"apiKey is required and must be a non-empty string\");\n }\n if (!config.userId || typeof config.userId !== \"string\") {\n throw new Error(\"userId is required in API-key mode\");\n }\n this.auth = {\n kind: \"api_key\",\n apiKey: config.apiKey,\n userId: config.userId,\n };\n } else {\n if (!config.accessToken || typeof config.accessToken !== \"string\") {\n throw new Error(\n \"accessToken is required and must be a non-empty string in user-token mode\",\n );\n }\n // agentId is optional — a pre-pick client (right after login) can\n // still hit account-scoped routes like listTeams(). Agent-scoped\n // routes will 4xx until one is set via updateAgentId().\n const agentId =\n typeof config.agentId === \"string\" && config.agentId.length > 0\n ? config.agentId\n : null;\n this.auth = {\n kind: \"user_token\",\n accessToken: config.accessToken,\n agentId,\n endUserId:\n typeof config.endUserId === \"string\" && config.endUserId.length > 0\n ? config.endUserId\n : null,\n };\n }\n\n this.baseURL = validateBaseURL(config.baseURL ?? DEFAULT_BASE_URL);\n this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);\n this.timeoutMs =\n typeof config.timeoutMs === \"number\" &&\n Number.isFinite(config.timeoutMs) &&\n config.timeoutMs > 0\n ? config.timeoutMs\n : DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Replace the current OIDC access token without reconstructing the client.\n * Use after refreshing via the host app's own token manager.\n * Throws if the client was created in API-key mode.\n */\n updateAccessToken(accessToken: string): void {\n if (this.auth.kind !== \"user_token\") {\n throw new Error(\"updateAccessToken is only valid in user-token mode\");\n }\n if (!accessToken || typeof accessToken !== \"string\") {\n throw new Error(\"accessToken must be a non-empty string\");\n }\n this.auth = { ...this.auth, accessToken };\n }\n\n /**\n * Swap the active agent for a user-token client. The backend verifies the\n * current developer has access to the new agent; a 403 comes back if not.\n */\n updateAgentId(agentId: string): void {\n if (this.auth.kind !== \"user_token\") {\n throw new Error(\"updateAgentId is only valid in user-token mode\");\n }\n if (!agentId || typeof agentId !== \"string\") {\n throw new Error(\"agentId must be a non-empty string\");\n }\n this.auth = { ...this.auth, agentId };\n }\n\n /**\n * Set (or clear) the end-user override for user-token mode.\n *\n * Pass `null` or an empty string to clear — subsequent requests go\n * back to scoping against the developer's own identity. Throws if\n * called in API-key mode, where end-user context already travels via\n * the constructor's `userId` field.\n */\n updateEndUserId(endUserId: string | null): void {\n if (this.auth.kind !== \"user_token\") {\n throw new Error(\"updateEndUserId is only valid in user-token mode\");\n }\n const normalized =\n typeof endUserId === \"string\" && endUserId.length > 0 ? endUserId : null;\n this.auth = { ...this.auth, endUserId: normalized };\n }\n\n /** Current end-user override in user-token mode, or `null` if unset. */\n get endUserId(): string | null {\n return this.auth.kind === \"user_token\" ? this.auth.endUserId : null;\n }\n\n /**\n * Active agent for user-token mode, or `null` if pre-pick (client\n * was constructed without one). For API-key mode the agent is baked\n * into the key, so this getter returns `null` there too — use\n * `authMode` to disambiguate.\n */\n get agentId(): string | null {\n return this.auth.kind === \"user_token\" ? this.auth.agentId : null;\n }\n\n /** Which auth mode this client was constructed with. */\n get authMode(): \"api_key\" | \"user_token\" {\n return this.auth.kind;\n }\n\n /**\n * Authorization + identity headers for the current auth mode, without\n * `Content-Type`. Suitable for JSON requests (paired with the JSON header\n * in the `headers` getter) and for multipart uploads where the browser\n * must set its own `Content-Type` boundary.\n */\n private get authHeaders(): Record<string, string> {\n if (this.auth.kind === \"api_key\") {\n return {\n Authorization: `Bearer ${this.auth.apiKey}`,\n \"X-End-User-ID\": this.auth.userId,\n };\n }\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.auth.accessToken}`,\n };\n if (this.auth.agentId) {\n headers[\"X-Agent-ID\"] = this.auth.agentId;\n }\n if (this.auth.endUserId) {\n headers[\"X-End-User-ID\"] = this.auth.endUserId;\n }\n return headers;\n }\n\n private get headers(): Record<string, string> {\n return {\n ...this.authHeaders,\n \"Content-Type\": \"application/json\",\n };\n }\n\n /**\n * Run one REST exchange under a single deadline covering connect, headers,\n * AND the body read. The body read is the part that matters: `json()` used\n * to sit outside every guard, so a response whose headers arrived but whose\n * body stalled hung forever — silently stranding callers that await it\n * (a stalled `getMessages` used to leave `StreamManager.restore()` parked\n * before it ever fetched the events it renders from).\n *\n * The controller is created per request and is deliberately NOT the\n * session's — that one means \"the user cancelled this turn\" and is null\n * outside a live turn. Aborting frees the socket; the race guarantees a\n * rejection even when an injected `fetch` ignores the signal.\n */\n private async withDeadline<T>(\n run: (signal: AbortSignal) => Promise<T>,\n ): Promise<T> {\n const controller = new AbortController();\n const timedOut = () =>\n new ConnectionError(`Request timed out after ${this.timeoutMs}ms`);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n reject(timedOut());\n }, this.timeoutMs);\n });\n try {\n // Promise.race subscribes to both inputs, so the loser's later\n // rejection (the abort landing after the deadline won) counts as\n // handled and can't surface as an unhandled rejection.\n return await Promise.race([run(controller.signal), deadline]);\n } catch (err) {\n // A signal-honouring fetch rejects with AbortError before the race\n // settles — normalize it to the same timeout error either way.\n if (controller.signal.aborted) throw timedOut();\n throw err;\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async request(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<Response> {\n return this.withDeadline((signal) => this.send(method, path, body, signal));\n }\n\n /** Fetch + status handling. Always called inside `withDeadline`. */\n private async send(\n method: string,\n path: string,\n body: unknown,\n signal: AbortSignal,\n ): Promise<Response> {\n const response = await this.fetchFn(`${this.baseURL}${path}`, {\n method,\n headers: this.headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n signal,\n }).catch((err) => {\n throw new ConnectionError(\n err instanceof Error ? err.message : \"Failed to connect\",\n );\n });\n await this.handleError(response);\n return response;\n }\n\n // DO NOT refactor these back into `request()` + `.json()`. Parsing the body\n // INSIDE the raced callback is the entire fix: `json()` outside the deadline\n // is the original bug (headers arrive, body stalls, caller hangs forever).\n // `request()` survives for `del()`, which never reads the body.\n\n async get<T>(path: string): Promise<T> {\n return this.withDeadline(async (signal) => {\n const response = await this.send(\"GET\", path, undefined, signal);\n return (await response.json()) as T;\n });\n }\n\n async post<T>(path: string, body: unknown): Promise<T> {\n return this.withDeadline(async (signal) => {\n const response = await this.send(\"POST\", path, body, signal);\n return (await response.json()) as T;\n });\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n return this.withDeadline(async (signal) => {\n const response = await this.send(\"PATCH\", path, body, signal);\n return (await response.json()) as T;\n });\n }\n\n private async del(path: string): Promise<void> {\n await this.request(\"DELETE\", path);\n }\n\n private async handleError(response: Response): Promise<void> {\n if (response.ok) return;\n const text = await response.text().catch(() => \"\");\n switch (response.status) {\n case 401:\n throw new AuthenticationError();\n case 429:\n throw createRateLimitErrorFromHttp(response, text);\n default: {\n const safeText = text ? sanitizeErrorText(text) : \"\";\n // Status carried alongside the message: callers that need to tell one\n // failure from another (a 404 meaning \"already gone\" from a 500 meaning\n // \"we don't know\") were left parsing prose otherwise.\n throw new ServerError(\n safeText || `HTTP ${response.status}`,\n response.status,\n );\n }\n }\n }\n\n // --- REST Methods ---\n\n async getHealth(): Promise<{\n status: string;\n version: string;\n ollama_connected: boolean;\n }> {\n return this.get(\"/v1/health\");\n }\n\n // Agent readiness check, scoped to the client's active agent via X-Agent-ID.\n async getAgentStatus(): Promise<AgentStatus> {\n const raw = await this.get<{\n is_ready: boolean;\n llm_configured: boolean;\n llm_provider?: string;\n llm_model?: string;\n message: string;\n ui_components?: {\n enabled?: boolean;\n protocol?: string | null;\n mime_type?: string | null;\n };\n capabilities?: Array<{ key?: string; enabled?: boolean }>;\n }>(\"/v1/agent/status\");\n const ui = raw.ui_components ?? {};\n return {\n isReady: raw.is_ready,\n llmConfigured: raw.llm_configured,\n llmProvider: raw.llm_provider,\n llmModel: raw.llm_model,\n message: raw.message,\n // Defaults to [] rather than undefined so a caller can iterate without a\n // guard, and so a server that predates the field behaves as \"reports\n // nothing\" instead of throwing. Entries missing a key are dropped: a\n // capability with no name cannot be matched against and would render as\n // an unlabelled row.\n capabilities: (raw.capabilities ?? []).flatMap((c) =>\n typeof c?.key === \"string\" && c.key.length > 0\n ? [{ key: c.key, enabled: Boolean(c.enabled) }]\n : [],\n ),\n uiComponents: {\n enabled: Boolean(ui.enabled),\n protocol: ui.protocol ?? null,\n mimeType: ui.mime_type ?? null,\n },\n };\n }\n\n /**\n * A page of conversations, newest-updated first.\n *\n * `options.repository` narrows to one project's tasks (`owner/repo`) — the same\n * paging applies within the filter, so a client showing tasks per project pages\n * each project separately. Tasks that named no repository fall outside every\n * such filter; list them with no filter at all.\n */\n async getConversations(\n limit = 50,\n offset = 0,\n options?: { repository?: string },\n ): Promise<Conversation[]> {\n const safeLimit = Math.max(1, Math.min(200, Math.floor(Number(limit))));\n const safeOffset = Math.max(0, Math.floor(Number(offset)));\n const filter = options?.repository\n ? `&repository=${encodeURIComponent(options.repository)}`\n : \"\";\n const raw = await this.get<\n {\n id: string;\n title: string;\n message_count: number;\n created_at: string;\n updated_at: string;\n repository?: string | null;\n }[]\n >(`/v1/conversations?limit=${safeLimit}&offset=${safeOffset}${filter}`);\n return raw.map((c) => camelizeKeys<Conversation>(c as unknown as Record<string, unknown>));\n }\n\n async getMessages(conversationId: string): Promise<Message[]> {\n const raw = await this.get<\n {\n id: string;\n conversation_id: string;\n role: \"user\" | \"assistant\" | \"system\";\n content: string;\n parent_id?: string;\n created_at: string;\n }[]\n >(`/v1/conversations/${encodeURIComponent(conversationId)}/messages`);\n return raw.map((m) => ({\n id: m.id,\n conversationId: m.conversation_id,\n role: m.role,\n content: m.content,\n parentId: m.parent_id,\n status: \"complete\" as const,\n createdAt: m.created_at,\n }));\n }\n\n /**\n * Replace the title the server generated from the conversation's first turn.\n *\n * The server does NOT bump `updated_at` for a rename — conversations list\n * newest-updated first, and relabelling one is not activity — so the\n * timestamp coming back is the original. Callers should merge the response\n * rather than stamp their own, or they reintroduce the reordering the\n * server deliberately avoids.\n */\n async renameConversation(id: string, title: string): Promise<Conversation> {\n const c = await this.patch<{\n id: string;\n title: string;\n message_count: number;\n created_at: string;\n updated_at: string;\n }>(`/v1/conversations/${encodeURIComponent(id)}`, { title });\n return camelizeKeys<Conversation>(c as unknown as Record<string, unknown>);\n }\n\n async deleteConversation(id: string): Promise<void> {\n await this.del(`/v1/conversations/${encodeURIComponent(id)}`);\n }\n\n /**\n * List the AI personas (sub-agents) available INSIDE the client's active\n * agent workspace — orchestrator + specialists, addressed per message via\n * `ChatStreamRequest.agent_name`. Not to be confused with `listAgents()`,\n * which enumerates the team-level agents a signed-in user can open.\n */\n async getAgents(): Promise<AgentInfo[]> {\n const raw = await this.get<\n {\n name: string;\n display_name: string;\n description: string;\n is_orchestrator: boolean;\n is_enabled: boolean;\n avatar_url?: string;\n code_projects_enabled?: boolean;\n }[]\n >(\"/v1/agents\");\n return raw.map((a) => camelizeKeys<AgentInfo>(a as unknown as Record<string, unknown>));\n }\n\n /**\n * List the models the caller may pick this turn — expanded from the curated\n * catalog of the providers the team has connected (client-side model\n * selection). Backs the composer's model picker. Scoped to the active agent\n * via X-Agent-ID, same as {@link getAgentStatus}.\n */\n async getModels(): Promise<ModelOption[]> {\n const raw = await this.get<Record<string, unknown>[]>(\"/v1/models\");\n // camelizeKeys, not a hand-written map: the previous version listed the\n // fields it knew and silently dropped the rest, so `effort_levels` was\n // emitted by the server for months and never reached a caller. Structural\n // mapping means the next field the API adds arrives on its own.\n //\n // Shallow is correct here rather than a limitation. `thinkingControl`'s\n // nested keys (`ladder`, `id`, `label`, `default`) are all single words, so\n // there is nothing to convert inside it — and recursing would rewrite keys\n // inside arbitrary JSON payloads elsewhere in the SDK, which is a worse\n // failure than the one it would prevent. `client.test.ts` pins the key set.\n //\n // Fields whose absence carries no signal — iconUrl/lastUsedAt/useCount and\n // contextWindow — normalize to null, so \"no data\" is one value rather than\n // undefined-vs-null ambiguity for consumers. thinkingControl deliberately\n // does NOT: there, absence IS the signal, meaning \"no control to render\".\n // That split is the rule to apply to the next optional field added here.\n return raw.map((m) => {\n const option = camelizeKeys<ModelOption>(m);\n return {\n ...option,\n iconUrl: option.iconUrl ?? null,\n lastUsedAt: option.lastUsedAt ?? null,\n useCount: option.useCount ?? null,\n contextWindow: option.contextWindow ?? null,\n };\n });\n }\n\n async getSkills(): Promise<SkillInfo[]> {\n const raw = await this.get<\n {\n name: string;\n display_name: string;\n description: string;\n is_enabled: boolean;\n }[]\n >(\"/v1/skills\");\n return raw.map((s) => camelizeKeys<SkillInfo>(s as unknown as Record<string, unknown>));\n }\n\n async getConversationEvents(\n conversationId: string,\n jobId?: string,\n ): Promise<ConversationEvent[]> {\n let url = `/v1/conversations/${encodeURIComponent(conversationId)}/events`;\n if (jobId) url += `?job_id=${encodeURIComponent(jobId)}`;\n return this.get(url);\n }\n\n async submitToolResult(request: ToolResultRequest): Promise<void> {\n await this.post(\"/v1/tool-result\", request);\n }\n\n async submitToolApproval(request: ToolApprovalRequest): Promise<void> {\n await this.post(\"/v1/tool-approval\", request);\n }\n\n // --- End-user tool-permission self-service ---\n\n /**\n * List the current end user's own remembered tool-permission grants.\n * Only `conversation`/`always` grants exist (`once` is never persisted).\n * Paginated via `limit` (default 100, max 200) / `offset`; `total` lets you\n * page through all of them.\n */\n async getMyToolPermissions(options?: {\n limit?: number;\n offset?: number;\n }): Promise<MyToolGrantsPage> {\n const params = new URLSearchParams();\n if (options?.limit != null) {\n const safeLimit = Math.max(\n 1,\n Math.min(200, Math.floor(Number(options.limit))),\n );\n params.set(\"limit\", String(safeLimit));\n }\n if (options?.offset != null) {\n const safeOffset = Math.max(0, Math.floor(Number(options.offset)));\n params.set(\"offset\", String(safeOffset));\n }\n const qs = params.toString();\n const raw = await this.get<{\n grants: {\n id: string;\n tool_name: string;\n decision: \"allow\" | \"deny\";\n scope: \"conversation\" | \"always\";\n conversation_id: string | null;\n created_at: string;\n }[];\n total: number;\n limit: number;\n offset: number;\n }>(`/v1/me/tool-permissions${qs ? `?${qs}` : \"\"}`);\n return {\n grants: raw.grants.map((g) => ({\n id: g.id,\n toolName: g.tool_name,\n decision: g.decision,\n scope: g.scope,\n conversationId: g.conversation_id,\n createdAt: g.created_at,\n })),\n total: raw.total,\n limit: raw.limit,\n offset: raw.offset,\n };\n }\n\n /**\n * Revoke one of the current end user's remembered grants by id. The agent\n * will ask again the next time that tool is used.\n */\n async revokeToolPermission(id: string): Promise<void> {\n await this.del(`/v1/me/tool-permissions/${encodeURIComponent(id)}`);\n }\n\n // --- Conversation Assets ---\n\n private mapAsset(raw: Record<string, unknown>): ConversationAsset {\n return {\n id: raw.id as string,\n kind: raw.kind as \"upload\" | \"output\",\n originalName: raw.original_name as string,\n mediaType: raw.media_type as string,\n sizeBytes: raw.size_bytes as number,\n workspacePath: raw.workspace_path as string | undefined,\n sourceMessageId: raw.source_message_id as string | undefined,\n agentName: raw.agent_name as string | undefined,\n // The API serializes an unsigned asset as `url: null`; normalize to\n // undefined so it matches the `url?: string` type and consumers that\n // check `!== undefined` never receive a null.\n url: (raw.url as string | null) ?? undefined,\n // This mapping is an ALLOWLIST — a field the API returns and this\n // function does not name is dropped silently, and no type error says so.\n // `content_url` shipped that way and was invisible to every consumer.\n contentUrl: (raw.content_url as string | null) ?? undefined,\n posterUrl: (raw.poster_url as string | null) ?? undefined,\n createdAt: raw.created_at as string,\n };\n }\n\n async uploadFile(\n conversationId: string,\n file: Blob,\n filename?: string,\n ): Promise<ConversationAsset> {\n const formData = new FormData();\n formData.append(\"file\", file, filename);\n\n const response = await this.fetchFn(\n `${this.baseURL}/v1/conversations/${encodeURIComponent(conversationId)}/uploads`,\n {\n method: \"POST\",\n headers: this.authHeaders,\n body: formData,\n },\n ).catch((err) => {\n throw new ConnectionError(\n err instanceof Error ? err.message : \"Failed to connect\",\n );\n });\n await this.handleError(response);\n const raw = await response.json();\n return this.mapAsset(raw as Record<string, unknown>);\n }\n\n // --- Voice input ---\n\n /** The agent's voice-input defaults (`GET /v1/voice/config`). */\n async getVoiceConfig(): Promise<VoiceConfig> {\n const raw = await this.get<Record<string, unknown>>(\"/v1/voice/config\");\n return {\n enabled: Boolean(raw.enabled),\n modes: (raw.modes as string[] | undefined) ?? [...VOICE_POLISH_MODES],\n // A mode this SDK does not know must not reach a `switch` typed as\n // `VoicePolishMode`; `structured` is the server's own default.\n defaultMode: isVoicePolishMode(raw.default_mode) ? raw.default_mode : \"structured\",\n silenceAutoStopSeconds:\n (raw.silence_auto_stop_seconds as number | undefined) ?? 2,\n autoSend: (raw.auto_send as boolean | undefined) ?? true,\n maxRecordingSeconds: (raw.max_recording_seconds as number | undefined) ?? 300,\n supportsStreaming: Boolean(raw.supports_streaming),\n hotwords: (raw.hotwords as string[] | undefined) ?? [],\n };\n }\n\n /**\n * Transcribe one recording with the agent's configured speech-to-text\n * provider (`POST /v1/voice/transcriptions`). 16 kHz mono 16-bit WAV is the\n * reference format; anything the provider accepts works.\n *\n * Deliberately outside `withDeadline`: a recording can run to\n * `VoiceConfig.maxRecordingSeconds`, so the 30 s default would cut real\n * uploads off. Pass `options.signal` to give up on a stalled one; the\n * promise then rejects with the abort reason — the runtime's `AbortError`,\n * or whatever was passed to `abort(reason)`.\n */\n async transcribeVoice(\n audio: Blob,\n options: VoiceTranscribeOptions = {},\n ): Promise<VoiceTranscript> {\n const formData = new FormData();\n formData.append(\"file\", audio, options.filename ?? \"recording.wav\");\n if (options.hotwords?.length) {\n // The form field is a comma-separated string — the server splits it on\n // commas (and newlines), so this is the wire format, not a choice made\n // here. A word containing a comma arrives as two.\n formData.append(\"hotwords\", options.hotwords.join(\", \"));\n }\n if (options.language) {\n formData.append(\"language\", options.language);\n }\n const response = await this.fetchFn(`${this.baseURL}/v1/voice/transcriptions`, {\n method: \"POST\",\n headers: this.authHeaders,\n body: formData,\n signal: options.signal,\n }).catch((err) => {\n // Any abort is the caller's, not a failure — including `abort(reason)`,\n // which rejects with the caller's own error rather than an AbortError.\n if (options.signal?.aborted) {\n throw err;\n }\n throw new ConnectionError(\n err instanceof Error ? err.message : \"Failed to connect\",\n );\n });\n await this.handleError(response);\n const raw = (await response.json()) as Record<string, unknown>;\n return {\n text: (raw.text as string | undefined) ?? \"\",\n language: (raw.language as string | null | undefined) ?? null,\n durationMs: (raw.duration_ms as number | null | undefined) ?? null,\n asrMs: (raw.asr_ms as number | undefined) ?? 0,\n };\n }\n\n /**\n * Stream the LLM rewrite of a transcript (`POST /v1/voice/polish`) as typed\n * frames.\n *\n * Failures the server reports mid-stream arrive as an `error` frame, but\n * the iteration itself can reject: aborting `signal` closes the connection\n * (which cancels the model call upstream) and rejects with\n * `StreamAbortedError`; a non-2xx open rejects with `AuthenticationError`,\n * `RateLimitError` or `ServerError`; a network failure with\n * `ConnectionError`. Wrap the `for await` accordingly.\n */\n async *streamVoicePolish(\n request: VoicePolishRequest,\n options: { signal?: AbortSignal } = {},\n ): AsyncGenerator<VoicePolishEvent> {\n const frames = streamJobSSE({\n url: `${this.baseURL}/v1/voice/polish`,\n headers: { ...this.headers, Accept: \"text/event-stream\" },\n method: \"POST\",\n body: JSON.stringify({\n text: request.text,\n mode: request.mode,\n hotwords: request.hotwords ?? [],\n }),\n signal: options.signal,\n fetchFn: this.fetchFn,\n });\n for await (const frame of frames) {\n const event = parseVoicePolishFrame(frame);\n if (event) yield event;\n }\n }\n\n async listUploads(conversationId: string): Promise<ConversationAsset[]> {\n const raw = await this.get<Record<string, unknown>[]>(\n `/v1/conversations/${encodeURIComponent(conversationId)}/uploads`,\n );\n return raw.map((r) => this.mapAsset(r));\n }\n\n async listOutputs(conversationId: string): Promise<ConversationAsset[]> {\n const raw = await this.get<Record<string, unknown>[]>(\n `/v1/conversations/${encodeURIComponent(conversationId)}/outputs`,\n );\n return raw.map((r) => this.mapAsset(r));\n }\n\n // --- Account-scoped discovery (user-token mode) ---\n //\n // Lets a signed-in user pick which team/agent they want to act on.\n // Backend gates these on OIDC user context (no X-Agent-ID required) —\n // sending them in API-key mode yields 401.\n\n async listTeams(): Promise<TeamSummary[]> {\n const raw = await this.get<\n Array<{\n id: string;\n name: string;\n slug: string;\n is_default: boolean;\n role: string;\n }>\n >(\"/v1/teams\");\n return raw.map((t) => camelizeKeys<TeamSummary>(t as unknown as Record<string, unknown>));\n }\n\n /**\n * List the team-level agents (formerly \"projects\") the signed-in user can\n * open — the pickable workspaces under a team. Not to be confused with\n * `getAgents()`, which lists the AI personas inside the active agent.\n */\n async listAgents(teamId: string): Promise<TeamAgentSummary[]> {\n const raw = await this.get<\n Array<{\n id: string;\n name: string;\n display_name?: string | null;\n team_id: string;\n created_at: string;\n updated_at: string;\n avatar_url?: string | null;\n }>\n >(`/v1/teams/${encodeURIComponent(teamId)}/agents`);\n return raw.map((a) => camelizeKeys<TeamAgentSummary>(a as unknown as Record<string, unknown>));\n }\n\n // --- Projects: the repositories this app user works with ---\n\n /**\n * The projects (GitHub repositories) this app user works with, and what they\n * may add.\n *\n * A project list is per app user: the developer connects the workspace's GitHub\n * account, and each user curates their own list from what that connection\n * covers. There is no agent-level gate — an agent with no GitHub lists nothing,\n * reports `not_installed` from `available()`, and refuses `add()`. Read\n * {@link AgentInfo.codeProjectsEnabled} to decide whether to show the surface.\n */\n readonly code = {\n projects: {\n /** This user's projects on the active agent, oldest first. */\n list: async (): Promise<CodeProject[]> => {\n const raw = await this.get<{ repo_full_name: string; added_at: string }[]>(\n \"/v1/code/projects\",\n );\n return raw.map((p) => camelizeKeys<CodeProject>(p as unknown as Record<string, unknown>));\n },\n\n /**\n * What the workspace's GitHub installations cover, minus what this user\n * has already added. Read `state` before the list: an empty `repositories`\n * means something different in each of its three values.\n */\n available: async (): Promise<AvailableRepositories> => {\n const raw = await this.get<{\n state: \"ok\" | \"unavailable\" | \"not_installed\";\n repositories: { full_name: string; private: boolean }[];\n total_count: number;\n partial: boolean;\n }>(\"/v1/code/projects/available\");\n return {\n state: raw.state,\n repositories: (raw.repositories ?? []).map((r) => ({\n fullName: r.full_name,\n private: r.private,\n })),\n // Defaulted like its neighbours: the `unavailable` branch has nothing\n // to count, and an absent field behind a `number` type prints\n // \"undefined\" in a picker rather than a number.\n totalCount: raw.total_count ?? 0,\n partial: raw.partial ?? false,\n };\n },\n\n /**\n * Add a repository. The server checks it against the workspace's own\n * installations and answers a repository it cannot reach the same way it\n * answers one owned by someone else — deliberately, so this call cannot be\n * used to discover which organisations use Astralform.\n */\n add: async (repoFullName: string): Promise<CodeProject> => {\n const raw = await this.post<{ repo_full_name: string; added_at: string }>(\n \"/v1/code/projects\",\n { repo_full_name: repoFullName },\n );\n return camelizeKeys<CodeProject>(raw as unknown as Record<string, unknown>);\n },\n\n /**\n * Remove a project. Tasks already bound to that repository keep their\n * binding — they simply stop grouping under it.\n */\n remove: async (owner: string, repo: string): Promise<void> => {\n await this.del(\n `/v1/code/projects/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,\n );\n },\n },\n };\n\n // --- Jobs API ---\n\n async createJob(request: ChatStreamRequest): Promise<JobCreateResponse> {\n return this.post<JobCreateResponse>(\"/v1/jobs\", request);\n }\n\n async *streamJobEvents(\n jobId: string,\n afterSeq = -1,\n signal?: AbortSignal,\n ): AsyncGenerator<ChatStreamEvent> {\n const url = `${this.baseURL}/v1/jobs/${encodeURIComponent(jobId)}/events?after=${afterSeq}`;\n yield* streamJobSSE({\n url,\n headers: this.headers,\n signal,\n fetchFn: this.fetchFn,\n });\n }\n\n async cancelJob(jobId: string): Promise<void> {\n await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/cancel`, {});\n }\n\n async getJob(jobId: string): Promise<JobStatus> {\n const raw = await this.get<{\n job_id: string;\n status: string;\n created_at?: string | null;\n started_at?: string | null;\n completed_at?: string | null;\n error_message?: string | null;\n input_tokens?: number;\n output_tokens?: number;\n }>(`/v1/jobs/${encodeURIComponent(jobId)}`);\n return {\n jobId: raw.job_id,\n status: raw.status,\n createdAt: raw.created_at ?? null,\n startedAt: raw.started_at ?? null,\n completedAt: raw.completed_at ?? null,\n errorMessage: raw.error_message ?? null,\n inputTokens: raw.input_tokens ?? 0,\n outputTokens: raw.output_tokens ?? 0,\n };\n }\n\n async submitFeedback(\n jobId: string,\n request: FeedbackRequest,\n ): Promise<FeedbackResponse> {\n const body: { rating: 1 | -1; comment?: string } = {\n rating: request.rating,\n };\n if (request.comment != null) body.comment = request.comment;\n const raw = await this.post<{\n id: string;\n job_id: string;\n rating: number;\n comment: string | null;\n created_at: string;\n }>(`/v1/jobs/${encodeURIComponent(jobId)}/feedback`, body);\n return camelizeKeys<FeedbackResponse>(raw as unknown as Record<string, unknown>);\n }\n\n async getActiveJob(conversationId: string): Promise<ActiveJob> {\n const raw = await this.get<{\n job_id: string | null;\n status: string;\n }>(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);\n return {\n jobId: raw.job_id ?? null,\n status: raw.status,\n };\n }\n\n async listJobs(conversationId: string): Promise<JobSummary[]> {\n const raw = await this.get<\n {\n job_id: string;\n status: string;\n replaces_job_id?: string | null;\n response_content?: Record<string, unknown> | null;\n metrics?: Record<string, unknown> | null;\n created_at?: string | null;\n }[]\n >(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);\n return raw.map((j) => ({\n jobId: j.job_id,\n status: j.status,\n replacesJobId: j.replaces_job_id ?? null,\n responseContent: j.response_content ?? null,\n metrics: j.metrics ?? null,\n createdAt: j.created_at ?? null,\n }));\n }\n}\n\n/**\n * Decode one SSE frame of `POST /v1/voice/polish`; null for frames the\n * client does not act on (pings, unknown events).\n */\nexport function parseVoicePolishFrame(frame: {\n event: string;\n data: string;\n}): VoicePolishEvent | null {\n let payload: Record<string, unknown>;\n try {\n const parsed: unknown = JSON.parse(frame.data);\n // `null`, `true`, `42` and `[]` all parse; only a plain object carries\n // the fields read below (an array would synthesize an `error` event), and\n // a throw here would end the whole polish generator.\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return null;\n payload = parsed as Record<string, unknown>;\n } catch {\n return null;\n }\n switch (frame.event) {\n case \"delta\":\n return typeof payload.text === \"string\"\n ? { type: \"delta\", text: payload.text }\n : null;\n case \"done\":\n return typeof payload.text === \"string\"\n ? {\n type: \"done\",\n text: payload.text,\n polishMs: (payload.polish_ms as number | undefined) ?? 0,\n }\n : null;\n case \"error\":\n return {\n type: \"error\",\n reason: (payload.reason as string | undefined) ?? \"unknown\",\n partial: (payload.partial as string | undefined) ?? \"\",\n ...(typeof payload.detail === \"string\" ? { detail: payload.detail } : {}),\n };\n default:\n return null;\n }\n}\n","import type { Conversation, Message } from \"./types.js\";\n\nexport interface ChatStorage {\n fetchConversations(): Promise<Conversation[]>;\n fetchConversation(id: string): Promise<Conversation | null>;\n createConversation(id: string, title: string): Promise<Conversation>;\n updateConversationTitle(id: string, title: string): Promise<void>;\n deleteConversation(id: string): Promise<void>;\n fetchMessages(conversationId: string): Promise<Message[]>;\n addMessage(message: Message, conversationId: string): Promise<void>;\n updateMessageStatus(id: string, status: Message[\"status\"]): Promise<void>;\n deleteMessage(id: string): Promise<void>;\n}\n\nexport class InMemoryStorage implements ChatStorage {\n private conversations = new Map<string, Conversation>();\n private messages = new Map<string, Message[]>();\n\n async fetchConversations(): Promise<Conversation[]> {\n return Array.from(this.conversations.values()).sort(\n (a, b) =>\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),\n );\n }\n\n async fetchConversation(id: string): Promise<Conversation | null> {\n return this.conversations.get(id) ?? null;\n }\n\n async createConversation(id: string, title: string): Promise<Conversation> {\n const now = new Date().toISOString();\n const conversation: Conversation = {\n id,\n title,\n messageCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n this.conversations.set(id, conversation);\n this.messages.set(id, []);\n return conversation;\n }\n\n async updateConversationTitle(id: string, title: string): Promise<void> {\n const conv = this.conversations.get(id);\n if (conv) {\n conv.title = title;\n conv.updatedAt = new Date().toISOString();\n }\n }\n\n async deleteConversation(id: string): Promise<void> {\n this.conversations.delete(id);\n this.messages.delete(id);\n }\n\n async fetchMessages(conversationId: string): Promise<Message[]> {\n return this.messages.get(conversationId) ?? [];\n }\n\n async addMessage(message: Message, conversationId: string): Promise<void> {\n const msgs = this.messages.get(conversationId) ?? [];\n msgs.push(message);\n this.messages.set(conversationId, msgs);\n\n const conv = this.conversations.get(conversationId);\n if (conv) {\n conv.messageCount = msgs.length;\n conv.updatedAt = new Date().toISOString();\n }\n }\n\n async updateMessageStatus(\n id: string,\n status: Message[\"status\"],\n ): Promise<void> {\n for (const msgs of this.messages.values()) {\n const msg = msgs.find((m) => m.id === id);\n if (msg) {\n msg.status = status;\n return;\n }\n }\n }\n\n async deleteMessage(id: string): Promise<void> {\n for (const [convId, msgs] of this.messages.entries()) {\n const idx = msgs.findIndex((m) => m.id === id);\n if (idx !== -1) {\n msgs.splice(idx, 1);\n const conv = this.conversations.get(convId);\n if (conv) {\n conv.messageCount = msgs.length;\n }\n return;\n }\n }\n }\n}\n","import type { ToolCallRequest, ToolDefinition, ToolResult } from \"./types.js\";\n\nexport type ToolHandler = (args: Record<string, unknown>) => Promise<string>;\n\ninterface RegisteredTool {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n handler: ToolHandler;\n}\n\n/** Validates tool name: alphanumeric, hyphens, underscores, dots only */\nconst TOOL_NAME_PATTERN = /^[a-zA-Z0-9_.\\-]+$/;\n\n/** Strips prototype-polluting keys from an object */\nfunction sanitizeArgs(args: Record<string, unknown>): Record<string, unknown> {\n const clean: Record<string, unknown> = Object.create(null);\n for (const key of Object.keys(args)) {\n if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") {\n continue;\n }\n clean[key] = args[key];\n }\n return clean;\n}\n\nexport class ToolRegistry {\n private tools = new Map<string, RegisteredTool>();\n\n registerTool(\n name: string,\n description: string,\n inputSchema: Record<string, unknown>,\n handler: ToolHandler,\n ): void {\n if (!name || !TOOL_NAME_PATTERN.test(name)) {\n throw new Error(\n `Invalid tool name \"${name}\" - must match ${TOOL_NAME_PATTERN}`,\n );\n }\n if (name.length > 256) {\n throw new Error(\"Tool name must be 256 characters or fewer\");\n }\n this.tools.set(name, { name, description, inputSchema, handler });\n }\n\n unregisterTool(name: string): boolean {\n return this.tools.delete(name);\n }\n\n hasTool(name: string): boolean {\n return this.tools.has(name);\n }\n\n async executeTool(request: ToolCallRequest): Promise<ToolResult> {\n const tool = this.tools.get(request.toolName);\n if (!tool) {\n return {\n call_id: request.callId,\n tool_name: request.toolName,\n result: `Tool \"${request.toolName}\" not found`,\n is_error: true,\n };\n }\n\n try {\n const result = await tool.handler(sanitizeArgs(request.arguments));\n return {\n call_id: request.callId,\n tool_name: request.toolName,\n result,\n is_error: false,\n };\n } catch (err) {\n return {\n call_id: request.callId,\n tool_name: request.toolName,\n result: err instanceof Error ? err.message : String(err),\n is_error: true,\n };\n }\n }\n\n getManifest(): ToolDefinition[] {\n return Array.from(this.tools.values()).map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.inputSchema,\n }));\n }\n\n getToolNames(): string[] {\n return Array.from(this.tools.keys());\n }\n\n clear(): void {\n this.tools.clear();\n }\n}\n","// =============================================================================\n// Protocol adapter registry — pluggable UI protocol layer\n// =============================================================================\n//\n// The SDK is framework-agnostic: it never renders. But when the backend\n// emits an MCP-style embedded resource (A2UI today, other protocols\n// tomorrow) the consumer needs to hand the payload to a renderer. This\n// file defines the contract between those two sides:\n//\n// • `ProtocolAdapter` — an opaque, framework-specific handle that\n// claims a MIME type. The SDK stores it; the consumer narrows the\n// type when reading it back out.\n//\n// • `ProtocolRegistry` — a MIME-keyed map of adapters. One lives on\n// each `ChatSession` so its lifecycle matches the session: clearing\n// on disconnect, swapping on reconnect with a different agent.\n//\n// The consumer decides _whether_ to register an adapter — typically by\n// consulting `session.agentStatus.uiComponents` after `connect()`.\n// The SDK never auto-registers anything; it just stores what it's told.\n// =============================================================================\n\n/**\n * Minimal adapter contract. Frontends extend this with a `render()`\n * method (or equivalent) returning their framework's view type.\n */\nexport interface ProtocolAdapter {\n /** IANA-style MIME type this adapter handles (e.g. ``application/json+a2ui``). */\n readonly mimeType: string;\n}\n\n/**\n * MIME-keyed adapter map, generic on the adapter subtype so consumers\n * can register richer shapes without casting on every read.\n */\nexport class ProtocolRegistry<T extends ProtocolAdapter = ProtocolAdapter> {\n private adapters = new Map<string, T>();\n\n /** Register or replace the adapter for a MIME type. */\n register(adapter: T): void {\n this.adapters.set(adapter.mimeType, adapter);\n }\n\n /** Remove the adapter for a MIME type. No-op if not registered. */\n unregister(mimeType: string): void {\n this.adapters.delete(mimeType);\n }\n\n /** Returns the adapter for a MIME type, or ``null`` if none is registered. */\n get(mimeType: string): T | null {\n return this.adapters.get(mimeType) ?? null;\n }\n\n has(mimeType: string): boolean {\n return this.adapters.has(mimeType);\n }\n\n /** Drop every adapter. Called when a session disconnects. */\n clear(): void {\n this.adapters.clear();\n }\n\n listMimeTypes(): string[] {\n return Array.from(this.adapters.keys());\n }\n}\n","// =============================================================================\n// Wire → ChatEvent translation\n//\n// Single source of truth for the pure translation from backend wire types\n// (snake_case, mirroring Pydantic) to consumer-facing ChatEvents (camelCase).\n// Shared between the live session loop and the persisted-event replay path,\n// so adding a new event type only requires updating one place.\n// =============================================================================\n\nimport type {\n BlockDeltaPayload,\n ChatEvent,\n WireBlockDeltaPayload,\n WireEvent,\n} from \"./types.js\";\nimport type { MemoryRecord, TodoItem } from \"./custom-events.js\";\n\n// --- Delta channels ---\n\n/**\n * Translate a WireBlockDeltaPayload into the consumer-facing shape. Returns\n * ``null`` for unknown channels so forward-compatible backends can add new\n * ones without breaking old clients.\n */\nexport function translateDelta(\n wire: WireBlockDeltaPayload,\n): BlockDeltaPayload | null {\n switch (wire.channel) {\n case \"text\":\n return { channel: \"text\", text: wire.text };\n case \"thinking\":\n return { channel: \"thinking\", text: wire.text };\n case \"signature\":\n return { channel: \"signature\", signature: wire.signature };\n case \"input\":\n return { channel: \"input\", partialJson: wire.partial_json };\n case \"input_arg\":\n return {\n channel: \"inputArg\",\n argName: wire.arg_name,\n text: wire.text,\n };\n case \"output\":\n return { channel: \"output\", stream: wire.stream, chunk: wire.chunk };\n case \"status\":\n return {\n channel: \"status\",\n status: wire.status,\n note: wire.note,\n };\n default:\n return null;\n }\n}\n\n// --- Custom events ---\n\nfunction translateAgentIdentity(raw: Record<string, unknown>) {\n return {\n name: (raw.name as string) ?? \"\",\n displayName: (raw.display_name as string | null) ?? null,\n avatarUrl: (raw.avatar_url as string | null) ?? null,\n description: (raw.description as string | null) ?? null,\n };\n}\n\n/**\n * Translate a wire TodoItem (snake_case — `active_form`, `blocked_by`,\n * mirroring `backend/src/stream/protocol.py`) into the consumer-facing\n * camelCase shape. The backend emits todos exactly as the payload catalog\n * defines them, so the translation is the single place the casing is fixed;\n * consumers can rely on `todo.activeForm` / `todo.blockedBy` matching the\n * declared types.\n */\nfunction translateTodoItem(raw: Record<string, unknown>): TodoItem {\n return {\n id: (raw.id as number) ?? 0,\n subject: (raw.subject as string) ?? \"\",\n status: (raw.status as TodoItem[\"status\"]) ?? \"pending\",\n description: (raw.description as string | null) ?? null,\n activeForm: (raw.active_form as string | null) ?? null,\n owner: (raw.owner as string | null) ?? null,\n blockedBy: (raw.blocked_by as number[] | null) ?? null,\n blocks: (raw.blocks as number[] | null) ?? null,\n priority: (raw.priority as number | null) ?? null,\n };\n}\n\n/**\n * Translate a wire custom event (``{type: \"custom\", name, data}``) into a\n * typed ChatEvent. Unknown names fall through to the generic ``custom``\n * passthrough so consumers can still observe future backends.\n */\nexport function translateCustomEvent(\n name: string,\n data: Record<string, unknown>,\n): ChatEvent {\n switch (name) {\n case \"user_message\":\n return {\n type: \"user_message\",\n content: (data.content as string) ?? \"\",\n createdAt: data.created_at as number | undefined,\n };\n case \"title_generated\":\n return {\n type: \"title_generated\",\n title: (data.title as string) ?? \"\",\n };\n case \"todo_update\":\n return {\n type: \"todo_update\",\n todos: ((data.todos as unknown[]) ?? []).map((t) =>\n translateTodoItem(t as Record<string, unknown>),\n ),\n };\n case \"plan_update\":\n return {\n type: \"plan_update\",\n plan: (data.plan as string) ?? \"\",\n };\n case \"note_update\":\n return {\n type: \"note_update\",\n notes: (data.notes as string[]) ?? [],\n };\n case \"context_update\":\n return {\n type: \"context_update\",\n context: (data.context as Record<string, unknown>) ?? {},\n phase: (data.phase as string | null) ?? null,\n updatedAt: (data.updated_at as number | null) ?? null,\n };\n case \"subagent_start\":\n return {\n type: \"subagent_start\",\n agent: translateAgentIdentity(\n (data.agent as Record<string, unknown>) ?? {},\n ),\n taskCallId: (data.task_call_id as string | null) ?? null,\n };\n case \"subagent_stop\":\n return {\n type: \"subagent_stop\",\n agent: translateAgentIdentity(\n (data.agent as Record<string, unknown>) ?? {},\n ),\n taskCallId: (data.task_call_id as string | null) ?? null,\n };\n case \"context_warning\":\n return {\n type: \"context_warning\",\n severity: (data.severity as string) ?? \"warning\",\n utilizationPct: (data.utilization_pct as number) ?? 0,\n remainingTokens: (data.remaining_tokens as number) ?? 0,\n windowTokens: (data.window_tokens as number) ?? 0,\n inputTokens: (data.input_tokens as number) ?? 0,\n message: (data.message as string) ?? \"\",\n };\n case \"memory_recall\":\n return {\n type: \"memory_recall\",\n memories: (data.memories as MemoryRecord[] | undefined) ?? [],\n };\n case \"memory_update\":\n return {\n type: \"memory_update\",\n action: (data.action as string) ?? \"\",\n memoryId: (data.memory_id as string | null) ?? null,\n key: (data.key as string | null) ?? null,\n namespace: (data.namespace as string | null) ?? null,\n };\n case \"desktop_stream\":\n return {\n type: \"desktop_stream\",\n url: (data.url as string) ?? \"\",\n sandboxId: (data.sandbox_id as string | null) ?? null,\n };\n case \"attachment_staged\":\n return {\n type: \"attachment_staged\",\n attachmentId: (data.attachment_id as string) ?? \"\",\n filename: (data.filename as string) ?? \"\",\n contentType: (data.content_type as string | null) ?? null,\n sizeBytes: (data.size_bytes as number | null) ?? null,\n };\n case \"workspace_ready\":\n return {\n type: \"workspace_ready\",\n sandboxId: (data.sandbox_id as string) ?? \"\",\n workspacePath: (data.workspace_path as string | null) ?? null,\n };\n case \"asset_created\":\n return {\n type: \"asset_created\",\n assetId: (data.asset_id as string) ?? \"\",\n filename: (data.filename as string) ?? \"\",\n url: (data.url as string | null) ?? null,\n contentType: (data.content_type as string | null) ?? null,\n };\n case \"tool_approval_requested\":\n return {\n type: \"tool_approval_requested\",\n toolName: (data.tool_name as string) ?? \"\",\n callId: (data.call_id as string) ?? \"\",\n arguments: (data.arguments as Record<string, unknown>) ?? {},\n riskLevel: (data.risk_level as string | null) ?? null,\n reason: (data.reason as string | null) ?? null,\n };\n case \"tool_approval_granted\":\n return {\n type: \"tool_approval_granted\",\n toolName: (data.tool_name as string) ?? \"\",\n callId: (data.call_id as string) ?? \"\",\n };\n case \"tool_permission_denied\":\n return {\n type: \"tool_permission_denied\",\n toolName: (data.tool_name as string) ?? \"\",\n callId: (data.call_id as string) ?? \"\",\n reason: (data.reason as string | null) ?? null,\n deniedBy: (data.denied_by as string | null) ?? null,\n };\n case \"tool_harness_warning\":\n return {\n type: \"tool_harness_warning\",\n toolName: (data.tool_name as string) ?? \"\",\n callId: (data.call_id as string) ?? \"\",\n message: (data.message as string | null) ?? null,\n details: (data.details as Record<string, unknown> | null) ?? null,\n };\n case \"user_unavailable\":\n return {\n type: \"user_unavailable\",\n consecutiveTimeouts: (data.consecutive_timeouts as number) ?? 0,\n toolName: (data.tool_name as string | null) ?? null,\n };\n case \"prompt_suggestion\":\n return {\n type: \"prompt_suggestion\",\n suggestions: (data.suggestions as string[]) ?? [],\n };\n case \"state_changed\":\n return {\n type: \"state_changed\",\n state: (data.state as string) ?? \"\",\n };\n default:\n return { type: \"custom\", name, data };\n }\n}\n\n// --- Top-level wire events ---\n\n/**\n * Legacy-transport payload extractor for events that aren't wrapped in a\n * ``CustomEvent`` envelope. The backend's ``writer.emit(name, data)`` path\n * spreads the payload fields at the top level of the wire object (see\n * ``backend/src/jobs/suggestion_hook.py`` for ``prompt_suggestion``), so\n * the wire object itself *is* the ``data`` dict for ``translateCustomEvent``.\n * This helper isolates the unsafe cast to one place.\n */\nfunction legacyCustomEventData(wire: WireEvent): Record<string, unknown> {\n return wire as unknown as Record<string, unknown>;\n}\n\n/**\n * Translate a full WireEvent into its typed ChatEvent counterpart. Returns\n * ``null`` when the wire payload is malformed (e.g. unknown delta channel)\n * so the caller can skip it without crashing the stream.\n */\nexport function translateWireEvent(wire: WireEvent): ChatEvent | null {\n // Legacy transport: ``prompt_suggestion`` is emitted via the raw\n // ``writer.emit()`` path rather than wrapped in a CustomEvent envelope,\n // so its ``type`` field is the event name itself. Route it through the\n // custom-event translator to keep the mapping in one place.\n if ((wire as { type: string }).type === \"prompt_suggestion\") {\n return translateCustomEvent(\n \"prompt_suggestion\",\n legacyCustomEventData(wire),\n );\n }\n switch (wire.type) {\n case \"message_start\":\n return {\n type: \"message_start\",\n turnId: wire.turn_id,\n model: wire.model,\n agentName: wire.agent_name,\n agentDisplayName: wire.agent_display_name,\n agentAvatarUrl: wire.agent_avatar_url,\n };\n case \"block_start\":\n return {\n type: \"block_start\",\n turnId: wire.turn_id,\n path: wire.path,\n parentPath: wire.parent_path ?? null,\n kind: wire.kind,\n metadata: wire.metadata,\n };\n case \"block_delta\": {\n const delta = translateDelta(wire.delta);\n if (!delta) return null;\n return {\n type: \"block_delta\",\n turnId: wire.turn_id,\n path: wire.path,\n delta,\n };\n }\n case \"block_stop\":\n return {\n type: \"block_stop\",\n turnId: wire.turn_id,\n path: wire.path,\n status: wire.status,\n final: wire.final,\n };\n case \"message_stop\":\n return {\n type: \"message_stop\",\n turnId: wire.turn_id,\n jobId: wire.job_id,\n stopReason: wire.stop_reason,\n usage: {\n inputTokens: wire.usage.input_tokens ?? 0,\n outputTokens: wire.usage.output_tokens ?? 0,\n cachedTokens: wire.usage.cached_tokens ?? 0,\n cacheCreationTokens: wire.usage.cache_creation_tokens ?? 0,\n },\n ttfbMs: wire.ttfb_ms,\n totalMs: wire.total_ms,\n stallCount: wire.stall_count,\n };\n case \"stall\":\n return {\n type: \"stall\",\n sinceLastEventMs: wire.since_last_event_ms,\n stallCount: wire.stall_count,\n };\n case \"retry\":\n return {\n type: \"retry\",\n attempt: wire.attempt,\n reason: wire.reason,\n backoffMs: wire.backoff_ms,\n strategy: wire.strategy ?? null,\n maxAttempts: wire.max_attempts ?? null,\n contextRecovery: wire.context_recovery ?? null,\n };\n case \"error\":\n return {\n type: \"error\",\n code: wire.code,\n message: wire.message,\n blockPath: wire.block_path ?? null,\n };\n case \"keepalive\":\n return {\n type: \"keepalive\",\n sinceLastEventMs: wire.since_last_event_ms,\n };\n case \"custom\":\n return translateCustomEvent(wire.name, wire.data);\n default: {\n // Exhaustive guard — a new WireEvent variant should force this switch\n // to be updated at compile time.\n const _exhaustive: never = wire;\n void _exhaustive;\n return null;\n }\n }\n}\n","import { AstralformClient } from \"./client.js\";\nimport { InMemoryStorage, type ChatStorage } from \"./storage.js\";\nimport { ToolRegistry } from \"./tools.js\";\nimport { ProtocolRegistry } from \"./protocol-registry.js\";\nimport { translateWireEvent } from \"./translate.js\";\nimport type {\n AgentInfo,\n AstralformConfig,\n ChatEvent,\n ChatStreamRequest,\n Conversation,\n ConversationEvent,\n Message,\n AgentStatus,\n SendOptions,\n SkillInfo,\n ToolCallRequest,\n ToolResult,\n WireEvent,\n} from \"./types.js\";\nimport { generateId } from \"./utils.js\";\nimport {\n AuthenticationError,\n ConnectionError,\n RateLimitError,\n ServerError,\n} from \"./errors.js\";\n\ntype ChatEventHandler = (event: ChatEvent) => void;\n\n/**\n * Bounded auto-reconnect for a live SSE stream that drops mid-turn (worker\n * restart, network blip). We resume from ``lastSeq`` — the backend replays\n * missed events (``?after=seq``) and, for a job that already died, back-fills a\n * terminal event — so the UI recovers without a manual page refresh. Backoff is\n * exponential and capped: the six sleeps sum to 17.5s (0.5+1+2+4+5+5), which\n * comfortably covers a server restart without spinning forever if the job is\n * genuinely gone.\n *\n * That 17.5s is the BACKOFF total, not the time to give up. An attempt that\n * fails fast costs ~nothing, but one that stalls burns SSE_STALL_TIMEOUT_MS\n * before it even registers as a failure — so with all 7 attempts stalling the\n * worst case is 7 * 135s + 17.5s ≈ 16 minutes. That ceiling is accepted, not\n * accidental: an all-stalls sequence needs every attempt to die the rare QUIC\n * way (fail-fast is the common failure), and shrinking the attempt budget to\n * hold the old ~5.5-minute bound would cost real resilience on flaky networks\n * to improve a pathological case. A stalled stream is indistinguishable from\n * a slow one until the watchdog fires, and cutting that shorter risks killing\n * healthy long-running turns.\n */\nconst SSE_MAX_RECONNECTS = 6;\n\n/**\n * Max silence tolerated on an established SSE stream before we declare it a\n * zombie and reconnect. The backend emits a keepalive on a fixed cadence\n * (``subscribe.py``), moving from every 15s to every 45s to cut mobile radio\n * wake-ups, so a healthy stream never goes quiet this long. This\n * matters because some failures (notably HTTP/3 / QUIC connection deaths)\n * leave ``reader.read()`` pending forever — no bytes, no error, no FIN — and\n * without a watchdog the retry loop below never engages and the UI hangs on\n * \"working\" indefinitely.\n *\n * DEPENDS ON THE KEEPALIVE'S FRAMING, not just its interval. The backend\n * sends it as a typed wire event (``{\"event\": \"keepalive\", \"data\": ...}``), so\n * it reaches ``streamJobSSE``'s parser as a real ``data:`` line and resets the\n * timer below. An SSE-protocol comment (``: keepalive``) would be silently\n * swallowed by that parser — it only reacts to ``event:``/``data:`` — and this\n * watchdog would then fire on every healthy turn that thinks this long. If\n * the backend ever changes that framing, this constant has to change with\n * it.\n *\n * 3x the PLANNED 45s keepalive cadence, not the current 15s one: 135s\n * tolerates both, so this client can ship ahead of the backend flip without\n * ever racing the keepalive it depends on.\n */\nconst SSE_STALL_TIMEOUT_MS = 135_000;\n\n// Retries for the client-tool result POST itself, independent of the SSE\n// reconnect loop — reconnecting the *stream* can't recover a failed *result\n// submission*, and retrying the POST avoids re-executing a client tool.\nconst TOOL_RESULT_MAX_RETRIES = 3;\n\n/**\n * Conversations fetched per page, by ``connect`` and ``loadMoreConversations``\n * alike. The two must use the same size: the offset is derived from how many\n * rows the server has returned so far, so a first page of a different size\n * would leave the second page's offset pointing at the wrong row.\n */\nexport const CONVERSATION_PAGE_SIZE = 50;\n\nfunction sseReconnectDelayMs(attempt: number): number {\n return Math.min(500 * 2 ** (attempt - 1), 5000);\n}\n\nfunction pathEquals(a: number[], b: number[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * ChatSession — translates the backend wire protocol into typed ChatEvents\n * for consumers. Owns HTTP + SSE plumbing, conversation state, and the\n * client-tool round-trip. Does NOT own block construction — consumers\n * build their own block state from the typed events.\n */\nexport class ChatSession {\n readonly client: AstralformClient;\n readonly toolRegistry: ToolRegistry;\n readonly storage: ChatStorage;\n /**\n * Pluggable UI protocol adapters. Consumers register a framework-\n * specific adapter (e.g. React) for each MIME type they can render,\n * typically gated on ``session.agentStatus.uiComponents.protocol``.\n * ``ToolBlock``-style consumers look up the adapter for an incoming\n * embedded resource and hand off rendering.\n */\n readonly protocols = new ProtocolRegistry();\n\n // State\n conversationId: string | null = null;\n conversations: Conversation[] = [];\n /**\n * Whether another page of conversations may exist on the server.\n *\n * Inferred from the last page being full, since the list endpoint returns a\n * bare array with no total. A total that happens to be an exact multiple of\n * the page size therefore costs one extra empty request before this flips —\n * cheaper than adding a count query to every list call.\n */\n hasMoreConversations = false;\n /** True while ``loadMoreConversations`` is in flight. */\n isLoadingConversations = false;\n messages: Message[] = [];\n /**\n * Which conversation ``messages`` currently holds.\n *\n * Distinct from ``conversationId``, and the distinction is the point:\n * ``loadConversation`` moves the POINTER synchronously and installs the LIST\n * an await later, so for the whole duration of every load the two disagree.\n * Anything pairing a message with a conversation — ``regenerate`` above all —\n * has to read this one, or it will pair the previous conversation's last\n * message with the new conversation's id.\n */\n messagesConversationId: string | null = null;\n isStreaming = false;\n agentStatus: AgentStatus | null = null;\n agents: AgentInfo[] = [];\n skills: SkillInfo[] = [];\n enabledClientTools = new Set<string>();\n modelDisplayName: string | null = null;\n\n /**\n * Ids of conversations the SERVER has handed us, which is the paging offset.\n *\n * Deliberately not ``conversations.length``. That array also holds\n * conversations created locally and unshifted on top (``createNewConversation``,\n * and the auto-created conversation in ``consumeJobStream``), so using its\n * length as the offset would over-count and silently skip a row of real\n * history on the next page. Tracking ids rather than a counter also makes\n * deletion self-correcting: removing a server-sourced conversation shifts\n * every later page up by one, and dropping its id from this set is exactly\n * that shift — while deleting a purely local one correctly changes nothing.\n */\n private serverConversationIds = new Set<string>();\n\n /**\n * Bumped every time ``connect()`` re-seeds the conversation list.\n *\n * A ``loadMoreConversations`` request issued before a re-seed describes the\n * OLD paging state, so applying its response afterwards both appends the\n * wrong rows and corrupts the offset. Concretely: with 100 rows held, an\n * offset-100 response landing after a reconnect has reset to rows 0-49 would\n * append rows 100-149 — a 50-row hole — and leave the id set at 100, so every\n * later page re-requests offset 100 and never advances again. The generation\n * is captured before the await and rechecked after, so a superseded response\n * is discarded instead.\n */\n private conversationsGeneration = 0;\n\n /**\n * Bumped by every ``loadConversation`` call, so an out-of-order fetch can\n * tell it is no longer the newest one and drop its result. Separate from\n * ``conversationsGeneration``, which guards the conversation LIST.\n */\n private loadGeneration = 0;\n\n /**\n * Ids of locally-created user messages the server has not acknowledged yet.\n *\n * Arrival time cannot answer \"could the reply have included this?\" on its\n * own. Every `loadConversation` in `StreamManager` sits behind the\n * active-job probe, so the ORDINARY ordering is that a send lands BEFORE the\n * load starts — and an arrival-time rule drops exactly those, losing the\n * prompt the user just sent while its stream is still running. Membership\n * here is set by `send` and cleared when a server row turns up carrying the\n * same turn, so the keep-decision no longer depends on which side of the\n * fetch the push landed on.\n *\n * The value is `serverRowsKnown` as of the `message_stop` that proved the\n * row committed, or 0 until then — including after the job response, which\n * hands back the id but starts the loop as a background task and so proves\n * nothing about the row. That stamp is what separates \"the snapshot predates\n * the row\" from \"the server does not have this row\" — see\n * `loadConversation`.\n *\n * It is an ANNOTATION ON `this.messages`: reconciliation only ever consults\n * entries of that array, so an id whose message has left it is dead weight.\n * `setMessages` is the single place the array is replaced, and it prunes.\n */\n private pendingUserMessages = new Map<string, number>();\n\n /**\n * Bumped once per completed turn, at `message_stop`, so a fetch can record\n * what was proven when it was ISSUED. A row proven committed before the\n * fetch went out must appear in its snapshot; one proven after may\n * legitimately be missing.\n */\n private serverRowsKnown = 0;\n\n /**\n * Replace the message list, keeping `pendingUserMessages` an annotation on\n * it. Every removal from the array goes through here — `push` is the only\n * other mutation and it cannot orphan an id.\n */\n private setMessages(next: Message[]): void {\n this.messages = next;\n if (this.pendingUserMessages.size === 0) return;\n const present = new Set(next.map((m) => m.id));\n for (const id of this.pendingUserMessages.keys()) {\n if (!present.has(id)) this.pendingUserMessages.delete(id);\n }\n }\n\n // Minimal in-session accumulation for the assistant message record.\n // Only top-level ``text`` blocks contribute; subagent / tool output\n // is tracked by the consumer's own block store.\n private accumulatedText = \"\";\n private currentTextPath: number[] | null = null;\n\n private handlers: Set<ChatEventHandler> = new Set();\n private abortController: AbortController | null = null;\n\n constructor(config: AstralformConfig, storage?: ChatStorage) {\n this.client = new AstralformClient(config);\n this.toolRegistry = new ToolRegistry();\n this.storage = storage ?? new InMemoryStorage();\n }\n\n on(handler: ChatEventHandler): () => void {\n this.handlers.add(handler);\n return () => {\n this.handlers.delete(handler);\n };\n }\n\n private emit(event: ChatEvent): void {\n for (const handler of this.handlers) {\n try {\n handler(event);\n } catch {\n // Don't let handler errors crash the session\n }\n }\n }\n\n async connect(): Promise<void> {\n const [status, conversations, agents, skills] = await Promise.allSettled([\n this.client.getAgentStatus(),\n this.client.getConversations(CONVERSATION_PAGE_SIZE),\n this.client.getAgents().catch(() => [] as AgentInfo[]),\n this.client.getSkills().catch(() => [] as SkillInfo[]),\n ]);\n\n if (status.status === \"fulfilled\") {\n this.agentStatus = status.value;\n }\n if (conversations.status === \"fulfilled\") {\n // Reconnect re-seeds page 1, so reset the paging state with it rather\n // than letting a previous connection's offset carry over. Bumping the\n // generation here (and only here — a FAILED fetch leaves the list intact,\n // so an in-flight page is still valid against it) invalidates any page\n // request already in flight against the old offset.\n this.conversationsGeneration++;\n this.conversations = conversations.value;\n this.serverConversationIds = new Set(\n conversations.value.map((c) => c.id),\n );\n this.hasMoreConversations =\n conversations.value.length === CONVERSATION_PAGE_SIZE;\n }\n if (agents.status === \"fulfilled\") {\n this.agents = agents.value;\n }\n if (skills.status === \"fulfilled\") {\n this.skills = skills.value;\n }\n\n this.emit({ type: \"connected\" });\n }\n\n async send(content: string, options?: SendOptions): Promise<void> {\n if ((options?.provider == null) !== (options?.model == null)) {\n throw new Error(\n \"`provider` and `model` must be supplied together (client-side model selection).\",\n );\n }\n if (this.isStreaming) return;\n\n const conversationId =\n options?.conversationId ?? this.conversationId ?? undefined;\n // Captured so a send that never reaches the wire can put the list back —\n // see the put-back at the end of this method.\n let relocatedFrom: {\n messages: Message[];\n messagesId: string | null;\n conversationId: string | null;\n generation: number;\n target: string;\n } | null = null;\n\n // Sending to an explicit conversation makes it the session's — catch the\n // pointer up now rather than waiting for an in-flight `loadConversation`\n // to land. `onSessionEvent` tags every emitted event with this field, so\n // leaving it behind would file this send's own stream events under the\n // conversation the caller just addressed away from: the same mis-tagging\n // the restore guards close, re-entering through the send path.\n //\n // A pointer move IS an event a load in flight must lose to — otherwise it\n // passes its own guard and installs its list under a conversation the send\n // has since relocated to. Same rule in `createNewConversation` and\n // `deleteConversation`.\n if (conversationId) {\n // Only a MOVE invalidates a load in flight. Re-stating the conversation\n // the session is already on — the ordinary case, since the manager\n // passes the active id on every send — must leave a load for that same\n // conversation alone, or sending while its history is still arriving\n // would throw the history away.\n if (conversationId !== this.conversationId) {\n this.loadGeneration++;\n // Both pointers, snapshotted separately: they are deliberately\n // distinct everywhere else here, and during an in-flight\n // `loadConversation` they disagree — restoring one into both would\n // rewind `conversationId` to wherever the MESSAGES were rather than\n // where the session pointed. The generation is claimed here too, so\n // the put-back can tell whether it still owns the session.\n relocatedFrom = {\n messages: this.messages,\n messagesId: this.messagesConversationId,\n conversationId: this.conversationId,\n generation: this.loadGeneration,\n target: conversationId,\n };\n // Drop the old conversation's list with the pointer. Leaving it behind\n // is the same one-conversation's-messages-under-another's-id pairing\n // the load guard exists to prevent — and here nothing re-fetches, so\n // for a direct `ChatSession` caller (this is a documented public\n // option) it would simply persist. Through `StreamManager` the\n // in-flight restore reinstalls the right list.\n this.setMessages([]);\n // NOT `conversationId`. After this the list holds at most this one\n // turn, which is not that conversation's history — and\n // `StreamManager.regenerate` trusts this field, so claiming it would\n // let regenerate fire against a view missing everything before it.\n // `null` keeps the claim honest and the gate closed until a real load.\n this.messagesConversationId = null;\n }\n this.conversationId = conversationId;\n }\n\n const userMessage: Message = {\n id: generateId(),\n conversationId: conversationId ?? \"\",\n role: \"user\",\n content,\n status: \"complete\",\n createdAt: new Date().toISOString(),\n };\n if (conversationId) {\n // Best-effort, matching the sibling call in `consumeJobStream`. On the\n // relocation path the session is ALREADY moved and the list already\n // emptied by the time this runs, so an uncaught rejection escapes `send`\n // before the put-back at the bottom and strands it there — pointed at\n // the target with an empty list and `messagesConversationId` null, i.e.\n // `regenerate` gated off with nothing left to reopen it.\n await this.storage\n .addMessage(userMessage, conversationId)\n .catch(() => {});\n }\n this.messages.push(userMessage);\n // 0: no job response yet, so the server may not hold this row at all.\n //\n // Unconditional. Gated on `conversationId`, the auto-created-conversation\n // path (a direct `session.send(\"hi\")` with no conversation anywhere) was\n // the one branch outside this machinery: `consumeJobStream`'s\n // reconciliation keys off membership here, so that row kept its client id\n // even though the job response had just returned the server's — leaving it\n // unprotected by the merge below and feeding `resendFromCheckpoint` an id\n // the server never issued. The window where an entry would be wrong is\n // already excluded: `loadConversation` filters on `m.conversationId === id`\n // and this row's is `\"\"` until `consumeJobStream` backfills it, which it\n // does immediately before the reconciliation.\n this.pendingUserMessages.set(userMessage.id, 0);\n\n const request: ChatStreamRequest = {\n message: content,\n conversation_id: conversationId,\n mcp_manifest: this.toolRegistry.getManifest(),\n enabled_mcp: Array.from(\n options?.enabledClientTools ?? this.enabledClientTools,\n ),\n upload_ids: options?.uploadIds,\n agent_name: options?.agentName,\n plan_mode: options?.planMode,\n image_mode: options?.imageMode,\n video_mode: options?.videoMode,\n goal: options?.goal,\n // The project this task belongs to, when it has one. Write-once\n // server-side: sent on every turn, honoured on the first.\n repository: options?.repository,\n // Per-request model choice (client-side model selection).\n provider: options?.provider,\n model: options?.model,\n reasoning_effort: options?.reasoningEffort,\n temperature: options?.temperature,\n };\n\n // `processStream` never rejects — it catches and emits an `error` event —\n // so a thrown-exception guard here would be dead code. The signal has to\n // SURVIVE the turn, which `currentJobId` does not: `message_stop` nulls it,\n // so after any completed send it reads exactly as it did before, and a\n // put-back keyed on it would fire on the success path and revert a\n // relocation that worked.\n // Did THIS send reach the wire? Per-call, not a session counter: the\n // `isStreaming` bail above fires before `processStream` sets the flag,\n // with an `await storage.addMessage` in between, so two sends can\n // interleave and a shared counter would answer \"did ANY job get created\n // during my await\". `currentJobId` cannot serve either — `message_stop`\n // nulls it, so after a completed turn it reads as it did before the send.\n const wire: { reached: boolean } = { reached: false };\n await this.processStream(request, wire);\n\n // The relocation above emptied the list before anything was sent. If no job\n // was created there is no new conversation's history to replace it and\n // nothing that will re-fetch, so a direct `ChatSession` caller would be\n // left holding nothing at all. Put it back.\n // Guarded like every other post-await mutation in this file. `createJob`\n // can hang and then fail, and the session can have moved on meanwhile — an\n // unguarded put-back would rewind it to a conversation the user left, which\n // is the failure this whole change exists to prevent, arriving through the\n // one path that was still missing the check.\n // No job means nothing can ever acknowledge this message, so its entry\n // would sit at `knownAt === 0` forever and every later load would\n // re-append a message the server never received. Unconditional, because\n // the put-back below only runs when the send RELOCATED — not the ordinary\n // shape. Keyed on the counter, not the throw: a job that WAS created has\n // had `userMessage.id` replaced with the server's.\n if (!wire.reached) {\n this.pendingUserMessages.delete(userMessage.id);\n // The ROW too, not just its map entry. A load resolving between the push\n // and here has already merged this row into `messages` — correctly, for\n // the success case — and it survives with a client-minted id, which\n // `regenerate` then hands to `resend_from`. Nothing acknowledged it, and\n // `status` says \"complete\" — a claim a send that never reached the wire\n // has no business making.\n const at = this.messages.indexOf(userMessage);\n if (at !== -1) this.messages.splice(at, 1);\n // And the STORAGE copy. `addMessage` above runs before `createJob`, so\n // it has already landed on this path — and `loadConversation` falls back\n // to `storage.fetchMessages` whenever the API fetch rejects, which would\n // reinstall the phantom on any later offline load. Addressable because\n // the id is still the client's: the rename in `consumeJobStream` only\n // runs after `createJob` resolves, which by definition did not happen.\n if (conversationId) {\n await this.storage.deleteMessage(userMessage.id).catch(() => {});\n }\n }\n if (\n relocatedFrom &&\n wire.reached &&\n this.loadGeneration === relocatedFrom.generation\n ) {\n // A SUFFIX of the target conversation rather than its whole history,\n // but it is that conversation's — and `regenerate`, the only consumer of\n // this pairing, needs just the last user turn, whose id is now the\n // server's. Left `null`, nothing here would ever reopen the gate.\n // From the snapshot, not from `conversationId ?? null`: the relocation\n // only runs under `if (conversationId)`, so that fallback was dead and\n // read as though the id could be absent here.\n this.messagesConversationId = relocatedFrom.target;\n } else if (\n relocatedFrom &&\n !wire.reached &&\n this.loadGeneration === relocatedFrom.generation\n ) {\n // No pending-id snapshot to restore alongside. `POST /v1/jobs` always\n // returns `message_id` (required on both sides of the contract; the\n // backend mints it with `uuid4()` before validating the request), so\n // every id the relocation pruned had already been reconciled and carries\n // a `knownAt` at or below any later fetch's issue point — meaning\n // `loadConversation` would not re-append it even if it were restored.\n this.setMessages(relocatedFrom.messages);\n this.messagesConversationId = relocatedFrom.messagesId;\n this.conversationId = relocatedFrom.conversationId;\n }\n }\n\n async resendFromCheckpoint(\n messageId: string,\n newContent: string,\n ): Promise<void> {\n if (this.isStreaming) return;\n\n const request: ChatStreamRequest = {\n message: newContent,\n conversation_id: this.conversationId ?? undefined,\n resend_from: messageId,\n mcp_manifest: this.toolRegistry.getManifest(),\n enabled_mcp: Array.from(this.enabledClientTools),\n };\n\n await this.processStream(request);\n }\n\n private resetStreamingState(): void {\n this.accumulatedText = \"\";\n this.currentTextPath = null;\n }\n\n private async processStream(\n request: ChatStreamRequest,\n wire?: { reached: boolean },\n ): Promise<void> {\n this.isStreaming = true;\n this.resetStreamingState();\n const controller = new AbortController();\n this.abortController = controller;\n\n try {\n await this.consumeJobStream(request, wire);\n } catch (err) {\n if (!(err instanceof DOMException && err.name === \"AbortError\")) {\n this.emit({\n type: \"error\",\n code: \"connection_error\",\n message: err instanceof Error ? err.message : String(err),\n blockPath: null,\n });\n }\n } finally {\n // Only if this invocation still OWNS the turn. `detach()` cannot\n // interrupt a client tool — `executeClientTools` awaits arbitrary\n // consumer code with no signal — so this `finally` can run long after a\n // switch handed the session to a newer turn. Clearing unconditionally\n // then nulls THAT turn's flag and controller: its stream keeps pumping\n // while the session reports idle, `stop()` aborts nothing, and the next\n // switch neither parks its job nor stops it emitting under the new\n // conversation's id. The mirror of `ownsTurnState`, on the live side.\n if (this.abortController === controller) {\n this.isStreaming = false;\n this.abortController = null;\n }\n }\n }\n\n /** Last received sequence number for resumable reconnection */\n private lastSeq = -1;\n\n /**\n * Client-tool call_ids whose result was already submitted this turn. On a\n * reconnect the resumed stream can replay a tool request we already handled;\n * this dedups so each is executed + submitted at most once (but a request we\n * never submitted still runs). Cleared at the start of each turn.\n */\n private submittedToolCallIds = new Set<string>();\n\n /** Current job ID for cancellation */\n currentJobId: string | null = null;\n\n private async consumeJobStream(\n request: ChatStreamRequest,\n wire?: { reached: boolean },\n ): Promise<void> {\n const job = await this.client.createJob(request);\n if (wire) wire.reached = true;\n this.currentJobId = job.job_id;\n\n const conversationId = job.conversation_id;\n if (!this.conversationId) {\n this.conversationId = conversationId;\n // The list in hand is this conversation's — the backend just created it\n // around the turn being sent. Without this the pairing never becomes\n // valid and `regenerate` is a permanent no-op for a consumer that\n // reached a conversation this way.\n this.messagesConversationId = conversationId;\n }\n // Ensure the conversation exists in both the local array and\n // ChatStorage so title_generated, completeStream, and fallback\n // reload all work for backend-created conversations.\n if (!this.conversations.some((c) => c.id === conversationId)) {\n const now = new Date().toISOString();\n const conv = {\n id: conversationId,\n title: \"\",\n messageCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n this.conversations.unshift(conv);\n await this.storage.createConversation(conversationId, \"\").catch(() => {});\n }\n // Backfill the just-sent user message if send() ran before we knew the\n // conversation id (first turn of an auto-created conversation).\n const lastMsg = this.messages[this.messages.length - 1];\n if (lastMsg?.role === \"user\" && !lastMsg.conversationId) {\n lastMsg.conversationId = conversationId;\n await this.storage.addMessage(lastMsg, conversationId).catch(() => {});\n }\n const promptMessageId = job.message_id;\n // Reconcile the local id with the server's. `send` minted a client id that\n // the backend never sees, so nothing downstream could ever match the two —\n // which forced acknowledgement to be guessed from role+content, and no\n // content heuristic can tell a genuinely repeated prompt (\"continue\",\n // \"retry\" — the norm in an agent chat) from one the server already holds.\n // The job response carries the real id, so take it and the question\n // becomes exact.\n if (\n promptMessageId &&\n lastMsg?.role === \"user\" &&\n this.pendingUserMessages.has(lastMsg.id)\n ) {\n this.pendingUserMessages.delete(lastMsg.id);\n const clientMintedId = lastMsg.id;\n lastMsg.id = promptMessageId;\n // Still 0 — the id is now the server's, but the ROW is not proven\n // committed. `POST /v1/jobs` mints the id and starts the loop as a\n // BACKGROUND task (`start_job_task`) before returning, so the prompt is\n // persisted by the loop, not by the handler. `message_stop` is the\n // earliest point the row is certainly there.\n this.pendingUserMessages.set(promptMessageId, 0);\n // Write the rename THROUGH to storage. In memory it lands either way,\n // and with `InMemoryStorage` the stored copy is the same object by\n // reference — so it silently picks the new id up and the divergence is\n // invisible in tests. A `ChatStorage` that serializes on write\n // (IndexedDB, SQLite — the reason the interface is public) keeps the\n // client id forever, and `loadConversation`'s fallback to\n // `storage.fetchMessages` would then reinstate it, handing\n // `resend_from` an id the server never issued. No `updateMessage` on\n // the interface, so delete + re-add.\n if (conversationId && clientMintedId !== promptMessageId) {\n await this.storage.deleteMessage(clientMintedId).catch(() => {});\n await this.storage.addMessage(lastMsg, conversationId).catch(() => {});\n }\n // A snapshot can resolve after the backend commits this row but before\n // the job response arrives, and until it does the local copy carries a\n // client id the server never saw — so there was no id to match on and\n // `loadConversation` kept both. Renaming would then put two rows under\n // one id, the state the assistant row's own id exists to prevent, and\n // `planRestore`'s `byId` would resolve to the wrong one. The local copy\n // is the survivor: it is the newest message, so the tail is where it\n // belongs. Removing the other cannot orphan its map entry, since the\n // survivor now carries the same id.\n const dupe = this.messages.findIndex(\n (m) => m !== lastMsg && m.id === promptMessageId,\n );\n if (dupe !== -1) this.messages.splice(dupe, 1);\n }\n this.lastSeq = -1;\n this.submittedToolCallIds.clear();\n\n await this.consumeEventStream(\n job.job_id,\n conversationId,\n promptMessageId,\n true, // executeClientTools\n );\n }\n\n /**\n * Shared event consumption loop. Parses each wire event, updates\n * minimal session state, and emits typed ChatEvents to consumers.\n */\n private async consumeEventStream(\n jobId: string,\n conversationId: string,\n promptMessageId: string,\n executeClientTools: boolean,\n ): Promise<void> {\n // Capture the signal ONCE. detach()/disconnect() abort the controller and\n // then null it out synchronously, so re-reading this.abortController later\n // would lose the aborted state (?. → undefined → falsy) and the loop would\n // reconnect an unstoppable, signal-less stream. The AbortSignal stays valid\n // (and stays aborted) even after the controller is gone.\n const signal = this.abortController?.signal;\n\n for (let attempt = 0; ; attempt++) {\n // Bail BEFORE building the next attempt. An already-aborted signal never\n // dispatches `abort` again, so the listener below would silently miss it\n // and this attempt would go out after the caller disconnected — emitting\n // events, and potentially running a client tool, on a session it believes\n // is gone. Passing the outer signal straight to fetch used to make that\n // impossible (fetch checks `signal.aborted` up front); the per-attempt\n // controller removes that guarantee unless it is restored here.\n if (signal?.aborted) return;\n // Each attempt gets its own controller, linked to the session signal,\n // so the stall watchdog can kill a zombie connection without aborting\n // the whole session — the retry below then resumes from lastSeq.\n const attemptController = new AbortController();\n const linkAbort = () => attemptController.abort();\n signal?.addEventListener(\"abort\", linkAbort);\n // Client tools stay enabled across reconnects; re-seen tool requests are\n // deduped by submitted call_id in dispatchWireEvent, so a tool whose\n // result we never posted (drop before submit) still runs on resume.\n let sawTerminal: boolean;\n try {\n const stream = this.client.streamJobEvents(\n jobId,\n this.lastSeq,\n attemptController.signal,\n );\n sawTerminal = await this.pumpStream(\n stream,\n conversationId,\n promptMessageId,\n executeClientTools,\n () => attemptController.abort(),\n );\n } catch (err) {\n if (signal?.aborted) return; // user cancelled / detached\n // Auth failures and rate limits can't be fixed by reconnecting (and\n // hammering a 429 is harmful) — surface them immediately. Genuine\n // connectivity failures (incl. a 5xx from a restarting server) retry.\n if (\n err instanceof AuthenticationError ||\n err instanceof RateLimitError\n ) {\n throw err;\n }\n if (attempt >= SSE_MAX_RECONNECTS) throw err;\n await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);\n continue; // resume from lastSeq\n } finally {\n signal?.removeEventListener(\"abort\", linkAbort);\n }\n\n // A terminal event (message_stop / error) ends the turn — including the\n // backend's back-filled terminal for a job that died mid-stream.\n if (sawTerminal || signal?.aborted) return;\n\n // Stream ended WITHOUT a terminal event: the worker/connection dropped\n // mid-turn. Resume from lastSeq so the backend can replay missed events\n // (and back-fill a terminal for a dead job) rather than leave the UI\n // hanging on \"working\".\n if (attempt >= SSE_MAX_RECONNECTS) {\n throw new ConnectionError(\"Lost connection to the response stream.\");\n }\n await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);\n }\n }\n\n /**\n * Consume a single SSE stream to exhaustion. Returns whether a terminal\n * event (``message_stop`` / ``error``) was seen, so the caller can decide\n * whether an ended stream means \"turn done\" vs \"dropped, reconnect\".\n *\n * ``onStall`` aborts the per-attempt connection: if no event arrives within\n * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15-45s), the stream is a\n * zombie — ``reader.read()`` will never settle — so we kill the fetch and\n * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.\n */\n private async pumpStream(\n stream: AsyncGenerator<{ data: string }>,\n conversationId: string,\n promptMessageId: string,\n executeClientTools: boolean,\n onStall?: () => void,\n ): Promise<boolean> {\n let sawTerminal = false;\n const iterator = stream[Symbol.asyncIterator]();\n try {\n while (true) {\n const next = iterator.next();\n let stallTimer: ReturnType<typeof setTimeout> | undefined;\n const stall = new Promise<never>((_, reject) => {\n stallTimer = setTimeout(() => {\n // Reject BEFORE aborting, and the order is load-bearing. Aborting\n // first makes the pending read reject too (StreamAbortedError from\n // `streaming.ts`), and whichever settles first wins the race below\n // — so a stall could surface as `stream_aborted`. Both retry\n // identically, but the diagnostic would then contradict this very\n // comment. Rejecting first settles the race deterministically;\n // `reject` changes state synchronously, so the later abort is a\n // no-op for the race.\n reject(\n new ConnectionError(\n `Stream stalled: no events for ${SSE_STALL_TIMEOUT_MS}ms`,\n ),\n );\n // Then cancel the zombie fetch so the connection is released\n // instead of leaking one per reconnect.\n onStall?.();\n }, SSE_STALL_TIMEOUT_MS);\n });\n let result: IteratorResult<{ data: string }>;\n try {\n result = await Promise.race([next, stall]);\n } finally {\n clearTimeout(stallTimer);\n }\n if (result.done) break;\n const raw = result.value;\n let parsed: WireEvent;\n try {\n const data = JSON.parse(raw.data);\n if (\n typeof data !== \"object\" ||\n data === null ||\n typeof data.type !== \"string\"\n ) {\n // Legacy \"done\" sentinel — backend still emits it for subscribers.\n // Silently consume; the new protocol uses message_stop for turn end.\n if (typeof (data as { seq?: unknown })?.seq === \"number\") {\n this.lastSeq = (data as { seq: number }).seq;\n }\n continue;\n }\n parsed = data as WireEvent;\n if (typeof (data as Record<string, unknown>).seq === \"number\") {\n this.lastSeq = (data as Record<string, unknown>).seq as number;\n }\n } catch {\n continue;\n }\n\n if (parsed.type === \"message_stop\" || parsed.type === \"error\") {\n sawTerminal = true;\n }\n\n await this.dispatchWireEvent(\n parsed,\n conversationId,\n promptMessageId,\n executeClientTools,\n );\n }\n } finally {\n // Fire-and-forget: on the stall path the pending read may never settle\n // (e.g. a custom fetchFn that ignores the abort signal), and awaiting\n // this would re-hang the pump the watchdog just rescued.\n void iterator.return?.(undefined).catch(() => {});\n }\n return sawTerminal;\n }\n\n /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */\n private sleepUnlessAborted(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) return resolve();\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n\n /** POST a client-tool result, retrying transient failures a few times. */\n private async submitToolResultWithRetry(\n payload: Parameters<AstralformClient[\"submitToolResult\"]>[0],\n ): Promise<void> {\n const signal = this.abortController?.signal;\n for (let attempt = 0; ; attempt++) {\n try {\n await this.client.submitToolResult(payload);\n return;\n } catch (err) {\n if (signal?.aborted) throw err;\n if (\n err instanceof AuthenticationError ||\n err instanceof RateLimitError\n ) {\n throw err;\n }\n if (attempt >= TOOL_RESULT_MAX_RETRIES) throw err;\n await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);\n }\n }\n }\n\n private async dispatchWireEvent(\n wire: WireEvent,\n conversationId: string,\n promptMessageId: string,\n executeClientTools: boolean,\n ): Promise<void> {\n // Side effects that depend on mutable session state must run before the\n // ChatEvent is emitted so consumers see a consistent view.\n this.applyWireSideEffects(wire, conversationId, promptMessageId, true);\n\n const event = translateWireEvent(wire);\n if (event) {\n this.emit(event);\n }\n\n // Client tool round-trip — deferred to block_stop with\n // status=awaiting_client_result, where the parsed input is in final.input.\n if (\n executeClientTools &&\n wire.type === \"block_stop\" &&\n wire.status === \"awaiting_client_result\" &&\n wire.final?.call_id\n ) {\n const f = wire.final;\n const callId = (f.call_id as string) ?? \"\";\n // Dedup across reconnects: a resumed stream can replay a tool request we\n // already handled. Execute + submit each call_id at most once — but DO\n // run requests not yet submitted (e.g. the drop happened before we could\n // post the result), rather than skipping client tools wholesale.\n if (callId && !this.submittedToolCallIds.has(callId)) {\n const request: ToolCallRequest = {\n callId,\n toolName: (f.tool_name as string) ?? \"\",\n arguments: (f.input as Record<string, unknown>) ?? {},\n isClientTool: true,\n };\n const results = await this.executeClientTools([request]);\n // Retry the POST itself before giving up: reconnecting the SSE stream\n // can't recover a failed result submission, and retrying here avoids\n // re-executing the tool on a transient network blip.\n await this.submitToolResultWithRetry({\n conversation_id: conversationId,\n // The message that TRIGGERED the tool calls, which is what the\n // backend stores it against — the prompt id, correctly.\n message_id: promptMessageId,\n tool_results: results,\n });\n // Marked only after a successful submit, so a drop mid-POST re-runs it.\n this.submittedToolCallIds.add(callId);\n }\n }\n }\n\n /**\n * Synchronous replay of a single stored wire event — the side-effect +\n * translate + emit core of ``dispatchWireEvent`` without the (live-only)\n * client-tool round-trip. Called in a tight synchronous loop during history\n * restore so the consumer's per-event store writes batch into ONE render\n * instead of re-typing the whole conversation event by event.\n */\n private replayWireEvent(wire: WireEvent, conversationId: string): void {\n // `live: false` — a replay must never touch the streaming lifecycle. It\n // cannot be inferred from `promptMessageId` being empty, because\n // `reconnectToJob` passes empty too and IS live.\n this.applyWireSideEffects(wire, conversationId, \"\", false);\n const event = translateWireEvent(wire);\n if (event) {\n this.emit(event);\n }\n }\n\n /**\n * State mutations driven by wire events. Kept separate from translation so\n * the pure wire → ChatEvent mapping can live in translate.ts and be reused\n * by the replay path.\n *\n * ``promptMessageId`` is the id of the USER turn that started this job —\n * `POST /v1/jobs` returns it and the backend tags the prompt with it\n * (`HumanMessage(content=..., id=message_id)`), which is what lets a restore\n * pair a job with its prompt by id instead of by position. It was previously\n * documented here as the ASSISTANT's id and used as one; there is no\n * server-assigned assistant id on the wire, so that row gets a local one.\n * Empty in the reconnect and conversation-switch replay paths, where the\n * messages have already been loaded from REST and must not be re-pushed —\n * so it doubles as the \"is this a live send?\" gate.\n */\n private applyWireSideEffects(\n wire: WireEvent,\n conversationId: string,\n promptMessageId: string,\n live: boolean,\n ): void {\n // `accumulatedText`, `currentTextPath`, `isStreaming` and `currentJobId`\n // are ONE turn's state, and a restore replaying completed turns over a\n // running one must not touch any of it. `send` bails only on the manager's\n // `streaming`, so it runs during `restoring` and nothing bumps the\n // generation — the replay loop's `superseded()` therefore stays false and\n // the two overlap. A live event always owns this state; a replayed one\n // only when nothing is running.\n const ownsTurnState = live || !this.isStreaming;\n\n switch (wire.type) {\n case \"message_start\":\n // Reset per-turn accumulator so multi-turn replay doesn't concatenate\n // text from prior turns into the next assistant message.\n if (ownsTurnState) this.resetStreamingState();\n if (wire.model) {\n this.modelDisplayName = wire.model;\n }\n return;\n\n case \"block_start\":\n // Track the currently open top-level text block so we can accumulate\n // its content for the assistant Message record.\n if (\n ownsTurnState &&\n wire.kind === \"text\" &&\n (!wire.parent_path || wire.parent_path.length === 0)\n ) {\n this.currentTextPath = wire.path;\n }\n return;\n\n case \"block_delta\":\n if (\n ownsTurnState &&\n wire.delta.channel === \"text\" &&\n this.currentTextPath !== null &&\n pathEquals(this.currentTextPath, wire.path)\n ) {\n this.accumulatedText += wire.delta.text;\n }\n return;\n\n case \"block_stop\":\n if (\n ownsTurnState &&\n this.currentTextPath !== null &&\n pathEquals(this.currentTextPath, wire.path)\n ) {\n this.currentTextPath = null;\n }\n return;\n\n case \"message_stop\":\n // The turn ran to completion, so the backend has persisted its prompt.\n // This — not the job response — is the proof `loadConversation` needs\n // before it may read \"absent from the snapshot\" as \"the server does\n // not have it\".\n if (promptMessageId && this.pendingUserMessages.has(promptMessageId)) {\n this.pendingUserMessages.set(promptMessageId, ++this.serverRowsKnown);\n }\n // Only record the assistant message on a live send — a non-empty\n // prompt id IS that signal. Reconnect/replay paths load messages via\n // REST instead.\n if (promptMessageId) {\n const assistantMessage: Message = {\n // NOT `promptMessageId` — that is the USER turn's id, which\n // `consumeJobStream` stamps onto the user row. Sharing it puts two\n // rows under one id, and `pendingUserMessages` is id-keyed. No\n // server-assigned assistant id exists on the wire, so this row is\n // local until a REST load replaces it.\n id: generateId(),\n conversationId,\n role: \"assistant\",\n content: this.accumulatedText,\n status: \"complete\",\n createdAt: new Date().toISOString(),\n };\n this.messages.push(assistantMessage);\n this.storage\n .addMessage(assistantMessage, conversationId)\n .catch(() => {});\n }\n // Only the LIVE stream owns these. A restore replaying a completed\n // turn over a running one used to clear both: `currentJobId` null\n // means the job can no longer be cancelled by `stop()` or parked by\n // `detachStreamingTurn`, and `isStreaming` false means the next send\n // passes its own guard and opens a second stream sharing this one's\n // `abortController` and `lastSeq`.\n if (ownsTurnState) {\n this.isStreaming = false;\n this.currentJobId = null;\n }\n return;\n\n case \"custom\":\n if (wire.name === \"title_generated\") {\n const title = (wire.data.title as string) ?? \"\";\n if (this.conversationId && title) {\n const conv = this.conversations.find(\n (c) => c.id === this.conversationId,\n );\n if (conv) {\n conv.title = title;\n }\n this.storage\n .updateConversationTitle(this.conversationId, title)\n .catch(() => {});\n }\n }\n return;\n\n default:\n return;\n }\n }\n\n private async executeClientTools(\n toolCalls: ToolCallRequest[],\n ): Promise<ToolResult[]> {\n const results: ToolResult[] = [];\n for (const call of toolCalls) {\n const result = await this.toolRegistry.executeTool(call);\n results.push(result);\n }\n return results;\n }\n\n /**\n * Load conversation context (messages) without replaying events.\n * Used before reconnectToJob — SSE replay handles event replay.\n */\n async loadConversation(id: string): Promise<void> {\n // Claimed BEFORE the await, so the check below is \"am I still the newest\n // load?\" rather than \"does the session still point where I left it?\".\n const load = ++this.loadGeneration;\n this.conversationId = id;\n // NOT while a turn is live. `resetStreamingState` nulls `currentTextPath`,\n // and the live turn's `block_start` has already gone by — so every later\n // `block_delta` fails the path check, accumulates nothing, and\n // `message_stop` persists an EMPTY assistant row. Only reachable when a\n // send lands in a switch's probe window (the normal switch path detaches\n // first, which clears `isStreaming`), which is exactly the window this\n // change is about.\n if (!this.isStreaming) this.resetStreamingState();\n // Read BEFORE the fetch goes out: the server evaluates it later still, so\n // any row already proven committed at this point has to be in the reply.\n const rowsKnownAtIssue = this.serverRowsKnown;\n const messages = await this.client\n .getMessages(id)\n .catch(() => this.storage.fetchMessages(id));\n // Nothing serializes callers, and this fetch is not instant. Install these\n // unconditionally and the session holds ONE conversation's id beside\n // ANOTHER's messages — the pair `send` (which posts to `conversationId`)\n // and `regenerate` (which resends `messages`' last user turn) read\n // together.\n //\n // A monotonic token rather than `this.conversationId !== id`, because that\n // comparison is ABA-blind: on A -> B -> A with A's FIRST fetch slow, the id\n // is back to A by the time that fetch lands, so the check passes and it\n // clobbers the fresh messages the second A load already installed. The id\n // says where the session is, not which load last spoke.\n if (load !== this.loadGeneration) return;\n // The reply is a SNAPSHOT of the server's state. A user message the server\n // has not persisted yet is not in it, and assigning straight over\n // `this.messages` drops it with nothing to restore it — the SSE handler\n // only ever appends the assistant's reply, never the user turn again. So\n // the send is posted correctly and then vanishes, leaving `regenerate` to\n // pick the PREVIOUS turn as the last user message.\n //\n // Matched on ID, which `consumeJobStream` reconciles with the server's as\n // soon as the job response lands — so this is exact, not a heuristic over\n // role/content/arrival order.\n const pending = this.messages.filter(\n (m) => this.pendingUserMessages.has(m.id) && m.conversationId === id,\n );\n const stillPending = pending.filter((m) => {\n if (messages.some((f) => f.id === m.id)) return false;\n const knownAt = this.pendingUserMessages.get(m.id) ?? 0;\n // Absent from the snapshot has two causes needing opposite handling.\n // 0 means the turn has not completed, so the row may not be committed —\n // keep it, that is the race this set exists for. Otherwise the turn\n // finished before this fetch was issued, so the snapshot HAD to contain\n // it; missing means the server does not have it, and re-appending would\n // resurrect it at the tail on every later load.\n return knownAt === 0 || knownAt > rowsKnownAtIssue;\n });\n for (const m of pending) {\n if (!stillPending.includes(m)) this.pendingUserMessages.delete(m.id);\n }\n this.setMessages(\n stillPending.length ? [...messages, ...stillPending] : messages,\n );\n this.messagesConversationId = id;\n }\n\n /**\n * Reconnect to a running job's SSE stream (e.g. after page reload).\n * Replays all events from the beginning and continues live.\n */\n async reconnectToJob(jobId: string): Promise<void> {\n if (this.isStreaming) return;\n\n this.isStreaming = true;\n this.currentJobId = jobId;\n this.lastSeq = -1;\n this.submittedToolCallIds.clear();\n this.resetStreamingState();\n const controller = new AbortController();\n this.abortController = controller;\n\n try {\n await this.consumeEventStream(\n jobId,\n this.conversationId ?? \"\",\n \"\",\n false, // don't execute client tools on reconnect\n );\n } catch (err) {\n this.emit({\n type: \"error\",\n code: \"connection_error\",\n message: err instanceof Error ? err.message : String(err),\n blockPath: null,\n });\n } finally {\n // Only if this invocation still OWNS the turn. `detach()` cannot\n // interrupt a client tool — `executeClientTools` awaits arbitrary\n // consumer code with no signal — so this `finally` can run long after a\n // switch handed the session to a newer turn. Clearing unconditionally\n // then nulls THAT turn's flag and controller: its stream keeps pumping\n // while the session reports idle, `stop()` aborts nothing, and the next\n // switch neither parks its job nor stops it emitting under the new\n // conversation's id. The mirror of `ownsTurnState`, on the live side.\n if (this.abortController === controller) {\n this.isStreaming = false;\n this.abortController = null;\n }\n }\n }\n\n /** Detach from the SSE stream without cancelling the job. */\n detach(): void {\n this.abortController?.abort();\n this.abortController = null;\n this.isStreaming = false;\n this.resetStreamingState();\n this.emit({ type: \"disconnected\" });\n }\n\n /**\n * Cancel the running turn: stop the job server-side and tear the stream\n * down, WITHOUT ending the session. A turn-level cancel is not a\n * session-level teardown, so unlike `disconnect()` this leaves the protocol\n * registry alone — the SDK never auto-registers adapters, so clearing them\n * on a Stop press would silently kill embedded-resource rendering for the\n * rest of the session with nothing to re-register it.\n */\n cancelTurn(): void {\n if (this.currentJobId) {\n this.client.cancelJob(this.currentJobId).catch(() => {});\n }\n this.detach();\n // `detach()` does not do this, and a cancelled job's id must not linger —\n // a later `stop()` would re-issue `cancelJob` against it.\n this.currentJobId = null;\n // A cancelled turn never reaches `message_stop`, so nothing can ever prove\n // its prompt committed, and an entry left at `knownAt === 0` means every\n // later load re-appends the row — forever, since `setMessages`'s prune\n // only drops rows that LEFT the list. The server is authoritative from\n // here: if it holds the prompt a load returns it, and if it does not (the\n // loop runs as a background task, so a fast cancel can beat the write)\n // the row correctly disappears. NOT done in `detach()`, where the job\n // keeps running and will persist.\n for (const [id, knownAt] of this.pendingUserMessages) {\n if (knownAt === 0) this.pendingUserMessages.delete(id);\n }\n }\n\n /** Stop the job and end the session's activity (explicit user action). */\n disconnect(): void {\n this.cancelTurn();\n // Drop all protocol adapters — lifecycle tied to the session.\n this.protocols.clear();\n }\n\n /**\n * A pointer move happened above this layer, so any `loadConversation` in\n * flight must lose to it. `StreamManager` owns a pointer of its own and\n * moves it before this one; without this the two halves would gate on\n * counters that bump at different instants — `generation` synchronously in\n * `setActiveConversation`, `loadGeneration` only once the switch's own load\n * actually runs, which is behind the active-job probe.\n */\n invalidateLoadsInFlight(): void {\n this.loadGeneration++;\n }\n\n async createNewConversation(): Promise<string> {\n const id = generateId();\n // Witness captured before the await, like `deleteConversation`'s re-test of\n // its own pointer. `storage.createConversation` is a real round-trip for\n // any non-memory `ChatStorage`, and anything that moves the pointer during\n // it — a switch, a relocating send — bumps this.\n const load = this.loadGeneration;\n const conversation = await this.storage.createConversation(\n id,\n \"New Conversation\",\n );\n this.conversations.unshift(conversation);\n // Created and in the list either way; only the RELOCATION is conditional.\n // Without this the manager could decline its own pointer move while the\n // session had already taken this one — manager on B, session on the new\n // id with an empty list, which is sticky: `regenerate` gates on that\n // pairing and `switchTo` early-returns on B, so re-clicking B does\n // nothing and the user has to visit a third conversation and come back.\n if (load !== this.loadGeneration) return id;\n // A pointer move — same rule as `send`'s relocation. Invisible from\n // `restore`, which bails at its next `superseded()` check, while the\n // session already holds the mismatched pairing.\n this.loadGeneration++;\n this.conversationId = id;\n this.setMessages([]);\n // The empty list IS this conversation's list — say so, or every consumer\n // of the pairing (regenerate) stays blocked on the previous conversation.\n this.messagesConversationId = id;\n return id;\n }\n\n /**\n * Replay one completed turn's already-fetched events, synchronously.\n *\n * Fetching is the caller's job (``StreamManager.restore`` loads every turn's\n * events in parallel and the message list once), so this is pure replay: no\n * awaits, so the whole restore runs in a single synchronous pass and the\n * consumer batches it into one render.\n *\n * ``userMessageContent`` is the prompt that triggered this turn. It's emitted\n * as a synthetic ``user_message`` BEFORE any of the turn's events: user\n * prompts aren't persisted in ``job_events``, and some events precede\n * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),\n * so leading with the prompt keeps the turn in order.\n */\n replayTurn(\n id: string,\n events: ConversationEvent[],\n userMessageContent?: string,\n userMessageId?: string,\n isSteer = false,\n ): void {\n this.conversationId = id;\n // Same rule as `loadConversation`: not while a turn is live. Nulling\n // `currentTextPath` after the live turn's `block_start` has gone by makes\n // every later delta accumulate nothing, and its `message_stop` then\n // persists the REPLAYED turn's text as the assistant row.\n if (!this.isStreaming) this.resetStreamingState();\n\n if (userMessageContent) {\n this.emit({\n type: \"user_message\",\n content: userMessageContent,\n ...(userMessageId ? { id: userMessageId } : {}),\n ...(isSteer ? { steer: true } : {}),\n });\n }\n\n // The data payload is authoritative for `type` (matching how\n // replay.ts#mapSseToChat reads it), with the SSE event name as a fallback\n // for pre-v2 rows.\n for (const ev of events) {\n const type = (ev.data.type as string) || ev.event;\n if (!type || type === \"done\") continue;\n\n const wire = { ...ev.data, type } as unknown as WireEvent;\n try {\n this.replayWireEvent(wire, id);\n } catch {\n // Skip malformed replay events\n }\n }\n }\n\n /**\n * Load a conversation's messages and replay its persisted history.\n *\n * Convenience for plain-``ChatSession`` consumers (the documented\n * conversation-management API). ``StreamManager`` drives restore itself —\n * loading messages once and replaying each turn in parallel — and does NOT\n * call this; it's kept so direct-Session usage doesn't break.\n *\n * Without ``jobId`` it replays the whole conversation; with one, just that\n * job's events.\n */\n async switchConversation(id: string, jobId?: string): Promise<void> {\n // Load the messages through `loadConversation` rather than fetching and\n // assigning them here: one implementation of the token check and the\n // pending merge, so the two cannot drift apart. Still parallel — the two\n // fetches start together.\n // `loadConversation` claims its token synchronously, before its first\n // await, so reading `loadGeneration` straight after the call gives the\n // token THIS switch is operating under.\n const loading = this.loadConversation(id);\n const token = this.loadGeneration;\n const [loadResult, eventsResult] = await Promise.allSettled([\n loading,\n this.client.getConversationEvents(id, jobId),\n ]);\n // Guarding the messages alone was not enough — and left this in a worse\n // state than before. `replayTurn` opens by assigning `this.conversationId`\n // and then emits the whole turn, so a superseded call still did both of\n // the things this guard exists to stop: re-pointed the session at the\n // conversation the caller left, and poured its events out of the stream.\n // With the messages now correctly dropped, the id moved back alone —\n // leaving one conversation's messages under another's id, precisely the\n // pairing `loadConversation` documents itself as eliminating. Previously\n // both moved together: still wrong, but at least coherent.\n // Checked against the token, not against whether the load itself was\n // superseded: on a plain A -> B the load for A COMPLETES before B starts,\n // so it reports success, and only the events fetch is still open when B\n // supersedes. The token catches both — the load losing mid-flight, and\n // this whole call losing after it.\n if (token !== this.loadGeneration) return;\n // Ordered AFTER the token check, because a load can be both rejected AND\n // superseded: blanking then wipes the NEWER conversation's freshly\n // installed list and stamps it with this conversation's id — the same\n // mismatch this whole guard exists to prevent, arriving through the\n // failure path. A rejected load has already moved the pointer, so when\n // this call does still own the session the list must not be left behind.\n // Only reachable through a custom `ChatStorage` whose fallback throws\n // (`InMemoryStorage` never does), but `ChatStorage` is a public interface.\n if (loadResult.status === \"rejected\") {\n this.setMessages([]);\n this.messagesConversationId = id;\n }\n this.replayTurn(\n id,\n eventsResult.status === \"fulfilled\" ? eventsResult.value : [],\n );\n }\n\n /**\n * Append the next page of conversation history to ``conversations``.\n *\n * The list is ordered ``updated_at DESC`` and paged by offset, so a\n * conversation bumped to the top mid-scroll can surface again in a later\n * page; ids already held are dropped rather than duplicated. Returns only\n * the conversations actually appended, which may be empty even on a full\n * page. Rejects on network failure with ``hasMoreConversations`` still true,\n * so the caller can retry.\n *\n * KNOWN LIMITATION — offset paging is only stable while the prefix already\n * consumed stays put. The offset tracking here corrects for perturbations\n * THIS session causes (local unshifts, ``deleteConversation``), but not for\n * ones it never sees:\n *\n * - a conversation this session hasn't loaded yet is bumped to the top (a\n * headless routine or another device posting to it), pushing the whole\n * list down — it lands inside the consumed prefix, which no later offset\n * revisits;\n * - a conversation is deleted from another tab/device, shrinking the list so\n * the next offset lands one row too far in.\n *\n * Each perturbation costs at most one conversation off the sidebar, and only\n * until the next ``connect()`` — that re-seeds page 1 and resets the paging\n * state, so a reload or reconnect always recovers it. Nothing is lost\n * server-side. Both cases are pinned by tests in\n * ``tests/conversation-paging.test.ts``.\n *\n * Closing the gap properly needs a stable server cursor (keyset paging on\n * ``(updated_at, id)``) rather than a raw offset, which is a backend change —\n * tracking ids client-side cannot discover a row that moved into a region\n * already scanned.\n */\n async loadMoreConversations(): Promise<Conversation[]> {\n // Scroll handlers fire far faster than the request completes; without this\n // guard every frame would refetch the same offset.\n if (this.isLoadingConversations || !this.hasMoreConversations) return [];\n this.isLoadingConversations = true;\n const generation = this.conversationsGeneration;\n try {\n const page = await this.client.getConversations(\n CONVERSATION_PAGE_SIZE,\n this.serverConversationIds.size,\n );\n // A reconnect re-seeded the list while this was in flight, so this page\n // describes a paging state that no longer exists. Drop it untouched —\n // connect() has already set conversations/hasMore for the new state, and\n // the caller's next call pages from there.\n if (generation !== this.conversationsGeneration) return [];\n this.hasMoreConversations = page.length === CONVERSATION_PAGE_SIZE;\n const fresh = page.filter((c) => !this.serverConversationIds.has(c.id));\n for (const c of page) this.serverConversationIds.add(c.id);\n // A locally-created conversation can already sit in the array from its\n // unshift; count it toward the offset (the server did return it) but\n // don't append a second copy.\n const known = new Set(this.conversations.map((c) => c.id));\n const appended = fresh.filter((c) => !known.has(c.id));\n this.conversations.push(...appended);\n return appended;\n } finally {\n this.isLoadingConversations = false;\n }\n }\n\n /**\n * Rename a conversation, server first.\n *\n * Server first, like the delete below: a failed rename written locally would\n * leave the sidebar showing a title the server never accepted, and nothing\n * refetches a conversation that is already in the loaded list. (The delete\n * used to be the counter-example here — it dropped the row whatever the\n * server said, on the theory that a failed one was self-correcting. It was\n * not: the row stayed deleted locally and alive on the server.)\n *\n * Mirrors the `title_generated` path: the entry in `conversations` is\n * mutated in place, which is what every consumer of the list reads.\n */\n async renameConversation(id: string, title: string): Promise<void> {\n const updated = await this.client.renameConversation(id, title);\n const conv = this.conversations.find((c) => c.id === id);\n if (conv) {\n // The server's title, not the caller's — it trims before storing.\n conv.title = updated.title;\n }\n await this.storage.updateConversationTitle(id, updated.title);\n }\n\n async deleteConversation(id: string): Promise<void> {\n try {\n await this.client.deleteConversation(id);\n } catch (err) {\n // 404 only. The backend 404s a conversation that is already gone, and\n // that IS this delete succeeding — drop it locally and carry on.\n //\n // Everything else means the conversation still exists: 403 (not yours),\n // 401, 429, 5xx, or the request never landing at all. This used to\n // swallow all of them and delete locally anyway, so a failed delete was\n // indistinguishable from a successful one — the row vanished from the\n // sidebar, survived on the server, and came back on the next device or\n // the next reload. Rethrowing BEFORE the local delete is what makes the\n // two outcomes tellable apart; the caller decides what to show.\n if (!(err instanceof ServerError) || err.status !== 404) throw err;\n }\n await this.storage.deleteConversation(id);\n // Shrinks the paging offset iff the server had handed us this one — every\n // later page now shifts up by one, and without this the next page would\n // skip a conversation. A purely local conversation isn't in the set, so\n // deleting it correctly leaves the offset alone.\n //\n // ``Set.delete`` reports whether it was there, which is exactly the\n // \"was this server-sourced?\" test. When it was, any page ALREADY in flight\n // is now stale for the same reason as a reconnect: its offset was computed\n // pre-delete but the server evaluates the query post-delete, so it starts\n // one row late and would skip that row for good. The adjustment above fixes\n // future requests and cannot rescue an outstanding one — so invalidate it.\n if (this.serverConversationIds.delete(id)) {\n this.conversationsGeneration++;\n }\n this.conversations = this.conversations.filter((c) => c.id !== id);\n if (this.conversationId === id) {\n // Same pointer-move rule as `createNewConversation`. Worse here if\n // skipped: `session.messages` is public, so the reinstalled list is the\n // DELETED conversation's history rendered under a null id.\n this.loadGeneration++;\n this.conversationId = null;\n this.setMessages([]);\n this.messagesConversationId = null;\n }\n }\n\n toggleClientTool(name: string): boolean {\n if (this.enabledClientTools.has(name)) {\n this.enabledClientTools.delete(name);\n return false;\n }\n this.enabledClientTools.add(name);\n return true;\n }\n}\n","/**\n * Deciding which prompt started which turn, when restoring a conversation.\n *\n * Restore has to pair each completed job with the user message that triggered\n * it. It used to do that by POSITION — the N-th completed job to the N-th user\n * message — which is only sound while every user message starts exactly one\n * job. A mid-run steer (`POST /conversations/{id}/steer`) is a user message\n * that starts none, so it shifted every later turn onto the wrong prompt and\n * pushed the tail off the end of the loop entirely.\n *\n * The backend now stamps a turn's prompt with the same id the job records\n * (`job.message_id`), so the pairing can be exact. Four kinds of row show up in\n * the two lists, and the id is what tells them apart:\n *\n * | row | has message id | job links to it |\n * |----------------------|----------------|-----------------|\n * | current turn prompt | yes | yes |\n * | mid-run steer | yes | no |\n * | legacy prompt | no | n/a |\n * | goal-continuation | hidden from the message list entirely |\n *\n * Classification keys off whether a job's `message_id` actually MATCHES a\n * message — never off a field being present. Both are always populated in real\n * data: `jobs.message_id` is NOT NULL, and the messages endpoint substitutes a\n * positional index string when a row carries no id of its own. A presence check\n * therefore reads every pre-link conversation as linked and strips every prompt\n * bubble from it.\n *\n * So: a job whose id matches a message is a turn; a message no job claims is a\n * steer; a job past the cutover whose id matches nothing visible is a\n * continuation, replaying with no bubble (what the positional version did by\n * accident when it ran off the end of the message list).\n *\n * Jobs are the spine, so continuations keep their place in the transcript.\n * Steers are interleaved at the point they appear in the message list, which\n * puts them after the turn that was running when they were sent.\n *\n * A conversation can also be reopened while a turn is STILL RUNNING, and that\n * turn's prompt is paired here too (`runningJob`) even though its events are\n * not replayed from storage — they arrive on the live stream the caller\n * reconnects to. Only the pairing is special-cased; the walk treats it as an\n * ordinary turn.\n *\n * Jobs are the spine, which means a conversation can also have NO spine: every\n * job failed or was cancelled, or the only one is still running and the\n * active-job probe did not resolve it. The walk is then empty, and the prompts\n * are carried entirely by the message list — so they are emitted on their own\n * rather than dropped with the jobs that would have anchored them.\n *\n * A conversation predating the link has no `message_id` on any job; there is\n * nothing to pair with, and no backfill is possible — inferring which historic\n * prompt started which job is exactly the ambiguity the link removes, so\n * guessing would bake the bug into the data. Those fall back to the positional\n * walk, unchanged, and keep the old behaviour including its flaw.\n */\n\nexport interface RestoreJob {\n job_id: string;\n /** The prompt that started this turn. Absent on pre-link rows and on\n * goal-continuation jobs, whose seed is hidden from the message list. */\n message_id?: string | null;\n}\n\nexport interface RestoreMessage {\n /** Present once the backend tags prompts; absent on pre-link rows. */\n id?: string;\n content: string;\n}\n\nexport type ReplayStep =\n /** Replay a job's events, optionally preceded by the prompt bubble. */\n | { kind: \"turn\"; jobId: string; content?: string; messageId?: string }\n /** A user message that started no turn — render the bubble alone. */\n | { kind: \"steer\"; content: string; messageId?: string };\n\n/**\n * Order the replay: which jobs to play, with which prompts, and where the\n * steers go between them. Pure, so the ordering rules are testable without a\n * session, a network, or a fake event stream.\n */\nexport function planRestore(args: {\n completedJobs: RestoreJob[];\n /**\n * The turn that is still RUNNING, if one is. It joins the walk as an\n * ordinary turn so it is paired with its prompt by the same rules as any\n * other — the caller simply holds no events for it, because those are the\n * live stream it reconnects to, so the step renders the bubble alone.\n *\n * Without it a live turn's prompt is paired with nothing while still being\n * `claimed` (below), so it is neither a turn nor a steer and vanishes from\n * the restore entirely — leaving a conversation reopened mid-turn showing\n * the running turn's blocks under no prompt at all.\n */\n runningJob?: RestoreJob;\n /**\n * Message ids claimed by ANY job, not just completed ones. A steer is a\n * prompt no job started, so testing against completed jobs alone reads a\n * prompt whose turn is still RUNNING as a steer — and replays it as a\n * second, `steer`-flagged bubble on top of the one the live send already\n * rendered. Optional so callers that only have the completed set keep the\n * old behaviour.\n *\n * Ignored when the walk is EMPTY — see the branch below. Claiming prevents a\n * second copy of a prompt, and a walk with no turns in it emits no first\n * copy, so honouring the set there would drop a running turn's prompt rather\n * than de-duplicate it.\n */\n claimedMessageIds?: (string | null | undefined)[];\n userMessages: RestoreMessage[];\n}): ReplayStep[] {\n const { completedJobs, runningJob, userMessages } = args;\n // The running turn sits after every completed one, which is where it belongs\n // both chronologically and positionally: on a pre-link conversation the\n // fallback walk pairs it with `userMessages[completedJobs.length]`, the\n // prompt that follows the last completed turn's.\n const jobs = runningJob ? [...completedJobs, runningJob] : completedJobs;\n const claimed = new Set(\n (args.claimedMessageIds ?? jobs.map((j) => j.message_id)).filter(\n (id): id is string => !!id,\n ),\n );\n\n const byId = new Map<string, number>();\n userMessages.forEach((m, i) => {\n if (m.id) byId.set(m.id, i);\n });\n /** The message index this job's prompt lives at, if it is visible. */\n const linkOf = (j: RestoreJob): number | undefined =>\n j.message_id ? byId.get(j.message_id) : undefined;\n\n // `slice`, not `jobs`: the caller hands this a PORTION of the walk (the\n // pre-cutover head, or the whole list), and reusing the outer name for\n // sometimes-the-same list made `jobs` mean two things in one function.\n const positional = (\n slice: RestoreJob[],\n msgs: RestoreMessage[],\n ): ReplayStep[] =>\n slice.map((job, i) => ({\n kind: \"turn\" as const,\n jobId: job.job_id,\n content: msgs[i]?.content,\n messageId: msgs[i]?.id,\n }));\n\n // No turn to walk AT ALL, which is not the same as a conversation with no\n // history. Two ways to get here, and they are the whole reason this branch\n // exists:\n //\n // - every job FAILED or was CANCELLED — neither is `completed`, so neither\n // reaches `completedJobs`;\n // - the only job is still RUNNING and the active-job probe did not resolve\n // it, so the caller passes no `runningJob` (the Redis liveness key can\n // expire while the row is still `in_progress`).\n //\n // `positional` maps over JOBS, so it returns [] for an empty walk and the\n // whole transcript renders empty — the prompt the user actually sent\n // disappears, leaving the turn's blocks under nothing at all.\n //\n // `claimed` is deliberately IGNORED here, and that is what separates the two\n // cases above. Claiming exists to stop a prompt being drawn twice: once by\n // the turn that anchors it and once as a steer. An empty walk emits no turn\n // at all, so nothing can anchor anything and there is no second copy to\n // avoid — while a running job IS claimed (the caller claims every job it\n // replays, and it replays every job but the one arriving live), so filtering\n // on it here would drop exactly the running-but-unresolved prompt this branch\n // has to rescue.\n //\n // Nor does that re-open the double-bubble the claim set guards: the caller\n // only replays after announcing `restoring`, i.e. after telling the consumer\n // to clear, and bails on `turnStarted` / `viewTakenOverByLiveTurn` when a\n // send owns the view instead. So reaching here means the view is empty.\n //\n // Checked before `firstLinked`, because an empty list has no linked job by\n // definition and would otherwise fall into the pre-link branch below and\n // return [] from there.\n if (jobs.length === 0) {\n return userMessages.map((m) => ({\n kind: \"steer\" as const,\n content: m.content,\n messageId: m.id,\n }));\n }\n\n // Detection keys off whether a job's id actually MATCHES a message — never\n // off a field merely being present. Both fields are always populated in real\n // data (`jobs.message_id` is NOT NULL, and the messages endpoint substitutes\n // a positional index string when a row has no id of its own), so a\n // presence check silently classifies every pre-link conversation as linked\n // and strips every prompt bubble from it.\n const firstLinked = jobs.findIndex((j) => linkOf(j) !== undefined);\n if (firstLinked === -1) {\n // Nothing matches: the whole conversation predates the tagging.\n return positional(jobs, userMessages);\n }\n\n // The tagging started at a point in time, so a conversation spanning it\n // splits cleanly: everything before the first linked turn has no usable\n // link and falls back to position; everything after is exact.\n const cutover = linkOf(jobs[firstLinked]!)!;\n const steps: ReplayStep[] = positional(\n jobs.slice(0, firstLinked),\n userMessages.slice(0, cutover),\n );\n\n let cursor = cutover;\n const isSteer = (m: RestoreMessage | undefined): m is RestoreMessage =>\n !!m?.id && !claimed.has(m.id);\n\n /** Emit every steer sitting before `stopAt`, and advance past it. */\n const drainTo = (stopAt: number) => {\n while (cursor < stopAt) {\n const m = userMessages[cursor++];\n if (isSteer(m)) {\n steps.push({ kind: \"steer\", content: m.content, messageId: m.id });\n }\n }\n cursor = stopAt + 1;\n };\n\n for (const job of jobs.slice(firstLinked)) {\n const at = linkOf(job);\n if (at !== undefined) {\n drainTo(at);\n const prompt = userMessages[at]!;\n steps.push({\n kind: \"turn\",\n jobId: job.job_id,\n content: prompt.content,\n messageId: prompt.id,\n });\n continue;\n }\n // Past the cutover, a job whose id matches nothing visible is a goal\n // continuation: its seed is deliberately hidden from the message list, so\n // it replays its events with no bubble — which is what the positional\n // version did by accident when it ran off the end of the messages.\n steps.push({ kind: \"turn\", jobId: job.job_id });\n }\n\n // Steers sent during the final turn sit past every prompt.\n for (let i = cursor; i < userMessages.length; i++) {\n const m = userMessages[i];\n if (isSteer(m)) {\n steps.push({ kind: \"steer\", content: m.content, messageId: m.id });\n }\n }\n\n return steps;\n}\n","/**\n * StreamManager — high-level conversation lifecycle coordinator.\n *\n * Sits on top of ChatSession and manages the state machine for\n * multi-conversation SSE streaming. Framework-agnostic: emits\n * typed events to registered handlers. Block construction is NOT\n * the SDK's concern — consumers build their own block tree from\n * the forwarded ``ChatEvent`` instances.\n *\n * import { ChatSession, StreamManager } from \"@astralform/js\";\n * const session = new ChatSession({ ... });\n * const manager = new StreamManager(session);\n * manager.on((event) => {\n * if (event.type === \"event\") {\n * // event.event is a typed ChatEvent — dispatch to your reducer\n * }\n * });\n * await manager.send(\"Hello\");\n */\nimport { planRestore } from \"./restore-plan\";\n\nimport type { ChatEvent, ModelChoiceOptions } from \"./types.js\";\nimport { ChatEventType } from \"./types.js\";\nimport type { ChatSession } from \"./session.js\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type StreamState = \"idle\" | \"streaming\" | \"restoring\" | \"detached\";\n\nexport interface SendOptions extends ModelChoiceOptions {\n agentName?: string;\n uploadIds?: string[];\n planMode?: boolean;\n /**\n * Attach the image-generation tool to this turn. Per-message and off by\n * default — generating costs the developer real money at a third-party\n * provider. Gate the affordance on `AgentStatus.capabilities`.\n */\n imageMode?: boolean;\n /**\n * Attach the video-generation tool to this turn. Mutually exclusive with\n * `imageMode` at the composer level. See `SendOptions.videoMode` in types.ts\n * for the full rule — a clip animates an existing image, is silent, and holds\n * one shared GPU for minutes, so it is per-message and off by default.\n */\n videoMode?: boolean;\n /**\n * Start a durable long-horizon goal for this run (goal mode) — the text is the\n * goal objective the backend drives to completion. Omit for a normal turn.\n */\n goal?: string;\n /**\n * The project this task belongs to (`owner/repo`), when it has one.\n *\n * Write-once server-side, and the refusal is the part that matters: the FIRST\n * turn that names a repository binds the task, a later turn may omit it or\n * repeat the same value, and a DIFFERENT value is refused (409) rather than\n * ignored — silently acting on the wrong repository is the failure that rule\n * exists to prevent. Omit it entirely and the task is an ordinary chat: since\n * Astralform 0.69.50 there is no agent mode that requires one, so a first turn\n * without it is no longer a 400. Astralform >= 0.69.46.\n */\n repository?: string;\n}\n\nexport type StreamManagerEvent =\n | { type: \"stateChange\"; state: StreamState; conversationId: string | null }\n | { type: \"conversationChanged\"; conversationId: string | null }\n | {\n type: \"backgroundJobsChanged\";\n jobs: ReadonlyMap<string, string>;\n }\n | { type: \"event\"; conversationId: string | null; event: ChatEvent }\n | { type: \"versionsReady\"; conversationId: string; count: number }\n | {\n /**\n * A history replay ran to the end — emitted for EVERY restored\n * conversation, whatever its jobs' statuses. This is the signal to\n * rehydrate per-turn state (attachment chips, composer modes, goal\n * runs) from the jobs endpoint. It exists separately from\n * ``versionsReady`` because that one is completed-only by contract\n * (a version is an answer to switch to), and a conversation whose\n * only turns were stopped or failed still needs this pass — gating\n * rehydration on ``versionsReady`` left such conversations\n * permanently chip-less.\n */\n type: \"restoreSettled\";\n conversationId: string;\n };\n\ntype EventHandler = (event: StreamManagerEvent) => void;\n\n/** One turn as the conversation's job list describes it. */\ninterface RestoreJob {\n job_id: string;\n status: string;\n message_id?: string | null;\n metrics?: Record<string, unknown>;\n}\n\n// =============================================================================\n// StreamManager\n// =============================================================================\n\nexport class StreamManager {\n private session: ChatSession;\n private _state: StreamState = \"idle\";\n private _activeConversationId: string | null = null;\n private _backgroundJobs = new Map<string, string>();\n private handlers: EventHandler[] = [];\n private unsub: (() => void) | null = null;\n /**\n * Bumped every time the active conversation moves. An async sequence that\n * captures it can then tell, at each await boundary, whether it is still the\n * one the user is waiting on — see ``restore``.\n */\n private generation = 0;\n /**\n * Bumped every time a turn STARTS. `generation` does not move for a send\n * (only a pointer move does), and the streaming state returns to idle when a\n * turn ends — so neither can tell a restore that a turn ran inside one of\n * its awaits. This can.\n */\n private turnCounter = 0;\n /**\n * True while a `resync` is between its probe and its restore. Visibility and\n * focus listeners can both fire for one return, and two overlapping resyncs\n * would each detach the other's stream mid-flight — the second call must\n * find the flag set and leave the first to converge.\n */\n private _resyncing = false;\n\n constructor(session: ChatSession) {\n this.session = session;\n this.attach();\n }\n\n // ── Public state ──────────────────────────────────────────────\n\n get state(): StreamState {\n return this._state;\n }\n\n get activeConversationId(): string | null {\n return this._activeConversationId;\n }\n\n get backgroundJobs(): ReadonlyMap<string, string> {\n return this._backgroundJobs;\n }\n\n // ── Event subscription ────────────────────────────────────────\n\n on(handler: EventHandler): () => void {\n this.handlers.push(handler);\n return () => {\n this.handlers = this.handlers.filter((h) => h !== handler);\n };\n }\n\n private emit(event: StreamManagerEvent): void {\n for (const handler of this.handlers) {\n try {\n handler(event);\n } catch {\n // Don't let handler errors crash the manager\n }\n }\n }\n\n private setState(state: StreamState): void {\n this._state = state;\n this.emit({\n type: \"stateChange\",\n state,\n conversationId: this._activeConversationId,\n });\n }\n\n // ── Session event wiring ──────────────────────────────────────\n\n private attach(): void {\n this.unsub = this.session.on((event: ChatEvent) => {\n this.onSessionEvent(event);\n });\n }\n\n private onSessionEvent(event: ChatEvent): void {\n const convId = this.session.conversationId;\n\n // Forward every event to subscribers as a typed envelope\n this.emit({\n type: \"event\",\n conversationId: convId,\n event,\n });\n\n // Handle completion — message_stop is the terminal turn event.\n if (event.type === ChatEventType.MessageStop) {\n // `!isStreaming` discriminates the LIVE stop from one a restore is\n // replaying over a running turn. On the live path the side effects run\n // before this emit, so the flag is already false by the time a real stop\n // arrives; a replayed one leaves it true. Settling on a replayed stop\n // announces idle mid-turn, after which `finalizeStream` and the real\n // stop both no-op — the state `settleIdle` documents.\n if (this._state === \"streaming\" && !this.session.isStreaming) {\n this.setState(\"idle\");\n }\n }\n }\n\n // ── Send ──────────────────────────────────────────────────────\n\n async send(content: string, options?: SendOptions): Promise<void> {\n if ((options?.provider == null) !== (options?.model == null)) {\n throw new Error(\n \"`provider` and `model` must be supplied together (client-side model selection).\",\n );\n }\n if (this._state === \"streaming\") return;\n\n // Auto-create conversation if none active.\n //\n // Deliberately NOT guarded the way `createConversation` is, and the\n // asymmetry is the point: that one is a navigation, so it loses to a\n // switch that lands in its await. This one is not — `send` must have a\n // target to send AT, and declining the pointer move would post the user's\n // composed text into whichever conversation they clicked meanwhile.\n // So `send` wins, and the two halves still agree: `session.send` relocates\n // to the same id a moment later. What it costs is that the bump can\n // supersede a `switchTo` that landed inside the await — recoverable,\n // unlike the sticky case, since the send's own `setState`/`finalizeStream`\n // announce and `_activeConversationId` is no longer that conversation, so\n // re-clicking it works.\n let target = this._activeConversationId;\n if (!target) {\n target = await this.session.createNewConversation();\n // Captured, not re-read below: `setActiveConversation` emits\n // `conversationChanged` synchronously, and a handler routing on the\n // pointer can `switchTo` from inside it — so re-reading would post the\n // text the user composed here into whichever conversation they landed\n // on instead. Same door `setActiveConversation` returns its claimed\n // generation to close.\n this.setActiveConversation(target);\n }\n\n // Counted where the turn is ANNOUNCED, not where the method is entered:\n // `turnStarted` is the one signal that survives a state which never\n // changed, so a bump on a path that returns without starting anything\n // reads to an in-flight restore as a takeover it must yield to. Above,\n // `createNewConversation` can reject and leave exactly that.\n this.turnCounter++;\n this.setState(\"streaming\");\n\n try {\n // Spread, not a hand-copied allowlist: this forward used to name each\n // field, and a field added to the session's options but not here was\n // dropped silently with types that said otherwise — which is exactly how\n // `repository` was lost. The manager's `SendOptions` carries no key the\n // session does not accept, so the spread is equivalent today and cannot\n // drift tomorrow.\n await this.session.send(content, {\n ...options,\n conversationId: target ?? undefined,\n });\n } catch {\n // AbortError from detach is expected\n }\n\n this.finalizeStream();\n }\n\n // ── Regenerate ────────────────────────────────────────────────\n\n async regenerate(): Promise<void> {\n if (this._state === \"streaming\") return;\n // Unlike `send`, regenerate cannot be addressed: `resendFromCheckpoint`\n // takes no conversation override, and the message id comes from\n // `session.messages` — which a settling switch can leave holding the\n // PREVIOUS conversation's list. Pairing that id with any conversation is\n // incoherent, so the only correct move is not to act.\n //\n // Gated on which conversation the MESSAGES belong to, not on the session's\n // conversation pointer. Pointer equality is wrong in the widest case\n // rather than an edge: `loadConversation`\n // assigns the pointer SYNCHRONOUSLY and installs the messages only when the\n // fetch returns, so for the whole duration of every ordinary load the two\n // pointers already agree while `messages` still holds the previous\n // conversation's turns. Regenerating there resends the OLD conversation's\n // last message under the NEW conversation's id.\n //\n // `messagesConversationId` moves with the list itself, so it answers the\n // question that actually matters — are these messages this conversation's?\n // Returns silently, as this method already does for a streaming state and\n // an empty history.\n if (this.session.messagesConversationId !== this._activeConversationId) {\n return;\n }\n\n const userMsgs = this.session.messages.filter(\n (m: { role: string }) => m.role === \"user\",\n );\n const lastUserMsg = userMsgs[userMsgs.length - 1];\n if (!lastUserMsg) return;\n\n // Past BOTH silent returns above, for the reason given at `send`'s bump.\n // The first of them is not an edge during a restore but the ordinary case:\n // `loadConversation` moves the pointer synchronously and installs the list\n // only when its fetch returns, so for that whole window these two disagree\n // and this method returns having done nothing.\n this.turnCounter++;\n this.setState(\"streaming\");\n\n try {\n await this.session.resendFromCheckpoint(\n lastUserMsg.id,\n lastUserMsg.content,\n );\n } catch {\n // AbortError from detach is expected\n }\n\n this.finalizeStream();\n }\n\n // ── Switch conversation ───────────────────────────────────────\n\n /**\n * Switch the active conversation.\n *\n * ``opts.skipHistoryReplay`` is a fast path for consumers that CACHE a\n * restored conversation's rendered blocks: it moves the active pointer and\n * loads the message list (needed for send / regenerate context) but skips\n * the expensive event fetch + replay, and never enters the ``restoring``\n * state, so a consumer that clears its block view on ``restoring`` keeps\n * showing the cached history with no flash of a spinner.\n *\n * It still confirms there is no live job before skipping: the in-memory\n * background-job map is empty on a fresh instance (page reload) and blind to\n * jobs started in another tab/device, so the fast path always asks the server\n * (``getActiveJob``) and falls through to a full reconnect if one is running.\n * That one small request is the only cost it doesn't skip, so passing the\n * flag whenever you hold cached blocks is safe.\n */\n async switchTo(\n conversationId: string,\n opts?: { skipHistoryReplay?: boolean },\n ): Promise<void> {\n if (conversationId === this._activeConversationId) return;\n\n // Capture BEFORE the delete below: a background job THIS instance detached\n // must reconnect, so it can never take the cached fast path.\n const targetHadBackgroundJob = this._backgroundJobs.has(conversationId);\n\n // If streaming, detach (job keeps running in background)\n this.detachStreamingTurn();\n\n // Clear background job for target (we're viewing it now). Captured,\n // because the delete is a CLAIM that this switch will take the job over —\n // see the undo after `restore`.\n const parkedJobId = this._backgroundJobs.get(conversationId);\n if (parkedJobId !== undefined) {\n this._backgroundJobs.delete(conversationId);\n this.emit({\n type: \"backgroundJobsChanged\",\n jobs: this._backgroundJobs,\n });\n }\n\n // Captured from the call itself, not read back afterwards — see\n // `setActiveConversation`.\n const gen = this.setActiveConversation(conversationId);\n\n if (opts?.skipHistoryReplay && !targetHadBackgroundJob) {\n // Cached fast path — but a job started before this instance existed (page\n // reload) or in another tab/device won't be in _backgroundJobs, so confirm\n // with the server that nothing is live before skipping the reconnect.\n let activeJobId: string | null = null;\n try {\n activeJobId = (await this.session.client.getActiveJob(conversationId))\n .jobId;\n } catch {\n // Network error — treat as no active job (best-effort, matches restore()).\n }\n if (gen !== this.generation) return;\n if (!activeJobId) {\n // Consumer already holds the rendered blocks. Load the message list so\n // send/regenerate have their context, but skip the fetch + replay and\n // stay out of the ``restoring`` state.\n // `finally`, for the same reason `restore`'s caller has one:\n // `loadConversation` rejects when the API fetch and the\n // `storage.fetchMessages` fallback both fail, and `detachStreamingTurn`\n // above recorded `idle` WITHOUT announcing on the contract that the\n // caller announces. Returning through a throw leaves the consumer's\n // last `stateChange` at `streaming` with nothing able to clear it —\n // `finalizeStream` and the `message_stop` branch both act only on\n // `streaming`, so the composer stays disabled for the session.\n try {\n await this.session.loadConversation(conversationId);\n } finally {\n if (gen === this.generation) this.settleIdle();\n }\n return;\n }\n // A live job is running — fall through to a full restore(), which\n // reconnects to its stream.\n }\n\n let tookOver = false;\n try {\n await this.restore(conversationId, gen);\n tookOver = gen === this.generation;\n } finally {\n // In a `finally`, because `restore` awaits `loadConversation` unguarded\n // and that rejects when the API fetch AND the `storage.fetchMessages`\n // fallback both fail. `switchTo` then rejects and everything below would\n // be skipped — losing the parked job permanently and leaving `_state` at\n // `restoring` for good, since nothing else announces and neither the\n // next switch nor a send resets it. `switchConversation` already defends\n // this same case on the grounds that `ChatStorage` is public; this path\n // was the inconsistency.\n //\n // Deleting the entry above was a CLAIM that this switch would take the\n // job over. A superseded or throwing restore never does, so the job is\n // left running with no local record: the badge vanishes and\n // `deleteConversation` can no longer cancel it, because\n // `_backgroundJobs.get(id)` is undefined and `wasActive` is false so\n // neither cancel path fires. Not re-added when the switch that\n // superseded us landed here — it owns the display now.\n // Suppressed only when a NEWER switch landed on this same conversation\n // and now owns its display — not merely because the pointer still names\n // it, which is also true when `restore` threw and we are still here.\n const supersededOntoSame =\n gen !== this.generation &&\n this._activeConversationId === conversationId;\n // ...and not for a conversation that was DELETED while we were parked.\n // `deleteConversation` cancels the parked job by reading\n // `_backgroundJobs.get(id)`, which this switch had already emptied — so\n // it found nothing to cancel, and putting the entry back afterwards\n // leaves a running job nothing can stop plus a badge on a conversation\n // no longer in the list. Exactly the harm the cancel branch argues\n // against, reached through the delete path.\n const stillExists = this.session.conversations.some(\n (c) => c.id === conversationId,\n );\n if (\n parkedJobId !== undefined &&\n !tookOver &&\n !supersededOntoSame &&\n stillExists\n ) {\n this._backgroundJobs.set(conversationId, parkedJobId);\n this.emit({\n type: \"backgroundJobsChanged\",\n jobs: this._backgroundJobs,\n });\n }\n // A throw leaves `restoring` announced with no successor to clear it.\n // Only when we are still the current generation: if a newer switch\n // superseded us it owns the announcement.\n if (!tookOver && gen === this.generation && this._state === \"restoring\") {\n this.settleIdle();\n }\n }\n }\n\n // ── Resync after the page sat in the background ───────────────\n\n /**\n * Re-attach to whatever the server says is live for the ACTIVE conversation.\n *\n * The reconnect machinery inside ``consumeEventStream`` only runs while a\n * stream is being consumed — and a page suspended in the background (locked\n * phone, app switch, hidden tab) can outlive it: timers are throttled or\n * suspended, so the stall watchdog may never fire while hidden; the\n * reconnect budget (``SSE_MAX_RECONNECTS``) can burn out in fail-fast\n * attempts; and a 401 from a rotated access token ends the loop outright as\n * non-retryable. What is left is a manager that believes a turn is streaming\n * (or has given up on one that is still running) with nothing attached — and\n * no navigation will ever fix it, because ``switchTo`` early-returns on the\n * conversation it is already on.\n *\n * So the consumer calls this when the user COMES BACK\n * (``visibilitychange → visible``, window ``focus``). One ``getActiveJob``\n * probe, then:\n *\n * - attached to exactly the job the server calls live → healthy. The stall\n * watchdog owns zombie recovery from here, now that timers run again.\n * No-op.\n * - anything else — attached to a job the server no longer calls live,\n * attached to nothing while a job runs, or idle with a live job another\n * tab/device started — → detach and re-run ``restore``, the same path a\n * conversation reopen takes, with all of its supersession and takeover\n * guards inherited.\n *\n * Skipped while a restore is already in flight (it is converging on server\n * truth by itself) and while another resync holds the flag — see\n * ``_resyncing``.\n */\n async resync(): Promise<void> {\n const conversationId = this._activeConversationId;\n if (!conversationId) return;\n if (this._state === \"restoring\" || this._resyncing) return;\n this._resyncing = true;\n // Captured BEFORE the probe, with nothing awaiting between here and the\n // set: a switch moves the generation, a send or regenerate moves the turn\n // counter (never the generation — see `turnCounter`), and whichever lands\n // inside the probe's await owns everything after it. `turnStarted`, not\n // `viewTakenOverByLiveTurn`: the zombie case this method exists for IS\n // `_state === \"streaming\"` with nothing attached, so the streaming state\n // alone cannot mean \"a live send took over\" — but a turn that STARTED\n // since the capture unambiguously is one.\n const gen = this.generation;\n const turn = this.turnCounter;\n try {\n let activeJobId: string | null = null;\n try {\n activeJobId = (await this.session.client.getActiveJob(conversationId))\n .jobId;\n } catch {\n // Can't ask the server — the stall watchdog still owns recovery.\n return;\n }\n if (gen !== this.generation || this.turnStarted(turn)) return;\n // Is this manager attached to a turn at all? `_state` is the synchronous\n // authority; `session.isStreaming` covers the window where a send/restore\n // raised it behind an await (see `viewTakenOverByLiveTurn`).\n const attached = this._state === \"streaming\" || this.session.isStreaming;\n // Nothing attached and nothing live: the ordinary case for a focus event,\n // and the one that must cost nothing beyond the probe above.\n if (activeJobId === null && !attached) return;\n // Attached to exactly the job the server calls live: healthy. The stall\n // watchdog owns zombie recovery from here, now that timers run again.\n if (\n activeJobId !== null &&\n this.session.isStreaming &&\n this.session.currentJobId === activeJobId\n ) {\n return;\n }\n // Attached to a job the server no longer calls live, or attached to\n // nothing while one runs: rebuild from server truth. `session.detach()`\n // aborts whatever socket remains and emits `disconnected` (consumers\n // clear their streaming flags on it) WITHOUT `detachStreamingTurn`'s\n // parking bookkeeping — this job is not becoming a background job, it\n // is about to be re-attached or replayed by the restore below. Its\n // `currentJobId` is cleared for the same reason `detachStreamingTurn`\n // clears it: `detach()` deliberately leaves it, `stop()` has no state\n // guard, and the branches below where the restore does NOT reconnect\n // would leave it naming a job the server just said is not live.\n this.session.detach();\n this.session.currentJobId = null;\n // Un-own the old turn WITHOUT announcing — `restore` announces from\n // here. Same contract as `detachStreamingTurn`'s tail: had this left\n // `streaming` set, `restore`'s `settleIdle` would read it as a live\n // send's state and refuse to announce, stranding the state.\n this._state = \"idle\";\n try {\n await this.restore(conversationId, gen);\n } catch {\n // The `finally` below settles the announced state. Swallowed\n // deliberately: this method is documented to be wired straight into\n // visibility/focus listeners with no `.catch` of their own, and the\n // failure is recoverable — the settle re-enables the next\n // visibility/focus event to retry rather than locking it out.\n }\n } finally {\n this._resyncing = false;\n // `restore` can reject at `loadConversation`, having already announced\n // `restoring`. Only when still current: a newer switch owns the\n // announcement. Without this the state is unrecoverable — the guard at\n // the top of this method locks out every later resync, and `switchTo`\n // early-returns on this very conversation. Compared through an asserted\n // local because the entry guard above narrows `_state` to exclude\n // \"restoring\" for the rest of this function, and `restore()`'s writes\n // to it are invisible to that narrowing.\n const restoring = \"restoring\" as StreamState;\n if (gen === this.generation && this._state === restoring) {\n this.settleIdle();\n }\n }\n }\n\n // ── Create / rename / delete conversation ─────────────────────\n\n /**\n * Create a conversation and make it active.\n *\n * The returned id is NOT guaranteed to be the active conversation: if a\n * switch lands inside the storage round-trip, this declines the pointer move\n * so the newer one wins, and no `conversationChanged` fires for the new id.\n * A caller that routes on the return value should `switchTo(id)` rather than\n * assume it is current — that call is not a no-op in the declined case.\n */\n async createConversation(): Promise<string> {\n // BEFORE `createNewConversation`, which is itself the relocation — it sets\n // `session.conversationId` and empties `session.messages`. `detach()`\n // emits `disconnected`, and `onSessionEvent` tags every event from\n // `session.conversationId`, so tearing down afterwards labels the OLD\n // conversation's teardown with the NEW conversation's id — and does it\n // before `conversationChanged` has fired. `switchTo` detaches while the\n // pointer is still the old one; this is the parity that comment claimed.\n this.detachStreamingTurn();\n let id: string;\n try {\n id = await this.session.createNewConversation();\n } catch (err) {\n // `detachStreamingTurn` above tore the turn down and recorded `idle`\n // without announcing; a rejecting `storage.createConversation` (quota, a\n // network-backed store) would otherwise propagate past both settle\n // points below and leave the composer disabled for good. Same shape\n // `deleteConversation` uses: announce what happened, then rethrow.\n this.settleIdle();\n throw err;\n }\n // A `switchTo` landing inside that await claimed the newer generation, and\n // relocating over it would bump the generation out from under its restore:\n // the consumer sees `conversationChanged: B` followed by this one, and B\n // never restores. Last writer wins, everywhere.\n //\n // Read the SESSION's outcome rather than deriving a second opinion from\n // our own counter. The two are not equivalent in both directions:\n // `setActiveConversation` bumps both, so a superseded manager implies a\n // declining session — but `loadConversation` and a relocating `send` bump\n // `loadGeneration` ALONE, so the session can decline while our generation\n // is untouched and we would relocate over it. Either mismatch leaves the\n // manager and the session naming different conversations, which is worse\n // than either outcome alone: `switchTo` early-returns on the one it thinks\n // is active, so the user cannot click their way out.\n if (this.session.conversationId !== id) {\n // `detachStreamingTurn` above already parked the job, detached the\n // stream and recorded `idle` WITHOUT announcing, on the contract that\n // the caller announces. Every sibling path does — `settleIdle` below,\n // `deleteConversation`'s catch, `switchTo`'s successor restore — and\n // this one did not. When a `switchTo` caused the decline its restore\n // covers the gap, but `loadGeneration` also moves via the public\n // `loadConversation` / `switchConversation`, and then nothing announces\n // and the composer stays disabled after the turn was torn down.\n this.settleIdle();\n return id;\n }\n this.setActiveConversation(id);\n // Settle the state here. `setActiveConversation` bumps the generation, so\n // a restore this supersedes now returns WITHOUT emitting — including\n // without the `idle` that would have cleared a consumer's spinner. Unlike\n // `switchTo`, there is no successor restore to announce it instead, so\n // `_state` would sit at `restoring` on a brand-new empty conversation\n // until the next `send` or `stop` happened to clear it. Being a\n // generation-bumping origin means owning the announcement — but not over\n // a live turn: `createNewConversation` is awaited, and a send landing\n // inside that await owns the streaming state and will finalize it itself.\n this.settleIdle();\n return id;\n }\n\n /**\n * Rename a conversation. Purely a relabel — no active-conversation or\n * background-job bookkeeping to do, unlike delete, so this is a passthrough.\n */\n async renameConversation(id: string, title: string): Promise<void> {\n await this.session.renameConversation(id, title);\n }\n\n async deleteConversation(id: string): Promise<void> {\n // Evaluated up front, because the cancel below has to happen BEFORE the\n // delete: `session.deleteConversation` nulls `conversationId` and empties\n // `messages`, and `disconnect()` emits `disconnected`, which\n // `onSessionEvent` tags from that same field — so cancelling afterwards\n // labels the teardown `null`, before `conversationChanged` has fired. Same\n // ordering fault as `createConversation` had. It also stops the job a\n // round-trip sooner.\n const wasActive = this._activeConversationId === id;\n const cancelled = wasActive && this._state === \"streaming\";\n if (cancelled) {\n // NOT `disconnect()`: it ends in `protocols.clear()`. Deleting one\n // conversation is not a session teardown.\n this.session.cancelTurn();\n this._state = \"idle\"; // cancelled, not parked — recorded, not announced\n }\n try {\n await this.session.deleteConversation(id);\n } catch (err) {\n // The delete did NOT happen. `ChatSession.deleteConversation` rejects\n // either because the server refused it (anything but a 404, which means\n // already-gone) or because `ChatStorage` threw — and both reject BEFORE\n // it filters `conversations` or nulls the pointer. Relocating here would\n // announce a deletion that never occurred, against a session that still\n // lists the conversation and still holds its messages.\n //\n // But the cancel above already tore the stream down and recorded `idle`\n // WITHOUT announcing, so that much has to be announced or the composer\n // stays spinning on a conversation that is neither deleted nor\n // streaming. Announce, relocate nothing, and let the caller see the\n // failure — swallowing it reports success for work that did not happen.\n if (cancelled) this.setState(\"idle\");\n throw err;\n }\n // Re-tested, not `wasActive`: that was read before a real DELETE, and\n // relocating on it would overwrite a switch that landed during the\n // round-trip and bump the generation out from under its restore.\n // `wasActive` is still the right read for the CANCEL, which must happen\n // before the delete.\n if (this._activeConversationId === id) {\n // CANCEL rather than park. `detachStreamingTurn` is right when the user\n // navigates away — the turn keeps running and can be rejoined — but this\n // conversation is gone, so its output has nowhere to land. Parking it\n // would also re-add the very entry deleted below: consumers would render\n // a running-job indicator on a conversation no longer in the list, and\n // `switchTo` would compute `targetHadBackgroundJob` and force a full\n // restore of it.\n this.setActiveConversation(null);\n // Same reason as `createConversation`: this bumps the generation, so any\n // restore it supersedes goes quiet, and nothing else will announce.\n this.settleIdle();\n }\n // AFTER the branch above, so nothing can put the entry back.\n //\n // Emitted, or the consumer's last snapshot keeps a running-job badge on a\n // conversation no longer in the list — the same harm the `wasActive`\n // comment above argues against, on the branch that does not take that fix.\n // Cancelled too, for parity with the active branch above: the conversation\n // is gone either way, so a parked job left running bills tokens for output\n // with nowhere to land. Whether you happened to be watching it when you\n // pressed delete should not decide that.\n const parkedJobId = this._backgroundJobs.get(id);\n if (this._backgroundJobs.delete(id)) {\n if (parkedJobId) {\n this.session.client.cancelJob(parkedJobId).catch(() => {});\n }\n this.emit({ type: \"backgroundJobsChanged\", jobs: this._backgroundJobs });\n }\n }\n\n // ── Stop (explicit cancel) ────────────────────────────────────\n\n stop(): void {\n // `cancelTurn`, not `disconnect`: Stop ends the TURN. `disconnect` ends in\n // `protocols.clear()`, so routing Stop through it dropped every registered\n // `ProtocolAdapter` for the rest of the session — the same harm\n // `deleteConversation` avoids, on the path users actually press.\n this.session.cancelTurn();\n this.setState(\"idle\");\n }\n\n // ── Cleanup ───────────────────────────────────────────────────\n\n destroy(): void {\n if (this.unsub) {\n this.unsub();\n this.unsub = null;\n }\n this.handlers = [];\n }\n\n // ── Internal: helpers ──────────────────────────────────────────\n\n /**\n * Park a streaming turn as a background job and detach from its SSE stream.\n *\n * Every method that relocates the active conversation has to do this before\n * announcing a new state. Announcing `idle` while `session.isStreaming` is\n * still true is worse than announcing nothing: `manager.send` no longer bails\n * on the streaming state, calls `session.send`, and THAT bails on its own\n * `isStreaming` — so the message is never posted, no error is emitted, and\n * the composer looks ready the whole time.\n */\n private detachStreamingTurn(): void {\n if (this._state !== \"streaming\") return;\n const oldConvId = this._activeConversationId;\n const jobId = this.session.currentJobId;\n if (oldConvId && jobId) {\n this._backgroundJobs.set(oldConvId, jobId);\n this.emit({ type: \"backgroundJobsChanged\", jobs: this._backgroundJobs });\n }\n this.session.detach();\n // `detach()` deliberately leaves `currentJobId`, but the job is parked now\n // — it is no longer THIS pointer's turn. Left behind, `stop()` (which has\n // no state guard) calls `cancelTurn()` and cancels the PARKED\n // conversation's job, while `_backgroundJobs` still lists it and emits\n // nothing, so the badge outlives the job. `cancelTurn` documents this\n // exact hazard for its own path.\n this.session.currentJobId = null;\n // Record — without announcing — that the manager no longer owns a live\n // turn. The caller announces, and this is what lets it use `settleIdle()`:\n // a `streaming` state seen there afterwards belongs to a NEW send that\n // landed during the caller's own awaits, which owns its own announcement.\n this._state = \"idle\";\n }\n\n /**\n * Announce `idle` unless a turn is actually streaming.\n *\n * A `send` can land inside any of the switch paths — the fast path most\n * easily, since it deliberately stays out of `restoring` and so leaves the\n * composer live for the whole probe. `send` sets `streaming` and does not\n * bump the generation, so the path resumes, passes its supersession check,\n * and would announce a ready composer over a running stream. From there\n * `finalizeStream` and the `message_stop` branch both no-op (they only act\n * on `streaming`), so it stays `idle` for the whole turn — and the next send\n * reaches `session.send`, which bails on its own `isStreaming`: message\n * never posted, no error, composer ready throughout.\n */\n private settleIdle(): void {\n if (this._state === \"streaming\") return;\n this.setState(\"idle\");\n }\n\n private finalizeStream(): void {\n if (this._state === \"streaming\") {\n this.setState(\"idle\");\n }\n }\n\n // ── Internal: restore ─────────────────────────────────────────\n\n /**\n * A conversation's turns, oldest first.\n *\n * Its own method so ``restore`` can put the request on the wire beside the\n * probe and the message list while ``replayHistory``, which consumes it,\n * keeps owning the shape it reads.\n */\n private jobList(conversationId: string): Promise<RestoreJob[]> {\n return this.session.client.get<RestoreJob[]>(\n `/v1/conversations/${encodeURIComponent(conversationId)}/jobs`,\n );\n }\n\n private async restore(conversationId: string, gen: number): Promise<void> {\n /**\n * Has the user moved on since this restore started?\n *\n * A restore is a long chain of awaits — the active-job probe, the message\n * list, the job list, then every completed turn's events in parallel. That\n * last one is seconds for a large conversation, and clicks are not\n * serialized, so a switch routinely lands mid-chain. Everything after this\n * point either mutates session state the newer switch now owns\n * (``loadConversation``, ``replayTurn``, ``reconnectToJob``) or announces a\n * state the newer switch is responsible for (``setState``), so a superseded\n * restore must stop rather than finish.\n *\n * Left to run, it re-pointed the session at the conversation it was\n * replaying and poured that conversation's whole history out of the event\n * stream, which the consumer rendered into the one on screen.\n *\n * Stopping is safe with a consumer that caches restored blocks: the blocks\n * for this conversation never arrive, so its cache stays empty and the next\n * open takes the full path again rather than the skip-replay fast path.\n */\n const superseded = (): boolean => gen !== this.generation;\n\n // Before the announce and the probe, not after. A handler routing on\n // `conversationChanged` can call `switchTo` synchronously from inside\n // `setActiveConversation`'s emit, so this restore can already be\n // superseded on entry — and announcing here tags `restoring` with the\n // NEWER conversation's id and burns a `getActiveJob` round-trip for one\n // nobody is waiting on. Same re-entrancy door the per-turn check in the\n // replay loop exists for.\n if (superseded()) return;\n // Not over a live turn — the `restoring` analogue of `settleIdle`. The\n // fast path deliberately stays out of `restoring` and leaves the composer\n // live for the whole probe, so a `send` can already own the streaming\n // state by the time a non-null job sends us here. `switchTo`'s own doc\n // says that path exists for \"a consumer that clears its block view on\n // `restoring`\" — announcing it here makes that consumer wipe the turn\n // still streaming into it. The state recovers (the active-job branch\n // re-announces `streaming`); the rendered blocks do not.\n //\n // Captured rather than re-read, because it is also the answer to \"was the\n // consumer told to clear its block view?\", which is what decides whether\n // the history replay below repaints an emptied view or duplicates one that\n // was never emptied. The two questions have the same answer by contract —\n // `restoring` is the documented signal to clear — so they share the flag.\n // Captured with the same timing as `gen`: before anything can await.\n const turn = this.turnCounter;\n const announcedRestoring = !this.session.isStreaming;\n if (announcedRestoring) this.setState(\"restoring\");\n // Re-checked, because `setState` emits SYNCHRONOUSLY and a handler routing\n // on `stateChange` can `switchTo` from inside it — the same re-entrancy\n // door as the check above, one line later. It did not have to be guarded\n // while `loadConversation` sat behind the probe's await and its own\n // supersession check; issuing the load WITH the probe puts it back in this\n // emit's synchronous path, where an unguarded load runs AFTER the newer\n // switch's and so claims the newer token — leaving the session holding the\n // abandoned conversation's id and messages under a manager pointing at the\n // one the user chose.\n //\n // Returning having announced `restoring` is what the check after the probe\n // already did: the superseding switch announces and settles its own state.\n if (superseded()) return;\n\n // The three requests a restore opens with, issued TOGETHER. None is an\n // input to another — the active-job probe, the message list and the job\n // list are all addressed by `conversationId` alone — so serially they cost\n // the SUM of three round trips before a single event is asked for, and any\n // one that stalls blocks the two behind it. Fired together, the restore\n // waits for the slowest instead.\n //\n // What that does NOT change is the order the results are consumed in.\n // `loadConversation` still lands before the replay, because the replay\n // reads the list it installs; parallelising the fetch moves when the\n // request leaves, not when its effect is observed.\n //\n // The two supersession checks either side of `loadConversation` collapse\n // into the one after the joint await: a joint await is still a single\n // await boundary. What does NOT collapse with them is the check above,\n // which now covers the announcement rather than the probe — the load is\n // synchronous with the emit, so that is where the door it has to close\n // moved to. `loadConversation` moving the session pointer sooner is\n // otherwise safe for the reason it is safe at all: `setActiveConversation`\n // bumps the session's load token, so an ASYNCHRONOUS switch landing in this\n // window makes the load in flight lose and its snapshot is never installed.\n const probeRequest = this.session.client\n .getActiveJob(conversationId)\n // Network error — assume no active job.\n .catch(() => null);\n const loadRequest = this.session.loadConversation(conversationId);\n // Handed to `replayHistory` rather than joined here, so that a stalled job\n // list cannot hold up the checks below, and so that its failure still\n // arrives inside the try that already treats a failed history load as\n // non-blocking. The `catch` is only to mark the rejection handled for the\n // paths that reach neither (superseded, or a live turn holding the view) —\n // `replayHistory` awaits the original and handles it itself.\n //\n // Gated on `announcedRestoring`, which `replayHistory` is gated on too:\n // reopening a conversation whose turn is still live takes the reconnect\n // and never replays, so fetching the list there would buy a whole round\n // trip to throw away — a regression in the one thing this change is about.\n // The flag is known synchronously, so keeping it costs no serialisation.\n // The other two discard paths cannot be gated the same way: both are\n // answers that only exist after the await this request is racing.\n const jobsRequest = announcedRestoring\n ? this.jobList(conversationId)\n : null;\n void jobsRequest?.catch(() => {});\n\n const [probe] = await Promise.all([probeRequest, loadRequest]);\n const activeJobId = probe?.jobId ?? null;\n if (superseded()) return;\n\n // History first, in BOTH branches. A live turn used to skip it entirely and\n // reconnect to the running job alone — but a prompt is not in `job_events`\n // (it lives in the messages table, see below), and neither is any earlier\n // turn, so everything except the running turn's own blocks was missing for\n // as long as the turn lasted. Long tool calls made that a matter of\n // minutes, which is exactly when a user switches away and back.\n //\n // Two conditions, answering two different questions, because either one\n // alone replays over a view that still holds content:\n //\n // - `announcedRestoring` — did we tell the consumer to clear? If we never\n // did, it still holds the blocks it rendered and replaying appends to\n // them. This applies to the SETTLED path too, which used to replay\n // regardless: that case now shows no history until the next open, which\n // is the same trade the live path takes and for the same reason — the\n // view was never cleared, so neither outcome is coherent.\n // - has a live turn taken the view over SINCE? `send` bails only on\n // `streaming` and we sit in `restoring`, so nothing gates a send for\n // the whole restore: it goes through, clears nothing, and renders its\n // own optimistic prompt. Replaying then re-emits the prompt it just\n // drew and appends the history under it.\n //\n // The pair is not redundant: a stream that ENDS during the probe leaves\n // no live turn over a view that was never cleared.\n //\n // They are separate statements rather than one `&&` because the answers\n // differ in what they forbid. A takeover means NEITHER half below is ours,\n // so this returns rather than\n // short-circuiting the `&&` into the reconnect: replaying lands under the\n // bubble the send drew, and the reconnect opens the running turn's stream\n // under it — `reconnectToJob`'s own `isStreaming` bail cannot see a send\n // still inside `storage.addMessage`, for the same one-await reason\n // `viewTakenOverByLiveTurn` exists. Gated on `announcedRestoring` so the\n // reading is unambiguous (we set `restoring` ourselves, so anything else is\n // a send or regenerate) and the never-cleared path still falls through to\n // the reconnect exactly as before.\n if (announcedRestoring && this.viewTakenOverByLiveTurn()) return;\n // Tested on `jobsRequest` rather than `announcedRestoring`: the two are the\n // same condition by construction above, and this spelling is the one that\n // narrows the request away from null.\n if (\n jobsRequest &&\n !(await this.replayHistory(\n conversationId,\n gen,\n activeJobId,\n turn,\n jobsRequest,\n ))\n )\n return;\n\n if (activeJobId) {\n this.setState(\"streaming\");\n try {\n await this.session.reconnectToJob(activeJobId);\n } catch {\n // Stream ended or aborted\n }\n // A switch during the stream already detached it and parked the job in\n // ``_backgroundJobs``; the newer switch owns the state from there.\n if (superseded()) return;\n // Discriminated on the SESSION: `_state === \"streaming\"` is also what a\n // `send` landing during the probe sets, and `reconnectToJob` bails on\n // `isStreaming` without reconnecting anything — so announcing `idle` here\n // lands over a running turn (see `settleIdle` for why that is\n // unrecoverable). `settleIdle` itself does not fit; this branch sets\n // `streaming` itself, so its test cannot tell the two cases apart.\n if (this._state === \"streaming\" && !this.session.isStreaming) {\n this.setState(\"idle\");\n }\n } else {\n // Re-checked even though `replayHistory` reports supersession: it does\n // not run at all when we never announced `restoring`, and it swallows a\n // failure that may have left the chain part-way. Either way this\n // announcement belongs to whichever switch is current.\n if (superseded()) return;\n this.settleIdle();\n }\n }\n\n /**\n * Has a live turn taken the block view over?\n *\n * ``_state`` is the SYNCHRONOUS authority and ``session.isStreaming`` lags it\n * by an await: ``send`` sets ``_state = \"streaming\"`` before its first await,\n * while the session only raises its flag inside ``processStream``, behind the\n * ``storage.addMessage`` write. For that whole window a send is underway —\n * composer cleared, optimistic bubble drawn — and the session flag still\n * reads false. Reading both closes the window from either end, since\n * ``reconnectToJob`` is the mirror case: it raises the session flag without\n * ever moving ``_state``.\n */\n private viewTakenOverByLiveTurn(): boolean {\n return this._state === \"streaming\" || this.session.isStreaming;\n }\n\n /**\n * Has a turn STARTED since ``turn`` was captured?\n *\n * ``viewTakenOverByLiveTurn`` reads the current state, so it cannot see a\n * turn that both started and ENDED inside one of the restore's awaits — a\n * send that fails fast (auth, rate limit) resolves in about the time the job\n * list takes, and leaves `_state` back at idle with its blocks already\n * rendered. A monotonic count is the only thing that survives a state that\n * has returned to where it started.\n */\n private turnStarted(since: number): boolean {\n return this.turnCounter !== since;\n }\n\n /**\n * Replay a conversation's persisted history into the consumer's block view.\n *\n * Returns false when this restore lost the right to finish — a newer switch\n * superseded it, or a send took the view over — in which case the caller\n * must stop rather than finish. See ``restore``.\n *\n * ``activeJobId`` names the turn that is still running, if any. Its events\n * are NOT fetched here: they are the live stream the caller reconnects to\n * straight after. It is passed so ``planRestore`` can pair it with the prompt\n * that started it, which is emitted as a bubble with no events — the whole\n * reason a conversation reopened mid-turn now shows the message that started\n * that turn.\n *\n * ``jobsRequest`` is the job list already IN FLIGHT — issued by ``restore``\n * alongside the probe and the message list rather than fetched here, so the\n * three round trips overlap. It is awaited inside the try below, which is\n * what keeps a failed job list non-blocking exactly as it was when the fetch\n * lived here.\n */\n private async replayHistory(\n conversationId: string,\n gen: number,\n activeJobId: string | null,\n turn: number,\n jobsRequest: Promise<RestoreJob[]>,\n ): Promise<boolean> {\n // Three ways to lose the right to replay, checked at every await boundary\n // below because all of them arrive from outside this function while it\n // waits: a newer switch (the generation); a turn holding the view right\n // now; and a turn that has already come and gone inside one of these\n // awaits, which the state check cannot see because the state is back where\n // it started. Nothing gates `send` during a restore — see the caller. The\n // caller treats any of them as \"stop\": a turn that took over owns the\n // state, so reconnecting the one we were restoring would open a second\n // stream under it.\n const stopReplay = (): boolean =>\n gen !== this.generation ||\n this.viewTakenOverByLiveTurn() ||\n this.turnStarted(turn);\n try {\n const jobs = await jobsRequest;\n if (stopReplay()) return false;\n // Every job EXCEPT the one we are about to reconnect to. The probe and\n // this list now LEAVE together, but they are still answered\n // independently, so a turn that ENDS between the two replies comes back\n // settled here while `activeJobId` still names it — putting the same job\n // in the replay set AND `runningJob`, which `planRestore` walks twice and\n // `eventsByJobId` then replays twice, before the reconnect delivers it a\n // third time. The window is narrower than it was (it used to span\n // `loadConversation` too) but it does not close, so the exclusion stays.\n //\n // Excluding it settles both halves at once: the job we reconnect to is\n // never in the events wave and can only enter the plan as the running\n // turn, so the live stream is its single source either way — a reconnect\n // to a job that has just finished still drains its whole event log.\n //\n // This used to also require `status === \"completed\"`, which silently made\n // a FAILED turn unrecoverable. Its events are persisted exactly like any\n // other — `job_events` is the forensic record, and the history endpoint\n // returns a failed job's stream complete, terminal `error` event and all —\n // but restore never asked for them, so the whole turn vanished on reload:\n // the tool calls, their output, and the error that explains why it\n // stopped. A conversation whose ONLY job failed came back blank.\n //\n // Status is the wrong axis for this decision. What decides whether a job\n // belongs in the events wave is where its events COME FROM: the live\n // stream for the one being reconnected to, storage for every other. How a\n // turn ended says nothing about that, and a client that hides failed turns\n // does not make them not have happened — it just stops the user seeing\n // what the agent did before it stopped.\n const replayableJobs = jobs.filter(\n (j: { job_id: string }) => j.job_id !== activeJobId,\n );\n\n // User prompts aren't persisted in job_events — they live in the\n // messages table, so each turn has to be paired with the message that\n // started it. `job.message_id` is that link; planRestore also decides\n // where mid-run steers (user messages that start no job) and goal\n // continuations (jobs with no visible prompt) belong. See\n // restore-plan.ts for why pairing by index was wrong.\n const userMessages = this.session.messages.filter(\n (m) => m.role === \"user\",\n );\n // Matched against the job LIST rather than trusted from the probe: the\n // prompt pairing needs the running job's `message_id`, which only the\n // list carries. A probe id absent from the list (raced purge) leaves\n // `runningJob` undefined — and because `claimedMessageIds` is derived\n // from that same list, its prompt is then unclaimed and surfaces as a\n // steer bubble instead of vanishing.\n const runningJob = activeJobId\n ? jobs.find((j) => j.job_id === activeJobId)\n : undefined;\n const plan = planRestore({\n completedJobs: replayableJobs.map((j) => ({\n job_id: j.job_id,\n message_id: j.message_id,\n })),\n runningJob: runningJob && {\n job_id: runningJob.job_id,\n message_id: runningJob.message_id,\n },\n // EVERY job, which is the same set the replay walk gets. The claim set\n // answers one question — \"will some turn step already draw this prompt?\"\n // — so it is a RESTATEMENT of the walk, and the two drifting apart is\n // what produces either a missing bubble or a doubled one.\n //\n // Failed and cancelled were excluded here for exactly one reason: they\n // produced no turn step, which is the bug fixed above. Now that they do,\n // the exclusion has no case left to describe.\n //\n // Being precise about what this change does and does not do: it is not\n // load-bearing TODAY. `planRestore` anchors a prompt at the index its\n // job links to and advances the cursor past it, so a message claimed by\n // a job inside the walk is never offered to the steer branch anyway —\n // the two spellings agree on current inputs. It is here because the\n // invariant is what keeps them agreeing: narrow the walk again without\n // narrowing this, and the prompts of the jobs dropped from it go with\n // them, silently. Deriving both from `jobs` makes that impossible to\n // get half-right.\n claimedMessageIds: jobs.map((j) => j.message_id),\n userMessages: userMessages.map((m) => ({\n id: m.id,\n content: m.content,\n })),\n });\n\n // Fetch every turn's events up front, in PARALLEL. The backend strips\n // live-only deltas from this path, so each response is small; parallel\n // fetch collapses N serial round-trips into one wave. We still fetch\n // per job (not the whole conversation in one call) so superseded\n // regeneration versions stay available for version navigation — the\n // whole-conversation endpoint drops them.\n //\n // The running turn is absent by construction (excluded above): its events\n // are the live stream the caller reconnects to, and fetching them here\n // would replay every block it is about to receive again.\n const eventLists = await Promise.all(\n replayableJobs.map((job: { job_id: string }) =>\n this.session.client\n .getConversationEvents(conversationId, job.job_id)\n .catch(() => []),\n ),\n );\n // THE window. This wave is the slow part of a restore — the events of\n // every completed turn — and a click during it is the ordinary case,\n // not a rare one. The fetched events are discarded rather than\n // replayed: the replay below is what re-points the session and floods\n // the consumer.\n if (stopReplay()) return false;\n\n const eventsByJobId = new Map(\n replayableJobs.map((job, i) => [job.job_id, eventLists[i] ?? []]),\n );\n\n // Replay every step in one SYNCHRONOUS pass (no awaits between events\n // or turns), so the consumer batches the whole history into a single\n // render instead of re-typing it event by event. A steer replays as a\n // turn with no events: the bubble, and nothing after it.\n for (const step of plan) {\n // Checked per TURN, not just before the loop: \"synchronous\" bounds\n // out awaits, not re-entrancy. `replayTurn` emits through\n // `onSessionEvent` to every handler, and nothing in the `on()`\n // contract stops a handler driving the manager straight back —\n // `switchTo`, `createConversation` and `deleteConversation` all bump\n // the generation from inside this loop. Without this the remaining\n // turns keep pouring out, tagged with the abandoned conversation's\n // id, which is the leak this guard exists to close, reached through\n // the one door an await boundary does not cover.\n if (stopReplay()) return false;\n if (step.kind === \"steer\") {\n this.session.replayTurn(\n conversationId,\n [],\n step.content,\n step.messageId,\n true,\n );\n continue;\n }\n // The running turn resolves to no entry here by design, so this emits\n // its prompt bubble and nothing else — and does so BEFORE the caller\n // reconnects, which is what keeps the bubble above the agent header\n // the live stream's `message_start` is about to open.\n this.session.replayTurn(\n conversationId,\n eventsByJobId.get(step.jobId) ?? [],\n step.content,\n step.messageId,\n );\n }\n\n // Before the announcements, not only before `setState` below. The\n // loop's check runs at the TOP of each turn, so a handler that\n // navigates away while the LAST turn replays — or the only turn, for a\n // single-job conversation — exits the loop normally with no iteration\n // left to catch it, and this would fire for the abandoned\n // conversation.\n if (stopReplay()) return false;\n\n // Two announcements, split on purpose. ``restoreSettled`` fires for\n // every replay that ran to the end: it is the rehydration signal\n // (attachment chips, composer modes, goal runs), and a conversation\n // whose only turns were stopped or failed still needs that pass.\n this.emit({ type: \"restoreSettled\", conversationId });\n\n // COMPLETED only, deliberately narrower than the replay set. This drives\n // version navigation, and a version is an answer the user can switch to —\n // a failed turn produced none, so counting it would offer a version that\n // does not exist. Widening the replay set is about showing what happened;\n // this is about what can be navigated between.\n const versionCount = replayableJobs.filter(\n (j: { status: string }) => j.status === \"completed\",\n ).length;\n if (versionCount > 0) {\n this.emit({\n type: \"versionsReady\",\n conversationId,\n count: versionCount,\n });\n }\n } catch {\n // History load failed — non-blocking. A live turn still reconnects, and\n // a settled one still announces idle; the transcript is what is lost,\n // exactly as before this was hoisted out of the completed-only branch.\n }\n return !stopReplay();\n }\n\n // ── Internal: set active conversation ─────────────────────────\n\n private setActiveConversation(id: string | null): number {\n this._activeConversationId = id;\n // The session's load token moves at the same instant as this one. Both\n // halves of a create then consult a counter that has actually changed —\n // otherwise `switchTo` bumps `generation` synchronously while\n // `loadGeneration` waits on the active-job probe, and for that whole\n // window the manager sees itself superseded and the session does not.\n this.session.invalidateLoadsInFlight();\n // EVERY move of the pointer bumps the generation, not just `switchTo`:\n // creating a conversation and deleting the active one relocate the user\n // just as much, and an in-flight restore has to yield to those too.\n const claimed = ++this.generation;\n // Returned so callers capture the generation THIS move claimed, before the\n // emit below. A handler reacting to `conversationChanged` by calling back\n // into the manager — routing on the conversation pointer is the obvious\n // consumer shape — bumps again synchronously, so a caller reading\n // `this.generation` afterwards would capture the INNER value and never see\n // itself as superseded. Both switches would then run to completion and the\n // abandoned one would replay its whole history: the same re-entrancy door\n // the replay loop already guards against.\n this.emit({ type: \"conversationChanged\", conversationId: id });\n return claimed;\n }\n}\n","// =============================================================================\n// Event replay — translate persisted wire events into ChatEvent sequences\n//\n// The backend persists wire events in the `job_events` table. When a\n// consumer needs to restore a conversation (page refresh, conversation\n// switch), it fetches these raw events and replays them through the same\n// ChatEvent pipeline used during live streaming.\n//\n// All wire → ChatEvent translation lives in `translate.ts`. This file just\n// adapts the persisted envelope shape (`{seq, event, data}`) and handles\n// user-message interleaving, which the backend doesn't record.\n// =============================================================================\n\nimport { translateWireEvent } from \"./translate.js\";\nimport type { ChatEvent, WireEvent } from \"./types.js\";\n\n/**\n * Raw SSE event shape returned by GET /v1/conversations/{id}/events.\n * Mirrors what JobEventWriter persists to the job_events table.\n */\nexport interface RawSseEvent {\n seq: number;\n event: string;\n data: Record<string, unknown>;\n /** Epoch ms when the event was persisted (from job_events.created_at). */\n created_at?: number;\n}\n\n/**\n * Build a ``WireEvent`` from the persisted envelope. The data payload is\n * authoritative (and always carries ``type`` in practice); the SSE event\n * name is a fallback for older rows that pre-date the v2 protocol.\n */\nfunction toWireEvent(raw: RawSseEvent): WireEvent | null {\n const type = (raw.data.type as string) || raw.event;\n if (!type || type === \"done\") return null;\n return { ...raw.data, type } as unknown as WireEvent;\n}\n\n/**\n * Map a raw SSE event (persisted in job_events) into the SDK ChatEvent\n * format. Returns an array because some rows (malformed / ``done`` sentinels)\n * map to zero events.\n */\nexport function mapSseToChat(raw: RawSseEvent): ChatEvent[] {\n const wire = toWireEvent(raw);\n if (!wire) return [];\n const event = translateWireEvent(wire);\n return event ? [event] : [];\n}\n\n/**\n * Replay persisted SSE events through the provided handler, interleaving\n * user messages from session.messages at the START of each turn (user\n * messages aren't persisted in job_events).\n *\n * The turn boundary is a change of ``job_id``, NOT ``message_start`` or\n * ``message_stop``. A completed job maps to exactly one user turn — but a\n * single job can contain several ``message_start``/``message_stop`` pairs (a\n * tool-use loop: LLM call → tool result → LLM call again), so neither event\n * reliably delimits turns. And within a job some events precede the first\n * ``message_start`` (e.g. ``memory_recall``, emitted during prompt prep), so\n * gating the user block on ``message_start`` would replay them above the\n * user's own message. Keying off ``job_id`` injects the prompt once per job,\n * before its first event — matching how the restore path\n * (``stream-manager.ts#restore`` → ``session.ts#replayTurn``) leads each turn\n * with its own prompt.\n */\nexport function replayEvents(\n sseEvents: RawSseEvent[],\n userMessages: { role: string; content: string }[],\n handleEvent: (event: ChatEvent) => void,\n addBlock: (block: { type: \"user\"; id: string; content: string }) => void,\n): void {\n const userMsgs = userMessages.filter((m) => m.role === \"user\");\n let userIdx = 0;\n let currentJobId: string | null = null;\n\n for (const raw of sseEvents) {\n const type = (raw.data.type as string) || raw.event;\n if (!type || type === \"done\") continue;\n\n // A new job_id starts a new user turn — inject its prompt before the\n // job's first event. Events without a job_id stay within the current\n // turn (never a boundary), so a stray untagged event can't misfire.\n const jobId = (raw.data.job_id as string | undefined) ?? null;\n if (jobId !== null && jobId !== currentJobId) {\n currentJobId = jobId;\n if (userIdx < userMsgs.length) {\n addBlock({\n type: \"user\",\n id: `replay_user_${userIdx}`,\n content: userMsgs[userIdx]!.content,\n });\n userIdx++;\n }\n }\n\n for (const ce of mapSseToChat(raw)) {\n handleEvent(ce);\n }\n }\n}\n","// =============================================================================\n// Embedded Resource detection — protocol-agnostic\n// =============================================================================\n//\n// The backend can emit rich UI surfaces (A2UI, and future protocols) by\n// wrapping a tool result in an MCP-style embedded resource:\n//\n// {\n// \"_embedded_resource\": true,\n// \"mime_type\": \"application/json+a2ui\",\n// \"uri\": \"a2ui://surface/<id>\",\n// \"payload\": { ...protocol-specific... }\n// }\n//\n// The SDK stays protocol-agnostic: it exposes a detector/parser so\n// frontends can route the payload to a registered renderer for the\n// matching MIME type. The SDK itself never imports a renderer.\n// =============================================================================\n\n/**\n * Parsed shape of an MCP-style embedded resource, as emitted by the\n * backend's UI component tools (``render_surface``, ``update_surface``).\n */\nexport interface EmbeddedResource {\n /** IANA-style MIME type, e.g. \"application/json+a2ui\". */\n mimeType: string;\n /** Opaque URI (e.g. \"a2ui://surface/my-form\"). */\n uri: string;\n /** Protocol-specific payload — shape depends on ``mimeType``. */\n payload: Record<string, unknown>;\n}\n\n/**\n * Detect whether a value is an embedded resource wrapper. The check is\n * purposely loose — any object with ``_embedded_resource: true`` is\n * accepted, which matches the MCP convention and keeps the SDK\n * forward-compatible with future protocols.\n */\nexport function isEmbeddedResource(value: unknown): value is {\n _embedded_resource: true;\n mime_type?: string;\n uri?: string;\n payload?: Record<string, unknown>;\n} {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { _embedded_resource?: unknown })._embedded_resource === true\n );\n}\n\n/**\n * Parse an embedded resource from arbitrary tool output. Returns\n * ``null`` when the value isn't an embedded resource or is malformed.\n *\n * Accepts either an object (the preferred wire format) or a JSON\n * string containing one (defense in depth — some transport layers\n * historically serialized tool results before sending).\n */\nexport function parseEmbeddedResource(value: unknown): EmbeddedResource | null {\n let candidate: unknown = value;\n if (typeof candidate === \"string\") {\n // Only try JSON.parse if it plausibly looks like a JSON object\n // starting with `{`. Avoids pathological input.\n const trimmed = candidate.trim();\n if (!trimmed.startsWith(\"{\")) return null;\n try {\n candidate = JSON.parse(trimmed);\n } catch {\n return null;\n }\n }\n if (!isEmbeddedResource(candidate)) return null;\n const mimeType = candidate.mime_type;\n const uri = candidate.uri;\n const payload = candidate.payload;\n if (typeof mimeType !== \"string\" || !mimeType) return null;\n if (typeof uri !== \"string\" || !uri) return null;\n if (!payload || typeof payload !== \"object\") return null;\n return {\n mimeType,\n uri,\n payload: payload as Record<string, unknown>,\n };\n}\n"],"mappings":"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","names":[]}