agents 0.17.0 → 0.17.3

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,"file":"index.js","names":["textEncoder","MSG_STREAM_RESUME_NONE"],"sources":["../../src/chat/sanitize.ts","../../src/chat/turn-queue.ts","../../src/chat/submit-concurrency.ts","../../src/chat/protocol.ts","../../src/chat/connection.ts","../../src/chat/resumable-stream.ts","../../src/chat/sql-batch.ts","../../src/chat/client-tools.ts","../../src/chat/continuation-state.ts","../../src/chat/pre-stream-turns.ts","../../src/chat/auto-continuation-controller.ts","../../src/chat/abort-registry.ts","../../src/chat/async-helpers.ts","../../src/chat/tool-state.ts","../../src/chat/parse-protocol.ts","../../src/chat/message-reconciler.ts","../../src/chat/repair-transcript.ts","../../src/chat/orphan-persist.ts","../../src/chat/recovery.ts","../../src/chat/recovery-codec.ts","../../src/chat/resume-handshake.ts","../../src/chat/recovery-incident.ts","../../src/chat/recovery-engine.ts","../../src/chat/stall-watchdog.ts"],"sourcesContent":["/**\n * Message sanitization and row-size enforcement utilities.\n *\n * Shared by @cloudflare/ai-chat and @cloudflare/think to ensure persistence\n * hygiene: stripping ephemeral provider metadata and compacting\n * oversized messages before writing to SQLite.\n */\n\nimport type { ProviderMetadata, ReasoningUIPart, UIMessage } from \"ai\";\nimport { truncateToolOutput } from \"./tool-output-truncation\";\n\nconst textEncoder = new TextEncoder();\n\n/** Maximum serialized message size before compaction (bytes). 1.8MB with headroom below SQLite's 2MB limit. */\nexport const ROW_MAX_BYTES = 1_800_000;\n\n/** Measure UTF-8 byte length of a string. */\nexport function byteLength(s: string): number {\n return textEncoder.encode(s).byteLength;\n}\n\n/**\n * Sanitize a message for persistence by removing ephemeral provider-specific\n * data that should not be stored or sent back in subsequent requests.\n *\n * 1. Strips OpenAI ephemeral fields (itemId, reasoningEncryptedContent)\n * 2. Filters truly empty reasoning parts (no text, no remaining providerMetadata)\n */\nexport function sanitizeMessage(message: UIMessage): UIMessage {\n const strippedParts = message.parts.map((part) => {\n let sanitizedPart = part;\n\n if (\n \"providerMetadata\" in sanitizedPart &&\n sanitizedPart.providerMetadata &&\n typeof sanitizedPart.providerMetadata === \"object\" &&\n \"openai\" in sanitizedPart.providerMetadata\n ) {\n sanitizedPart = stripOpenAIMetadata(sanitizedPart, \"providerMetadata\");\n }\n\n if (\n \"callProviderMetadata\" in sanitizedPart &&\n sanitizedPart.callProviderMetadata &&\n typeof sanitizedPart.callProviderMetadata === \"object\" &&\n \"openai\" in sanitizedPart.callProviderMetadata\n ) {\n sanitizedPart = stripOpenAIMetadata(\n sanitizedPart,\n \"callProviderMetadata\"\n );\n }\n\n return sanitizedPart;\n }) as UIMessage[\"parts\"];\n\n const sanitizedParts = strippedParts.filter((part) => {\n if (part.type === \"reasoning\") {\n const reasoningPart = part as ReasoningUIPart;\n if (!reasoningPart.text || reasoningPart.text.trim() === \"\") {\n if (\n \"providerMetadata\" in reasoningPart &&\n reasoningPart.providerMetadata &&\n typeof reasoningPart.providerMetadata === \"object\" &&\n Object.keys(reasoningPart.providerMetadata).length > 0\n ) {\n return true;\n }\n return false;\n }\n }\n return true;\n });\n\n return { ...message, parts: sanitizedParts };\n}\n\nfunction stripOpenAIMetadata<T extends UIMessage[\"parts\"][number]>(\n part: T,\n metadataKey: \"providerMetadata\" | \"callProviderMetadata\"\n): T {\n const metadata = (part as Record<string, unknown>)[metadataKey] as {\n openai?: Record<string, unknown>;\n [key: string]: unknown;\n };\n\n if (!metadata?.openai) return part;\n\n const {\n itemId: _itemId,\n reasoningEncryptedContent: _rec,\n ...restOpenai\n } = metadata.openai;\n\n const hasOtherOpenaiFields = Object.keys(restOpenai).length > 0;\n const { openai: _openai, ...restMetadata } = metadata;\n\n let newMetadata: ProviderMetadata | undefined;\n if (hasOtherOpenaiFields) {\n newMetadata = { ...restMetadata, openai: restOpenai } as ProviderMetadata;\n } else if (Object.keys(restMetadata).length > 0) {\n newMetadata = restMetadata as ProviderMetadata;\n }\n\n const { [metadataKey]: _oldMeta, ...restPart } = part as Record<\n string,\n unknown\n >;\n\n if (newMetadata) {\n return { ...restPart, [metadataKey]: newMetadata } as T;\n }\n return restPart as T;\n}\n\n/** Optional hooks for {@link enforceRowSizeLimit}. */\nexport interface EnforceRowSizeLimitOptions {\n /**\n * Optional logger invoked when a message has to be compacted/truncated. The\n * package supplies its own log prefix (log prefixes stay package-specific).\n */\n warn?: (message: string) => void;\n}\n\n/**\n * Enforce SQLite row size limits by compacting tool outputs and text parts\n * when a serialized message exceeds the safety threshold (1.8MB). Shared by\n * `@cloudflare/ai-chat` and `@cloudflare/think` so both compact identically.\n *\n * Compaction strategy:\n * 1. Compact tool outputs over 1KB with {@link truncateToolOutput}, preserving\n * the structured output shape, and annotate `metadata.compactedToolOutputs`\n * with the compacted tool-call IDs.\n * 2. If still too big, truncate text parts from oldest to newest, annotating\n * `metadata.compactedTextParts` with the truncated part indices.\n */\nexport function enforceRowSizeLimit(\n message: UIMessage,\n options?: EnforceRowSizeLimitOptions\n): UIMessage {\n let json = JSON.stringify(message);\n let size = byteLength(json);\n if (size <= ROW_MAX_BYTES) return message;\n\n if (message.role !== \"assistant\") {\n options?.warn?.(\n `Non-assistant message ${message.id} is ${size} bytes, exceeds row ` +\n `limit. Truncating text parts.`\n );\n return truncateTextParts(message);\n }\n\n options?.warn?.(\n `Message ${message.id} is ${size} bytes, compacting tool outputs to fit ` +\n `SQLite row limit`\n );\n\n const compactedToolCallIds: string[] = [];\n const compactedParts = message.parts.map((part) => {\n if (\n \"output\" in part &&\n \"toolCallId\" in part &&\n \"state\" in part &&\n part.state === \"output-available\"\n ) {\n const output = (part as { output: unknown }).output;\n const truncated = truncateToolOutput(output, 1000);\n if (truncated.truncated) {\n compactedToolCallIds.push(part.toolCallId as string);\n return {\n ...part,\n output: truncated.output\n };\n }\n }\n return part;\n }) as UIMessage[\"parts\"];\n\n const result: UIMessage = { ...message, parts: compactedParts };\n if (compactedToolCallIds.length > 0) {\n result.metadata = {\n ...(result.metadata ?? {}),\n compactedToolOutputs: compactedToolCallIds\n };\n }\n\n json = JSON.stringify(result);\n size = byteLength(json);\n if (size <= ROW_MAX_BYTES) return result;\n\n options?.warn?.(\n `Message ${message.id} still ${size} bytes after tool compaction, ` +\n `truncating text parts`\n );\n return truncateTextParts(result);\n}\n\nfunction truncateTextParts(message: UIMessage): UIMessage {\n const compactedTextPartIndices: number[] = [];\n const parts = [...message.parts];\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n if (part.type === \"text\" && \"text\" in part) {\n const text = (part as { text: string }).text;\n if (text.length > 1000) {\n compactedTextPartIndices.push(i);\n parts[i] = {\n ...part,\n text:\n `[Text truncated for storage (${text.length} chars). ` +\n `First 500 chars: ${text.slice(0, 500)}...]`\n } as UIMessage[\"parts\"][number];\n\n const candidate = { ...message, parts };\n if (byteLength(JSON.stringify(candidate)) <= ROW_MAX_BYTES) {\n break;\n }\n }\n }\n }\n\n const result: UIMessage = { ...message, parts };\n if (compactedTextPartIndices.length > 0) {\n result.metadata = {\n ...(result.metadata ?? {}),\n compactedTextParts: compactedTextPartIndices\n };\n }\n return result;\n}\n","/**\n * TurnQueue — serial async queue with generation-based invalidation.\n *\n * Serializes async work via a promise chain, tracks which request is\n * currently active, and lets callers invalidate all queued work by\n * advancing a generation counter.\n *\n * Used by @cloudflare/ai-chat (full concurrency policy spectrum) and\n * @cloudflare/think (simple serial queue) to prevent overlapping\n * chat turns.\n */\n\nexport type TurnResult<T> =\n | { status: \"completed\"; value: T }\n | { status: \"stale\" };\n\nexport interface EnqueueOptions {\n /**\n * Generation to bind this turn to. Defaults to the current generation\n * at the time of the `enqueue` call. If the queue's generation has\n * advanced past this value by the time the turn reaches the front,\n * `fn` is not called and `{ status: \"stale\" }` is returned.\n */\n generation?: number;\n}\n\nexport class TurnQueue {\n private _queue: Promise<void> = Promise.resolve();\n private _generation = 0;\n private _activeRequestId: string | null = null;\n private _countsByGeneration = new Map<number, number>();\n\n get generation(): number {\n return this._generation;\n }\n\n get activeRequestId(): string | null {\n return this._activeRequestId;\n }\n\n get isActive(): boolean {\n return this._activeRequestId !== null;\n }\n\n async enqueue<T>(\n requestId: string,\n fn: () => Promise<T>,\n options?: EnqueueOptions\n ): Promise<TurnResult<T>> {\n const previousTurn = this._queue;\n let releaseTurn!: () => void;\n const capturedGeneration = options?.generation ?? this._generation;\n\n this._countsByGeneration.set(\n capturedGeneration,\n (this._countsByGeneration.get(capturedGeneration) ?? 0) + 1\n );\n\n this._queue = new Promise<void>((resolve) => {\n releaseTurn = resolve;\n });\n\n await previousTurn;\n\n if (this._generation !== capturedGeneration) {\n this._decrementCount(capturedGeneration);\n releaseTurn();\n return { status: \"stale\" };\n }\n\n this._activeRequestId = requestId;\n try {\n const value = await fn();\n return { status: \"completed\", value };\n } finally {\n this._activeRequestId = null;\n this._decrementCount(capturedGeneration);\n releaseTurn();\n }\n }\n\n /**\n * Advance the generation counter. All turns enqueued under older\n * generations will be skipped when they reach the front of the queue.\n */\n reset(): void {\n this._generation++;\n }\n\n /**\n * Wait until the queue is fully drained (no pending or active turns).\n */\n async waitForIdle(): Promise<void> {\n let queue: Promise<void>;\n do {\n queue = this._queue;\n await queue;\n } while (this._queue !== queue);\n }\n\n /**\n * Number of active + queued turns for a given generation.\n * Defaults to the current generation.\n */\n queuedCount(generation?: number): number {\n return this._countsByGeneration.get(generation ?? this._generation) ?? 0;\n }\n\n private _decrementCount(generation: number): void {\n const count = (this._countsByGeneration.get(generation) ?? 1) - 1;\n if (count <= 0) {\n this._countsByGeneration.delete(generation);\n } else {\n this._countsByGeneration.set(generation, count);\n }\n }\n}\n","import type { MessageConcurrency } from \"./lifecycle\";\n\nexport type NormalizedMessageConcurrency =\n | \"queue\"\n | \"latest\"\n | \"merge\"\n | \"drop\"\n | {\n strategy: \"debounce\";\n debounceMs: number;\n };\n\nexport type SubmitConcurrencyDecision = {\n action: \"execute\" | \"drop\";\n strategy: NormalizedMessageConcurrency | null;\n submitSequence: number | null;\n debounceUntilMs: number | null;\n};\n\nexport class SubmitConcurrencyController {\n private _submitSequence = 0;\n private _latestOverlappingSubmitSequence = 0;\n private _pendingEnqueueCount = 0;\n private _resetEpoch = 0;\n private _activeDebounceTimers = new Set<ReturnType<typeof setTimeout>>();\n private _activeDebounceResolves = new Set<() => void>();\n\n constructor(private readonly options: { defaultDebounceMs: number }) {}\n\n get pendingEnqueueCount(): number {\n return this._pendingEnqueueCount;\n }\n\n get overlappingSubmitCount(): number {\n return this._latestOverlappingSubmitSequence;\n }\n\n decide(options: {\n concurrency: MessageConcurrency;\n isSubmitMessage: boolean;\n queuedTurns: number;\n }): SubmitConcurrencyDecision {\n const queuedTurnsInCurrentEpoch =\n options.queuedTurns + this._pendingEnqueueCount;\n\n if (!options.isSubmitMessage || queuedTurnsInCurrentEpoch === 0) {\n return {\n action: \"execute\",\n strategy: null,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n const concurrency = this.normalize(options.concurrency);\n if (concurrency === \"drop\") {\n return {\n action: \"drop\",\n strategy: concurrency,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n if (concurrency === \"queue\") {\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n const submitSequence = ++this._submitSequence;\n this._latestOverlappingSubmitSequence = submitSequence;\n\n if (concurrency === \"latest\" || concurrency === \"merge\") {\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence,\n debounceUntilMs: null\n };\n }\n\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence,\n debounceUntilMs: Date.now() + concurrency.debounceMs\n };\n }\n\n /**\n * Mark a submit as accepted and in-flight between admission and turn\n * queue registration. Returns an idempotent `release()` function that\n * must be called when the submit either reaches the turn queue or is\n * abandoned. The returned function is bound to the controller's reset\n * epoch — releases from before the most recent `reset()` are no-ops,\n * so post-reset submits keep an accurate count.\n */\n beginEnqueue(): () => void {\n this._pendingEnqueueCount++;\n const epoch = this._resetEpoch;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (this._resetEpoch !== epoch) return;\n this._pendingEnqueueCount = Math.max(0, this._pendingEnqueueCount - 1);\n };\n }\n\n isSuperseded(submitSequence: number | null): boolean {\n return (\n submitSequence !== null &&\n submitSequence < this._latestOverlappingSubmitSequence\n );\n }\n\n async waitForTimestamp(timestampMs: number): Promise<void> {\n const remainingMs = timestampMs - Date.now();\n if (remainingMs <= 0) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n const wrappedResolve = () => {\n this._activeDebounceResolves.delete(wrappedResolve);\n resolve();\n };\n const timer = setTimeout(() => {\n this._activeDebounceTimers.delete(timer);\n wrappedResolve();\n }, remainingMs);\n\n this._activeDebounceTimers.add(timer);\n this._activeDebounceResolves.add(wrappedResolve);\n });\n }\n\n cancelActiveDebounce(): void {\n for (const timer of this._activeDebounceTimers) {\n clearTimeout(timer);\n }\n this._activeDebounceTimers.clear();\n\n const resolves = [...this._activeDebounceResolves];\n this._activeDebounceResolves.clear();\n for (const resolve of resolves) {\n resolve();\n }\n }\n\n reset(): void {\n this._resetEpoch++;\n this._pendingEnqueueCount = 0;\n this.cancelActiveDebounce();\n }\n\n async waitForIdle(waitForQueueIdle: () => Promise<void>): Promise<void> {\n while (true) {\n await waitForQueueIdle();\n if (this._pendingEnqueueCount === 0) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 5));\n }\n }\n\n private normalize(\n concurrency: MessageConcurrency\n ): NormalizedMessageConcurrency {\n if (typeof concurrency === \"string\") {\n return concurrency;\n }\n\n const debounceMs = concurrency.debounceMs;\n\n return {\n strategy: \"debounce\",\n debounceMs:\n typeof debounceMs === \"number\" &&\n Number.isFinite(debounceMs) &&\n debounceMs >= 0\n ? debounceMs\n : this.options.defaultDebounceMs\n };\n }\n}\n","/**\n * Wire protocol message type constants for the cf_agent_chat_* protocol.\n *\n * These are the string values used on the wire between agent servers and\n * clients. Both @cloudflare/ai-chat (via its MessageType enum) and\n * @cloudflare/think use these values.\n */\nexport const CHAT_MESSAGE_TYPES = {\n CHAT_MESSAGES: \"cf_agent_chat_messages\",\n USE_CHAT_REQUEST: \"cf_agent_use_chat_request\",\n USE_CHAT_RESPONSE: \"cf_agent_use_chat_response\",\n CHAT_CLEAR: \"cf_agent_chat_clear\",\n CHAT_REQUEST_CANCEL: \"cf_agent_chat_request_cancel\",\n STREAM_RESUMING: \"cf_agent_stream_resuming\",\n STREAM_RESUME_ACK: \"cf_agent_stream_resume_ack\",\n STREAM_RESUME_REQUEST: \"cf_agent_stream_resume_request\",\n STREAM_RESUME_NONE: \"cf_agent_stream_resume_none\",\n // Server→client: a turn has been accepted but its resumable stream has not\n // started yet (queued, debouncing, waiting on MCP setup, or running async\n // work in `onChatMessage`). Sent in response to a resume request (or on\n // connect) so a reconnecting/re-mounting client keeps its expectation instead\n // of resolving its resume probe to \"no stream\" and then false-timing-out the\n // turn. Resolved by a later `STREAM_RESUMING` (stream started) or\n // `STREAM_RESUME_NONE` (turn settled without streaming). Backward-compatible —\n // clients that don't understand it ignore it. See issue #1784.\n STREAM_PENDING: \"cf_agent_stream_pending\",\n TOOL_RESULT: \"cf_agent_tool_result\",\n TOOL_APPROVAL: \"cf_agent_tool_approval\",\n MESSAGE_UPDATED: \"cf_agent_message_updated\",\n // Server→client: a durable chat turn is being recovered (interrupted by a\n // deploy/eviction or a stream-stall watchdog abort and now resuming). Sent\n // when a recovery continuation is scheduled and cleared on every terminal\n // outcome; `@cloudflare/think` also replays it on connect so a client that\n // joins mid-recovery learns it. Purely a progress hint — backward-compatible\n // (clients that don't understand it ignore it). See issue #1620.\n CHAT_RECOVERING: \"cf_agent_chat_recovering\"\n} as const;\n","/**\n * Connection I/O — shared WebSocket send guard for chat agents.\n *\n * `@internal` — sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API. See\n * `design/rfc-chat-recovery-foundation.md`.\n *\n * Both packages (and `continuation-state`) hand-maintained byte-identical\n * copies of `sendIfOpen` / `isWebSocketClosedSendError`; this is the single\n * shared implementation.\n */\n\n/**\n * Minimal connection interface for sending WebSocket messages. Matches the\n * `Connection` type from `agents` without importing it: `Connection` extends\n * `WebSocket` with its own `send` overload, so it is structurally assignable.\n */\nexport interface ChatConnection {\n readonly id: string;\n send(message: string): void;\n}\n\n/**\n * Send a message on a connection, swallowing the specific\n * \"send after close\" error a racing disconnect produces. Returns `true` if the\n * send went out, `false` if the socket was already closed. Any other error\n * rethrows.\n */\nexport function sendIfOpen(\n connection: ChatConnection,\n message: string\n): boolean {\n try {\n connection.send(message);\n return true;\n } catch (error) {\n if (isWebSocketClosedSendError(error)) return false;\n throw error;\n }\n}\n\n/** Whether an error is the \"WebSocket send() after close\" `TypeError`. */\nexport function isWebSocketClosedSendError(error: unknown): boolean {\n return (\n error instanceof TypeError &&\n error.message.includes(\"WebSocket send() after close\")\n );\n}\n","/**\n * ResumableStream: Standalone class for buffering, persisting, and replaying\n * stream chunks in SQLite. Extracted from AIChatAgent to separate concerns.\n *\n * Handles:\n * - Chunk buffering (batched writes to SQLite for performance)\n * - Stream lifecycle (start, complete, error)\n * - Chunk replay for reconnecting clients\n * - Stale stream cleanup\n * - Active stream restoration after agent restart\n */\n\nimport { nanoid } from \"nanoid\";\nimport type { Connection } from \"agents\";\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\nimport { sendIfOpen } from \"./connection\";\n\n/** Number of chunks to pack into a single SQLite row before flushing */\nconst CHUNK_BUFFER_SIZE = 10;\n/** Maximum buffer size to prevent memory issues on rapid reconnections */\nconst CHUNK_BUFFER_MAX_SIZE = 100;\n/**\n * Max accumulated raw chunk bytes packed into one row before forcing a flush.\n * The SQLite row limit is 2 MB; packing serializes bodies into a JSON array,\n * which re-escapes their contents (quotes/backslashes), so we keep the raw\n * total well under the limit to leave generous headroom for escaping overhead.\n * A chunk larger than this is flushed as its own (unwrapped) row.\n */\nconst SEGMENT_MAX_BYTES = 512_000;\n/** Default cleanup interval for old streams (ms) - every 10 minutes */\nconst CLEANUP_INTERVAL_MS = 10 * 60 * 1000;\n/**\n * Retention for completed/errored stream buffers, measured from completion.\n *\n * The assistant message is persisted separately (`cf_ai_chat_agent_messages`),\n * so once a stream completes its buffer is no longer the source of truth — it\n * is only a brief reconnect-and-replay grace window: long enough to cover a\n * client that dropped at the completion boundary and reconnects to replay the\n * just-finished stream, and to deliver a pending terminal error frame on a\n * resumed stream (#1645). It is deliberately short (not the chat's lifetime)\n * so idle/one-off chat DOs don't accumulate stale buffers (#1706).\n */\nconst COMPLETED_RETENTION_MS = 10 * 60 * 1000;\n/**\n * Retention for abandoned `streaming` rows, measured from LAST chunk activity.\n *\n * Generous relative to {@link COMPLETED_RETENTION_MS}: an interrupted turn must\n * have ample time to be resumed by a reconnecting client or healed by fiber\n * recovery before its buffer is reaped. Only a stream that has produced no\n * chunk for this long is treated as truly dead. Keyed off last activity (not\n * start time) so a long but still-active stream is never swept mid-flight.\n */\nconst ABANDONED_STREAM_RETENTION_MS = 60 * 60 * 1000;\n/** Shared encoder for UTF-8 byte length measurement */\nconst textEncoder = new TextEncoder();\n\n/**\n * How far ahead (seconds) to schedule the resumable-stream buffer cleanup\n * alarm. Set to the short completion-grace window ({@link COMPLETED_RETENTION_MS},\n * 10m) so a finished buffer is reclaimed promptly. The re-arm-while-reclaimable\n * loop (see {@link cleanupStreamBuffers}) revisits any longer-lived rows — e.g.\n * an abandoned in-flight buffer on its 1h window — by waking again each interval\n * until they age out, then stops. Driving cleanup from an alarm (rather than\n * only piggybacking on the next stream completion) ensures idle/one-off chat\n * DOs still reclaim their buffers without waking forever (#1706). Shared by\n * `AIChatAgent` and `Think`.\n */\nexport const STREAM_CLEANUP_DELAY_SECONDS = 10 * 60;\n\n/**\n * A stored row body is either a single chunk body (a JSON object string —\n * legacy per-chunk rows and single-chunk segments) or a packed segment (a JSON\n * array of chunk body strings). Unpack to the individual chunk bodies in order.\n *\n * Stored chunk bodies are always serialized JSON *objects*, never arrays, so\n * `Array.isArray` reliably distinguishes a packed segment from a single body.\n */\nfunction unpackSegmentBody(rowBody: string): string[] {\n try {\n const parsed = JSON.parse(rowBody);\n if (Array.isArray(parsed)) {\n return parsed as string[];\n }\n } catch {\n // Not valid JSON — treat as a single opaque body.\n }\n return [rowBody];\n}\n\nfunction isMissingMetadataColumnError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return (\n (message.includes(\"message_id\") || message.includes(\"is_continuation\")) &&\n (message.toLowerCase().includes(\"no such column\") ||\n message.toLowerCase().includes(\"has no column named\"))\n );\n}\n\n/**\n * Stored stream chunk for resumable streaming\n */\ntype StreamChunk = {\n id: string;\n stream_id: string;\n body: string;\n chunk_index: number;\n created_at: number;\n};\n\n/**\n * Stream metadata for tracking active streams\n */\ntype StreamMetadata = {\n id: string;\n request_id: string;\n status: \"streaming\" | \"completed\" | \"error\";\n created_at: number;\n completed_at: number | null;\n /**\n * The assistant message id this stream is producing, captured when the\n * stream starts. This is the SAME id the live path persists under, so orphan\n * recovery (#1691) can re-associate reconstructed chunks with the correct\n * message even when the provider stream carries no `start.messageId`. Null on\n * legacy rows written before this column existed.\n */\n message_id: string | null;\n /**\n * Whether this stream is a continuation (appends to the last assistant\n * message rather than starting a new one). Live broadcast frames carry\n * `continuation: true`, and replay frames must too (#1733): without it a\n * reconnecting client treats a replayed continuation as a fresh message\n * and drops the parts streamed before the continuation. SQLite has no\n * boolean type — 1/0/null (legacy rows predating the column).\n */\n is_continuation: number | null;\n};\n\n/**\n * Minimal SQL interface matching Agent's this.sql tagged template.\n * Allows ResumableStream to work with the Agent's SQLite without\n * depending on the full Agent class.\n */\nexport type SqlTaggedTemplate = {\n <T = Record<string, unknown>>(\n strings: TemplateStringsArray,\n ...values: (string | number | boolean | null)[]\n ): T[];\n};\n\nexport class ResumableStream {\n private _activeStreamId: string | null = null;\n private _activeRequestId: string | null = null;\n /** Monotonic row-ordering index; one increment per flushed segment row. */\n private _segmentIndex = 0;\n\n /**\n * Whether the active stream was started in this instance (true) or\n * restored from SQLite after hibernation/restart (false). An orphaned\n * stream has no live LLM reader — the ReadableStream was lost when the\n * DO was evicted.\n */\n private _isLive = false;\n\n /**\n * Whether the active stream is a continuation. Mirrors the durable\n * `is_continuation` column so replay frames can carry the flag without a\n * per-replay query; restored from SQLite after hibernation in restore().\n */\n private _activeIsContinuation = false;\n\n private _chunkBuffer: Array<{ streamId: string; body: string }> = [];\n private _chunkBufferBytes = 0;\n private _isFlushingChunks = false;\n private _lastCleanupTime = 0;\n\n constructor(private sql: SqlTaggedTemplate) {\n // Create tables for stream chunks and metadata\n this.sql`create table if not exists cf_ai_chat_stream_chunks (\n id text primary key,\n stream_id text not null,\n body text not null,\n chunk_index integer not null,\n created_at integer not null\n )`;\n\n this.sql`create table if not exists cf_ai_chat_stream_metadata (\n id text primary key,\n request_id text not null,\n status text not null,\n created_at integer not null,\n completed_at integer,\n message_id text,\n is_continuation integer\n )`;\n\n this.sql`create index if not exists idx_stream_chunks_stream_id \n on cf_ai_chat_stream_chunks(stream_id, chunk_index)`;\n\n // Restore any active stream from a previous session\n this.restore();\n }\n\n /**\n * Add metadata columns for rows created before they existed. Constructors\n * intentionally do not run this: most wakes never start a stream, so paying a\n * schema-introspection read every time is wasteful. New tables include these\n * columns in CREATE TABLE; legacy tables migrate lazily only if a write/read\n * discovers the columns are missing.\n */\n private _migrateMetadataColumns() {\n const columns =\n this.sql<{ name: string }>`\n select name from pragma_table_info('cf_ai_chat_stream_metadata')\n ` ?? [];\n const hasMessageId = columns.some((column) => column.name === \"message_id\");\n if (!hasMessageId) {\n this\n .sql`alter table cf_ai_chat_stream_metadata add column message_id text`;\n }\n const hasIsContinuation = columns.some(\n (column) => column.name === \"is_continuation\"\n );\n if (!hasIsContinuation) {\n this\n .sql`alter table cf_ai_chat_stream_metadata add column is_continuation integer`;\n }\n }\n\n // ── State accessors ────────────────────────────────────────────────\n\n get activeStreamId(): string | null {\n return this._activeStreamId;\n }\n\n get activeRequestId(): string | null {\n return this._activeRequestId;\n }\n\n hasActiveStream(): boolean {\n return this._activeStreamId !== null;\n }\n\n /**\n * Whether the active stream has a live LLM reader (started in this\n * instance) vs being restored from SQLite after hibernation (orphaned).\n */\n get isLive(): boolean {\n return this._isLive;\n }\n\n // ── Stream lifecycle ───────────────────────────────────────────────\n\n /**\n * Start tracking a new stream for resumable streaming.\n * Creates metadata entry in SQLite and sets up tracking state.\n * @param requestId - The unique ID of the chat request\n * @returns The generated stream ID\n */\n start(\n requestId: string,\n options: { messageId?: string; continuation?: boolean } = {}\n ): string {\n // Flush any pending chunks from previous streams to prevent mixing\n this.flushBuffer();\n\n const streamId = nanoid();\n this._activeStreamId = streamId;\n this._activeRequestId = requestId;\n this._segmentIndex = 0;\n this._isLive = true;\n this._activeIsContinuation = options.continuation ?? false;\n\n const messageId = options.messageId ?? null;\n\n try {\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at, message_id, is_continuation)\n values (${streamId}, ${requestId}, 'streaming', ${Date.now()}, ${messageId}, ${this._activeIsContinuation ? 1 : 0})\n `;\n } catch (error) {\n if (!isMissingMetadataColumnError(error)) throw error;\n this._migrateMetadataColumns();\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at, message_id, is_continuation)\n values (${streamId}, ${requestId}, 'streaming', ${Date.now()}, ${messageId}, ${this._activeIsContinuation ? 1 : 0})\n `;\n }\n\n return streamId;\n }\n\n /**\n * The assistant message id an orphaned stream was producing — the same id the\n * live path persists under, so recovery re-associates reconstructed chunks\n * with the correct message (#1691). Returns null when the row is missing or\n * is a legacy row written before the `message_id` column existed.\n */\n getStreamMessageId(streamId: string): string | null {\n let rows: Array<{ message_id: string | null }>;\n try {\n rows = this.sql<{ message_id: string | null }>`\n select message_id from cf_ai_chat_stream_metadata\n where id = ${streamId}\n `;\n } catch (error) {\n if (!isMissingMetadataColumnError(error)) throw error;\n return null;\n }\n if (!rows || rows.length === 0) return null;\n return rows[0].message_id ?? null;\n }\n\n /**\n * Mark a stream as completed and flush any pending chunks.\n * @param streamId - The stream to mark as completed\n */\n complete(streamId: string) {\n this.flushBuffer();\n\n this.sql`\n update cf_ai_chat_stream_metadata \n set status = 'completed', completed_at = ${Date.now()} \n where id = ${streamId}\n `;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n this._isLive = false;\n this._activeIsContinuation = false;\n\n // Periodically clean up old streams\n this._maybeCleanupOldStreams();\n }\n\n /**\n * Mark a stream as errored and clean up state.\n * @param streamId - The stream to mark as errored\n */\n markError(streamId: string) {\n this.flushBuffer();\n\n this.sql`\n update cf_ai_chat_stream_metadata \n set status = 'error', completed_at = ${Date.now()} \n where id = ${streamId}\n `;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n this._isLive = false;\n this._activeIsContinuation = false;\n }\n\n // ── Chunk storage ──────────────────────────────────────────────────\n\n /** Maximum chunk body size before skipping storage (bytes). Prevents SQLite row limit crash. */\n private static CHUNK_MAX_BYTES = 1_800_000;\n\n /**\n * Buffer a stream chunk for batch write to SQLite.\n * Chunks exceeding the row size limit are skipped to prevent crashes.\n * The chunk is still broadcast to live clients (caller handles that),\n * but will be missing from replay on reconnection.\n * @param streamId - The stream this chunk belongs to\n * @param body - The serialized chunk body\n */\n storeChunk(streamId: string, body: string) {\n // Guard against chunks that would exceed SQLite row limit.\n // The chunk is still broadcast to live clients; only replay storage is skipped.\n const bodyBytes = textEncoder.encode(body).byteLength;\n if (bodyBytes > ResumableStream.CHUNK_MAX_BYTES) {\n console.warn(\n `[ResumableStream] Skipping oversized chunk (${bodyBytes} bytes) ` +\n `to prevent SQLite row limit crash. Live clients still receive it.`\n );\n return;\n }\n\n // Force flush if buffer is at max to prevent memory issues\n if (this._chunkBuffer.length >= CHUNK_BUFFER_MAX_SIZE) {\n this.flushBuffer();\n }\n\n // Byte guard: keep a packed segment safely under the SQLite row limit. If\n // the buffer already holds chunks and adding this body would push the\n // segment past the threshold, flush first so this chunk starts a fresh\n // segment. A single large chunk therefore ends up alone and is written\n // unwrapped by flushBuffer (no array-escaping inflation).\n if (\n this._chunkBuffer.length > 0 &&\n this._chunkBufferBytes + bodyBytes > SEGMENT_MAX_BYTES\n ) {\n this.flushBuffer();\n }\n\n this._chunkBuffer.push({ streamId, body });\n this._chunkBufferBytes += bodyBytes;\n\n // Flush when buffer reaches the per-segment chunk threshold\n if (this._chunkBuffer.length >= CHUNK_BUFFER_SIZE) {\n this.flushBuffer();\n }\n }\n\n /**\n * Flush the buffered chunks to SQLite as a single packed row.\n * Uses a lock to prevent concurrent flush operations.\n *\n * The whole buffer becomes one row: a single-chunk segment is stored\n * unwrapped (legacy object format) so a large chunk avoids array-escaping\n * inflation, while a multi-chunk segment stores a JSON array of bodies. This\n * collapses N chunk rows into one, cutting rows written / stored / scanned.\n */\n flushBuffer() {\n if (this._isFlushingChunks || this._chunkBuffer.length === 0) {\n return;\n }\n\n this._isFlushingChunks = true;\n try {\n const chunks = this._chunkBuffer;\n this._chunkBuffer = [];\n this._chunkBufferBytes = 0;\n\n // All chunks in a buffer belong to the same stream: start() flushes\n // before switching streams, so the buffer is never cross-stream.\n const streamId = chunks[0].streamId;\n const segmentBody =\n chunks.length === 1\n ? chunks[0].body\n : JSON.stringify(chunks.map((chunk) => chunk.body));\n\n this.sql`\n insert into cf_ai_chat_stream_chunks (id, stream_id, body, chunk_index, created_at)\n values (${nanoid()}, ${streamId}, ${segmentBody}, ${this._segmentIndex}, ${Date.now()})\n `;\n this._segmentIndex++;\n } finally {\n this._isFlushingChunks = false;\n }\n }\n\n // ── Chunk replay ───────────────────────────────────────────────────\n\n /**\n * Send stored stream chunks to a connection for replay.\n * Chunks are marked with replay: true so the client can batch-apply them.\n *\n * Three outcomes:\n * - **Live stream**: sends chunks + `replayComplete` — client flushes and\n * continues receiving live chunks from the LLM reader.\n * - **Orphaned stream** (restored from SQLite after hibernation, no reader):\n * sends chunks + `done` and completes the stream. The caller should\n * reconstruct and persist the partial message from the stored chunks.\n * - **Completed during replay** (defensive): sends chunks + `done`.\n *\n * All sends use {@link sendIfOpen}, so a WebSocket closing mid-replay\n * does not throw. If the connection drops while iterating chunks the\n * stream is left active so the next reconnect can retry.\n *\n * @param connection - The WebSocket connection\n * @param requestId - The original request ID\n * @returns The stream ID if the stream was orphaned and finalized, null otherwise.\n * When non-null the caller should reconstruct the message from chunks.\n */\n replayChunks(connection: Connection, requestId: string): string | null {\n const streamId = this._activeStreamId;\n if (!streamId) return null;\n\n this.flushBuffer();\n\n // Replay frames must mirror what a live client observed — including the\n // continuation flag (#1733): a replayed continuation `start` that lacks\n // it would be treated as a fresh message by the client and drop the\n // parts streamed before the continuation.\n const continuation = this._activeIsContinuation;\n\n const chunks = this.sql<StreamChunk>`\n select * from cf_ai_chat_stream_chunks \n where stream_id = ${streamId} \n order by chunk_index asc\n `;\n\n for (const chunk of chunks || []) {\n for (const body of unpackSegmentBody(chunk.body)) {\n if (\n !sendIfOpen(\n connection,\n JSON.stringify({\n body,\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n )\n ) {\n // Connection closed mid-replay — leave the stream active so the\n // next reconnect can retry from the start.\n return null;\n }\n }\n }\n\n if (this._activeStreamId !== streamId) {\n // Stream completed between our check above and now — send done.\n // In practice this cannot happen (DO is single-threaded and replay is\n // synchronous), but we guard defensively in case the flow changes.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n );\n return null;\n }\n\n if (!this._isLive) {\n // Orphaned stream — restored from SQLite after hibernation but the\n // LLM ReadableStream reader was lost. No more live chunks will ever\n // arrive, so finalize it: best-effort send done, then mark completed\n // in SQLite. The orphan-cleanup decision is committed regardless of\n // whether this particular connection received the done frame, so the\n // caller can persist the reconstructed message.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n );\n this.complete(streamId);\n return streamId;\n }\n\n // Stream is still active with a live reader — signal that replay is\n // complete so the client can flush accumulated parts to React state.\n // Without this, replayed chunks sit in activeStreamRef unflushed\n // until the next live chunk arrives.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n replayComplete: true,\n ...(continuation && { continuation: true })\n })\n );\n return null;\n }\n\n replayCompletedChunksByRequestId(\n connection: Connection,\n requestId: string\n ): boolean {\n const stream = this._latestStreamForRequest(requestId, \"completed\");\n if (!stream) return false;\n\n const continuation = stream.is_continuation === 1;\n if (\n !this._replayStoredChunks(connection, stream.id, requestId, continuation)\n ) {\n return false;\n }\n\n return sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n );\n }\n\n /**\n * Replay the stored chunks of an errored stream for a request, WITHOUT a\n * terminal frame — the caller follows up with the `done: true, error: true`\n * frame carrying the durable terminal record's error text, mirroring what a\n * live client observed (content chunks, then the error). Without this, a\n * client that missed broadcast frames while disconnected has no other\n * channel to the pre-error partial content: the server does not push\n * messages on connect, and {@link replayCompletedChunksByRequestId} only\n * serves `completed` streams (#1575).\n *\n * Returns true when the caller should proceed to send its terminal frame:\n * either no errored stream existed (nothing to replay) or its chunks were\n * replayed successfully. Returns false only when a send failed mid-replay,\n * signalling the caller to skip the terminal frame — the connection is gone\n * and the next reconnect retries the whole sequence.\n */\n replayErroredChunksByRequestId(\n connection: Connection,\n requestId: string\n ): boolean {\n const stream = this._latestStreamForRequest(requestId, \"error\");\n if (!stream) return true;\n\n return this._replayStoredChunks(\n connection,\n stream.id,\n requestId,\n stream.is_continuation === 1\n );\n }\n\n /** Latest stream row for a request with the given terminal status. */\n private _latestStreamForRequest(\n requestId: string,\n status: \"completed\" | \"error\"\n ): StreamMetadata | undefined {\n this.flushBuffer();\n\n const streams = this.sql<StreamMetadata>`\n select * from cf_ai_chat_stream_metadata\n where request_id = ${requestId}\n and status = ${status}\n order by created_at desc\n limit 1\n `;\n return streams[0];\n }\n\n /**\n * Send a finished stream's stored chunks to a connection as replay frames.\n * Returns false if the connection closed mid-replay.\n */\n private _replayStoredChunks(\n connection: Connection,\n streamId: string,\n requestId: string,\n continuation = false\n ): boolean {\n const chunks = this.sql<StreamChunk>`\n select * from cf_ai_chat_stream_chunks\n where stream_id = ${streamId}\n order by chunk_index asc\n `;\n\n for (const chunk of chunks || []) {\n for (const body of unpackSegmentBody(chunk.body)) {\n if (\n !sendIfOpen(\n connection,\n JSON.stringify({\n body,\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n )\n ) {\n return false;\n }\n }\n }\n\n return true;\n }\n\n // ── Restore / cleanup ──────────────────────────────────────────────\n\n /**\n * Restore active stream state if the agent was restarted during streaming.\n * All streams are restored regardless of age — stale cleanup happens\n * lazily in _maybeCleanupOldStreams after recovery has had its chance.\n */\n restore() {\n const activeStreams = this.sql<StreamMetadata>`\n select * from cf_ai_chat_stream_metadata \n where status = 'streaming' \n order by created_at desc \n limit 1\n `;\n\n if (activeStreams && activeStreams.length > 0) {\n const stream = activeStreams[0];\n this._activeStreamId = stream.id;\n this._activeRequestId = stream.request_id;\n // Rehydrate the continuation flag so an orphaned continuation stream\n // replayed after hibernation still carries `continuation: true` on\n // its frames (#1733). Legacy rows predate the column → null → false.\n this._activeIsContinuation = stream.is_continuation === 1;\n\n // Resume the segment row-ordering index past the highest stored value.\n const lastChunk = this.sql<{ max_index: number }>`\n select max(chunk_index) as max_index \n from cf_ai_chat_stream_chunks \n where stream_id = ${this._activeStreamId}\n `;\n this._segmentIndex =\n lastChunk && lastChunk[0]?.max_index != null\n ? lastChunk[0].max_index + 1\n : 0;\n }\n }\n\n /**\n * Clear all stream data (called on chat history clear).\n */\n clearAll() {\n this._chunkBuffer = [];\n this._chunkBufferBytes = 0;\n this.sql`delete from cf_ai_chat_stream_chunks`;\n this.sql`delete from cf_ai_chat_stream_metadata`;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n this._activeIsContinuation = false;\n }\n\n /**\n * Drop all stream tables (called on destroy).\n */\n destroy() {\n this.flushBuffer();\n this.sql`drop table if exists cf_ai_chat_stream_chunks`;\n this.sql`drop table if exists cf_ai_chat_stream_metadata`;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._activeIsContinuation = false;\n }\n\n /**\n * Force a sweep of aged stream buffers now, bypassing the lazy interval\n * gate used by {@link _maybeCleanupOldStreams}. Intended to be driven by an\n * alarm so idle/hibernated chat DOs still reclaim buffers even when no\n * further stream ever completes to trigger the lazy path.\n */\n cleanup(now: number = Date.now()): void {\n this._lastCleanupTime = now;\n this._sweepOldStreams(now);\n }\n\n /**\n * True if any stream rows remain at all. Used by alarm-driven cleanup to\n * decide whether to re-arm: once no rows remain there is nothing left to\n * sweep, so the DO can stop waking itself.\n */\n hasReclaimableStreams(): boolean {\n const rows = this.sql<{ n: number }>`\n select count(*) as n from cf_ai_chat_stream_metadata\n `;\n return (rows?.[0]?.n ?? 0) > 0;\n }\n\n // ── Internal ───────────────────────────────────────────────────────\n\n private _maybeCleanupOldStreams() {\n const now = Date.now();\n if (now - this._lastCleanupTime < CLEANUP_INTERVAL_MS) {\n return;\n }\n this._lastCleanupTime = now;\n this._sweepOldStreams(now);\n }\n\n /** Delete completed/errored buffers past the completion grace window, plus\n * abandoned \"streaming\" rows past the stale-in-flight window. The two use\n * different retentions: a completed buffer is redundant with the persisted\n * message and needs only a brief replay grace, whereas an in-flight buffer\n * must outlive resume/recovery before it is presumed dead. */\n private _sweepOldStreams(now: number) {\n const completedCutoff = now - COMPLETED_RETENTION_MS;\n this.sql`\n delete from cf_ai_chat_stream_chunks \n where stream_id in (\n select id from cf_ai_chat_stream_metadata \n where status in ('completed', 'error') and completed_at < ${completedCutoff}\n )\n `;\n this.sql`\n delete from cf_ai_chat_stream_metadata \n where status in ('completed', 'error') and completed_at < ${completedCutoff}\n `;\n\n // Clean up abandoned \"streaming\" rows. These are orphaned streams that\n // were never completed or recovered (e.g. non-durable agents that never\n // reconnected). By this point, fiber recovery has already had its chance\n // to claim them — safe to delete.\n //\n // \"Abandoned\" is keyed off LAST ACTIVITY (the most recent chunk write),\n // not the stream's start time: a long-running stream that is still\n // actively emitting chunks must never be swept mid-flight just because it\n // started long ago. A row with no chunks falls back to its start time.\n // Note `created_at <= max(chunk.created_at)` always (the row is inserted\n // before any chunk), so this set is stable across the two deletes even\n // though the first removes the chunks the second's subquery reads.\n const abandonedCutoff = now - ABANDONED_STREAM_RETENTION_MS;\n this.sql`\n delete from cf_ai_chat_stream_chunks\n where stream_id in (\n select m.id from cf_ai_chat_stream_metadata m\n where m.status = 'streaming'\n and coalesce(\n (select max(c.created_at) from cf_ai_chat_stream_chunks c\n where c.stream_id = m.id),\n m.created_at\n ) < ${abandonedCutoff}\n )\n `;\n this.sql`\n delete from cf_ai_chat_stream_metadata\n where id in (\n select m.id from cf_ai_chat_stream_metadata m\n where m.status = 'streaming'\n and coalesce(\n (select max(c.created_at) from cf_ai_chat_stream_chunks c\n where c.stream_id = m.id),\n m.created_at\n ) < ${abandonedCutoff}\n )\n `;\n }\n\n // ── Test helpers (matching old AIChatAgent test API) ────────────────\n\n /**\n * Return the stored chunks for a stream as individual chunk bodies in order,\n * unpacking packed segment rows. The returned `chunk_index` is a running\n * per-chunk sequence (0, 1, 2, …) — stable across calls because rows are\n * append-only — so callers can use it as a monotonic chunk sequence.\n */\n getStreamChunks(\n streamId: string\n ): Array<{ body: string; chunk_index: number }> {\n const rows =\n this.sql<{ body: string }>`\n select body from cf_ai_chat_stream_chunks \n where stream_id = ${streamId} \n order by chunk_index asc\n ` || [];\n const out: Array<{ body: string; chunk_index: number }> = [];\n let index = 0;\n for (const row of rows) {\n for (const body of unpackSegmentBody(row.body)) {\n out.push({ body, chunk_index: index });\n index++;\n }\n }\n return out;\n }\n\n /** @internal For testing only */\n getStreamMetadata(\n streamId: string\n ): { status: string; request_id: string } | null {\n const result = this.sql<{ status: string; request_id: string }>`\n select status, request_id from cf_ai_chat_stream_metadata \n where id = ${streamId}\n `;\n return result && result.length > 0 ? result[0] : null;\n }\n\n /** @internal For testing only */\n getAllStreamMetadata(): Array<{\n id: string;\n status: string;\n request_id: string;\n created_at: number;\n }> {\n return (\n this.sql<{\n id: string;\n status: string;\n request_id: string;\n created_at: number;\n }>`select id, status, request_id, created_at from cf_ai_chat_stream_metadata` ||\n []\n );\n }\n\n /** @internal For testing only */\n insertStaleStream(streamId: string, requestId: string, ageMs: number): void {\n const createdAt = Date.now() - ageMs;\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at)\n values (${streamId}, ${requestId}, 'streaming', ${createdAt})\n `;\n }\n\n /**\n * Append a chunk to a stream dated `ageMs` in the past. Used to exercise the\n * last-activity sweep threshold: a long-running streaming row with a *recent*\n * chunk must survive even when its start time is older than the cutoff.\n * @internal For testing only\n */\n insertChunkAt(streamId: string, body: string, ageMs: number): void {\n const createdAt = Date.now() - ageMs;\n this.sql`\n insert into cf_ai_chat_stream_chunks (id, stream_id, body, chunk_index, created_at)\n values (${nanoid()}, ${streamId}, ${body}, 0, ${createdAt})\n `;\n }\n}\n\n/**\n * The buffer-cleanup alarm body: sweep aged stream buffers, then re-arm only\n * while rows remain so a fully-swept DO stops waking itself. `rearm` schedules\n * the next sweep — it MUST schedule a non-idempotent alarm, because this runs\n * INSIDE the currently-executing one-shot schedule row, which `alarm()` deletes\n * only after it returns; an idempotent reschedule would dedup onto that row and\n * be deleted with it, so the re-arm would silently never fire and buffers that\n * survived this sweep (e.g. a younger turn) would go uncollected. A fresh\n * delayed row survives the deletion. Shared by `AIChatAgent` and `Think`.\n *\n * `@internal`\n */\nexport async function cleanupStreamBuffers(\n stream: Pick<ResumableStream, \"cleanup\" | \"hasReclaimableStreams\">,\n rearm: () => Promise<void>\n): Promise<void> {\n stream.cleanup();\n if (stream.hasReclaimableStreams()) {\n await rearm();\n }\n}\n","/**\n * Helpers for building batched SQLite statements that run through the Agent's\n * `sql` tagged template (which interleaves a `?` placeholder between every\n * string fragment). Used to collapse per-row INSERT/DELETE loops into a small\n * number of multi-row statements.\n *\n * SQLite (Durable Object / D1) caps bound parameters at 100 per query, so\n * callers must chunk their inputs to stay within {@link MAX_BOUND_PARAMS}.\n * See https://developers.cloudflare.com/d1/platform/limits/\n */\n\n/** Maximum bound parameters allowed in a single SQLite (DO / D1) query. */\nexport const MAX_BOUND_PARAMS = 100;\n\n/**\n * Attach a self-referential `raw` property so a plain string[] satisfies the\n * TemplateStringsArray shape. `sql` only reads indexed string fragments, so\n * `raw` is never consumed — this just keeps the type system happy.\n */\nfunction asTemplateStringsArray(parts: string[]): TemplateStringsArray {\n (parts as unknown as { raw: readonly string[] }).raw = parts;\n return parts as unknown as TemplateStringsArray;\n}\n\n/**\n * Build a TemplateStringsArray for a single-column `IN (...)` clause. Produces\n * fragments for:\n * `${prefix}(?, ?, ...)`\n *\n * @throws if `count` is less than 1.\n */\nexport function buildInClauseStrings(\n prefix: string,\n count: number\n): TemplateStringsArray {\n if (count < 1) {\n throw new Error(`buildInClauseStrings requires count >= 1 (got ${count})`);\n }\n const parts = new Array<string>(count + 1);\n parts[0] = `${prefix}(`;\n for (let i = 1; i < count; i++) {\n parts[i] = \", \";\n }\n parts[count] = \")\";\n return asTemplateStringsArray(parts);\n}\n","/**\n * Client tool schema handling for the cf_agent_chat protocol.\n *\n * Converts client-provided tool schemas (JSON wire format) into AI SDK\n * tool definitions. By default these tools have no `execute` function —\n * when the model calls them, the tool call is sent back to the client.\n *\n * When an `execute` delegate is supplied (e.g. a parent agent that drives a\n * Think sub-agent over RPC and can run the client tools itself), the tools are\n * built WITH an `execute` so the model's call is resolved inline within the\n * same turn instead of being surfaced as a dangling tool call.\n *\n * Used by both @cloudflare/ai-chat and @cloudflare/think.\n */\n\nimport type { JSONSchema7, Tool, ToolSet } from \"ai\";\nimport { tool, jsonSchema } from \"ai\";\n\n/**\n * Wire-format tool schema sent from the client.\n * Uses `parameters` (JSONSchema7) rather than AI SDK's `inputSchema`\n * because Zod schemas cannot be serialized over the wire.\n */\nexport type ClientToolSchema = {\n /** Unique name for the tool */\n name: string;\n /** Human-readable description of what the tool does */\n description?: Tool[\"description\"];\n /** JSON Schema defining the tool's input parameters */\n parameters?: JSONSchema7;\n};\n\n/**\n * Executes a client-defined tool and returns its output.\n *\n * Used for the RPC path (e.g. a parent agent delegating to a Think sub-agent)\n * where the caller can run the client tools itself, rather than the\n * browser/WebSocket path where results are sent back asynchronously.\n */\nexport type ClientToolExecutor = (call: {\n /** The name of the client tool the model called. */\n toolName: string;\n /** The model-generated input for the tool call. */\n input: unknown;\n /** The AI SDK tool-call id for the invocation. */\n toolCallId: string;\n}) => unknown | Promise<unknown>;\n\n/**\n * Converts client tool schemas to AI SDK tool format.\n *\n * By default these tools have no `execute` function — when the AI model calls\n * them, the tool call is sent back to the client for execution.\n *\n * When `options.execute` is provided, each tool is built WITH an `execute` that\n * delegates to it. This is used by the RPC path (e.g. a parent agent driving a\n * Think sub-agent) so the model's client-tool call is resolved inline within\n * the same turn.\n *\n * @param clientTools - Array of tool schemas from the client\n * @param options - Optional `execute` delegate to run the tools inline\n * @returns Record of AI SDK tools that can be spread into your tools object\n */\nexport function createToolsFromClientSchemas(\n clientTools?: ClientToolSchema[],\n options?: { execute?: ClientToolExecutor }\n): ToolSet {\n if (!clientTools || clientTools.length === 0) {\n return {};\n }\n\n const seenNames = new Set<string>();\n for (const t of clientTools) {\n if (seenNames.has(t.name)) {\n console.warn(\n `[createToolsFromClientSchemas] Duplicate tool name \"${t.name}\" found. Later definitions will override earlier ones.`\n );\n }\n seenNames.add(t.name);\n }\n\n const execute = options?.execute;\n // The AI SDK's `tool()` overloads infer the input type as `never` when an\n // `execute` is combined with a runtime `jsonSchema(...)`. Cast to a\n // permissive signature (same approach as `agentTool()`), since the wire\n // schema is untyped JSON.\n const createTool = tool as unknown as (config: {\n description: string;\n inputSchema: ReturnType<typeof jsonSchema>;\n execute?: (\n input: unknown,\n executeOptions?: { toolCallId?: string }\n ) => unknown | Promise<unknown>;\n }) => Tool;\n\n return Object.fromEntries(\n clientTools.map((t) => [\n t.name,\n createTool({\n description: t.description ?? \"\",\n inputSchema: jsonSchema(t.parameters ?? { type: \"object\" }),\n ...(execute\n ? {\n execute: (\n input: unknown,\n executeOptions?: { toolCallId?: string }\n ) =>\n execute({\n toolName: t.name,\n input,\n toolCallId: executeOptions?.toolCallId ?? \"\"\n })\n }\n : {})\n })\n ])\n );\n}\n","/**\n * ContinuationState — shared state container for auto-continuation lifecycle.\n *\n * Tracks pending, deferred, and active continuation state for the\n * tool-result → auto-continue flow. Both AIChatAgent and Think use this\n * to manage which connection/tools/body a continuation turn should use\n * and to coordinate with clients requesting stream resume.\n *\n * The scheduling algorithm (prerequisite chaining, debounce, TurnQueue\n * enrollment) stays in the host — this class only manages the data.\n */\n\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\nimport type { ClientToolSchema } from \"./client-tools\";\nimport { sendIfOpen, type ChatConnection } from \"./connection\";\n\nconst MSG_STREAM_RESUME_NONE = CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE;\n\n/**\n * Minimal connection interface for sending WebSocket messages. Alias of the\n * shared {@link ChatConnection} — kept as a named export for back-compat with\n * existing `ContinuationConnection` consumers.\n */\nexport type ContinuationConnection = ChatConnection;\n\nexport interface ContinuationPending<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n connectionId: string | null;\n requestId: string;\n clientTools?: ClientToolSchema[];\n body?: Record<string, unknown>;\n errorPrefix: string | null;\n prerequisite: Promise<boolean> | null;\n pastCoalesce: boolean;\n}\n\nexport interface ContinuationDeferred<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n connectionId: string | null;\n clientTools?: ClientToolSchema[];\n body?: Record<string, unknown>;\n errorPrefix: string;\n prerequisite: Promise<boolean> | null;\n}\n\nexport class ContinuationState<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n pending: ContinuationPending<TConnection> | null = null;\n deferred: ContinuationDeferred<TConnection> | null = null;\n activeRequestId: string | null = null;\n activeConnectionId: string | null = null;\n awaitingConnections: Map<string, TConnection> = new Map();\n\n /** Clear pending state and awaiting connections (without sending RESUME_NONE). */\n clearPending(): void {\n this.pending = null;\n this.awaitingConnections.clear();\n }\n\n clearDeferred(): void {\n this.deferred = null;\n }\n\n clearAll(): void {\n this.clearPending();\n this.clearDeferred();\n this.activeRequestId = null;\n this.activeConnectionId = null;\n }\n\n /**\n * Mark a connection as no longer available without canceling the\n * continuation it initiated.\n */\n releaseConnection(connectionId: string): void {\n this.awaitingConnections.delete(connectionId);\n if (this.pending?.connectionId === connectionId) {\n this.pending = { ...this.pending, connectionId: null };\n }\n if (this.deferred?.connectionId === connectionId) {\n this.deferred = { ...this.deferred, connectionId: null };\n }\n if (this.activeConnectionId === connectionId) {\n this.activeConnectionId = null;\n }\n }\n\n /**\n * Send STREAM_RESUME_NONE to all connections waiting for a\n * continuation stream to start, then clear the map.\n */\n sendResumeNone(): void {\n const msg = JSON.stringify({ type: MSG_STREAM_RESUME_NONE });\n for (const connection of this.awaitingConnections.values()) {\n sendIfOpen(connection, msg);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Flush awaiting connections by notifying each one via the provided\n * callback (typically sends STREAM_RESUMING), then clear.\n */\n flushAwaitingConnections(notify: (conn: TConnection) => void): void {\n for (const connection of this.awaitingConnections.values()) {\n notify(connection);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Transition pending → active. Called when the continuation stream\n * actually starts. Moves request/connection IDs to active slots,\n * clears pending fields.\n */\n activatePending(): void {\n if (!this.pending) return;\n this.activeRequestId = this.pending.requestId;\n this.activeConnectionId = this.pending.connectionId;\n this.pending = null;\n }\n\n /**\n * Transition deferred → pending. Called when a continuation turn\n * completes and there's a deferred follow-up waiting.\n *\n * Returns the new pending state (so the host can enqueue the turn),\n * or null if there was nothing deferred.\n */\n activateDeferred(\n generateRequestId: () => string\n ): ContinuationPending<TConnection> | null {\n if (this.pending || !this.deferred) return null;\n\n const d = this.deferred;\n this.deferred = null;\n this.activeRequestId = null;\n this.activeConnectionId = null;\n\n this.pending = {\n connection: d.connection,\n connectionId: d.connectionId,\n requestId: generateRequestId(),\n clientTools: d.clientTools,\n body: d.body,\n errorPrefix: d.errorPrefix,\n prerequisite: d.prerequisite,\n pastCoalesce: false\n };\n\n if (d.connectionId !== null) {\n this.awaitingConnections.set(d.connectionId, d.connection);\n }\n return this.pending;\n }\n}\n","/**\n * PreStreamTurns — tracks accepted chat turns that have not yet started a\n * resumable stream, and the connections parked waiting for one.\n *\n * The resume handshake can resume an ACTIVE stream (chunks buffering) and can\n * replay a TERMINAL outcome, but a normal turn spends a window between \"request\n * accepted\" and \"first chunk produced\" in neither state: it is queued, waiting\n * on `waitForMcpConnections`, debouncing, or simply running async setup inside\n * `onChatMessage` before a stream object exists. A client that reconnects or\n * re-mounts in that window used to get `cf_agent_stream_resume_none` and give\n * up, so the turn the server went on to complete normally never drove the\n * client's AI-SDK `status` (issue #1784).\n *\n * This container lets a host represent that window as resumable: the handshake\n * parks the reconnecting connection here (and tells it to keep waiting), and the\n * host flushes the parked connections into the normal `STREAM_RESUMING` path the\n * moment a stream actually starts — or releases them with `resume_none` if the\n * turn settles without ever streaming.\n *\n * Concurrency-safe under queued turns via an accepted-request set: parked\n * connections are only released with `resume_none` once EVERY accepted turn has\n * settled with no active stream, so a client parked during the gap between one\n * turn finishing and the next starting still resumes onto the next stream.\n *\n * Pure data + send-through-callback, mirroring {@link ContinuationState}: the\n * host owns the actual frame sends and the stream-start / turn-settle wiring.\n *\n * @internal Sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API.\n */\n\nimport { sendIfOpen, type ChatConnection } from \"./connection\";\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\n\nconst MSG_STREAM_PENDING = CHAT_MESSAGE_TYPES.STREAM_PENDING;\nconst MSG_STREAM_RESUME_NONE = CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE;\n\nexport class PreStreamTurns<\n TConnection extends ChatConnection = ChatConnection\n> {\n /**\n * Accepted-but-not-yet-streamed request ids. A turn enters on `begin()` and\n * leaves on `settle()`; the set being non-empty means \"pre-stream work is in\n * flight\", which gates parking and the eventual `resume_none` release.\n */\n private readonly _accepted = new Set<string>();\n\n /** Connections parked waiting for a stream to start. */\n readonly awaitingConnections = new Map<string, TConnection>();\n\n /** The most recently accepted pre-stream request id (for the keep-waiting frame). */\n private _latestRequestId: string | null = null;\n\n /** Mark a freshly-accepted turn as in flight (pre-stream). */\n begin(requestId: string): void {\n this._accepted.add(requestId);\n this._latestRequestId = requestId;\n }\n\n /**\n * Mark an accepted turn as settled. Returns `true` when no accepted turn\n * remains in flight (the caller should release parked connections if no\n * stream is active).\n */\n settle(requestId: string): boolean {\n this._accepted.delete(requestId);\n if (this._accepted.size === 0) {\n this._latestRequestId = null;\n return true;\n }\n return false;\n }\n\n /** Whether any accepted turn is still pre-stream. */\n hasInFlight(): boolean {\n return this._accepted.size > 0;\n }\n\n /** The request id to advertise in the keep-waiting frame, if known. */\n get latestRequestId(): string | null {\n return this._latestRequestId;\n }\n\n /**\n * Park a reconnecting connection and tell it to keep waiting (so its\n * transport does not resolve `reconnectToStream` early). No-op when nothing\n * is in flight. Parked connections are deliberately NOT added to the host's\n * `pendingResumeConnections` — they must keep receiving any live broadcast —\n * until the host flushes them through `notifyStreamResuming` on stream start.\n */\n park(connection: TConnection): boolean {\n if (!this.hasInFlight()) return false;\n this.awaitingConnections.set(connection.id, connection);\n sendIfOpen(\n connection,\n JSON.stringify({\n type: MSG_STREAM_PENDING,\n ...(this._latestRequestId ? { id: this._latestRequestId } : {})\n })\n );\n return true;\n }\n\n /** Drop a single connection (e.g. on socket close) without releasing others. */\n release(connectionId: string): void {\n this.awaitingConnections.delete(connectionId);\n }\n\n /**\n * A stream has started: hand every parked connection to `notify` (the host's\n * `notifyStreamResuming`, which sends `STREAM_RESUMING` and excludes the\n * connection from live broadcast until it ACKs), then clear the awaiting map.\n * The accepted set is untouched — the turn is still running.\n */\n flushOnStreamStart(notify: (connection: TConnection) => void): void {\n for (const connection of this.awaitingConnections.values()) {\n notify(connection);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Release every parked connection with `STREAM_RESUME_NONE` (the turn settled\n * without ever starting a stream) and clear the awaiting map. Safe to call\n * when the map is empty (no-op), so the host can call it liberally from a\n * turn-settle path.\n */\n releaseAwaiting(): void {\n const msg = JSON.stringify({ type: MSG_STREAM_RESUME_NONE });\n for (const connection of this.awaitingConnections.values()) {\n sendIfOpen(connection, msg);\n }\n this.awaitingConnections.clear();\n }\n\n /** Drop all state (chat clear / destroy). Does not send any frames. */\n reset(): void {\n this._accepted.clear();\n this.awaitingConnections.clear();\n this._latestRequestId = null;\n }\n}\n","/**\n * AutoContinuationController — shared auto-continuation barrier for the\n * tool-result → auto-continue flow (#1649 / #1650).\n *\n * Both `@cloudflare/ai-chat` (`AIChatAgent`) and `@cloudflare/think` (`Think`)\n * drive an identical event-driven barrier: a tool result/approval that opts in\n * with `autoContinue` schedules a continuation, rapid sibling results coalesce\n * into a single fire via a debounce timer, and the actual fire is gated on a\n * complete parallel tool batch and no active stream. This controller owns that\n * machinery exactly once:\n *\n * - the coalesce/debounce timer ({@link AutoContinuationController.COALESCE_MS}),\n * - the `barrierActive` double-fire guard,\n * - the create/update/defer scheduling branch ({@link schedule}),\n * - the completeness-gated drain orchestration ({@link fireWhenStable}).\n *\n * Everything host-specific is expressed through {@link AutoContinuationHost}: the\n * stream-active signal, the incomplete-batch / pending-interaction predicates,\n * the apply-drain primitive, and the actual continuation turn ({@link\n * AutoContinuationHost.fire}, each host's inference/reply pipeline). The shared\n * {@link ContinuationState} data layer stays on the host and is read/mutated\n * here through {@link AutoContinuationHost.continuation}.\n *\n * @internal Sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API.\n */\n\nimport type { ClientToolSchema } from \"./client-tools\";\nimport type {\n ContinuationConnection,\n ContinuationState\n} from \"./continuation-state\";\n\n/**\n * The data a host supplies to schedule (or re-target) a pending/deferred\n * auto-continuation. Mirrors the fields a host writes onto\n * {@link ContinuationState.pending} — the host owns where the values come from\n * (e.g. Think hardcodes a fixed `errorPrefix` and `body: undefined`; ai-chat\n * threads them per tool-result event).\n */\nexport interface ContinuationSpec<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n clientTools: ClientToolSchema[] | undefined;\n body: Record<string, unknown> | undefined;\n errorPrefix: string;\n}\n\n/**\n * Host substrate the controller parameterizes over. Implemented by the agent\n * (typically via a small adapter object capturing `this`).\n */\nexport interface AutoContinuationHost<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n /** Shared continuation state (pending/deferred/awaiting connections). */\n readonly continuation: ContinuationState<TConnection>;\n /** Generate a request id for a freshly-created continuation turn. */\n generateRequestId(): string;\n /**\n * `true` while an assistant turn is streaming — the parallel tool batch can\n * still grow with tool calls the model hasn't emitted yet, so no completeness\n * check is meaningful. (`_streamingAssistant !== null` in Think;\n * `_streamingTurnActive` in ai-chat.)\n */\n isStreamActive(): boolean;\n /** `true` while a tool-result/approval apply is in flight. */\n hasPendingInteraction(): boolean;\n /**\n * `true` when the latest assistant message is mid-batch (a settled tool\n * result beside an unanswered tool call/approval — the #1649 signature).\n */\n hasIncompleteToolBatch(): boolean;\n /**\n * Drain every in-flight tool-result/approval apply (including any enqueued\n * while draining) so the subsequent completeness re-check sees every result\n * that has already arrived. Bounded by real apply activity, never a timer.\n */\n drainInteractionApplies(): Promise<void>;\n /** Hold the isolate alive for the duration of `fn` (alarm heartbeats). */\n keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>;\n /**\n * Run the continuation turn for the current {@link ContinuationState.pending}.\n * Each host's inference/reply pipeline (Think: `_turnQueue.enqueue` +\n * `_runInferenceLoop`; ai-chat: `_runExclusiveChatTurn` + `onChatMessage`).\n * Reads everything it needs from `continuation.pending`, so it takes no args.\n */\n fire(): void;\n}\n\nexport class AutoContinuationController<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n /**\n * Small debounce window to batch adjacent client-side tool results/approvals\n * into a single server continuation barrier check (#1650).\n */\n static readonly COALESCE_MS = 50;\n\n /**\n * Coalesce/debounce timer for the event-driven barrier (#1650). Each tool\n * result/approval re-arms it; on fire it runs {@link fireWhenStable}.\n */\n private _timer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * Double-fire guard (#1650). Ensures only one in-flight apply-drain runs;\n * that drain re-checks completeness on completion before firing. A sibling\n * that re-arms the coalesce timer during a drain is absorbed by the\n * in-progress drain rather than starting its own.\n */\n private _barrierActive = false;\n\n constructor(private readonly host: AutoContinuationHost<TConnection>) {}\n\n /**\n * Schedule an auto-continuation for a tool result/approval that opted in with\n * `autoContinue` (#1650). Coalesces rapid sibling results into a single\n * continuation via the debounce timer; the actual fire is gated by\n * {@link fireWhenStable}. If a continuation is already running\n * (`pastCoalesce`), the new result is stored as the deferred follow-up\n * instead of re-arming.\n */\n schedule(spec: ContinuationSpec<TConnection>): void {\n const c = this.host.continuation;\n\n if (c.pending?.pastCoalesce) {\n // A continuation is already running; the new result coalesces/defers into\n // the next one rather than re-arming this one.\n c.deferred = {\n connection: spec.connection,\n connectionId: spec.connection.id,\n clientTools: spec.clientTools,\n body: spec.body,\n errorPrefix: spec.errorPrefix,\n prerequisite: null\n };\n return;\n }\n\n if (c.pending) {\n c.pending.connection = spec.connection;\n c.pending.connectionId = spec.connection.id;\n c.pending.clientTools = spec.clientTools;\n c.pending.body = spec.body;\n c.pending.errorPrefix = spec.errorPrefix;\n c.awaitingConnections.set(spec.connection.id, spec.connection);\n this.armTimer();\n return;\n }\n\n c.pending = {\n connection: spec.connection,\n connectionId: spec.connection.id,\n requestId: this.host.generateRequestId(),\n clientTools: spec.clientTools,\n body: spec.body,\n errorPrefix: spec.errorPrefix,\n prerequisite: null,\n pastCoalesce: false\n };\n c.awaitingConnections.set(spec.connection.id, spec.connection);\n this.armTimer();\n }\n\n /**\n * Re-arm the barrier for a result/approval that arrived WITHOUT `autoContinue`\n * (#1650). A standalone errored result declines to continue on its own, but in\n * a parallel batch a SIBLING may already have opted in — and this result can\n * be the one that completes the batch, so we must re-run the barrier check.\n * Unlike {@link schedule} this NEVER creates a pending continuation, and\n * no-ops once the continuation is running (`pastCoalesce`).\n */\n rearmForBatch(): void {\n const pending = this.host.continuation.pending;\n if (!pending || pending.pastCoalesce) return;\n this.armTimer();\n }\n\n /** (Re)arm the coalesce timer; on fire, run {@link fireWhenStable}. */\n armTimer(): void {\n if (this._timer) {\n clearTimeout(this._timer);\n }\n this._timer = setTimeout(() => {\n this._timer = null;\n if (!this.host.continuation.pending) return;\n this.fireWhenStable();\n }, AutoContinuationController.COALESCE_MS);\n }\n\n /**\n * Fire an auto-continuation, but only once the model's parallel tool-call\n * batch is fully answered (#1649) and no assistant turn is mid-stream (#1650).\n * The barrier is event-driven with NO orphan timeout: when the batch is still\n * incomplete we drain the in-flight applies, re-check, and — if still\n * incomplete — return WITHOUT firing and WITHOUT holding the isolate, leaving\n * `continuation.pending` in place. The next sibling's result re-arms the\n * coalesce timer and re-runs this check; the continuation fires once the final\n * sibling lands. A true orphan (a sibling that never arrives) simply never\n * auto-continues — a later user turn / chat recovery repairs the transcript.\n */\n fireWhenStable(): void {\n const c = this.host.continuation;\n if (!c.pending) return;\n // The continuation is already running (a sibling re-armed after it started).\n // New results coalesce/defer into it — don't double-fire.\n if (c.pending.pastCoalesce) return;\n // A drain is already in progress; the sibling that re-armed the timer is\n // absorbed by it. Only one drain runs, and it re-checks on completion.\n if (this._barrierActive) return;\n // Stream-active gate (#1650, #1649): while the model is still streaming the\n // assistant turn we cannot know the parallel batch is complete — a fast\n // client tool can resolve before its slower siblings have even been\n // streamed, so they exist nowhere yet and firing now would repair them to\n // errored. The stream-finalize hook re-runs this check once the stream ends.\n if (this.host.isStreamActive()) return;\n // Fast path: no apply in flight and the leaf step is not mid-batch.\n if (\n !this.host.hasPendingInteraction() &&\n !this.host.hasIncompleteToolBatch()\n ) {\n this.cancelTimer();\n this.host.fire();\n return;\n }\n this._barrierActive = true;\n // keepAlive only for the bounded drain — the duration of the applies that\n // have ALREADY arrived, not an open-ended wait for siblings that haven't.\n this.host\n .keepAliveWhile(() => this.host.drainInteractionApplies())\n .catch(() => {})\n .finally(() => {\n // Clear the flag and re-check synchronously — no `await` between here\n // and the fire/return decision, so a sibling-armed coalesce timer (a\n // macrotask) cannot interleave and double-fire. `cancelTimer()` below\n // kills that timer on the fire path; the incomplete-return path leaves\n // it armed so the sibling that armed it re-runs this check.\n this._barrierActive = false;\n const pending = c.pending;\n if (!pending || pending.pastCoalesce) return;\n // A stream (re)started during the drain — hold; the finalize re-trigger\n // re-checks once the batch is fully materialized.\n if (this.host.isStreamActive()) return;\n // Still waiting on an unanswered sibling — return without firing. The\n // result that completes the batch re-triggers this via its own\n // schedule(); we do not pin the isolate in the interim.\n if (this.host.hasIncompleteToolBatch()) return;\n this.cancelTimer();\n this.host.fire();\n });\n }\n\n /**\n * Transition the deferred follow-up (stored while a continuation was running)\n * to pending and re-run the barrier — its batch may still be incomplete (or a\n * stream active), in which case it parks and re-arms instead of firing blind.\n */\n activateDeferredAndReschedule(): void {\n const pending = this.host.continuation.activateDeferred(() =>\n this.host.generateRequestId()\n );\n if (!pending) return;\n this.fireWhenStable();\n }\n\n /**\n * Cancel any still-armed coalesce timer. Called on the fire path so a sibling\n * result that re-armed it during a barrier wait can't fire a duplicate\n * continuation after this one starts (#1649 / #1650).\n */\n cancelTimer(): void {\n if (this._timer) {\n clearTimeout(this._timer);\n this._timer = null;\n }\n }\n\n /**\n * `true` when the barrier is going to fire on its own — its coalesce timer is\n * still pending or its completeness drain is in progress. The host combines\n * this with its own pending/`pastCoalesce` checks to decide idle/stable.\n */\n isArmed(): boolean {\n return this._timer !== null || this._barrierActive;\n }\n\n /**\n * Tear down the controller-owned barrier state (timer + double-fire guard).\n * Scoped to ONLY this controller's fields — the host clears the rest of its\n * turn state (stream gate, interaction tail, continuation data) separately.\n */\n reset(): void {\n this.cancelTimer();\n this._barrierActive = false;\n }\n}\n","/**\n * AbortRegistry — manages per-request AbortControllers.\n *\n * Shared between AIChatAgent and Think for chat turn cancellation.\n * Each request gets its own AbortController keyed by request ID.\n * Controllers are created lazily on first signal access.\n */\n\nconst NOOP = () => {};\n\nexport class AbortRegistry {\n private controllers = new Map<string, AbortController>();\n\n /**\n * Get or create an AbortController for the given ID and return its signal.\n * Creates the controller lazily on first access.\n */\n getSignal(id: string): AbortSignal | undefined {\n if (typeof id !== \"string\") {\n return undefined;\n }\n\n if (!this.controllers.has(id)) {\n this.controllers.set(id, new AbortController());\n }\n\n return this.controllers.get(id)!.signal;\n }\n\n /**\n * Get the signal for an existing controller without creating one.\n * Returns undefined if no controller exists for this ID.\n */\n getExistingSignal(id: string): AbortSignal | undefined {\n return this.controllers.get(id)?.signal;\n }\n\n /**\n * Cancel a specific request by aborting its controller. Optionally\n * propagate a reason — surfaces as `signal.reason` on the registry's\n * controller and through any `AbortError` it produces downstream.\n */\n cancel(id: string, reason?: unknown): void {\n this.controllers.get(id)?.abort(reason);\n }\n\n /** Remove a controller after the request completes. */\n remove(id: string): void {\n this.controllers.delete(id);\n }\n\n /**\n * Abort all pending requests and clear the registry. Optionally propagate a\n * reason — surfaces as `signal.reason` on each controller and through any\n * `AbortError` it produces downstream, exactly like {@link cancel}.\n */\n destroyAll(reason?: unknown): void {\n for (const controller of this.controllers.values()) {\n controller.abort(reason);\n }\n this.controllers.clear();\n }\n\n /** Check if a controller exists for the given ID. */\n has(id: string): boolean {\n return this.controllers.has(id);\n }\n\n /** Number of tracked controllers. */\n get size(): number {\n return this.controllers.size;\n }\n\n /**\n * Link an external `AbortSignal` to the controller for `id`. When the\n * external signal aborts, the registry's controller is cancelled —\n * propagating the abort reason — exactly the same way an internal\n * cancel would (e.g. via a `chat-request-cancel` WebSocket message).\n *\n * This is the integration point for callers that drive a chat turn\n * programmatically and want to cancel it from outside without knowing\n * the internally-generated request id (e.g. the helper-as-sub-agent\n * pattern, where a parent's `AbortSignal` from the AI SDK tool\n * `execute` needs to land inside a `Think.saveMessages` call running\n * on a child DO).\n *\n * Behavior:\n *\n * - Passing `undefined` is a no-op and returns a no-op detacher, so\n * callers can unconditionally call this with `options?.signal`.\n * - If the external signal is already aborted, the registry's\n * controller is created (if needed) and cancelled synchronously.\n * - Otherwise a one-shot `abort` listener is attached. The returned\n * function detaches it.\n *\n * **Always call the returned detacher in a `finally` block** — the\n * external signal may outlive the request (a parent chat turn that\n * drives many helper turns reuses one signal across all of them) and\n * leaving listeners attached pins closures and grows the listener\n * list on each turn.\n *\n * @returns A detacher function. Call it after the request finishes\n * (success or failure) to remove the abort listener from `signal`.\n */\n linkExternal(id: string, signal: AbortSignal | undefined): () => void {\n if (!signal) return NOOP;\n\n if (signal.aborted) {\n // Ensure the registry controller for `id` exists, then cancel it.\n // Calling getSignal first means an early external abort still\n // produces a controller for downstream observers (`getExistingSignal`)\n // rather than a silently-empty registry.\n this.getSignal(id);\n this.cancel(id, signal.reason);\n return NOOP;\n }\n\n const listener = () => this.cancel(id, signal.reason);\n signal.addEventListener(\"abort\", listener, { once: true });\n return () => signal.removeEventListener(\"abort\", listener);\n }\n}\n","/**\n * @internal Small async control-flow helpers shared by the chat hosts\n * (`@cloudflare/ai-chat` and `@cloudflare/think`) — not a public API. Extracted\n * so the host idle/stable waits and the interaction-apply completeness drain\n * stay byte-identical across both. See `design/chat-shared-layer.md`.\n */\n\n/**\n * Sentinel returned by {@link awaitWithDeadline} when the deadline elapses\n * before the awaited promise settles. A single shared symbol so both hosts\n * compare against the same identity.\n */\nexport const TIMED_OUT = Symbol(\"timed-out\");\n\n/**\n * Await `promise`, but give up and resolve to {@link TIMED_OUT} once `deadline`\n * (an absolute `Date.now()` ms timestamp) passes. A `null` deadline waits\n * indefinitely (the promise is returned unchanged). The timeout timer is always\n * cleared so it can't pin the isolate awake past resolution.\n */\nexport async function awaitWithDeadline<T>(\n promise: Promise<T>,\n deadline: number | null\n): Promise<T | typeof TIMED_OUT> {\n if (deadline == null) {\n return promise;\n }\n const remainingMs = Math.max(0, deadline - Date.now());\n let timer: ReturnType<typeof setTimeout>;\n const result = await Promise.race([\n promise,\n new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(TIMED_OUT), remainingMs);\n })\n ]);\n clearTimeout(timer!);\n return result;\n}\n\n/**\n * Drain the host's interaction-apply chain so a subsequent completeness check\n * (e.g. `hasIncompleteToolBatch`) sees every tool result that has ALREADY\n * arrived.\n *\n * Bounded by real apply activity (a storage write each), never a fixed timer:\n * `getTail` is re-read after every await because a sibling can extend the tail\n * mid-drain, and the loop stops once the tail stops advancing. Bails early when\n * `hasPending()` goes false (the pending continuation was cleared by a chat\n * clear / turn reset) so a stale drain can't hold the isolate awake.\n */\nexport async function drainInteractionApplies(\n hasPending: () => boolean,\n getTail: () => Promise<unknown>\n): Promise<void> {\n let tail = getTail();\n for (;;) {\n if (!hasPending()) return;\n try {\n await tail;\n } catch {\n // A rejected apply is irrelevant to completeness — re-read and re-check.\n }\n if (getTail() === tail) return;\n tail = getTail();\n }\n}\n","/**\n * Tool State — shared update builders and applicator for tool part state changes.\n *\n * Used by both AIChatAgent and Think to apply tool results and approvals\n * to message parts. Each agent handles find-message, persist, and broadcast\n * in their own way; this module provides the state matching and update logic.\n */\n\n/**\n * Describes an update to apply to a tool part.\n */\nexport type ToolPartUpdate = {\n toolCallId: string;\n matchStates: string[];\n apply: (part: Record<string, unknown>) => Record<string, unknown>;\n};\n\n/**\n * Apply a tool part update to a parts array.\n * Finds the first part matching `update.toolCallId` in one of `update.matchStates`,\n * applies the update immutably, and returns the new parts array with the index.\n *\n * Returns `null` if no matching part was found.\n */\nexport function applyToolUpdate(\n parts: Array<Record<string, unknown>>,\n update: ToolPartUpdate\n): { parts: Array<Record<string, unknown>>; index: number } | null {\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n if (\n \"toolCallId\" in part &&\n part.toolCallId === update.toolCallId &&\n \"state\" in part &&\n update.matchStates.includes(part.state as string)\n ) {\n const updatedParts = [...parts];\n updatedParts[i] = update.apply(part);\n return { parts: updatedParts, index: i };\n }\n }\n return null;\n}\n\n/**\n * Build an update descriptor for applying a tool result.\n *\n * Matches parts in `input-available`, `approval-requested`, or `approval-responded` state.\n * Sets state to `output-available` (with output) or `output-error` (with errorText).\n */\nexport function toolResultUpdate(\n toolCallId: string,\n output: unknown,\n overrideState?: \"output-error\",\n errorText?: string\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\n \"input-available\",\n \"approval-requested\",\n \"approval-responded\"\n ],\n apply: (part) => ({\n ...part,\n ...(overrideState === \"output-error\"\n ? {\n state: \"output-error\",\n errorText: errorText ?? \"Tool execution denied by user\"\n }\n : { state: \"output-available\", output, preliminary: false })\n })\n };\n}\n\n/**\n * Build an update descriptor for a terminal tool result that belongs to a\n * tool part in a *different* (earlier) assistant message than the one\n * currently being streamed.\n *\n * This is the \"cross-message\" case: an approved server tool executes during a\n * continuation stream, but its tool part lives in the assistant message that\n * originally requested it. `StreamAccumulator` surfaces this as a\n * `cross-message-tool-update` action because the accumulator only owns the\n * current turn's new content and cannot mutate a part from a prior message.\n *\n * Compared to {@link toolResultUpdate} this builder is deliberately more\n * defensive, mirroring the equivalent fallback in `@cloudflare/ai-chat`:\n *\n * - It matches the broad set of pre-terminal **and** terminal states, so a\n * provider that replays the entire prior tool round-trip during a\n * continuation (notably the OpenAI Responses API — issue #1404) still\n * resolves to the same part instead of silently missing it.\n * - It is **first-write-wins**: a chunk arriving for a tool that already holds\n * a terminal result is treated as a replay and the existing output is never\n * overwritten. In that case `apply` returns the *same part reference*, which\n * callers use as an idempotent-no-op signal to skip the durable write and a\n * redundant `MESSAGE_UPDATED` broadcast.\n * - It preserves a streamed `preliminary` flag when one is present, otherwise\n * marks the result final (`preliminary: false`).\n */\nexport function crossMessageToolResultUpdate(\n toolCallId: string,\n updateType: \"output-available\" | \"output-error\",\n output?: unknown,\n errorText?: string,\n preliminary?: boolean\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\n \"input-streaming\",\n \"input-available\",\n \"approval-requested\",\n \"approval-responded\",\n \"output-available\",\n \"output-error\",\n \"output-denied\"\n ],\n apply: (part) => {\n if (\n part.state === \"output-available\" ||\n part.state === \"output-error\" ||\n part.state === \"output-denied\"\n ) {\n return part;\n }\n if (updateType === \"output-error\") {\n return {\n ...part,\n state: \"output-error\",\n errorText: errorText ?? \"Tool execution failed\"\n };\n }\n return {\n ...part,\n state: \"output-available\",\n output,\n preliminary: preliminary ?? false\n };\n }\n };\n}\n\n/**\n * Build an update descriptor that replaces the output of a *paused durable\n * execution* tool part (e.g. a codemode runtime tool that paused for\n * approval).\n *\n * A paused execution completes its tool call normally — the part is already\n * `output-available` with an output of `{ status: \"paused\", executionId }`.\n * When the host later approves/rejects the execution, the new outcome\n * (completed / rejected / paused-again) must replace that output in place.\n *\n * Matching is deliberately narrow and idempotent:\n *\n * - only `output-available` parts are considered;\n * - the existing output must be a paused-execution object carrying the same\n * `executionId` — anything else (already replaced, different execution)\n * returns the *same part reference*, which callers treat as a no-op signal\n * (skip persist + broadcast), mirroring {@link crossMessageToolResultUpdate}.\n */\nexport function pausedExecutionUpdate(\n toolCallId: string,\n executionId: string,\n output: unknown\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\"output-available\"],\n apply: (part) => {\n const current = part.output as\n | { status?: unknown; executionId?: unknown }\n | null\n | undefined;\n if (\n current == null ||\n typeof current !== \"object\" ||\n current.status !== \"paused\" ||\n current.executionId !== executionId\n ) {\n return part;\n }\n return { ...part, output, preliminary: false };\n }\n };\n}\n\n/**\n * Build an update descriptor for applying a tool approval.\n *\n * Matches parts in `input-available` or `approval-requested` state.\n * Sets state to `approval-responded` (if approved) or `output-denied` (if denied).\n */\nexport function toolApprovalUpdate(\n toolCallId: string,\n approved: boolean\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\"input-available\", \"approval-requested\"],\n apply: (part) => {\n const approval =\n typeof part.approval === \"object\" &&\n part.approval !== null &&\n !Array.isArray(part.approval)\n ? (part.approval as Record<string, unknown>)\n : undefined;\n const approvalId =\n typeof approval?.id === \"string\" ? approval.id : toolCallId;\n\n return {\n ...part,\n state: approved ? \"approval-responded\" : \"output-denied\",\n approval: {\n ...approval,\n id: approvalId,\n approved\n }\n };\n }\n };\n}\n\n// ── Client-interaction predicates (recovery classification) ─────────────────\n//\n// `@internal` — these leaf predicates are byte-identical in `AIChatAgent` and\n// `Think`. The broad-vs-client-only asymmetry lives in each package's\n// higher-level `hasPendingInteraction` / `hasPendingClientInteraction`\n// wrappers, which both call the identical leaf, so the wrappers stay\n// package-local. See `design/rfc-chat-recovery-foundation.md`.\n\n/** A minimal message shape for the leaf tool/interaction scans. */\ntype ToolBatchMessage = {\n role: string;\n parts: ReadonlyArray<unknown>;\n};\n\n/** Extract a tool part's name from its `tool-<name>` / `dynamic-tool` shape. */\nexport function toolPartName(\n record: Record<string, unknown>\n): string | undefined {\n const type = typeof record.type === \"string\" ? record.type : \"\";\n if (type === \"dynamic-tool\") {\n return typeof record.toolName === \"string\" ? record.toolName : undefined;\n }\n if (type.startsWith(\"tool-\")) {\n return type.slice(\"tool-\".length);\n }\n return undefined;\n}\n\n/**\n * Whether a part is still awaiting a CLIENT interaction that can genuinely\n * arrive after a restart: an `approval-requested` part (a reconnecting client\n * replays the approval) or an `input-available` part for a CLIENT tool (the SPA\n * replays the `tool-result`). A SERVER tool's `input-available` is NOT pending —\n * its `execute()` died with the isolate.\n */\nexport function partAwaitsClientInteraction(\n part: unknown,\n clientResolvable: Set<string>\n): boolean {\n if (typeof part !== \"object\" || part === null || !(\"state\" in part)) {\n return false;\n }\n const record = part as Record<string, unknown>;\n const state = record.state;\n if (state === \"approval-requested\") return true;\n if (state !== \"input-available\") return false;\n const toolName = toolPartName(record);\n return toolName != null && clientResolvable.has(toolName);\n}\n\n/**\n * Names of the CLIENT-resolvable tools — the client-provided schemas from the\n * last request, which have no server `execute`. An interrupted `input-available`\n * part for one of these can still be resolved by the client replaying a\n * `tool-result`; a server tool's cannot.\n */\nexport function clientResolvableToolNames(\n tools: ReadonlyArray<{ name?: string } | null | undefined> | undefined\n): Set<string> {\n const names = new Set<string>();\n for (const tool of tools ?? []) {\n if (tool?.name) names.add(tool.name);\n }\n return names;\n}\n\n/**\n * `true` when the latest assistant message is mid-batch: it carries at least\n * one settled tool result AND at least one tool call/approval still awaiting a\n * client result. That is the #1649 signature — the model fanned out parallel\n * tool calls and only some have been answered. Scoped to the leaf (the step the\n * continuation answers) so an unrelated dangling tool in an earlier message\n * doesn't block a legitimate follow-up continuation.\n */\nexport function hasIncompleteToolBatch(\n messages: ReadonlyArray<ToolBatchMessage>\n): boolean {\n // Zero-allocation backward scan for the latest assistant message — this runs\n // on every barrier poll tick, and `messages` can be large.\n let leaf: ToolBatchMessage | undefined;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"assistant\") {\n leaf = messages[i];\n break;\n }\n }\n if (!leaf) return false;\n let hasPending = false;\n let hasSettled = false;\n for (const part of leaf.parts) {\n const record = part as Record<string, unknown>;\n const state = record.state;\n if (state === \"input-available\" || state === \"approval-requested\") {\n hasPending = true;\n } else if (\n typeof record.type === \"string\" &&\n (record.type.startsWith(\"tool-\") || record.type === \"dynamic-tool\") &&\n (state === \"output-available\" ||\n state === \"output-error\" ||\n state === \"output-denied\" ||\n state === \"approval-responded\")\n ) {\n hasSettled = true;\n }\n if (hasPending && hasSettled) return true;\n }\n return false;\n}\n","/**\n * Protocol Message Parser — typed parsing of cf_agent_chat_* WebSocket messages.\n *\n * Parses raw WebSocket messages into a discriminated union of protocol events.\n * Both AIChatAgent and Think can use this instead of manual JSON.parse + type checking.\n */\n\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\n\n/**\n * Discriminated union of all incoming chat protocol events.\n *\n * Each agent handles the events it cares about and ignores the rest.\n * Returns `null` for non-JSON messages or unrecognized types.\n */\nexport type ChatProtocolEvent =\n | {\n type: \"chat-request\";\n id: string;\n init: { method?: string; body?: string; [key: string]: unknown };\n }\n | { type: \"clear\" }\n | { type: \"cancel\"; id: string }\n | {\n type: \"tool-result\";\n toolCallId: string;\n toolName: string;\n output: unknown;\n state?: string;\n errorText?: string;\n autoContinue?: boolean;\n clientTools?: Array<{\n name: string;\n description?: string;\n parameters?: unknown;\n }>;\n }\n | {\n type: \"tool-approval\";\n toolCallId: string;\n approved: boolean;\n autoContinue?: boolean;\n }\n | { type: \"stream-resume-request\" }\n | { type: \"stream-resume-ack\"; id: string }\n | { type: \"messages\"; messages: unknown[] };\n\n/**\n * Parse a raw WebSocket message string into a typed protocol event.\n *\n * Returns `null` if the message is not valid JSON or not a recognized\n * protocol message type. Callers should fall through to the user's\n * `onMessage` handler when `null` is returned.\n *\n * @example\n * ```typescript\n * const event = parseProtocolMessage(rawMessage);\n * if (!event) return userOnMessage(connection, rawMessage);\n *\n * switch (event.type) {\n * case \"chat-request\": { ... }\n * case \"clear\": { ... }\n * case \"tool-result\": { ... }\n * }\n * ```\n */\nexport function parseProtocolMessage(raw: string): ChatProtocolEvent | null {\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const wireType = data.type as string | undefined;\n if (!wireType) return null;\n\n switch (wireType) {\n case CHAT_MESSAGE_TYPES.USE_CHAT_REQUEST:\n return {\n type: \"chat-request\",\n id: data.id as string,\n init: (data.init as { method?: string; body?: string }) ?? {}\n };\n\n case CHAT_MESSAGE_TYPES.CHAT_CLEAR:\n return { type: \"clear\" };\n\n case CHAT_MESSAGE_TYPES.CHAT_REQUEST_CANCEL:\n return { type: \"cancel\", id: data.id as string };\n\n case CHAT_MESSAGE_TYPES.TOOL_RESULT:\n return {\n type: \"tool-result\",\n toolCallId: data.toolCallId as string,\n toolName: (data.toolName as string) ?? \"\",\n output: data.output,\n state: data.state as string | undefined,\n errorText: data.errorText as string | undefined,\n autoContinue: data.autoContinue as boolean | undefined,\n clientTools: data.clientTools as\n | Array<{\n name: string;\n description?: string;\n parameters?: unknown;\n }>\n | undefined\n };\n\n case CHAT_MESSAGE_TYPES.TOOL_APPROVAL:\n return {\n type: \"tool-approval\",\n toolCallId: data.toolCallId as string,\n approved: data.approved as boolean,\n autoContinue: data.autoContinue as boolean | undefined\n };\n\n case CHAT_MESSAGE_TYPES.STREAM_RESUME_REQUEST:\n return { type: \"stream-resume-request\" };\n\n case CHAT_MESSAGE_TYPES.STREAM_RESUME_ACK:\n return {\n type: \"stream-resume-ack\",\n id: data.id as string\n };\n\n case CHAT_MESSAGE_TYPES.CHAT_MESSAGES:\n return {\n type: \"messages\",\n messages: (data.messages as unknown[]) ?? []\n };\n\n default:\n return null;\n }\n}\n","/**\n * Message reconciliation — pure functions for aligning client messages\n * with server state during persistence.\n *\n * Three strategies applied in order:\n * 1. Merge server-known tool outputs into stale client messages\n * 2. Reconcile assistant IDs (exact match → content-key → toolCallId)\n * 3. Per-message toolCallId dedup for persistence\n */\n\nimport type { UIMessage } from \"ai\";\n\n/**\n * Reconcile incoming client messages against server state.\n *\n * 1. Merges server-known tool outputs into incoming messages that still\n * show stale states (input-available, approval-requested, approval-responded)\n * 2. Reconciles assistant IDs: exact match → content-key match → toolCallId match\n *\n * @param incoming - Messages from the client\n * @param serverMessages - Current server-side messages (source of truth)\n * @param sanitizeForContentKey - Function to sanitize a message before computing\n * its content key (typically strips ephemeral provider metadata)\n * @returns Reconciled messages ready for persistence\n */\nexport function reconcileMessages(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[],\n sanitizeForContentKey?: (message: UIMessage) => UIMessage\n): UIMessage[] {\n const withMergedToolOutputs = mergeServerToolOutputs(\n incoming,\n serverMessages\n );\n return reconcileAssistantIds(\n withMergedToolOutputs,\n serverMessages,\n sanitizeForContentKey\n );\n}\n\n/**\n * For a single message, resolve its ID by matching toolCallId against server state.\n * Prevents duplicate DB rows when client IDs differ from server IDs.\n * Tool call IDs are unique per conversation, so matching is safe regardless of state.\n */\nexport function resolveToolMergeId(\n message: UIMessage,\n serverMessages: readonly UIMessage[]\n): UIMessage {\n if (message.role !== \"assistant\") {\n return message;\n }\n\n for (const part of message.parts) {\n if (\"toolCallId\" in part && part.toolCallId) {\n const toolCallId = part.toolCallId as string;\n const existing = findMessageByToolCallId(serverMessages, toolCallId);\n if (existing && existing.id !== message.id) {\n return { ...message, id: existing.id };\n }\n }\n }\n\n return message;\n}\n\n/**\n * Merge a freshly-reconstructed orphaned partial onto the assistant message\n * that already owns its target id (the orphan-persist **(c)** step).\n *\n * Used by hosts whose store can hold an assistant row for the SAME id BEFORE\n * the stream finalizes — e.g. an early persist at tool-approval time, or a\n * continuation resuming the prior assistant message. On recovery the engine\n * replays the same chunks, so a naive append would leave two parts per tool\n * call. The merge therefore:\n *\n * - keeps ALL existing parts (the persisted row is authoritative for tool\n * parts that had a client result applied IN PLACE — that result lives only\n * in storage, never in the chunk stream, so a whole-message replace would\n * clobber it);\n * - appends only the reconstructed parts whose `toolCallId` is NOT already\n * present (dedup by tool-call identity);\n * - overlays the incoming metadata onto the existing metadata (incoming wins\n * on conflicts), falling back to whichever side is present.\n *\n * The result carries the INCOMING message's id/role (the caller has already\n * resolved the incoming id to the existing row's id via the (b) target-id\n * step), so it is safe to write straight back through `updateMessage`.\n *\n * Hosts whose orphan persist only ever runs at stream finalize (no early/\n * mid-stream row for the same id) never hit the merge branch and don't need\n * this — a plain append/replace is already dedup-safe because the shared\n * reconstruction (`StreamAccumulator` / `applyChunkToParts`) is idempotent by\n * `toolCallId`.\n */\nexport function reconcileOrphanPartial(\n existing: UIMessage,\n incoming: UIMessage\n): UIMessage {\n const existingToolCallIds = new Set(\n existing.parts\n .filter((p): p is typeof p & { toolCallId: string } => \"toolCallId\" in p)\n .map((p) => p.toolCallId)\n );\n const newParts = incoming.parts.filter(\n (p) => !(\"toolCallId\" in p && existingToolCallIds.has(p.toolCallId))\n );\n\n const merged: UIMessage = {\n ...incoming,\n parts: [...existing.parts, ...newParts]\n };\n if (existing.metadata) {\n merged.metadata = incoming.metadata\n ? { ...existing.metadata, ...incoming.metadata }\n : existing.metadata;\n }\n return merged;\n}\n\n/**\n * Content key for assistant messages used for dedup of identical short replies.\n * Returns JSON of sanitized parts, or undefined for non-assistant messages.\n */\nexport function assistantContentKey(\n message: UIMessage,\n sanitize?: (message: UIMessage) => UIMessage\n): string | undefined {\n if (message.role !== \"assistant\") {\n return undefined;\n }\n const sanitized = sanitize ? sanitize(message) : message;\n return JSON.stringify(sanitized.parts);\n}\n\nfunction mergeServerToolOutputs(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[]\n): UIMessage[] {\n // Index the server's RESOLVED tool parts so a stale client part (still in a\n // pre-output state) can't clobber the server's terminal state on persist.\n // All three terminal states must be protected, not just `output-available`:\n // otherwise a client that hasn't seen the server's `output-error` /\n // `output-denied` yet would persist its stale `input-available` over the\n // resolved result, losing the error/denial.\n const serverResolvedParts = new Map<string, Record<string, unknown>>();\n for (const msg of serverMessages) {\n if (msg.role !== \"assistant\") continue;\n for (const part of msg.parts) {\n const record = part as Record<string, unknown>;\n if (\n \"toolCallId\" in record &&\n \"state\" in record &&\n (record.state === \"output-available\" ||\n record.state === \"output-error\" ||\n record.state === \"output-denied\")\n ) {\n serverResolvedParts.set(record.toolCallId as string, record);\n }\n }\n }\n\n if (serverResolvedParts.size === 0) return incoming;\n\n return incoming.map((msg) => {\n if (msg.role !== \"assistant\") return msg;\n\n let hasChanges = false;\n const updatedParts = msg.parts.map((part) => {\n const record = part as Record<string, unknown>;\n if (\n \"toolCallId\" in record &&\n \"state\" in record &&\n (record.state === \"input-available\" ||\n record.state === \"approval-requested\" ||\n record.state === \"approval-responded\") &&\n serverResolvedParts.has(record.toolCallId as string)\n ) {\n hasChanges = true;\n const server = serverResolvedParts.get(record.toolCallId as string)!;\n // Overlay the server's resolved state, keeping the client part's\n // identity/input. Carry ONLY the result field that belongs to the\n // server's terminal state — so a stray `output` left on an\n // `output-error` part can't ride along and be misread as a result.\n const merged: Record<string, unknown> = {\n ...part,\n state: server.state\n };\n if (server.state === \"output-available\") {\n if (\"output\" in server) merged.output = server.output;\n } else if (server.state === \"output-error\") {\n if (\"errorText\" in server) merged.errorText = server.errorText;\n } else if (server.state === \"output-denied\") {\n if (\"approval\" in server) merged.approval = server.approval;\n }\n return merged;\n }\n return part;\n }) as UIMessage[\"parts\"];\n\n return hasChanges ? { ...msg, parts: updatedParts } : msg;\n });\n}\n\nfunction reconcileAssistantIds(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[],\n sanitize?: (message: UIMessage) => UIMessage\n): UIMessage[] {\n if (serverMessages.length === 0) return incoming;\n\n const claimedServerIndices = new Set<number>();\n const exactMatchMap = new Map<number, number>();\n\n for (let i = 0; i < incoming.length; i++) {\n const serverIdx = serverMessages.findIndex(\n (sm, si) => !claimedServerIndices.has(si) && sm.id === incoming[i].id\n );\n if (serverIdx !== -1) {\n claimedServerIndices.add(serverIdx);\n exactMatchMap.set(i, serverIdx);\n }\n }\n\n return incoming.map((incomingMessage, incomingIdx) => {\n if (exactMatchMap.has(incomingIdx)) {\n return incomingMessage;\n }\n\n if (\n incomingMessage.role !== \"assistant\" ||\n hasToolCallPart(incomingMessage)\n ) {\n return incomingMessage;\n }\n\n const incomingKey = assistantContentKey(incomingMessage, sanitize);\n if (!incomingKey) {\n return incomingMessage;\n }\n\n for (let i = 0; i < serverMessages.length; i++) {\n if (claimedServerIndices.has(i)) continue;\n\n const serverMessage = serverMessages[i];\n if (\n serverMessage.role !== \"assistant\" ||\n hasToolCallPart(serverMessage)\n ) {\n continue;\n }\n\n if (assistantContentKey(serverMessage, sanitize) === incomingKey) {\n claimedServerIndices.add(i);\n return { ...incomingMessage, id: serverMessage.id };\n }\n }\n\n return incomingMessage;\n });\n}\n\nfunction hasToolCallPart(message: UIMessage): boolean {\n return message.parts.some((part) => \"toolCallId\" in part);\n}\n\nfunction findMessageByToolCallId(\n messages: readonly UIMessage[],\n toolCallId: string\n): UIMessage | undefined {\n for (const msg of messages) {\n if (msg.role !== \"assistant\") continue;\n for (const part of msg.parts) {\n if (\"toolCallId\" in part && part.toolCallId === toolCallId) {\n return msg;\n }\n }\n }\n return undefined;\n}\n","/**\n * Transcript repair — flip interrupted tool calls (a `tool-*` / `dynamic-tool`\n * part with no settled result, left behind when a stream was cut off mid-flight)\n * into a settled shape so the next provider call does not 400 with\n * `AI_MissingToolResultsError`, and normalize malformed tool `input`.\n *\n * This is the shared, host-agnostic core extracted from `@cloudflare/think`'s\n * `_repairToolTranscriptParts`. Both AI-SDK chat hosts (`@cloudflare/think` and\n * `@cloudflare/ai-chat`) run it before re-entering inference on a recovered turn\n * so an interrupted SERVER tool (whose `execute()` died with the evicted\n * isolate, leaving an `input-available` orphan that nothing will ever resolve)\n * is converted to an errored tool-result and the turn can continue, instead of\n * being abandoned.\n *\n * Pure: it returns a new messages array plus repair stats and never touches\n * storage, broadcast, or events — the host owns those side effects.\n *\n * @internal Shared chat-recovery internals; not a public API.\n */\n\nimport type { UIMessage } from \"ai\";\nimport { normalizeToolInput } from \"./message-builder\";\n\n/**\n * Whether a tool part already has a settled result the provider accepts, so it\n * must NOT be re-repaired into an errored result.\n *\n * Single source of truth for the terminal tool states. Mirrors the AI SDK's\n * terminal states: `convertToModelMessages` emits a `tool-result` for\n * `output-available`, `output-error`, AND `output-denied` (a user-denied\n * approval — its denial reason becomes the tool-result). Omitting any of these\n * makes repair re-flip the part every turn — clobbering a real `errorText` /\n * denial with the generic \"interrupted\" message.\n */\nexport function toolPartHasSettledResult(\n record: Record<string, unknown>\n): boolean {\n if (\"output\" in record || \"result\" in record) return true;\n const state = typeof record.state === \"string\" ? record.state : \"\";\n return (\n state === \"output-available\" ||\n state === \"output-error\" ||\n state === \"output-denied\"\n );\n}\n\nexport interface RepairInterruptedToolPartsOptions {\n /**\n * Decide the replacement for an interrupted tool part (no settled result, not\n * `approval-responded`). Its `input` has already been normalized to a valid\n * object. The default host behavior flips it to an errored tool-result; hosts\n * expose this as an overridable `repairInterruptedToolPart` hook so a subclass\n * can, e.g., convert an interrupted client-resolved tool into a text part.\n */\n repairPart: (part: UIMessage[\"parts\"][number]) => UIMessage[\"parts\"][number];\n /**\n * Whether a tool part already carries a settled result (defaults to\n * {@link toolPartHasSettledResult}).\n */\n isSettled?: (record: Record<string, unknown>) => boolean;\n /**\n * Normalize a tool part's `input` (defaults to the shared\n * {@link normalizeToolInput}).\n */\n normalizeInput?: (input: unknown) => { input: unknown; changed: boolean };\n /**\n * Whether an interrupted tool part (no settled result, not\n * `approval-responded`) should be repaired at all. Defaults to `true` (repair\n * everything, like Think — which converts even client tools via its\n * `repairPart` override). A host whose default `repairPart` errors the part\n * (ai-chat) passes this to SKIP a part still legitimately awaiting a CLIENT\n * interaction (an `input-available` client tool or an `approval-requested`\n * part the user may still answer) so it is left verbatim rather than clobbered\n * with an error. Skipped parts are not counted in `removedToolCalls`.\n */\n shouldRepair?: (part: UIMessage[\"parts\"][number]) => boolean;\n}\n\nexport interface RepairInterruptedToolPartsResult {\n /** A new messages array; unchanged messages keep their original reference. */\n messages: UIMessage[];\n /** Count of interrupted tool calls flipped to a repaired shape. */\n removedToolCalls: number;\n /** Count of tool parts whose malformed `input` was normalized. */\n normalizedInputs: number;\n /** The tool-call ids that were repaired. */\n toolCallIds: string[];\n}\n\n/**\n * Repair interrupted tool calls and normalize malformed tool inputs across a\n * transcript. Behavior mirrors `@cloudflare/think`'s original\n * `_repairToolTranscriptParts`:\n *\n * - a tool part with NO settled result and state `approval-responded` is kept\n * verbatim (an approved server tool waiting for its continuation to run\n * `execute()` — not abandoned);\n * - a tool part with NO settled result for which `shouldRepair` returns false\n * is kept verbatim (a part still awaiting a CLIENT interaction; see option);\n * - any other tool part with no settled result is normalized then handed to\n * `repairPart` (default: flipped to an errored result);\n * - a tool part WITH a settled result only has its `input` normalized.\n *\n * Messages with no changed part keep their original object reference so callers\n * can cheaply detect what to persist.\n */\nexport function repairInterruptedToolParts(\n messages: UIMessage[],\n options: RepairInterruptedToolPartsOptions\n): RepairInterruptedToolPartsResult {\n const isSettled = options.isSettled ?? toolPartHasSettledResult;\n const normalizeInput = options.normalizeInput ?? normalizeToolInput;\n const { repairPart } = options;\n const shouldRepair = options.shouldRepair ?? (() => true);\n\n let removedToolCalls = 0;\n let normalizedInputs = 0;\n const toolCallIds: string[] = [];\n const repaired: UIMessage[] = [];\n\n for (const message of messages) {\n const parts: UIMessage[\"parts\"] = [];\n let messageChanged = false;\n for (const part of message.parts) {\n const record = part as Record<string, unknown>;\n const toolCallId =\n typeof record.toolCallId === \"string\" ? record.toolCallId : undefined;\n const isToolPart =\n typeof record.type === \"string\" &&\n (record.type.startsWith(\"tool-\") || record.type === \"dynamic-tool\") &&\n toolCallId;\n if (!isToolPart) {\n parts.push(part);\n continue;\n }\n\n if (!isSettled(record)) {\n const state = typeof record.state === \"string\" ? record.state : \"\";\n // An approved server tool waits at `approval-responded` until its\n // scheduled continuation runs `execute()`. It is not abandoned, so\n // preserve it verbatim — flipping it to an error (or removing it) would\n // strand the approval and prevent the real result from ever being\n // produced by the continuation.\n if (state === \"approval-responded\") {\n parts.push(part);\n continue;\n }\n // A part still legitimately awaiting a CLIENT interaction (the host's\n // `shouldRepair` returns false) is left verbatim — erroring it would\n // clobber a tool-result / approval the client may still replay. Only\n // hosts whose default repair ERRORS the part opt into this; Think keeps\n // the default (repair everything, converting client tools via its hook).\n if (!shouldRepair(part)) {\n parts.push(part);\n continue;\n }\n // Preserve the interrupted/abandoned tool call instead of deleting it.\n // Deleting makes the call \"disappear\" from the (broadcast) transcript\n // and lets the model silently re-run it. `input` is normalized to a\n // valid object first, then `repairPart` decides the replacement shape\n // (default: an errored result, so conversion still gets a tool-result\n // and the provider doesn't 400 with AI_MissingToolResultsError).\n const normalized = normalizeInput(\n \"input\" in record ? record.input : undefined\n );\n parts.push(\n repairPart({\n ...part,\n input: normalized.input\n } as UIMessage[\"parts\"][number])\n );\n if (normalized.changed) normalizedInputs++;\n removedToolCalls++;\n messageChanged = true;\n toolCallIds.push(toolCallId);\n continue;\n }\n\n const normalized = normalizeInput(\n \"input\" in record ? record.input : undefined\n );\n if (normalized.changed) {\n parts.push({\n ...part,\n input: normalized.input\n } as UIMessage[\"parts\"][number]);\n normalizedInputs++;\n messageChanged = true;\n continue;\n }\n\n parts.push(part);\n }\n\n repaired.push(messageChanged ? { ...message, parts } : message);\n }\n\n return {\n messages: repaired,\n removedToolCalls,\n normalizedInputs,\n toolCallIds\n };\n}\n","/**\n * Orphan-persist core — reconstruct a message from an interrupted stream's\n * buffered chunks and upsert it through an {@link OrphanPersistStore}.\n *\n * This is the genuinely-shared skeleton extracted from the two AI-SDK chat\n * hosts' `_persistOrphanedStream` (`@cloudflare/think` and\n * `@cloudflare/ai-chat`): the accumulate loop plus the\n * `getMessage → updateMessage(merge) XOR appendMessage` upsert. The\n * deliberately host-specific bits stay in the callers:\n *\n * - buffer flush (Think flushes defensively first; ai-chat doesn't);\n * - the fallback message id;\n * - `prepare` — Think strips internal parts and may skip (`null`); ai-chat\n * resolves the persist-target id from stream metadata;\n * - `merge` — Think replaces the whole message; ai-chat reconciles partials\n * so an in-place tool result isn't re-advanced by a replayed chunk; and\n * - broadcast (Think broadcasts after; ai-chat broadcasts inside its store's\n * `persistMessages`).\n *\n * Pure orchestration: it performs exactly one store write (update XOR append),\n * never touches the buffer, and never broadcasts.\n *\n * @internal Shared chat-recovery internals; not a public API.\n */\n\nimport type { UIMessage } from \"ai\";\nimport { StreamAccumulator } from \"./stream-accumulator\";\nimport type { StreamChunkData } from \"./message-builder\";\nimport type { OrphanPersistStore } from \"./orphan-store\";\n\nexport interface PersistReconstructedOrphanOptions<\n TMessage extends UIMessage = UIMessage\n> {\n /** The store seam to upsert through (a `SessionProvider` write-subset). */\n store: OrphanPersistStore<TMessage>;\n /**\n * Id for the reconstructed message when the stream carried no provider\n * `start.messageId` to adopt. The accumulator still adopts a provider id when\n * present.\n */\n fallbackId: string;\n /**\n * Finalize the reconstructed message before upsert — e.g. strip internal\n * parts or resolve the persist-target id. Return `null` to skip persistence\n * entirely (e.g. an empty structural-only message).\n */\n prepare: (message: TMessage) => TMessage | null;\n /**\n * Combine an existing row with the reconstructed message when a row already\n * owns the id (replace, or reconcile partials).\n */\n merge: (existing: TMessage, incoming: TMessage) => TMessage;\n}\n\n/**\n * Reconstruct a message from `chunks` and upsert it via the store. Returns\n * `true` when a write happened (so a caller that broadcasts after — Think — can\n * gate its broadcast on it), `false` when there was nothing to persist (no\n * parts, or `prepare` returned `null`).\n */\nexport async function persistReconstructedOrphan<\n TMessage extends UIMessage = UIMessage\n>(\n chunks: ReadonlyArray<{ body: string }>,\n options: PersistReconstructedOrphanOptions<TMessage>\n): Promise<boolean> {\n if (chunks.length === 0) return false;\n\n const accumulator = new StreamAccumulator({ messageId: options.fallbackId });\n for (const chunk of chunks) {\n try {\n accumulator.applyChunk(JSON.parse(chunk.body) as StreamChunkData);\n } catch {\n // Skip malformed chunk bodies.\n }\n }\n\n if (accumulator.parts.length === 0) return false;\n\n const prepared = options.prepare(accumulator.toMessage() as TMessage);\n if (prepared === null) return false;\n\n const existing = await options.store.getMessage(prepared.id);\n if (existing) {\n await options.store.updateMessage(options.merge(existing, prepared));\n } else {\n await options.store.appendMessage(prepared);\n }\n return true;\n}\n","import type { ClientToolSchema } from \"./client-tools\";\n\n/**\n * The minimal transcript-tail shape {@link createChatFiberSnapshot} reads to\n * derive the snapshot's `latest*Id` markers. Deliberately NOT `UIMessage`: the\n * snapshot only ever needs each message's `id` + `role`, so any host transcript\n * (AI SDK `UIMessage[]`, `Think`'s session leaves, or the pi adapter's plain\n * `AgentMessage[]`) satisfies it structurally. Keeping this off `UIMessage` is\n * the Phase-5 genericity seam — the snapshot builder must not couple to the AI\n * SDK message shape.\n */\nexport interface SnapshotMessage {\n id?: string;\n role: string;\n}\n\nexport type ChatFiberSnapshot<Kind extends string = string> = {\n kind: Kind;\n version: 1;\n requestId: string;\n recoveryRootRequestId?: string;\n continuation: boolean;\n latestMessageId?: string;\n latestMessageRole?: string;\n latestUserMessageId?: string;\n startedAt: number;\n lastBody?: Record<string, unknown>;\n lastClientTools?: ClientToolSchema[];\n};\n\nexport function createChatFiberSnapshot<Kind extends string>({\n kind,\n requestId,\n recoveryRootRequestId,\n continuation,\n messages,\n lastBody,\n lastClientTools\n}: {\n kind: Kind;\n requestId: string;\n recoveryRootRequestId?: string;\n continuation: boolean;\n messages: ReadonlyArray<SnapshotMessage>;\n lastBody?: Record<string, unknown>;\n lastClientTools?: ClientToolSchema[];\n}): ChatFiberSnapshot<Kind> {\n const latestMessage =\n messages.length > 0 ? messages[messages.length - 1] : undefined;\n let latestUser: SnapshotMessage | undefined;\n\n for (let index = messages.length - 1; index >= 0; index--) {\n if (messages[index].role === \"user\") {\n latestUser = messages[index];\n break;\n }\n }\n\n return {\n kind,\n version: 1,\n requestId,\n recoveryRootRequestId,\n continuation,\n latestMessageId: latestMessage?.id,\n latestMessageRole: latestMessage?.role,\n latestUserMessageId: latestUser?.id,\n startedAt: Date.now(),\n lastBody,\n lastClientTools\n };\n}\n\nexport function wrapChatFiberSnapshot<Kind extends string>(\n key: string,\n snapshot: ChatFiberSnapshot<Kind>,\n user: unknown | null\n): Record<string, unknown> {\n return { [key]: snapshot, user };\n}\n\nexport function unwrapChatFiberSnapshot<Kind extends string>(\n key: string,\n value: unknown,\n expectedKind?: Kind\n): {\n snapshot: ChatFiberSnapshot<Kind> | null;\n user: unknown | null;\n} {\n if (typeof value !== \"object\" || value === null || !(key in value)) {\n return { snapshot: null, user: value };\n }\n\n const envelope = value as Record<string, unknown>;\n const snapshot = envelope[key];\n if (typeof snapshot !== \"object\" || snapshot === null) {\n return { snapshot: null, user: value };\n }\n const candidate = snapshot as Record<string, unknown>;\n if (\n candidate.version !== 1 ||\n (expectedKind !== undefined && candidate.kind !== expectedKind) ||\n typeof candidate.requestId !== \"string\" ||\n typeof candidate.continuation !== \"boolean\"\n ) {\n return { snapshot: null, user: value };\n }\n\n return {\n snapshot: snapshot as ChatFiberSnapshot<Kind>,\n user: envelope.user ?? null\n };\n}\n","/**\n * `ChatRecoveryCodec` — the streaming-codec seam the recovery engine replays an\n * interrupted turn's durable buffer through to reconstruct its partial assistant\n * state. The engine and hosts only ever see the wire-agnostic `RecoveryPartial`\n * shape (`{ text, parts }`); the codec owns the chunk-vocabulary differences.\n *\n * Two implementations exist today: {@link AISDKRecoveryCodec} (AI SDK SSE chunks,\n * used by `@cloudflare/ai-chat` and `@cloudflare/think`) and `PiRecoveryCodec`\n * (the pi `AgentEvent` vocabulary, in the `experimental/pi-recovery` fixture).\n * Formalizing the interface here is the proof that the codec — not the engine —\n * carries the chunk-shape contract.\n *\n * @internal Shared chat-recovery internals; not a public API.\n */\n\nimport { getPartialStreamText } from \"./message-builder\";\nimport type { MessagePart } from \"./message-builder\";\nimport type { RecoveryPartial } from \"./recovery-engine\";\n\n/**\n * Whether a reconstructed AI SDK `UIMessage` parts array carries any settled\n * (provider-accepted) tool result — the completed, often non-idempotent work\n * that a `{ persist: false }` recovery return would otherwise silently discard\n * (#1631). A part counts as settled when it is a tool part (`tool-*` /\n * `dynamic-tool`) carrying an `output`/`result`, or whose state reached a\n * terminal `output-{available,error,denied}`.\n *\n * This is the AI SDK codec's implementation of the per-vocabulary \"did this\n * partial settle a tool?\" question. It lives with {@link AISDKRecoveryCodec}\n * (not in the engine) because the codec owns the part vocabulary — the engine\n * only ever reads the precomputed `RecoveryPartial.hasSettledToolResults`\n * boolean and never names a part type. Foreign codecs (e.g. AG-UI) compute the\n * same boolean from their own chunk vocabulary without producing AI SDK parts.\n */\nexport function partialHasSettledToolResults(parts: MessagePart[]): boolean {\n return parts.some((part) => {\n const record = part as Record<string, unknown>;\n const type = typeof record.type === \"string\" ? record.type : \"\";\n if (!(type.startsWith(\"tool-\") || type === \"dynamic-tool\")) return false;\n if (\"output\" in record || \"result\" in record) return true;\n const state = typeof record.state === \"string\" ? record.state : \"\";\n return (\n state === \"output-available\" ||\n state === \"output-error\" ||\n state === \"output-denied\"\n );\n });\n}\n\n/**\n * Reconstructs the partial assistant state of an interrupted turn from its\n * stored `ResumableStream` chunk bodies (oldest-first).\n */\nexport interface ChatRecoveryCodec {\n /**\n * Replay the stored chunk bodies into the engine's `RecoveryPartial`. The\n * codec — not the engine — both reconstructs `parts` (in its own vocabulary,\n * opaque to the engine) AND decides `hasSettledToolResults`, so the engine\n * never names a part type.\n */\n toRecoveryPartial(bodies: string[]): RecoveryPartial;\n /**\n * Whether a stored chunk of this wire `type` is a **progress milestone** — a\n * started text/reasoning segment or a settled tool input/output — that should\n * always credit the host's recovery no-progress window (#1637). The chunk-type\n * list lives HERE (the codec owns the chunk vocabulary). A `undefined` type (a\n * non-JSON / typeless body) is never progress.\n */\n isProgressChunk(type: string | undefined): boolean;\n /**\n * Whether a stored chunk of this wire `type` is **mid-segment streaming\n * content** — a delta extending an already-started segment (text/reasoning\n * body, partial tool input). On its own a delta is too granular to credit per\n * token, but a long single segment that produces only deltas (no new\n * milestone) must still register forward progress across repeated crashes, or\n * its no-progress window can false-fire while content is genuinely streaming.\n * Hosts credit these through a time throttle (see {@link\n * shouldCreditStreamProgress}). Disjoint from {@link isProgressChunk}; a\n * `undefined` type is never streaming content.\n */\n isStreamingContentChunk(type: string | undefined): boolean;\n}\n\n/** Minimal per-isolate throttle gate (see `StreamProgressCreditThrottle`). */\nexport interface ProgressCreditThrottle {\n shouldCredit(now: number): boolean;\n}\n\n/**\n * The single, host-agnostic rule for crediting recovery forward progress from a\n * stored stream chunk — the convergence of what `AIChatAgent` and `Think`\n * previously each decided on their own (ai-chat keyed on chunk type only; Think\n * keyed on its flush cadence). Both hosts now call this at chunk-store time so\n * the bump TIMING is identical:\n *\n * - a **milestone** ({@link ChatRecoveryCodec.isProgressChunk}) always credits;\n * - **streaming content** ({@link ChatRecoveryCodec.isStreamingContentChunk})\n * credits at most once per throttle window, so a long single segment still\n * registers progress across crashes without writing storage per token;\n * - anything else never credits.\n *\n * Finer than either host's prior cadence in the worst case and never coarser, so\n * it can only delay/avoid a false `no_progress_timeout`, never hasten give-up.\n */\nexport function shouldCreditStreamProgress(input: {\n codec: Pick<ChatRecoveryCodec, \"isProgressChunk\" | \"isStreamingContentChunk\">;\n type: string | undefined;\n throttle: ProgressCreditThrottle;\n now: number;\n}): boolean {\n const { codec, type, throttle, now } = input;\n if (codec.isProgressChunk(type)) return true;\n if (codec.isStreamingContentChunk(type)) return throttle.shouldCredit(now);\n return false;\n}\n\n/**\n * The AI SDK codec: replays SSE chunk bodies through {@link getPartialStreamText}\n * (`applyChunkToParts` under the hood). Stateless — share the\n * {@link aiSdkRecoveryCodec} singleton rather than constructing per call.\n */\nexport class AISDKRecoveryCodec implements ChatRecoveryCodec {\n // Return type is intentionally INFERRED (not annotated `RecoveryPartial`) so it\n // keeps the concrete `parts: MessagePart[]`, which the AI SDK hosts' own\n // `_getPartialStreamText` callers rely on. It is still assignable to\n // `RecoveryPartial` (whose `parts` is `unknown[]`), so the engine seam stays\n // vocabulary-agnostic while AI SDK callers keep their typed parts.\n toRecoveryPartial(bodies: string[]): {\n text: string;\n parts: MessagePart[];\n hasSettledToolResults: boolean;\n } {\n const { text, parts } = getPartialStreamText(\n bodies.map((body) => ({ body }))\n );\n return {\n text,\n parts,\n hasSettledToolResults: partialHasSettledToolResults(parts)\n };\n }\n\n isProgressChunk(type: string | undefined): boolean {\n return (\n type === \"text-start\" ||\n type === \"reasoning-start\" ||\n type === \"tool-input-available\" ||\n type === \"tool-output-available\" ||\n type === \"tool-output-error\" ||\n type === \"tool-output-denied\"\n );\n }\n\n isStreamingContentChunk(type: string | undefined): boolean {\n return (\n type === \"text-delta\" ||\n type === \"reasoning-delta\" ||\n type === \"tool-input-delta\"\n );\n }\n}\n\n/** Shared stateless {@link AISDKRecoveryCodec} instance. */\nexport const aiSdkRecoveryCodec = new AISDKRecoveryCodec();\n","/**\n * Shared resume-handshake driver (rfc-chat-recovery-foundation, Tier-2).\n *\n * The server side of the WebSocket stream-resume protocol — the byte-parallel\n * block `@cloudflare/ai-chat` and `@cloudflare/think` previously hand-maintained\n * in lockstep, now shared here:\n * the proactive `STREAM_RESUMING` notify, the `STREAM_RESUME_REQUEST` decision\n * tree, the `STREAM_RESUME_ACK` decision tree, and the terminal-replay path\n * (#1645). The two hosts diverge only in the idle-connect payload (kept\n * host-owned, NOT here) and the use-chat-response message-type constant (a\n * {@link ResumeHandshakeHost} field).\n *\n * Frame shapes are frozen by the golden fixture in\n * `__tests__/resume-handshake-frames.ts`; this driver must keep emitting exactly\n * those bytes.\n *\n * `@internal` — sibling-package support, not a public API. See\n * `design/rfc-chat-recovery-foundation.md`.\n */\n\nimport type { Connection } from \"agents\";\nimport { sendIfOpen } from \"./connection\";\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\nimport type { ContinuationState } from \"./continuation-state\";\nimport type { PreStreamTurns } from \"./pre-stream-turns\";\nimport type { ResumableStream } from \"./resumable-stream\";\n\n/** A pending terminal outcome captured before connect (#1645). */\nexport interface PendingChatTerminal {\n requestId: string;\n body: string;\n}\n\n/**\n * The host-owned surface the resume handshake threads. `pendingResumeConnections`\n * (and the continuation `awaitingConnections` map) stay host-owned — they are\n * also touched by the streaming loop, which is NOT part of this extraction — so\n * the driver only reads/mutates them through this seam rather than owning them.\n */\nexport interface ResumeHandshakeHost {\n /** The host's use-chat-response message-type constant (wire string). */\n readonly responseMessageType: string;\n readonly resumableStream: ResumableStream;\n readonly continuation: ContinuationState<Connection>;\n /**\n * Accepted-but-not-yet-streamed turns (#1784). Optional: experimental\n * recovery adapters that don't track a pre-stream window omit it and keep the\n * legacy `resume_none` behavior. When present, the handshake parks a\n * reconnecting client here (sending a keep-waiting `STREAM_PENDING`) instead\n * of telling it there is nothing to resume.\n */\n readonly preStream?: PreStreamTurns<Connection>;\n /**\n * Connections notified of a resumable stream, excluded from live broadcast\n * until they ACK. Host-owned (shared with the streaming loop).\n */\n readonly pendingResumeConnections: Set<string>;\n /** Read the pending terminal outcome (#1645), or `null` when none survives. */\n pendingChatTerminal(): Promise<PendingChatTerminal | null>;\n /** Materialize an orphaned stream's partial into a persisted assistant message. */\n persistOrphanedStream(streamId: string): Promise<void>;\n /**\n * Whether the connection that owns the active continuation stream is still\n * connected. Optional: when omitted the handshake assumes it is (legacy\n * behavior). Hosts wire their live connection registry so a continuation\n * stream whose owner vanished on an abrupt (1006) reconnect can still be\n * resumed by the replacement connection (#1784).\n */\n isConnectionPresent?(connectionId: string): boolean;\n}\n\n/**\n * Drives the server side of the stream-resume protocol over a\n * {@link ResumeHandshakeHost}. Construct once per agent (the host wires its\n * `ResumableStream` / `ContinuationState` / pending set in) and call the three\n * public methods from the host's existing onConnect / onMessage wiring, so\n * handler registration timing stays host-owned.\n */\nexport class ResumeHandshake {\n constructor(private readonly host: ResumeHandshakeHost) {}\n\n /**\n * Notify a connection that an active stream can be resumed; it should reply\n * with `STREAM_RESUME_ACK` to receive the replay.\n *\n * A connection can legitimately be notified more than once for the same\n * request — proactively from onConnect AND in response to its explicit\n * `STREAM_RESUME_REQUEST` (#1733). This is intentional and must NOT be deduped\n * here: an explicit request always deserves a response (else the client's\n * `reconnectToStream` hangs to its timeout with no replay), and the proactive\n * notify is required for clients that never send a request. The notify is one\n * tiny frame; the client dedupes its ACK so the buffer is not replayed twice.\n */\n notifyStreamResuming(connection: Connection): void {\n const { resumableStream, pendingResumeConnections } = this.host;\n if (!resumableStream.hasActiveStream()) return;\n const sent = sendIfOpen(\n connection,\n JSON.stringify({\n type: CHAT_MESSAGE_TYPES.STREAM_RESUMING,\n id: resumableStream.activeRequestId\n })\n );\n if (sent) {\n // Add to pending set — excluded from live broadcasts until they ACK to\n // receive the full stream replay.\n pendingResumeConnections.add(connection.id);\n }\n }\n\n /**\n * Handle a client `STREAM_RESUME_REQUEST`. The client sends this after its\n * message handler is registered, avoiding the race where a proactive\n * `STREAM_RESUMING` from onConnect arrives before the handler is ready.\n */\n async handleResumeRequest(connection: Connection): Promise<void> {\n const { resumableStream, continuation, preStream } = this.host;\n if (resumableStream.hasActiveStream()) {\n if (\n continuation.activeRequestId === resumableStream.activeRequestId &&\n continuation.activeConnectionId !== null &&\n continuation.activeConnectionId !== connection.id &&\n this._ownerStillPresent(continuation.activeConnectionId)\n ) {\n sendIfOpen(\n connection,\n JSON.stringify({ type: CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE })\n );\n } else {\n this.notifyStreamResuming(connection);\n }\n } else if (\n continuation.pending !== null &&\n (continuation.pending.connectionId === null ||\n continuation.pending.connectionId === connection.id)\n ) {\n // A continuation turn is accepted but its stream has not started yet.\n // Park the connection AND tell it to keep waiting (#1784) so its resume\n // probe does not time out before the continuation stream begins; the host\n // flushes `awaitingConnections` into `STREAM_RESUMING` on stream start.\n continuation.awaitingConnections.set(connection.id, connection);\n this._sendStreamPending(connection, continuation.pending.requestId);\n } else if (await this._replayTerminalOnResume(connection)) {\n // A turn terminalized while no client was connected (#1645): drive the\n // resume handshake so the terminal error frame can be delivered on the\n // resumed stream (the only path that surfaces as an error on the client)\n // once this connection ACKs — see `_replayTerminalOnAck`.\n } else if (preStream?.park(connection)) {\n // A normal turn is accepted but its resumable stream has not started yet\n // (#1784). `park` enrolled the connection and sent `STREAM_PENDING`; the\n // host flushes it into `STREAM_RESUMING` on stream start, or releases it\n // with `STREAM_RESUME_NONE` if the turn settles without streaming.\n } else {\n sendIfOpen(\n connection,\n JSON.stringify({ type: CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE })\n );\n }\n }\n\n /** Send a keep-waiting `STREAM_PENDING` frame (#1784). */\n private _sendStreamPending(connection: Connection, requestId?: string): void {\n sendIfOpen(\n connection,\n JSON.stringify({\n type: CHAT_MESSAGE_TYPES.STREAM_PENDING,\n ...(requestId ? { id: requestId } : {})\n })\n );\n }\n\n /** Whether the active continuation's owner connection is still present. */\n private _ownerStillPresent(connectionId: string): boolean {\n return this.host.isConnectionPresent\n ? this.host.isConnectionPresent(connectionId)\n : true;\n }\n\n /** Handle a client `STREAM_RESUME_ACK` for `requestId`. */\n async handleResumeAck(\n connection: Connection,\n requestId: string\n ): Promise<void> {\n const { resumableStream, pendingResumeConnections, responseMessageType } =\n this.host;\n pendingResumeConnections.delete(connection.id);\n\n if (\n resumableStream.hasActiveStream() &&\n resumableStream.activeRequestId === requestId\n ) {\n const orphanedStreamId = resumableStream.replayChunks(\n connection,\n resumableStream.activeRequestId\n );\n // If the stream was orphaned (restored from SQLite after hibernation with\n // no live reader), reconstruct the partial assistant message from stored\n // chunks and persist it so it survives further page refreshes.\n if (orphanedStreamId) {\n await this.host.persistOrphanedStream(orphanedStreamId);\n }\n } else if (resumableStream.hasActiveStream()) {\n // Ignore ACKs for a different active stream request id.\n } else if (await this._replayTerminalOnAck(connection, requestId)) {\n // Delivered the pending terminal error frame on the resumed stream the\n // client just ACKed (#1645).\n } else if (\n !resumableStream.replayCompletedChunksByRequestId(connection, requestId)\n ) {\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: responseMessageType,\n replay: true\n })\n );\n }\n }\n\n /**\n * Replay a pending terminal outcome (#1645) over the resume handshake so a\n * reconnecting client surfaces it exactly like a live exhaustion. The bare\n * terminal frame is dropped by the client unless it arrives on a resumed\n * stream — the only path that reaches the transport's stream reader and\n * becomes `useChat.error` — so we drive `STREAM_RESUMING` here and deliver the\n * error frame once the client ACKs (see {@link _replayTerminalOnAck}). Returns\n * `true` if a terminal was pending (and `STREAM_RESUMING` was sent).\n */\n private async _replayTerminalOnResume(\n connection: Connection\n ): Promise<boolean> {\n const pending = await this.host.pendingChatTerminal();\n if (!pending) return false;\n sendIfOpen(\n connection,\n JSON.stringify({\n type: CHAT_MESSAGE_TYPES.STREAM_RESUMING,\n id: pending.requestId\n })\n );\n return true;\n }\n\n /**\n * Deliver the pending terminal error frame on the resumed stream the client\n * ACKed (#1645). The record is retained (not cleared) so concurrent reconnects\n * (e.g. multiple tabs) each learn the outcome; it is cleared when a later turn\n * supersedes it.\n */\n private async _replayTerminalOnAck(\n connection: Connection,\n requestId: string\n ): Promise<boolean> {\n const { resumableStream, responseMessageType } = this.host;\n const pending = await this.host.pendingChatTerminal();\n if (!pending || pending.requestId !== requestId) return false;\n // Replay any partial content the errored stream produced before the error,\n // so the reconnecting client observes the same sequence a live client did —\n // content chunks, then the terminal error (#1575). If the connection drops\n // mid-replay, skip the terminal frame; the record is retained, so the next\n // reconnect retries the whole sequence.\n if (\n !resumableStream.replayErroredChunksByRequestId(\n connection,\n pending.requestId\n )\n ) {\n return true;\n }\n sendIfOpen(\n connection,\n JSON.stringify({\n body: pending.body,\n done: true,\n error: true,\n id: pending.requestId,\n type: responseMessageType\n })\n );\n return true;\n }\n}\n","/**\n * Shared chat-recovery incident math.\n *\n * `@internal` — sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API. See\n * `design/rfc-chat-recovery-foundation.md`.\n *\n * `@cloudflare/ai-chat` and `@cloudflare/think` previously hand-maintained a\n * byte-identical incident-budget decision (`_beginChatRecoveryIncident`) apart\n * from a package log prefix, the pending-interaction predicate name, and Think's\n * extra client-tool rehydration guard. This module is now the single source of\n * that decision — one pure function both packages call, with one test surface.\n *\n * Pure here means: no Durable Object storage, no global clock, no broadcasts.\n * The caller still owns storage I/O (reading the existing incident, sweeping\n * stale incidents, persisting the result), the progress counter, the\n * pending-interaction predicate, and event emission. This function receives the\n * already-resolved inputs and returns the next incident, whether it is\n * exhausted, and the observability events to emit — keeping the budget logic\n * unit-testable against a deterministic clock with no Workers runtime.\n *\n * The serialized `ChatRecoveryIncident` shape, the storage keys, and the\n * incident-id formula are all part of the persisted cutover contract (see the\n * RFC's \"Cutover invariants\" table) and MUST NOT change without a migration.\n */\n\nimport type {\n ChatRecoveryConfig,\n ChatRecoveryProgressContext,\n ResolvedChatRecoveryConfig\n} from \"./lifecycle\";\n\n/**\n * Whether a recovery is retrying an unanswered user turn or continuing a\n * partial assistant turn. Intentionally NOT part of the incident identity (see\n * {@link chatRecoveryIncidentId}).\n */\nexport type ChatRecoveryKind = \"retry\" | \"continue\";\n\n/**\n * Durable per-incident recovery record.\n *\n * PERSISTED CONTRACT — this shape round-trips across deploys (including the\n * deploy that ships the shared engine, which is itself a deploy-mid-recovery).\n * Fields are added as optional so older persisted incidents keep recovering.\n */\nexport type ChatRecoveryIncident = {\n incidentId: string;\n requestId: string;\n /** Stable request ID for the whole continuation chain (the recovery root). */\n recoveryRootRequestId?: string;\n recoveryKind: ChatRecoveryKind;\n attempt: number;\n maxAttempts: number;\n status:\n | \"detected\"\n | \"scheduled\"\n | \"attempting\"\n | \"completed\"\n | \"skipped\"\n | \"exhausted\"\n | \"failed\";\n firstSeenAt: number;\n lastAttemptAt: number;\n /**\n * Epoch ms of the last attempt that observed forward progress. The recovery\n * budget is keyed to this (`now - lastProgressAt > noProgressTimeoutMs`), so a\n * turn that keeps producing content survives churn indefinitely while a\n * genuinely stuck turn is sealed within the window (#1637). Optional for\n * backward-compat — falls back to `firstSeenAt`.\n */\n lastProgressAt?: number;\n reason?: string;\n /**\n * High-water mark of the durable, monotonic recovery-progress counter\n * observed for this incident. Distinguishes a turn making forward progress\n * but repeatedly interrupted by isolate resets (deploys) — which must NOT\n * exhaust the budget — from one that genuinely fails to advance. Sourced from\n * a persisted counter, never the compactable transcript (#1628).\n */\n progress?: number;\n /**\n * Value of the durable progress counter when this incident opened. The\n * runaway-loop work budget is `progress - workBaseline`, compared against\n * `maxRecoveryWork`. Optional for backward-compat — a missing baseline is\n * treated as the current marker (zero work so far), so an in-flight incident\n * from an older build is never falsely sealed.\n */\n workBaseline?: number;\n};\n\n// ── Persisted storage keys (cutover contract) ──────────────────────────────\n\nexport const CHAT_RECOVERY_INCIDENT_KEY_PREFIX = \"cf:chat-recovery:incident:\";\n/**\n * Durable, monotonic forward-progress counter for recovery budget resets.\n * Bumped at production time when new content is streamed, so it reflects\n * genuinely new content and is immune to reconnects/re-persists; never\n * recomputed from the (compactable) transcript.\n */\nexport const CHAT_RECOVERY_PROGRESS_KEY = \"cf:chat-recovery:progress\";\n/**\n * Durable record of an in-progress recovery so a \"recovering…\" status (#1620)\n * can be broadcast live and survive the set/clear happening in different\n * isolates (a continuation runs in a later alarm invocation).\n */\nexport const CHAT_RECOVERING_KEY = \"cf:chat:recovering\";\n/**\n * Durable record of the last turn that ended in a terminal error / abandoned\n * recovery (#1645). Replayed on the next reconnect via the resume handshake;\n * cleared when a later turn supersedes it.\n */\nexport const CHAT_LAST_TERMINAL_KEY = \"cf:chat:last-terminal\";\n\n// ── Budget defaults and tuning constants ───────────────────────────────────\n\n/**\n * Secondary backstop only. The primary recovery bound is the no-progress wall\n * clock; with alarm debounce this cap rarely binds (it catches a pathological\n * tight alarm-loop). Kept high so the no-progress window seals first under\n * normal deploy cadence (#1637).\n */\nexport const DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS = 10;\n/**\n * Runaway-loop guard default. `Infinity` = no SDK-imposed work cap: a turn that\n * keeps making forward progress is never terminated by the framework on its own\n * (rfc-chat-recovery-work-budget). Integrators bound a content-emitting runaway\n * by setting `maxRecoveryWork` or a `shouldKeepRecovering` predicate.\n */\nexport const DEFAULT_CHAT_RECOVERY_MAX_WORK = Number.POSITIVE_INFINITY;\nexport const DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS = 10_000;\n/**\n * Delay before retrying a recovery that timed out waiting for stable state.\n * Gives an actively-churning isolate (e.g. a deploy in flight) time to settle.\n */\nexport const CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS = 3;\nexport const DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE =\n \"The assistant was interrupted and could not recover. Please try again.\";\n/**\n * Incidents that have not seen a new attempt within this window are assumed\n * abandoned and swept so durable storage does not grow without bound.\n */\nexport const CHAT_RECOVERY_INCIDENT_TTL_MS = 60 * 60 * 1000;\n/** Max keys per Durable Object KV `delete([...])` call. */\nexport const KV_DELETE_MAX_KEYS = 128;\n/**\n * PRIMARY recovery bound (#1637): seal an incident that has made no forward\n * progress for this long. Keyed to `lastProgressAt`, which resets on every\n * progress-bearing attempt — so a turn that keeps producing content survives\n * deploy churn indefinitely, while a genuinely stuck turn dies within 5 min.\n */\nexport const DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS = 5 * 60 * 1000;\n/**\n * Alarm debounce: recovery alarms bunched within this window collapse into a\n * single attempt. A deploy rollout drops/reconnects the socket several times\n * over ~11–22s; without this, one logical deploy would burn several attempts.\n */\nexport const CHAT_RECOVERY_ALARM_DEBOUNCE_MS = 30 * 1000;\n/**\n * Staleness bound for the live \"recovering…\" flag (#1620). A flag older than\n * this is treated as abandoned so it can neither pin the indicator on forever\n * nor suppress a genuinely-new recovering signal. NOT a recovery budget.\n */\nexport const CHAT_RECOVERING_FLAG_TTL_MS = 15 * 60 * 1000;\n\n// ── Pure helpers ───────────────────────────────────────────────────────────\n\n/**\n * Resolve a raw `chatRecovery` config field into the fully-defaulted form the\n * engine reasons about. Identical defaulting in both packages today.\n */\nexport function resolveChatRecoveryConfig(\n raw: ChatRecoveryConfig | undefined\n): ResolvedChatRecoveryConfig {\n const custom = typeof raw === \"object\" && raw !== null ? raw : undefined;\n return {\n enabled: raw !== false,\n maxAttempts: Math.max(\n 1,\n Math.floor(custom?.maxAttempts ?? DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS)\n ),\n stableTimeoutMs: Math.max(\n 0,\n Math.floor(\n custom?.stableTimeoutMs ?? DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS\n )\n ),\n terminalMessage:\n custom?.terminalMessage ?? DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE,\n noProgressTimeoutMs: Math.max(\n 0,\n Math.floor(\n custom?.noProgressTimeoutMs ??\n DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS\n )\n ),\n maxRecoveryWork:\n typeof custom?.maxRecoveryWork === \"number\" && custom.maxRecoveryWork >= 0\n ? custom.maxRecoveryWork\n : DEFAULT_CHAT_RECOVERY_MAX_WORK,\n ...(custom?.shouldKeepRecovering\n ? { shouldKeepRecovering: custom.shouldKeepRecovering }\n : {}),\n ...(custom?.onExhausted ? { onExhausted: custom.onExhausted } : {})\n };\n}\n\n/**\n * Stable identifier for a recovery incident.\n *\n * `recoveryKind` is intentionally NOT part of the identity: a single\n * interrupted turn can flip between \"retry\" (no chunks persisted) and\n * \"continue\" (partial chunks exist) across restarts, and the attempt budget\n * must be shared so recovery stays bounded by `maxAttempts`. This formula is a\n * cutover invariant.\n */\nexport function chatRecoveryIncidentId(input: {\n requestId: string;\n recoveryRootRequestId?: string | null;\n latestUserMessageId?: string | null;\n recoveryKind: ChatRecoveryKind;\n}): string {\n return [\n input.recoveryRootRequestId ?? input.requestId,\n input.latestUserMessageId ?? \"\"\n ].join(\":\");\n}\n\n/** Durable storage key for an incident record. */\nexport function chatRecoveryIncidentKey(incidentId: string): string {\n return `${CHAT_RECOVERY_INCIDENT_KEY_PREFIX}${encodeURIComponent(incidentId)}`;\n}\n\n/**\n * Select incident keys that have been inactive past the TTL. Pure over a map of\n * stored incidents; the caller performs the batched delete.\n */\nexport function selectStaleIncidentKeys(\n entries: Map<string, ChatRecoveryIncident | undefined>,\n now: number\n): string[] {\n const staleKeys: string[] = [];\n for (const [key, incident] of entries) {\n const lastActive = incident?.lastAttemptAt ?? incident?.firstSeenAt ?? 0;\n if (now - lastActive > CHAT_RECOVERY_INCIDENT_TTL_MS) {\n staleKeys.push(key);\n }\n }\n return staleKeys;\n}\n\n/**\n * Sweep recovery incidents inactive past the TTL from durable storage. Lists by\n * the incident key prefix, selects stale keys (`selectStaleIncidentKeys`), and\n * batch-deletes them — the DO KV `delete([...])` accepts up to\n * `KV_DELETE_MAX_KEYS` per call, collapsing N awaited round-trips into\n * ceil(N / 128). Shared by `AIChatAgent` and `Think` so the sweep policy lives in\n * one place. See `design/rfc-chat-recovery-foundation.md`.\n */\nexport async function sweepStaleChatRecoveryIncidents(\n storage: Pick<DurableObjectStorage, \"list\" | \"delete\">,\n now: number\n): Promise<void> {\n const entries = await storage.list<ChatRecoveryIncident>({\n prefix: CHAT_RECOVERY_INCIDENT_KEY_PREFIX\n });\n const staleKeys = selectStaleIncidentKeys(entries, now);\n for (let i = 0; i < staleKeys.length; i += KV_DELETE_MAX_KEYS) {\n await storage.delete(staleKeys.slice(i, i + KV_DELETE_MAX_KEYS));\n }\n}\n\n/**\n * Summarize a child agent's persisted recovery incidents for the parent's\n * agent-tool reattach decision: `\"in-progress\"` if any incident is still live\n * (detected/scheduled/attempting), else `\"failed\"` if any terminalized\n * (exhausted/failed), else `\"none\"`. In-progress takes precedence so a parent\n * never gives up on a child that is still recovering. Shared by `AIChatAgent`\n * and `Think`. See `design/rfc-chat-recovery-foundation.md`.\n */\nexport async function classifyAgentToolChildRecovery(\n storage: Pick<DurableObjectStorage, \"list\">\n): Promise<\"in-progress\" | \"failed\" | \"none\"> {\n const entries = await storage.list<ChatRecoveryIncident>({\n prefix: CHAT_RECOVERY_INCIDENT_KEY_PREFIX\n });\n let failed = false;\n for (const incident of entries.values()) {\n if (\n incident.status === \"detected\" ||\n incident.status === \"scheduled\" ||\n incident.status === \"attempting\"\n ) {\n return \"in-progress\";\n }\n if (incident.status === \"exhausted\" || incident.status === \"failed\") {\n failed = true;\n }\n }\n return failed ? \"failed\" : \"none\";\n}\n\n/**\n * Read the durable monotonic recovery-progress counter (0 when unset). The value\n * feeds the no-progress budget decision; shared by `AIChatAgent` and `Think`.\n */\nexport async function readChatRecoveryProgress(\n storage: Pick<DurableObjectStorage, \"get\">\n): Promise<number> {\n return (await storage.get<number>(CHAT_RECOVERY_PROGRESS_KEY)) ?? 0;\n}\n\n/**\n * Advance the durable recovery-progress counter by one. Called when genuinely new\n * content is durably flushed (real, reconnect-immune forward progress); shared by\n * `AIChatAgent` and `Think`.\n */\nexport async function bumpChatRecoveryProgress(\n storage: Pick<DurableObjectStorage, \"get\" | \"put\">\n): Promise<void> {\n const current = (await storage.get<number>(CHAT_RECOVERY_PROGRESS_KEY)) ?? 0;\n await storage.put(CHAT_RECOVERY_PROGRESS_KEY, current + 1);\n}\n\n/**\n * Throttle window for crediting a parent turn's recovery progress from forwarded\n * sub-agent (agent-tool) stream chunks (N9). Forwarding a child's chunks IS\n * forward progress for the parent, but the credit must not write storage per\n * token.\n */\nexport const AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS = 5_000;\n\n/**\n * Per-isolate throttle gate for agent-tool stream-progress crediting (N9). The\n * `_lastBumpAt` clock is in-memory, so it resets per isolate and the first\n * forwarded chunk after a restart always credits. `shouldCredit(now)` returns\n * `true` at most once per `AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS` window and\n * records the time on each credit. Shared by `AIChatAgent` and `Think`.\n */\nexport class AgentToolStreamProgressThrottle {\n private _lastBumpAt = 0;\n shouldCredit(now: number): boolean {\n if (now - this._lastBumpAt < AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS) {\n return false;\n }\n this._lastBumpAt = now;\n return true;\n }\n}\n\n/**\n * Throttle window for crediting recovery progress from mid-segment streaming\n * content (text/reasoning/tool-input deltas). A milestone chunk credits\n * unconditionally; deltas credit at most once per window so a long single\n * segment registers forward progress across crashes without writing storage per\n * token. 5s is far finer than the 300s no-progress budget, so any crash gap\n * longer than this window over an actively-streaming segment still credits.\n */\nexport const CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS = 5_000;\n\n/**\n * Per-isolate throttle gate for crediting recovery progress from mid-segment\n * streaming-content chunks — the delta arm of {@link shouldCreditStreamProgress}.\n * The `_lastBumpAt` clock is in-memory, so it resets per isolate and the first\n * delta after a restart always credits. Shared by `AIChatAgent` and `Think`.\n */\nexport class StreamProgressCreditThrottle {\n private _lastBumpAt = 0;\n shouldCredit(now: number): boolean {\n if (now - this._lastBumpAt < CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS) {\n return false;\n }\n this._lastBumpAt = now;\n return true;\n }\n}\n\n// ── Terminal + recovering status storage glue ──────────────────────────────\n//\n// Durable records for the terminal-error / \"recovering…\" reconnect UX. The\n// keys (`CHAT_LAST_TERMINAL_KEY`, `CHAT_RECOVERING_KEY`) and the flag TTL are\n// cutover-contract constants above; these helpers were byte-identical in both\n// packages apart from the recovering-broadcast wire-type enum and broadcast\n// wrapper, which are passed in. Shared by `AIChatAgent` and `Think`.\n\n/** Durable record of the last turn that ended in a terminal error (#1645). */\nexport type ChatTerminalRecord = { requestId: string; body: string };\n\n/**\n * Persist a durable record of the last terminal turn so a client that\n * (re)connects after the turn ended still learns its outcome (#1645). Kept\n * until a later turn supersedes it ({@link clearChatTerminal}); a single record\n * is sufficient because only the most recent terminal is relevant.\n */\nexport async function recordChatTerminal(\n storage: Pick<DurableObjectStorage, \"put\">,\n requestId: string,\n body: string\n): Promise<void> {\n await storage.put(CHAT_LAST_TERMINAL_KEY, { requestId, body });\n}\n\n/** Clear the durable terminal record once a later turn supersedes it (#1645). */\nexport async function clearChatTerminal(\n storage: Pick<DurableObjectStorage, \"delete\">\n): Promise<void> {\n await storage.delete(CHAT_LAST_TERMINAL_KEY);\n}\n\n/** Read the pending terminal record, or `null` if none is stored (#1645). */\nexport async function pendingChatTerminal(\n storage: Pick<DurableObjectStorage, \"get\">\n): Promise<ChatTerminalRecord | null> {\n return (\n (await storage.get<ChatTerminalRecord>(CHAT_LAST_TERMINAL_KEY)) ?? null\n );\n}\n\n/** Durable record shape for the live \"recovering…\" flag (#1620). */\ntype RecoveringRecord = { requestId?: string; at?: number };\n\n/**\n * Build the on-connect \"recovering…\" replay frame (#1620), or `null` when no\n * (non-stale) recovery is in progress. A client that connects between recovery\n * attempts (no active stream) reads the turn as working rather than frozen. A\n * record older than the flag TTL is treated as abandoned (its terminal-clear\n * never ran) and skipped, so a dead recovery can't show \"recovering…\" forever.\n * `messageType` is the package's recovering wire-type enum.\n */\nexport async function buildChatRecoveringFrame(\n storage: Pick<DurableObjectStorage, \"get\">,\n messageType: string,\n now: number\n): Promise<Record<string, unknown> | null> {\n const recovering = await storage.get<RecoveringRecord>(CHAT_RECOVERING_KEY);\n if (\n !recovering ||\n now - (recovering.at ?? 0) >= CHAT_RECOVERING_FLAG_TTL_MS\n ) {\n return null;\n }\n return {\n type: messageType,\n recovering: true,\n ...(recovering.requestId ? { id: recovering.requestId } : {})\n };\n}\n\n/**\n * Set or clear the live \"recovering…\" status (#1620). Persists a durable record\n * (so set/clear stay consistent across the isolates a recovery spans) and\n * broadcasts a recovering frame — but only on a genuine transition, so a\n * deploy/reconnect storm (which re-detects recovery many times) doesn't spam\n * the wire. A flag older than the TTL is stale: the owning incident was\n * abandoned without a terminal (e.g. the DO went idle before recovery could\n * resolve), so it is treated as not-recovering and can neither pin the\n * indicator on forever nor suppress a genuinely-new recovering signal.\n * `messageType` is the package's recovering wire-type enum; `broadcast` is the\n * package's chat-broadcast wrapper.\n */\nexport async function setChatRecovering(\n active: boolean,\n requestId: string | undefined,\n deps: {\n storage: Pick<DurableObjectStorage, \"get\" | \"put\" | \"delete\">;\n messageType: string;\n broadcast: (frame: Record<string, unknown>) => void;\n now: number;\n }\n): Promise<void> {\n const { storage, messageType, broadcast, now } = deps;\n const existing = await storage.get<RecoveringRecord>(CHAT_RECOVERING_KEY);\n const activeExisting =\n existing && now - (existing.at ?? 0) < CHAT_RECOVERING_FLAG_TTL_MS;\n if (active) {\n if (activeExisting) return; // already recovering — idempotent, no re-broadcast\n await storage.put(CHAT_RECOVERING_KEY, {\n ...(requestId ? { requestId } : {}),\n at: now\n });\n } else {\n if (!existing) return; // not recovering — nothing to clear\n await storage.delete(CHAT_RECOVERING_KEY);\n requestId = requestId ?? existing.requestId;\n }\n broadcast({\n type: messageType,\n recovering: active,\n ...(requestId ? { id: requestId } : {})\n });\n}\n\n// ── Incident budget evaluation ─────────────────────────────────────────────\n\n/**\n * Observability event produced by an incident evaluation or a status\n * transition, emitted by the caller. The `detected`/`attempt` events come from\n * the budget evaluation (begin path); the `scheduled` event comes from\n * `ChatRecoveryEngine.scheduleRecovery`; the `completed`/`skipped`/`failed`\n * events come from `ChatRecoveryEngine.updateIncident`. `reason` is carried only\n * by the `skipped`/`failed` transitions that record a cause.\n */\nexport type ChatRecoveryIncidentEvent = {\n type:\n | \"chat:recovery:detected\"\n | \"chat:recovery:attempt\"\n | \"chat:recovery:scheduled\"\n | \"chat:recovery:completed\"\n | \"chat:recovery:skipped\"\n | \"chat:recovery:failed\";\n incidentId: string;\n requestId: string;\n attempt: number;\n maxAttempts: number;\n recoveryKind: ChatRecoveryKind;\n reason?: string;\n};\n\nexport type EvaluateChatRecoveryIncidentInput = {\n /** Recovery identity for this turn. */\n identity: {\n requestId: string;\n recoveryRootRequestId?: string | null;\n latestUserMessageId?: string | null;\n recoveryKind: ChatRecoveryKind;\n };\n /** Fully-resolved recovery config. */\n config: ResolvedChatRecoveryConfig;\n /** The existing incident for this identity, or `null` if this is fresh. */\n existing: ChatRecoveryIncident | null;\n /** Current value of the durable monotonic progress counter. */\n currentProgress: number;\n /**\n * Whether the turn is parked on a pending CLIENT interaction (an\n * `input-available` client-tool part or an `approval-requested` part). Such a\n * turn is waiting on the human, not stuck, so it is budget-free.\n */\n awaitingClientInteraction: boolean;\n /** Injected clock (epoch ms) for deterministic tests. */\n now: number;\n /**\n * Invoked when `config.shouldKeepRecovering` throws. Lets each package keep\n * its own log prefix. A throwing predicate is treated as \"keep recovering\".\n */\n onShouldKeepRecoveringError?: (error: unknown) => void;\n};\n\nexport type EvaluateChatRecoveryIncidentResult = {\n /** The next incident record to persist. */\n incident: ChatRecoveryIncident;\n /** Whether this incident is now sealed as exhausted. */\n exhausted: boolean;\n /** Observability events to emit, in order. */\n events: ChatRecoveryIncidentEvent[];\n};\n\n/**\n * Compute the next recovery incident and budget decision.\n *\n * This is the durable recovery budget — a faithful extraction of\n * `_beginChatRecoveryIncident` from both `AIChatAgent` and `Think`. The\n * instruments are decoupled by what they catch:\n *\n * - STUCK — no-progress window: `lastProgressAt` resets on every\n * progress-bearing attempt, so a turn that keeps producing content survives\n * churn indefinitely; a stuck turn is sealed after `noProgressTimeoutMs`.\n * - DEBOUNCE — alarms bunched within `CHAT_RECOVERY_ALARM_DEBOUNCE_MS` collapse\n * into one attempt, so a single rollout's reconnect storm isn't N attempts.\n * - ALARM-LOOP — the attempt cap (resets on progress) catches a tight\n * no-progress alarm loop.\n * - RUNAWAY — the work budget seals a loop that keeps emitting content but\n * never converges. Keyed to WORK done, not wall-clock. Defaults to no cap.\n * - CALLER — `shouldKeepRecovering` lets the integrator express a\n * token/cost/step budget the SDK should not hardcode. Consulted only when no\n * hard bound has already sealed the incident, and never on first detection.\n *\n * A turn parked on a pending client interaction is budget-free: every bound is\n * suppressed and the no-progress clock kept fresh.\n */\nexport async function evaluateChatRecoveryIncident(\n input: EvaluateChatRecoveryIncidentInput\n): Promise<EvaluateChatRecoveryIncidentResult> {\n const {\n identity,\n config,\n existing,\n currentProgress,\n awaitingClientInteraction,\n now\n } = input;\n\n const incidentId = chatRecoveryIncidentId(identity);\n const recoveryRootRequestId =\n identity.recoveryRootRequestId ?? identity.requestId;\n\n // Forward-progress detection. A mid-turn deploy resets the Durable Object;\n // the interrupted continuation is re-detected on the next wake. A turn that\n // followed real progress (more durably-produced content than the last attempt\n // saw) is environmental churn, not a poison turn.\n const prevProgress = existing?.progress ?? 0;\n const madeProgress = existing != null && currentProgress > prevProgress;\n\n // While a client interaction is pending the turn is budget-free, and the\n // no-progress clock is kept fresh so the turn has a full window once the human\n // finally answers.\n const lastProgressAt =\n madeProgress || awaitingClientInteraction\n ? now\n : (existing?.lastProgressAt ?? existing?.firstSeenAt ?? now);\n const noProgressExceeded =\n existing != null &&\n !awaitingClientInteraction &&\n now - lastProgressAt > config.noProgressTimeoutMs;\n\n // Reuse the durable progress counter as a work meter. Baseline is captured\n // when the incident opens; `work` is what the turn produced since.\n const workBaseline = existing?.workBaseline ?? currentProgress;\n const progress = Math.max(prevProgress, currentProgress);\n const work = progress - workBaseline;\n const workBudgetExceeded =\n existing != null &&\n Number.isFinite(config.maxRecoveryWork) &&\n work > config.maxRecoveryWork;\n\n const debounced =\n existing != null &&\n !madeProgress &&\n now - existing.lastAttemptAt < CHAT_RECOVERY_ALARM_DEBOUNCE_MS;\n\n const attempt = madeProgress\n ? 1\n : debounced\n ? (existing?.attempt ?? 1)\n : (existing?.attempt ?? 0) + 1;\n\n // Consult the caller predicate only when no hard bound has already sealed the\n // incident — a buggy/expensive hook must not run after we've decided, and a\n // throwing hook must not wedge the turn (log and treat as \"continue\"). Never\n // called on first detection (existing == null).\n let abortedByCaller = false;\n if (\n existing != null &&\n !awaitingClientInteraction &&\n config.shouldKeepRecovering &&\n !noProgressExceeded &&\n !workBudgetExceeded &&\n attempt <= config.maxAttempts\n ) {\n try {\n const ctx: ChatRecoveryProgressContext = {\n incidentId,\n requestId: identity.requestId,\n recoveryRootRequestId,\n attempt,\n maxAttempts: config.maxAttempts,\n recoveryKind: identity.recoveryKind,\n work,\n ageMs: now - (existing.firstSeenAt ?? now)\n };\n const decision = await config.shouldKeepRecovering(ctx);\n abortedByCaller = decision === false;\n } catch (error) {\n input.onShouldKeepRecoveringError?.(error);\n }\n }\n\n const exhausted =\n !awaitingClientInteraction &&\n (noProgressExceeded ||\n workBudgetExceeded ||\n abortedByCaller ||\n attempt > config.maxAttempts);\n\n const incident: ChatRecoveryIncident = {\n incidentId,\n requestId: identity.requestId,\n recoveryRootRequestId,\n recoveryKind: identity.recoveryKind,\n attempt,\n maxAttempts: config.maxAttempts,\n status: exhausted ? \"exhausted\" : \"attempting\",\n firstSeenAt: existing?.firstSeenAt ?? now,\n lastAttemptAt: now,\n lastProgressAt,\n progress,\n workBaseline,\n ...(exhausted\n ? {\n reason: workBudgetExceeded\n ? \"work_budget_exceeded\"\n : noProgressExceeded\n ? \"no_progress_timeout\"\n : abortedByCaller\n ? \"recovery_aborted\"\n : \"max_attempts_exceeded\"\n }\n : {})\n };\n\n const events: ChatRecoveryIncidentEvent[] = [];\n if (!existing) {\n events.push({\n type: \"chat:recovery:detected\",\n incidentId,\n requestId: identity.requestId,\n attempt,\n maxAttempts: config.maxAttempts,\n recoveryKind: identity.recoveryKind\n });\n }\n events.push({\n type: \"chat:recovery:attempt\",\n incidentId,\n requestId: identity.requestId,\n attempt,\n maxAttempts: config.maxAttempts,\n recoveryKind: identity.recoveryKind\n });\n\n return { incident, exhausted, events };\n}\n","/**\n * Shared chat-recovery engine (sibling-package support for `@cloudflare/ai-chat`\n * and `@cloudflare/think`). Owns the recovery orchestration both packages must\n * perform identically — scheduling policy and incident-begin sequencing — behind\n * a thin {@link ChatRecoveryAdapter} seam so the package-specific host I/O\n * (storage, clock, events, interaction predicate) stays in the package. See\n * `design/rfc-chat-recovery-foundation.md`.\n *\n * @internal Not a public API.\n */\n\nimport type { FiberRecoveryContext } from \"../index\";\nimport type {\n ChatRecoveryExhaustedContext,\n ChatRecoveryOptions,\n ResolvedChatRecoveryConfig\n} from \"./lifecycle\";\nimport type { ChatFiberSnapshot } from \"./recovery\";\nimport {\n CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS,\n chatRecoveryIncidentId,\n chatRecoveryIncidentKey,\n evaluateChatRecoveryIncident\n} from \"./recovery-incident\";\nimport type {\n ChatRecoveryIncident,\n ChatRecoveryIncidentEvent,\n ChatRecoveryKind\n} from \"./recovery-incident\";\n\n/** The scheduled-callback entrypoints a recovery schedule can target. */\nexport type ChatRecoveryScheduleCallback =\n | \"_chatRecoveryContinue\"\n | \"_chatRecoveryRetry\";\n\n/**\n * Why a recovery callback is being scheduled. The idempotency of the underlying\n * `schedule()` call depends ONLY on this:\n *\n * - `\"initial\"` — the first schedule of a continuation/retry when an interrupted\n * turn is detected on wake. A deploy rollout drops/reconnects the socket\n * several times, re-triggering detection; idempotent scheduling (dedup on\n * callback + payload) collapses that storm into a single enqueued continuation\n * instead of N duplicates.\n *\n * - `\"stable_timeout_retry\"` — a reschedule issued from INSIDE the currently-\n * executing one-shot schedule row (a continuation that timed out waiting for\n * stable state). `alarm()` deletes that row only AFTER the callback returns,\n * so an idempotent reschedule would dedup onto the doomed row and be deleted\n * with it — the retry would never fire. A fresh (non-idempotent) delayed row\n * survives the deletion.\n */\nexport type ChatRecoveryScheduleReason = \"initial\" | \"stable_timeout_retry\";\n\n/**\n * A reconstructed orphaned-stream partial. The engine seam is deliberately\n * **wire-vocabulary-agnostic**: `text` is the accumulated assistant text and\n * `parts` is OPAQUE to the engine (`unknown[]`) — each host casts it back to its\n * own message-part vocabulary (AI SDK `UIMessage` parts, AG-UI tool parts, …).\n * The single fact the engine needs about parts — does the partial carry settled\n * (non-idempotent) tool work that must survive a `{ persist: false }` recovery\n * (#1631)? — is precomputed by the {@link ChatRecoveryCodec} as\n * `hasSettledToolResults`. So the engine never imports a part vocabulary; the\n * codec owns it (see `partialHasSettledToolResults` in `recovery-codec.ts` for\n * the AI SDK codec's implementation of that predicate).\n */\nexport type RecoveryPartial = {\n text: string;\n parts: unknown[];\n hasSettledToolResults: boolean;\n};\n\n/** Lifecycle status of a recovered stream's metadata row. */\nexport type ChatStreamStatus = \"streaming\" | \"completed\" | \"error\";\n\n/**\n * Resolve the `schedule()` idempotency option for a recovery schedule. Single\n * source of truth for both packages; see {@link ChatRecoveryScheduleReason} for\n * the rationale behind each case.\n *\n * This is a cutover invariant: flipping either case silently breaks deploy-storm\n * dedup (initial) or stalls stable-timeout retries (reschedule), and neither is\n * caught by a type error — only by the recovery suites.\n */\nexport function chatRecoverySchedulePolicy(\n reason: ChatRecoveryScheduleReason\n): { idempotent: boolean } {\n return { idempotent: reason === \"initial\" };\n}\n\n/** Identity + context for opening (or re-evaluating) a recovery incident. */\nexport interface BeginChatRecoveryIncidentInput {\n requestId: string;\n recoveryRootRequestId?: string | null;\n latestUserMessageId?: string | null;\n recoveryKind: ChatRecoveryKind;\n /** Test-only clock injection for deterministic debounce/window timing. */\n nowMs?: number;\n}\n\nexport interface BeginChatRecoveryIncidentResult {\n incident: ChatRecoveryIncident;\n config: ResolvedChatRecoveryConfig;\n exhausted: boolean;\n}\n\n/**\n * Package-specific host operations the engine drives during incident\n * orchestration. Every method is a thin pass-through to the package's existing\n * storage / clock / event / interaction primitives — the engine owns only the\n * *sequence*, not the I/O.\n */\nexport interface ChatRecoveryAdapter {\n /** Resolve the effective recovery config (defaults + caller overrides). */\n resolveConfig(): ResolvedChatRecoveryConfig;\n /** Wall clock; only consulted when the input carries no test `nowMs`. */\n now(): number;\n /** Evict incidents past the TTL. Runs before the existing-record read. */\n sweepStaleIncidents(now: number): Promise<void>;\n /** Read the persisted incident for `key`, or `null` if none. */\n getIncident(key: string): Promise<ChatRecoveryIncident | null>;\n /**\n * Optional: rehydrate any state the interaction predicate depends on. Invoked\n * after the existing-incident read and BEFORE `isAwaitingClientInteraction`.\n * `Think` uses this to restore client tools from durable storage on a cold\n * boot-recovery wake (so a HITL turn is not misread as stuck); `AIChatAgent`\n * has no such state and omits it.\n */\n ensureInteractionStateLoaded?(): void;\n /**\n * Optional: give the package a chance to handle a NON-chat fiber before chat\n * recovery inspects it. Returns `true` if the package fully consumed the\n * fiber, in which case the engine tells the caller to skip chat-recovery\n * processing for it. `Think` uses this for its messenger/workflow reply fibers\n * (`think:messenger-reply`); `AIChatAgent` has no non-chat fibers and omits it\n * (the engine then treats every recovered fiber as a chat-recovery candidate).\n *\n * Ordering invariant: the engine dispatches this FIRST, before the\n * chat-fiber-name gate, so a non-chat fiber is never misclassified as an\n * orphaned chat turn.\n */\n tryHandleNonChatFiberRecovery?(ctx: FiberRecoveryContext): Promise<boolean>;\n /** Monotonic forward-progress marker for the no-progress budget. */\n readProgress(): Promise<number>;\n /**\n * Whether the turn is parked on a pending CLIENT interaction (waiting on the\n * human, not stuck). When true the engine keeps the incident budget-free.\n * Optional: a host with no client-interaction/HITL substrate (e.g. the pi\n * fixture) omits it and the engine treats the turn as never parked (`false`).\n */\n isAwaitingClientInteraction?(): boolean;\n /** Persist the evaluated incident under `key`. */\n putIncident(key: string, incident: ChatRecoveryIncident): Promise<void>;\n /**\n * Delete the incident record under `key`. The engine calls this on the\n * terminal `completed` transition (a completed recovery is never retried, so\n * its record is dropped rather than left in storage forever).\n */\n deleteIncident(key: string): Promise<void>;\n /** Broadcast a lifecycle event produced by the evaluation or a transition. */\n emitRecoveryEvent(event: ChatRecoveryIncidentEvent): void;\n /**\n * Enqueue a recovery callback. A thin pass-through to the package's\n * `schedule(delaySeconds, callback, data, chatRecoverySchedulePolicy(reason))`\n * — the engine owns the surrounding orchestration (the transition + emit for\n * the initial schedule in {@link ChatRecoveryEngine.scheduleRecovery}, the\n * attempt bump for {@link ChatRecoveryEngine.rescheduleAfterStableTimeout});\n * the package owns the Durable Object alarm write and the payload shape.\n * `reason` selects the idempotency policy and `delaySeconds` the alarm delay\n * (`0` for the initial enqueue, `CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS` for\n * a stable-timeout reschedule).\n */\n scheduleRecovery(\n callback: ChatRecoveryScheduleCallback,\n data: Record<string, unknown>,\n reason: ChatRecoveryScheduleReason,\n delaySeconds: number\n ): Promise<void>;\n /**\n * Set or clear the live \"recovering…\" status (#1620). The engine calls this on\n * the incident transitions: `scheduled` → active (keyed by the recovery-root\n * request id, falling back to the incident's request id), and\n * `completed`/`skipped`/`failed` → cleared. The package owns the underlying\n * staleness / idempotency / broadcast I/O.\n */\n setRecovering(active: boolean, requestId?: string): Promise<void>;\n /**\n * Report a throw from the caller's `shouldKeepRecovering` hook. Optional: a\n * host that does not surface this diagnostic omits it (the engine swallows the\n * report).\n */\n onShouldKeepRecoveringError?(error: unknown): void;\n /**\n * Terminalize a given-up recovery turn: deliver the exhaustion notification\n * plus the package-owned terminal record / banner / submission writes. A thin\n * pass-through to the package's `_exhaustChatRecovery` (which composes\n * {@link runChatRecoveryExhaustion}). Driven by\n * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}; the engine owns the\n * surrounding read → re-entry-guard → build → terminalize → seal sequence, the\n * package owns the terminal writes (uniformly broadcast-first; their set\n * differs — `Think` also writes a submission row).\n */\n exhaustChatRecovery(\n incident: ChatRecoveryIncident,\n config: ResolvedChatRecoveryConfig,\n partial: RecoveryPartial,\n streamId: string,\n createdAt: number\n ): Promise<void>;\n /**\n * Resolve the orphaned stream identity for a (recovery-root) request id —\n * `streamId` is `\"\"` when no stream metadata survives. Drives BOTH the wake\n * path (which consumes the full {@link ResolvedRecoveryStream}) and the\n * give-up path (which reads only `.streamId`). A thin pass-through to the\n * package's stream-metadata lookup: the newest row keyed by the request id,\n * else the live active stream.\n */\n resolveRecoveryStream(requestId: string): ResolvedRecoveryStream;\n /** Reconstruct the partial text/parts buffered for `streamId`. */\n getPartialStreamText(streamId: string): RecoveryPartial;\n /**\n * The in-flight recovery-root request id, consulted as a fallback in the\n * give-up root-id chain when the payload carries no `originalRequestId` /\n * `recoveredRequestId` and no incident record survives. `undefined` when no\n * recovery chain is active. (`AIChatAgent` and `Think` both back this with\n * `_activeChatRecoveryRootRequestId`.)\n */\n activeChatRecoveryRootRequestId(): string | undefined;\n /**\n * Report a tolerated best-effort bookkeeping failure during give-up: the\n * incident `\"read\"` (before synthesizing) or the sealing `\"seal\"` write\n * (after terminalization). Neither aborts terminalization — see\n * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.\n */\n onGiveUpBookkeepingError(phase: \"read\" | \"seal\", error: unknown): void;\n}\n\n/** Resolved orphaned-stream identity for a recovered chat turn. */\nexport interface ResolvedRecoveryStream {\n /** The orphaned stream id, or `\"\"` when no stream metadata survives. */\n streamId: string;\n /**\n * Whether the orphaned stream is still the live in-flight stream (so its\n * partial has not already been persisted + completed by an ACK-driven\n * reconnect). Gates persistence and stream completion.\n */\n streamStillActive: boolean;\n /**\n * The stream metadata row's lifecycle status, when the host tracks it\n * (`Think`). `undefined` for hosts that do not model terminal streams\n * (`AIChatAgent`) — those keep every terminal-stream branch dead, per the\n * \"substrate capabilities are optional\" decision in the RFC.\n */\n streamStatus?: ChatStreamStatus;\n}\n\n/** Input to {@link ChatFiberWakeHooks.classifyRecoveredTurn}. */\nexport interface ClassifyRecoveredTurnInput {\n snapshot: ChatFiberSnapshot | null;\n requestId: string;\n streamId: string;\n partial: RecoveryPartial;\n streamStillActive: boolean;\n streamStatus?: ChatStreamStatus;\n}\n\n/** Input to {@link ChatFiberWakeHooks.invokeOnChatRecovery}. */\nexport interface InvokeOnChatRecoveryInput {\n incident: ChatRecoveryIncident;\n recoveryKind: ChatRecoveryKind;\n recoveryRootRequestId: string;\n requestId: string;\n streamId: string;\n partial: RecoveryPartial;\n snapshot: ChatFiberSnapshot | null;\n recoveryData: unknown;\n createdAt: number;\n}\n\n/** Input to {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}. */\nexport interface PersistOrphanedPartialInput {\n streamId: string;\n streamStillActive: boolean;\n streamStatus?: ChatStreamStatus;\n snapshot: ChatFiberSnapshot | null;\n}\n\n/** Input to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}. */\nexport interface DispatchRecoveredTurnInput<TClassify> {\n incident: ChatRecoveryIncident;\n config: ResolvedChatRecoveryConfig;\n recoveryKind: ChatRecoveryKind;\n options: ChatRecoveryOptions;\n snapshot: ChatFiberSnapshot | null;\n requestId: string;\n recoveryRootRequestId: string;\n streamId: string;\n streamStatus?: ChatStreamStatus;\n /** The package-specific classification detail produced by `classifyRecoveredTurn`. */\n detail: TClassify;\n}\n\n/**\n * The wake-dispatch host operations the engine drives when an interrupted CHAT\n * fiber is detected on restart — the divergent organs the frame-collapse map\n * flagged. Kept SEPARATE from {@link ChatRecoveryAdapter} (and passed per call to\n * {@link ChatRecoveryEngine.handleChatFiberRecovery}) so the incident/give-up\n * adapter stays focused, and generic over `TClassify` so the\n * `classifyRecoveredTurn` → `dispatchRecoveredTurn` handoff is type-safe without a\n * class-level generic.\n *\n * The engine owns the wake LIFECYCLE (gate → parse → unwrap → stream → partial →\n * classify → begin-incident → exhausted-branch → onChatRecovery → persist →\n * complete → dispatch → catch→failed) and the shared persist clause; these hooks\n * own the package-specific I/O and the retry/continue/skip decision.\n */\nexport interface ChatFiberWakeHooks<TClassify> {\n /** The chat-fiber name prefix (`CHAT_FIBER_NAME + \":\"`) gating the wake path. */\n chatFiberPrefix(): string;\n /** Decode the fiber snapshot into the recovery snapshot + checkpointed user data. */\n unwrapRecoverySnapshot(ctx: FiberRecoveryContext): {\n snapshot: ChatFiberSnapshot | null;\n recoveryData: unknown;\n };\n /**\n * Classify the recovered turn as a `retry` or `continue` and return any\n * package-specific detail the dispatch decision needs (e.g. the pre-stream\n * retry target id). Runs before the incident is opened.\n */\n classifyRecoveredTurn(\n input: ClassifyRecoveredTurnInput\n ):\n | { recoveryKind: ChatRecoveryKind; detail: TClassify }\n | Promise<{ recoveryKind: ChatRecoveryKind; detail: TClassify }>;\n /**\n * Build the package's `ChatRecoveryContext` and invoke the user `onChatRecovery`\n * hook, returning its (defaulted) options. The engine wraps this in the\n * incident `failed`-on-throw guard. Optional: a host with no user\n * `onChatRecovery` surface (e.g. the pi fixture) omits it and the engine\n * proceeds with empty options (`{}`).\n */\n invokeOnChatRecovery?(\n input: InvokeOnChatRecoveryInput\n ): Promise<ChatRecoveryOptions | void>;\n /**\n * The BASE persist gate: whether the orphaned partial is eligible to be\n * materialized at all (live stream, or terminal-but-not-yet-persisted). The\n * engine ANDs this with the shared `options.persist !== false ||\n * partial.hasSettledToolResults` clause, so settled work is never dropped.\n */\n shouldPersistOrphanedPartial(\n input: PersistOrphanedPartialInput\n ): boolean | Promise<boolean>;\n /** Materialize the orphaned stream's partial into a persisted assistant message. */\n persistOrphanedStream(streamId: string): Promise<void>;\n /** Mark the (still-active) recovered stream complete and schedule cleanup. */\n completeRecoveredStream(streamId: string): void | Promise<void>;\n /**\n * The retry/continue/skip DECISION — the package-owned core. Runs after persist\n * + complete; owns the leaf/submission computation, the schedule calls (via\n * {@link ChatRecoveryEngine.scheduleRecovery}), the skip transitions, and any\n * package-specific terminal/broadcast writes.\n */\n dispatchRecoveredTurn(\n input: DispatchRecoveredTurnInput<TClassify>\n ): Promise<void>;\n}\n\n/**\n * Drives the shared recovery orchestration over a {@link ChatRecoveryAdapter}.\n * The incident *budget math* lives in the pure `evaluateChatRecoveryIncident`;\n * this class owns the surrounding sequence and its ordering invariants.\n */\nexport class ChatRecoveryEngine {\n constructor(private readonly adapter: ChatRecoveryAdapter) {}\n\n /**\n * Open or re-evaluate the recovery incident for `input`, persist the result,\n * and broadcast its lifecycle events. Returns the incident, the resolved\n * config, and whether the budget is now exhausted.\n */\n /**\n * Dispatch a recovered fiber to the package's non-chat handler (the\n * messenger/workflow seam) before any chat-recovery processing. Returns `true`\n * when the package consumed the fiber — the caller must then skip chat\n * recovery for it. The engine owns the *ordering* (this runs before the\n * chat-fiber gate); the *behavior* is adapter-owned. No-op (`false`) when the\n * adapter omits {@link ChatRecoveryAdapter.tryHandleNonChatFiberRecovery}.\n */\n async handleNonChatFiber(ctx: FiberRecoveryContext): Promise<boolean> {\n return (await this.adapter.tryHandleNonChatFiberRecovery?.(ctx)) ?? false;\n }\n\n /**\n * The shared wake-recovery LIFECYCLE for an interrupted chat fiber. Both\n * packages drove this exact frame; the divergent organs are the\n * {@link ChatFiberWakeHooks}. In order:\n *\n * 1. non-chat dispatch ({@link handleNonChatFiber}) FIRST, then the chat-fiber\n * name gate — a non-chat fiber is never misread as an orphaned chat turn;\n * 2. parse the request id, unwrap the snapshot, resolve the orphaned stream +\n * reconstruct its partial;\n * 3. classify the turn (retry/continue + package detail) and open the incident;\n * 4. if the budget is already exhausted, persist the settled partial (so\n * non-idempotent tool results are not discarded — #1631) and terminalize\n * BEFORE consulting `onChatRecovery`;\n * 5. otherwise, inside a `failed`-on-throw guard: invoke `onChatRecovery`,\n * apply the shared persist gate (base eligibility AND `persist !== false ||\n * settled tool results`), complete the live stream, then hand the\n * retry/continue/skip DECISION to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}.\n *\n * Returns `true` when the fiber was a chat (or non-chat) recovery the engine\n * handled, `false` when it was not a chat fiber (the caller keeps looking). Any\n * throw after the incident opens flips it to `failed` so it is never left\n * leaking in `attempting`.\n */\n async handleChatFiberRecovery<TClassify>(\n ctx: FiberRecoveryContext,\n wake: ChatFiberWakeHooks<TClassify>\n ): Promise<boolean> {\n const { adapter } = this;\n\n // Ordering invariant: non-chat (messenger/workflow) fibers dispatch BEFORE\n // the chat-fiber gate.\n if (await this.handleNonChatFiber(ctx)) return true;\n\n const chatPrefix = wake.chatFiberPrefix();\n if (!ctx.name.startsWith(chatPrefix)) return false;\n\n const requestId = ctx.name.slice(chatPrefix.length);\n const { snapshot, recoveryData } = wake.unwrapRecoverySnapshot(ctx);\n const stream = adapter.resolveRecoveryStream(requestId);\n const { streamId, streamStillActive, streamStatus } = stream;\n const partial = streamId\n ? adapter.getPartialStreamText(streamId)\n : { text: \"\", parts: [], hasSettledToolResults: false };\n\n const { recoveryKind, detail } = await wake.classifyRecoveredTurn({\n snapshot,\n requestId,\n streamId,\n partial,\n streamStillActive,\n streamStatus\n });\n const recoveryRootRequestId = snapshot?.recoveryRootRequestId ?? requestId;\n\n const { incident, config, exhausted } = await this.beginIncident({\n requestId,\n recoveryRootRequestId,\n latestUserMessageId: snapshot?.latestUserMessageId,\n recoveryKind\n });\n\n if (exhausted) {\n // Preserve the settled partial before sealing. Exhaustion is decided BEFORE\n // `onChatRecovery`, so without this the settled (often non-idempotent) tool\n // results the turn already produced are discarded and the model re-runs\n // them on the next message (#1631). Same gating as the normal path (with no\n // `options`, so the persist clause collapses to base eligibility) — never\n // duplicating a partial an earlier attempt already saved.\n if (\n await this._shouldPersistOrphanedPartial(wake, {\n streamId,\n streamStillActive,\n streamStatus,\n snapshot,\n options: undefined,\n partial\n })\n ) {\n await wake.persistOrphanedStream(streamId);\n }\n await adapter.exhaustChatRecovery(\n incident,\n config,\n partial,\n streamId,\n ctx.createdAt\n );\n return true;\n }\n\n // Any throw after the incident opens (user `onChatRecovery`, orphan\n // persistence, scheduling) must flip the incident to terminal `failed` and\n // emit, otherwise it leaks in `attempting` and is never observable as stuck.\n try {\n const options =\n (await wake.invokeOnChatRecovery?.({\n incident,\n recoveryKind,\n recoveryRootRequestId,\n requestId,\n streamId,\n partial,\n snapshot,\n recoveryData,\n createdAt: ctx.createdAt\n })) ?? {};\n\n if (\n await this._shouldPersistOrphanedPartial(wake, {\n streamId,\n streamStillActive,\n streamStatus,\n snapshot,\n options,\n partial\n })\n ) {\n await wake.persistOrphanedStream(streamId);\n }\n\n if (streamStillActive) {\n await wake.completeRecoveredStream(streamId);\n }\n\n await wake.dispatchRecoveredTurn({\n incident,\n config,\n recoveryKind,\n options,\n snapshot,\n requestId,\n recoveryRootRequestId,\n streamId,\n streamStatus,\n detail\n });\n\n return true;\n } catch (error) {\n await this.updateIncident(\n incident.incidentId,\n \"failed\",\n error instanceof Error ? error.message : String(error)\n );\n throw error;\n }\n }\n\n /**\n * The shared persist gate: base eligibility (the package's\n * {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}) AND the\n * never-drop-settled-work clause `options.persist !== false ||\n * partial.hasSettledToolResults`. `options: undefined` (the exhausted branch)\n * collapses the clause to the base gate. The clause lives here — not in each\n * package — because settled-work preservation is a cross-package invariant\n * (#1631), and the codec (not the engine) decides whether a partial carries\n * settled tool work, so the engine stays wire-vocabulary-agnostic.\n */\n private async _shouldPersistOrphanedPartial<TClassify>(\n wake: ChatFiberWakeHooks<TClassify>,\n input: PersistOrphanedPartialInput & {\n options: ChatRecoveryOptions | undefined;\n partial: RecoveryPartial;\n }\n ): Promise<boolean> {\n const base = await wake.shouldPersistOrphanedPartial({\n streamId: input.streamId,\n streamStillActive: input.streamStillActive,\n streamStatus: input.streamStatus,\n snapshot: input.snapshot\n });\n return (\n base &&\n (input.options?.persist !== false || input.partial.hasSettledToolResults)\n );\n }\n\n async beginIncident(\n input: BeginChatRecoveryIncidentInput\n ): Promise<BeginChatRecoveryIncidentResult> {\n const { adapter } = this;\n const config = adapter.resolveConfig();\n const key = chatRecoveryIncidentKey(chatRecoveryIncidentId(input));\n const now = input.nowMs ?? adapter.now();\n // Ordering invariant: sweep stale incidents BEFORE reading the existing\n // record. A TTL-expired identity is also past its no-progress window, so\n // sweeping first lets a genuinely abandoned turn start fresh instead of\n // resuming a dead budget.\n await adapter.sweepStaleIncidents(now);\n const existing = await adapter.getIncident(key);\n // Ordering invariant: rehydrate interaction state BEFORE the budget reads\n // `isAwaitingClientInteraction()` (see the adapter hook's contract).\n adapter.ensureInteractionStateLoaded?.();\n const currentProgress = await adapter.readProgress();\n\n const { incident, exhausted, events } = await evaluateChatRecoveryIncident({\n identity: input,\n config,\n existing,\n currentProgress,\n awaitingClientInteraction:\n adapter.isAwaitingClientInteraction?.() ?? false,\n now,\n onShouldKeepRecoveringError: (error) =>\n adapter.onShouldKeepRecoveringError?.(error)\n });\n\n await adapter.putIncident(key, incident);\n for (const event of events) {\n adapter.emitRecoveryEvent(event);\n }\n return { incident, config, exhausted };\n }\n\n /**\n * Schedule a recovery continuation/retry: the transition + emit + enqueue\n * triplet both packages repeat at every fiber-recovery and stall-routing\n * decision. In order:\n *\n * 1. transition the incident to `scheduled` (persist + drive the #1620\n * \"recovering…\" status) via {@link updateIncident};\n * 2. emit `chat:recovery:scheduled`; and\n * 3. enqueue the callback through the adapter's idempotent schedule.\n *\n * `recoveryKind` is passed explicitly (not read off the incident) because a\n * caller can legitimately report a different kind than the incident was opened\n * with — e.g. `AIChatAgent`'s lost-partial branch opens a `continue` incident\n * but schedules (and reports) a `retry`. `requestId` always matches\n * `incident.requestId` (the evaluation rewrites it to the current attempt), so\n * it is read from the incident.\n */\n async scheduleRecovery(input: {\n incident: ChatRecoveryIncident;\n recoveryKind: ChatRecoveryKind;\n callback: ChatRecoveryScheduleCallback;\n data: Record<string, unknown>;\n reason?: ChatRecoveryScheduleReason;\n }): Promise<void> {\n const { incident } = input;\n await this.updateIncident(incident.incidentId, \"scheduled\");\n this.adapter.emitRecoveryEvent({\n type: \"chat:recovery:scheduled\",\n incidentId: incident.incidentId,\n requestId: incident.requestId,\n attempt: incident.attempt,\n maxAttempts: incident.maxAttempts,\n recoveryKind: input.recoveryKind\n });\n await this.adapter.scheduleRecovery(\n input.callback,\n input.data,\n input.reason ?? \"initial\",\n 0\n );\n }\n\n /**\n * Reschedule a recovery continuation/retry that timed out waiting for stable\n * state, INSIDE the currently-executing one-shot schedule row. Reads the\n * incident; if it is still under the attempt cap, bumps `attempt`, marks it\n * `scheduled` with `reason:\"stable_timeout_retry\"`, and issues a delayed,\n * NON-idempotent schedule (`alarm()` deletes the executing row only after this\n * returns, so an idempotent reschedule would dedup onto that doomed row and\n * never fire — see {@link chatRecoverySchedulePolicy}).\n *\n * Returns `true` when a retry was scheduled, `false` when there is no incident\n * (no id / record gone) or the attempt budget is already spent — in which case\n * the caller falls through to the give-up path. Deliberately bypasses the\n * `evaluateChatRecoveryIncident` budget (this is a coarse stable-state retry,\n * not a fresh interruption) and {@link updateIncident} (no `scheduled` event /\n * recovering-flag churn on a same-turn reschedule).\n */\n async rescheduleAfterStableTimeout(input: {\n incidentId: string | undefined;\n callback: ChatRecoveryScheduleCallback;\n data: Record<string, unknown> | undefined;\n fallbackMaxAttempts: number;\n }): Promise<boolean> {\n const { adapter } = this;\n if (!input.incidentId) return false;\n const key = chatRecoveryIncidentKey(input.incidentId);\n const incident = await adapter.getIncident(key);\n if (!incident) return false;\n const attempt = incident.attempt ?? 0;\n if (attempt >= (incident.maxAttempts ?? input.fallbackMaxAttempts)) {\n return false;\n }\n await adapter.putIncident(key, {\n ...incident,\n attempt: attempt + 1,\n status: \"scheduled\",\n lastAttemptAt: adapter.now(),\n reason: \"stable_timeout_retry\"\n });\n await adapter.scheduleRecovery(\n input.callback,\n input.data ?? {},\n \"stable_timeout_retry\",\n CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS\n );\n return true;\n }\n\n /**\n * Give up on a recovery turn whose retry budget drained, terminalizing it so\n * it can never become an eternal spinner (#1645). The shared spine both\n * packages repeated verbatim:\n *\n * 1. resolve config + the incident key from `data.incidentId`;\n * 2. best-effort READ the stored incident — a failed read is tolerated\n * (reported via `onGiveUpBookkeepingError(\"read\", …)`) and the incident is\n * synthesized, because the read backs only the re-entry guard, not the\n * terminal UX;\n * 3. re-entry guard: a `stored.status === \"exhausted\"` record means\n * terminalization already fired, so a duplicate stale alarm returns without\n * re-broadcasting the banner;\n * 4. build the exhausted incident (reuse `stored`, or synthesize a minimal one\n * so a swept/missing record STILL terminalizes through `onExhausted`);\n * 5. resolve the orphaned stream id + partial;\n * 6. terminalize via `exhaustChatRecovery` — BEFORE sealing. The terminal\n * writes can reject with a platform transient in the deploy/storage window\n * a give-up runs in (#1730); letting that throw propagate is deliberate, so\n * `Agent._executeScheduleCallback` defers the one-shot row and the WHOLE\n * give-up re-runs on a healthy isolate. Sealing first would arm the\n * re-entry guard and turn that re-run into a no-op, dropping the durable\n * terminal record. The re-run is idempotent (terminal writes overwrite the\n * same key); a second banner is the documented at-least-once edge; and\n * 7. best-effort SEAL write so the re-entry guard sees `exhausted` on a\n * duplicate alarm — a failed seal (reported via\n * `onGiveUpBookkeepingError(\"seal\", …)`) costs at most one re-delivered\n * banner.\n *\n * The two packages diverged only in parameters the caller supplies:\n * `reason` (`Think` passes `stable_timeout` | `recovery_error`; `AIChatAgent`\n * always `stable_timeout`) and the root-id chain (`Think` includes\n * `recoveredRequestId`; `AIChatAgent` never sets it, so the unified chain\n * collapses identically). Exactly-once terminalization rests on the re-entry\n * guard alone in `AIChatAgent`; `Think` additionally short-circuits duplicate\n * alarms earlier in its durable-submission layer.\n */\n async exhaustRecoveryGiveUp(input: {\n callback: ChatRecoveryScheduleCallback;\n data:\n | {\n incidentId?: string;\n originalRequestId?: string;\n recoveredRequestId?: string;\n }\n | undefined;\n reason: string;\n }): Promise<void> {\n const { adapter } = this;\n const config = adapter.resolveConfig();\n const incidentKey = input.data?.incidentId\n ? chatRecoveryIncidentKey(input.data.incidentId)\n : null;\n\n let stored: ChatRecoveryIncident | null = null;\n if (incidentKey) {\n try {\n stored = await adapter.getIncident(incidentKey);\n } catch (readError) {\n adapter.onGiveUpBookkeepingError(\"read\", readError);\n }\n }\n\n // Re-entry guard: a sealed incident means terminalization already happened,\n // so a duplicate stale alarm must not re-fire `onExhausted` / the banner.\n if (stored?.status === \"exhausted\") return;\n\n const rootRequestId =\n input.data?.originalRequestId ??\n input.data?.recoveredRequestId ??\n adapter.activeChatRecoveryRootRequestId() ??\n stored?.recoveryRootRequestId ??\n stored?.requestId ??\n \"\";\n\n const incident: ChatRecoveryIncident = stored\n ? { ...stored, status: \"exhausted\", reason: input.reason }\n : {\n // Silent-drop guard: the record is gone (no `incidentId`, or it was\n // swept/deleted before this stale alarm). Synthesize a minimal\n // incident so the turn STILL terminalizes instead of vanishing.\n incidentId: input.data?.incidentId ?? crypto.randomUUID(),\n requestId: rootRequestId,\n recoveryRootRequestId: rootRequestId,\n recoveryKind:\n input.callback === \"_chatRecoveryRetry\" ? \"retry\" : \"continue\",\n attempt: config.maxAttempts,\n maxAttempts: config.maxAttempts,\n status: \"exhausted\",\n firstSeenAt: adapter.now(),\n lastAttemptAt: adapter.now(),\n reason: input.reason\n };\n\n const { streamId } = adapter.resolveRecoveryStream(\n incident.recoveryRootRequestId ?? incident.requestId\n );\n const partial = streamId\n ? adapter.getPartialStreamText(streamId)\n : { text: \"\", parts: [], hasSettledToolResults: false };\n\n await adapter.exhaustChatRecovery(\n incident,\n config,\n partial,\n streamId,\n incident.firstSeenAt\n );\n\n if (incidentKey) {\n try {\n await adapter.putIncident(incidentKey, incident);\n } catch (writeError) {\n adapter.onGiveUpBookkeepingError(\"seal\", writeError);\n }\n }\n }\n\n /**\n * Apply a status transition to the recovery incident `incidentId`:\n *\n * - `completed` → drop the record (terminal, never retried);\n * - any other status → persist the new status (and `reason`), so the attempt\n * budget survives restarts until the TTL sweep reclaims it;\n * - emit the matching `completed`/`skipped`/`failed` lifecycle event; and\n * - drive the live \"recovering…\" status (#1620): `scheduled` marks it active\n * (keyed by the recovery-root request id), terminal states clear it.\n *\n * No-op when `incidentId` is undefined or the record is already gone. This is\n * the transition twin of {@link beginIncident}: all I/O is adapter-owned, the\n * engine owns only the state-machine shape.\n */\n async updateIncident(\n incidentId: string | undefined,\n status: ChatRecoveryIncident[\"status\"],\n reason?: string\n ): Promise<void> {\n if (!incidentId) return;\n const { adapter } = this;\n const key = chatRecoveryIncidentKey(incidentId);\n const incident = await adapter.getIncident(key);\n if (!incident) return;\n\n if (status === \"completed\") {\n await adapter.deleteIncident(key);\n } else {\n await adapter.putIncident(key, {\n ...incident,\n status,\n ...(reason ? { reason } : {})\n });\n }\n\n const eventType =\n status === \"completed\"\n ? \"chat:recovery:completed\"\n : status === \"skipped\"\n ? \"chat:recovery:skipped\"\n : status === \"failed\"\n ? \"chat:recovery:failed\"\n : undefined;\n if (eventType) {\n adapter.emitRecoveryEvent({\n type: eventType,\n incidentId,\n requestId: incident.requestId,\n attempt: incident.attempt,\n maxAttempts: incident.maxAttempts,\n recoveryKind: incident.recoveryKind,\n ...(reason ? { reason } : {})\n });\n }\n\n if (status === \"scheduled\") {\n await adapter.setRecovering(\n true,\n incident.recoveryRootRequestId ?? incident.requestId\n );\n } else if (\n status === \"completed\" ||\n status === \"skipped\" ||\n status === \"failed\"\n ) {\n await adapter.setRecovering(false);\n }\n }\n}\n\n/**\n * Build the `ChatRecoveryExhaustedContext` delivered to `onExhausted` and the\n * `chat:recovery:exhausted` event. Pure field-mapping shared by both packages;\n * the `reason` falls back to `max_attempts_exceeded` when the incident did not\n * record a more specific cause.\n */\nexport function buildChatRecoveryExhaustedContext(input: {\n incident: ChatRecoveryIncident;\n config: ResolvedChatRecoveryConfig;\n partialText: string;\n partialParts: ChatRecoveryExhaustedContext[\"partialParts\"];\n streamId: string;\n createdAt: number;\n}): ChatRecoveryExhaustedContext {\n const { incident, config } = input;\n return {\n incidentId: incident.incidentId,\n requestId: incident.requestId,\n recoveryRootRequestId: incident.recoveryRootRequestId ?? incident.requestId,\n attempt: incident.attempt,\n maxAttempts: incident.maxAttempts,\n recoveryKind: incident.recoveryKind,\n streamId: input.streamId,\n createdAt: input.createdAt,\n partialText: input.partialText,\n partialParts: input.partialParts,\n reason: incident.reason ?? \"max_attempts_exceeded\",\n terminalMessage: config.terminalMessage\n };\n}\n\n/**\n * Run the shared exhaustion notification: emit `chat:recovery:exhausted`, then\n * invoke the caller's `onExhausted` hook. A throwing hook is swallowed (and\n * reported via `onError`) so it can NEVER prevent the caller from delivering\n * terminal UX — a tested invariant in both packages. The terminal record /\n * banner / submission writes that follow are intentionally package-owned (their\n * ordering legitimately diverges), so they are NOT part of this helper.\n */\nexport async function notifyChatRecoveryExhausted(\n ctx: ChatRecoveryExhaustedContext,\n hooks: {\n emit: (ctx: ChatRecoveryExhaustedContext) => void;\n onExhausted?: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;\n onError: (error: unknown) => void;\n }\n): Promise<void> {\n hooks.emit(ctx);\n try {\n await hooks.onExhausted?.(ctx);\n } catch (error) {\n hooks.onError(error);\n }\n}\n\n/**\n * The complete give-up choreography from a single call: build the exhausted\n * context, fire the shared notification ({@link notifyChatRecoveryExhausted}),\n * then hand that context to the host's `terminalize` step. Folds the\n * `buildChatRecoveryExhaustedContext` → `notifyChatRecoveryExhausted` → host\n * terminalize sequence that every host's `_exhaustChatRecovery` repeated.\n *\n * What this OWNS (the invariant, so it cannot drift per host):\n * - the notification ALWAYS runs before any terminal write, and\n * - a throwing `onExhausted` can NEVER block terminal delivery — it is swallowed\n * via `onError` (a tested invariant in both published packages).\n *\n * What it deliberately does NOT own: the terminal-record / broadcast /\n * recovering-clear writes — their exact set diverges per host (both\n * `AIChatAgent` and `Think` broadcast the banner first so it survives a storage\n * write that rejects mid-deploy; `Think` additionally writes a submission row)\n * — see {@link ChatRecoveryAdapter.exhaustChatRecovery}. The host expresses\n * those writes inside `terminalize`. A `terminalize` that throws DOES propagate,\n * so the whole give-up re-runs on a healthy isolate (#1730); see\n * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.\n *\n * `partialParts` is passed explicitly (not derived from a `RecoveryPartial`) so a\n * foreign-vocabulary host can pass `[]` rather than fabricate AI-SDK parts — the\n * engine seam stays parts-vocabulary-agnostic.\n */\nexport async function runChatRecoveryExhaustion(\n input: {\n incident: ChatRecoveryIncident;\n config: ResolvedChatRecoveryConfig;\n partialText: string;\n partialParts: ChatRecoveryExhaustedContext[\"partialParts\"];\n streamId: string;\n createdAt: number;\n },\n hooks: {\n emit: (ctx: ChatRecoveryExhaustedContext) => void;\n onExhausted?: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;\n onError: (error: unknown) => void;\n terminalize: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;\n }\n): Promise<void> {\n const ctx = buildChatRecoveryExhaustedContext({\n incident: input.incident,\n config: input.config,\n partialText: input.partialText,\n partialParts: input.partialParts,\n streamId: input.streamId,\n createdAt: input.createdAt\n });\n await notifyChatRecoveryExhausted(ctx, {\n emit: hooks.emit,\n onExhausted: hooks.onExhausted,\n onError: hooks.onError\n });\n await hooks.terminalize(ctx);\n}\n","/**\n * Shared inactivity watchdog for UI-message streams.\n *\n * A model/transport stream can park indefinitely without ever throwing (a hung\n * provider, a wedged transport). Left unguarded, the consumer read-loop waits\n * forever. {@link iterateWithStallWatchdog} wraps such a stream so that a gap of\n * `timeoutMs` between chunks aborts the upstream and throws\n * {@link ChatStreamStalledError}, letting the consumer route the stall into\n * bounded recovery (#1626) — a transient hang is retried within the existing\n * recovery budget — while genuine in-band errors stay terminal.\n *\n * @internal Sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API. See\n * `design/rfc-chat-recovery-foundation.md`.\n */\n\n/**\n * Thrown by {@link iterateWithStallWatchdog} when the inactivity watchdog fires\n * (a model/transport stream that parks without ever throwing). Distinct from\n * in-band model/stream errors so the read-loop catch can route a stall into\n * bounded recovery (#1626) — a transient hang is retried within the existing\n * recovery budget — while genuine errors stay terminal.\n */\nexport class ChatStreamStalledError extends Error {\n readonly isChatStreamStall = true;\n constructor(message: string) {\n super(message);\n this.name = \"ChatStreamStalledError\";\n }\n}\n\n/**\n * Wrap a UI-message stream with an inactivity watchdog. If no chunk arrives\n * within `timeoutMs`, `onStall` runs (aborting the upstream model stream) and\n * the iterator throws, so the consumer loop exits with a terminal error\n * instead of parking forever on a hung provider/transport. `timeoutMs <= 0`\n * passes the source through untouched.\n */\nexport async function* iterateWithStallWatchdog<T>(\n source: AsyncIterable<T>,\n timeoutMs: number,\n onStall: () => void\n): AsyncGenerator<T> {\n if (!(timeoutMs > 0)) {\n yield* source;\n return;\n }\n const iterator = source[Symbol.asyncIterator]();\n // Tracks whether the watchdog itself aborted the upstream. In that case we\n // must NOT also `iterator.return()` it: cancelling the readable after the\n // abort makes the AI SDK pipeline write to an already-cancelled readable\n // (\"readable side is no longer readable\"). Letting the abort error the\n // stream is the clean path.\n let selfAborted = false;\n try {\n while (true) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let stalled = false;\n const stall = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n stalled = true;\n reject(\n new ChatStreamStalledError(\n `Chat stream stalled: no activity for ${timeoutMs}ms; the turn was aborted by the stall watchdog.`\n )\n );\n }, timeoutMs);\n });\n const nextPromise = iterator.next();\n // If the watchdog wins the race we abandon this read; aborting the\n // upstream stream makes it reject later, so pre-attach a no-op catch to\n // keep that abandoned rejection from surfacing as an unhandled rejection.\n nextPromise.catch(() => {});\n let next: IteratorResult<T>;\n try {\n next = await Promise.race([nextPromise, stall]);\n } catch (err) {\n if (stalled) {\n selfAborted = true;\n onStall();\n }\n throw err;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n if (next.done) return;\n yield next.value;\n }\n } finally {\n // Forward early termination (consumer `break`/`throw`, e.g. an in-band\n // stream error where the abort signal is NOT set) to the source so its\n // reader is cancelled — otherwise the wrapped source would leak when the\n // consumer stops reading mid-stream. Skipped after a watchdog stall, which\n // already aborted the upstream (see `selfAborted` above).\n if (!selfAborted) {\n await iterator.return?.(undefined as never).catch(() => {});\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["textEncoder","MSG_STREAM_RESUME_NONE"],"sources":["../../src/chat/sanitize.ts","../../src/chat/turn-queue.ts","../../src/chat/submit-concurrency.ts","../../src/chat/protocol.ts","../../src/chat/connection.ts","../../src/chat/resumable-stream.ts","../../src/chat/sql-batch.ts","../../src/chat/client-tools.ts","../../src/chat/continuation-state.ts","../../src/chat/pre-stream-turns.ts","../../src/chat/auto-continuation-controller.ts","../../src/chat/abort-registry.ts","../../src/chat/async-helpers.ts","../../src/chat/tool-state.ts","../../src/chat/parse-protocol.ts","../../src/chat/message-reconciler.ts","../../src/chat/repair-transcript.ts","../../src/chat/orphan-persist.ts","../../src/chat/recovery.ts","../../src/chat/recovery-codec.ts","../../src/chat/resume-handshake.ts","../../src/chat/recovery-incident.ts","../../src/chat/recovery-engine.ts","../../src/chat/stall-watchdog.ts"],"sourcesContent":["/**\n * Message sanitization and row-size enforcement utilities.\n *\n * Shared by @cloudflare/ai-chat and @cloudflare/think to ensure persistence\n * hygiene: stripping ephemeral provider metadata and compacting\n * oversized messages before writing to SQLite.\n */\n\nimport type { ProviderMetadata, ReasoningUIPart, UIMessage } from \"ai\";\nimport { truncateToolOutput } from \"./tool-output-truncation\";\n\nconst textEncoder = new TextEncoder();\n\n/** Maximum serialized message size before compaction (bytes). 1.8MB with headroom below SQLite's 2MB limit. */\nexport const ROW_MAX_BYTES = 1_800_000;\n\n/** Measure UTF-8 byte length of a string. */\nexport function byteLength(s: string): number {\n return textEncoder.encode(s).byteLength;\n}\n\n/**\n * Sanitize a message for persistence by removing ephemeral provider-specific\n * data that should not be stored or sent back in subsequent requests.\n *\n * 1. Strips OpenAI ephemeral fields (itemId, reasoningEncryptedContent)\n * 2. Filters truly empty reasoning parts (no text, no remaining providerMetadata)\n */\nexport function sanitizeMessage(message: UIMessage): UIMessage {\n const strippedParts = message.parts.map((part) => {\n let sanitizedPart = part;\n\n if (\n \"providerMetadata\" in sanitizedPart &&\n sanitizedPart.providerMetadata &&\n typeof sanitizedPart.providerMetadata === \"object\" &&\n \"openai\" in sanitizedPart.providerMetadata\n ) {\n sanitizedPart = stripOpenAIMetadata(sanitizedPart, \"providerMetadata\");\n }\n\n if (\n \"callProviderMetadata\" in sanitizedPart &&\n sanitizedPart.callProviderMetadata &&\n typeof sanitizedPart.callProviderMetadata === \"object\" &&\n \"openai\" in sanitizedPart.callProviderMetadata\n ) {\n sanitizedPart = stripOpenAIMetadata(\n sanitizedPart,\n \"callProviderMetadata\"\n );\n }\n\n return sanitizedPart;\n }) as UIMessage[\"parts\"];\n\n const sanitizedParts = strippedParts.filter((part) => {\n if (part.type === \"reasoning\") {\n const reasoningPart = part as ReasoningUIPart;\n if (!reasoningPart.text || reasoningPart.text.trim() === \"\") {\n if (\n \"providerMetadata\" in reasoningPart &&\n reasoningPart.providerMetadata &&\n typeof reasoningPart.providerMetadata === \"object\" &&\n Object.keys(reasoningPart.providerMetadata).length > 0\n ) {\n return true;\n }\n return false;\n }\n }\n return true;\n });\n\n return { ...message, parts: sanitizedParts };\n}\n\nfunction stripOpenAIMetadata<T extends UIMessage[\"parts\"][number]>(\n part: T,\n metadataKey: \"providerMetadata\" | \"callProviderMetadata\"\n): T {\n const metadata = (part as Record<string, unknown>)[metadataKey] as {\n openai?: Record<string, unknown>;\n [key: string]: unknown;\n };\n\n if (!metadata?.openai) return part;\n\n const {\n itemId: _itemId,\n reasoningEncryptedContent: _rec,\n ...restOpenai\n } = metadata.openai;\n\n const hasOtherOpenaiFields = Object.keys(restOpenai).length > 0;\n const { openai: _openai, ...restMetadata } = metadata;\n\n let newMetadata: ProviderMetadata | undefined;\n if (hasOtherOpenaiFields) {\n newMetadata = { ...restMetadata, openai: restOpenai } as ProviderMetadata;\n } else if (Object.keys(restMetadata).length > 0) {\n newMetadata = restMetadata as ProviderMetadata;\n }\n\n const { [metadataKey]: _oldMeta, ...restPart } = part as Record<\n string,\n unknown\n >;\n\n if (newMetadata) {\n return { ...restPart, [metadataKey]: newMetadata } as T;\n }\n return restPart as T;\n}\n\n/** Optional hooks for {@link enforceRowSizeLimit}. */\nexport interface EnforceRowSizeLimitOptions {\n /**\n * Optional logger invoked when a message has to be compacted/truncated. The\n * package supplies its own log prefix (log prefixes stay package-specific).\n */\n warn?: (message: string) => void;\n}\n\n/**\n * Enforce SQLite row size limits by compacting tool outputs and text parts\n * when a serialized message exceeds the safety threshold (1.8MB). Shared by\n * `@cloudflare/ai-chat` and `@cloudflare/think` so both compact identically.\n *\n * Compaction strategy:\n * 1. Compact tool outputs over 1KB with {@link truncateToolOutput}, preserving\n * the structured output shape, and annotate `metadata.compactedToolOutputs`\n * with the compacted tool-call IDs.\n * 2. If still too big, truncate text parts from oldest to newest, annotating\n * `metadata.compactedTextParts` with the truncated part indices.\n */\nexport function enforceRowSizeLimit(\n message: UIMessage,\n options?: EnforceRowSizeLimitOptions\n): UIMessage {\n let json = JSON.stringify(message);\n let size = byteLength(json);\n if (size <= ROW_MAX_BYTES) return message;\n\n if (message.role !== \"assistant\") {\n options?.warn?.(\n `Non-assistant message ${message.id} is ${size} bytes, exceeds row ` +\n `limit. Truncating text parts.`\n );\n return truncateTextParts(message);\n }\n\n options?.warn?.(\n `Message ${message.id} is ${size} bytes, compacting tool outputs to fit ` +\n `SQLite row limit`\n );\n\n const compactedToolCallIds: string[] = [];\n const compactedParts = message.parts.map((part) => {\n if (\n \"output\" in part &&\n \"toolCallId\" in part &&\n \"state\" in part &&\n part.state === \"output-available\"\n ) {\n const output = (part as { output: unknown }).output;\n const truncated = truncateToolOutput(output, 1000);\n if (truncated.truncated) {\n compactedToolCallIds.push(part.toolCallId as string);\n return {\n ...part,\n output: truncated.output\n };\n }\n }\n return part;\n }) as UIMessage[\"parts\"];\n\n const result: UIMessage = { ...message, parts: compactedParts };\n if (compactedToolCallIds.length > 0) {\n result.metadata = {\n ...(result.metadata ?? {}),\n compactedToolOutputs: compactedToolCallIds\n };\n }\n\n json = JSON.stringify(result);\n size = byteLength(json);\n if (size <= ROW_MAX_BYTES) return result;\n\n options?.warn?.(\n `Message ${message.id} still ${size} bytes after tool compaction, ` +\n `truncating text parts`\n );\n return truncateTextParts(result);\n}\n\nfunction truncateTextParts(message: UIMessage): UIMessage {\n const compactedTextPartIndices: number[] = [];\n const parts = [...message.parts];\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n if (part.type === \"text\" && \"text\" in part) {\n const text = (part as { text: string }).text;\n if (text.length > 1000) {\n compactedTextPartIndices.push(i);\n parts[i] = {\n ...part,\n text:\n `[Text truncated for storage (${text.length} chars). ` +\n `First 500 chars: ${text.slice(0, 500)}...]`\n } as UIMessage[\"parts\"][number];\n\n const candidate = { ...message, parts };\n if (byteLength(JSON.stringify(candidate)) <= ROW_MAX_BYTES) {\n break;\n }\n }\n }\n }\n\n const result: UIMessage = { ...message, parts };\n if (compactedTextPartIndices.length > 0) {\n result.metadata = {\n ...(result.metadata ?? {}),\n compactedTextParts: compactedTextPartIndices\n };\n }\n return result;\n}\n","/**\n * TurnQueue — serial async queue with generation-based invalidation.\n *\n * Serializes async work via a promise chain, tracks which request is\n * currently active, and lets callers invalidate all queued work by\n * advancing a generation counter.\n *\n * Used by @cloudflare/ai-chat (full concurrency policy spectrum) and\n * @cloudflare/think (simple serial queue) to prevent overlapping\n * chat turns.\n */\n\nexport type TurnResult<T> =\n | { status: \"completed\"; value: T }\n | { status: \"stale\" };\n\nexport interface EnqueueOptions {\n /**\n * Generation to bind this turn to. Defaults to the current generation\n * at the time of the `enqueue` call. If the queue's generation has\n * advanced past this value by the time the turn reaches the front,\n * `fn` is not called and `{ status: \"stale\" }` is returned.\n */\n generation?: number;\n}\n\nexport class TurnQueue {\n private _queue: Promise<void> = Promise.resolve();\n private _generation = 0;\n private _activeRequestId: string | null = null;\n private _countsByGeneration = new Map<number, number>();\n\n get generation(): number {\n return this._generation;\n }\n\n get activeRequestId(): string | null {\n return this._activeRequestId;\n }\n\n get isActive(): boolean {\n return this._activeRequestId !== null;\n }\n\n async enqueue<T>(\n requestId: string,\n fn: () => Promise<T>,\n options?: EnqueueOptions\n ): Promise<TurnResult<T>> {\n const previousTurn = this._queue;\n let releaseTurn!: () => void;\n const capturedGeneration = options?.generation ?? this._generation;\n\n this._countsByGeneration.set(\n capturedGeneration,\n (this._countsByGeneration.get(capturedGeneration) ?? 0) + 1\n );\n\n this._queue = new Promise<void>((resolve) => {\n releaseTurn = resolve;\n });\n\n await previousTurn;\n\n if (this._generation !== capturedGeneration) {\n this._decrementCount(capturedGeneration);\n releaseTurn();\n return { status: \"stale\" };\n }\n\n this._activeRequestId = requestId;\n try {\n const value = await fn();\n return { status: \"completed\", value };\n } finally {\n this._activeRequestId = null;\n this._decrementCount(capturedGeneration);\n releaseTurn();\n }\n }\n\n /**\n * Advance the generation counter. All turns enqueued under older\n * generations will be skipped when they reach the front of the queue.\n */\n reset(): void {\n this._generation++;\n }\n\n /**\n * Wait until the queue is fully drained (no pending or active turns).\n */\n async waitForIdle(): Promise<void> {\n let queue: Promise<void>;\n do {\n queue = this._queue;\n await queue;\n } while (this._queue !== queue);\n }\n\n /**\n * Number of active + queued turns for a given generation.\n * Defaults to the current generation.\n */\n queuedCount(generation?: number): number {\n return this._countsByGeneration.get(generation ?? this._generation) ?? 0;\n }\n\n private _decrementCount(generation: number): void {\n const count = (this._countsByGeneration.get(generation) ?? 1) - 1;\n if (count <= 0) {\n this._countsByGeneration.delete(generation);\n } else {\n this._countsByGeneration.set(generation, count);\n }\n }\n}\n","import type { MessageConcurrency } from \"./lifecycle\";\n\nexport type NormalizedMessageConcurrency =\n | \"queue\"\n | \"latest\"\n | \"merge\"\n | \"drop\"\n | {\n strategy: \"debounce\";\n debounceMs: number;\n };\n\nexport type SubmitConcurrencyDecision = {\n action: \"execute\" | \"drop\";\n strategy: NormalizedMessageConcurrency | null;\n submitSequence: number | null;\n debounceUntilMs: number | null;\n};\n\nexport class SubmitConcurrencyController {\n private _submitSequence = 0;\n private _latestOverlappingSubmitSequence = 0;\n private _pendingEnqueueCount = 0;\n private _resetEpoch = 0;\n private _activeDebounceTimers = new Set<ReturnType<typeof setTimeout>>();\n private _activeDebounceResolves = new Set<() => void>();\n\n constructor(private readonly options: { defaultDebounceMs: number }) {}\n\n get pendingEnqueueCount(): number {\n return this._pendingEnqueueCount;\n }\n\n get overlappingSubmitCount(): number {\n return this._latestOverlappingSubmitSequence;\n }\n\n decide(options: {\n concurrency: MessageConcurrency;\n isSubmitMessage: boolean;\n queuedTurns: number;\n }): SubmitConcurrencyDecision {\n const queuedTurnsInCurrentEpoch =\n options.queuedTurns + this._pendingEnqueueCount;\n\n if (!options.isSubmitMessage || queuedTurnsInCurrentEpoch === 0) {\n return {\n action: \"execute\",\n strategy: null,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n const concurrency = this.normalize(options.concurrency);\n if (concurrency === \"drop\") {\n return {\n action: \"drop\",\n strategy: concurrency,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n if (concurrency === \"queue\") {\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n const submitSequence = ++this._submitSequence;\n this._latestOverlappingSubmitSequence = submitSequence;\n\n if (concurrency === \"latest\" || concurrency === \"merge\") {\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence,\n debounceUntilMs: null\n };\n }\n\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence,\n debounceUntilMs: Date.now() + concurrency.debounceMs\n };\n }\n\n /**\n * Mark a submit as accepted and in-flight between admission and turn\n * queue registration. Returns an idempotent `release()` function that\n * must be called when the submit either reaches the turn queue or is\n * abandoned. The returned function is bound to the controller's reset\n * epoch — releases from before the most recent `reset()` are no-ops,\n * so post-reset submits keep an accurate count.\n */\n beginEnqueue(): () => void {\n this._pendingEnqueueCount++;\n const epoch = this._resetEpoch;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (this._resetEpoch !== epoch) return;\n this._pendingEnqueueCount = Math.max(0, this._pendingEnqueueCount - 1);\n };\n }\n\n isSuperseded(submitSequence: number | null): boolean {\n return (\n submitSequence !== null &&\n submitSequence < this._latestOverlappingSubmitSequence\n );\n }\n\n async waitForTimestamp(timestampMs: number): Promise<void> {\n const remainingMs = timestampMs - Date.now();\n if (remainingMs <= 0) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n const wrappedResolve = () => {\n this._activeDebounceResolves.delete(wrappedResolve);\n resolve();\n };\n const timer = setTimeout(() => {\n this._activeDebounceTimers.delete(timer);\n wrappedResolve();\n }, remainingMs);\n\n this._activeDebounceTimers.add(timer);\n this._activeDebounceResolves.add(wrappedResolve);\n });\n }\n\n cancelActiveDebounce(): void {\n for (const timer of this._activeDebounceTimers) {\n clearTimeout(timer);\n }\n this._activeDebounceTimers.clear();\n\n const resolves = [...this._activeDebounceResolves];\n this._activeDebounceResolves.clear();\n for (const resolve of resolves) {\n resolve();\n }\n }\n\n reset(): void {\n this._resetEpoch++;\n this._pendingEnqueueCount = 0;\n this.cancelActiveDebounce();\n }\n\n async waitForIdle(waitForQueueIdle: () => Promise<void>): Promise<void> {\n while (true) {\n await waitForQueueIdle();\n if (this._pendingEnqueueCount === 0) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 5));\n }\n }\n\n private normalize(\n concurrency: MessageConcurrency\n ): NormalizedMessageConcurrency {\n if (typeof concurrency === \"string\") {\n return concurrency;\n }\n\n const debounceMs = concurrency.debounceMs;\n\n return {\n strategy: \"debounce\",\n debounceMs:\n typeof debounceMs === \"number\" &&\n Number.isFinite(debounceMs) &&\n debounceMs >= 0\n ? debounceMs\n : this.options.defaultDebounceMs\n };\n }\n}\n","/**\n * Wire protocol message type constants for the cf_agent_chat_* protocol.\n *\n * These are the string values used on the wire between agent servers and\n * clients. Both @cloudflare/ai-chat (via its MessageType enum) and\n * @cloudflare/think use these values.\n */\nexport const CHAT_MESSAGE_TYPES = {\n CHAT_MESSAGES: \"cf_agent_chat_messages\",\n USE_CHAT_REQUEST: \"cf_agent_use_chat_request\",\n USE_CHAT_RESPONSE: \"cf_agent_use_chat_response\",\n CHAT_CLEAR: \"cf_agent_chat_clear\",\n CHAT_REQUEST_CANCEL: \"cf_agent_chat_request_cancel\",\n STREAM_RESUMING: \"cf_agent_stream_resuming\",\n STREAM_RESUME_ACK: \"cf_agent_stream_resume_ack\",\n STREAM_RESUME_REQUEST: \"cf_agent_stream_resume_request\",\n STREAM_RESUME_NONE: \"cf_agent_stream_resume_none\",\n // Server→client: a turn has been accepted but its resumable stream has not\n // started yet (queued, debouncing, waiting on MCP setup, or running async\n // work in `onChatMessage`). Sent in response to a resume request (or on\n // connect) so a reconnecting/re-mounting client keeps its expectation instead\n // of resolving its resume probe to \"no stream\" and then false-timing-out the\n // turn. Resolved by a later `STREAM_RESUMING` (stream started) or\n // `STREAM_RESUME_NONE` (turn settled without streaming). Backward-compatible —\n // clients that don't understand it ignore it. See issue #1784.\n STREAM_PENDING: \"cf_agent_stream_pending\",\n TOOL_RESULT: \"cf_agent_tool_result\",\n TOOL_APPROVAL: \"cf_agent_tool_approval\",\n MESSAGE_UPDATED: \"cf_agent_message_updated\",\n // Server→client: a durable chat turn is being recovered (interrupted by a\n // deploy/eviction or a stream-stall watchdog abort and now resuming). Sent\n // when a recovery continuation is scheduled and cleared on every terminal\n // outcome; `@cloudflare/think` also replays it on connect so a client that\n // joins mid-recovery learns it. Purely a progress hint — backward-compatible\n // (clients that don't understand it ignore it). See issue #1620.\n CHAT_RECOVERING: \"cf_agent_chat_recovering\"\n} as const;\n","/**\n * Connection I/O — shared WebSocket send guard for chat agents.\n *\n * `@internal` — sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API. See\n * `design/rfc-chat-recovery-foundation.md`.\n *\n * Both packages (and `continuation-state`) hand-maintained byte-identical\n * copies of `sendIfOpen` / `isWebSocketClosedSendError`; this is the single\n * shared implementation.\n */\n\n/**\n * Minimal connection interface for sending WebSocket messages. Matches the\n * `Connection` type from `agents` without importing it: `Connection` extends\n * `WebSocket` with its own `send` overload, so it is structurally assignable.\n */\nexport interface ChatConnection {\n readonly id: string;\n send(message: string): void;\n}\n\n/**\n * Send a message on a connection, swallowing the specific\n * \"send after close\" error a racing disconnect produces. Returns `true` if the\n * send went out, `false` if the socket was already closed. Any other error\n * rethrows.\n */\nexport function sendIfOpen(\n connection: ChatConnection,\n message: string\n): boolean {\n try {\n connection.send(message);\n return true;\n } catch (error) {\n if (isWebSocketClosedSendError(error)) return false;\n throw error;\n }\n}\n\n/** Whether an error is the \"WebSocket send() after close\" `TypeError`. */\nexport function isWebSocketClosedSendError(error: unknown): boolean {\n return (\n error instanceof TypeError &&\n error.message.includes(\"WebSocket send() after close\")\n );\n}\n","/**\n * ResumableStream: Standalone class for buffering, persisting, and replaying\n * stream chunks in SQLite. Extracted from AIChatAgent to separate concerns.\n *\n * Handles:\n * - Chunk buffering (batched writes to SQLite for performance)\n * - Stream lifecycle (start, complete, error)\n * - Chunk replay for reconnecting clients\n * - Stale stream cleanup\n * - Active stream restoration after agent restart\n */\n\nimport { nanoid } from \"nanoid\";\nimport type { Connection } from \"agents\";\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\nimport { sendIfOpen } from \"./connection\";\n\n/** Number of chunks to pack into a single SQLite row before flushing */\nconst CHUNK_BUFFER_SIZE = 10;\n/** Maximum buffer size to prevent memory issues on rapid reconnections */\nconst CHUNK_BUFFER_MAX_SIZE = 100;\n/**\n * Max accumulated raw chunk bytes packed into one row before forcing a flush.\n * The SQLite row limit is 2 MB; packing serializes bodies into a JSON array,\n * which re-escapes their contents (quotes/backslashes), so we keep the raw\n * total well under the limit to leave generous headroom for escaping overhead.\n * A chunk larger than this is flushed as its own (unwrapped) row.\n */\nconst SEGMENT_MAX_BYTES = 512_000;\n/** Default cleanup interval for old streams (ms) - every 10 minutes */\nconst CLEANUP_INTERVAL_MS = 10 * 60 * 1000;\n/**\n * Retention for completed/errored stream buffers, measured from completion.\n *\n * The assistant message is persisted separately (`cf_ai_chat_agent_messages`),\n * so once a stream completes its buffer is no longer the source of truth — it\n * is only a brief reconnect-and-replay grace window: long enough to cover a\n * client that dropped at the completion boundary and reconnects to replay the\n * just-finished stream, and to deliver a pending terminal error frame on a\n * resumed stream (#1645). It is deliberately short (not the chat's lifetime)\n * so idle/one-off chat DOs don't accumulate stale buffers (#1706).\n */\nconst COMPLETED_RETENTION_MS = 10 * 60 * 1000;\n/**\n * Retention for abandoned `streaming` rows, measured from LAST chunk activity.\n *\n * Generous relative to {@link COMPLETED_RETENTION_MS}: an interrupted turn must\n * have ample time to be resumed by a reconnecting client or healed by fiber\n * recovery before its buffer is reaped. Only a stream that has produced no\n * chunk for this long is treated as truly dead. Keyed off last activity (not\n * start time) so a long but still-active stream is never swept mid-flight.\n */\nconst ABANDONED_STREAM_RETENTION_MS = 60 * 60 * 1000;\n/** Shared encoder for UTF-8 byte length measurement */\nconst textEncoder = new TextEncoder();\n\n/**\n * How far ahead (seconds) to schedule the resumable-stream buffer cleanup\n * alarm. Set to the short completion-grace window ({@link COMPLETED_RETENTION_MS},\n * 10m) so a finished buffer is reclaimed promptly. The re-arm-while-reclaimable\n * loop (see {@link cleanupStreamBuffers}) revisits any longer-lived rows — e.g.\n * an abandoned in-flight buffer on its 1h window — by waking again each interval\n * until they age out, then stops. Driving cleanup from an alarm (rather than\n * only piggybacking on the next stream completion) ensures idle/one-off chat\n * DOs still reclaim their buffers without waking forever (#1706). Shared by\n * `AIChatAgent` and `Think`.\n */\nexport const STREAM_CLEANUP_DELAY_SECONDS = 10 * 60;\n\n/**\n * A stored row body is either a single chunk body (a JSON object string —\n * legacy per-chunk rows and single-chunk segments) or a packed segment (a JSON\n * array of chunk body strings). Unpack to the individual chunk bodies in order.\n *\n * Stored chunk bodies are always serialized JSON *objects*, never arrays, so\n * `Array.isArray` reliably distinguishes a packed segment from a single body.\n */\nfunction unpackSegmentBody(rowBody: string): string[] {\n try {\n const parsed = JSON.parse(rowBody);\n if (Array.isArray(parsed)) {\n return parsed as string[];\n }\n } catch {\n // Not valid JSON — treat as a single opaque body.\n }\n return [rowBody];\n}\n\nfunction isMissingMetadataColumnError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return (\n (message.includes(\"message_id\") || message.includes(\"is_continuation\")) &&\n (message.toLowerCase().includes(\"no such column\") ||\n message.toLowerCase().includes(\"has no column named\"))\n );\n}\n\n/**\n * Stored stream chunk for resumable streaming\n */\ntype StreamChunk = {\n id: string;\n stream_id: string;\n body: string;\n chunk_index: number;\n created_at: number;\n};\n\n/**\n * Stream metadata for tracking active streams\n */\ntype StreamMetadata = {\n id: string;\n request_id: string;\n status: \"streaming\" | \"completed\" | \"error\";\n created_at: number;\n completed_at: number | null;\n /**\n * The assistant message id this stream is producing, captured when the\n * stream starts. This is the SAME id the live path persists under, so orphan\n * recovery (#1691) can re-associate reconstructed chunks with the correct\n * message even when the provider stream carries no `start.messageId`. Null on\n * legacy rows written before this column existed.\n */\n message_id: string | null;\n /**\n * Whether this stream is a continuation (appends to the last assistant\n * message rather than starting a new one). Live broadcast frames carry\n * `continuation: true`, and replay frames must too (#1733): without it a\n * reconnecting client treats a replayed continuation as a fresh message\n * and drops the parts streamed before the continuation. SQLite has no\n * boolean type — 1/0/null (legacy rows predating the column).\n */\n is_continuation: number | null;\n};\n\n/**\n * Minimal SQL interface matching Agent's this.sql tagged template.\n * Allows ResumableStream to work with the Agent's SQLite without\n * depending on the full Agent class.\n */\nexport type SqlTaggedTemplate = {\n <T = Record<string, unknown>>(\n strings: TemplateStringsArray,\n ...values: (string | number | boolean | null)[]\n ): T[];\n};\n\nexport class ResumableStream {\n private _activeStreamId: string | null = null;\n private _activeRequestId: string | null = null;\n /** Monotonic row-ordering index; one increment per flushed segment row. */\n private _segmentIndex = 0;\n\n /**\n * Whether the active stream was started in this instance (true) or\n * restored from SQLite after hibernation/restart (false). An orphaned\n * stream has no live LLM reader — the ReadableStream was lost when the\n * DO was evicted.\n */\n private _isLive = false;\n\n /**\n * Whether the active stream is a continuation. Mirrors the durable\n * `is_continuation` column so replay frames can carry the flag without a\n * per-replay query; restored from SQLite after hibernation in restore().\n */\n private _activeIsContinuation = false;\n\n private _chunkBuffer: Array<{ streamId: string; body: string }> = [];\n private _chunkBufferBytes = 0;\n private _isFlushingChunks = false;\n private _lastCleanupTime = 0;\n\n constructor(private sql: SqlTaggedTemplate) {\n // Create tables for stream chunks and metadata\n this.sql`create table if not exists cf_ai_chat_stream_chunks (\n id text primary key,\n stream_id text not null,\n body text not null,\n chunk_index integer not null,\n created_at integer not null\n )`;\n\n this.sql`create table if not exists cf_ai_chat_stream_metadata (\n id text primary key,\n request_id text not null,\n status text not null,\n created_at integer not null,\n completed_at integer,\n message_id text,\n is_continuation integer\n )`;\n\n this.sql`create index if not exists idx_stream_chunks_stream_id \n on cf_ai_chat_stream_chunks(stream_id, chunk_index)`;\n\n // Restore any active stream from a previous session\n this.restore();\n }\n\n /**\n * Add metadata columns for rows created before they existed. Constructors\n * intentionally do not run this: most wakes never start a stream, so paying a\n * schema-introspection read every time is wasteful. New tables include these\n * columns in CREATE TABLE; legacy tables migrate lazily only if a write/read\n * discovers the columns are missing.\n */\n private _migrateMetadataColumns() {\n const columns =\n this.sql<{ name: string }>`\n select name from pragma_table_info('cf_ai_chat_stream_metadata')\n ` ?? [];\n const hasMessageId = columns.some((column) => column.name === \"message_id\");\n if (!hasMessageId) {\n this\n .sql`alter table cf_ai_chat_stream_metadata add column message_id text`;\n }\n const hasIsContinuation = columns.some(\n (column) => column.name === \"is_continuation\"\n );\n if (!hasIsContinuation) {\n this\n .sql`alter table cf_ai_chat_stream_metadata add column is_continuation integer`;\n }\n }\n\n // ── State accessors ────────────────────────────────────────────────\n\n get activeStreamId(): string | null {\n return this._activeStreamId;\n }\n\n get activeRequestId(): string | null {\n return this._activeRequestId;\n }\n\n hasActiveStream(): boolean {\n return this._activeStreamId !== null;\n }\n\n /**\n * Whether the active stream has a live LLM reader (started in this\n * instance) vs being restored from SQLite after hibernation (orphaned).\n */\n get isLive(): boolean {\n return this._isLive;\n }\n\n // ── Stream lifecycle ───────────────────────────────────────────────\n\n /**\n * Start tracking a new stream for resumable streaming.\n * Creates metadata entry in SQLite and sets up tracking state.\n * @param requestId - The unique ID of the chat request\n * @returns The generated stream ID\n */\n start(\n requestId: string,\n options: { messageId?: string; continuation?: boolean } = {}\n ): string {\n // Flush any pending chunks from previous streams to prevent mixing\n this.flushBuffer();\n\n const streamId = nanoid();\n this._activeStreamId = streamId;\n this._activeRequestId = requestId;\n this._segmentIndex = 0;\n this._isLive = true;\n this._activeIsContinuation = options.continuation ?? false;\n\n const messageId = options.messageId ?? null;\n\n try {\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at, message_id, is_continuation)\n values (${streamId}, ${requestId}, 'streaming', ${Date.now()}, ${messageId}, ${this._activeIsContinuation ? 1 : 0})\n `;\n } catch (error) {\n if (!isMissingMetadataColumnError(error)) throw error;\n this._migrateMetadataColumns();\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at, message_id, is_continuation)\n values (${streamId}, ${requestId}, 'streaming', ${Date.now()}, ${messageId}, ${this._activeIsContinuation ? 1 : 0})\n `;\n }\n\n return streamId;\n }\n\n /**\n * The assistant message id an orphaned stream was producing — the same id the\n * live path persists under, so recovery re-associates reconstructed chunks\n * with the correct message (#1691). Returns null when the row is missing or\n * is a legacy row written before the `message_id` column existed.\n */\n getStreamMessageId(streamId: string): string | null {\n let rows: Array<{ message_id: string | null }>;\n try {\n rows = this.sql<{ message_id: string | null }>`\n select message_id from cf_ai_chat_stream_metadata\n where id = ${streamId}\n `;\n } catch (error) {\n if (!isMissingMetadataColumnError(error)) throw error;\n return null;\n }\n if (!rows || rows.length === 0) return null;\n return rows[0].message_id ?? null;\n }\n\n /**\n * Mark a stream as completed and flush any pending chunks.\n * @param streamId - The stream to mark as completed\n */\n complete(streamId: string) {\n this.flushBuffer();\n\n this.sql`\n update cf_ai_chat_stream_metadata \n set status = 'completed', completed_at = ${Date.now()} \n where id = ${streamId}\n `;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n this._isLive = false;\n this._activeIsContinuation = false;\n\n // Periodically clean up old streams\n this._maybeCleanupOldStreams();\n }\n\n /**\n * Mark a stream as errored and clean up state.\n * @param streamId - The stream to mark as errored\n */\n markError(streamId: string) {\n this.flushBuffer();\n\n this.sql`\n update cf_ai_chat_stream_metadata \n set status = 'error', completed_at = ${Date.now()} \n where id = ${streamId}\n `;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n this._isLive = false;\n this._activeIsContinuation = false;\n }\n\n // ── Chunk storage ──────────────────────────────────────────────────\n\n /** Maximum chunk body size before skipping storage (bytes). Prevents SQLite row limit crash. */\n private static CHUNK_MAX_BYTES = 1_800_000;\n\n /**\n * Buffer a stream chunk for batch write to SQLite.\n * Chunks exceeding the row size limit are skipped to prevent crashes.\n * The chunk is still broadcast to live clients (caller handles that),\n * but will be missing from replay on reconnection.\n * @param streamId - The stream this chunk belongs to\n * @param body - The serialized chunk body\n */\n storeChunk(streamId: string, body: string) {\n // Guard against chunks that would exceed SQLite row limit.\n // The chunk is still broadcast to live clients; only replay storage is skipped.\n const bodyBytes = textEncoder.encode(body).byteLength;\n if (bodyBytes > ResumableStream.CHUNK_MAX_BYTES) {\n console.warn(\n `[ResumableStream] Skipping oversized chunk (${bodyBytes} bytes) ` +\n `to prevent SQLite row limit crash. Live clients still receive it.`\n );\n return;\n }\n\n // Force flush if buffer is at max to prevent memory issues\n if (this._chunkBuffer.length >= CHUNK_BUFFER_MAX_SIZE) {\n this.flushBuffer();\n }\n\n // Byte guard: keep a packed segment safely under the SQLite row limit. If\n // the buffer already holds chunks and adding this body would push the\n // segment past the threshold, flush first so this chunk starts a fresh\n // segment. A single large chunk therefore ends up alone and is written\n // unwrapped by flushBuffer (no array-escaping inflation).\n if (\n this._chunkBuffer.length > 0 &&\n this._chunkBufferBytes + bodyBytes > SEGMENT_MAX_BYTES\n ) {\n this.flushBuffer();\n }\n\n this._chunkBuffer.push({ streamId, body });\n this._chunkBufferBytes += bodyBytes;\n\n // Flush when buffer reaches the per-segment chunk threshold\n if (this._chunkBuffer.length >= CHUNK_BUFFER_SIZE) {\n this.flushBuffer();\n }\n }\n\n /**\n * Flush the buffered chunks to SQLite as a single packed row.\n * Uses a lock to prevent concurrent flush operations.\n *\n * The whole buffer becomes one row: a single-chunk segment is stored\n * unwrapped (legacy object format) so a large chunk avoids array-escaping\n * inflation, while a multi-chunk segment stores a JSON array of bodies. This\n * collapses N chunk rows into one, cutting rows written / stored / scanned.\n */\n flushBuffer() {\n if (this._isFlushingChunks || this._chunkBuffer.length === 0) {\n return;\n }\n\n this._isFlushingChunks = true;\n try {\n const chunks = this._chunkBuffer;\n this._chunkBuffer = [];\n this._chunkBufferBytes = 0;\n\n // All chunks in a buffer belong to the same stream: start() flushes\n // before switching streams, so the buffer is never cross-stream.\n const streamId = chunks[0].streamId;\n const segmentBody =\n chunks.length === 1\n ? chunks[0].body\n : JSON.stringify(chunks.map((chunk) => chunk.body));\n\n this.sql`\n insert into cf_ai_chat_stream_chunks (id, stream_id, body, chunk_index, created_at)\n values (${nanoid()}, ${streamId}, ${segmentBody}, ${this._segmentIndex}, ${Date.now()})\n `;\n this._segmentIndex++;\n } finally {\n this._isFlushingChunks = false;\n }\n }\n\n // ── Chunk replay ───────────────────────────────────────────────────\n\n /**\n * Send stored stream chunks to a connection for replay.\n * Chunks are marked with replay: true so the client can batch-apply them.\n *\n * Three outcomes:\n * - **Live stream**: sends chunks + `replayComplete` — client flushes and\n * continues receiving live chunks from the LLM reader.\n * - **Orphaned stream** (restored from SQLite after hibernation, no reader):\n * sends chunks + `done` and completes the stream. The caller should\n * reconstruct and persist the partial message from the stored chunks.\n * - **Completed during replay** (defensive): sends chunks + `done`.\n *\n * All sends use {@link sendIfOpen}, so a WebSocket closing mid-replay\n * does not throw. If the connection drops while iterating chunks the\n * stream is left active so the next reconnect can retry.\n *\n * @param connection - The WebSocket connection\n * @param requestId - The original request ID\n * @returns The stream ID if the stream was orphaned and finalized, null otherwise.\n * When non-null the caller should reconstruct the message from chunks.\n */\n replayChunks(connection: Connection, requestId: string): string | null {\n const streamId = this._activeStreamId;\n if (!streamId) return null;\n\n this.flushBuffer();\n\n // Replay frames must mirror what a live client observed — including the\n // continuation flag (#1733): a replayed continuation `start` that lacks\n // it would be treated as a fresh message by the client and drop the\n // parts streamed before the continuation.\n const continuation = this._activeIsContinuation;\n\n const chunks = this.sql<StreamChunk>`\n select * from cf_ai_chat_stream_chunks \n where stream_id = ${streamId} \n order by chunk_index asc\n `;\n\n for (const chunk of chunks || []) {\n for (const body of unpackSegmentBody(chunk.body)) {\n if (\n !sendIfOpen(\n connection,\n JSON.stringify({\n body,\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n )\n ) {\n // Connection closed mid-replay — leave the stream active so the\n // next reconnect can retry from the start.\n return null;\n }\n }\n }\n\n if (this._activeStreamId !== streamId) {\n // Stream completed between our check above and now — send done.\n // In practice this cannot happen (DO is single-threaded and replay is\n // synchronous), but we guard defensively in case the flow changes.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n );\n return null;\n }\n\n if (!this._isLive) {\n // Orphaned stream — restored from SQLite after hibernation but the\n // LLM ReadableStream reader was lost. No more live chunks will ever\n // arrive, so finalize it: best-effort send done, then mark completed\n // in SQLite. The orphan-cleanup decision is committed regardless of\n // whether this particular connection received the done frame, so the\n // caller can persist the reconstructed message.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n );\n this.complete(streamId);\n return streamId;\n }\n\n // Stream is still active with a live reader — signal that replay is\n // complete so the client can flush accumulated parts to React state.\n // Without this, replayed chunks sit in activeStreamRef unflushed\n // until the next live chunk arrives.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n replayComplete: true,\n ...(continuation && { continuation: true })\n })\n );\n return null;\n }\n\n replayCompletedChunksByRequestId(\n connection: Connection,\n requestId: string\n ): boolean {\n const stream = this._latestStreamForRequest(requestId, \"completed\");\n if (!stream) return false;\n\n const continuation = stream.is_continuation === 1;\n if (\n !this._replayStoredChunks(connection, stream.id, requestId, continuation)\n ) {\n return false;\n }\n\n return sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n );\n }\n\n /**\n * Replay the stored chunks of an errored stream for a request, WITHOUT a\n * terminal frame — the caller follows up with the `done: true, error: true`\n * frame carrying the durable terminal record's error text, mirroring what a\n * live client observed (content chunks, then the error). Without this, a\n * client that missed broadcast frames while disconnected has no other\n * channel to the pre-error partial content: the server does not push\n * messages on connect, and {@link replayCompletedChunksByRequestId} only\n * serves `completed` streams (#1575).\n *\n * Returns true when the caller should proceed to send its terminal frame:\n * either no errored stream existed (nothing to replay) or its chunks were\n * replayed successfully. Returns false only when a send failed mid-replay,\n * signalling the caller to skip the terminal frame — the connection is gone\n * and the next reconnect retries the whole sequence.\n */\n replayErroredChunksByRequestId(\n connection: Connection,\n requestId: string\n ): boolean {\n const stream = this._latestStreamForRequest(requestId, \"error\");\n if (!stream) return true;\n\n return this._replayStoredChunks(\n connection,\n stream.id,\n requestId,\n stream.is_continuation === 1\n );\n }\n\n /** Latest stream row for a request with the given terminal status. */\n private _latestStreamForRequest(\n requestId: string,\n status: \"completed\" | \"error\"\n ): StreamMetadata | undefined {\n this.flushBuffer();\n\n const streams = this.sql<StreamMetadata>`\n select * from cf_ai_chat_stream_metadata\n where request_id = ${requestId}\n and status = ${status}\n order by created_at desc\n limit 1\n `;\n return streams[0];\n }\n\n /**\n * Send a finished stream's stored chunks to a connection as replay frames.\n * Returns false if the connection closed mid-replay.\n */\n private _replayStoredChunks(\n connection: Connection,\n streamId: string,\n requestId: string,\n continuation = false\n ): boolean {\n const chunks = this.sql<StreamChunk>`\n select * from cf_ai_chat_stream_chunks\n where stream_id = ${streamId}\n order by chunk_index asc\n `;\n\n for (const chunk of chunks || []) {\n for (const body of unpackSegmentBody(chunk.body)) {\n if (\n !sendIfOpen(\n connection,\n JSON.stringify({\n body,\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n ...(continuation && { continuation: true })\n })\n )\n ) {\n return false;\n }\n }\n }\n\n return true;\n }\n\n // ── Restore / cleanup ──────────────────────────────────────────────\n\n /**\n * Restore active stream state if the agent was restarted during streaming.\n * All streams are restored regardless of age — stale cleanup happens\n * lazily in _maybeCleanupOldStreams after recovery has had its chance.\n */\n restore() {\n const activeStreams = this.sql<StreamMetadata>`\n select * from cf_ai_chat_stream_metadata \n where status = 'streaming' \n order by created_at desc \n limit 1\n `;\n\n if (activeStreams && activeStreams.length > 0) {\n const stream = activeStreams[0];\n this._activeStreamId = stream.id;\n this._activeRequestId = stream.request_id;\n // Rehydrate the continuation flag so an orphaned continuation stream\n // replayed after hibernation still carries `continuation: true` on\n // its frames (#1733). Legacy rows predate the column → null → false.\n this._activeIsContinuation = stream.is_continuation === 1;\n\n // Resume the segment row-ordering index past the highest stored value.\n const lastChunk = this.sql<{ max_index: number }>`\n select max(chunk_index) as max_index \n from cf_ai_chat_stream_chunks \n where stream_id = ${this._activeStreamId}\n `;\n this._segmentIndex =\n lastChunk && lastChunk[0]?.max_index != null\n ? lastChunk[0].max_index + 1\n : 0;\n }\n }\n\n /**\n * Clear all stream data (called on chat history clear).\n */\n clearAll() {\n this._chunkBuffer = [];\n this._chunkBufferBytes = 0;\n this.sql`delete from cf_ai_chat_stream_chunks`;\n this.sql`delete from cf_ai_chat_stream_metadata`;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n this._activeIsContinuation = false;\n }\n\n /**\n * Drop all stream tables (called on destroy).\n */\n destroy() {\n this.flushBuffer();\n this.sql`drop table if exists cf_ai_chat_stream_chunks`;\n this.sql`drop table if exists cf_ai_chat_stream_metadata`;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._activeIsContinuation = false;\n }\n\n /**\n * Force a sweep of aged stream buffers now, bypassing the lazy interval\n * gate used by {@link _maybeCleanupOldStreams}. Intended to be driven by an\n * alarm so idle/hibernated chat DOs still reclaim buffers even when no\n * further stream ever completes to trigger the lazy path.\n */\n cleanup(now: number = Date.now()): void {\n this._lastCleanupTime = now;\n this._sweepOldStreams(now);\n }\n\n /**\n * True if any stream rows remain at all. Used by alarm-driven cleanup to\n * decide whether to re-arm: once no rows remain there is nothing left to\n * sweep, so the DO can stop waking itself.\n */\n hasReclaimableStreams(): boolean {\n const rows = this.sql<{ n: number }>`\n select count(*) as n from cf_ai_chat_stream_metadata\n `;\n return (rows?.[0]?.n ?? 0) > 0;\n }\n\n // ── Internal ───────────────────────────────────────────────────────\n\n private _maybeCleanupOldStreams() {\n const now = Date.now();\n if (now - this._lastCleanupTime < CLEANUP_INTERVAL_MS) {\n return;\n }\n this._lastCleanupTime = now;\n this._sweepOldStreams(now);\n }\n\n /** Delete completed/errored buffers past the completion grace window, plus\n * abandoned \"streaming\" rows past the stale-in-flight window. The two use\n * different retentions: a completed buffer is redundant with the persisted\n * message and needs only a brief replay grace, whereas an in-flight buffer\n * must outlive resume/recovery before it is presumed dead. */\n private _sweepOldStreams(now: number) {\n const completedCutoff = now - COMPLETED_RETENTION_MS;\n this.sql`\n delete from cf_ai_chat_stream_chunks \n where stream_id in (\n select id from cf_ai_chat_stream_metadata \n where status in ('completed', 'error') and completed_at < ${completedCutoff}\n )\n `;\n this.sql`\n delete from cf_ai_chat_stream_metadata \n where status in ('completed', 'error') and completed_at < ${completedCutoff}\n `;\n\n // Clean up abandoned \"streaming\" rows. These are orphaned streams that\n // were never completed or recovered (e.g. non-durable agents that never\n // reconnected). By this point, fiber recovery has already had its chance\n // to claim them — safe to delete.\n //\n // \"Abandoned\" is keyed off LAST ACTIVITY (the most recent chunk write),\n // not the stream's start time: a long-running stream that is still\n // actively emitting chunks must never be swept mid-flight just because it\n // started long ago. A row with no chunks falls back to its start time.\n // Note `created_at <= max(chunk.created_at)` always (the row is inserted\n // before any chunk), so this set is stable across the two deletes even\n // though the first removes the chunks the second's subquery reads.\n const abandonedCutoff = now - ABANDONED_STREAM_RETENTION_MS;\n this.sql`\n delete from cf_ai_chat_stream_chunks\n where stream_id in (\n select m.id from cf_ai_chat_stream_metadata m\n where m.status = 'streaming'\n and coalesce(\n (select max(c.created_at) from cf_ai_chat_stream_chunks c\n where c.stream_id = m.id),\n m.created_at\n ) < ${abandonedCutoff}\n )\n `;\n this.sql`\n delete from cf_ai_chat_stream_metadata\n where id in (\n select m.id from cf_ai_chat_stream_metadata m\n where m.status = 'streaming'\n and coalesce(\n (select max(c.created_at) from cf_ai_chat_stream_chunks c\n where c.stream_id = m.id),\n m.created_at\n ) < ${abandonedCutoff}\n )\n `;\n }\n\n // ── Test helpers (matching old AIChatAgent test API) ────────────────\n\n /**\n * Return the stored chunks for a stream as individual chunk bodies in order,\n * unpacking packed segment rows. The returned `chunk_index` is a running\n * per-chunk sequence (0, 1, 2, …) — stable across calls because rows are\n * append-only — so callers can use it as a monotonic chunk sequence.\n */\n getStreamChunks(\n streamId: string\n ): Array<{ body: string; chunk_index: number }> {\n const rows =\n this.sql<{ body: string }>`\n select body from cf_ai_chat_stream_chunks \n where stream_id = ${streamId} \n order by chunk_index asc\n ` || [];\n const out: Array<{ body: string; chunk_index: number }> = [];\n let index = 0;\n for (const row of rows) {\n for (const body of unpackSegmentBody(row.body)) {\n out.push({ body, chunk_index: index });\n index++;\n }\n }\n return out;\n }\n\n /** @internal For testing only */\n getStreamMetadata(\n streamId: string\n ): { status: string; request_id: string } | null {\n const result = this.sql<{ status: string; request_id: string }>`\n select status, request_id from cf_ai_chat_stream_metadata \n where id = ${streamId}\n `;\n return result && result.length > 0 ? result[0] : null;\n }\n\n /** @internal For testing only */\n getAllStreamMetadata(): Array<{\n id: string;\n status: string;\n request_id: string;\n created_at: number;\n }> {\n return (\n this.sql<{\n id: string;\n status: string;\n request_id: string;\n created_at: number;\n }>`select id, status, request_id, created_at from cf_ai_chat_stream_metadata` ||\n []\n );\n }\n\n /** @internal For testing only */\n insertStaleStream(streamId: string, requestId: string, ageMs: number): void {\n const createdAt = Date.now() - ageMs;\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at)\n values (${streamId}, ${requestId}, 'streaming', ${createdAt})\n `;\n }\n\n /**\n * Append a chunk to a stream dated `ageMs` in the past. Used to exercise the\n * last-activity sweep threshold: a long-running streaming row with a *recent*\n * chunk must survive even when its start time is older than the cutoff.\n * @internal For testing only\n */\n insertChunkAt(streamId: string, body: string, ageMs: number): void {\n const createdAt = Date.now() - ageMs;\n this.sql`\n insert into cf_ai_chat_stream_chunks (id, stream_id, body, chunk_index, created_at)\n values (${nanoid()}, ${streamId}, ${body}, 0, ${createdAt})\n `;\n }\n}\n\n/**\n * The buffer-cleanup alarm body: sweep aged stream buffers, then re-arm only\n * while rows remain so a fully-swept DO stops waking itself. `rearm` schedules\n * the next sweep — it MUST schedule a non-idempotent alarm, because this runs\n * INSIDE the currently-executing one-shot schedule row, which `alarm()` deletes\n * only after it returns; an idempotent reschedule would dedup onto that row and\n * be deleted with it, so the re-arm would silently never fire and buffers that\n * survived this sweep (e.g. a younger turn) would go uncollected. A fresh\n * delayed row survives the deletion. Shared by `AIChatAgent` and `Think`.\n *\n * `@internal`\n */\nexport async function cleanupStreamBuffers(\n stream: Pick<ResumableStream, \"cleanup\" | \"hasReclaimableStreams\">,\n rearm: () => Promise<void>\n): Promise<void> {\n stream.cleanup();\n if (stream.hasReclaimableStreams()) {\n await rearm();\n }\n}\n","/**\n * Helpers for building batched SQLite statements that run through the Agent's\n * `sql` tagged template (which interleaves a `?` placeholder between every\n * string fragment). Used to collapse per-row INSERT/DELETE loops into a small\n * number of multi-row statements.\n *\n * SQLite (Durable Object / D1) caps bound parameters at 100 per query, so\n * callers must chunk their inputs to stay within {@link MAX_BOUND_PARAMS}.\n * See https://developers.cloudflare.com/d1/platform/limits/\n */\n\n/** Maximum bound parameters allowed in a single SQLite (DO / D1) query. */\nexport const MAX_BOUND_PARAMS = 100;\n\n/**\n * Attach a self-referential `raw` property so a plain string[] satisfies the\n * TemplateStringsArray shape. `sql` only reads indexed string fragments, so\n * `raw` is never consumed — this just keeps the type system happy.\n */\nfunction asTemplateStringsArray(parts: string[]): TemplateStringsArray {\n (parts as unknown as { raw: readonly string[] }).raw = parts;\n return parts as unknown as TemplateStringsArray;\n}\n\n/**\n * Build a TemplateStringsArray for a single-column `IN (...)` clause. Produces\n * fragments for:\n * `${prefix}(?, ?, ...)`\n *\n * @throws if `count` is less than 1.\n */\nexport function buildInClauseStrings(\n prefix: string,\n count: number\n): TemplateStringsArray {\n if (count < 1) {\n throw new Error(`buildInClauseStrings requires count >= 1 (got ${count})`);\n }\n const parts = new Array<string>(count + 1);\n parts[0] = `${prefix}(`;\n for (let i = 1; i < count; i++) {\n parts[i] = \", \";\n }\n parts[count] = \")\";\n return asTemplateStringsArray(parts);\n}\n","/**\n * Client tool schema handling for the cf_agent_chat protocol.\n *\n * Converts client-provided tool schemas (JSON wire format) into AI SDK\n * tool definitions. By default these tools have no `execute` function —\n * when the model calls them, the tool call is sent back to the client.\n *\n * When an `execute` delegate is supplied (e.g. a parent agent that drives a\n * Think sub-agent over RPC and can run the client tools itself), the tools are\n * built WITH an `execute` so the model's call is resolved inline within the\n * same turn instead of being surfaced as a dangling tool call.\n *\n * Used by both @cloudflare/ai-chat and @cloudflare/think.\n */\n\nimport type { JSONSchema7, Tool, ToolSet } from \"ai\";\nimport { tool, jsonSchema } from \"ai\";\n\n/**\n * Wire-format tool schema sent from the client.\n * Uses `parameters` (JSONSchema7) rather than AI SDK's `inputSchema`\n * because Zod schemas cannot be serialized over the wire.\n */\nexport type ClientToolSchema = {\n /** Unique name for the tool */\n name: string;\n /** Human-readable description of what the tool does */\n description?: Tool[\"description\"];\n /** JSON Schema defining the tool's input parameters */\n parameters?: JSONSchema7;\n};\n\n/**\n * Executes a client-defined tool and returns its output.\n *\n * Used for the RPC path (e.g. a parent agent delegating to a Think sub-agent)\n * where the caller can run the client tools itself, rather than the\n * browser/WebSocket path where results are sent back asynchronously.\n */\nexport type ClientToolExecutor = (call: {\n /** The name of the client tool the model called. */\n toolName: string;\n /** The model-generated input for the tool call. */\n input: unknown;\n /** The AI SDK tool-call id for the invocation. */\n toolCallId: string;\n}) => unknown | Promise<unknown>;\n\n/**\n * Converts client tool schemas to AI SDK tool format.\n *\n * By default these tools have no `execute` function — when the AI model calls\n * them, the tool call is sent back to the client for execution.\n *\n * When `options.execute` is provided, each tool is built WITH an `execute` that\n * delegates to it. This is used by the RPC path (e.g. a parent agent driving a\n * Think sub-agent) so the model's client-tool call is resolved inline within\n * the same turn.\n *\n * @param clientTools - Array of tool schemas from the client\n * @param options - Optional `execute` delegate to run the tools inline\n * @returns Record of AI SDK tools that can be spread into your tools object\n */\nexport function createToolsFromClientSchemas(\n clientTools?: ClientToolSchema[],\n options?: { execute?: ClientToolExecutor }\n): ToolSet {\n if (!clientTools || clientTools.length === 0) {\n return {};\n }\n\n const seenNames = new Set<string>();\n for (const t of clientTools) {\n if (seenNames.has(t.name)) {\n console.warn(\n `[createToolsFromClientSchemas] Duplicate tool name \"${t.name}\" found. Later definitions will override earlier ones.`\n );\n }\n seenNames.add(t.name);\n }\n\n const execute = options?.execute;\n // The AI SDK's `tool()` overloads infer the input type as `never` when an\n // `execute` is combined with a runtime `jsonSchema(...)`. Cast to a\n // permissive signature (same approach as `agentTool()`), since the wire\n // schema is untyped JSON.\n const createTool = tool as unknown as (config: {\n description: string;\n inputSchema: ReturnType<typeof jsonSchema>;\n execute?: (\n input: unknown,\n executeOptions?: { toolCallId?: string }\n ) => unknown | Promise<unknown>;\n }) => Tool;\n\n return Object.fromEntries(\n clientTools.map((t) => [\n t.name,\n createTool({\n description: t.description ?? \"\",\n inputSchema: jsonSchema(t.parameters ?? { type: \"object\" }),\n ...(execute\n ? {\n execute: (\n input: unknown,\n executeOptions?: { toolCallId?: string }\n ) =>\n execute({\n toolName: t.name,\n input,\n toolCallId: executeOptions?.toolCallId ?? \"\"\n })\n }\n : {})\n })\n ])\n );\n}\n","/**\n * ContinuationState — shared state container for auto-continuation lifecycle.\n *\n * Tracks pending, deferred, and active continuation state for the\n * tool-result → auto-continue flow. Both AIChatAgent and Think use this\n * to manage which connection/tools/body a continuation turn should use\n * and to coordinate with clients requesting stream resume.\n *\n * The scheduling algorithm (prerequisite chaining, debounce, TurnQueue\n * enrollment) stays in the host — this class only manages the data.\n */\n\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\nimport type { ClientToolSchema } from \"./client-tools\";\nimport { sendIfOpen, type ChatConnection } from \"./connection\";\n\nconst MSG_STREAM_RESUME_NONE = CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE;\n\n/**\n * Minimal connection interface for sending WebSocket messages. Alias of the\n * shared {@link ChatConnection} — kept as a named export for back-compat with\n * existing `ContinuationConnection` consumers.\n */\nexport type ContinuationConnection = ChatConnection;\n\nexport interface ContinuationPending<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n connectionId: string | null;\n requestId: string;\n clientTools?: ClientToolSchema[];\n body?: Record<string, unknown>;\n errorPrefix: string | null;\n prerequisite: Promise<boolean> | null;\n pastCoalesce: boolean;\n}\n\nexport interface ContinuationDeferred<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n connectionId: string | null;\n clientTools?: ClientToolSchema[];\n body?: Record<string, unknown>;\n errorPrefix: string;\n prerequisite: Promise<boolean> | null;\n}\n\nexport class ContinuationState<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n pending: ContinuationPending<TConnection> | null = null;\n deferred: ContinuationDeferred<TConnection> | null = null;\n activeRequestId: string | null = null;\n activeConnectionId: string | null = null;\n awaitingConnections: Map<string, TConnection> = new Map();\n\n /** Clear pending state and awaiting connections (without sending RESUME_NONE). */\n clearPending(): void {\n this.pending = null;\n this.awaitingConnections.clear();\n }\n\n clearDeferred(): void {\n this.deferred = null;\n }\n\n clearAll(): void {\n this.clearPending();\n this.clearDeferred();\n this.activeRequestId = null;\n this.activeConnectionId = null;\n }\n\n /**\n * Mark a connection as no longer available without canceling the\n * continuation it initiated.\n */\n releaseConnection(connectionId: string): void {\n this.awaitingConnections.delete(connectionId);\n if (this.pending?.connectionId === connectionId) {\n this.pending = { ...this.pending, connectionId: null };\n }\n if (this.deferred?.connectionId === connectionId) {\n this.deferred = { ...this.deferred, connectionId: null };\n }\n if (this.activeConnectionId === connectionId) {\n this.activeConnectionId = null;\n }\n }\n\n /**\n * Send STREAM_RESUME_NONE to all connections waiting for a\n * continuation stream to start, then clear the map.\n */\n sendResumeNone(): void {\n const msg = JSON.stringify({ type: MSG_STREAM_RESUME_NONE });\n for (const connection of this.awaitingConnections.values()) {\n sendIfOpen(connection, msg);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Flush awaiting connections by notifying each one via the provided\n * callback (typically sends STREAM_RESUMING), then clear.\n */\n flushAwaitingConnections(notify: (conn: TConnection) => void): void {\n for (const connection of this.awaitingConnections.values()) {\n notify(connection);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Transition pending → active. Called when the continuation stream\n * actually starts. Moves request/connection IDs to active slots,\n * clears pending fields.\n */\n activatePending(): void {\n if (!this.pending) return;\n this.activeRequestId = this.pending.requestId;\n this.activeConnectionId = this.pending.connectionId;\n this.pending = null;\n }\n\n /**\n * Transition deferred → pending. Called when a continuation turn\n * completes and there's a deferred follow-up waiting.\n *\n * Returns the new pending state (so the host can enqueue the turn),\n * or null if there was nothing deferred.\n */\n activateDeferred(\n generateRequestId: () => string\n ): ContinuationPending<TConnection> | null {\n if (this.pending || !this.deferred) return null;\n\n const d = this.deferred;\n this.deferred = null;\n this.activeRequestId = null;\n this.activeConnectionId = null;\n\n this.pending = {\n connection: d.connection,\n connectionId: d.connectionId,\n requestId: generateRequestId(),\n clientTools: d.clientTools,\n body: d.body,\n errorPrefix: d.errorPrefix,\n prerequisite: d.prerequisite,\n pastCoalesce: false\n };\n\n if (d.connectionId !== null) {\n this.awaitingConnections.set(d.connectionId, d.connection);\n }\n return this.pending;\n }\n}\n","/**\n * PreStreamTurns — tracks accepted chat turns that have not yet started a\n * resumable stream, and the connections parked waiting for one.\n *\n * The resume handshake can resume an ACTIVE stream (chunks buffering) and can\n * replay a TERMINAL outcome, but a normal turn spends a window between \"request\n * accepted\" and \"first chunk produced\" in neither state: it is queued, waiting\n * on `waitForMcpConnections`, debouncing, or simply running async setup inside\n * `onChatMessage` before a stream object exists. A client that reconnects or\n * re-mounts in that window used to get `cf_agent_stream_resume_none` and give\n * up, so the turn the server went on to complete normally never drove the\n * client's AI-SDK `status` (issue #1784).\n *\n * This container lets a host represent that window as resumable: the handshake\n * parks the reconnecting connection here (and tells it to keep waiting), and the\n * host flushes the parked connections into the normal `STREAM_RESUMING` path the\n * moment a stream actually starts — or releases them with `resume_none` if the\n * turn settles without ever streaming.\n *\n * Concurrency-safe under queued turns via an accepted-request set: parked\n * connections are only released with `resume_none` once EVERY accepted turn has\n * settled with no active stream, so a client parked during the gap between one\n * turn finishing and the next starting still resumes onto the next stream.\n *\n * Pure data + send-through-callback, mirroring {@link ContinuationState}: the\n * host owns the actual frame sends and the stream-start / turn-settle wiring.\n *\n * @internal Sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API.\n */\n\nimport { sendIfOpen, type ChatConnection } from \"./connection\";\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\n\nconst MSG_STREAM_PENDING = CHAT_MESSAGE_TYPES.STREAM_PENDING;\nconst MSG_STREAM_RESUME_NONE = CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE;\n\nexport class PreStreamTurns<\n TConnection extends ChatConnection = ChatConnection\n> {\n /**\n * Accepted-but-not-yet-streamed request ids. A turn enters on `begin()` and\n * leaves on `settle()`; the set being non-empty means \"pre-stream work is in\n * flight\", which gates parking and the eventual `resume_none` release.\n */\n private readonly _accepted = new Set<string>();\n\n /** Connections parked waiting for a stream to start. */\n readonly awaitingConnections = new Map<string, TConnection>();\n\n /** The most recently accepted pre-stream request id (for the keep-waiting frame). */\n private _latestRequestId: string | null = null;\n\n /** Mark a freshly-accepted turn as in flight (pre-stream). */\n begin(requestId: string): void {\n this._accepted.add(requestId);\n this._latestRequestId = requestId;\n }\n\n /**\n * Mark an accepted turn as settled. Returns `true` when no accepted turn\n * remains in flight (the caller should release parked connections if no\n * stream is active).\n */\n settle(requestId: string): boolean {\n this._accepted.delete(requestId);\n if (this._accepted.size === 0) {\n this._latestRequestId = null;\n return true;\n }\n return false;\n }\n\n /** Whether any accepted turn is still pre-stream. */\n hasInFlight(): boolean {\n return this._accepted.size > 0;\n }\n\n /** The request id to advertise in the keep-waiting frame, if known. */\n get latestRequestId(): string | null {\n return this._latestRequestId;\n }\n\n /**\n * Park a reconnecting connection and tell it to keep waiting (so its\n * transport does not resolve `reconnectToStream` early). No-op when nothing\n * is in flight. Parked connections are deliberately NOT added to the host's\n * `pendingResumeConnections` — they must keep receiving any live broadcast —\n * until the host flushes them through `notifyStreamResuming` on stream start.\n */\n park(connection: TConnection): boolean {\n if (!this.hasInFlight()) return false;\n this.awaitingConnections.set(connection.id, connection);\n sendIfOpen(\n connection,\n JSON.stringify({\n type: MSG_STREAM_PENDING,\n ...(this._latestRequestId ? { id: this._latestRequestId } : {})\n })\n );\n return true;\n }\n\n /** Drop a single connection (e.g. on socket close) without releasing others. */\n release(connectionId: string): void {\n this.awaitingConnections.delete(connectionId);\n }\n\n /**\n * A stream has started: hand every parked connection to `notify` (the host's\n * `notifyStreamResuming`, which sends `STREAM_RESUMING` and excludes the\n * connection from live broadcast until it ACKs), then clear the awaiting map.\n * The accepted set is untouched — the turn is still running.\n */\n flushOnStreamStart(notify: (connection: TConnection) => void): void {\n for (const connection of this.awaitingConnections.values()) {\n notify(connection);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Release every parked connection with `STREAM_RESUME_NONE` (the turn settled\n * without ever starting a stream) and clear the awaiting map. Safe to call\n * when the map is empty (no-op), so the host can call it liberally from a\n * turn-settle path.\n */\n releaseAwaiting(): void {\n const msg = JSON.stringify({ type: MSG_STREAM_RESUME_NONE });\n for (const connection of this.awaitingConnections.values()) {\n sendIfOpen(connection, msg);\n }\n this.awaitingConnections.clear();\n }\n\n /** Drop all state (chat clear / destroy). Does not send any frames. */\n reset(): void {\n this._accepted.clear();\n this.awaitingConnections.clear();\n this._latestRequestId = null;\n }\n}\n","/**\n * AutoContinuationController — shared auto-continuation barrier for the\n * tool-result → auto-continue flow (#1649 / #1650).\n *\n * Both `@cloudflare/ai-chat` (`AIChatAgent`) and `@cloudflare/think` (`Think`)\n * drive an identical event-driven barrier: a tool result/approval that opts in\n * with `autoContinue` schedules a continuation, rapid sibling results coalesce\n * into a single fire via a debounce timer, and the actual fire is gated on a\n * complete parallel tool batch and no active stream. This controller owns that\n * machinery exactly once:\n *\n * - the coalesce/debounce timer ({@link AutoContinuationController.COALESCE_MS}),\n * - the `barrierActive` double-fire guard,\n * - the create/update/defer scheduling branch ({@link schedule}),\n * - the completeness-gated drain orchestration ({@link fireWhenStable}).\n *\n * Everything host-specific is expressed through {@link AutoContinuationHost}: the\n * stream-active signal, the incomplete-batch / pending-interaction predicates,\n * the apply-drain primitive, and the actual continuation turn ({@link\n * AutoContinuationHost.fire}, each host's inference/reply pipeline). The shared\n * {@link ContinuationState} data layer stays on the host and is read/mutated\n * here through {@link AutoContinuationHost.continuation}.\n *\n * @internal Sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API.\n */\n\nimport type { ClientToolSchema } from \"./client-tools\";\nimport type {\n ContinuationConnection,\n ContinuationState\n} from \"./continuation-state\";\n\n/**\n * The data a host supplies to schedule (or re-target) a pending/deferred\n * auto-continuation. Mirrors the fields a host writes onto\n * {@link ContinuationState.pending} — the host owns where the values come from\n * (e.g. Think hardcodes a fixed `errorPrefix` and `body: undefined`; ai-chat\n * threads them per tool-result event).\n */\nexport interface ContinuationSpec<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n clientTools: ClientToolSchema[] | undefined;\n body: Record<string, unknown> | undefined;\n errorPrefix: string;\n}\n\n/**\n * Host substrate the controller parameterizes over. Implemented by the agent\n * (typically via a small adapter object capturing `this`).\n */\nexport interface AutoContinuationHost<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n /** Shared continuation state (pending/deferred/awaiting connections). */\n readonly continuation: ContinuationState<TConnection>;\n /** Generate a request id for a freshly-created continuation turn. */\n generateRequestId(): string;\n /**\n * `true` while an assistant turn is streaming — the parallel tool batch can\n * still grow with tool calls the model hasn't emitted yet, so no completeness\n * check is meaningful. (`_streamingAssistant !== null` in Think;\n * `_streamingTurnActive` in ai-chat.)\n */\n isStreamActive(): boolean;\n /** `true` while a tool-result/approval apply is in flight. */\n hasPendingInteraction(): boolean;\n /**\n * `true` when the latest assistant message is mid-batch (a settled tool\n * result beside an unanswered tool call/approval — the #1649 signature).\n */\n hasIncompleteToolBatch(): boolean;\n /**\n * Drain every in-flight tool-result/approval apply (including any enqueued\n * while draining) so the subsequent completeness re-check sees every result\n * that has already arrived. Bounded by real apply activity, never a timer.\n */\n drainInteractionApplies(): Promise<void>;\n /** Hold the isolate alive for the duration of `fn` (alarm heartbeats). */\n keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>;\n /**\n * Run the continuation turn for the current {@link ContinuationState.pending}.\n * Each host's inference/reply pipeline (Think: `_turnQueue.enqueue` +\n * `_runInferenceLoop`; ai-chat: `_runExclusiveChatTurn` + `onChatMessage`).\n * Reads everything it needs from `continuation.pending`, so it takes no args.\n */\n fire(): void;\n}\n\nexport class AutoContinuationController<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n /**\n * Small debounce window to batch adjacent client-side tool results/approvals\n * into a single server continuation barrier check (#1650).\n */\n static readonly COALESCE_MS = 50;\n\n /**\n * Coalesce/debounce timer for the event-driven barrier (#1650). Each tool\n * result/approval re-arms it; on fire it runs {@link fireWhenStable}.\n */\n private _timer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * Double-fire guard (#1650). Ensures only one in-flight apply-drain runs;\n * that drain re-checks completeness on completion before firing. A sibling\n * that re-arms the coalesce timer during a drain is absorbed by the\n * in-progress drain rather than starting its own.\n */\n private _barrierActive = false;\n\n constructor(private readonly host: AutoContinuationHost<TConnection>) {}\n\n /**\n * Schedule an auto-continuation for a tool result/approval that opted in with\n * `autoContinue` (#1650). Coalesces rapid sibling results into a single\n * continuation via the debounce timer; the actual fire is gated by\n * {@link fireWhenStable}. If a continuation is already running\n * (`pastCoalesce`), the new result is stored as the deferred follow-up\n * instead of re-arming.\n */\n schedule(spec: ContinuationSpec<TConnection>): void {\n const c = this.host.continuation;\n\n if (c.pending?.pastCoalesce) {\n // A continuation is already running; the new result coalesces/defers into\n // the next one rather than re-arming this one.\n c.deferred = {\n connection: spec.connection,\n connectionId: spec.connection.id,\n clientTools: spec.clientTools,\n body: spec.body,\n errorPrefix: spec.errorPrefix,\n prerequisite: null\n };\n return;\n }\n\n if (c.pending) {\n c.pending.connection = spec.connection;\n c.pending.connectionId = spec.connection.id;\n c.pending.clientTools = spec.clientTools;\n c.pending.body = spec.body;\n c.pending.errorPrefix = spec.errorPrefix;\n c.awaitingConnections.set(spec.connection.id, spec.connection);\n this.armTimer();\n return;\n }\n\n c.pending = {\n connection: spec.connection,\n connectionId: spec.connection.id,\n requestId: this.host.generateRequestId(),\n clientTools: spec.clientTools,\n body: spec.body,\n errorPrefix: spec.errorPrefix,\n prerequisite: null,\n pastCoalesce: false\n };\n c.awaitingConnections.set(spec.connection.id, spec.connection);\n this.armTimer();\n }\n\n /**\n * Re-arm the barrier for a result/approval that arrived WITHOUT `autoContinue`\n * (#1650). A standalone errored result declines to continue on its own, but in\n * a parallel batch a SIBLING may already have opted in — and this result can\n * be the one that completes the batch, so we must re-run the barrier check.\n * Unlike {@link schedule} this NEVER creates a pending continuation, and\n * no-ops once the continuation is running (`pastCoalesce`).\n */\n rearmForBatch(): void {\n const pending = this.host.continuation.pending;\n if (!pending || pending.pastCoalesce) return;\n this.armTimer();\n }\n\n /** (Re)arm the coalesce timer; on fire, run {@link fireWhenStable}. */\n armTimer(): void {\n if (this._timer) {\n clearTimeout(this._timer);\n }\n this._timer = setTimeout(() => {\n this._timer = null;\n if (!this.host.continuation.pending) return;\n this.fireWhenStable();\n }, AutoContinuationController.COALESCE_MS);\n }\n\n /**\n * Fire an auto-continuation, but only once the model's parallel tool-call\n * batch is fully answered (#1649) and no assistant turn is mid-stream (#1650).\n * The barrier is event-driven with NO orphan timeout: when the batch is still\n * incomplete we drain the in-flight applies, re-check, and — if still\n * incomplete — return WITHOUT firing and WITHOUT holding the isolate, leaving\n * `continuation.pending` in place. The next sibling's result re-arms the\n * coalesce timer and re-runs this check; the continuation fires once the final\n * sibling lands. A true orphan (a sibling that never arrives) simply never\n * auto-continues — a later user turn / chat recovery repairs the transcript.\n */\n fireWhenStable(): void {\n const c = this.host.continuation;\n if (!c.pending) return;\n // The continuation is already running (a sibling re-armed after it started).\n // New results coalesce/defer into it — don't double-fire.\n if (c.pending.pastCoalesce) return;\n // A drain is already in progress; the sibling that re-armed the timer is\n // absorbed by it. Only one drain runs, and it re-checks on completion.\n if (this._barrierActive) return;\n // Stream-active gate (#1650, #1649): while the model is still streaming the\n // assistant turn we cannot know the parallel batch is complete — a fast\n // client tool can resolve before its slower siblings have even been\n // streamed, so they exist nowhere yet and firing now would repair them to\n // errored. The stream-finalize hook re-runs this check once the stream ends.\n if (this.host.isStreamActive()) return;\n // Fast path: no apply in flight and the leaf step is not mid-batch.\n if (\n !this.host.hasPendingInteraction() &&\n !this.host.hasIncompleteToolBatch()\n ) {\n this.cancelTimer();\n this.host.fire();\n return;\n }\n this._barrierActive = true;\n // keepAlive only for the bounded drain — the duration of the applies that\n // have ALREADY arrived, not an open-ended wait for siblings that haven't.\n this.host\n .keepAliveWhile(() => this.host.drainInteractionApplies())\n .catch(() => {})\n .finally(() => {\n // Clear the flag and re-check synchronously — no `await` between here\n // and the fire/return decision, so a sibling-armed coalesce timer (a\n // macrotask) cannot interleave and double-fire. `cancelTimer()` below\n // kills that timer on the fire path; the incomplete-return path leaves\n // it armed so the sibling that armed it re-runs this check.\n this._barrierActive = false;\n const pending = c.pending;\n if (!pending || pending.pastCoalesce) return;\n // A stream (re)started during the drain — hold; the finalize re-trigger\n // re-checks once the batch is fully materialized.\n if (this.host.isStreamActive()) return;\n // Still waiting on an unanswered sibling — return without firing. The\n // result that completes the batch re-triggers this via its own\n // schedule(); we do not pin the isolate in the interim.\n if (this.host.hasIncompleteToolBatch()) return;\n this.cancelTimer();\n this.host.fire();\n });\n }\n\n /**\n * Transition the deferred follow-up (stored while a continuation was running)\n * to pending and re-run the barrier — its batch may still be incomplete (or a\n * stream active), in which case it parks and re-arms instead of firing blind.\n */\n activateDeferredAndReschedule(): void {\n const pending = this.host.continuation.activateDeferred(() =>\n this.host.generateRequestId()\n );\n if (!pending) return;\n this.fireWhenStable();\n }\n\n /**\n * Cancel any still-armed coalesce timer. Called on the fire path so a sibling\n * result that re-armed it during a barrier wait can't fire a duplicate\n * continuation after this one starts (#1649 / #1650).\n */\n cancelTimer(): void {\n if (this._timer) {\n clearTimeout(this._timer);\n this._timer = null;\n }\n }\n\n /**\n * `true` when the barrier is going to fire on its own — its coalesce timer is\n * still pending or its completeness drain is in progress. The host combines\n * this with its own pending/`pastCoalesce` checks to decide idle/stable.\n */\n isArmed(): boolean {\n return this._timer !== null || this._barrierActive;\n }\n\n /**\n * Tear down the controller-owned barrier state (timer + double-fire guard).\n * Scoped to ONLY this controller's fields — the host clears the rest of its\n * turn state (stream gate, interaction tail, continuation data) separately.\n */\n reset(): void {\n this.cancelTimer();\n this._barrierActive = false;\n }\n}\n","/**\n * AbortRegistry — manages per-request AbortControllers.\n *\n * Shared between AIChatAgent and Think for chat turn cancellation.\n * Each request gets its own AbortController keyed by request ID.\n * Controllers are created lazily on first signal access.\n */\n\nconst NOOP = () => {};\n\nexport class AbortRegistry {\n private controllers = new Map<string, AbortController>();\n\n /**\n * Get or create an AbortController for the given ID and return its signal.\n * Creates the controller lazily on first access.\n */\n getSignal(id: string): AbortSignal | undefined {\n if (typeof id !== \"string\") {\n return undefined;\n }\n\n if (!this.controllers.has(id)) {\n this.controllers.set(id, new AbortController());\n }\n\n return this.controllers.get(id)!.signal;\n }\n\n /**\n * Get the signal for an existing controller without creating one.\n * Returns undefined if no controller exists for this ID.\n */\n getExistingSignal(id: string): AbortSignal | undefined {\n return this.controllers.get(id)?.signal;\n }\n\n /**\n * Cancel a specific request by aborting its controller. Optionally\n * propagate a reason — surfaces as `signal.reason` on the registry's\n * controller and through any `AbortError` it produces downstream.\n */\n cancel(id: string, reason?: unknown): void {\n this.controllers.get(id)?.abort(reason);\n }\n\n /** Remove a controller after the request completes. */\n remove(id: string): void {\n this.controllers.delete(id);\n }\n\n /**\n * Abort all pending requests and clear the registry. Optionally propagate a\n * reason — surfaces as `signal.reason` on each controller and through any\n * `AbortError` it produces downstream, exactly like {@link cancel}.\n */\n destroyAll(reason?: unknown): void {\n for (const controller of this.controllers.values()) {\n controller.abort(reason);\n }\n this.controllers.clear();\n }\n\n /** Check if a controller exists for the given ID. */\n has(id: string): boolean {\n return this.controllers.has(id);\n }\n\n /** Number of tracked controllers. */\n get size(): number {\n return this.controllers.size;\n }\n\n /**\n * Link an external `AbortSignal` to the controller for `id`. When the\n * external signal aborts, the registry's controller is cancelled —\n * propagating the abort reason — exactly the same way an internal\n * cancel would (e.g. via a `chat-request-cancel` WebSocket message).\n *\n * This is the integration point for callers that drive a chat turn\n * programmatically and want to cancel it from outside without knowing\n * the internally-generated request id (e.g. the helper-as-sub-agent\n * pattern, where a parent's `AbortSignal` from the AI SDK tool\n * `execute` needs to land inside a `Think.saveMessages` call running\n * on a child DO).\n *\n * Behavior:\n *\n * - Passing `undefined` is a no-op and returns a no-op detacher, so\n * callers can unconditionally call this with `options?.signal`.\n * - If the external signal is already aborted, the registry's\n * controller is created (if needed) and cancelled synchronously.\n * - Otherwise a one-shot `abort` listener is attached. The returned\n * function detaches it.\n *\n * **Always call the returned detacher in a `finally` block** — the\n * external signal may outlive the request (a parent chat turn that\n * drives many helper turns reuses one signal across all of them) and\n * leaving listeners attached pins closures and grows the listener\n * list on each turn.\n *\n * @returns A detacher function. Call it after the request finishes\n * (success or failure) to remove the abort listener from `signal`.\n */\n linkExternal(id: string, signal: AbortSignal | undefined): () => void {\n if (!signal) return NOOP;\n\n if (signal.aborted) {\n // Ensure the registry controller for `id` exists, then cancel it.\n // Calling getSignal first means an early external abort still\n // produces a controller for downstream observers (`getExistingSignal`)\n // rather than a silently-empty registry.\n this.getSignal(id);\n this.cancel(id, signal.reason);\n return NOOP;\n }\n\n const listener = () => this.cancel(id, signal.reason);\n signal.addEventListener(\"abort\", listener, { once: true });\n return () => signal.removeEventListener(\"abort\", listener);\n }\n}\n","/**\n * @internal Small async control-flow helpers shared by the chat hosts\n * (`@cloudflare/ai-chat` and `@cloudflare/think`) — not a public API. Extracted\n * so the host idle/stable waits and the interaction-apply completeness drain\n * stay byte-identical across both. See `design/chat-shared-layer.md`.\n */\n\n/**\n * Sentinel returned by {@link awaitWithDeadline} when the deadline elapses\n * before the awaited promise settles. A single shared symbol so both hosts\n * compare against the same identity.\n */\nexport const TIMED_OUT = Symbol(\"timed-out\");\n\n/**\n * Await `promise`, but give up and resolve to {@link TIMED_OUT} once `deadline`\n * (an absolute `Date.now()` ms timestamp) passes. A `null` deadline waits\n * indefinitely (the promise is returned unchanged). The timeout timer is always\n * cleared so it can't pin the isolate awake past resolution.\n */\nexport async function awaitWithDeadline<T>(\n promise: Promise<T>,\n deadline: number | null\n): Promise<T | typeof TIMED_OUT> {\n if (deadline == null) {\n return promise;\n }\n const remainingMs = Math.max(0, deadline - Date.now());\n let timer: ReturnType<typeof setTimeout>;\n const result = await Promise.race([\n promise,\n new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(TIMED_OUT), remainingMs);\n })\n ]);\n clearTimeout(timer!);\n return result;\n}\n\n/**\n * Drain the host's interaction-apply chain so a subsequent completeness check\n * (e.g. `hasIncompleteToolBatch`) sees every tool result that has ALREADY\n * arrived.\n *\n * Bounded by real apply activity (a storage write each), never a fixed timer:\n * `getTail` is re-read after every await because a sibling can extend the tail\n * mid-drain, and the loop stops once the tail stops advancing. Bails early when\n * `hasPending()` goes false (the pending continuation was cleared by a chat\n * clear / turn reset) so a stale drain can't hold the isolate awake.\n */\nexport async function drainInteractionApplies(\n hasPending: () => boolean,\n getTail: () => Promise<unknown>\n): Promise<void> {\n let tail = getTail();\n for (;;) {\n if (!hasPending()) return;\n try {\n await tail;\n } catch {\n // A rejected apply is irrelevant to completeness — re-read and re-check.\n }\n if (getTail() === tail) return;\n tail = getTail();\n }\n}\n","/**\n * Tool State — shared update builders and applicator for tool part state changes.\n *\n * Used by both AIChatAgent and Think to apply tool results and approvals\n * to message parts. Each agent handles find-message, persist, and broadcast\n * in their own way; this module provides the state matching and update logic.\n */\n\n/**\n * Describes an update to apply to a tool part.\n */\nexport type ToolPartUpdate = {\n toolCallId: string;\n matchStates: string[];\n apply: (part: Record<string, unknown>) => Record<string, unknown>;\n};\n\n/**\n * Apply a tool part update to a parts array.\n * Finds the first part matching `update.toolCallId` in one of `update.matchStates`,\n * applies the update immutably, and returns the new parts array with the index.\n *\n * Returns `null` if no matching part was found.\n */\nexport function applyToolUpdate(\n parts: Array<Record<string, unknown>>,\n update: ToolPartUpdate\n): { parts: Array<Record<string, unknown>>; index: number } | null {\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n if (\n \"toolCallId\" in part &&\n part.toolCallId === update.toolCallId &&\n \"state\" in part &&\n update.matchStates.includes(part.state as string)\n ) {\n const updatedParts = [...parts];\n updatedParts[i] = update.apply(part);\n return { parts: updatedParts, index: i };\n }\n }\n return null;\n}\n\n/**\n * Build an update descriptor for applying a tool result.\n *\n * Matches parts in `input-available`, `approval-requested`, or `approval-responded` state.\n * Sets state to `output-available` (with output) or `output-error` (with errorText).\n */\nexport function toolResultUpdate(\n toolCallId: string,\n output: unknown,\n overrideState?: \"output-error\",\n errorText?: string\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\n \"input-available\",\n \"approval-requested\",\n \"approval-responded\"\n ],\n apply: (part) => ({\n ...part,\n ...(overrideState === \"output-error\"\n ? {\n state: \"output-error\",\n errorText: errorText ?? \"Tool execution denied by user\"\n }\n : { state: \"output-available\", output, preliminary: false })\n })\n };\n}\n\n/**\n * Build an update descriptor for a terminal tool result that belongs to a\n * tool part in a *different* (earlier) assistant message than the one\n * currently being streamed.\n *\n * This is the \"cross-message\" case: an approved server tool executes during a\n * continuation stream, but its tool part lives in the assistant message that\n * originally requested it. `StreamAccumulator` surfaces this as a\n * `cross-message-tool-update` action because the accumulator only owns the\n * current turn's new content and cannot mutate a part from a prior message.\n *\n * Compared to {@link toolResultUpdate} this builder is deliberately more\n * defensive, mirroring the equivalent fallback in `@cloudflare/ai-chat`:\n *\n * - It matches the broad set of pre-terminal **and** terminal states, so a\n * provider that replays the entire prior tool round-trip during a\n * continuation (notably the OpenAI Responses API — issue #1404) still\n * resolves to the same part instead of silently missing it.\n * - It is **first-write-wins**: a chunk arriving for a tool that already holds\n * a terminal result is treated as a replay and the existing output is never\n * overwritten. In that case `apply` returns the *same part reference*, which\n * callers use as an idempotent-no-op signal to skip the durable write and a\n * redundant `MESSAGE_UPDATED` broadcast.\n * - It preserves a streamed `preliminary` flag when one is present, otherwise\n * marks the result final (`preliminary: false`).\n */\nexport function crossMessageToolResultUpdate(\n toolCallId: string,\n updateType: \"output-available\" | \"output-error\",\n output?: unknown,\n errorText?: string,\n preliminary?: boolean\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\n \"input-streaming\",\n \"input-available\",\n \"approval-requested\",\n \"approval-responded\",\n \"output-available\",\n \"output-error\",\n \"output-denied\"\n ],\n apply: (part) => {\n if (\n part.state === \"output-available\" ||\n part.state === \"output-error\" ||\n part.state === \"output-denied\"\n ) {\n return part;\n }\n if (updateType === \"output-error\") {\n return {\n ...part,\n state: \"output-error\",\n errorText: errorText ?? \"Tool execution failed\"\n };\n }\n return {\n ...part,\n state: \"output-available\",\n output,\n preliminary: preliminary ?? false\n };\n }\n };\n}\n\n/**\n * Build an update descriptor that replaces the output of a *paused durable\n * execution* tool part (e.g. a codemode runtime tool that paused for\n * approval).\n *\n * A paused execution completes its tool call normally — the part is already\n * `output-available` with an output of `{ status: \"paused\", executionId }`.\n * When the host later approves/rejects the execution, the new outcome\n * (completed / rejected / paused-again) must replace that output in place.\n *\n * Matching is deliberately narrow and idempotent:\n *\n * - only `output-available` parts are considered;\n * - the existing output must be a paused-execution object carrying the same\n * `executionId` — anything else (already replaced, different execution)\n * returns the *same part reference*, which callers treat as a no-op signal\n * (skip persist + broadcast), mirroring {@link crossMessageToolResultUpdate}.\n */\nexport function pausedExecutionUpdate(\n toolCallId: string,\n executionId: string,\n output: unknown\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\"output-available\"],\n apply: (part) => {\n const current = part.output as\n | { status?: unknown; executionId?: unknown }\n | null\n | undefined;\n if (\n current == null ||\n typeof current !== \"object\" ||\n current.status !== \"paused\" ||\n current.executionId !== executionId\n ) {\n return part;\n }\n return { ...part, output, preliminary: false };\n }\n };\n}\n\n/**\n * Build an update descriptor for applying a tool approval.\n *\n * Matches parts in `input-available` or `approval-requested` state.\n * Sets state to `approval-responded` (if approved) or `output-denied` (if denied).\n */\nexport function toolApprovalUpdate(\n toolCallId: string,\n approved: boolean\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\"input-available\", \"approval-requested\"],\n apply: (part) => {\n const approval =\n typeof part.approval === \"object\" &&\n part.approval !== null &&\n !Array.isArray(part.approval)\n ? (part.approval as Record<string, unknown>)\n : undefined;\n const approvalId =\n typeof approval?.id === \"string\" ? approval.id : toolCallId;\n\n return {\n ...part,\n state: approved ? \"approval-responded\" : \"output-denied\",\n approval: {\n ...approval,\n id: approvalId,\n approved\n }\n };\n }\n };\n}\n\n// ── Client-interaction predicates (recovery classification) ─────────────────\n//\n// `@internal` — these leaf predicates are byte-identical in `AIChatAgent` and\n// `Think`. The broad-vs-client-only asymmetry lives in each package's\n// higher-level `hasPendingInteraction` / `hasPendingClientInteraction`\n// wrappers, which both call the identical leaf, so the wrappers stay\n// package-local. See `design/rfc-chat-recovery-foundation.md`.\n\n/** A minimal message shape for the leaf tool/interaction scans. */\ntype ToolBatchMessage = {\n role: string;\n parts: ReadonlyArray<unknown>;\n};\n\n/** Extract a tool part's name from its `tool-<name>` / `dynamic-tool` shape. */\nexport function toolPartName(\n record: Record<string, unknown>\n): string | undefined {\n const type = typeof record.type === \"string\" ? record.type : \"\";\n if (type === \"dynamic-tool\") {\n return typeof record.toolName === \"string\" ? record.toolName : undefined;\n }\n if (type.startsWith(\"tool-\")) {\n return type.slice(\"tool-\".length);\n }\n return undefined;\n}\n\n/**\n * Whether a part is still awaiting a CLIENT interaction that can genuinely\n * arrive after a restart: an `approval-requested` part (a reconnecting client\n * replays the approval) or an `input-available` part for a CLIENT tool (the SPA\n * replays the `tool-result`). A SERVER tool's `input-available` is NOT pending —\n * its `execute()` died with the isolate.\n */\nexport function partAwaitsClientInteraction(\n part: unknown,\n clientResolvable: Set<string>\n): boolean {\n if (typeof part !== \"object\" || part === null || !(\"state\" in part)) {\n return false;\n }\n const record = part as Record<string, unknown>;\n const state = record.state;\n if (state === \"approval-requested\") return true;\n if (state !== \"input-available\") return false;\n const toolName = toolPartName(record);\n return toolName != null && clientResolvable.has(toolName);\n}\n\n/**\n * Names of the CLIENT-resolvable tools — the client-provided schemas from the\n * last request, which have no server `execute`. An interrupted `input-available`\n * part for one of these can still be resolved by the client replaying a\n * `tool-result`; a server tool's cannot.\n */\nexport function clientResolvableToolNames(\n tools: ReadonlyArray<{ name?: string } | null | undefined> | undefined\n): Set<string> {\n const names = new Set<string>();\n for (const tool of tools ?? []) {\n if (tool?.name) names.add(tool.name);\n }\n return names;\n}\n\n/**\n * `true` when the latest assistant message is mid-batch: it carries at least\n * one settled tool result AND at least one tool call/approval still awaiting a\n * client result. That is the #1649 signature — the model fanned out parallel\n * tool calls and only some have been answered. Scoped to the leaf (the step the\n * continuation answers) so an unrelated dangling tool in an earlier message\n * doesn't block a legitimate follow-up continuation.\n */\nexport function hasIncompleteToolBatch(\n messages: ReadonlyArray<ToolBatchMessage>\n): boolean {\n // Zero-allocation backward scan for the latest assistant message — this runs\n // on every barrier poll tick, and `messages` can be large.\n let leaf: ToolBatchMessage | undefined;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"assistant\") {\n leaf = messages[i];\n break;\n }\n }\n if (!leaf) return false;\n let hasPending = false;\n let hasSettled = false;\n for (const part of leaf.parts) {\n const record = part as Record<string, unknown>;\n const state = record.state;\n if (state === \"input-available\" || state === \"approval-requested\") {\n hasPending = true;\n } else if (\n typeof record.type === \"string\" &&\n (record.type.startsWith(\"tool-\") || record.type === \"dynamic-tool\") &&\n (state === \"output-available\" ||\n state === \"output-error\" ||\n state === \"output-denied\" ||\n state === \"approval-responded\")\n ) {\n hasSettled = true;\n }\n if (hasPending && hasSettled) return true;\n }\n return false;\n}\n","/**\n * Protocol Message Parser — typed parsing of cf_agent_chat_* WebSocket messages.\n *\n * Parses raw WebSocket messages into a discriminated union of protocol events.\n * Both AIChatAgent and Think can use this instead of manual JSON.parse + type checking.\n */\n\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\n\n/**\n * Discriminated union of all incoming chat protocol events.\n *\n * Each agent handles the events it cares about and ignores the rest.\n * Returns `null` for non-JSON messages or unrecognized types.\n */\nexport type ChatProtocolEvent =\n | {\n type: \"chat-request\";\n id: string;\n init: { method?: string; body?: string; [key: string]: unknown };\n }\n | { type: \"clear\" }\n | { type: \"cancel\"; id: string }\n | {\n type: \"tool-result\";\n toolCallId: string;\n toolName: string;\n output: unknown;\n state?: string;\n errorText?: string;\n autoContinue?: boolean;\n clientTools?: Array<{\n name: string;\n description?: string;\n parameters?: unknown;\n }>;\n }\n | {\n type: \"tool-approval\";\n toolCallId: string;\n approved: boolean;\n autoContinue?: boolean;\n }\n | { type: \"stream-resume-request\" }\n | { type: \"stream-resume-ack\"; id: string }\n | { type: \"messages\"; messages: unknown[] };\n\n/**\n * Parse a raw WebSocket message string into a typed protocol event.\n *\n * Returns `null` if the message is not valid JSON or not a recognized\n * protocol message type. Callers should fall through to the user's\n * `onMessage` handler when `null` is returned.\n *\n * @example\n * ```typescript\n * const event = parseProtocolMessage(rawMessage);\n * if (!event) return userOnMessage(connection, rawMessage);\n *\n * switch (event.type) {\n * case \"chat-request\": { ... }\n * case \"clear\": { ... }\n * case \"tool-result\": { ... }\n * }\n * ```\n */\nexport function parseProtocolMessage(raw: string): ChatProtocolEvent | null {\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const wireType = data.type as string | undefined;\n if (!wireType) return null;\n\n switch (wireType) {\n case CHAT_MESSAGE_TYPES.USE_CHAT_REQUEST:\n return {\n type: \"chat-request\",\n id: data.id as string,\n init: (data.init as { method?: string; body?: string }) ?? {}\n };\n\n case CHAT_MESSAGE_TYPES.CHAT_CLEAR:\n return { type: \"clear\" };\n\n case CHAT_MESSAGE_TYPES.CHAT_REQUEST_CANCEL:\n return { type: \"cancel\", id: data.id as string };\n\n case CHAT_MESSAGE_TYPES.TOOL_RESULT:\n return {\n type: \"tool-result\",\n toolCallId: data.toolCallId as string,\n toolName: (data.toolName as string) ?? \"\",\n output: data.output,\n state: data.state as string | undefined,\n errorText: data.errorText as string | undefined,\n autoContinue: data.autoContinue as boolean | undefined,\n clientTools: data.clientTools as\n | Array<{\n name: string;\n description?: string;\n parameters?: unknown;\n }>\n | undefined\n };\n\n case CHAT_MESSAGE_TYPES.TOOL_APPROVAL:\n return {\n type: \"tool-approval\",\n toolCallId: data.toolCallId as string,\n approved: data.approved as boolean,\n autoContinue: data.autoContinue as boolean | undefined\n };\n\n case CHAT_MESSAGE_TYPES.STREAM_RESUME_REQUEST:\n return { type: \"stream-resume-request\" };\n\n case CHAT_MESSAGE_TYPES.STREAM_RESUME_ACK:\n return {\n type: \"stream-resume-ack\",\n id: data.id as string\n };\n\n case CHAT_MESSAGE_TYPES.CHAT_MESSAGES:\n return {\n type: \"messages\",\n messages: (data.messages as unknown[]) ?? []\n };\n\n default:\n return null;\n }\n}\n","/**\n * Message reconciliation — pure functions for aligning client messages\n * with server state during persistence.\n *\n * Three strategies applied in order:\n * 1. Merge server-known tool outputs into stale client messages\n * 2. Reconcile assistant IDs (exact match → content-key → toolCallId)\n * 3. Per-message toolCallId dedup for persistence\n */\n\nimport type { UIMessage } from \"ai\";\n\n/**\n * Reconcile incoming client messages against server state.\n *\n * 1. Merges server-known tool outputs into incoming messages that still\n * show stale states (input-available, approval-requested, approval-responded)\n * 2. Reconciles assistant IDs: exact match → content-key match → toolCallId match\n *\n * @param incoming - Messages from the client\n * @param serverMessages - Current server-side messages (source of truth)\n * @param sanitizeForContentKey - Function to sanitize a message before computing\n * its content key (typically strips ephemeral provider metadata)\n * @returns Reconciled messages ready for persistence\n */\nexport function reconcileMessages(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[],\n sanitizeForContentKey?: (message: UIMessage) => UIMessage\n): UIMessage[] {\n const withMergedToolOutputs = mergeServerToolOutputs(\n incoming,\n serverMessages\n );\n return reconcileAssistantIds(\n withMergedToolOutputs,\n serverMessages,\n sanitizeForContentKey\n );\n}\n\n/**\n * For a single message, resolve its ID by matching toolCallId against server state.\n * Prevents duplicate DB rows when client IDs differ from server IDs.\n * Tool call IDs are unique per conversation, so matching is safe regardless of state.\n */\nexport function resolveToolMergeId(\n message: UIMessage,\n serverMessages: readonly UIMessage[]\n): UIMessage {\n if (message.role !== \"assistant\") {\n return message;\n }\n\n for (const part of message.parts) {\n if (\"toolCallId\" in part && part.toolCallId) {\n const toolCallId = part.toolCallId as string;\n const existing = findMessageByToolCallId(serverMessages, toolCallId);\n if (existing && existing.id !== message.id) {\n return { ...message, id: existing.id };\n }\n }\n }\n\n return message;\n}\n\n/**\n * Merge a freshly-reconstructed orphaned partial onto the assistant message\n * that already owns its target id (the orphan-persist **(c)** step).\n *\n * Used by hosts whose store can hold an assistant row for the SAME id BEFORE\n * the stream finalizes — e.g. an early persist at tool-approval time, or a\n * continuation resuming the prior assistant message. On recovery the engine\n * replays the same chunks, so a naive append would leave two parts per tool\n * call. The merge therefore:\n *\n * - keeps ALL existing parts (the persisted row is authoritative for tool\n * parts that had a client result applied IN PLACE — that result lives only\n * in storage, never in the chunk stream, so a whole-message replace would\n * clobber it);\n * - appends only the reconstructed parts whose `toolCallId` is NOT already\n * present (dedup by tool-call identity);\n * - overlays the incoming metadata onto the existing metadata (incoming wins\n * on conflicts), falling back to whichever side is present.\n *\n * The result carries the INCOMING message's id/role (the caller has already\n * resolved the incoming id to the existing row's id via the (b) target-id\n * step), so it is safe to write straight back through `updateMessage`.\n *\n * Hosts whose orphan persist only ever runs at stream finalize (no early/\n * mid-stream row for the same id) never hit the merge branch and don't need\n * this — a plain append/replace is already dedup-safe because the shared\n * reconstruction (`StreamAccumulator` / `applyChunkToParts`) is idempotent by\n * `toolCallId`.\n */\nexport function reconcileOrphanPartial(\n existing: UIMessage,\n incoming: UIMessage\n): UIMessage {\n const existingToolCallIds = new Set(\n existing.parts\n .filter((p): p is typeof p & { toolCallId: string } => \"toolCallId\" in p)\n .map((p) => p.toolCallId)\n );\n const newParts = incoming.parts.filter(\n (p) => !(\"toolCallId\" in p && existingToolCallIds.has(p.toolCallId))\n );\n\n const merged: UIMessage = {\n ...incoming,\n parts: [...existing.parts, ...newParts]\n };\n if (existing.metadata) {\n merged.metadata = incoming.metadata\n ? { ...existing.metadata, ...incoming.metadata }\n : existing.metadata;\n }\n return merged;\n}\n\n/**\n * Content key for assistant messages used for dedup of identical short replies.\n * Returns JSON of sanitized parts, or undefined for non-assistant messages.\n */\nexport function assistantContentKey(\n message: UIMessage,\n sanitize?: (message: UIMessage) => UIMessage\n): string | undefined {\n if (message.role !== \"assistant\") {\n return undefined;\n }\n const sanitized = sanitize ? sanitize(message) : message;\n return JSON.stringify(sanitized.parts);\n}\n\nfunction mergeServerToolOutputs(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[]\n): UIMessage[] {\n // Index the server's RESOLVED tool parts so a stale client part (still in a\n // pre-output state) can't clobber the server's terminal state on persist.\n // All three terminal states must be protected, not just `output-available`:\n // otherwise a client that hasn't seen the server's `output-error` /\n // `output-denied` yet would persist its stale `input-available` over the\n // resolved result, losing the error/denial.\n const serverResolvedParts = new Map<string, Record<string, unknown>>();\n for (const msg of serverMessages) {\n if (msg.role !== \"assistant\") continue;\n for (const part of msg.parts) {\n const record = part as Record<string, unknown>;\n if (\n \"toolCallId\" in record &&\n \"state\" in record &&\n (record.state === \"output-available\" ||\n record.state === \"output-error\" ||\n record.state === \"output-denied\")\n ) {\n serverResolvedParts.set(record.toolCallId as string, record);\n }\n }\n }\n\n if (serverResolvedParts.size === 0) return incoming;\n\n return incoming.map((msg) => {\n if (msg.role !== \"assistant\") return msg;\n\n let hasChanges = false;\n const updatedParts = msg.parts.map((part) => {\n const record = part as Record<string, unknown>;\n if (\n \"toolCallId\" in record &&\n \"state\" in record &&\n (record.state === \"input-available\" ||\n record.state === \"approval-requested\" ||\n record.state === \"approval-responded\") &&\n serverResolvedParts.has(record.toolCallId as string)\n ) {\n hasChanges = true;\n const server = serverResolvedParts.get(record.toolCallId as string)!;\n // Overlay the server's resolved state, keeping the client part's\n // identity/input. Carry ONLY the result field that belongs to the\n // server's terminal state — so a stray `output` left on an\n // `output-error` part can't ride along and be misread as a result.\n const merged: Record<string, unknown> = {\n ...part,\n state: server.state\n };\n if (server.state === \"output-available\") {\n if (\"output\" in server) merged.output = server.output;\n } else if (server.state === \"output-error\") {\n if (\"errorText\" in server) merged.errorText = server.errorText;\n } else if (server.state === \"output-denied\") {\n if (\"approval\" in server) merged.approval = server.approval;\n }\n return merged;\n }\n return part;\n }) as UIMessage[\"parts\"];\n\n return hasChanges ? { ...msg, parts: updatedParts } : msg;\n });\n}\n\nfunction reconcileAssistantIds(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[],\n sanitize?: (message: UIMessage) => UIMessage\n): UIMessage[] {\n if (serverMessages.length === 0) return incoming;\n\n const claimedServerIndices = new Set<number>();\n const exactMatchMap = new Map<number, number>();\n\n for (let i = 0; i < incoming.length; i++) {\n const serverIdx = serverMessages.findIndex(\n (sm, si) => !claimedServerIndices.has(si) && sm.id === incoming[i].id\n );\n if (serverIdx !== -1) {\n claimedServerIndices.add(serverIdx);\n exactMatchMap.set(i, serverIdx);\n }\n }\n\n return incoming.map((incomingMessage, incomingIdx) => {\n if (exactMatchMap.has(incomingIdx)) {\n return incomingMessage;\n }\n\n if (\n incomingMessage.role !== \"assistant\" ||\n hasToolCallPart(incomingMessage)\n ) {\n return incomingMessage;\n }\n\n const incomingKey = assistantContentKey(incomingMessage, sanitize);\n if (!incomingKey) {\n return incomingMessage;\n }\n\n for (let i = 0; i < serverMessages.length; i++) {\n if (claimedServerIndices.has(i)) continue;\n\n const serverMessage = serverMessages[i];\n if (\n serverMessage.role !== \"assistant\" ||\n hasToolCallPart(serverMessage)\n ) {\n continue;\n }\n\n if (assistantContentKey(serverMessage, sanitize) === incomingKey) {\n claimedServerIndices.add(i);\n return { ...incomingMessage, id: serverMessage.id };\n }\n }\n\n return incomingMessage;\n });\n}\n\nfunction hasToolCallPart(message: UIMessage): boolean {\n return message.parts.some((part) => \"toolCallId\" in part);\n}\n\nfunction findMessageByToolCallId(\n messages: readonly UIMessage[],\n toolCallId: string\n): UIMessage | undefined {\n for (const msg of messages) {\n if (msg.role !== \"assistant\") continue;\n for (const part of msg.parts) {\n if (\"toolCallId\" in part && part.toolCallId === toolCallId) {\n return msg;\n }\n }\n }\n return undefined;\n}\n","/**\n * Transcript repair — flip interrupted tool calls (a `tool-*` / `dynamic-tool`\n * part with no settled result, left behind when a stream was cut off mid-flight)\n * into a settled shape so the next provider call does not 400 with\n * `AI_MissingToolResultsError`, and normalize malformed tool `input`.\n *\n * This is the shared, host-agnostic core extracted from `@cloudflare/think`'s\n * `_repairToolTranscriptParts`. Both AI-SDK chat hosts (`@cloudflare/think` and\n * `@cloudflare/ai-chat`) run it before re-entering inference on a recovered turn\n * so an interrupted SERVER tool (whose `execute()` died with the evicted\n * isolate, leaving an `input-available` orphan that nothing will ever resolve)\n * is converted to an errored tool-result and the turn can continue, instead of\n * being abandoned.\n *\n * Pure: it returns a new messages array plus repair stats and never touches\n * storage, broadcast, or events — the host owns those side effects.\n *\n * @internal Shared chat-recovery internals; not a public API.\n */\n\nimport type { UIMessage } from \"ai\";\nimport { normalizeToolInput } from \"./message-builder\";\n\n/**\n * Whether a tool part already has a settled result the provider accepts, so it\n * must NOT be re-repaired into an errored result.\n *\n * Single source of truth for the terminal tool states. Mirrors the AI SDK's\n * terminal states: `convertToModelMessages` emits a `tool-result` for\n * `output-available`, `output-error`, AND `output-denied` (a user-denied\n * approval — its denial reason becomes the tool-result). Omitting any of these\n * makes repair re-flip the part every turn — clobbering a real `errorText` /\n * denial with the generic \"interrupted\" message.\n */\nexport function toolPartHasSettledResult(\n record: Record<string, unknown>\n): boolean {\n if (\"output\" in record || \"result\" in record) return true;\n const state = typeof record.state === \"string\" ? record.state : \"\";\n return (\n state === \"output-available\" ||\n state === \"output-error\" ||\n state === \"output-denied\"\n );\n}\n\nexport interface RepairInterruptedToolPartsOptions {\n /**\n * Decide the replacement for an interrupted tool part (no settled result, not\n * `approval-responded`). Its `input` has already been normalized to a valid\n * object. The default host behavior flips it to an errored tool-result; hosts\n * expose this as an overridable `repairInterruptedToolPart` hook so a subclass\n * can, e.g., convert an interrupted client-resolved tool into a text part.\n */\n repairPart: (part: UIMessage[\"parts\"][number]) => UIMessage[\"parts\"][number];\n /**\n * Whether a tool part already carries a settled result (defaults to\n * {@link toolPartHasSettledResult}).\n */\n isSettled?: (record: Record<string, unknown>) => boolean;\n /**\n * Normalize a tool part's `input` (defaults to the shared\n * {@link normalizeToolInput}).\n */\n normalizeInput?: (input: unknown) => { input: unknown; changed: boolean };\n /**\n * Whether an interrupted tool part (no settled result, not\n * `approval-responded`) should be repaired at all. Defaults to `true` (repair\n * everything, like Think — which converts even client tools via its\n * `repairPart` override). A host whose default `repairPart` errors the part\n * (ai-chat) passes this to SKIP a part still legitimately awaiting a CLIENT\n * interaction (an `input-available` client tool or an `approval-requested`\n * part the user may still answer) so it is left verbatim rather than clobbered\n * with an error. Skipped parts are not counted in `removedToolCalls`.\n */\n shouldRepair?: (part: UIMessage[\"parts\"][number]) => boolean;\n}\n\nexport interface RepairInterruptedToolPartsResult {\n /** A new messages array; unchanged messages keep their original reference. */\n messages: UIMessage[];\n /** Count of interrupted tool calls flipped to a repaired shape. */\n removedToolCalls: number;\n /** Count of tool parts whose malformed `input` was normalized. */\n normalizedInputs: number;\n /** The tool-call ids that were repaired. */\n toolCallIds: string[];\n}\n\n/**\n * Repair interrupted tool calls and normalize malformed tool inputs across a\n * transcript. Behavior mirrors `@cloudflare/think`'s original\n * `_repairToolTranscriptParts`:\n *\n * - a tool part with NO settled result and state `approval-responded` is kept\n * verbatim (an approved server tool waiting for its continuation to run\n * `execute()` — not abandoned);\n * - a tool part with NO settled result for which `shouldRepair` returns false\n * is kept verbatim (a part still awaiting a CLIENT interaction; see option);\n * - any other tool part with no settled result is normalized then handed to\n * `repairPart` (default: flipped to an errored result);\n * - a tool part WITH a settled result only has its `input` normalized.\n *\n * Messages with no changed part keep their original object reference so callers\n * can cheaply detect what to persist.\n */\nexport function repairInterruptedToolParts(\n messages: UIMessage[],\n options: RepairInterruptedToolPartsOptions\n): RepairInterruptedToolPartsResult {\n const isSettled = options.isSettled ?? toolPartHasSettledResult;\n const normalizeInput = options.normalizeInput ?? normalizeToolInput;\n const { repairPart } = options;\n const shouldRepair = options.shouldRepair ?? (() => true);\n\n let removedToolCalls = 0;\n let normalizedInputs = 0;\n const toolCallIds: string[] = [];\n const repaired: UIMessage[] = [];\n\n for (const message of messages) {\n const parts: UIMessage[\"parts\"] = [];\n let messageChanged = false;\n for (const part of message.parts) {\n const record = part as Record<string, unknown>;\n const toolCallId =\n typeof record.toolCallId === \"string\" ? record.toolCallId : undefined;\n const isToolPart =\n typeof record.type === \"string\" &&\n (record.type.startsWith(\"tool-\") || record.type === \"dynamic-tool\") &&\n toolCallId;\n if (!isToolPart) {\n parts.push(part);\n continue;\n }\n\n if (!isSettled(record)) {\n const state = typeof record.state === \"string\" ? record.state : \"\";\n // An approved server tool waits at `approval-responded` until its\n // scheduled continuation runs `execute()`. It is not abandoned, so\n // preserve it verbatim — flipping it to an error (or removing it) would\n // strand the approval and prevent the real result from ever being\n // produced by the continuation.\n if (state === \"approval-responded\") {\n parts.push(part);\n continue;\n }\n // A part still legitimately awaiting a CLIENT interaction (the host's\n // `shouldRepair` returns false) is left verbatim — erroring it would\n // clobber a tool-result / approval the client may still replay. Only\n // hosts whose default repair ERRORS the part opt into this; Think keeps\n // the default (repair everything, converting client tools via its hook).\n if (!shouldRepair(part)) {\n parts.push(part);\n continue;\n }\n // Preserve the interrupted/abandoned tool call instead of deleting it.\n // Deleting makes the call \"disappear\" from the (broadcast) transcript\n // and lets the model silently re-run it. `input` is normalized to a\n // valid object first, then `repairPart` decides the replacement shape\n // (default: an errored result, so conversion still gets a tool-result\n // and the provider doesn't 400 with AI_MissingToolResultsError).\n const normalized = normalizeInput(\n \"input\" in record ? record.input : undefined\n );\n parts.push(\n repairPart({\n ...part,\n input: normalized.input\n } as UIMessage[\"parts\"][number])\n );\n if (normalized.changed) normalizedInputs++;\n removedToolCalls++;\n messageChanged = true;\n toolCallIds.push(toolCallId);\n continue;\n }\n\n const normalized = normalizeInput(\n \"input\" in record ? record.input : undefined\n );\n if (normalized.changed) {\n parts.push({\n ...part,\n input: normalized.input\n } as UIMessage[\"parts\"][number]);\n normalizedInputs++;\n messageChanged = true;\n continue;\n }\n\n parts.push(part);\n }\n\n repaired.push(messageChanged ? { ...message, parts } : message);\n }\n\n return {\n messages: repaired,\n removedToolCalls,\n normalizedInputs,\n toolCallIds\n };\n}\n","/**\n * Orphan-persist core — reconstruct a message from an interrupted stream's\n * buffered chunks and upsert it through an {@link OrphanPersistStore}.\n *\n * This is the genuinely-shared skeleton extracted from the two AI-SDK chat\n * hosts' `_persistOrphanedStream` (`@cloudflare/think` and\n * `@cloudflare/ai-chat`): the accumulate loop plus the\n * `getMessage → updateMessage(merge) XOR appendMessage` upsert. The\n * deliberately host-specific bits stay in the callers:\n *\n * - buffer flush (Think flushes defensively first; ai-chat doesn't);\n * - the fallback message id;\n * - `prepare` — Think strips internal parts and may skip (`null`); ai-chat\n * resolves the persist-target id from stream metadata;\n * - `merge` — Think replaces the whole message; ai-chat reconciles partials\n * so an in-place tool result isn't re-advanced by a replayed chunk; and\n * - broadcast (Think broadcasts after; ai-chat broadcasts inside its store's\n * `persistMessages`).\n *\n * Pure orchestration: it performs exactly one store write (update XOR append),\n * never touches the buffer, and never broadcasts.\n *\n * @internal Shared chat-recovery internals; not a public API.\n */\n\nimport type { UIMessage } from \"ai\";\nimport { StreamAccumulator } from \"./stream-accumulator\";\nimport type { StreamChunkData } from \"./message-builder\";\nimport type { OrphanPersistStore } from \"./orphan-store\";\n\nexport interface PersistReconstructedOrphanOptions<\n TMessage extends UIMessage = UIMessage\n> {\n /** The store seam to upsert through (a `SessionProvider` write-subset). */\n store: OrphanPersistStore<TMessage>;\n /**\n * Id for the reconstructed message when the stream carried no provider\n * `start.messageId` to adopt. The accumulator still adopts a provider id when\n * present.\n */\n fallbackId: string;\n /**\n * Finalize the reconstructed message before upsert — e.g. strip internal\n * parts or resolve the persist-target id. Return `null` to skip persistence\n * entirely (e.g. an empty structural-only message).\n */\n prepare: (message: TMessage) => TMessage | null;\n /**\n * Combine an existing row with the reconstructed message when a row already\n * owns the id (replace, or reconcile partials).\n */\n merge: (existing: TMessage, incoming: TMessage) => TMessage;\n}\n\n/**\n * Reconstruct a message from `chunks` and upsert it via the store. Returns\n * `true` when a write happened (so a caller that broadcasts after — Think — can\n * gate its broadcast on it), `false` when there was nothing to persist (no\n * parts, or `prepare` returned `null`).\n */\nexport async function persistReconstructedOrphan<\n TMessage extends UIMessage = UIMessage\n>(\n chunks: ReadonlyArray<{ body: string }>,\n options: PersistReconstructedOrphanOptions<TMessage>\n): Promise<boolean> {\n if (chunks.length === 0) return false;\n\n const accumulator = new StreamAccumulator({ messageId: options.fallbackId });\n for (const chunk of chunks) {\n try {\n accumulator.applyChunk(JSON.parse(chunk.body) as StreamChunkData);\n } catch {\n // Skip malformed chunk bodies.\n }\n }\n\n if (accumulator.parts.length === 0) return false;\n\n const prepared = options.prepare(accumulator.toMessage() as TMessage);\n if (prepared === null) return false;\n\n const existing = await options.store.getMessage(prepared.id);\n if (existing) {\n await options.store.updateMessage(options.merge(existing, prepared));\n } else {\n await options.store.appendMessage(prepared);\n }\n return true;\n}\n","import type { ClientToolSchema } from \"./client-tools\";\n\n/**\n * The minimal transcript-tail shape {@link createChatFiberSnapshot} reads to\n * derive the snapshot's `latest*Id` markers. Deliberately NOT `UIMessage`: the\n * snapshot only ever needs each message's `id` + `role`, so any host transcript\n * (AI SDK `UIMessage[]`, `Think`'s session leaves, or the pi adapter's plain\n * `AgentMessage[]`) satisfies it structurally. Keeping this off `UIMessage` is\n * the Phase-5 genericity seam — the snapshot builder must not couple to the AI\n * SDK message shape.\n */\nexport interface SnapshotMessage {\n id?: string;\n role: string;\n}\n\nexport type ChatFiberSnapshot<Kind extends string = string> = {\n kind: Kind;\n version: 1;\n requestId: string;\n recoveryRootRequestId?: string;\n continuation: boolean;\n latestMessageId?: string;\n latestMessageRole?: string;\n latestUserMessageId?: string;\n startedAt: number;\n lastBody?: Record<string, unknown>;\n lastClientTools?: ClientToolSchema[];\n};\n\nexport function createChatFiberSnapshot<Kind extends string>({\n kind,\n requestId,\n recoveryRootRequestId,\n continuation,\n messages,\n lastBody,\n lastClientTools\n}: {\n kind: Kind;\n requestId: string;\n recoveryRootRequestId?: string;\n continuation: boolean;\n messages: ReadonlyArray<SnapshotMessage>;\n lastBody?: Record<string, unknown>;\n lastClientTools?: ClientToolSchema[];\n}): ChatFiberSnapshot<Kind> {\n const latestMessage =\n messages.length > 0 ? messages[messages.length - 1] : undefined;\n let latestUser: SnapshotMessage | undefined;\n\n for (let index = messages.length - 1; index >= 0; index--) {\n if (messages[index].role === \"user\") {\n latestUser = messages[index];\n break;\n }\n }\n\n return {\n kind,\n version: 1,\n requestId,\n recoveryRootRequestId,\n continuation,\n latestMessageId: latestMessage?.id,\n latestMessageRole: latestMessage?.role,\n latestUserMessageId: latestUser?.id,\n startedAt: Date.now(),\n lastBody,\n lastClientTools\n };\n}\n\nexport function wrapChatFiberSnapshot<Kind extends string>(\n key: string,\n snapshot: ChatFiberSnapshot<Kind>,\n user: unknown | null\n): Record<string, unknown> {\n return { [key]: snapshot, user };\n}\n\nexport function unwrapChatFiberSnapshot<Kind extends string>(\n key: string,\n value: unknown,\n expectedKind?: Kind\n): {\n snapshot: ChatFiberSnapshot<Kind> | null;\n user: unknown | null;\n} {\n if (typeof value !== \"object\" || value === null || !(key in value)) {\n return { snapshot: null, user: value };\n }\n\n const envelope = value as Record<string, unknown>;\n const snapshot = envelope[key];\n if (typeof snapshot !== \"object\" || snapshot === null) {\n return { snapshot: null, user: value };\n }\n const candidate = snapshot as Record<string, unknown>;\n if (\n candidate.version !== 1 ||\n (expectedKind !== undefined && candidate.kind !== expectedKind) ||\n typeof candidate.requestId !== \"string\" ||\n typeof candidate.continuation !== \"boolean\"\n ) {\n return { snapshot: null, user: value };\n }\n\n return {\n snapshot: snapshot as ChatFiberSnapshot<Kind>,\n user: envelope.user ?? null\n };\n}\n","/**\n * `ChatRecoveryCodec` — the streaming-codec seam the recovery engine replays an\n * interrupted turn's durable buffer through to reconstruct its partial assistant\n * state. The engine and hosts only ever see the wire-agnostic `RecoveryPartial`\n * shape (`{ text, parts }`); the codec owns the chunk-vocabulary differences.\n *\n * Two implementations exist today: {@link AISDKRecoveryCodec} (AI SDK SSE chunks,\n * used by `@cloudflare/ai-chat` and `@cloudflare/think`) and `PiRecoveryCodec`\n * (the pi `AgentEvent` vocabulary, in the `experimental/pi-recovery` fixture).\n * Formalizing the interface here is the proof that the codec — not the engine —\n * carries the chunk-shape contract.\n *\n * @internal Shared chat-recovery internals; not a public API.\n */\n\nimport { getPartialStreamText } from \"./message-builder\";\nimport type { MessagePart } from \"./message-builder\";\nimport type { RecoveryPartial } from \"./recovery-engine\";\n\n/**\n * Whether a reconstructed AI SDK `UIMessage` parts array carries any settled\n * (provider-accepted) tool result — the completed, often non-idempotent work\n * that a `{ persist: false }` recovery return would otherwise silently discard\n * (#1631). A part counts as settled when it is a tool part (`tool-*` /\n * `dynamic-tool`) carrying an `output`/`result`, or whose state reached a\n * terminal `output-{available,error,denied}`.\n *\n * This is the AI SDK codec's implementation of the per-vocabulary \"did this\n * partial settle a tool?\" question. It lives with {@link AISDKRecoveryCodec}\n * (not in the engine) because the codec owns the part vocabulary — the engine\n * only ever reads the precomputed `RecoveryPartial.hasSettledToolResults`\n * boolean and never names a part type. Foreign codecs (e.g. AG-UI) compute the\n * same boolean from their own chunk vocabulary without producing AI SDK parts.\n */\nexport function partialHasSettledToolResults(parts: MessagePart[]): boolean {\n return parts.some((part) => {\n const record = part as Record<string, unknown>;\n const type = typeof record.type === \"string\" ? record.type : \"\";\n if (!(type.startsWith(\"tool-\") || type === \"dynamic-tool\")) return false;\n if (\"output\" in record || \"result\" in record) return true;\n const state = typeof record.state === \"string\" ? record.state : \"\";\n return (\n state === \"output-available\" ||\n state === \"output-error\" ||\n state === \"output-denied\"\n );\n });\n}\n\n/**\n * Reconstructs the partial assistant state of an interrupted turn from its\n * stored `ResumableStream` chunk bodies (oldest-first).\n */\nexport interface ChatRecoveryCodec {\n /**\n * Replay the stored chunk bodies into the engine's `RecoveryPartial`. The\n * codec — not the engine — both reconstructs `parts` (in its own vocabulary,\n * opaque to the engine) AND decides `hasSettledToolResults`, so the engine\n * never names a part type.\n */\n toRecoveryPartial(bodies: string[]): RecoveryPartial;\n /**\n * Whether a stored chunk of this wire `type` is a **progress milestone** — a\n * started text/reasoning segment or a settled tool input/output — that should\n * always credit the host's recovery no-progress window (#1637). The chunk-type\n * list lives HERE (the codec owns the chunk vocabulary). A `undefined` type (a\n * non-JSON / typeless body) is never progress.\n */\n isProgressChunk(type: string | undefined): boolean;\n /**\n * Whether a stored chunk of this wire `type` is **mid-segment streaming\n * content** — a delta extending an already-started segment (text/reasoning\n * body, partial tool input). On its own a delta is too granular to credit per\n * token, but a long single segment that produces only deltas (no new\n * milestone) must still register forward progress across repeated crashes, or\n * its no-progress window can false-fire while content is genuinely streaming.\n * Hosts credit these through a time throttle (see {@link\n * shouldCreditStreamProgress}). Disjoint from {@link isProgressChunk}; a\n * `undefined` type is never streaming content.\n */\n isStreamingContentChunk(type: string | undefined): boolean;\n}\n\n/** Minimal per-isolate throttle gate (see `StreamProgressCreditThrottle`). */\nexport interface ProgressCreditThrottle {\n shouldCredit(now: number): boolean;\n}\n\n/**\n * The single, host-agnostic rule for crediting recovery forward progress from a\n * stored stream chunk — the convergence of what `AIChatAgent` and `Think`\n * previously each decided on their own (ai-chat keyed on chunk type only; Think\n * keyed on its flush cadence). Both hosts now call this at chunk-store time so\n * the bump TIMING is identical:\n *\n * - a **milestone** ({@link ChatRecoveryCodec.isProgressChunk}) always credits;\n * - **streaming content** ({@link ChatRecoveryCodec.isStreamingContentChunk})\n * credits at most once per throttle window, so a long single segment still\n * registers progress across crashes without writing storage per token;\n * - anything else never credits.\n *\n * Finer than either host's prior cadence in the worst case and never coarser, so\n * it can only delay/avoid a false `no_progress_timeout`, never hasten give-up.\n */\nexport function shouldCreditStreamProgress(input: {\n codec: Pick<ChatRecoveryCodec, \"isProgressChunk\" | \"isStreamingContentChunk\">;\n type: string | undefined;\n throttle: ProgressCreditThrottle;\n now: number;\n}): boolean {\n const { codec, type, throttle, now } = input;\n if (codec.isProgressChunk(type)) return true;\n if (codec.isStreamingContentChunk(type)) return throttle.shouldCredit(now);\n return false;\n}\n\n/**\n * The AI SDK codec: replays SSE chunk bodies through {@link getPartialStreamText}\n * (`applyChunkToParts` under the hood). Stateless — share the\n * {@link aiSdkRecoveryCodec} singleton rather than constructing per call.\n */\nexport class AISDKRecoveryCodec implements ChatRecoveryCodec {\n // Return type is intentionally INFERRED (not annotated `RecoveryPartial`) so it\n // keeps the concrete `parts: MessagePart[]`, which the AI SDK hosts' own\n // `_getPartialStreamText` callers rely on. It is still assignable to\n // `RecoveryPartial` (whose `parts` is `unknown[]`), so the engine seam stays\n // vocabulary-agnostic while AI SDK callers keep their typed parts.\n toRecoveryPartial(bodies: string[]): {\n text: string;\n parts: MessagePart[];\n hasSettledToolResults: boolean;\n } {\n const { text, parts } = getPartialStreamText(\n bodies.map((body) => ({ body }))\n );\n return {\n text,\n parts,\n hasSettledToolResults: partialHasSettledToolResults(parts)\n };\n }\n\n isProgressChunk(type: string | undefined): boolean {\n return (\n type === \"text-start\" ||\n type === \"reasoning-start\" ||\n type === \"tool-input-available\" ||\n type === \"tool-output-available\" ||\n type === \"tool-output-error\" ||\n type === \"tool-output-denied\"\n );\n }\n\n isStreamingContentChunk(type: string | undefined): boolean {\n return (\n type === \"text-delta\" ||\n type === \"reasoning-delta\" ||\n type === \"tool-input-delta\"\n );\n }\n}\n\n/** Shared stateless {@link AISDKRecoveryCodec} instance. */\nexport const aiSdkRecoveryCodec = new AISDKRecoveryCodec();\n","/**\n * Shared resume-handshake driver (rfc-chat-recovery-foundation, Tier-2).\n *\n * The server side of the WebSocket stream-resume protocol — the byte-parallel\n * block `@cloudflare/ai-chat` and `@cloudflare/think` previously hand-maintained\n * in lockstep, now shared here:\n * the proactive `STREAM_RESUMING` notify, the `STREAM_RESUME_REQUEST` decision\n * tree, the `STREAM_RESUME_ACK` decision tree, and the terminal-replay path\n * (#1645). The two hosts diverge only in the idle-connect payload (kept\n * host-owned, NOT here) and the use-chat-response message-type constant (a\n * {@link ResumeHandshakeHost} field).\n *\n * Frame shapes are frozen by the golden fixture in\n * `__tests__/resume-handshake-frames.ts`; this driver must keep emitting exactly\n * those bytes.\n *\n * `@internal` — sibling-package support, not a public API. See\n * `design/rfc-chat-recovery-foundation.md`.\n */\n\nimport type { Connection } from \"agents\";\nimport { sendIfOpen } from \"./connection\";\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\nimport type { ContinuationState } from \"./continuation-state\";\nimport type { PreStreamTurns } from \"./pre-stream-turns\";\nimport type { ResumableStream } from \"./resumable-stream\";\n\n/** A pending terminal outcome captured before connect (#1645). */\nexport interface PendingChatTerminal {\n requestId: string;\n body: string;\n}\n\n/**\n * The host-owned surface the resume handshake threads. `pendingResumeConnections`\n * (and the continuation `awaitingConnections` map) stay host-owned — they are\n * also touched by the streaming loop, which is NOT part of this extraction — so\n * the driver only reads/mutates them through this seam rather than owning them.\n */\nexport interface ResumeHandshakeHost {\n /** The host's use-chat-response message-type constant (wire string). */\n readonly responseMessageType: string;\n readonly resumableStream: ResumableStream;\n readonly continuation: ContinuationState<Connection>;\n /**\n * Accepted-but-not-yet-streamed turns (#1784). Optional: experimental\n * recovery adapters that don't track a pre-stream window omit it and keep the\n * legacy `resume_none` behavior. When present, the handshake parks a\n * reconnecting client here (sending a keep-waiting `STREAM_PENDING`) instead\n * of telling it there is nothing to resume.\n */\n readonly preStream?: PreStreamTurns<Connection>;\n /**\n * Connections notified of a resumable stream, excluded from live broadcast\n * until they ACK. Host-owned (shared with the streaming loop).\n */\n readonly pendingResumeConnections: Set<string>;\n /** Read the pending terminal outcome (#1645), or `null` when none survives. */\n pendingChatTerminal(): Promise<PendingChatTerminal | null>;\n /** Materialize an orphaned stream's partial into a persisted assistant message. */\n persistOrphanedStream(streamId: string): Promise<void>;\n /**\n * Whether the connection that owns the active continuation stream is still\n * connected. Optional: when omitted the handshake assumes it is (legacy\n * behavior). Hosts wire their live connection registry so a continuation\n * stream whose owner vanished on an abrupt (1006) reconnect can still be\n * resumed by the replacement connection (#1784).\n */\n isConnectionPresent?(connectionId: string): boolean;\n}\n\n/**\n * Drives the server side of the stream-resume protocol over a\n * {@link ResumeHandshakeHost}. Construct once per agent (the host wires its\n * `ResumableStream` / `ContinuationState` / pending set in) and call the three\n * public methods from the host's existing onConnect / onMessage wiring, so\n * handler registration timing stays host-owned.\n */\nexport class ResumeHandshake {\n constructor(private readonly host: ResumeHandshakeHost) {}\n\n /**\n * Notify a connection that an active stream can be resumed; it should reply\n * with `STREAM_RESUME_ACK` to receive the replay.\n *\n * A connection can legitimately be notified more than once for the same\n * request — proactively from onConnect AND in response to its explicit\n * `STREAM_RESUME_REQUEST` (#1733). This is intentional and must NOT be deduped\n * here: an explicit request always deserves a response (else the client's\n * `reconnectToStream` hangs to its timeout with no replay), and the proactive\n * notify is required for clients that never send a request. The notify is one\n * tiny frame; the client dedupes its ACK so the buffer is not replayed twice.\n */\n notifyStreamResuming(connection: Connection): void {\n const { resumableStream, pendingResumeConnections } = this.host;\n if (!resumableStream.hasActiveStream()) return;\n const sent = sendIfOpen(\n connection,\n JSON.stringify({\n type: CHAT_MESSAGE_TYPES.STREAM_RESUMING,\n id: resumableStream.activeRequestId\n })\n );\n if (sent) {\n // Add to pending set — excluded from live broadcasts until they ACK to\n // receive the full stream replay.\n pendingResumeConnections.add(connection.id);\n }\n }\n\n /**\n * Handle a client `STREAM_RESUME_REQUEST`. The client sends this after its\n * message handler is registered, avoiding the race where a proactive\n * `STREAM_RESUMING` from onConnect arrives before the handler is ready.\n */\n async handleResumeRequest(connection: Connection): Promise<void> {\n const { resumableStream, continuation, preStream } = this.host;\n if (resumableStream.hasActiveStream()) {\n if (\n continuation.activeRequestId === resumableStream.activeRequestId &&\n continuation.activeConnectionId !== null &&\n continuation.activeConnectionId !== connection.id &&\n this._ownerStillPresent(continuation.activeConnectionId)\n ) {\n sendIfOpen(\n connection,\n JSON.stringify({ type: CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE })\n );\n } else {\n this.notifyStreamResuming(connection);\n }\n } else if (\n continuation.pending !== null &&\n (continuation.pending.connectionId === null ||\n continuation.pending.connectionId === connection.id)\n ) {\n // A continuation turn is accepted but its stream has not started yet.\n // Park the connection AND tell it to keep waiting (#1784) so its resume\n // probe does not time out before the continuation stream begins; the host\n // flushes `awaitingConnections` into `STREAM_RESUMING` on stream start.\n continuation.awaitingConnections.set(connection.id, connection);\n this._sendStreamPending(connection, continuation.pending.requestId);\n } else if (await this._replayTerminalOnResume(connection)) {\n // A turn terminalized while no client was connected (#1645): drive the\n // resume handshake so the terminal error frame can be delivered on the\n // resumed stream (the only path that surfaces as an error on the client)\n // once this connection ACKs — see `_replayTerminalOnAck`.\n } else if (preStream?.park(connection)) {\n // A normal turn is accepted but its resumable stream has not started yet\n // (#1784). `park` enrolled the connection and sent `STREAM_PENDING`; the\n // host flushes it into `STREAM_RESUMING` on stream start, or releases it\n // with `STREAM_RESUME_NONE` if the turn settles without streaming.\n } else {\n sendIfOpen(\n connection,\n JSON.stringify({ type: CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE })\n );\n }\n }\n\n /** Send a keep-waiting `STREAM_PENDING` frame (#1784). */\n private _sendStreamPending(connection: Connection, requestId?: string): void {\n sendIfOpen(\n connection,\n JSON.stringify({\n type: CHAT_MESSAGE_TYPES.STREAM_PENDING,\n ...(requestId ? { id: requestId } : {})\n })\n );\n }\n\n /** Whether the active continuation's owner connection is still present. */\n private _ownerStillPresent(connectionId: string): boolean {\n return this.host.isConnectionPresent\n ? this.host.isConnectionPresent(connectionId)\n : true;\n }\n\n /** Handle a client `STREAM_RESUME_ACK` for `requestId`. */\n async handleResumeAck(\n connection: Connection,\n requestId: string\n ): Promise<void> {\n const { resumableStream, pendingResumeConnections, responseMessageType } =\n this.host;\n pendingResumeConnections.delete(connection.id);\n\n if (\n resumableStream.hasActiveStream() &&\n resumableStream.activeRequestId === requestId\n ) {\n const orphanedStreamId = resumableStream.replayChunks(\n connection,\n resumableStream.activeRequestId\n );\n // If the stream was orphaned (restored from SQLite after hibernation with\n // no live reader), reconstruct the partial assistant message from stored\n // chunks and persist it so it survives further page refreshes.\n if (orphanedStreamId) {\n await this.host.persistOrphanedStream(orphanedStreamId);\n }\n } else if (resumableStream.hasActiveStream()) {\n // Ignore ACKs for a different active stream request id.\n } else if (await this._replayTerminalOnAck(connection, requestId)) {\n // Delivered the pending terminal error frame on the resumed stream the\n // client just ACKed (#1645).\n } else if (\n !resumableStream.replayCompletedChunksByRequestId(connection, requestId)\n ) {\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: responseMessageType,\n replay: true\n })\n );\n }\n }\n\n /**\n * Replay a pending terminal outcome (#1645) over the resume handshake so a\n * reconnecting client surfaces it exactly like a live exhaustion. The bare\n * terminal frame is dropped by the client unless it arrives on a resumed\n * stream — the only path that reaches the transport's stream reader and\n * becomes `useChat.error` — so we drive `STREAM_RESUMING` here and deliver the\n * error frame once the client ACKs (see {@link _replayTerminalOnAck}). Returns\n * `true` if a terminal was pending (and `STREAM_RESUMING` was sent).\n */\n private async _replayTerminalOnResume(\n connection: Connection\n ): Promise<boolean> {\n const pending = await this.host.pendingChatTerminal();\n if (!pending) return false;\n sendIfOpen(\n connection,\n JSON.stringify({\n type: CHAT_MESSAGE_TYPES.STREAM_RESUMING,\n id: pending.requestId\n })\n );\n return true;\n }\n\n /**\n * Deliver the pending terminal error frame on the resumed stream the client\n * ACKed (#1645). The record is retained (not cleared) so concurrent reconnects\n * (e.g. multiple tabs) each learn the outcome; it is cleared when a later turn\n * supersedes it.\n */\n private async _replayTerminalOnAck(\n connection: Connection,\n requestId: string\n ): Promise<boolean> {\n const { resumableStream, responseMessageType } = this.host;\n const pending = await this.host.pendingChatTerminal();\n if (!pending || pending.requestId !== requestId) return false;\n // Replay any partial content the errored stream produced before the error,\n // so the reconnecting client observes the same sequence a live client did —\n // content chunks, then the terminal error (#1575). If the connection drops\n // mid-replay, skip the terminal frame; the record is retained, so the next\n // reconnect retries the whole sequence.\n if (\n !resumableStream.replayErroredChunksByRequestId(\n connection,\n pending.requestId\n )\n ) {\n return true;\n }\n sendIfOpen(\n connection,\n JSON.stringify({\n body: pending.body,\n done: true,\n error: true,\n id: pending.requestId,\n type: responseMessageType\n })\n );\n return true;\n }\n}\n","/**\n * Shared chat-recovery incident math.\n *\n * `@internal` — sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API. See\n * `design/rfc-chat-recovery-foundation.md`.\n *\n * `@cloudflare/ai-chat` and `@cloudflare/think` previously hand-maintained a\n * byte-identical incident-budget decision (`_beginChatRecoveryIncident`) apart\n * from a package log prefix, the pending-interaction predicate name, and Think's\n * extra client-tool rehydration guard. This module is now the single source of\n * that decision — one pure function both packages call, with one test surface.\n *\n * Pure here means: no Durable Object storage, no global clock, no broadcasts.\n * The caller still owns storage I/O (reading the existing incident, sweeping\n * stale incidents, persisting the result), the progress counter, the\n * pending-interaction predicate, and event emission. This function receives the\n * already-resolved inputs and returns the next incident, whether it is\n * exhausted, and the observability events to emit — keeping the budget logic\n * unit-testable against a deterministic clock with no Workers runtime.\n *\n * The serialized `ChatRecoveryIncident` shape, the storage keys, and the\n * incident-id formula are all part of the persisted cutover contract (see the\n * RFC's \"Cutover invariants\" table) and MUST NOT change without a migration.\n */\n\nimport type {\n ChatRecoveryConfig,\n ChatRecoveryProgressContext,\n ResolvedChatRecoveryConfig\n} from \"./lifecycle\";\n\n/**\n * Whether a recovery is retrying an unanswered user turn or continuing a\n * partial assistant turn. Intentionally NOT part of the incident identity (see\n * {@link chatRecoveryIncidentId}).\n */\nexport type ChatRecoveryKind = \"retry\" | \"continue\";\n\n/**\n * Durable per-incident recovery record.\n *\n * PERSISTED CONTRACT — this shape round-trips across deploys (including the\n * deploy that ships the shared engine, which is itself a deploy-mid-recovery).\n * Fields are added as optional so older persisted incidents keep recovering.\n */\nexport type ChatRecoveryIncident = {\n incidentId: string;\n requestId: string;\n /** Stable request ID for the whole continuation chain (the recovery root). */\n recoveryRootRequestId?: string;\n recoveryKind: ChatRecoveryKind;\n attempt: number;\n maxAttempts: number;\n status:\n | \"detected\"\n | \"scheduled\"\n | \"attempting\"\n | \"completed\"\n | \"skipped\"\n | \"exhausted\"\n | \"failed\";\n firstSeenAt: number;\n lastAttemptAt: number;\n /**\n * Epoch ms of the last attempt that observed forward progress. The recovery\n * budget is keyed to this (`now - lastProgressAt > noProgressTimeoutMs`), so a\n * turn that keeps producing content survives churn indefinitely while a\n * genuinely stuck turn is sealed within the window (#1637). Optional for\n * backward-compat — falls back to `firstSeenAt`.\n */\n lastProgressAt?: number;\n reason?: string;\n /**\n * High-water mark of the durable, monotonic recovery-progress counter\n * observed for this incident. Distinguishes a turn making forward progress\n * but repeatedly interrupted by isolate resets (deploys) — which must NOT\n * exhaust the budget — from one that genuinely fails to advance. Sourced from\n * a persisted counter, never the compactable transcript (#1628).\n */\n progress?: number;\n /**\n * Value of the durable progress counter when this incident opened. The\n * runaway-loop work budget is `progress - workBaseline`, compared against\n * `maxRecoveryWork`. Optional for backward-compat — a missing baseline is\n * treated as the current marker (zero work so far), so an in-flight incident\n * from an older build is never falsely sealed.\n */\n workBaseline?: number;\n /**\n * Count of recovery attempts for this incident that ended in a Durable Object\n * memory-limit reset (the isolate exceeded its 128 MB limit — see\n * `isDurableObjectMemoryLimitReset`). An OOM is a poison signal: re-running the\n * same memory-heavy turn deterministically re-OOMs, so unlike a deploy/eviction\n * it must NOT credit forward progress and must be bounded by a tight,\n * OOM-specific budget (`maxOomRetries`) rather than the generic attempt cap\n * (which resets on progress). Bumped by `ChatRecoveryEngine.recordOomAndDecide`\n * when a recovery callback observes an OOM; exceeding `maxOomRetries` seals the\n * incident with `reason=\"out_of_memory\"` (#1825). Optional for backward-compat.\n */\n oomAttempts?: number;\n};\n\n// ── Persisted storage keys (cutover contract) ──────────────────────────────\n\nexport const CHAT_RECOVERY_INCIDENT_KEY_PREFIX = \"cf:chat-recovery:incident:\";\n/**\n * Durable, monotonic forward-progress counter for recovery budget resets.\n * Bumped at production time when new content is streamed, so it reflects\n * genuinely new content and is immune to reconnects/re-persists; never\n * recomputed from the (compactable) transcript.\n */\nexport const CHAT_RECOVERY_PROGRESS_KEY = \"cf:chat-recovery:progress\";\n/**\n * Durable record of an in-progress recovery so a \"recovering…\" status (#1620)\n * can be broadcast live and survive the set/clear happening in different\n * isolates (a continuation runs in a later alarm invocation).\n */\nexport const CHAT_RECOVERING_KEY = \"cf:chat:recovering\";\n/**\n * Durable record of the last turn that ended in a terminal error / abandoned\n * recovery (#1645). Replayed on the next reconnect via the resume handshake;\n * cleared when a later turn supersedes it.\n */\nexport const CHAT_LAST_TERMINAL_KEY = \"cf:chat:last-terminal\";\n\n// ── Budget defaults and tuning constants ───────────────────────────────────\n\n/**\n * Secondary backstop only. The primary recovery bound is the no-progress wall\n * clock; with alarm debounce this cap rarely binds (it catches a pathological\n * tight alarm-loop). Kept high so the no-progress window seals first under\n * normal deploy cadence (#1637).\n */\nexport const DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS = 10;\n/**\n * Runaway-loop guard default — the framework-imposed backstop on cumulative\n * recovery WORK (produced content/tool units) since an incident opened.\n *\n * Originally `Infinity` (rfc-chat-recovery-work-budget): the SDK shipped the\n * *mechanism* but no default cap, so a progressing turn was never terminated on\n * its own. Production issue #1825 showed that this is a footgun: an isolate that\n * OOMs mid-stream still credits a little progress before it dies, which resets\n * BOTH progress-keyed bounds (the attempt cap and the no-progress window) on\n * every wake — and a fast crash loop (each attempt inside the alarm-debounce\n * window) pins the attempt counter too. With `maxRecoveryWork = Infinity` the\n * ONLY instrument whose meter still climbs across such a loop is disabled, so\n * recovery re-runs the turn (and its LLM calls) forever.\n *\n * A finite default closes that loop out of the box: work climbs regardless of\n * debounce/progress resets, so a content-emitting runaway is always sealed with\n * `reason=\"work_budget_exceeded\"`. The value is deliberately generous — it\n * bounds wasted re-run cost without clipping a normal interrupted turn (work\n * only accrues from the first interruption until the turn completes, after which\n * the incident is deleted). A very long agentic turn under heavy interruption\n * that legitimately needs more should raise `maxRecoveryWork` (or set it to\n * `Infinity` to restore the pre-#1825 unbounded behavior).\n */\nexport const DEFAULT_CHAT_RECOVERY_MAX_WORK = 1000;\n/**\n * Tight, OOM-specific retry budget (#1825). A Durable Object memory-limit reset\n * (`isDurableObjectMemoryLimitReset`) is usually deterministic — the turn's\n * working set no longer fits in the isolate's 128 MB — so re-running it re-OOMs.\n * But a single OOM CAN be a transient spike (the isolate's 128 MB is shared\n * across the global scope / noisy neighbors), so recovery retries a small number\n * of times before sealing with `reason=\"out_of_memory\"` rather than abandoning a\n * turn that one more attempt might have completed. Far tighter than the generic\n * `maxRecoveryWork` backstop because an OOM is attributable and re-running it is\n * expensive (it re-runs the model). Counts attempts that ended in an OOM, not\n * total attempts, so a turn interrupted by deploys (no OOM) is unaffected.\n */\nexport const DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES = 3;\nexport const DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS = 10_000;\n/**\n * Delay before retrying a recovery that timed out waiting for stable state.\n * Gives an actively-churning isolate (e.g. a deploy in flight) time to settle.\n */\nexport const CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS = 3;\nexport const DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE =\n \"The assistant was interrupted and could not recover. Please try again.\";\n/**\n * Incidents that have not seen a new attempt within this window are assumed\n * abandoned and swept so durable storage does not grow without bound.\n */\nexport const CHAT_RECOVERY_INCIDENT_TTL_MS = 60 * 60 * 1000;\n/** Max keys per Durable Object KV `delete([...])` call. */\nexport const KV_DELETE_MAX_KEYS = 128;\n/**\n * PRIMARY recovery bound (#1637): seal an incident that has made no forward\n * progress for this long. Keyed to `lastProgressAt`, which resets on every\n * progress-bearing attempt — so a turn that keeps producing content survives\n * deploy churn indefinitely, while a genuinely stuck turn dies within 5 min.\n */\nexport const DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS = 5 * 60 * 1000;\n/**\n * Alarm debounce: recovery alarms bunched within this window collapse into a\n * single attempt. A deploy rollout drops/reconnects the socket several times\n * over ~11–22s; without this, one logical deploy would burn several attempts.\n */\nexport const CHAT_RECOVERY_ALARM_DEBOUNCE_MS = 30 * 1000;\n/**\n * Staleness bound for the live \"recovering…\" flag (#1620). A flag older than\n * this is treated as abandoned so it can neither pin the indicator on forever\n * nor suppress a genuinely-new recovering signal. NOT a recovery budget.\n */\nexport const CHAT_RECOVERING_FLAG_TTL_MS = 15 * 60 * 1000;\n\n// ── Pure helpers ───────────────────────────────────────────────────────────\n\n/**\n * Resolve a raw `chatRecovery` config field into the fully-defaulted form the\n * engine reasons about. Identical defaulting in both packages today.\n */\nexport function resolveChatRecoveryConfig(\n raw: ChatRecoveryConfig | undefined\n): ResolvedChatRecoveryConfig {\n const custom = typeof raw === \"object\" && raw !== null ? raw : undefined;\n return {\n enabled: raw !== false,\n maxAttempts: Math.max(\n 1,\n Math.floor(custom?.maxAttempts ?? DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS)\n ),\n stableTimeoutMs: Math.max(\n 0,\n Math.floor(\n custom?.stableTimeoutMs ?? DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS\n )\n ),\n terminalMessage:\n custom?.terminalMessage ?? DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE,\n noProgressTimeoutMs: Math.max(\n 0,\n Math.floor(\n custom?.noProgressTimeoutMs ??\n DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS\n )\n ),\n maxRecoveryWork:\n typeof custom?.maxRecoveryWork === \"number\" && custom.maxRecoveryWork >= 0\n ? custom.maxRecoveryWork\n : DEFAULT_CHAT_RECOVERY_MAX_WORK,\n maxOomRetries:\n typeof custom?.maxOomRetries === \"number\" && custom.maxOomRetries >= 0\n ? Math.floor(custom.maxOomRetries)\n : DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES,\n ...(custom?.shouldKeepRecovering\n ? { shouldKeepRecovering: custom.shouldKeepRecovering }\n : {}),\n ...(custom?.onExhausted ? { onExhausted: custom.onExhausted } : {})\n };\n}\n\n/**\n * Stable identifier for a recovery incident.\n *\n * `recoveryKind` is intentionally NOT part of the identity: a single\n * interrupted turn can flip between \"retry\" (no chunks persisted) and\n * \"continue\" (partial chunks exist) across restarts, and the attempt budget\n * must be shared so recovery stays bounded by `maxAttempts`. This formula is a\n * cutover invariant.\n */\nexport function chatRecoveryIncidentId(input: {\n requestId: string;\n recoveryRootRequestId?: string | null;\n latestUserMessageId?: string | null;\n recoveryKind: ChatRecoveryKind;\n}): string {\n return [\n input.recoveryRootRequestId ?? input.requestId,\n input.latestUserMessageId ?? \"\"\n ].join(\":\");\n}\n\n/** Durable storage key for an incident record. */\nexport function chatRecoveryIncidentKey(incidentId: string): string {\n return `${CHAT_RECOVERY_INCIDENT_KEY_PREFIX}${encodeURIComponent(incidentId)}`;\n}\n\n/**\n * Select incident keys that have been inactive past the TTL. Pure over a map of\n * stored incidents; the caller performs the batched delete.\n */\nexport function selectStaleIncidentKeys(\n entries: Map<string, ChatRecoveryIncident | undefined>,\n now: number\n): string[] {\n const staleKeys: string[] = [];\n for (const [key, incident] of entries) {\n const lastActive = incident?.lastAttemptAt ?? incident?.firstSeenAt ?? 0;\n if (now - lastActive > CHAT_RECOVERY_INCIDENT_TTL_MS) {\n staleKeys.push(key);\n }\n }\n return staleKeys;\n}\n\n/**\n * Sweep recovery incidents inactive past the TTL from durable storage. Lists by\n * the incident key prefix, selects stale keys (`selectStaleIncidentKeys`), and\n * batch-deletes them — the DO KV `delete([...])` accepts up to\n * `KV_DELETE_MAX_KEYS` per call, collapsing N awaited round-trips into\n * ceil(N / 128). Shared by `AIChatAgent` and `Think` so the sweep policy lives in\n * one place. See `design/rfc-chat-recovery-foundation.md`.\n */\nexport async function sweepStaleChatRecoveryIncidents(\n storage: Pick<DurableObjectStorage, \"list\" | \"delete\">,\n now: number\n): Promise<void> {\n const entries = await storage.list<ChatRecoveryIncident>({\n prefix: CHAT_RECOVERY_INCIDENT_KEY_PREFIX\n });\n const staleKeys = selectStaleIncidentKeys(entries, now);\n for (let i = 0; i < staleKeys.length; i += KV_DELETE_MAX_KEYS) {\n await storage.delete(staleKeys.slice(i, i + KV_DELETE_MAX_KEYS));\n }\n}\n\n/**\n * List the persisted recovery incidents that are still live (status\n * `detected` / `scheduled` / `attempting`) — i.e. NOT yet terminalized\n * (`exhausted` / `failed`). Used by the alarm-boundary OOM circuit breaker\n * (#1825) to find the incident(s) it must seal when the in-DO budgets could not.\n * Lists by the incident key prefix so the storage layout stays encapsulated.\n */\nexport async function listActiveChatRecoveryIncidents(\n storage: Pick<DurableObjectStorage, \"list\">\n): Promise<{ key: string; incident: ChatRecoveryIncident }[]> {\n const entries = await storage.list<ChatRecoveryIncident>({\n prefix: CHAT_RECOVERY_INCIDENT_KEY_PREFIX\n });\n const active: { key: string; incident: ChatRecoveryIncident }[] = [];\n for (const [key, incident] of entries) {\n if (\n incident &&\n (incident.status === \"detected\" ||\n incident.status === \"scheduled\" ||\n incident.status === \"attempting\")\n ) {\n active.push({ key, incident });\n }\n }\n return active;\n}\n\n/**\n * Summarize a child agent's persisted recovery incidents for the parent's\n * agent-tool reattach decision: `\"in-progress\"` if any incident is still live\n * (detected/scheduled/attempting), else `\"failed\"` if any terminalized\n * (exhausted/failed), else `\"none\"`. In-progress takes precedence so a parent\n * never gives up on a child that is still recovering. Shared by `AIChatAgent`\n * and `Think`. See `design/rfc-chat-recovery-foundation.md`.\n */\nexport async function classifyAgentToolChildRecovery(\n storage: Pick<DurableObjectStorage, \"list\">\n): Promise<\"in-progress\" | \"failed\" | \"none\"> {\n const entries = await storage.list<ChatRecoveryIncident>({\n prefix: CHAT_RECOVERY_INCIDENT_KEY_PREFIX\n });\n let failed = false;\n for (const incident of entries.values()) {\n if (\n incident.status === \"detected\" ||\n incident.status === \"scheduled\" ||\n incident.status === \"attempting\"\n ) {\n return \"in-progress\";\n }\n if (incident.status === \"exhausted\" || incident.status === \"failed\") {\n failed = true;\n }\n }\n return failed ? \"failed\" : \"none\";\n}\n\n/**\n * Read the durable monotonic recovery-progress counter (0 when unset). The value\n * feeds the no-progress budget decision; shared by `AIChatAgent` and `Think`.\n */\nexport async function readChatRecoveryProgress(\n storage: Pick<DurableObjectStorage, \"get\">\n): Promise<number> {\n return (await storage.get<number>(CHAT_RECOVERY_PROGRESS_KEY)) ?? 0;\n}\n\n/**\n * Advance the durable recovery-progress counter by one. Called when genuinely new\n * content is durably flushed (real, reconnect-immune forward progress); shared by\n * `AIChatAgent` and `Think`.\n */\nexport async function bumpChatRecoveryProgress(\n storage: Pick<DurableObjectStorage, \"get\" | \"put\">\n): Promise<void> {\n const current = (await storage.get<number>(CHAT_RECOVERY_PROGRESS_KEY)) ?? 0;\n await storage.put(CHAT_RECOVERY_PROGRESS_KEY, current + 1);\n}\n\n/**\n * Throttle window for crediting a parent turn's recovery progress from forwarded\n * sub-agent (agent-tool) stream chunks (N9). Forwarding a child's chunks IS\n * forward progress for the parent, but the credit must not write storage per\n * token.\n */\nexport const AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS = 5_000;\n\n/**\n * Per-isolate throttle gate for agent-tool stream-progress crediting (N9). The\n * `_lastBumpAt` clock is in-memory, so it resets per isolate and the first\n * forwarded chunk after a restart always credits. `shouldCredit(now)` returns\n * `true` at most once per `AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS` window and\n * records the time on each credit. Shared by `AIChatAgent` and `Think`.\n */\nexport class AgentToolStreamProgressThrottle {\n private _lastBumpAt = 0;\n shouldCredit(now: number): boolean {\n if (now - this._lastBumpAt < AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS) {\n return false;\n }\n this._lastBumpAt = now;\n return true;\n }\n}\n\n/**\n * Throttle window for crediting recovery progress from mid-segment streaming\n * content (text/reasoning/tool-input deltas). A milestone chunk credits\n * unconditionally; deltas credit at most once per window so a long single\n * segment registers forward progress across crashes without writing storage per\n * token. 5s is far finer than the 300s no-progress budget, so any crash gap\n * longer than this window over an actively-streaming segment still credits.\n */\nexport const CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS = 5_000;\n\n/**\n * Per-isolate throttle gate for crediting recovery progress from mid-segment\n * streaming-content chunks — the delta arm of {@link shouldCreditStreamProgress}.\n * The `_lastBumpAt` clock is in-memory, so it resets per isolate and the first\n * delta after a restart always credits. Shared by `AIChatAgent` and `Think`.\n */\nexport class StreamProgressCreditThrottle {\n private _lastBumpAt = 0;\n shouldCredit(now: number): boolean {\n if (now - this._lastBumpAt < CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS) {\n return false;\n }\n this._lastBumpAt = now;\n return true;\n }\n}\n\n// ── Terminal + recovering status storage glue ──────────────────────────────\n//\n// Durable records for the terminal-error / \"recovering…\" reconnect UX. The\n// keys (`CHAT_LAST_TERMINAL_KEY`, `CHAT_RECOVERING_KEY`) and the flag TTL are\n// cutover-contract constants above; these helpers were byte-identical in both\n// packages apart from the recovering-broadcast wire-type enum and broadcast\n// wrapper, which are passed in. Shared by `AIChatAgent` and `Think`.\n\n/** Durable record of the last turn that ended in a terminal error (#1645). */\nexport type ChatTerminalRecord = { requestId: string; body: string };\n\n/**\n * Persist a durable record of the last terminal turn so a client that\n * (re)connects after the turn ended still learns its outcome (#1645). Kept\n * until a later turn supersedes it ({@link clearChatTerminal}); a single record\n * is sufficient because only the most recent terminal is relevant.\n */\nexport async function recordChatTerminal(\n storage: Pick<DurableObjectStorage, \"put\">,\n requestId: string,\n body: string\n): Promise<void> {\n await storage.put(CHAT_LAST_TERMINAL_KEY, { requestId, body });\n}\n\n/** Clear the durable terminal record once a later turn supersedes it (#1645). */\nexport async function clearChatTerminal(\n storage: Pick<DurableObjectStorage, \"delete\">\n): Promise<void> {\n await storage.delete(CHAT_LAST_TERMINAL_KEY);\n}\n\n/** Read the pending terminal record, or `null` if none is stored (#1645). */\nexport async function pendingChatTerminal(\n storage: Pick<DurableObjectStorage, \"get\">\n): Promise<ChatTerminalRecord | null> {\n return (\n (await storage.get<ChatTerminalRecord>(CHAT_LAST_TERMINAL_KEY)) ?? null\n );\n}\n\n/** Durable record shape for the live \"recovering…\" flag (#1620). */\ntype RecoveringRecord = { requestId?: string; at?: number };\n\n/**\n * Build the on-connect \"recovering…\" replay frame (#1620), or `null` when no\n * (non-stale) recovery is in progress. A client that connects between recovery\n * attempts (no active stream) reads the turn as working rather than frozen. A\n * record older than the flag TTL is treated as abandoned (its terminal-clear\n * never ran) and skipped, so a dead recovery can't show \"recovering…\" forever.\n * `messageType` is the package's recovering wire-type enum.\n */\nexport async function buildChatRecoveringFrame(\n storage: Pick<DurableObjectStorage, \"get\">,\n messageType: string,\n now: number\n): Promise<Record<string, unknown> | null> {\n const recovering = await storage.get<RecoveringRecord>(CHAT_RECOVERING_KEY);\n if (\n !recovering ||\n now - (recovering.at ?? 0) >= CHAT_RECOVERING_FLAG_TTL_MS\n ) {\n return null;\n }\n return {\n type: messageType,\n recovering: true,\n ...(recovering.requestId ? { id: recovering.requestId } : {})\n };\n}\n\n/**\n * Set or clear the live \"recovering…\" status (#1620). Persists a durable record\n * (so set/clear stay consistent across the isolates a recovery spans) and\n * broadcasts a recovering frame — but only on a genuine transition, so a\n * deploy/reconnect storm (which re-detects recovery many times) doesn't spam\n * the wire. A flag older than the TTL is stale: the owning incident was\n * abandoned without a terminal (e.g. the DO went idle before recovery could\n * resolve), so it is treated as not-recovering and can neither pin the\n * indicator on forever nor suppress a genuinely-new recovering signal.\n * `messageType` is the package's recovering wire-type enum; `broadcast` is the\n * package's chat-broadcast wrapper.\n */\nexport async function setChatRecovering(\n active: boolean,\n requestId: string | undefined,\n deps: {\n storage: Pick<DurableObjectStorage, \"get\" | \"put\" | \"delete\">;\n messageType: string;\n broadcast: (frame: Record<string, unknown>) => void;\n now: number;\n }\n): Promise<void> {\n const { storage, messageType, broadcast, now } = deps;\n const existing = await storage.get<RecoveringRecord>(CHAT_RECOVERING_KEY);\n const activeExisting =\n existing && now - (existing.at ?? 0) < CHAT_RECOVERING_FLAG_TTL_MS;\n if (active) {\n if (activeExisting) return; // already recovering — idempotent, no re-broadcast\n await storage.put(CHAT_RECOVERING_KEY, {\n ...(requestId ? { requestId } : {}),\n at: now\n });\n } else {\n if (!existing) return; // not recovering — nothing to clear\n await storage.delete(CHAT_RECOVERING_KEY);\n requestId = requestId ?? existing.requestId;\n }\n broadcast({\n type: messageType,\n recovering: active,\n ...(requestId ? { id: requestId } : {})\n });\n}\n\n// ── Incident budget evaluation ─────────────────────────────────────────────\n\n/**\n * Observability event produced by an incident evaluation or a status\n * transition, emitted by the caller. The `detected`/`attempt` events come from\n * the budget evaluation (begin path); the `scheduled` event comes from\n * `ChatRecoveryEngine.scheduleRecovery`; the `completed`/`skipped`/`failed`\n * events come from `ChatRecoveryEngine.updateIncident`. `reason` is carried only\n * by the `skipped`/`failed` transitions that record a cause.\n */\nexport type ChatRecoveryIncidentEvent = {\n type:\n | \"chat:recovery:detected\"\n | \"chat:recovery:attempt\"\n | \"chat:recovery:scheduled\"\n | \"chat:recovery:completed\"\n | \"chat:recovery:skipped\"\n | \"chat:recovery:failed\";\n incidentId: string;\n requestId: string;\n attempt: number;\n maxAttempts: number;\n recoveryKind: ChatRecoveryKind;\n reason?: string;\n};\n\nexport type EvaluateChatRecoveryIncidentInput = {\n /** Recovery identity for this turn. */\n identity: {\n requestId: string;\n recoveryRootRequestId?: string | null;\n latestUserMessageId?: string | null;\n recoveryKind: ChatRecoveryKind;\n };\n /** Fully-resolved recovery config. */\n config: ResolvedChatRecoveryConfig;\n /** The existing incident for this identity, or `null` if this is fresh. */\n existing: ChatRecoveryIncident | null;\n /** Current value of the durable monotonic progress counter. */\n currentProgress: number;\n /**\n * Whether the turn is parked on a pending CLIENT interaction (an\n * `input-available` client-tool part or an `approval-requested` part). Such a\n * turn is waiting on the human, not stuck, so it is budget-free.\n */\n awaitingClientInteraction: boolean;\n /** Injected clock (epoch ms) for deterministic tests. */\n now: number;\n /**\n * Invoked when `config.shouldKeepRecovering` throws. Lets each package keep\n * its own log prefix. A throwing predicate is treated as \"keep recovering\".\n */\n onShouldKeepRecoveringError?: (error: unknown) => void;\n};\n\nexport type EvaluateChatRecoveryIncidentResult = {\n /** The next incident record to persist. */\n incident: ChatRecoveryIncident;\n /** Whether this incident is now sealed as exhausted. */\n exhausted: boolean;\n /** Observability events to emit, in order. */\n events: ChatRecoveryIncidentEvent[];\n};\n\n/**\n * Compute the next recovery incident and budget decision.\n *\n * This is the durable recovery budget — a faithful extraction of\n * `_beginChatRecoveryIncident` from both `AIChatAgent` and `Think`. The\n * instruments are decoupled by what they catch:\n *\n * - STUCK — no-progress window: `lastProgressAt` resets on every\n * progress-bearing attempt, so a turn that keeps producing content survives\n * churn indefinitely; a stuck turn is sealed after `noProgressTimeoutMs`.\n * - DEBOUNCE — alarms bunched within `CHAT_RECOVERY_ALARM_DEBOUNCE_MS` collapse\n * into one attempt, so a single rollout's reconnect storm isn't N attempts.\n * - ALARM-LOOP — the attempt cap (resets on progress) catches a tight\n * no-progress alarm loop.\n * - RUNAWAY — the work budget seals a loop that keeps emitting content but\n * never converges. Keyed to WORK done, not wall-clock. Defaults to no cap.\n * - CALLER — `shouldKeepRecovering` lets the integrator express a\n * token/cost/step budget the SDK should not hardcode. Consulted only when no\n * hard bound has already sealed the incident, and never on first detection.\n *\n * A turn parked on a pending client interaction is budget-free: every bound is\n * suppressed and the no-progress clock kept fresh.\n */\nexport async function evaluateChatRecoveryIncident(\n input: EvaluateChatRecoveryIncidentInput\n): Promise<EvaluateChatRecoveryIncidentResult> {\n const {\n identity,\n config,\n existing,\n currentProgress,\n awaitingClientInteraction,\n now\n } = input;\n\n const incidentId = chatRecoveryIncidentId(identity);\n const recoveryRootRequestId =\n identity.recoveryRootRequestId ?? identity.requestId;\n\n // Forward-progress detection. A mid-turn deploy resets the Durable Object;\n // the interrupted continuation is re-detected on the next wake. A turn that\n // followed real progress (more durably-produced content than the last attempt\n // saw) is environmental churn, not a poison turn.\n const prevProgress = existing?.progress ?? 0;\n const madeProgress = existing != null && currentProgress > prevProgress;\n\n // While a client interaction is pending the turn is budget-free, and the\n // no-progress clock is kept fresh so the turn has a full window once the human\n // finally answers.\n const lastProgressAt =\n madeProgress || awaitingClientInteraction\n ? now\n : (existing?.lastProgressAt ?? existing?.firstSeenAt ?? now);\n const noProgressExceeded =\n existing != null &&\n !awaitingClientInteraction &&\n now - lastProgressAt > config.noProgressTimeoutMs;\n\n // Reuse the durable progress counter as a work meter. Baseline is captured\n // when the incident opens; `work` is what the turn produced since.\n const workBaseline = existing?.workBaseline ?? currentProgress;\n const progress = Math.max(prevProgress, currentProgress);\n const work = progress - workBaseline;\n const workBudgetExceeded =\n existing != null &&\n Number.isFinite(config.maxRecoveryWork) &&\n work > config.maxRecoveryWork;\n\n // OOM budget (#1825). `oomAttempts` is bumped out-of-band by\n // `recordOomAndDecide` when a recovery callback observes a memory-limit reset;\n // it is carried forward across begins here so the count is not lost, and seals\n // the incident once it crosses the tight OOM-specific budget. Unlike the\n // attempt cap this never resets on progress — an OOM that streams a little\n // before it dies must still count, since that \"progress\" is the very re-run\n // that re-OOMs. The seal authority is shared with `recordOomAndDecide` (which\n // seals on the catchable path); this begin-path check is the backstop for a\n // fiber re-detection that reopens the incident after the count crossed.\n const oomAttempts = existing?.oomAttempts ?? 0;\n const oomBudgetExceeded =\n existing != null &&\n !awaitingClientInteraction &&\n oomAttempts > config.maxOomRetries;\n\n const debounced =\n existing != null &&\n !madeProgress &&\n now - existing.lastAttemptAt < CHAT_RECOVERY_ALARM_DEBOUNCE_MS;\n\n const attempt = madeProgress\n ? 1\n : debounced\n ? (existing?.attempt ?? 1)\n : (existing?.attempt ?? 0) + 1;\n\n // Consult the caller predicate only when no hard bound has already sealed the\n // incident — a buggy/expensive hook must not run after we've decided, and a\n // throwing hook must not wedge the turn (log and treat as \"continue\"). Never\n // called on first detection (existing == null).\n let abortedByCaller = false;\n if (\n existing != null &&\n !awaitingClientInteraction &&\n config.shouldKeepRecovering &&\n !noProgressExceeded &&\n !workBudgetExceeded &&\n !oomBudgetExceeded &&\n attempt <= config.maxAttempts\n ) {\n try {\n const ctx: ChatRecoveryProgressContext = {\n incidentId,\n requestId: identity.requestId,\n recoveryRootRequestId,\n attempt,\n maxAttempts: config.maxAttempts,\n recoveryKind: identity.recoveryKind,\n work,\n ageMs: now - (existing.firstSeenAt ?? now)\n };\n const decision = await config.shouldKeepRecovering(ctx);\n abortedByCaller = decision === false;\n } catch (error) {\n input.onShouldKeepRecoveringError?.(error);\n }\n }\n\n const exhausted =\n !awaitingClientInteraction &&\n (oomBudgetExceeded ||\n noProgressExceeded ||\n workBudgetExceeded ||\n abortedByCaller ||\n attempt > config.maxAttempts);\n\n const incident: ChatRecoveryIncident = {\n incidentId,\n requestId: identity.requestId,\n recoveryRootRequestId,\n recoveryKind: identity.recoveryKind,\n attempt,\n maxAttempts: config.maxAttempts,\n status: exhausted ? \"exhausted\" : \"attempting\",\n firstSeenAt: existing?.firstSeenAt ?? now,\n lastAttemptAt: now,\n lastProgressAt,\n progress,\n workBaseline,\n // Carry the OOM count forward so a begin-path re-evaluation never loses what\n // `recordOomAndDecide` accrued between begins.\n ...(oomAttempts > 0 ? { oomAttempts } : {}),\n ...(exhausted\n ? {\n reason: oomBudgetExceeded\n ? \"out_of_memory\"\n : workBudgetExceeded\n ? \"work_budget_exceeded\"\n : noProgressExceeded\n ? \"no_progress_timeout\"\n : abortedByCaller\n ? \"recovery_aborted\"\n : \"max_attempts_exceeded\"\n }\n : {})\n };\n\n const events: ChatRecoveryIncidentEvent[] = [];\n if (!existing) {\n events.push({\n type: \"chat:recovery:detected\",\n incidentId,\n requestId: identity.requestId,\n attempt,\n maxAttempts: config.maxAttempts,\n recoveryKind: identity.recoveryKind\n });\n }\n events.push({\n type: \"chat:recovery:attempt\",\n incidentId,\n requestId: identity.requestId,\n attempt,\n maxAttempts: config.maxAttempts,\n recoveryKind: identity.recoveryKind\n });\n\n return { incident, exhausted, events };\n}\n","/**\n * Shared chat-recovery engine (sibling-package support for `@cloudflare/ai-chat`\n * and `@cloudflare/think`). Owns the recovery orchestration both packages must\n * perform identically — scheduling policy and incident-begin sequencing — behind\n * a thin {@link ChatRecoveryAdapter} seam so the package-specific host I/O\n * (storage, clock, events, interaction predicate) stays in the package. See\n * `design/rfc-chat-recovery-foundation.md`.\n *\n * @internal Not a public API.\n */\n\nimport type { FiberRecoveryContext } from \"../index\";\nimport type {\n ChatRecoveryExhaustedContext,\n ChatRecoveryOptions,\n ResolvedChatRecoveryConfig\n} from \"./lifecycle\";\nimport type { ChatFiberSnapshot } from \"./recovery\";\nimport {\n CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS,\n chatRecoveryIncidentId,\n chatRecoveryIncidentKey,\n evaluateChatRecoveryIncident\n} from \"./recovery-incident\";\nimport type {\n ChatRecoveryIncident,\n ChatRecoveryIncidentEvent,\n ChatRecoveryKind\n} from \"./recovery-incident\";\n\n/** The scheduled-callback entrypoints a recovery schedule can target. */\nexport type ChatRecoveryScheduleCallback =\n | \"_chatRecoveryContinue\"\n | \"_chatRecoveryRetry\";\n\n/**\n * Why a recovery callback is being scheduled. The idempotency of the underlying\n * `schedule()` call depends ONLY on this:\n *\n * - `\"initial\"` — the first schedule of a continuation/retry when an interrupted\n * turn is detected on wake. A deploy rollout drops/reconnects the socket\n * several times, re-triggering detection; idempotent scheduling (dedup on\n * callback + payload) collapses that storm into a single enqueued continuation\n * instead of N duplicates.\n *\n * - `\"stable_timeout_retry\"` — a reschedule issued from INSIDE the currently-\n * executing one-shot schedule row (a continuation that timed out waiting for\n * stable state). `alarm()` deletes that row only AFTER the callback returns,\n * so an idempotent reschedule would dedup onto the doomed row and be deleted\n * with it — the retry would never fire. A fresh (non-idempotent) delayed row\n * survives the deletion.\n */\nexport type ChatRecoveryScheduleReason = \"initial\" | \"stable_timeout_retry\";\n\n/**\n * A reconstructed orphaned-stream partial. The engine seam is deliberately\n * **wire-vocabulary-agnostic**: `text` is the accumulated assistant text and\n * `parts` is OPAQUE to the engine (`unknown[]`) — each host casts it back to its\n * own message-part vocabulary (AI SDK `UIMessage` parts, AG-UI tool parts, …).\n * The single fact the engine needs about parts — does the partial carry settled\n * (non-idempotent) tool work that must survive a `{ persist: false }` recovery\n * (#1631)? — is precomputed by the {@link ChatRecoveryCodec} as\n * `hasSettledToolResults`. So the engine never imports a part vocabulary; the\n * codec owns it (see `partialHasSettledToolResults` in `recovery-codec.ts` for\n * the AI SDK codec's implementation of that predicate).\n */\nexport type RecoveryPartial = {\n text: string;\n parts: unknown[];\n hasSettledToolResults: boolean;\n};\n\n/** Lifecycle status of a recovered stream's metadata row. */\nexport type ChatStreamStatus = \"streaming\" | \"completed\" | \"error\";\n\n/**\n * Resolve the `schedule()` idempotency option for a recovery schedule. Single\n * source of truth for both packages; see {@link ChatRecoveryScheduleReason} for\n * the rationale behind each case.\n *\n * This is a cutover invariant: flipping either case silently breaks deploy-storm\n * dedup (initial) or stalls stable-timeout retries (reschedule), and neither is\n * caught by a type error — only by the recovery suites.\n */\nexport function chatRecoverySchedulePolicy(\n reason: ChatRecoveryScheduleReason\n): { idempotent: boolean } {\n return { idempotent: reason === \"initial\" };\n}\n\n/** Identity + context for opening (or re-evaluating) a recovery incident. */\nexport interface BeginChatRecoveryIncidentInput {\n requestId: string;\n recoveryRootRequestId?: string | null;\n latestUserMessageId?: string | null;\n recoveryKind: ChatRecoveryKind;\n /** Test-only clock injection for deterministic debounce/window timing. */\n nowMs?: number;\n}\n\nexport interface BeginChatRecoveryIncidentResult {\n incident: ChatRecoveryIncident;\n config: ResolvedChatRecoveryConfig;\n exhausted: boolean;\n}\n\n/**\n * Package-specific host operations the engine drives during incident\n * orchestration. Every method is a thin pass-through to the package's existing\n * storage / clock / event / interaction primitives — the engine owns only the\n * *sequence*, not the I/O.\n */\nexport interface ChatRecoveryAdapter {\n /** Resolve the effective recovery config (defaults + caller overrides). */\n resolveConfig(): ResolvedChatRecoveryConfig;\n /** Wall clock; only consulted when the input carries no test `nowMs`. */\n now(): number;\n /** Evict incidents past the TTL. Runs before the existing-record read. */\n sweepStaleIncidents(now: number): Promise<void>;\n /** Read the persisted incident for `key`, or `null` if none. */\n getIncident(key: string): Promise<ChatRecoveryIncident | null>;\n /**\n * Optional: rehydrate any state the interaction predicate depends on. Invoked\n * after the existing-incident read and BEFORE `isAwaitingClientInteraction`.\n * `Think` uses this to restore client tools from durable storage on a cold\n * boot-recovery wake (so a HITL turn is not misread as stuck); `AIChatAgent`\n * has no such state and omits it.\n */\n ensureInteractionStateLoaded?(): void;\n /**\n * Optional: give the package a chance to handle a NON-chat fiber before chat\n * recovery inspects it. Returns `true` if the package fully consumed the\n * fiber, in which case the engine tells the caller to skip chat-recovery\n * processing for it. `Think` uses this for its messenger/workflow reply fibers\n * (`think:messenger-reply`); `AIChatAgent` has no non-chat fibers and omits it\n * (the engine then treats every recovered fiber as a chat-recovery candidate).\n *\n * Ordering invariant: the engine dispatches this FIRST, before the\n * chat-fiber-name gate, so a non-chat fiber is never misclassified as an\n * orphaned chat turn.\n */\n tryHandleNonChatFiberRecovery?(ctx: FiberRecoveryContext): Promise<boolean>;\n /** Monotonic forward-progress marker for the no-progress budget. */\n readProgress(): Promise<number>;\n /**\n * Whether the turn is parked on a pending CLIENT interaction (waiting on the\n * human, not stuck). When true the engine keeps the incident budget-free.\n * Optional: a host with no client-interaction/HITL substrate (e.g. the pi\n * fixture) omits it and the engine treats the turn as never parked (`false`).\n */\n isAwaitingClientInteraction?(): boolean;\n /** Persist the evaluated incident under `key`. */\n putIncident(key: string, incident: ChatRecoveryIncident): Promise<void>;\n /**\n * Delete the incident record under `key`. The engine calls this on the\n * terminal `completed` transition (a completed recovery is never retried, so\n * its record is dropped rather than left in storage forever).\n */\n deleteIncident(key: string): Promise<void>;\n /** Broadcast a lifecycle event produced by the evaluation or a transition. */\n emitRecoveryEvent(event: ChatRecoveryIncidentEvent): void;\n /**\n * Enqueue a recovery callback. A thin pass-through to the package's\n * `schedule(delaySeconds, callback, data, chatRecoverySchedulePolicy(reason))`\n * — the engine owns the surrounding orchestration (the transition + emit for\n * the initial schedule in {@link ChatRecoveryEngine.scheduleRecovery}, the\n * attempt bump for {@link ChatRecoveryEngine.rescheduleAfterStableTimeout});\n * the package owns the Durable Object alarm write and the payload shape.\n * `reason` selects the idempotency policy and `delaySeconds` the alarm delay\n * (`0` for the initial enqueue, `CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS` for\n * a stable-timeout reschedule).\n */\n scheduleRecovery(\n callback: ChatRecoveryScheduleCallback,\n data: Record<string, unknown>,\n reason: ChatRecoveryScheduleReason,\n delaySeconds: number\n ): Promise<void>;\n /**\n * Set or clear the live \"recovering…\" status (#1620). The engine calls this on\n * the incident transitions: `scheduled` → active (keyed by the recovery-root\n * request id, falling back to the incident's request id), and\n * `completed`/`skipped`/`failed` → cleared. The package owns the underlying\n * staleness / idempotency / broadcast I/O.\n */\n setRecovering(active: boolean, requestId?: string): Promise<void>;\n /**\n * Report a throw from the caller's `shouldKeepRecovering` hook. Optional: a\n * host that does not surface this diagnostic omits it (the engine swallows the\n * report).\n */\n onShouldKeepRecoveringError?(error: unknown): void;\n /**\n * Terminalize a given-up recovery turn: deliver the exhaustion notification\n * plus the package-owned terminal record / banner / submission writes. A thin\n * pass-through to the package's `_exhaustChatRecovery` (which composes\n * {@link runChatRecoveryExhaustion}). Driven by\n * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}; the engine owns the\n * surrounding read → re-entry-guard → build → terminalize → seal sequence, the\n * package owns the terminal writes (uniformly broadcast-first; their set\n * differs — `Think` also writes a submission row).\n */\n exhaustChatRecovery(\n incident: ChatRecoveryIncident,\n config: ResolvedChatRecoveryConfig,\n partial: RecoveryPartial,\n streamId: string,\n createdAt: number\n ): Promise<void>;\n /**\n * Resolve the orphaned stream identity for a (recovery-root) request id —\n * `streamId` is `\"\"` when no stream metadata survives. Drives BOTH the wake\n * path (which consumes the full {@link ResolvedRecoveryStream}) and the\n * give-up path (which reads only `.streamId`). A thin pass-through to the\n * package's stream-metadata lookup: the newest row keyed by the request id,\n * else the live active stream.\n */\n resolveRecoveryStream(requestId: string): ResolvedRecoveryStream;\n /** Reconstruct the partial text/parts buffered for `streamId`. */\n getPartialStreamText(streamId: string): RecoveryPartial;\n /**\n * The in-flight recovery-root request id, consulted as a fallback in the\n * give-up root-id chain when the payload carries no `originalRequestId` /\n * `recoveredRequestId` and no incident record survives. `undefined` when no\n * recovery chain is active. (`AIChatAgent` and `Think` both back this with\n * `_activeChatRecoveryRootRequestId`.)\n */\n activeChatRecoveryRootRequestId(): string | undefined;\n /**\n * Report a tolerated best-effort bookkeeping failure during give-up: the\n * incident `\"read\"` (before synthesizing) or the sealing `\"seal\"` write\n * (after terminalization). Neither aborts terminalization — see\n * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.\n */\n onGiveUpBookkeepingError(phase: \"read\" | \"seal\", error: unknown): void;\n}\n\n/** Resolved orphaned-stream identity for a recovered chat turn. */\nexport interface ResolvedRecoveryStream {\n /** The orphaned stream id, or `\"\"` when no stream metadata survives. */\n streamId: string;\n /**\n * Whether the orphaned stream is still the live in-flight stream (so its\n * partial has not already been persisted + completed by an ACK-driven\n * reconnect). Gates persistence and stream completion.\n */\n streamStillActive: boolean;\n /**\n * The stream metadata row's lifecycle status, when the host tracks it\n * (`Think`). `undefined` for hosts that do not model terminal streams\n * (`AIChatAgent`) — those keep every terminal-stream branch dead, per the\n * \"substrate capabilities are optional\" decision in the RFC.\n */\n streamStatus?: ChatStreamStatus;\n}\n\n/** Input to {@link ChatFiberWakeHooks.classifyRecoveredTurn}. */\nexport interface ClassifyRecoveredTurnInput {\n snapshot: ChatFiberSnapshot | null;\n requestId: string;\n streamId: string;\n partial: RecoveryPartial;\n streamStillActive: boolean;\n streamStatus?: ChatStreamStatus;\n}\n\n/** Input to {@link ChatFiberWakeHooks.invokeOnChatRecovery}. */\nexport interface InvokeOnChatRecoveryInput {\n incident: ChatRecoveryIncident;\n recoveryKind: ChatRecoveryKind;\n recoveryRootRequestId: string;\n requestId: string;\n streamId: string;\n partial: RecoveryPartial;\n snapshot: ChatFiberSnapshot | null;\n recoveryData: unknown;\n createdAt: number;\n}\n\n/** Input to {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}. */\nexport interface PersistOrphanedPartialInput {\n streamId: string;\n streamStillActive: boolean;\n streamStatus?: ChatStreamStatus;\n snapshot: ChatFiberSnapshot | null;\n}\n\n/** Input to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}. */\nexport interface DispatchRecoveredTurnInput<TClassify> {\n incident: ChatRecoveryIncident;\n config: ResolvedChatRecoveryConfig;\n recoveryKind: ChatRecoveryKind;\n options: ChatRecoveryOptions;\n snapshot: ChatFiberSnapshot | null;\n requestId: string;\n recoveryRootRequestId: string;\n streamId: string;\n streamStatus?: ChatStreamStatus;\n /** The package-specific classification detail produced by `classifyRecoveredTurn`. */\n detail: TClassify;\n}\n\n/**\n * The wake-dispatch host operations the engine drives when an interrupted CHAT\n * fiber is detected on restart — the divergent organs the frame-collapse map\n * flagged. Kept SEPARATE from {@link ChatRecoveryAdapter} (and passed per call to\n * {@link ChatRecoveryEngine.handleChatFiberRecovery}) so the incident/give-up\n * adapter stays focused, and generic over `TClassify` so the\n * `classifyRecoveredTurn` → `dispatchRecoveredTurn` handoff is type-safe without a\n * class-level generic.\n *\n * The engine owns the wake LIFECYCLE (gate → parse → unwrap → stream → partial →\n * classify → begin-incident → exhausted-branch → onChatRecovery → persist →\n * complete → dispatch → catch→failed) and the shared persist clause; these hooks\n * own the package-specific I/O and the retry/continue/skip decision.\n */\nexport interface ChatFiberWakeHooks<TClassify> {\n /** The chat-fiber name prefix (`CHAT_FIBER_NAME + \":\"`) gating the wake path. */\n chatFiberPrefix(): string;\n /** Decode the fiber snapshot into the recovery snapshot + checkpointed user data. */\n unwrapRecoverySnapshot(ctx: FiberRecoveryContext): {\n snapshot: ChatFiberSnapshot | null;\n recoveryData: unknown;\n };\n /**\n * Classify the recovered turn as a `retry` or `continue` and return any\n * package-specific detail the dispatch decision needs (e.g. the pre-stream\n * retry target id). Runs before the incident is opened.\n */\n classifyRecoveredTurn(\n input: ClassifyRecoveredTurnInput\n ):\n | { recoveryKind: ChatRecoveryKind; detail: TClassify }\n | Promise<{ recoveryKind: ChatRecoveryKind; detail: TClassify }>;\n /**\n * Build the package's `ChatRecoveryContext` and invoke the user `onChatRecovery`\n * hook, returning its (defaulted) options. The engine wraps this in the\n * incident `failed`-on-throw guard. Optional: a host with no user\n * `onChatRecovery` surface (e.g. the pi fixture) omits it and the engine\n * proceeds with empty options (`{}`).\n */\n invokeOnChatRecovery?(\n input: InvokeOnChatRecoveryInput\n ): Promise<ChatRecoveryOptions | void>;\n /**\n * The BASE persist gate: whether the orphaned partial is eligible to be\n * materialized at all (live stream, or terminal-but-not-yet-persisted). The\n * engine ANDs this with the shared `options.persist !== false ||\n * partial.hasSettledToolResults` clause, so settled work is never dropped.\n */\n shouldPersistOrphanedPartial(\n input: PersistOrphanedPartialInput\n ): boolean | Promise<boolean>;\n /** Materialize the orphaned stream's partial into a persisted assistant message. */\n persistOrphanedStream(streamId: string): Promise<void>;\n /** Mark the (still-active) recovered stream complete and schedule cleanup. */\n completeRecoveredStream(streamId: string): void | Promise<void>;\n /**\n * The retry/continue/skip DECISION — the package-owned core. Runs after persist\n * + complete; owns the leaf/submission computation, the schedule calls (via\n * {@link ChatRecoveryEngine.scheduleRecovery}), the skip transitions, and any\n * package-specific terminal/broadcast writes.\n */\n dispatchRecoveredTurn(\n input: DispatchRecoveredTurnInput<TClassify>\n ): Promise<void>;\n}\n\n/**\n * Drives the shared recovery orchestration over a {@link ChatRecoveryAdapter}.\n * The incident *budget math* lives in the pure `evaluateChatRecoveryIncident`;\n * this class owns the surrounding sequence and its ordering invariants.\n */\nexport class ChatRecoveryEngine {\n constructor(private readonly adapter: ChatRecoveryAdapter) {}\n\n /**\n * Open or re-evaluate the recovery incident for `input`, persist the result,\n * and broadcast its lifecycle events. Returns the incident, the resolved\n * config, and whether the budget is now exhausted.\n */\n /**\n * Dispatch a recovered fiber to the package's non-chat handler (the\n * messenger/workflow seam) before any chat-recovery processing. Returns `true`\n * when the package consumed the fiber — the caller must then skip chat\n * recovery for it. The engine owns the *ordering* (this runs before the\n * chat-fiber gate); the *behavior* is adapter-owned. No-op (`false`) when the\n * adapter omits {@link ChatRecoveryAdapter.tryHandleNonChatFiberRecovery}.\n */\n async handleNonChatFiber(ctx: FiberRecoveryContext): Promise<boolean> {\n return (await this.adapter.tryHandleNonChatFiberRecovery?.(ctx)) ?? false;\n }\n\n /**\n * The shared wake-recovery LIFECYCLE for an interrupted chat fiber. Both\n * packages drove this exact frame; the divergent organs are the\n * {@link ChatFiberWakeHooks}. In order:\n *\n * 1. non-chat dispatch ({@link handleNonChatFiber}) FIRST, then the chat-fiber\n * name gate — a non-chat fiber is never misread as an orphaned chat turn;\n * 2. parse the request id, unwrap the snapshot, resolve the orphaned stream +\n * reconstruct its partial;\n * 3. classify the turn (retry/continue + package detail) and open the incident;\n * 4. if the budget is already exhausted, persist the settled partial (so\n * non-idempotent tool results are not discarded — #1631) and terminalize\n * BEFORE consulting `onChatRecovery`;\n * 5. otherwise, inside a `failed`-on-throw guard: invoke `onChatRecovery`,\n * apply the shared persist gate (base eligibility AND `persist !== false ||\n * settled tool results`), complete the live stream, then hand the\n * retry/continue/skip DECISION to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}.\n *\n * Returns `true` when the fiber was a chat (or non-chat) recovery the engine\n * handled, `false` when it was not a chat fiber (the caller keeps looking). Any\n * throw after the incident opens flips it to `failed` so it is never left\n * leaking in `attempting`.\n */\n async handleChatFiberRecovery<TClassify>(\n ctx: FiberRecoveryContext,\n wake: ChatFiberWakeHooks<TClassify>\n ): Promise<boolean> {\n const { adapter } = this;\n\n // Ordering invariant: non-chat (messenger/workflow) fibers dispatch BEFORE\n // the chat-fiber gate.\n if (await this.handleNonChatFiber(ctx)) return true;\n\n const chatPrefix = wake.chatFiberPrefix();\n if (!ctx.name.startsWith(chatPrefix)) return false;\n\n const requestId = ctx.name.slice(chatPrefix.length);\n const { snapshot, recoveryData } = wake.unwrapRecoverySnapshot(ctx);\n const stream = adapter.resolveRecoveryStream(requestId);\n const { streamId, streamStillActive, streamStatus } = stream;\n const partial = streamId\n ? adapter.getPartialStreamText(streamId)\n : { text: \"\", parts: [], hasSettledToolResults: false };\n\n const { recoveryKind, detail } = await wake.classifyRecoveredTurn({\n snapshot,\n requestId,\n streamId,\n partial,\n streamStillActive,\n streamStatus\n });\n const recoveryRootRequestId = snapshot?.recoveryRootRequestId ?? requestId;\n\n const { incident, config, exhausted } = await this.beginIncident({\n requestId,\n recoveryRootRequestId,\n latestUserMessageId: snapshot?.latestUserMessageId,\n recoveryKind\n });\n\n if (exhausted) {\n // Preserve the settled partial before sealing. Exhaustion is decided BEFORE\n // `onChatRecovery`, so without this the settled (often non-idempotent) tool\n // results the turn already produced are discarded and the model re-runs\n // them on the next message (#1631). Same gating as the normal path (with no\n // `options`, so the persist clause collapses to base eligibility) — never\n // duplicating a partial an earlier attempt already saved.\n if (\n await this._shouldPersistOrphanedPartial(wake, {\n streamId,\n streamStillActive,\n streamStatus,\n snapshot,\n options: undefined,\n partial\n })\n ) {\n await wake.persistOrphanedStream(streamId);\n }\n await adapter.exhaustChatRecovery(\n incident,\n config,\n partial,\n streamId,\n ctx.createdAt\n );\n return true;\n }\n\n // Any throw after the incident opens (user `onChatRecovery`, orphan\n // persistence, scheduling) must flip the incident to terminal `failed` and\n // emit, otherwise it leaks in `attempting` and is never observable as stuck.\n try {\n const options =\n (await wake.invokeOnChatRecovery?.({\n incident,\n recoveryKind,\n recoveryRootRequestId,\n requestId,\n streamId,\n partial,\n snapshot,\n recoveryData,\n createdAt: ctx.createdAt\n })) ?? {};\n\n if (\n await this._shouldPersistOrphanedPartial(wake, {\n streamId,\n streamStillActive,\n streamStatus,\n snapshot,\n options,\n partial\n })\n ) {\n await wake.persistOrphanedStream(streamId);\n }\n\n if (streamStillActive) {\n await wake.completeRecoveredStream(streamId);\n }\n\n await wake.dispatchRecoveredTurn({\n incident,\n config,\n recoveryKind,\n options,\n snapshot,\n requestId,\n recoveryRootRequestId,\n streamId,\n streamStatus,\n detail\n });\n\n return true;\n } catch (error) {\n await this.updateIncident(\n incident.incidentId,\n \"failed\",\n error instanceof Error ? error.message : String(error)\n );\n throw error;\n }\n }\n\n /**\n * The shared persist gate: base eligibility (the package's\n * {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}) AND the\n * never-drop-settled-work clause `options.persist !== false ||\n * partial.hasSettledToolResults`. `options: undefined` (the exhausted branch)\n * collapses the clause to the base gate. The clause lives here — not in each\n * package — because settled-work preservation is a cross-package invariant\n * (#1631), and the codec (not the engine) decides whether a partial carries\n * settled tool work, so the engine stays wire-vocabulary-agnostic.\n */\n private async _shouldPersistOrphanedPartial<TClassify>(\n wake: ChatFiberWakeHooks<TClassify>,\n input: PersistOrphanedPartialInput & {\n options: ChatRecoveryOptions | undefined;\n partial: RecoveryPartial;\n }\n ): Promise<boolean> {\n const base = await wake.shouldPersistOrphanedPartial({\n streamId: input.streamId,\n streamStillActive: input.streamStillActive,\n streamStatus: input.streamStatus,\n snapshot: input.snapshot\n });\n return (\n base &&\n (input.options?.persist !== false || input.partial.hasSettledToolResults)\n );\n }\n\n async beginIncident(\n input: BeginChatRecoveryIncidentInput\n ): Promise<BeginChatRecoveryIncidentResult> {\n const { adapter } = this;\n const config = adapter.resolveConfig();\n const key = chatRecoveryIncidentKey(chatRecoveryIncidentId(input));\n const now = input.nowMs ?? adapter.now();\n // Ordering invariant: sweep stale incidents BEFORE reading the existing\n // record. A TTL-expired identity is also past its no-progress window, so\n // sweeping first lets a genuinely abandoned turn start fresh instead of\n // resuming a dead budget.\n await adapter.sweepStaleIncidents(now);\n const existing = await adapter.getIncident(key);\n // Ordering invariant: rehydrate interaction state BEFORE the budget reads\n // `isAwaitingClientInteraction()` (see the adapter hook's contract).\n adapter.ensureInteractionStateLoaded?.();\n const currentProgress = await adapter.readProgress();\n\n const { incident, exhausted, events } = await evaluateChatRecoveryIncident({\n identity: input,\n config,\n existing,\n currentProgress,\n awaitingClientInteraction:\n adapter.isAwaitingClientInteraction?.() ?? false,\n now,\n onShouldKeepRecoveringError: (error) =>\n adapter.onShouldKeepRecoveringError?.(error)\n });\n\n await adapter.putIncident(key, incident);\n for (const event of events) {\n adapter.emitRecoveryEvent(event);\n }\n return { incident, config, exhausted };\n }\n\n /**\n * Schedule a recovery continuation/retry: the transition + emit + enqueue\n * triplet both packages repeat at every fiber-recovery and stall-routing\n * decision. In order:\n *\n * 1. transition the incident to `scheduled` (persist + drive the #1620\n * \"recovering…\" status) via {@link updateIncident};\n * 2. emit `chat:recovery:scheduled`; and\n * 3. enqueue the callback through the adapter's idempotent schedule.\n *\n * `recoveryKind` is passed explicitly (not read off the incident) because a\n * caller can legitimately report a different kind than the incident was opened\n * with — e.g. `AIChatAgent`'s lost-partial branch opens a `continue` incident\n * but schedules (and reports) a `retry`. `requestId` always matches\n * `incident.requestId` (the evaluation rewrites it to the current attempt), so\n * it is read from the incident.\n */\n async scheduleRecovery(input: {\n incident: ChatRecoveryIncident;\n recoveryKind: ChatRecoveryKind;\n callback: ChatRecoveryScheduleCallback;\n data: Record<string, unknown>;\n reason?: ChatRecoveryScheduleReason;\n }): Promise<void> {\n const { incident } = input;\n await this.updateIncident(incident.incidentId, \"scheduled\");\n this.adapter.emitRecoveryEvent({\n type: \"chat:recovery:scheduled\",\n incidentId: incident.incidentId,\n requestId: incident.requestId,\n attempt: incident.attempt,\n maxAttempts: incident.maxAttempts,\n recoveryKind: input.recoveryKind\n });\n await this.adapter.scheduleRecovery(\n input.callback,\n input.data,\n input.reason ?? \"initial\",\n 0\n );\n }\n\n /**\n * Reschedule a recovery continuation/retry that timed out waiting for stable\n * state, INSIDE the currently-executing one-shot schedule row. Reads the\n * incident; if it is still under the attempt cap, bumps `attempt`, marks it\n * `scheduled` with `reason:\"stable_timeout_retry\"`, and issues a delayed,\n * NON-idempotent schedule (`alarm()` deletes the executing row only after this\n * returns, so an idempotent reschedule would dedup onto that doomed row and\n * never fire — see {@link chatRecoverySchedulePolicy}).\n *\n * Returns `true` when a retry was scheduled, `false` when there is no incident\n * (no id / record gone) or the attempt budget is already spent — in which case\n * the caller falls through to the give-up path. Deliberately bypasses the\n * `evaluateChatRecoveryIncident` budget (this is a coarse stable-state retry,\n * not a fresh interruption) and {@link updateIncident} (no `scheduled` event /\n * recovering-flag churn on a same-turn reschedule).\n */\n async rescheduleAfterStableTimeout(input: {\n incidentId: string | undefined;\n callback: ChatRecoveryScheduleCallback;\n data: Record<string, unknown> | undefined;\n fallbackMaxAttempts: number;\n }): Promise<boolean> {\n const { adapter } = this;\n if (!input.incidentId) return false;\n const key = chatRecoveryIncidentKey(input.incidentId);\n const incident = await adapter.getIncident(key);\n if (!incident) return false;\n const attempt = incident.attempt ?? 0;\n if (attempt >= (incident.maxAttempts ?? input.fallbackMaxAttempts)) {\n return false;\n }\n await adapter.putIncident(key, {\n ...incident,\n attempt: attempt + 1,\n status: \"scheduled\",\n lastAttemptAt: adapter.now(),\n reason: \"stable_timeout_retry\"\n });\n await adapter.scheduleRecovery(\n input.callback,\n input.data ?? {},\n \"stable_timeout_retry\",\n CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS\n );\n return true;\n }\n\n /**\n * Record that a recovery callback observed a Durable Object memory-limit reset\n * (the isolate exceeded its 128 MB limit — `isDurableObjectMemoryLimitReset`)\n * and decide what to do next (#1825).\n *\n * Bumps the incident's durable `oomAttempts` counter, then:\n * - if it is still within `maxOomRetries`, issues a delayed, NON-idempotent\n * reschedule of the SAME callback (same machinery as\n * {@link rescheduleAfterStableTimeout}: the executing one-shot row is\n * deleted only after the callback returns, so an idempotent reschedule\n * would dedup onto that doomed row) and returns `\"rescheduled\"`. The small\n * delay lets a transient memory spike clear before the re-run;\n * - otherwise leaves the incremented count persisted (so a begin-path\n * re-evaluation agrees) and returns `\"exhausted\"` — the caller then\n * terminalizes via the give-up path with `reason=\"out_of_memory\"`.\n *\n * Returns `\"exhausted\"` when there is no incident to track against (no id /\n * record gone): an OOM we cannot bound must seal rather than loop. Unlike a\n * stable-state retry this is gated by the OOM-specific budget, NOT the generic\n * attempt cap — re-running an OOM streams a little \"progress\" that would\n * otherwise reset the attempt cap forever (the #1825 loop).\n */\n async recordOomAndDecide(input: {\n incidentId: string | undefined;\n callback: ChatRecoveryScheduleCallback;\n data: Record<string, unknown> | undefined;\n maxOomRetries: number;\n }): Promise<\"rescheduled\" | \"exhausted\"> {\n const { adapter } = this;\n if (!input.incidentId) return \"exhausted\";\n const key = chatRecoveryIncidentKey(input.incidentId);\n const incident = await adapter.getIncident(key);\n if (!incident) return \"exhausted\";\n const oomAttempts = (incident.oomAttempts ?? 0) + 1;\n if (oomAttempts > input.maxOomRetries) {\n // Persist the crossed count so the begin-path backstop in\n // `evaluateChatRecoveryIncident` agrees, then let the caller terminalize.\n await adapter.putIncident(key, {\n ...incident,\n oomAttempts,\n lastAttemptAt: adapter.now(),\n reason: \"out_of_memory\"\n });\n return \"exhausted\";\n }\n await adapter.putIncident(key, {\n ...incident,\n oomAttempts,\n status: \"scheduled\",\n lastAttemptAt: adapter.now(),\n reason: \"oom_retry\"\n });\n await adapter.scheduleRecovery(\n input.callback,\n input.data ?? {},\n \"stable_timeout_retry\",\n CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS\n );\n return \"rescheduled\";\n }\n\n /**\n * Give up on a recovery turn whose retry budget drained, terminalizing it so\n * it can never become an eternal spinner (#1645). The shared spine both\n * packages repeated verbatim:\n *\n * 1. resolve config + the incident key from `data.incidentId`;\n * 2. best-effort READ the stored incident — a failed read is tolerated\n * (reported via `onGiveUpBookkeepingError(\"read\", …)`) and the incident is\n * synthesized, because the read backs only the re-entry guard, not the\n * terminal UX;\n * 3. re-entry guard: a `stored.status === \"exhausted\"` record means\n * terminalization already fired, so a duplicate stale alarm returns without\n * re-broadcasting the banner;\n * 4. build the exhausted incident (reuse `stored`, or synthesize a minimal one\n * so a swept/missing record STILL terminalizes through `onExhausted`);\n * 5. resolve the orphaned stream id + partial;\n * 6. terminalize via `exhaustChatRecovery` — BEFORE sealing. The terminal\n * writes can reject with a platform transient in the deploy/storage window\n * a give-up runs in (#1730); letting that throw propagate is deliberate, so\n * `Agent._executeScheduleCallback` defers the one-shot row and the WHOLE\n * give-up re-runs on a healthy isolate. Sealing first would arm the\n * re-entry guard and turn that re-run into a no-op, dropping the durable\n * terminal record. The re-run is idempotent (terminal writes overwrite the\n * same key); a second banner is the documented at-least-once edge; and\n * 7. best-effort SEAL write so the re-entry guard sees `exhausted` on a\n * duplicate alarm — a failed seal (reported via\n * `onGiveUpBookkeepingError(\"seal\", …)`) costs at most one re-delivered\n * banner.\n *\n * The two packages diverged only in parameters the caller supplies:\n * `reason` (`Think` passes `stable_timeout` | `recovery_error`; `AIChatAgent`\n * always `stable_timeout`) and the root-id chain (`Think` includes\n * `recoveredRequestId`; `AIChatAgent` never sets it, so the unified chain\n * collapses identically). Exactly-once terminalization rests on the re-entry\n * guard alone in `AIChatAgent`; `Think` additionally short-circuits duplicate\n * alarms earlier in its durable-submission layer.\n */\n async exhaustRecoveryGiveUp(input: {\n callback: ChatRecoveryScheduleCallback;\n data:\n | {\n incidentId?: string;\n originalRequestId?: string;\n recoveredRequestId?: string;\n }\n | undefined;\n reason: string;\n }): Promise<void> {\n const { adapter } = this;\n const config = adapter.resolveConfig();\n const incidentKey = input.data?.incidentId\n ? chatRecoveryIncidentKey(input.data.incidentId)\n : null;\n\n let stored: ChatRecoveryIncident | null = null;\n if (incidentKey) {\n try {\n stored = await adapter.getIncident(incidentKey);\n } catch (readError) {\n adapter.onGiveUpBookkeepingError(\"read\", readError);\n }\n }\n\n // Re-entry guard: a sealed incident means terminalization already happened,\n // so a duplicate stale alarm must not re-fire `onExhausted` / the banner.\n if (stored?.status === \"exhausted\") return;\n\n const rootRequestId =\n input.data?.originalRequestId ??\n input.data?.recoveredRequestId ??\n adapter.activeChatRecoveryRootRequestId() ??\n stored?.recoveryRootRequestId ??\n stored?.requestId ??\n \"\";\n\n const incident: ChatRecoveryIncident = stored\n ? { ...stored, status: \"exhausted\", reason: input.reason }\n : {\n // Silent-drop guard: the record is gone (no `incidentId`, or it was\n // swept/deleted before this stale alarm). Synthesize a minimal\n // incident so the turn STILL terminalizes instead of vanishing.\n incidentId: input.data?.incidentId ?? crypto.randomUUID(),\n requestId: rootRequestId,\n recoveryRootRequestId: rootRequestId,\n recoveryKind:\n input.callback === \"_chatRecoveryRetry\" ? \"retry\" : \"continue\",\n attempt: config.maxAttempts,\n maxAttempts: config.maxAttempts,\n status: \"exhausted\",\n firstSeenAt: adapter.now(),\n lastAttemptAt: adapter.now(),\n reason: input.reason\n };\n\n const { streamId } = adapter.resolveRecoveryStream(\n incident.recoveryRootRequestId ?? incident.requestId\n );\n const partial = streamId\n ? adapter.getPartialStreamText(streamId)\n : { text: \"\", parts: [], hasSettledToolResults: false };\n\n await adapter.exhaustChatRecovery(\n incident,\n config,\n partial,\n streamId,\n incident.firstSeenAt\n );\n\n if (incidentKey) {\n try {\n await adapter.putIncident(incidentKey, incident);\n } catch (writeError) {\n adapter.onGiveUpBookkeepingError(\"seal\", writeError);\n }\n }\n }\n\n /**\n * Apply a status transition to the recovery incident `incidentId`:\n *\n * - `completed` → drop the record (terminal, never retried);\n * - any other status → persist the new status (and `reason`), so the attempt\n * budget survives restarts until the TTL sweep reclaims it;\n * - emit the matching `completed`/`skipped`/`failed` lifecycle event; and\n * - drive the live \"recovering…\" status (#1620): `scheduled` marks it active\n * (keyed by the recovery-root request id), terminal states clear it.\n *\n * No-op when `incidentId` is undefined or the record is already gone. This is\n * the transition twin of {@link beginIncident}: all I/O is adapter-owned, the\n * engine owns only the state-machine shape.\n */\n async updateIncident(\n incidentId: string | undefined,\n status: ChatRecoveryIncident[\"status\"],\n reason?: string\n ): Promise<void> {\n if (!incidentId) return;\n const { adapter } = this;\n const key = chatRecoveryIncidentKey(incidentId);\n const incident = await adapter.getIncident(key);\n if (!incident) return;\n\n if (status === \"completed\") {\n await adapter.deleteIncident(key);\n } else {\n await adapter.putIncident(key, {\n ...incident,\n status,\n ...(reason ? { reason } : {})\n });\n }\n\n const eventType =\n status === \"completed\"\n ? \"chat:recovery:completed\"\n : status === \"skipped\"\n ? \"chat:recovery:skipped\"\n : status === \"failed\"\n ? \"chat:recovery:failed\"\n : undefined;\n if (eventType) {\n adapter.emitRecoveryEvent({\n type: eventType,\n incidentId,\n requestId: incident.requestId,\n attempt: incident.attempt,\n maxAttempts: incident.maxAttempts,\n recoveryKind: incident.recoveryKind,\n ...(reason ? { reason } : {})\n });\n }\n\n if (status === \"scheduled\") {\n await adapter.setRecovering(\n true,\n incident.recoveryRootRequestId ?? incident.requestId\n );\n } else if (\n status === \"completed\" ||\n status === \"skipped\" ||\n status === \"failed\"\n ) {\n await adapter.setRecovering(false);\n }\n }\n}\n\n/**\n * Build the `ChatRecoveryExhaustedContext` delivered to `onExhausted` and the\n * `chat:recovery:exhausted` event. Pure field-mapping shared by both packages;\n * the `reason` falls back to `max_attempts_exceeded` when the incident did not\n * record a more specific cause.\n */\nexport function buildChatRecoveryExhaustedContext(input: {\n incident: ChatRecoveryIncident;\n config: ResolvedChatRecoveryConfig;\n partialText: string;\n partialParts: ChatRecoveryExhaustedContext[\"partialParts\"];\n streamId: string;\n createdAt: number;\n}): ChatRecoveryExhaustedContext {\n const { incident, config } = input;\n return {\n incidentId: incident.incidentId,\n requestId: incident.requestId,\n recoveryRootRequestId: incident.recoveryRootRequestId ?? incident.requestId,\n attempt: incident.attempt,\n maxAttempts: incident.maxAttempts,\n recoveryKind: incident.recoveryKind,\n streamId: input.streamId,\n createdAt: input.createdAt,\n partialText: input.partialText,\n partialParts: input.partialParts,\n reason: incident.reason ?? \"max_attempts_exceeded\",\n terminalMessage: config.terminalMessage\n };\n}\n\n/**\n * Run the shared exhaustion notification: emit `chat:recovery:exhausted`, then\n * invoke the caller's `onExhausted` hook. A throwing hook is swallowed (and\n * reported via `onError`) so it can NEVER prevent the caller from delivering\n * terminal UX — a tested invariant in both packages. The terminal record /\n * banner / submission writes that follow are intentionally package-owned (their\n * ordering legitimately diverges), so they are NOT part of this helper.\n */\nexport async function notifyChatRecoveryExhausted(\n ctx: ChatRecoveryExhaustedContext,\n hooks: {\n emit: (ctx: ChatRecoveryExhaustedContext) => void;\n onExhausted?: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;\n onError: (error: unknown) => void;\n }\n): Promise<void> {\n hooks.emit(ctx);\n try {\n await hooks.onExhausted?.(ctx);\n } catch (error) {\n hooks.onError(error);\n }\n}\n\n/**\n * The complete give-up choreography from a single call: build the exhausted\n * context, fire the shared notification ({@link notifyChatRecoveryExhausted}),\n * then hand that context to the host's `terminalize` step. Folds the\n * `buildChatRecoveryExhaustedContext` → `notifyChatRecoveryExhausted` → host\n * terminalize sequence that every host's `_exhaustChatRecovery` repeated.\n *\n * What this OWNS (the invariant, so it cannot drift per host):\n * - the notification ALWAYS runs before any terminal write, and\n * - a throwing `onExhausted` can NEVER block terminal delivery — it is swallowed\n * via `onError` (a tested invariant in both published packages).\n *\n * What it deliberately does NOT own: the terminal-record / broadcast /\n * recovering-clear writes — their exact set diverges per host (both\n * `AIChatAgent` and `Think` broadcast the banner first so it survives a storage\n * write that rejects mid-deploy; `Think` additionally writes a submission row)\n * — see {@link ChatRecoveryAdapter.exhaustChatRecovery}. The host expresses\n * those writes inside `terminalize`. A `terminalize` that throws DOES propagate,\n * so the whole give-up re-runs on a healthy isolate (#1730); see\n * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.\n *\n * `partialParts` is passed explicitly (not derived from a `RecoveryPartial`) so a\n * foreign-vocabulary host can pass `[]` rather than fabricate AI-SDK parts — the\n * engine seam stays parts-vocabulary-agnostic.\n */\nexport async function runChatRecoveryExhaustion(\n input: {\n incident: ChatRecoveryIncident;\n config: ResolvedChatRecoveryConfig;\n partialText: string;\n partialParts: ChatRecoveryExhaustedContext[\"partialParts\"];\n streamId: string;\n createdAt: number;\n },\n hooks: {\n emit: (ctx: ChatRecoveryExhaustedContext) => void;\n onExhausted?: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;\n onError: (error: unknown) => void;\n terminalize: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;\n }\n): Promise<void> {\n const ctx = buildChatRecoveryExhaustedContext({\n incident: input.incident,\n config: input.config,\n partialText: input.partialText,\n partialParts: input.partialParts,\n streamId: input.streamId,\n createdAt: input.createdAt\n });\n await notifyChatRecoveryExhausted(ctx, {\n emit: hooks.emit,\n onExhausted: hooks.onExhausted,\n onError: hooks.onError\n });\n await hooks.terminalize(ctx);\n}\n","/**\n * Shared inactivity watchdog for UI-message streams.\n *\n * A model/transport stream can park indefinitely without ever throwing (a hung\n * provider, a wedged transport). Left unguarded, the consumer read-loop waits\n * forever. {@link iterateWithStallWatchdog} wraps such a stream so that a gap of\n * `timeoutMs` between chunks aborts the upstream and throws\n * {@link ChatStreamStalledError}, letting the consumer route the stall into\n * bounded recovery (#1626) — a transient hang is retried within the existing\n * recovery budget — while genuine in-band errors stay terminal.\n *\n * @internal Sibling-package support for `@cloudflare/ai-chat` and\n * `@cloudflare/think`, not a public API. See\n * `design/rfc-chat-recovery-foundation.md`.\n */\n\n/**\n * Thrown by {@link iterateWithStallWatchdog} when the inactivity watchdog fires\n * (a model/transport stream that parks without ever throwing). Distinct from\n * in-band model/stream errors so the read-loop catch can route a stall into\n * bounded recovery (#1626) — a transient hang is retried within the existing\n * recovery budget — while genuine errors stay terminal.\n */\nexport class ChatStreamStalledError extends Error {\n readonly isChatStreamStall = true;\n constructor(message: string) {\n super(message);\n this.name = \"ChatStreamStalledError\";\n }\n}\n\n/**\n * Wrap a UI-message stream with an inactivity watchdog. If no chunk arrives\n * within `timeoutMs`, `onStall` runs (aborting the upstream model stream) and\n * the iterator throws, so the consumer loop exits with a terminal error\n * instead of parking forever on a hung provider/transport. `timeoutMs <= 0`\n * passes the source through untouched.\n */\nexport async function* iterateWithStallWatchdog<T>(\n source: AsyncIterable<T>,\n timeoutMs: number,\n onStall: () => void\n): AsyncGenerator<T> {\n if (!(timeoutMs > 0)) {\n yield* source;\n return;\n }\n const iterator = source[Symbol.asyncIterator]();\n // Tracks whether the watchdog itself aborted the upstream. In that case we\n // must NOT also `iterator.return()` it: cancelling the readable after the\n // abort makes the AI SDK pipeline write to an already-cancelled readable\n // (\"readable side is no longer readable\"). Letting the abort error the\n // stream is the clean path.\n let selfAborted = false;\n try {\n while (true) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let stalled = false;\n const stall = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n stalled = true;\n reject(\n new ChatStreamStalledError(\n `Chat stream stalled: no activity for ${timeoutMs}ms; the turn was aborted by the stall watchdog.`\n )\n );\n }, timeoutMs);\n });\n const nextPromise = iterator.next();\n // If the watchdog wins the race we abandon this read; aborting the\n // upstream stream makes it reject later, so pre-attach a no-op catch to\n // keep that abandoned rejection from surfacing as an unhandled rejection.\n nextPromise.catch(() => {});\n let next: IteratorResult<T>;\n try {\n next = await Promise.race([nextPromise, stall]);\n } catch (err) {\n if (stalled) {\n selfAborted = true;\n onStall();\n }\n throw err;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n if (next.done) return;\n yield next.value;\n }\n } finally {\n // Forward early termination (consumer `break`/`throw`, e.g. an in-band\n // stream error where the abort signal is NOT set) to the source so its\n // reader is cancelled — otherwise the wrapped source would leak when the\n // consumer stops reading mid-stream. Skipped after a watchdog stall, which\n // already aborted the upstream (see `selfAborted` above).\n if (!selfAborted) {\n await iterator.return?.(undefined as never).catch(() => {});\n }\n }\n}\n"],"mappings":"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"}