agents 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +20 -1
  2. package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-B2XLNMxq.d.ts} +1094 -443
  3. package/dist/agent-routing.d.ts +1 -1
  4. package/dist/agent-tool-types.d.ts +26 -26
  5. package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-Dj5Mzlw7.d.ts} +7 -7
  6. package/dist/agent-tools.d.ts +21 -21
  7. package/dist/browser/ai.js +4 -1
  8. package/dist/browser/ai.js.map +1 -1
  9. package/dist/browser/tanstack-ai.js.map +1 -1
  10. package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
  11. package/dist/callable-decorator.d.ts +18 -0
  12. package/dist/callable-decorator.js +71 -0
  13. package/dist/callable-decorator.js.map +1 -0
  14. package/dist/{capability-BjSKYpzg.js → capability-B4WbF81e.js} +2 -1
  15. package/dist/capability-B4WbF81e.js.map +1 -0
  16. package/dist/capability-runner-BUBa6Ake.d.ts +484 -0
  17. package/dist/channel-Bnm4S7T2.d.ts +491 -0
  18. package/dist/channels/ai-sdk.d.ts +46 -0
  19. package/dist/channels/ai-sdk.js +120 -0
  20. package/dist/channels/ai-sdk.js.map +1 -0
  21. package/dist/channels/email.d.ts +95 -0
  22. package/dist/channels/email.js +323 -0
  23. package/dist/channels/email.js.map +1 -0
  24. package/dist/channels/index.d.ts +233 -0
  25. package/dist/channels/index.js +608 -0
  26. package/dist/channels/index.js.map +1 -0
  27. package/dist/channels/slack.d.ts +140 -0
  28. package/dist/channels/slack.js +614 -0
  29. package/dist/channels/slack.js.map +1 -0
  30. package/dist/channels/tanstack-ai.d.ts +39 -0
  31. package/dist/channels/tanstack-ai.js +17 -0
  32. package/dist/channels/tanstack-ai.js.map +1 -0
  33. package/dist/channels/telegram.d.ts +106 -0
  34. package/dist/channels/telegram.js +427 -0
  35. package/dist/channels/telegram.js.map +1 -0
  36. package/dist/channels/voice.d.ts +45 -0
  37. package/dist/channels/voice.js +122 -0
  38. package/dist/channels/voice.js.map +1 -0
  39. package/dist/chat/index.d.ts +2319 -2010
  40. package/dist/chat/index.js +892 -518
  41. package/dist/chat/index.js.map +1 -1
  42. package/dist/chat-sdk/index.d.ts +7 -7
  43. package/dist/chat-sdk/index.js +1 -1
  44. package/dist/client-BxdkCuVy.d.ts +411 -0
  45. package/dist/client-C9DAjZIY.js +518 -0
  46. package/dist/client-C9DAjZIY.js.map +1 -0
  47. package/dist/{client-jagG8a9_.js → client-fyQrLGkD.js} +2 -2
  48. package/dist/{client-jagG8a9_.js.map → client-fyQrLGkD.js.map} +1 -1
  49. package/dist/client.d.ts +27 -283
  50. package/dist/client.js +2 -274
  51. package/dist/context/index.d.ts +216 -0
  52. package/dist/context/index.js +454 -0
  53. package/dist/context/index.js.map +1 -0
  54. package/dist/current-agent-C4w86kcN.d.ts +93 -0
  55. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  56. package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
  57. package/dist/diagnostics-BzvaX2UT.js.map +1 -0
  58. package/dist/diagnostics-C4jcz3VK.js +360 -0
  59. package/dist/diagnostics-C4jcz3VK.js.map +1 -0
  60. package/dist/durable-object-lifecycle-ei7M7lqB.d.ts +157 -0
  61. package/dist/index-BB0kqhIz.d.ts +101 -0
  62. package/dist/index-BVVgDSdq.d.ts +1 -0
  63. package/dist/index-Dlgtd3Mj.d.ts +78 -0
  64. package/dist/index-XDkuQ7zm.d.ts +89 -0
  65. package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
  66. package/dist/index.d.ts +92 -83
  67. package/dist/index.js +4 -3
  68. package/dist/ingress-BfetZbMO.js +83 -0
  69. package/dist/ingress-BfetZbMO.js.map +1 -0
  70. package/dist/internal-CYlgHl1l.js +59 -0
  71. package/dist/internal-CYlgHl1l.js.map +1 -0
  72. package/dist/internal_context.d.ts +1 -1
  73. package/dist/lifecycle/index.d.ts +39 -19
  74. package/dist/lifecycle/index.js +2 -2
  75. package/dist/lifecycle-Mm_jQh7r.js +1314 -0
  76. package/dist/lifecycle-Mm_jQh7r.js.map +1 -0
  77. package/dist/mcp/client/index.d.ts +20 -20
  78. package/dist/mcp/client/index.js +1 -1
  79. package/dist/mcp/index.d.ts +35 -35
  80. package/dist/mcp/index.js +2 -2
  81. package/dist/observability/index.d.ts +1 -1
  82. package/dist/observability/index.js +1 -1
  83. package/dist/queue/index.d.ts +123 -0
  84. package/dist/queue/index.js +2 -0
  85. package/dist/queue-BC9F5cUo.js +406 -0
  86. package/dist/queue-BC9F5cUo.js.map +1 -0
  87. package/dist/react.d.ts +34 -16
  88. package/dist/react.js +31 -8
  89. package/dist/react.js.map +1 -1
  90. package/dist/{retries-CAvxtG9d.d.ts → retries-D9B2UCq3.d.ts} +23 -6
  91. package/dist/retries.d.ts +10 -6
  92. package/dist/retries.js +21 -1
  93. package/dist/retries.js.map +1 -1
  94. package/dist/routing/index.d.ts +137 -0
  95. package/dist/routing/index.js +244 -0
  96. package/dist/routing/index.js.map +1 -0
  97. package/dist/sanitize-D9TujEK8.js +79 -0
  98. package/dist/sanitize-D9TujEK8.js.map +1 -0
  99. package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-BGq6M5Kd.d.ts} +20 -21
  100. package/dist/scheduler-D_KHqxBa.js +658 -0
  101. package/dist/scheduler-D_KHqxBa.js.map +1 -0
  102. package/dist/schedules/index.d.ts +1 -1
  103. package/dist/schedules/index.js +1 -1
  104. package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
  105. package/dist/serializable.d.ts +1 -1
  106. package/dist/sessions/index.d.ts +441 -0
  107. package/dist/sessions/index.js +2063 -0
  108. package/dist/sessions/index.js.map +1 -0
  109. package/dist/skills/index.d.ts +99 -0
  110. package/dist/skills/index.js +254 -5
  111. package/dist/skills/index.js.map +1 -1
  112. package/dist/{src-5W6JNKVb.js → src-BNU3ZiJM.js} +1524 -1441
  113. package/dist/src-BNU3ZiJM.js.map +1 -0
  114. package/dist/state/index.d.ts +6 -0
  115. package/dist/state/index.js +137 -0
  116. package/dist/state/index.js.map +1 -0
  117. package/dist/streams/index.d.ts +120 -0
  118. package/dist/streams/index.js +107 -0
  119. package/dist/streams/index.js.map +1 -0
  120. package/dist/streams-BcE8JP2X.d.ts +370 -0
  121. package/dist/streams-CCPRV6dt.js +709 -0
  122. package/dist/streams-CCPRV6dt.js.map +1 -0
  123. package/dist/sub-routing.d.ts +12 -12
  124. package/dist/surface-bZZJqBka.js +17 -0
  125. package/dist/surface-bZZJqBka.js.map +1 -0
  126. package/dist/tasks/index.d.ts +64 -0
  127. package/dist/tasks/index.js +2 -0
  128. package/dist/tasks-D4nLqVSI.js +1656 -0
  129. package/dist/tasks-D4nLqVSI.js.map +1 -0
  130. package/dist/tasks-V2lCT8ZV.d.ts +517 -0
  131. package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
  132. package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
  133. package/dist/text-stream-CpdiKrJB.js +272 -0
  134. package/dist/text-stream-CpdiKrJB.js.map +1 -0
  135. package/dist/tokens-nHAKcN6M.js +52 -0
  136. package/dist/tokens-nHAKcN6M.js.map +1 -0
  137. package/dist/tool-schema-CBjGPrsQ.js +31 -0
  138. package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
  139. package/dist/transport-protocol-BxyngQ_R.js +25 -0
  140. package/dist/transport-protocol-BxyngQ_R.js.map +1 -0
  141. package/dist/transport-protocol-ZTew5Wso.d.ts +27 -0
  142. package/dist/types-B7LojTe4.d.ts +202 -0
  143. package/dist/types-DCD6BNZH.d.ts +70 -0
  144. package/dist/types-_Faxb570.d.ts +439 -0
  145. package/dist/voice/client.d.ts +226 -0
  146. package/dist/voice/client.js +932 -0
  147. package/dist/voice/client.js.map +1 -0
  148. package/dist/voice/errors.d.ts +43 -0
  149. package/dist/voice/errors.js +41 -0
  150. package/dist/voice/errors.js.map +1 -0
  151. package/dist/voice/index.d.ts +271 -0
  152. package/dist/voice/index.js +1812 -0
  153. package/dist/voice/index.js.map +1 -0
  154. package/dist/voice/react.d.ts +167 -0
  155. package/dist/voice/react.js +234 -0
  156. package/dist/voice/react.js.map +1 -0
  157. package/dist/voice/sfu.d.ts +71 -0
  158. package/dist/voice/sfu.js +157 -0
  159. package/dist/voice/sfu.js.map +1 -0
  160. package/dist/voice/text.d.ts +6 -0
  161. package/dist/voice/text.js +2 -0
  162. package/dist/voice/types.d.ts +58 -0
  163. package/dist/voice/types.js +18 -0
  164. package/dist/voice/types.js.map +1 -0
  165. package/dist/voice/workers-ai.d.ts +136 -0
  166. package/dist/voice/workers-ai.js +568 -0
  167. package/dist/voice/workers-ai.js.map +1 -0
  168. package/dist/websockets/index.d.ts +301 -0
  169. package/dist/websockets/index.js +3 -0
  170. package/dist/websockets-D7IzWbZb.js +1005 -0
  171. package/dist/websockets-D7IzWbZb.js.map +1 -0
  172. package/dist/workflow-types.d.ts +25 -25
  173. package/dist/workflows.d.ts +21 -21
  174. package/dist/workflows.js +1 -1
  175. package/docs/agent-class.md +4 -4
  176. package/docs/agent-tools.md +2 -1
  177. package/docs/channels.md +323 -0
  178. package/docs/chat-agents.md +6 -13
  179. package/docs/client-sdk.md +30 -0
  180. package/docs/context.md +131 -0
  181. package/docs/index.md +16 -13
  182. package/docs/lifecycle.md +179 -57
  183. package/docs/long-running-agents.md +2 -2
  184. package/docs/mcp-servers.md +5 -1
  185. package/docs/queue.md +135 -241
  186. package/docs/resumable-streaming.md +1 -1
  187. package/docs/retries.md +1 -1
  188. package/docs/routing.md +105 -0
  189. package/docs/sessions.md +237 -871
  190. package/docs/streams.md +213 -0
  191. package/docs/sub-agents.md +184 -124
  192. package/docs/tasks.md +246 -0
  193. package/docs/voice.md +745 -0
  194. package/package.json +126 -14
  195. package/dist/capability-BjSKYpzg.js.map +0 -1
  196. package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
  197. package/dist/client.js.map +0 -1
  198. package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
  199. package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
  200. package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
  201. package/dist/current-agent-CuMErtly.d.ts +0 -283
  202. package/dist/diagnostics-CaBjfz4J.js.map +0 -1
  203. package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
  204. package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
  205. package/dist/experimental/memory/session/index.d.ts +0 -671
  206. package/dist/experimental/memory/session/index.js +0 -2379
  207. package/dist/experimental/memory/session/index.js.map +0 -1
  208. package/dist/experimental/memory/utils/index.d.ts +0 -96
  209. package/dist/experimental/memory/utils/index.js +0 -79
  210. package/dist/experimental/memory/utils/index.js.map +0 -1
  211. package/dist/scheduler-CR9RHGos.js +0 -857
  212. package/dist/scheduler-CR9RHGos.js.map +0 -1
  213. package/dist/src-5W6JNKVb.js.map +0 -1
  214. package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
  215. package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
@@ -1,15 +1,18 @@
1
+ import { o as isPlatformFailure } from "../retries-D9B2UCq3.js";
1
2
  import {
2
- H as AgentToolEvent,
3
- U as AgentToolEventMessage,
4
- W as AgentToolEventState,
5
- et as AgentToolRunState,
6
- v as FiberRecoveryContext
7
- } from "../agent-routing-CnkaHb-v.js";
3
+ I as AgentToolEvent,
4
+ J as AgentToolRunState,
5
+ L as AgentToolEventMessage,
6
+ R as AgentToolEventState,
7
+ _ as FiberRecoveryContext$1
8
+ } from "../agent-routing-B2XLNMxq.js";
9
+ import { _ as TaskStep, m as TaskRunOptions } from "../tasks-V2lCT8ZV.js";
8
10
  import {
9
11
  n as ClientToolSchema,
10
12
  r as createToolsFromClientSchemas,
11
13
  t as ClientToolExecutor
12
14
  } from "../client-tools-aIBO0Fk7.js";
15
+ import { n as Streams } from "../streams-BcE8JP2X.js";
13
16
  import {
14
17
  a as applyAgentToolEvent,
15
18
  i as AgentToolProgressEmitter,
@@ -18,9 +21,10 @@ import {
18
21
  r as AgentToolProgressEmitResult,
19
22
  s as interceptAgentToolBroadcast,
20
23
  t as AgentToolBroadcastHooks
21
- } from "../agent-tools-C0XJqWzB.js";
24
+ } from "../agent-tools-Dj5Mzlw7.js";
25
+ import { l as SessionMessage } from "../types-B7LojTe4.js";
22
26
  import { JSONSchema7, UIMessage } from "ai";
23
- import { Connection } from "agents";
27
+ import { Connection, FiberRecoveryContext } from "agents";
24
28
 
25
29
  //#region src/chat/message-builder.d.ts
26
30
  /** The parts array type from UIMessage */
@@ -151,10 +155,16 @@ declare function isReplayChunk(
151
155
  chunk: StreamChunkData
152
156
  ): boolean;
153
157
  //#endregion
154
- //#region src/chat/sanitize.d.ts
155
- /** Maximum serialized message size before compaction (bytes). 1.8MB with headroom below SQLite's 2MB limit. */
156
- declare const ROW_MAX_BYTES = 1800000;
157
- /** Measure UTF-8 byte length of a string. */
158
+ //#region src/sessions/sanitize.d.ts
159
+ /**
160
+ * Message hygiene shared by every Sessions write: UTF-8 measurement without a
161
+ * full encoded copy, and removal of ephemeral provider metadata that must not
162
+ * be stored or replayed.
163
+ */
164
+ /**
165
+ * Measure UTF-8 byte length without allocating a complete encoded copy.
166
+ * Memory stays bounded by a 16 KiB buffer even for near-row-limit strings.
167
+ */
158
168
  declare function byteLength(s: string): number;
159
169
  /**
160
170
  * Sanitize a message for persistence by removing ephemeral provider-specific
@@ -163,7 +173,15 @@ declare function byteLength(s: string): number;
163
173
  * 1. Strips OpenAI ephemeral fields (itemId, reasoningEncryptedContent)
164
174
  * 2. Filters truly empty reasoning parts (no text, no remaining providerMetadata)
165
175
  */
166
- declare function sanitizeMessage(message: UIMessage): UIMessage;
176
+ declare function sanitizeMessage<
177
+ M extends {
178
+ parts: readonly object[];
179
+ }
180
+ >(message: M): M;
181
+ //#endregion
182
+ //#region src/chat/sanitize.d.ts
183
+ /** Maximum serialized message size before compaction (bytes). 1.8MB with headroom below SQLite's 2MB limit. */
184
+ declare const ROW_MAX_BYTES = 1800000;
167
185
  /** Optional hooks for {@link enforceRowSizeLimit}. */
168
186
  interface EnforceRowSizeLimitOptions {
169
187
  /**
@@ -330,7 +348,7 @@ type TextSegmentEvent =
330
348
  /**
331
349
  * @internal Converts structured stream chunks into text and boundary events
332
350
  * without gluing words across non-text chunks. Sibling-package support for
333
- * `@cloudflare/voice` and `@cloudflare/think`, not a public API.
351
+ * Voice and `@cloudflare/think`, not a public API.
334
352
  */
335
353
  declare class TextSegmentJoiner {
336
354
  #private;
@@ -528,7 +546,7 @@ type ChatRecoveryProgressContext = {
528
546
  * Configuration for durable chat recovery, which is always enabled for chat
529
547
  * turns. `true` uses these defaults: `maxAttempts: 10`,
530
548
  * `stableTimeoutMs: 10_000`, `noProgressTimeoutMs: 300_000` (5 min),
531
- * `maxRecoveryWork: 1000`, and a generic terminal message. Assign an object to
549
+ * `maxRecoveryWork: 10000`, and a generic terminal message. Assign an object to
532
550
  * tune recovery without disabling its durable bookkeeping.
533
551
  *
534
552
  * **Apply this as a class field or in the constructor — never assign it in
@@ -737,21 +755,16 @@ declare function transition(
737
755
  //#endregion
738
756
  //#region src/chat/resumable-stream.d.ts
739
757
  /**
740
- * How far ahead (seconds) to schedule the resumable-stream buffer cleanup
741
- * alarm. Set to the short completion-grace window ({@link COMPLETED_RETENTION_MS},
742
- * 10m) so a finished buffer is reclaimed promptly. The re-arm-while-reclaimable
743
- * loop (see {@link cleanupStreamBuffers}) revisits any longer-lived rows — e.g.
744
- * an abandoned in-flight buffer on its 1h window — by waking again each interval
745
- * until they age out, then stops. Driving cleanup from an alarm (rather than
746
- * only piggybacking on the next stream completion) ensures idle/one-off chat
747
- * DOs still reclaim their buffers without waking forever (#1706). Shared by
748
- * `AIChatAgent` and `Think`.
758
+ * Construct the Streams capability instance a chat host must install to back
759
+ * its `ResumableStream`: identical to `new Streams()` except for the raised
760
+ * per-chunk ceiling that chat's packed segments require.
749
761
  */
750
- declare const STREAM_CLEANUP_DELAY_SECONDS: number;
762
+ declare function createChatStreams(): Streams;
763
+ /** Public status vocabulary predates the Streams state names. */
764
+ type PublicStreamStatus = "streaming" | "completed" | "error";
751
765
  /**
752
- * Minimal SQL interface matching Agent's this.sql tagged template.
753
- * Allows ResumableStream to work with the Agent's SQLite without
754
- * depending on the full Agent class.
766
+ * Minimal SQL interface matching Agent's this.sql tagged template. The
767
+ * adapter uses it exclusively for chat's own legacy tables during migration.
755
768
  */
756
769
  type SqlTaggedTemplate = {
757
770
  <T = Record<string, unknown>>(
@@ -759,12 +772,21 @@ type SqlTaggedTemplate = {
759
772
  ...values: (string | number | boolean | null)[]
760
773
  ): T[];
761
774
  };
775
+ /** Host hooks for the chat stream adapter. */
776
+ type ResumableStreamOptions = {
777
+ /**
778
+ * Called with the durable part of the recovery progress marker (retired
779
+ * segments plus credits plus the seeded legacy counter) after it advances,
780
+ * outside any transaction. Hosts mirror it to the pre-derivation KV key so
781
+ * a build rolled back to the KV counter never reads a marker lower than
782
+ * an incident recorded under this one. Fires per stream retired and per
783
+ * credit, never per chunk.
784
+ */
785
+ onProgress?: (durableSegments: number) => void;
786
+ };
762
787
  declare class ResumableStream {
763
- private sql;
764
788
  private _activeStreamId;
765
789
  private _activeRequestId;
766
- /** Monotonic row-ordering index; one increment per flushed segment row. */
767
- private _segmentIndex;
768
790
  /**
769
791
  * Whether the active stream was started in this instance (true) or
770
792
  * restored from SQLite after hibernation/restart (false). An orphaned
@@ -774,23 +796,103 @@ declare class ResumableStream {
774
796
  private _isLive;
775
797
  /**
776
798
  * Whether the active stream is a continuation. Mirrors the durable
777
- * `is_continuation` column so replay frames can carry the flag without a
778
- * per-replay query; restored from SQLite after hibernation in restore().
799
+ * metadata so replay frames can carry the flag without a per-replay query;
800
+ * restored from SQLite after hibernation in restore().
779
801
  */
780
802
  private _activeIsContinuation;
781
803
  private _chunkBuffer;
782
804
  private _chunkBufferBytes;
783
805
  private _isFlushingChunks;
784
- private _lastCleanupTime;
785
- constructor(sql: SqlTaggedTemplate);
786
806
  /**
787
- * Add metadata columns for rows created before they existed. Constructors
788
- * intentionally do not run this: most wakes never start a stream, so paying a
789
- * schema-introspection read every time is wasteful. New tables include these
790
- * columns in CREATE TABLE; legacy tables migrate lazily only if a write/read
791
- * discovers the columns are missing.
807
+ * A stream whose producer finished but whose row is deliberately still
808
+ * `streaming`: the host will {@link cutover} it together with the message
809
+ * write, or {@link finalizePending} it when there is nothing to persist.
810
+ */
811
+ private _pendingCutover;
812
+ private readonly ops;
813
+ constructor(
814
+ streams: Streams,
815
+ sql: SqlTaggedTemplate,
816
+ options?: ResumableStreamOptions
817
+ );
818
+ private readonly _sql;
819
+ private readonly _onProgress;
820
+ /**
821
+ * Tell the host the durable part of the marker moved. Called after the
822
+ * write that moved it has left any transaction, never inside one: the
823
+ * host's mirror is an async KV put, which a synchronous transaction
824
+ * would reject.
825
+ */
826
+ private _notifyProgress;
827
+ /**
828
+ * One row: `retired` accumulates the segments of deleted streams and
829
+ * explicit credits; `legacy` holds the pre-derivation KV counter, folded
830
+ * in once. They are separate columns so a seed can never swallow
831
+ * segments retired before it landed, and a repeated seed is idempotent.
832
+ */
833
+ private _ensureProgressTable;
834
+ private _retiredSegments;
835
+ /** Add `segments` to the retired total. One row write; a no-op for zero. */
836
+ private _retire;
837
+ /**
838
+ * Segments a row still accounts for: a live stream's log tail, a settled
839
+ * stream's final cursor (stamped exact at settlement).
840
+ */
841
+ private _segmentsOf;
842
+ /**
843
+ * Monotonic count of durably flushed chat segments on this Durable
844
+ * Object, plus explicit credits (see {@link creditProgress}): the recovery
845
+ * engine's forward-progress marker. Advances only when a segment lands in
846
+ * the log — never on a reconnect replay or a recovery re-persist, which
847
+ * read the log without appending — and is untouched by compaction, which
848
+ * rewrites the transcript, not the log. Reads the stream rows plus one
849
+ * log-tail row per live stream: called at incident evaluation, not on the
850
+ * hot path.
851
+ *
852
+ * A chat row leaving the table by any path — this adapter's cutover,
853
+ * reclaim and clear, or the capability's own `delete()` — passes through
854
+ * the deletion hook, so its segments are retired before they are gone
855
+ * and the marker never moves on a deletion.
856
+ */
857
+ progressMarker(): number;
858
+ /**
859
+ * Credit one unit of forward progress that the log cannot see: a parent
860
+ * forwarding a sub-agent's output (N9) produces no chunks of its own, yet
861
+ * that output is the parent turn advancing. One row write; callers
862
+ * throttle.
863
+ */
864
+ creditProgress(): void;
865
+ /**
866
+ * Carry the pre-derivation KV counter forward: the marker must not read
867
+ * lower after the upgrade than the high-water mark an in-flight incident
868
+ * already recorded, or a progressing turn would look stuck until the log
869
+ * caught up. The counter is never written again, so its value is a
870
+ * constant this folds into its own column by max — idempotent across
871
+ * isolates, and never touching segments retired before the seed landed.
872
+ * A no-op for zero, so a fresh object never writes.
873
+ *
874
+ * The counter already credited a stream that was in flight at the
875
+ * upgrade, and that stream's live segments count again here, so the
876
+ * first read after the upgrade can exceed the counter by those segments.
877
+ * That reads as progress once, and hands an in-flight incident one extra
878
+ * no-progress window; it cannot recur.
879
+ */
880
+ seedProgress(legacyTotal: number): void;
881
+ /**
882
+ * Delete chat rows. The deletion hook folds each row's segments into the
883
+ * retired total in the same synchronous block, retire before delete, so
884
+ * a partial commit could only ever count a stream twice, never lose it.
792
885
  */
793
- private _migrateMetadataColumns;
886
+ private _deleteRetiring;
887
+ /**
888
+ * One-time migration of the pre-capability `cf_ai_chat_stream_*` tables
889
+ * into the Streams tables, preserving in-flight resumability across the
890
+ * upgrade (an active stream keeps its id, chunks, and last-activity), then
891
+ * dropping the legacy tables. Tolerates the pre-#1691/#1733 metadata
892
+ * schema (no `message_id` / `is_continuation` columns). The host `sql`
893
+ * handle touches only these chat-owned legacy tables.
894
+ */
895
+ private _migrateLegacyTables;
794
896
  get activeStreamId(): string | null;
795
897
  get activeRequestId(): string | null;
796
898
  hasActiveStream(): boolean;
@@ -801,7 +903,7 @@ declare class ResumableStream {
801
903
  get isLive(): boolean;
802
904
  /**
803
905
  * Start tracking a new stream for resumable streaming.
804
- * Creates metadata entry in SQLite and sets up tracking state.
906
+ * Creates the backing stream row and sets up tracking state.
805
907
  * @param requestId - The unique ID of the chat request
806
908
  * @returns The generated stream ID
807
909
  */
@@ -816,7 +918,7 @@ declare class ResumableStream {
816
918
  * The assistant message id an orphaned stream was producing — the same id the
817
919
  * live path persists under, so recovery re-associates reconstructed chunks
818
920
  * with the correct message (#1691). Returns null when the row is missing or
819
- * is a legacy row written before the `message_id` column existed.
921
+ * predates message-id tracking.
820
922
  */
821
923
  getStreamMessageId(streamId: string): string | null;
822
924
  /**
@@ -824,15 +926,47 @@ declare class ResumableStream {
824
926
  * @param streamId - The stream to mark as completed
825
927
  */
826
928
  complete(streamId: string): void;
929
+ /**
930
+ * The producer finished, but leave the row `streaming` for the cutover:
931
+ * the host persists the message and settles the stream in one
932
+ * transaction with {@link cutover}. Until then a crash leaves the stream
933
+ * live — exactly the evidence recovery rebuilds the message from. The
934
+ * host MUST follow with {@link cutover} or {@link finalizePending}.
935
+ */
936
+ finish(streamId: string): void;
937
+ /** The stream {@link finish}ed and awaiting its cutover, if any. */
938
+ get pendingCutoverId(): string | null;
939
+ /**
940
+ * The cutover: settle the stream, run `persist` (synchronous writes — the
941
+ * message), and delete the stream's rows in one SQLite transaction. A
942
+ * crash leaves either the live stream or the finished message, never
943
+ * neither; nothing is left to sweep. `discard: false` keeps the settled
944
+ * rows (an agent-tool child whose parent still tails them); they are
945
+ * reclaimed by the next {@link start}. The settlement and `persist`
946
+ * writes commit or roll back together.
947
+ */
948
+ cutover(
949
+ streamId: string,
950
+ persist: () => void,
951
+ options?: {
952
+ discard?: boolean;
953
+ }
954
+ ): void;
955
+ /**
956
+ * Settle a {@link finish}ed stream that had nothing to persist (no parts,
957
+ * a persist that threw). Idempotent; a no-op when nothing is pending.
958
+ * The pending marker clears only once settlement succeeds, so a caller
959
+ * may retry after a settlement failure — matching {@link cutover}.
960
+ */
961
+ finalizePending(): void;
962
+ private _clearActive;
827
963
  /**
828
964
  * Mark a stream as errored and clean up state.
829
965
  * @param streamId - The stream to mark as errored
830
966
  */
831
967
  markError(streamId: string): void;
832
- /** Maximum chunk body size before skipping storage (bytes). Prevents SQLite row limit crash. */
833
- private static CHUNK_MAX_BYTES;
834
968
  /**
835
- * Buffer a stream chunk for batch write to SQLite.
969
+ * Buffer a stream chunk for batch write to storage.
836
970
  * Chunks exceeding the row size limit are skipped to prevent crashes.
837
971
  * The chunk is still broadcast to live clients (caller handles that),
838
972
  * but will be missing from replay on reconnection.
@@ -841,15 +975,24 @@ declare class ResumableStream {
841
975
  */
842
976
  storeChunk(streamId: string, body: string): void;
843
977
  /**
844
- * Flush the buffered chunks to SQLite as a single packed row.
978
+ * Flush the buffered chunks to storage as a single packed segment.
845
979
  * Uses a lock to prevent concurrent flush operations.
846
980
  *
847
- * The whole buffer becomes one row: a single-chunk segment is stored
848
- * unwrapped (legacy object format) so a large chunk avoids array-escaping
849
- * inflation, while a multi-chunk segment stores a JSON array of bodies. This
850
- * collapses N chunk rows into one, cutting rows written / stored / scanned.
981
+ * The whole buffer becomes one stored chunk on the backing stream: a
982
+ * single-chunk segment is stored unwrapped so a large chunk avoids
983
+ * array-escaping inflation, while a multi-chunk segment stores a JSON
984
+ * array of bodies. This collapses N chunk writes into one fenced append,
985
+ * cutting rows written / stored / scanned.
851
986
  */
852
987
  flushBuffer(): void;
988
+ /**
989
+ * Stored chunk bodies for one stream, packed segments expanded, in order.
990
+ * A generator over paged reads, so replaying a large turn holds one page
991
+ * of segments in memory instead of the whole stored stream; iteration is
992
+ * synchronous end to end (WebSocket sends don't await), so the pages see
993
+ * a consistent log.
994
+ */
995
+ private _storedBodies;
853
996
  /**
854
997
  * Send stored stream chunks to a connection for replay.
855
998
  * Chunks are marked with replay: true so the client can batch-apply them.
@@ -860,11 +1003,10 @@ declare class ResumableStream {
860
1003
  * - **Orphaned stream** (restored from SQLite after hibernation, no reader):
861
1004
  * sends chunks + `done` and completes the stream. The caller should
862
1005
  * reconstruct and persist the partial message from the stored chunks.
863
- * - **Completed during replay** (defensive): sends chunks + `done`.
864
1006
  *
865
- * All sends use {@link sendIfOpen}, so a WebSocket closing mid-replay
866
- * does not throw. If the connection drops while iterating chunks the
867
- * stream is left active so the next reconnect can retry.
1007
+ * All sends tolerate a WebSocket closing mid-replay. If the connection
1008
+ * drops while iterating chunks the stream is left active so the next
1009
+ * reconnect can retry.
868
1010
  *
869
1011
  * @param connection - The WebSocket connection
870
1012
  * @param requestId - The original request ID
@@ -872,6 +1014,13 @@ declare class ResumableStream {
872
1014
  * When non-null the caller should reconstruct the message from chunks.
873
1015
  */
874
1016
  replayChunks(connection: Connection, requestId: string): string | null;
1017
+ /**
1018
+ * Latest CHAT-owned row carrying a request tag. The stream table is
1019
+ * shared with application producers and tags are non-unique, so the
1020
+ * newest row by tag alone could be an unrelated stream masking chat's
1021
+ * recovery evidence — ownership is the `cfChat` metadata marker.
1022
+ */
1023
+ private _latestChatRowByTag;
875
1024
  replayCompletedChunksByRequestId(
876
1025
  connection: Connection,
877
1026
  requestId: string
@@ -882,9 +1031,7 @@ declare class ResumableStream {
882
1031
  * frame carrying the durable terminal record's error text, mirroring what a
883
1032
  * live client observed (content chunks, then the error). Without this, a
884
1033
  * client that missed broadcast frames while disconnected has no other
885
- * channel to the pre-error partial content: the server does not push
886
- * messages on connect, and {@link replayCompletedChunksByRequestId} only
887
- * serves `completed` streams (#1575).
1034
+ * channel to the pre-error partial content (#1575).
888
1035
  *
889
1036
  * Returns true when the caller should proceed to send its terminal frame:
890
1037
  * either no errored stream existed (nothing to replay) or its chunks were
@@ -896,13 +1043,24 @@ declare class ResumableStream {
896
1043
  connection: Connection,
897
1044
  requestId: string
898
1045
  ): boolean;
899
- /** Latest stream row for a request with the given terminal status. */
900
- private _latestStreamForRequest;
901
1046
  /**
902
- * Send a finished stream's stored chunks to a connection as replay frames.
903
- * Returns false if the connection closed mid-replay.
1047
+ * Latest chat stream row for a request regardless of status — the recovery
1048
+ * engines' stream-evidence lookup.
904
1049
  */
905
- private _replayStoredChunks;
1050
+ latestStreamInfoForRequest(requestId: string): {
1051
+ id: string;
1052
+ status: PublicStreamStatus;
1053
+ createdAt: number;
1054
+ } | null;
1055
+ /**
1056
+ * Latest in-flight chat stream for a request — recoverable-turn evidence.
1057
+ */
1058
+ latestActiveStreamInfoForRequest(requestId: string): {
1059
+ id: string;
1060
+ createdAt: number;
1061
+ } | null;
1062
+ /** Every chat-owned stream row, newest first. */
1063
+ private _chatRows;
906
1064
  /**
907
1065
  * Restore active stream state if the agent was restarted during streaming.
908
1066
  * All streams are restored regardless of age — stale cleanup happens
@@ -910,38 +1068,37 @@ declare class ResumableStream {
910
1068
  */
911
1069
  restore(): void;
912
1070
  /**
913
- * Clear all stream data (called on chat history clear).
1071
+ * Clear all chat stream data (called on chat history clear). Streams other
1072
+ * producers opened on the same Durable Object are untouched.
914
1073
  */
915
1074
  clearAll(): void;
916
1075
  /**
917
- * Drop all stream tables (called on destroy).
1076
+ * Remove all chat stream data (called on destroy). The backing tables
1077
+ * belong to the Streams capability and are shared with other producers,
1078
+ * so this deletes chat's rows rather than dropping tables. Buffered
1079
+ * chunks are dropped (clearAll resets the buffer), not flushed: they
1080
+ * belong to a chat-owned stream this very call deletes, so writing them
1081
+ * first would only pay row writes for rows that die in the same
1082
+ * synchronous block.
918
1083
  */
919
1084
  destroy(): void;
920
1085
  /**
921
- * Force a sweep of aged stream buffers now, bypassing the lazy interval
922
- * gate used by {@link _maybeCleanupOldStreams}. Intended to be driven by an
923
- * alarm so idle/hibernated chat DOs still reclaim buffers even when no
924
- * further stream ever completes to trigger the lazy path.
1086
+ * Delete every chat stream row this Durable Object no longer needs:
1087
+ * finished streams (their messages are persisted; the cutover normally
1088
+ * deletes them in the same transaction, so these are crash leftovers) and
1089
+ * in-flight rows abandoned past {@link ABANDONED_STREAM_RETENTION_MS} by
1090
+ * last chunk activity. Runs on every {@link start}, so nothing needs an
1091
+ * alarm to be reclaimed; a Durable Object that never starts another turn
1092
+ * keeps at most one turn's rows. Streams other producers opened on the
1093
+ * same object are untouched.
1094
+ * @returns How many rows were reclaimed.
925
1095
  */
926
- cleanup(now?: number): void;
927
- /**
928
- * True if any stream rows remain at all. Used by alarm-driven cleanup to
929
- * decide whether to re-arm: once no rows remain there is nothing left to
930
- * sweep, so the DO can stop waking itself.
931
- */
932
- hasReclaimableStreams(): boolean;
933
- private _maybeCleanupOldStreams;
934
- /** Delete completed/errored buffers past the completion grace window, plus
935
- * abandoned "streaming" rows past the stale-in-flight window. The two use
936
- * different retentions: a completed buffer is redundant with the persisted
937
- * message and needs only a brief replay grace, whereas an in-flight buffer
938
- * must outlive resume/recovery before it is presumed dead. */
939
- private _sweepOldStreams;
1096
+ reclaim(now?: number): number;
940
1097
  /**
941
1098
  * Return the stored chunks for a stream as individual chunk bodies in order,
942
- * unpacking packed segment rows. The returned `chunk_index` is a running
943
- * per-chunk sequence (0, 1, 2, …) — stable across calls because rows are
944
- * append-only — so callers can use it as a monotonic chunk sequence.
1099
+ * unpacking packed segments. The returned `chunk_index` is a running
1100
+ * per-chunk sequence (0, 1, 2, …) — stable across calls because segments
1101
+ * are append-only — so callers can use it as a monotonic chunk sequence.
945
1102
  */
946
1103
  getStreamChunks(streamId: string): Array<{
947
1104
  body: string;
@@ -958,2108 +1115,2216 @@ declare class ResumableStream {
958
1115
  status: string;
959
1116
  request_id: string;
960
1117
  created_at: number;
1118
+ message_id: string | null;
961
1119
  }>;
962
1120
  /** @internal For testing only */
963
1121
  insertStaleStream(streamId: string, requestId: string, ageMs: number): void;
964
1122
  /**
965
- * Append a chunk to a stream dated `ageMs` in the past. Used to exercise the
966
- * last-activity sweep threshold: a long-running streaming row with a *recent*
967
- * chunk must survive even when its start time is older than the cutoff.
1123
+ * Append a chunk to a stream dated `ageMs` in the past. Used to exercise
1124
+ * reclaim's phase-2 verification: a long-running streaming row with a
1125
+ * *recent* chunk must survive even when its row `updated_at` (stamped at
1126
+ * open, not per append) is older than the coarse cutoff.
968
1127
  * @internal For testing only
969
1128
  */
970
1129
  insertChunkAt(streamId: string, body: string, ageMs: number): void;
971
1130
  }
972
- /**
973
- * The buffer-cleanup alarm body: sweep aged stream buffers, then re-arm only
974
- * while rows remain so a fully-swept DO stops waking itself. `rearm` schedules
975
- * the next sweep — it MUST schedule a non-idempotent alarm, because this runs
976
- * INSIDE the currently-executing one-shot schedule row, which `alarm()` deletes
977
- * only after it returns; an idempotent reschedule would dedup onto that row and
978
- * be deleted with it, so the re-arm would silently never fire and buffers that
979
- * survived this sweep (e.g. a younger turn) would go uncollected. A fresh
980
- * delayed row survives the deletion. Shared by `AIChatAgent` and `Think`.
981
- *
982
- * `@internal`
983
- */
984
- declare function cleanupStreamBuffers(
985
- stream: Pick<ResumableStream, "cleanup" | "hasReclaimableStreams">,
986
- rearm: () => Promise<void>
987
- ): Promise<void>;
988
1131
  //#endregion
989
- //#region src/chat/sql-batch.d.ts
990
- /**
991
- * Helpers for building batched SQLite statements that run through the Agent's
992
- * `sql` tagged template (which interleaves a `?` placeholder between every
993
- * string fragment). Used to collapse per-row INSERT/DELETE loops into a small
994
- * number of multi-row statements.
995
- *
996
- * SQLite (Durable Object / D1) caps bound parameters at 100 per query, so
997
- * callers must chunk their inputs to stay within {@link MAX_BOUND_PARAMS}.
998
- * See https://developers.cloudflare.com/d1/platform/limits/
999
- */
1000
- /** Maximum bound parameters allowed in a single SQLite (DO / D1) query. */
1001
- declare const MAX_BOUND_PARAMS = 100;
1132
+ //#region src/chat/turn-task.d.ts
1133
+ /** One live turn closure, registered under its nonce while this isolate runs it. */
1134
+ type ChatTurnClosureEntry = {
1135
+ /** The snapshot persisted at claim, before the turn produces anything. */ readonly initial: unknown /** Wrap user stash data into the durable snapshot envelope. */;
1136
+ wrap(data: unknown): unknown /** Execute the live turn. */;
1137
+ run(): Promise<unknown> /** Settles the caller awaiting the turn's outcome. */;
1138
+ readonly settle: {
1139
+ resolve(value: unknown): void;
1140
+ reject(error: unknown): void;
1141
+ };
1142
+ };
1143
+ /** The host internals one chat-turn definition runs against. */
1144
+ type ChatTurnTaskHooks = {
1145
+ /** The registered definition name (the host's `CHAT_FIBER_NAME`). */ readonly definitionName: string;
1146
+ readonly storage: DurableObjectStorage /** The run row's creation time, for recovery staleness accounting. */;
1147
+ getRunCreatedAt(runId: string): Promise<number | null>;
1148
+ getLiveClosure(nonce: string): ChatTurnClosureEntry | undefined;
1149
+ keepAliveWhile<T>(
1150
+ fn: () => Promise<T>
1151
+ ): Promise<T> /** The host's fiber stash context, so `this.stash()` keeps working. */;
1152
+ withStash<T>(
1153
+ context: {
1154
+ id: string;
1155
+ signal: AbortSignal;
1156
+ stash: (data: unknown) => void;
1157
+ },
1158
+ fn: () => Promise<T>
1159
+ ): Promise<T> /** The unchanged recovery seam (ChatRecoveryEngine behind it). */;
1160
+ handleRecovery(ctx: FiberRecoveryContext): Promise<unknown>;
1161
+ };
1002
1162
  /**
1003
- * Build a TemplateStringsArray for a single-column `IN (...)` clause. Produces
1004
- * fragments for:
1005
- * `${prefix}(?, ?, ...)`
1006
- *
1007
- * @throws if `count` is less than 1.
1163
+ * Build the chat-turn Task handler for one host. Registered under the
1164
+ * host's `CHAT_FIBER_NAME` via `Tasks#register`.
1008
1165
  */
1009
- declare function buildInClauseStrings(
1010
- prefix: string,
1011
- count: number
1012
- ): TemplateStringsArray;
1166
+ declare function createChatTurnTaskDefinition(
1167
+ hooks: ChatTurnTaskHooks
1168
+ ): (input: unknown, step: TaskStep) => Promise<void>;
1013
1169
  //#endregion
1014
- //#region src/chat/protocol.d.ts
1170
+ //#region src/chat/recovery.d.ts
1015
1171
  /**
1016
- * Wire protocol message type constants for the cf_agent_chat_* protocol.
1017
- *
1018
- * These are the string values used on the wire between agent servers and
1019
- * clients. Both @cloudflare/ai-chat (via its MessageType enum) and
1020
- * @cloudflare/think use these values.
1172
+ * The minimal transcript-tail shape {@link createChatFiberSnapshot} reads to
1173
+ * derive the snapshot's `latest*Id` markers. Deliberately NOT `UIMessage`: the
1174
+ * snapshot only ever needs each message's `id` + `role`, so any host transcript
1175
+ * (AI SDK `UIMessage[]`, `Think`'s session leaves, or the pi adapter's plain
1176
+ * `AgentMessage[]`) satisfies it structurally. Keeping this off `UIMessage` is
1177
+ * the Phase-5 genericity seam — the snapshot builder must not couple to the AI
1178
+ * SDK message shape.
1021
1179
  */
1022
- declare const STREAM_RESUME_NONE_REASONS: {
1023
- /** No active, pending, or terminal stream exists for this agent. */ readonly IDLE: "idle" /** An active tool continuation is owned by another live connection. */;
1024
- readonly CONTINUATION_OWNED: "continuation-owned";
1180
+ interface SnapshotMessage {
1181
+ id?: string;
1182
+ role: string;
1183
+ }
1184
+ type ChatFiberSnapshot<Kind extends string = string> = {
1185
+ kind: Kind;
1186
+ version: 1;
1187
+ requestId: string;
1188
+ recoveryRootRequestId?: string;
1189
+ continuation: boolean;
1190
+ latestMessageId?: string;
1191
+ latestMessageRole?: string;
1192
+ latestUserMessageId?: string;
1193
+ startedAt: number;
1194
+ lastBody?: Record<string, unknown>;
1195
+ lastClientTools?: ClientToolSchema[];
1025
1196
  };
1026
- type StreamResumeNoneReason =
1027
- (typeof STREAM_RESUME_NONE_REASONS)[keyof typeof STREAM_RESUME_NONE_REASONS];
1028
- declare const CHAT_MESSAGE_TYPES: {
1029
- readonly CHAT_MESSAGES: "cf_agent_chat_messages";
1030
- readonly USE_CHAT_REQUEST: "cf_agent_use_chat_request";
1031
- readonly USE_CHAT_RESPONSE: "cf_agent_use_chat_response";
1032
- readonly CHAT_CLEAR: "cf_agent_chat_clear";
1033
- readonly CHAT_REQUEST_CANCEL: "cf_agent_chat_request_cancel";
1034
- readonly STREAM_RESUMING: "cf_agent_stream_resuming";
1035
- readonly STREAM_RESUME_ACK: "cf_agent_stream_resume_ack";
1036
- readonly STREAM_RESUME_REQUEST: "cf_agent_stream_resume_request";
1037
- readonly STREAM_RESUME_NONE: "cf_agent_stream_resume_none";
1038
- readonly STREAM_PENDING: "cf_agent_stream_pending";
1039
- readonly TOOL_RESULT: "cf_agent_tool_result";
1040
- readonly TOOL_APPROVAL: "cf_agent_tool_approval";
1041
- readonly MESSAGE_UPDATED: "cf_agent_message_updated";
1042
- readonly CHAT_RECOVERING: "cf_agent_chat_recovering";
1197
+ declare function createChatFiberSnapshot<Kind extends string>({
1198
+ kind,
1199
+ requestId,
1200
+ recoveryRootRequestId,
1201
+ continuation,
1202
+ messages,
1203
+ lastBody,
1204
+ lastClientTools
1205
+ }: {
1206
+ kind: Kind;
1207
+ requestId: string;
1208
+ recoveryRootRequestId?: string;
1209
+ continuation: boolean;
1210
+ messages: ReadonlyArray<SnapshotMessage>;
1211
+ lastBody?: Record<string, unknown>;
1212
+ lastClientTools?: ClientToolSchema[];
1213
+ }): ChatFiberSnapshot<Kind>;
1214
+ declare function wrapChatFiberSnapshot<Kind extends string>(
1215
+ key: string,
1216
+ snapshot: ChatFiberSnapshot<Kind>,
1217
+ user: unknown | null
1218
+ ): Record<string, unknown>;
1219
+ declare function unwrapChatFiberSnapshot<Kind extends string>(
1220
+ key: string,
1221
+ value: unknown,
1222
+ expectedKind?: Kind
1223
+ ): {
1224
+ snapshot: ChatFiberSnapshot<Kind> | null;
1225
+ user: unknown | null;
1043
1226
  };
1044
1227
  //#endregion
1045
- //#region src/chat/wire-types.d.ts
1228
+ //#region src/chat/recovery-incident.d.ts
1046
1229
  /**
1047
- * Enum for message types to improve type safety and maintainability
1230
+ * Whether a recovery is retrying an unanswered user turn or continuing a
1231
+ * partial assistant turn. Intentionally NOT part of the incident identity (see
1232
+ * {@link chatRecoveryIncidentId}).
1048
1233
  */
1049
- declare enum MessageType {
1050
- CF_AGENT_CHAT_MESSAGES = "cf_agent_chat_messages",
1051
- CF_AGENT_USE_CHAT_REQUEST = "cf_agent_use_chat_request",
1052
- CF_AGENT_USE_CHAT_RESPONSE = "cf_agent_use_chat_response",
1053
- CF_AGENT_CHAT_CLEAR = "cf_agent_chat_clear",
1054
- CF_AGENT_CHAT_REQUEST_CANCEL = "cf_agent_chat_request_cancel",
1055
- /** Sent by server when client connects and there's an active stream to resume */
1056
- CF_AGENT_STREAM_RESUMING = "cf_agent_stream_resuming",
1057
- /** Sent by client to acknowledge stream resuming notification and request chunks */
1058
- CF_AGENT_STREAM_RESUME_ACK = "cf_agent_stream_resume_ack",
1059
- /** Sent by client after message handler is ready, requesting stream resume check */
1060
- CF_AGENT_STREAM_RESUME_REQUEST = "cf_agent_stream_resume_request",
1061
- /** Sent by server when client requests resume but no active stream exists */
1062
- CF_AGENT_STREAM_RESUME_NONE = "cf_agent_stream_resume_none",
1234
+ type ChatRecoveryKind = "retry" | "continue";
1235
+ /**
1236
+ * Durable per-incident recovery record.
1237
+ *
1238
+ * PERSISTED CONTRACT — this shape round-trips across deploys (including the
1239
+ * deploy that ships the shared engine, which is itself a deploy-mid-recovery).
1240
+ * Fields are added as optional so older persisted incidents keep recovering.
1241
+ */
1242
+ type ChatRecoveryIncident = {
1243
+ incidentId: string;
1244
+ requestId: string /** Stable request ID for the whole continuation chain (the recovery root). */;
1245
+ recoveryRootRequestId?: string;
1246
+ recoveryKind: ChatRecoveryKind;
1247
+ attempt: number;
1248
+ maxAttempts: number;
1249
+ status:
1250
+ | "detected"
1251
+ | "scheduled"
1252
+ | "attempting"
1253
+ | "completed"
1254
+ | "skipped"
1255
+ | "exhausted"
1256
+ | "failed";
1257
+ firstSeenAt: number;
1258
+ lastAttemptAt: number;
1063
1259
  /**
1064
- * Sent by server when a turn is accepted but its resumable stream has not
1065
- * started yet (queued / debouncing / waiting on MCP / async setup). Tells a
1066
- * reconnecting client to keep waiting rather than resolve its resume probe to
1067
- * "no stream". Resolved by a later `CF_AGENT_STREAM_RESUMING` (stream started)
1068
- * or `CF_AGENT_STREAM_RESUME_NONE` (settled without streaming). See #1784.
1260
+ * Epoch ms of the last attempt that observed forward progress. The recovery
1261
+ * budget is keyed to this (`now - lastProgressAt > noProgressTimeoutMs`), so a
1262
+ * turn that keeps producing content survives churn indefinitely while a
1263
+ * genuinely stuck turn is sealed within the window (#1637). Optional for
1264
+ * backward-compat — falls back to `firstSeenAt`.
1069
1265
  */
1070
- CF_AGENT_STREAM_PENDING = "cf_agent_stream_pending",
1071
- /** Client sends tool result to server (for client-side tools) */
1072
- CF_AGENT_TOOL_RESULT = "cf_agent_tool_result",
1073
- /** Server notifies client that a message was updated (e.g., tool result applied) */
1074
- CF_AGENT_MESSAGE_UPDATED = "cf_agent_message_updated",
1075
- /** Client sends tool approval response to server (for tools with needsApproval) */
1076
- CF_AGENT_TOOL_APPROVAL = "cf_agent_tool_approval",
1266
+ lastProgressAt?: number;
1267
+ reason?: string;
1077
1268
  /**
1078
- * Server→client progress hint: a durable chat turn is being recovered
1079
- * (interrupted by a deploy/eviction or a stream-stall watchdog abort and now
1080
- * resuming). Sent when a recovery continuation is scheduled and cleared on
1081
- * every terminal outcome. (`@cloudflare/think` also replays it on connect;
1082
- * `@cloudflare/ai-chat` broadcasts the live signal only — see #1645.)
1083
- * Backward-compatible — clients that don't understand it ignore it. See #1620.
1269
+ * High-water mark of the durable, monotonic recovery-progress counter
1270
+ * observed for this incident. Distinguishes a turn making forward progress
1271
+ * but repeatedly interrupted by isolate resets (deploys) — which must NOT
1272
+ * exhaust the budget — from one that genuinely fails to advance. Sourced from
1273
+ * a persisted counter, never the compactable transcript (#1628).
1084
1274
  */
1085
- CF_AGENT_CHAT_RECOVERING = "cf_agent_chat_recovering"
1086
- }
1275
+ progress?: number;
1276
+ /**
1277
+ * Value of the durable progress counter when this incident opened. The
1278
+ * runaway-loop work budget is `progress - workBaseline`, compared against
1279
+ * `maxRecoveryWork`. Optional for backward-compat — a missing baseline is
1280
+ * treated as the current marker (zero work so far), so an in-flight incident
1281
+ * from an older build is never falsely sealed.
1282
+ */
1283
+ workBaseline?: number;
1284
+ /**
1285
+ * Count of recovery attempts for this incident that ended in a Durable Object
1286
+ * memory-limit reset (the isolate exceeded its 128 MB limit — see
1287
+ * `isDurableObjectMemoryLimitReset`). An OOM is a poison signal: re-running the
1288
+ * same memory-heavy turn deterministically re-OOMs, so unlike a deploy/eviction
1289
+ * it must NOT credit forward progress and must be bounded by a tight,
1290
+ * OOM-specific budget (`maxOomRetries`) rather than the generic attempt cap
1291
+ * (which resets on progress). Bumped by `ChatRecoveryEngine.recordOomAndDecide`
1292
+ * when a recovery callback observes an OOM; exceeding `maxOomRetries` seals the
1293
+ * incident with `reason="out_of_memory"` (#1825). Optional for backward-compat.
1294
+ */
1295
+ oomAttempts?: number;
1296
+ };
1297
+ declare const CHAT_RECOVERY_INCIDENT_KEY_PREFIX = "cf:chat-recovery:incident:";
1087
1298
  /**
1088
- * Types of messages sent from the Agent to clients
1299
+ * The pre-derivation forward-progress counter: a KV integer bumped per
1300
+ * credited chunk. The marker is now derived from the stream log
1301
+ * (`ResumableStream.progressMarker`), and this key is only read — once per
1302
+ * isolate, to seed the derived marker so it never reads lower than the
1303
+ * high-water mark an incident recorded before the upgrade. Current code
1304
+ * writes it only as a mirror of the derived marker's durable part — one put
1305
+ * per stream retired, none per chunk — so a build rolled back to the
1306
+ * counter never reads a marker lower than an incident recorded here.
1089
1307
  */
1090
- type OutgoingMessage<ChatMessage extends UIMessage = UIMessage> =
1091
- | {
1092
- /** Indicates this message is a command to clear chat history */ type: MessageType.CF_AGENT_CHAT_CLEAR;
1093
- }
1094
- | {
1095
- /** Indicates this message contains updated chat messages */ type: MessageType.CF_AGENT_CHAT_MESSAGES /** Array of chat messages */;
1096
- messages: readonly ChatMessage[];
1097
- }
1098
- | {
1099
- /** Indicates this message is a response to a chat request */ type: MessageType.CF_AGENT_USE_CHAT_RESPONSE /** Unique ID of the request this response corresponds to */;
1100
- id: string /** Content body of the response */;
1101
- body: string /** Whether this is the final chunk of the response */;
1102
- done: boolean /** Whether this response contains an error */;
1103
- error?: boolean /** Whether this is a continuation (append to last assistant message) */;
1104
- continuation?: boolean /** Whether this chunk is being replayed from storage (stream resumption) */;
1105
- replay?: boolean /** Signals that replay of stored chunks is complete (stream is still active) */;
1106
- replayComplete?: boolean;
1107
- }
1108
- | {
1109
- /** Indicates the server is resuming an active stream */ type: MessageType.CF_AGENT_STREAM_RESUMING /** The request ID of the stream being resumed */;
1110
- id: string /** Present when this offer directly answers a client resume probe. */;
1111
- probeId?: string;
1112
- }
1113
- | {
1114
- /** Server notifies client that a message was updated (e.g., tool result applied) */ type: MessageType.CF_AGENT_MESSAGE_UPDATED /** The updated message */;
1115
- message: ChatMessage;
1116
- }
1117
- | {
1118
- /** Server responds to a resume request with no stream for this client. */ type: MessageType.CF_AGENT_STREAM_RESUME_NONE;
1119
- /**
1120
- * Why no stream was offered. Only `idle` proves global inactivity;
1121
- * omitted by older servers and by non-authoritative delayed releases.
1122
- */
1123
- reason?: StreamResumeNoneReason /** Correlates an authoritative response to its client resume probe. */;
1124
- probeId?: string;
1125
- }
1126
- | {
1127
- /**
1128
- * Server signals an accepted turn whose resumable stream has not started
1129
- * yet — the client should keep waiting for `STREAM_RESUMING` (or a later
1130
- * `STREAM_RESUME_NONE`) rather than give up. See #1784.
1131
- */
1132
- type: MessageType.CF_AGENT_STREAM_PENDING /** The accepted request id, when known. */;
1133
- id?: string /** Correlates a direct keep-waiting response to its client probe. */;
1134
- probeId?: string;
1135
- }
1136
- | {
1137
- /**
1138
- * Progress hint: a durable chat turn is being recovered (`recovering:
1139
- * true`) or recovery has resolved (`recovering: false`). Purely advisory;
1140
- * a client renders a "recovering…" indicator while true.
1141
- */
1142
- type: MessageType.CF_AGENT_CHAT_RECOVERING /** Whether recovery is in progress (true) or has resolved (false). */;
1143
- recovering: boolean /** The recovery-root request id of the turn being recovered, if known. */;
1144
- id?: string;
1145
- };
1308
+ declare const CHAT_RECOVERY_PROGRESS_KEY = "cf:chat-recovery:progress";
1146
1309
  /**
1147
- * Types of messages sent from clients to the Agent
1310
+ * Durable record of an in-progress recovery so a "recovering…" status (#1620)
1311
+ * can be broadcast live and survive the set/clear happening in different
1312
+ * isolates (a continuation runs in a later alarm invocation).
1148
1313
  */
1149
- type IncomingMessage<ChatMessage extends UIMessage = UIMessage> =
1150
- | {
1151
- /** Indicates this message is a command to clear chat history */ type: MessageType.CF_AGENT_CHAT_CLEAR;
1152
- }
1153
- | {
1154
- /** Indicates this message is a request to the chat API */ type: MessageType.CF_AGENT_USE_CHAT_REQUEST /** Unique ID for this request */;
1155
- id: string /** Request initialization options */;
1156
- init: Pick<
1157
- RequestInit,
1158
- | "method"
1159
- | "keepalive"
1160
- | "headers"
1161
- | "body"
1162
- | "redirect"
1163
- | "integrity"
1164
- | "credentials"
1165
- | "mode"
1166
- | "referrer"
1167
- | "referrerPolicy"
1168
- | "window"
1169
- >;
1170
- }
1171
- | {
1172
- /** Indicates this message contains updated chat messages */ type: MessageType.CF_AGENT_CHAT_MESSAGES /** Array of chat messages */;
1173
- messages: ChatMessage[];
1174
- }
1175
- | {
1176
- /** Indicates the user wants to stop generation of this message */ type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL;
1177
- id: string;
1178
- }
1179
- | {
1180
- /** Client acknowledges stream resuming notification and is ready to receive chunks */ type: MessageType.CF_AGENT_STREAM_RESUME_ACK /** The request ID of the stream being resumed */;
1181
- id: string;
1182
- }
1183
- | {
1184
- /** Client requests stream resume check after message handler is registered */ type: MessageType.CF_AGENT_STREAM_RESUME_REQUEST /** Opaque correlation id echoed by direct server responses. */;
1185
- probeId?: string;
1186
- }
1187
- | {
1188
- /** Client sends tool result to server (for client-side tools) */ type: MessageType.CF_AGENT_TOOL_RESULT /** The tool call ID this result is for */;
1189
- toolCallId: string /** The name of the tool */;
1190
- toolName: string /** The output from the tool execution */;
1191
- output: unknown /** Override the tool part state (e.g. "output-error" for custom denial) */;
1192
- state?:
1193
- | "output-available"
1194
- | "output-error" /** Error message when state is "output-error" */;
1195
- errorText?: string /** Whether server should auto-continue the conversation after applying result */;
1196
- autoContinue?: boolean /** Client tool schemas for continuation (client is source of truth) */;
1197
- clientTools?: Array<{
1198
- name: string;
1199
- description?: string;
1200
- parameters?: JSONSchema7;
1201
- }>;
1202
- }
1203
- | {
1204
- /** Client sends tool approval response to server (for tools with needsApproval) */ type: MessageType.CF_AGENT_TOOL_APPROVAL /** The tool call ID this approval is for */;
1205
- toolCallId: string /** Whether the tool execution was approved */;
1206
- approved: boolean /** Whether server should auto-continue the conversation after applying approval */;
1207
- autoContinue?: boolean;
1208
- };
1209
- //#endregion
1210
- //#region src/chat/connection.d.ts
1314
+ declare const CHAT_RECOVERING_KEY = "cf:chat:recovering";
1211
1315
  /**
1212
- * Connection I/O — shared WebSocket send guard for chat agents.
1316
+ * Durable record of the last turn that ended in a terminal error / abandoned
1317
+ * recovery (#1645). Replayed on the next reconnect via the resume handshake;
1318
+ * cleared when a later turn supersedes it.
1319
+ */
1320
+ declare const CHAT_LAST_TERMINAL_KEY = "cf:chat:last-terminal";
1321
+ /**
1322
+ * Secondary backstop only. The primary recovery bound is the no-progress wall
1323
+ * clock; with alarm debounce this cap rarely binds (it catches a pathological
1324
+ * tight alarm-loop). Kept high so the no-progress window seals first under
1325
+ * normal deploy cadence (#1637).
1326
+ */
1327
+ declare const DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS = 10;
1328
+ /**
1329
+ * Runaway-loop guard default — the framework-imposed backstop on cumulative
1330
+ * recovery WORK (produced content/tool units) since an incident opened.
1213
1331
  *
1214
- * `@internal` — sibling-package support for `@cloudflare/ai-chat` and
1215
- * `@cloudflare/think`, not a public API. See
1216
- * `design/rfc-chat-recovery-foundation.md`.
1332
+ * Originally `Infinity` (rfc-chat-recovery-work-budget): the SDK shipped the
1333
+ * *mechanism* but no default cap, so a progressing turn was never terminated on
1334
+ * its own. Production issue #1825 showed that this is a footgun: an isolate that
1335
+ * OOMs mid-stream still credits a little progress before it dies, which resets
1336
+ * BOTH progress-keyed bounds (the attempt cap and the no-progress window) on
1337
+ * every wake — and a fast crash loop (each attempt inside the alarm-debounce
1338
+ * window) pins the attempt counter too. With `maxRecoveryWork = Infinity` the
1339
+ * ONLY instrument whose meter still climbs across such a loop is disabled, so
1340
+ * recovery re-runs the turn (and its LLM calls) forever.
1217
1341
  *
1218
- * Both packages (and `continuation-state`) hand-maintained byte-identical
1219
- * copies of `sendIfOpen` / `isWebSocketClosedSendError`; this is the single
1220
- * shared implementation.
1342
+ * A finite default closes that loop out of the box: work climbs regardless of
1343
+ * debounce/progress resets, so a content-emitting runaway is always sealed with
1344
+ * `reason="work_budget_exceeded"`. The value is deliberately generous — it
1345
+ * bounds wasted re-run cost without clipping a normal interrupted turn (work
1346
+ * only accrues from the first interruption until the turn completes, after which
1347
+ * the incident is deleted). A very long agentic turn under heavy interruption
1348
+ * that legitimately needs more should raise `maxRecoveryWork` (or set it to
1349
+ * `Infinity` to restore the pre-#1825 unbounded behavior).
1350
+ *
1351
+ * The unit is one durable stream segment — about ten packed streaming chunks,
1352
+ * or one settled tool result, which is flushed on its own — plus one per
1353
+ * explicit credit for forwarded sub-agent output. The marker is derived from
1354
+ * the stream log (`ResumableStream.progressMarker`), so that is what it can
1355
+ * count. The earlier KV counter credited per milestone chunk and per five
1356
+ * seconds of deltas, a coarser measure of streamed text; 10 000 segments
1357
+ * (on the order of 100 000 chunks of re-run output) keeps the budget as
1358
+ * generous as 1 000 credits was for delta-heavy turns, and still finite.
1221
1359
  */
1360
+ declare const DEFAULT_CHAT_RECOVERY_MAX_WORK = 10000;
1222
1361
  /**
1223
- * Minimal connection interface for sending WebSocket messages. Matches the
1224
- * `Connection` type from `agents` without importing it: `Connection` extends
1225
- * `WebSocket` with its own `send` overload, so it is structurally assignable.
1362
+ * Tight, OOM-specific retry budget (#1825). A Durable Object memory-limit reset
1363
+ * (`isDurableObjectMemoryLimitReset`) is usually deterministic — the turn's
1364
+ * working set no longer fits in the isolate's 128 MB — so re-running it re-OOMs.
1365
+ * But a single OOM CAN be a transient spike (the isolate's 128 MB is shared
1366
+ * across the global scope / noisy neighbors), so recovery retries a small number
1367
+ * of times before sealing with `reason="out_of_memory"` rather than abandoning a
1368
+ * turn that one more attempt might have completed. Far tighter than the generic
1369
+ * `maxRecoveryWork` backstop because an OOM is attributable and re-running it is
1370
+ * expensive (it re-runs the model). Counts attempts that ended in an OOM, not
1371
+ * total attempts, so a turn interrupted by deploys (no OOM) is unaffected.
1226
1372
  */
1227
- interface ChatConnection {
1228
- readonly id: string;
1229
- send(message: string): void;
1230
- }
1373
+ declare const DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES = 3;
1374
+ declare const DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS = 10000;
1231
1375
  /**
1232
- * Send a message on a connection, swallowing the specific
1233
- * "send after close" error a racing disconnect produces. Returns `true` if the
1234
- * send went out, `false` if the socket was already closed. Any other error
1235
- * rethrows.
1376
+ * Delay before retrying a recovery that timed out waiting for stable state.
1377
+ * Gives an actively-churning isolate (e.g. a deploy in flight) time to settle.
1236
1378
  */
1237
- declare function sendIfOpen(
1238
- connection: ChatConnection,
1239
- message: string
1240
- ): boolean;
1241
- //#endregion
1242
- //#region src/chat/continuation-state.d.ts
1379
+ declare const CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS = 3;
1380
+ declare const DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE =
1381
+ "The assistant was interrupted and could not recover. Please try again.";
1243
1382
  /**
1244
- * Minimal connection interface for sending WebSocket messages. Alias of the
1245
- * shared {@link ChatConnection} — kept as a named export for back-compat with
1246
- * existing `ContinuationConnection` consumers.
1383
+ * Incidents that have not seen a new attempt within this window are assumed
1384
+ * abandoned and swept so durable storage does not grow without bound.
1247
1385
  */
1248
- type ContinuationConnection = ChatConnection;
1249
- interface ContinuationPending<
1250
- TConnection extends ContinuationConnection = ContinuationConnection
1251
- > {
1252
- connection: TConnection;
1253
- connectionId: string | null;
1254
- requestId: string;
1255
- clientTools?: ClientToolSchema[];
1256
- body?: Record<string, unknown>;
1257
- errorPrefix: string | null;
1258
- prerequisite: Promise<boolean> | null;
1259
- pastCoalesce: boolean;
1260
- }
1261
- interface ContinuationDeferred<
1262
- TConnection extends ContinuationConnection = ContinuationConnection
1263
- > {
1264
- connection: TConnection;
1265
- connectionId: string | null;
1266
- clientTools?: ClientToolSchema[];
1267
- body?: Record<string, unknown>;
1268
- errorPrefix: string;
1269
- prerequisite: Promise<boolean> | null;
1270
- }
1271
- declare class ContinuationState<
1272
- TConnection extends ContinuationConnection = ContinuationConnection
1273
- > {
1274
- pending: ContinuationPending<TConnection> | null;
1275
- deferred: ContinuationDeferred<TConnection> | null;
1276
- activeRequestId: string | null;
1277
- activeConnectionId: string | null;
1278
- awaitingConnections: Map<string, TConnection>;
1279
- /** Clear pending state and awaiting connections (without sending RESUME_NONE). */
1280
- clearPending(): void;
1281
- clearDeferred(): void;
1282
- clearAll(): void;
1283
- /**
1284
- * Mark a connection as no longer available without canceling the
1285
- * continuation it initiated.
1286
- */
1287
- releaseConnection(connectionId: string): void;
1288
- /**
1289
- * Send STREAM_RESUME_NONE to all connections waiting for a
1290
- * continuation stream to start, then clear the map.
1291
- */
1292
- sendResumeNone(): void;
1293
- /**
1294
- * Flush awaiting connections by notifying each one via the provided
1295
- * callback (typically sends STREAM_RESUMING), then clear.
1296
- */
1297
- flushAwaitingConnections(notify: (conn: TConnection) => void): void;
1298
- /**
1299
- * Transition pending → active. Called when the continuation stream
1300
- * actually starts. Moves request/connection IDs to active slots,
1301
- * clears pending fields.
1302
- */
1303
- activatePending(): void;
1304
- /**
1305
- * Transition deferred → pending. Called when a continuation turn
1306
- * completes and there's a deferred follow-up waiting.
1307
- *
1308
- * Returns the new pending state (so the host can enqueue the turn),
1309
- * or null if there was nothing deferred.
1310
- */
1311
- activateDeferred(
1312
- generateRequestId: () => string
1313
- ): ContinuationPending<TConnection> | null;
1314
- }
1315
- //#endregion
1316
- //#region src/chat/pre-stream-turns.d.ts
1317
- declare class PreStreamTurns<
1318
- TConnection extends ChatConnection = ChatConnection
1319
- > {
1320
- /**
1321
- * Accepted-but-not-yet-streamed request ids. A turn enters on `begin()` and
1322
- * leaves on `settle()`; the set being non-empty means "pre-stream work is in
1323
- * flight", which gates parking and the eventual `resume_none` release.
1324
- */
1325
- private readonly _accepted;
1326
- /** Connections parked waiting for a stream to start. */
1327
- readonly awaitingConnections: Map<string, TConnection>;
1328
- /** The most recently accepted pre-stream request id (for the keep-waiting frame). */
1329
- private _latestRequestId;
1330
- /** Mark a freshly-accepted turn as in flight (pre-stream). */
1331
- begin(requestId: string): void;
1332
- /**
1333
- * Mark an accepted turn as settled. Returns `true` when no accepted turn
1334
- * remains in flight (the caller should release parked connections if no
1335
- * stream is active).
1336
- */
1337
- settle(requestId: string): boolean;
1338
- /** Whether any accepted turn is still pre-stream. */
1339
- hasInFlight(): boolean;
1340
- /** The request id to advertise in the keep-waiting frame, if known. */
1341
- get latestRequestId(): string | null;
1342
- /**
1343
- * Park a reconnecting connection and tell it to keep waiting (so its
1344
- * transport does not resolve `reconnectToStream` early). No-op when nothing
1345
- * is in flight. Parked connections are deliberately NOT added to the host's
1346
- * `pendingResumeConnections` — they must keep receiving any live broadcast —
1347
- * until the host flushes them through `notifyStreamResuming` on stream start.
1348
- */
1349
- park(connection: TConnection, probeId?: string): boolean;
1350
- /** Drop a single connection (e.g. on socket close) without releasing others. */
1351
- release(connectionId: string): void;
1352
- /**
1353
- * A stream has started: hand every parked connection to `notify` (the host's
1354
- * `notifyStreamResuming`, which sends `STREAM_RESUMING` and excludes the
1355
- * connection from live broadcast until it ACKs), then clear the awaiting map.
1356
- * The accepted set is untouched — the turn is still running.
1357
- */
1358
- flushOnStreamStart(notify: (connection: TConnection) => void): void;
1359
- /**
1360
- * Release every parked connection with `STREAM_RESUME_NONE` (the turn settled
1361
- * without ever starting a stream) and clear the awaiting map. Safe to call
1362
- * when the map is empty (no-op), so the host can call it liberally from a
1363
- * turn-settle path.
1364
- */
1365
- releaseAwaiting(): void;
1366
- /** Drop all state (chat clear / destroy). Does not send any frames. */
1367
- reset(): void;
1368
- }
1369
- //#endregion
1370
- //#region src/chat/auto-continuation-controller.d.ts
1386
+ declare const CHAT_RECOVERY_INCIDENT_TTL_MS: number;
1387
+ /** Max keys per Durable Object KV `delete([...])` call. */
1388
+ declare const KV_DELETE_MAX_KEYS = 128;
1371
1389
  /**
1372
- * The data a host supplies to schedule (or re-target) a pending/deferred
1373
- * auto-continuation. Mirrors the fields a host writes onto
1374
- * {@link ContinuationState.pending} — the host owns where the values come from
1375
- * (e.g. Think hardcodes a fixed `errorPrefix` and `body: undefined`; ai-chat
1376
- * threads them per tool-result event).
1390
+ * PRIMARY recovery bound (#1637): seal an incident that has made no forward
1391
+ * progress for this long. Keyed to `lastProgressAt`, which resets on every
1392
+ * progress-bearing attempt — so a turn that keeps producing content survives
1393
+ * deploy churn indefinitely, while a genuinely stuck turn dies within 5 min.
1377
1394
  */
1378
- interface ContinuationSpec<
1379
- TConnection extends ContinuationConnection = ContinuationConnection
1380
- > {
1381
- connection: TConnection;
1382
- clientTools: ClientToolSchema[] | undefined;
1383
- body: Record<string, unknown> | undefined;
1384
- errorPrefix: string;
1385
- }
1395
+ declare const DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS: number;
1386
1396
  /**
1387
- * Host substrate the controller parameterizes over. Implemented by the agent
1388
- * (typically via a small adapter object capturing `this`).
1397
+ * Alarm debounce: recovery alarms bunched within this window collapse into a
1398
+ * single attempt. A deploy rollout drops/reconnects the socket several times
1399
+ * over ~11–22s; without this, one logical deploy would burn several attempts.
1389
1400
  */
1390
- interface AutoContinuationHost<
1391
- TConnection extends ContinuationConnection = ContinuationConnection
1392
- > {
1393
- /** Shared continuation state (pending/deferred/awaiting connections). */
1394
- readonly continuation: ContinuationState<TConnection>;
1395
- /** Generate a request id for a freshly-created continuation turn. */
1396
- generateRequestId(): string;
1397
- /**
1398
- * `true` while an assistant turn is streaming — the parallel tool batch can
1399
- * still grow with tool calls the model hasn't emitted yet, so no completeness
1400
- * check is meaningful. (`_streamingAssistant !== null` in Think;
1401
- * `_streamingTurnActive` in ai-chat.)
1402
- */
1403
- isStreamActive(): boolean;
1404
- /** `true` while a tool-result/approval apply is in flight. */
1405
- hasPendingInteraction(): boolean;
1406
- /**
1407
- * `true` when the latest assistant message is mid-batch (a settled tool
1408
- * result beside an unanswered tool call/approval — the #1649 signature).
1409
- */
1410
- hasIncompleteToolBatch(): boolean;
1411
- /**
1412
- * Drain every in-flight tool-result/approval apply (including any enqueued
1413
- * while draining) so the subsequent completeness re-check sees every result
1414
- * that has already arrived. Bounded by real apply activity, never a timer.
1415
- */
1416
- drainInteractionApplies(): Promise<void>;
1417
- /** Hold the isolate alive for the duration of `fn` (alarm heartbeats). */
1418
- keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>;
1419
- /**
1420
- * Run the continuation turn for the current {@link ContinuationState.pending}.
1421
- * Each host's inference/reply pipeline (Think: `_turnQueue.enqueue` +
1422
- * `_runInferenceLoop`; ai-chat: `_runExclusiveChatTurn` + `onChatMessage`).
1423
- * Reads everything it needs from `continuation.pending`, so it takes no args.
1424
- */
1425
- fire(): void;
1426
- }
1427
- declare class AutoContinuationController<
1428
- TConnection extends ContinuationConnection = ContinuationConnection
1429
- > {
1430
- private readonly host;
1431
- /**
1432
- * Small debounce window to batch adjacent client-side tool results/approvals
1433
- * into a single server continuation barrier check (#1650).
1434
- */
1435
- static readonly COALESCE_MS = 50;
1436
- /**
1437
- * Coalesce/debounce timer for the event-driven barrier (#1650). Each tool
1438
- * result/approval re-arms it; on fire it runs {@link fireWhenStable}.
1439
- */
1440
- private _timer;
1441
- /**
1442
- * Double-fire guard (#1650). Ensures only one in-flight apply-drain runs;
1443
- * that drain re-checks completeness on completion before firing. A sibling
1444
- * that re-arms the coalesce timer during a drain is absorbed by the
1445
- * in-progress drain rather than starting its own.
1446
- */
1447
- private _barrierActive;
1448
- constructor(host: AutoContinuationHost<TConnection>);
1449
- /**
1450
- * Schedule an auto-continuation for a tool result/approval that opted in with
1451
- * `autoContinue` (#1650). Coalesces rapid sibling results into a single
1452
- * continuation via the debounce timer; the actual fire is gated by
1453
- * {@link fireWhenStable}. If a continuation is already running
1454
- * (`pastCoalesce`), the new result is stored as the deferred follow-up
1455
- * instead of re-arming.
1456
- */
1457
- schedule(spec: ContinuationSpec<TConnection>): void;
1458
- /**
1459
- * Re-arm the barrier for a result/approval that arrived WITHOUT `autoContinue`
1460
- * (#1650). A standalone errored result declines to continue on its own, but in
1461
- * a parallel batch a SIBLING may already have opted in — and this result can
1462
- * be the one that completes the batch, so we must re-run the barrier check.
1463
- * Unlike {@link schedule} this NEVER creates a pending continuation, and
1464
- * no-ops once the continuation is running (`pastCoalesce`).
1465
- */
1466
- rearmForBatch(): void;
1467
- /** (Re)arm the coalesce timer; on fire, run {@link fireWhenStable}. */
1468
- armTimer(): void;
1469
- /**
1470
- * Fire an auto-continuation, but only once the model's parallel tool-call
1471
- * batch is fully answered (#1649) and no assistant turn is mid-stream (#1650).
1472
- * The barrier is event-driven with NO orphan timeout: when the batch is still
1473
- * incomplete we drain the in-flight applies, re-check, and — if still
1474
- * incomplete — return WITHOUT firing and WITHOUT holding the isolate, leaving
1475
- * `continuation.pending` in place. The next sibling's result re-arms the
1476
- * coalesce timer and re-runs this check; the continuation fires once the final
1477
- * sibling lands. A true orphan (a sibling that never arrives) simply never
1478
- * auto-continues — a later user turn / chat recovery repairs the transcript.
1479
- */
1480
- fireWhenStable(): void;
1481
- /**
1482
- * Transition the deferred follow-up (stored while a continuation was running)
1483
- * to pending and re-run the barrier — its batch may still be incomplete (or a
1484
- * stream active), in which case it parks and re-arms instead of firing blind.
1485
- */
1486
- activateDeferredAndReschedule(): void;
1487
- /**
1488
- * Cancel any still-armed coalesce timer. Called on the fire path so a sibling
1489
- * result that re-armed it during a barrier wait can't fire a duplicate
1490
- * continuation after this one starts (#1649 / #1650).
1491
- */
1492
- cancelTimer(): void;
1493
- /**
1494
- * `true` when the barrier is going to fire on its own — its coalesce timer is
1495
- * still pending or its completeness drain is in progress. The host combines
1496
- * this with its own pending/`pastCoalesce` checks to decide idle/stable.
1497
- */
1498
- isArmed(): boolean;
1499
- /**
1500
- * Tear down the controller-owned barrier state (timer + double-fire guard).
1501
- * Scoped to ONLY this controller's fields — the host clears the rest of its
1502
- * turn state (stream gate, interaction tail, continuation data) separately.
1503
- */
1504
- reset(): void;
1505
- }
1506
- //#endregion
1507
- //#region src/chat/abort-registry.d.ts
1401
+ declare const CHAT_RECOVERY_ALARM_DEBOUNCE_MS: number;
1508
1402
  /**
1509
- * AbortRegistry — manages per-request AbortControllers.
1510
- *
1511
- * Shared between AIChatAgent and Think for chat turn cancellation.
1512
- * Each request gets its own AbortController keyed by request ID.
1513
- * Controllers are created lazily on first signal access.
1514
- */
1515
- declare class AbortRegistry {
1516
- private controllers;
1517
- /**
1518
- * Get or create an AbortController for the given ID and return its signal.
1519
- * Creates the controller lazily on first access.
1520
- */
1521
- getSignal(id: string): AbortSignal | undefined;
1522
- /**
1523
- * Get the signal for an existing controller without creating one.
1524
- * Returns undefined if no controller exists for this ID.
1525
- */
1526
- getExistingSignal(id: string): AbortSignal | undefined;
1527
- /**
1528
- * Cancel a specific request by aborting its controller. Optionally
1529
- * propagate a reason — surfaces as `signal.reason` on the registry's
1530
- * controller and through any `AbortError` it produces downstream.
1531
- */
1532
- cancel(id: string, reason?: unknown): void;
1533
- /** Remove a controller after the request completes. */
1534
- remove(id: string): void;
1535
- /**
1536
- * Abort all pending requests and clear the registry. Optionally propagate a
1537
- * reason — surfaces as `signal.reason` on each controller and through any
1538
- * `AbortError` it produces downstream, exactly like {@link cancel}.
1539
- */
1540
- destroyAll(reason?: unknown): void;
1541
- /** Check if a controller exists for the given ID. */
1542
- has(id: string): boolean;
1543
- /** Number of tracked controllers. */
1544
- get size(): number;
1545
- /**
1546
- * Link an external `AbortSignal` to the controller for `id`. When the
1547
- * external signal aborts, the registry's controller is cancelled —
1548
- * propagating the abort reason — exactly the same way an internal
1549
- * cancel would (e.g. via a `chat-request-cancel` WebSocket message).
1550
- *
1551
- * This is the integration point for callers that drive a chat turn
1552
- * programmatically and want to cancel it from outside without knowing
1553
- * the internally-generated request id (e.g. the helper-as-sub-agent
1554
- * pattern, where a parent's `AbortSignal` from the AI SDK tool
1555
- * `execute` needs to land inside a `Think.saveMessages` call running
1556
- * on a child DO).
1557
- *
1558
- * Behavior:
1559
- *
1560
- * - Passing `undefined` is a no-op and returns a no-op detacher, so
1561
- * callers can unconditionally call this with `options?.signal`.
1562
- * - If the external signal is already aborted, the registry's
1563
- * controller is created (if needed) and cancelled synchronously.
1564
- * - Otherwise a one-shot `abort` listener is attached. The returned
1565
- * function detaches it.
1566
- *
1567
- * **Always call the returned detacher in a `finally` block** — the
1568
- * external signal may outlive the request (a parent chat turn that
1569
- * drives many helper turns reuses one signal across all of them) and
1570
- * leaving listeners attached pins closures and grows the listener
1571
- * list on each turn.
1572
- *
1573
- * @returns A detacher function. Call it after the request finishes
1574
- * (success or failure) to remove the abort listener from `signal`.
1575
- */
1576
- linkExternal(id: string, signal: AbortSignal | undefined): () => void;
1577
- }
1578
- //#endregion
1579
- //#region src/chat/async-helpers.d.ts
1580
- /**
1581
- * @internal Small async control-flow helpers shared by the chat hosts
1582
- * (`@cloudflare/ai-chat` and `@cloudflare/think`) — not a public API. Extracted
1583
- * so the host idle/stable waits and the interaction-apply completeness drain
1584
- * stay byte-identical across both. See `design/chat-shared-layer.md`.
1403
+ * Staleness bound for the live "recovering…" flag (#1620). A flag older than
1404
+ * this is treated as abandoned so it can neither pin the indicator on forever
1405
+ * nor suppress a genuinely-new recovering signal. NOT a recovery budget.
1585
1406
  */
1407
+ declare const CHAT_RECOVERING_FLAG_TTL_MS: number;
1586
1408
  /**
1587
- * Sentinel returned by {@link awaitWithDeadline} when the deadline elapses
1588
- * before the awaited promise settles. A single shared symbol so both hosts
1589
- * compare against the same identity.
1409
+ * Resolve a raw `chatRecovery` config field into the fully-defaulted form the
1410
+ * engine reasons about. Durable recovery is always enabled; a legacy runtime
1411
+ * `false` value from previously compiled JavaScript safely receives defaults.
1590
1412
  */
1591
- declare const TIMED_OUT: unique symbol;
1413
+ declare function resolveChatRecoveryConfig(
1414
+ raw: ChatRecoveryConfig | undefined
1415
+ ): ResolvedChatRecoveryConfig;
1592
1416
  /**
1593
- * Await `promise`, but give up and resolve to {@link TIMED_OUT} once `deadline`
1594
- * (an absolute `Date.now()` ms timestamp) passes. A `null` deadline waits
1595
- * indefinitely (the promise is returned unchanged). The timeout timer is always
1596
- * cleared so it can't pin the isolate awake past resolution.
1417
+ * Sweep recovery incidents inactive past the TTL from durable storage. Lists by
1418
+ * the incident key prefix, selects stale keys (`selectStaleIncidentKeys`), and
1419
+ * batch-deletes them — the DO KV `delete([...])` accepts up to
1420
+ * `KV_DELETE_MAX_KEYS` per call, collapsing N awaited round-trips into
1421
+ * ceil(N / 128). Shared by `AIChatAgent` and `Think` so the sweep policy lives in
1422
+ * one place. See `design/rfc-chat-recovery-foundation.md`.
1597
1423
  */
1598
- declare function awaitWithDeadline<T>(
1599
- promise: Promise<T>,
1600
- deadline: number | null
1601
- ): Promise<T | typeof TIMED_OUT>;
1424
+ declare function sweepStaleChatRecoveryIncidents(
1425
+ storage: Pick<DurableObjectStorage, "list" | "delete">,
1426
+ now: number
1427
+ ): Promise<void>;
1602
1428
  /**
1603
- * Drain the host's interaction-apply chain so a subsequent completeness check
1604
- * (e.g. `hasIncompleteToolBatch`) sees every tool result that has ALREADY
1605
- * arrived.
1606
- *
1607
- * Bounded by real apply activity (a storage write each), never a fixed timer:
1608
- * `getTail` is re-read after every await because a sibling can extend the tail
1609
- * mid-drain, and the loop stops once the tail stops advancing. Bails early when
1610
- * `hasPending()` goes false (the pending continuation was cleared by a chat
1611
- * clear / turn reset) so a stale drain can't hold the isolate awake.
1429
+ * List the persisted recovery incidents that are still live (status
1430
+ * `detected` / `scheduled` / `attempting`) — i.e. NOT yet terminalized
1431
+ * (`exhausted` / `failed`). Used by the alarm-boundary OOM circuit breaker
1432
+ * (#1825) to find the incident(s) it must seal when the in-DO budgets could not.
1433
+ * Lists by the incident key prefix so the storage layout stays encapsulated.
1612
1434
  */
1613
- declare function drainInteractionApplies(
1614
- hasPending: () => boolean,
1615
- getTail: () => Promise<unknown>
1616
- ): Promise<void>;
1617
- //#endregion
1618
- //#region src/chat/tool-state.d.ts
1435
+ declare function listActiveChatRecoveryIncidents(
1436
+ storage: Pick<DurableObjectStorage, "list">
1437
+ ): Promise<
1438
+ {
1439
+ key: string;
1440
+ incident: ChatRecoveryIncident;
1441
+ }[]
1442
+ >;
1619
1443
  /**
1620
- * Tool State — shared update builders and applicator for tool part state changes.
1621
- *
1622
- * Used by both AIChatAgent and Think to apply tool results and approvals
1623
- * to message parts. Each agent handles find-message, persist, and broadcast
1624
- * in their own way; this module provides the state matching and update logic.
1444
+ * Summarize a child agent's persisted recovery incidents for the parent's
1445
+ * agent-tool reattach decision: `"in-progress"` if any incident is still live
1446
+ * (detected/scheduled/attempting), else `"failed"` if any terminalized
1447
+ * (exhausted/failed), else `"none"`. In-progress takes precedence so a parent
1448
+ * never gives up on a child that is still recovering. Shared by `AIChatAgent`
1449
+ * and `Think`. See `design/rfc-chat-recovery-foundation.md`.
1625
1450
  */
1451
+ declare function classifyAgentToolChildRecovery(
1452
+ storage: Pick<DurableObjectStorage, "list">
1453
+ ): Promise<"in-progress" | "failed" | "none">;
1626
1454
  /**
1627
- * Describes an update to apply to a tool part.
1455
+ * Read the durable monotonic recovery-progress counter (0 when unset). The value
1456
+ * feeds the no-progress budget decision; shared by `AIChatAgent` and `Think`.
1628
1457
  */
1629
- type ToolPartUpdate = {
1630
- toolCallId: string;
1631
- matchStates: string[];
1632
- apply: (part: Record<string, unknown>) => Record<string, unknown>;
1633
- };
1458
+ declare function readChatRecoveryProgress(
1459
+ storage: Pick<DurableObjectStorage, "get">
1460
+ ): Promise<number>;
1634
1461
  /**
1635
- * Apply a tool part update to a parts array.
1636
- * Finds the first part matching `update.toolCallId` in one of `update.matchStates`,
1637
- * applies the update immutably, and returns the new parts array with the index.
1462
+ * Advance the KV progress counter by one.
1638
1463
  *
1639
- * Returns `null` if no matching part was found.
1464
+ * @deprecated Hosts no longer bump a counter per credited chunk: the marker
1465
+ * is derived from the stream log (`ResumableStream.progressMarker`) and
1466
+ * explicit credits go through `ResumableStream.creditProgress`. Kept for
1467
+ * code that still maintains the KV counter; a value written here is folded
1468
+ * into the derived marker on the next seed.
1640
1469
  */
1641
- declare function applyToolUpdate(
1642
- parts: Array<Record<string, unknown>>,
1643
- update: ToolPartUpdate
1644
- ): {
1645
- parts: Array<Record<string, unknown>>;
1646
- index: number;
1647
- } | null;
1470
+ declare function bumpChatRecoveryProgress(
1471
+ storage: Pick<DurableObjectStorage, "get" | "put">
1472
+ ): Promise<void>;
1648
1473
  /**
1649
- * Build an update descriptor for applying a tool result.
1650
- *
1651
- * Matches parts in `input-available`, `approval-requested`, or `approval-responded` state.
1652
- * Sets state to `output-available` (with output) or `output-error` (with errorText).
1474
+ * Throttle window for crediting a parent turn's recovery progress from forwarded
1475
+ * sub-agent (agent-tool) stream chunks (N9). Forwarding a child's chunks IS
1476
+ * forward progress for the parent, but the credit must not write storage per
1477
+ * token.
1653
1478
  */
1654
- declare function toolResultUpdate(
1655
- toolCallId: string,
1656
- output: unknown,
1657
- overrideState?: "output-error",
1658
- errorText?: string
1659
- ): ToolPartUpdate;
1479
+ declare const AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS = 5000;
1660
1480
  /**
1661
- * Build an update descriptor for a terminal tool result that belongs to a
1662
- * tool part in a *different* (earlier) assistant message than the one
1663
- * currently being streamed.
1664
- *
1665
- * This is the "cross-message" case: an approved server tool executes during a
1666
- * continuation stream, but its tool part lives in the assistant message that
1667
- * originally requested it. `StreamAccumulator` surfaces this as a
1668
- * `cross-message-tool-update` action because the accumulator only owns the
1669
- * current turn's new content and cannot mutate a part from a prior message.
1670
- *
1671
- * Compared to {@link toolResultUpdate} this builder is deliberately more
1672
- * defensive, mirroring the equivalent fallback in `@cloudflare/ai-chat`:
1673
- *
1674
- * - It matches the broad set of pre-terminal **and** terminal states, so a
1675
- * provider that replays the entire prior tool round-trip during a
1676
- * continuation (notably the OpenAI Responses API — issue #1404) still
1677
- * resolves to the same part instead of silently missing it.
1678
- * - It is **first-write-wins**: a chunk arriving for a tool that already holds
1679
- * a terminal result is treated as a replay and the existing output is never
1680
- * overwritten. In that case `apply` returns the *same part reference*, which
1681
- * callers use as an idempotent-no-op signal to skip the durable write and a
1682
- * redundant `MESSAGE_UPDATED` broadcast.
1683
- * - It preserves a streamed `preliminary` flag when one is present, otherwise
1684
- * marks the result final (`preliminary: false`).
1481
+ * Per-isolate throttle gate for agent-tool stream-progress crediting (N9). The
1482
+ * `_lastBumpAt` clock is in-memory, so it resets per isolate and the first
1483
+ * forwarded chunk after a restart always credits. `shouldCredit(now)` returns
1484
+ * `true` at most once per `AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS` window and
1485
+ * records the time on each credit. Shared by `AIChatAgent` and `Think`.
1685
1486
  */
1686
- declare function crossMessageToolResultUpdate(
1687
- toolCallId: string,
1688
- updateType: "output-available" | "output-error",
1689
- output?: unknown,
1690
- errorText?: string,
1691
- preliminary?: boolean
1692
- ): ToolPartUpdate;
1487
+ declare class AgentToolStreamProgressThrottle {
1488
+ private _lastBumpAt;
1489
+ shouldCredit(now: number): boolean;
1490
+ }
1693
1491
  /**
1694
- * Build an update descriptor that replaces the output of a *paused durable
1695
- * execution* tool part (e.g. a codemode runtime tool that paused for
1696
- * approval).
1697
- *
1698
- * A paused execution completes its tool call normally — the part is already
1699
- * `output-available` with an output of `{ status: "paused", executionId }`.
1700
- * When the host later approves/rejects the execution, the new outcome
1701
- * (completed / rejected / paused-again) must replace that output in place.
1702
- *
1703
- * Matching is deliberately narrow and idempotent:
1704
- *
1705
- * - only `output-available` parts are considered;
1706
- * - the existing output must be a paused-execution object carrying the same
1707
- * `executionId` — anything else (already replaced, different execution)
1708
- * returns the *same part reference*, which callers treat as a no-op signal
1709
- * (skip persist + broadcast), mirroring {@link crossMessageToolResultUpdate}.
1492
+ * Throttle window for crediting recovery progress from mid-segment streaming
1493
+ * content (text/reasoning/tool-input deltas). A milestone chunk credits
1494
+ * unconditionally; deltas credit at most once per window so a long single
1495
+ * segment registers forward progress across crashes without writing storage per
1496
+ * token. 5s is far finer than the 300s no-progress budget, so any crash gap
1497
+ * longer than this window over an actively-streaming segment still credits.
1710
1498
  */
1711
- declare function pausedExecutionUpdate(
1712
- toolCallId: string,
1713
- executionId: string,
1714
- output: unknown
1715
- ): ToolPartUpdate;
1499
+ declare const CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS = 5000;
1716
1500
  /**
1717
- * Build an update descriptor for applying a tool approval.
1718
- *
1719
- * Matches parts in `input-available` or `approval-requested` state.
1720
- * Sets state to `approval-responded` (if approved) or `output-denied` (if denied).
1501
+ * Per-isolate throttle gate for crediting recovery progress from mid-segment
1502
+ * streaming-content chunks — the delta arm of {@link shouldCreditStreamProgress}.
1503
+ * The `_lastBumpAt` clock is in-memory, so it resets per isolate and the first
1504
+ * delta after a restart always credits. Shared by `AIChatAgent` and `Think`.
1721
1505
  */
1722
- declare function toolApprovalUpdate(
1723
- toolCallId: string,
1724
- approved: boolean
1725
- ): ToolPartUpdate;
1726
- /** A minimal message shape for the leaf tool/interaction scans. */
1727
- type ToolBatchMessage = {
1728
- role: string;
1729
- parts: ReadonlyArray<unknown>;
1506
+ declare class StreamProgressCreditThrottle {
1507
+ private _lastBumpAt;
1508
+ shouldCredit(now: number): boolean;
1509
+ }
1510
+ /** Durable record of the last turn that ended in a terminal error (#1645). */
1511
+ type ChatTerminalRecord = {
1512
+ requestId: string;
1513
+ body: string;
1730
1514
  };
1731
- /** Extract a tool part's name from its `tool-<name>` / `dynamic-tool` shape. */
1732
1515
  /**
1733
- * Whether a part is still awaiting a CLIENT interaction that can genuinely
1734
- * arrive after a restart: an `approval-requested` part (a reconnecting client
1735
- * replays the approval) or an `input-available` part for a CLIENT tool (the SPA
1736
- * replays the `tool-result`). A SERVER tool's `input-available` is NOT pending —
1737
- * its `execute()` died with the isolate.
1516
+ * Persist a durable record of the last terminal turn so a client that
1517
+ * (re)connects after the turn ended still learns its outcome (#1645). Kept
1518
+ * until a later turn supersedes it ({@link clearChatTerminal}); a single record
1519
+ * is sufficient because only the most recent terminal is relevant.
1738
1520
  */
1739
- declare function partAwaitsClientInteraction(
1740
- part: unknown,
1741
- clientResolvable: Set<string>
1742
- ): boolean;
1743
- /**
1744
- * Names of the CLIENT-resolvable tools — the client-provided schemas from the
1745
- * last request, which have no server `execute`. An interrupted `input-available`
1746
- * part for one of these can still be resolved by the client replaying a
1747
- * `tool-result`; a server tool's cannot.
1748
- */
1749
- declare function clientResolvableToolNames(
1750
- tools:
1751
- | ReadonlyArray<
1752
- | {
1753
- name?: string;
1754
- }
1755
- | null
1756
- | undefined
1757
- >
1758
- | undefined
1759
- ): Set<string>;
1760
- /**
1761
- * `true` when the latest assistant message is mid-batch: it carries at least
1762
- * one settled tool result AND at least one tool call/approval still awaiting a
1763
- * client result. That is the #1649 signature — the model fanned out parallel
1764
- * tool calls and only some have been answered. Scoped to the leaf (the step the
1765
- * continuation answers) so an unrelated dangling tool in an earlier message
1766
- * doesn't block a legitimate follow-up continuation.
1767
- */
1768
- declare function hasIncompleteToolBatch(
1769
- messages: ReadonlyArray<ToolBatchMessage>
1770
- ): boolean;
1771
- //#endregion
1772
- //#region src/chat/parse-protocol.d.ts
1521
+ declare function recordChatTerminal(
1522
+ storage: Pick<DurableObjectStorage, "put">,
1523
+ requestId: string,
1524
+ body: string
1525
+ ): Promise<void>;
1526
+ /** Clear the durable terminal record once a later turn supersedes it (#1645). */
1527
+ declare function clearChatTerminal(
1528
+ storage: Pick<DurableObjectStorage, "delete">
1529
+ ): Promise<void>;
1530
+ /** Read the pending terminal record, or `null` if none is stored (#1645). */
1531
+ declare function pendingChatTerminal(
1532
+ storage: Pick<DurableObjectStorage, "get">
1533
+ ): Promise<ChatTerminalRecord | null>;
1773
1534
  /**
1774
- * Protocol Message Parser — typed parsing of cf_agent_chat_* WebSocket messages.
1775
- *
1776
- * Parses raw WebSocket messages into a discriminated union of protocol events.
1777
- * Both AIChatAgent and Think can use this instead of manual JSON.parse + type checking.
1535
+ * Build the on-connect "recovering…" replay frame (#1620), or `null` when no
1536
+ * (non-stale) recovery is in progress. A client that connects between recovery
1537
+ * attempts (no active stream) reads the turn as working rather than frozen. A
1538
+ * record older than the flag TTL is treated as abandoned (its terminal-clear
1539
+ * never ran) and skipped, so a dead recovery can't show "recovering…" forever.
1540
+ * `messageType` is the package's recovering wire-type enum.
1778
1541
  */
1542
+ declare function buildChatRecoveringFrame(
1543
+ storage: Pick<DurableObjectStorage, "get">,
1544
+ messageType: string,
1545
+ now: number
1546
+ ): Promise<Record<string, unknown> | null>;
1779
1547
  /**
1780
- * Discriminated union of all incoming chat protocol events.
1781
- *
1782
- * Each agent handles the events it cares about and ignores the rest.
1783
- * Returns `null` for non-JSON messages or unrecognized types.
1548
+ * Set or clear the live "recovering…" status (#1620). Persists a durable record
1549
+ * (so set/clear stay consistent across the isolates a recovery spans) and
1550
+ * broadcasts a recovering frame — but only on a genuine transition, so a
1551
+ * deploy/reconnect storm (which re-detects recovery many times) doesn't spam
1552
+ * the wire. A flag older than the TTL is stale: the owning incident was
1553
+ * abandoned without a terminal (e.g. the DO went idle before recovery could
1554
+ * resolve), so it is treated as not-recovering and can neither pin the
1555
+ * indicator on forever nor suppress a genuinely-new recovering signal.
1556
+ * `messageType` is the package's recovering wire-type enum; `broadcast` is the
1557
+ * package's chat-broadcast wrapper.
1784
1558
  */
1785
- type ChatProtocolEvent =
1786
- | {
1787
- type: "chat-request";
1788
- id: string;
1789
- init: {
1790
- method?: string;
1791
- body?: string;
1792
- [key: string]: unknown;
1793
- };
1794
- }
1795
- | {
1796
- type: "clear";
1797
- }
1798
- | {
1799
- type: "cancel";
1800
- id: string;
1801
- }
1802
- | {
1803
- type: "tool-result";
1804
- toolCallId: string;
1805
- toolName: string;
1806
- output: unknown;
1807
- state?: string;
1808
- errorText?: string;
1809
- autoContinue?: boolean;
1810
- clientTools?: Array<{
1811
- name: string;
1812
- description?: string;
1813
- parameters?: unknown;
1814
- }>;
1815
- }
1816
- | {
1817
- type: "tool-approval";
1818
- toolCallId: string;
1819
- approved: boolean;
1820
- autoContinue?: boolean;
1821
- }
1822
- | {
1823
- type: "stream-resume-request";
1824
- probeId?: string;
1825
- }
1826
- | {
1827
- type: "stream-resume-ack";
1828
- id: string;
1829
- }
1830
- | {
1831
- type: "messages";
1832
- messages: unknown[];
1833
- };
1559
+ declare function setChatRecovering(
1560
+ active: boolean,
1561
+ requestId: string | undefined,
1562
+ deps: {
1563
+ storage: Pick<DurableObjectStorage, "get" | "put" | "delete">;
1564
+ messageType: string;
1565
+ broadcast: (frame: Record<string, unknown>) => void;
1566
+ now: number;
1567
+ }
1568
+ ): Promise<void>;
1834
1569
  /**
1835
- * Parse a raw WebSocket message string into a typed protocol event.
1836
- *
1837
- * Returns `null` if the message is not valid JSON or not a recognized
1838
- * protocol message type. Callers should fall through to the user's
1839
- * `onMessage` handler when `null` is returned.
1840
- *
1841
- * @example
1842
- * ```typescript
1843
- * const event = parseProtocolMessage(rawMessage);
1844
- * if (!event) return userOnMessage(connection, rawMessage);
1845
- *
1846
- * switch (event.type) {
1847
- * case "chat-request": { ... }
1848
- * case "clear": { ... }
1849
- * case "tool-result": { ... }
1850
- * }
1851
- * ```
1570
+ * Observability event produced by an incident evaluation or a status
1571
+ * transition, emitted by the caller. The `detected`/`attempt` events come from
1572
+ * the budget evaluation (begin path); the `scheduled` event comes from
1573
+ * `ChatRecoveryEngine.scheduleRecovery`; the `completed`/`skipped`/`failed`
1574
+ * events come from `ChatRecoveryEngine.updateIncident`. `reason` is carried only
1575
+ * by the `skipped`/`failed` transitions that record a cause.
1852
1576
  */
1853
- declare function parseProtocolMessage(raw: string): ChatProtocolEvent | null;
1577
+ type ChatRecoveryIncidentEvent = {
1578
+ type:
1579
+ | "chat:recovery:detected"
1580
+ | "chat:recovery:attempt"
1581
+ | "chat:recovery:scheduled"
1582
+ | "chat:recovery:completed"
1583
+ | "chat:recovery:skipped"
1584
+ | "chat:recovery:failed";
1585
+ incidentId: string;
1586
+ requestId: string;
1587
+ attempt: number;
1588
+ maxAttempts: number;
1589
+ recoveryKind: ChatRecoveryKind;
1590
+ reason?: string;
1591
+ };
1592
+ type EvaluateChatRecoveryIncidentInput = {
1593
+ /** Recovery identity for this turn. */ identity: {
1594
+ requestId: string;
1595
+ recoveryRootRequestId?: string | null;
1596
+ latestUserMessageId?: string | null;
1597
+ recoveryKind: ChatRecoveryKind;
1598
+ } /** Fully-resolved recovery config. */;
1599
+ config: ResolvedChatRecoveryConfig /** The existing incident for this identity, or `null` if this is fresh. */;
1600
+ existing: ChatRecoveryIncident | null /** Current value of the durable monotonic progress counter. */;
1601
+ currentProgress: number;
1602
+ /**
1603
+ * Whether the turn is parked on a pending CLIENT interaction (an
1604
+ * `input-available` client-tool part or an `approval-requested` part). Such a
1605
+ * turn is waiting on the human, not stuck, so it is budget-free.
1606
+ */
1607
+ awaitingClientInteraction: boolean /** Injected clock (epoch ms) for deterministic tests. */;
1608
+ now: number;
1609
+ /**
1610
+ * Invoked when `config.shouldKeepRecovering` throws. Lets each package keep
1611
+ * its own log prefix. A throwing predicate is treated as "keep recovering".
1612
+ */
1613
+ onShouldKeepRecoveringError?: (error: unknown) => void;
1614
+ };
1615
+ type EvaluateChatRecoveryIncidentResult = {
1616
+ /** The next incident record to persist. */ incident: ChatRecoveryIncident /** Whether this incident is now sealed as exhausted. */;
1617
+ exhausted: boolean /** Observability events to emit, in order. */;
1618
+ events: ChatRecoveryIncidentEvent[];
1619
+ };
1854
1620
  //#endregion
1855
- //#region src/chat/message-reconciler.d.ts
1621
+ //#region src/chat/recovery-engine.d.ts
1622
+ /** The scheduled-callback entrypoints a recovery schedule can target. */
1623
+ type ChatRecoveryScheduleCallback =
1624
+ | "_chatRecoveryContinue"
1625
+ | "_chatRecoveryRetry";
1856
1626
  /**
1857
- * Reconcile incoming client messages against server state.
1627
+ * Why a recovery callback is being scheduled. The idempotency of the
1628
+ * underlying Task run's `idempotencyKey` depends ONLY on this:
1858
1629
  *
1859
- * 1. Merges server-known tool outputs into incoming messages that still
1860
- * show stale states (input-available, approval-requested, approval-responded)
1861
- * 2. Reconciles assistant IDs: exact match → content-key match → toolCallId match
1630
+ * - `"initial"` — the first schedule of a continuation/retry when an interrupted
1631
+ * turn is detected on wake. A deploy rollout drops/reconnects the socket
1632
+ * several times, re-triggering detection; idempotent scheduling (dedup on
1633
+ * callback + payload) collapses that storm into a single enqueued continuation
1634
+ * instead of N duplicates.
1862
1635
  *
1863
- * @param incoming - Messages from the client
1864
- * @param serverMessages - Current server-side messages (source of truth)
1865
- * @param sanitizeForContentKey - Function to sanitize a message before computing
1866
- * its content key (typically strips ephemeral provider metadata)
1867
- * @returns Reconciled messages ready for persistence
1636
+ * - `"stable_timeout_retry"` — a reschedule issued from INSIDE the currently-
1637
+ * executing recovery attempt (a continuation that timed out waiting for
1638
+ * stable state). That attempt — a `__cf_internal_chat_recovery` Task run —
1639
+ * settles only AFTER the callback returns, so an idempotent reschedule would
1640
+ * dedup onto the doomed attempt and settle with it — the retry would never
1641
+ * fire. A fresh (non-idempotent) delayed attempt survives.
1868
1642
  */
1869
- declare function reconcileMessages(
1870
- incoming: UIMessage[],
1871
- serverMessages: readonly UIMessage[],
1872
- sanitizeForContentKey?: (message: UIMessage) => UIMessage
1873
- ): UIMessage[];
1643
+ type ChatRecoveryScheduleReason = "initial" | "stable_timeout_retry";
1874
1644
  /**
1875
- * For a single message, resolve its ID by matching toolCallId against server state.
1876
- * Prevents duplicate DB rows when client IDs differ from server IDs.
1877
- * Tool call IDs are unique per conversation, so matching is safe regardless of state.
1645
+ * A reconstructed orphaned-stream partial. The engine seam is deliberately
1646
+ * **wire-vocabulary-agnostic**: `text` is the accumulated assistant text and
1647
+ * `parts` is OPAQUE to the engine (`unknown[]`) — each host casts it back to its
1648
+ * own message-part vocabulary (AI SDK `UIMessage` parts, AG-UI tool parts, …).
1649
+ * The single fact the engine needs about parts — does the partial carry settled
1650
+ * (non-idempotent) tool work that must survive a `{ persist: false }` recovery
1651
+ * (#1631)? — is precomputed by the {@link ChatRecoveryCodec} as
1652
+ * `hasSettledToolResults`. So the engine never imports a part vocabulary; the
1653
+ * codec owns it (see `partialHasSettledToolResults` in `recovery-codec.ts` for
1654
+ * the AI SDK codec's implementation of that predicate).
1878
1655
  */
1879
- declare function resolveToolMergeId(
1880
- message: UIMessage,
1881
- serverMessages: readonly UIMessage[]
1882
- ): UIMessage;
1883
- /**
1884
- * Merge a freshly-reconstructed orphaned partial onto the assistant message
1885
- * that already owns its target id (the orphan-persist **(c)** step).
1886
- *
1887
- * Used by hosts whose store can hold an assistant row for the SAME id BEFORE
1888
- * the stream finalizes — e.g. an early persist at tool-approval time, or a
1889
- * continuation resuming the prior assistant message. On recovery the engine
1890
- * replays the same chunks, so a naive append would leave two parts per tool
1891
- * call. The merge therefore:
1892
- *
1893
- * - keeps ALL existing parts (the persisted row is authoritative for tool
1894
- * parts that had a client result applied IN PLACE — that result lives only
1895
- * in storage, never in the chunk stream, so a whole-message replace would
1896
- * clobber it);
1897
- * - appends only the reconstructed parts whose `toolCallId` is NOT already
1898
- * present (dedup by tool-call identity);
1899
- * - overlays the incoming metadata onto the existing metadata (incoming wins
1900
- * on conflicts), falling back to whichever side is present.
1901
- *
1902
- * The result carries the INCOMING message's id/role (the caller has already
1903
- * resolved the incoming id to the existing row's id via the (b) target-id
1904
- * step), so it is safe to write straight back through `updateMessage`.
1905
- *
1906
- * Hosts whose orphan persist only ever runs at stream finalize (no early/
1907
- * mid-stream row for the same id) never hit the merge branch and don't need
1908
- * this — a plain append/replace is already dedup-safe because the shared
1909
- * reconstruction (`StreamAccumulator` / `applyChunkToParts`) is idempotent by
1910
- * `toolCallId`.
1911
- */
1912
- declare function reconcileOrphanPartial(
1913
- existing: UIMessage,
1914
- incoming: UIMessage
1915
- ): UIMessage;
1916
- //#endregion
1917
- //#region src/chat/repair-transcript.d.ts
1656
+ type RecoveryPartial = {
1657
+ text: string;
1658
+ parts: unknown[];
1659
+ hasSettledToolResults: boolean;
1660
+ };
1661
+ /** Lifecycle status of a recovered stream's metadata row. */
1662
+ type ChatStreamStatus = "streaming" | "completed" | "error";
1663
+ /** Identity + context for opening (or re-evaluating) a recovery incident. */
1664
+ interface BeginChatRecoveryIncidentInput {
1665
+ requestId: string;
1666
+ recoveryRootRequestId?: string | null;
1667
+ latestUserMessageId?: string | null;
1668
+ recoveryKind: ChatRecoveryKind;
1669
+ /** Test-only clock injection for deterministic debounce/window timing. */
1670
+ nowMs?: number;
1671
+ }
1672
+ interface BeginChatRecoveryIncidentResult {
1673
+ incident: ChatRecoveryIncident;
1674
+ config: ResolvedChatRecoveryConfig;
1675
+ exhausted: boolean;
1676
+ }
1918
1677
  /**
1919
- * Whether a tool part already has a settled result the provider accepts, so it
1920
- * must NOT be re-repaired into an errored result.
1921
- *
1922
- * Single source of truth for the terminal tool states. Mirrors the AI SDK's
1923
- * terminal states: `convertToModelMessages` emits a `tool-result` for
1924
- * `output-available`, `output-error`, AND `output-denied` (a user-denied
1925
- * approval — its denial reason becomes the tool-result). Omitting any of these
1926
- * makes repair re-flip the part every turn — clobbering a real `errorText` /
1927
- * denial with the generic "interrupted" message.
1678
+ * Package-specific host operations the engine drives during incident
1679
+ * orchestration. Every method is a thin pass-through to the package's existing
1680
+ * storage / clock / event / interaction primitives — the engine owns only the
1681
+ * *sequence*, not the I/O.
1928
1682
  */
1929
- declare function toolPartHasSettledResult(
1930
- record: Record<string, unknown>
1931
- ): boolean;
1932
- interface RepairInterruptedToolPartsOptions {
1683
+ interface ChatRecoveryAdapter {
1684
+ /** Resolve the effective recovery config (defaults + caller overrides). */
1685
+ resolveConfig(): ResolvedChatRecoveryConfig;
1686
+ /** Wall clock; only consulted when the input carries no test `nowMs`. */
1687
+ now(): number;
1688
+ /** Evict incidents past the TTL. Runs before the existing-record read. */
1689
+ sweepStaleIncidents(now: number): Promise<void>;
1690
+ /** Read the persisted incident for `key`, or `null` if none. */
1691
+ getIncident(key: string): Promise<ChatRecoveryIncident | null>;
1933
1692
  /**
1934
- * Decide the replacement for an interrupted tool part (no settled result, not
1935
- * `approval-responded`). Its `input` has already been normalized to a valid
1936
- * object. The default host behavior flips it to an errored tool-result; hosts
1937
- * expose this as an overridable `repairInterruptedToolPart` hook so a subclass
1938
- * can, e.g., convert an interrupted client-resolved tool into a text part.
1693
+ * Optional: rehydrate any state the interaction predicate depends on. Invoked
1694
+ * after the existing-incident read and BEFORE `isAwaitingClientInteraction`.
1695
+ * `Think` uses this to restore client tools from durable storage on a cold
1696
+ * boot-recovery wake (so a HITL turn is not misread as stuck); `AIChatAgent`
1697
+ * has no such state and omits it.
1939
1698
  */
1940
- repairPart: (part: UIMessage["parts"][number]) => UIMessage["parts"][number];
1699
+ ensureInteractionStateLoaded?(): void;
1941
1700
  /**
1942
- * Whether a tool part already carries a settled result (defaults to
1943
- * {@link toolPartHasSettledResult}).
1701
+ * Optional: give the package a chance to handle a NON-chat fiber before chat
1702
+ * recovery inspects it. Returns `true` if the package fully consumed the
1703
+ * fiber, in which case the engine tells the caller to skip chat-recovery
1704
+ * processing for it. `Think` uses this for its messenger/workflow reply fibers
1705
+ * (`think:messenger-reply`); `AIChatAgent` has no non-chat fibers and omits it
1706
+ * (the engine then treats every recovered fiber as a chat-recovery candidate).
1707
+ *
1708
+ * Ordering invariant: the engine dispatches this FIRST, before the
1709
+ * chat-fiber-name gate, so a non-chat fiber is never misclassified as an
1710
+ * orphaned chat turn.
1944
1711
  */
1945
- isSettled?: (record: Record<string, unknown>) => boolean;
1712
+ tryHandleNonChatFiberRecovery?(ctx: FiberRecoveryContext$1): Promise<boolean>;
1713
+ /** Monotonic forward-progress marker for the no-progress budget. */
1714
+ readProgress(): Promise<number>;
1946
1715
  /**
1947
- * Normalize a tool part's `input` (defaults to the shared
1948
- * {@link normalizeToolInput}).
1716
+ * Whether the turn is parked on a pending CLIENT interaction (waiting on the
1717
+ * human, not stuck). When true the engine keeps the incident budget-free.
1718
+ * Optional: a host with no client-interaction/HITL substrate (e.g. the pi
1719
+ * fixture) omits it and the engine treats the turn as never parked (`false`).
1949
1720
  */
1950
- normalizeInput?: (input: unknown) => {
1951
- input: unknown;
1952
- changed: boolean;
1953
- };
1721
+ isAwaitingClientInteraction?(): boolean;
1722
+ /** Persist the evaluated incident under `key`. */
1723
+ putIncident(key: string, incident: ChatRecoveryIncident): Promise<void>;
1954
1724
  /**
1955
- * Whether an interrupted tool part (no settled result, not
1956
- * `approval-responded`) should be repaired at all. Defaults to `true` (repair
1957
- * everything, like Think — which converts even client tools via its
1958
- * `repairPart` override). A host whose default `repairPart` errors the part
1959
- * (ai-chat) passes this to SKIP a part still legitimately awaiting a CLIENT
1960
- * interaction (an `input-available` client tool or an `approval-requested`
1961
- * part the user may still answer) so it is left verbatim rather than clobbered
1962
- * with an error. Skipped parts are not counted in `removedToolCalls`.
1725
+ * Delete the incident record under `key`. The engine calls this on the
1726
+ * terminal `completed` transition (a completed recovery is never retried, so
1727
+ * its record is dropped rather than left in storage forever).
1963
1728
  */
1964
- shouldRepair?: (part: UIMessage["parts"][number]) => boolean;
1965
- }
1966
- interface RepairInterruptedToolPartsResult {
1967
- /** A new messages array; unchanged messages keep their original reference. */
1968
- messages: UIMessage[];
1969
- /** Count of interrupted tool calls flipped to a repaired shape. */
1970
- removedToolCalls: number;
1971
- /** Count of tool parts whose malformed `input` was normalized. */
1972
- normalizedInputs: number;
1973
- /** The tool-call ids that were repaired. */
1974
- toolCallIds: string[];
1975
- }
1976
- /**
1977
- * Repair interrupted tool calls and normalize malformed tool inputs across a
1978
- * transcript. Behavior mirrors `@cloudflare/think`'s original
1979
- * `_repairToolTranscriptParts`:
1980
- *
1981
- * - a tool part with NO settled result and state `approval-responded` is kept
1982
- * verbatim (an approved server tool waiting for its continuation to run
1983
- * `execute()` — not abandoned);
1984
- * - a tool part with NO settled result for which `shouldRepair` returns false
1985
- * is kept verbatim (a part still awaiting a CLIENT interaction; see option);
1986
- * - any other tool part with no settled result is normalized then handed to
1987
- * `repairPart` (default: flipped to an errored result);
1988
- * - a tool part WITH a settled result only has its `input` normalized.
1989
- *
1990
- * Messages with no changed part keep their original object reference so callers
1991
- * can cheaply detect what to persist.
1992
- */
1993
- declare function repairInterruptedToolParts(
1994
- messages: UIMessage[],
1995
- options: RepairInterruptedToolPartsOptions
1996
- ): RepairInterruptedToolPartsResult;
1997
- //#endregion
1998
- //#region src/chat/orphan-store.d.ts
1999
- interface OrphanPersistStore<
2000
- M extends {
2001
- id: string;
2002
- } = UIMessage
2003
- > {
2004
- /** Read the stored message with this id, or `null` if none exists. */
2005
- getMessage(id: string): M | null | Promise<M | null>;
1729
+ deleteIncident(key: string): Promise<void>;
1730
+ /** Broadcast a lifecycle event produced by the evaluation or a transition. */
1731
+ emitRecoveryEvent(event: ChatRecoveryIncidentEvent): void;
2006
1732
  /**
2007
- * Append a new message. `parentId` is honored by tree-structured stores
2008
- * (`undefined` → attach to the latest leaf); flat-array stores ignore it.
1733
+ * Enqueue a recovery callback. The engine owns the surrounding orchestration
1734
+ * and the package chooses its durable transport: a Task for a root chat agent,
1735
+ * or the temporary routed-schedule fallback for a dynamic agent. `reason`
1736
+ * selects deduplication and `delaySeconds` is `0` initially or the stable
1737
+ * retry delay for a chained attempt.
2009
1738
  */
2010
- appendMessage(message: M, parentId?: string | null): void | Promise<void>;
2011
- /** Replace the stored message that owns `message.id`. */
2012
- updateMessage(message: M): void | Promise<void>;
2013
- }
2014
- //#endregion
2015
- //#region src/chat/orphan-persist.d.ts
2016
- interface PersistReconstructedOrphanOptions<
2017
- TMessage extends UIMessage = UIMessage
2018
- > {
2019
- /** The store seam to upsert through (a `SessionProvider` write-subset). */
2020
- store: OrphanPersistStore<TMessage>;
1739
+ scheduleRecovery(
1740
+ callback: ChatRecoveryScheduleCallback,
1741
+ data: Record<string, unknown>,
1742
+ reason: ChatRecoveryScheduleReason,
1743
+ delaySeconds: number
1744
+ ): Promise<void>;
2021
1745
  /**
2022
- * Id for the reconstructed message when the stream carried no provider
2023
- * `start.messageId` to adopt. The accumulator still adopts a provider id when
2024
- * present.
1746
+ * Set or clear the live "recovering…" status (#1620). The engine calls this on
1747
+ * the incident transitions: `scheduled` → active (keyed by the recovery-root
1748
+ * request id, falling back to the incident's request id), and
1749
+ * `completed`/`skipped`/`failed` → cleared. The package owns the underlying
1750
+ * staleness / idempotency / broadcast I/O.
2025
1751
  */
2026
- fallbackId: string;
1752
+ setRecovering(active: boolean, requestId?: string): Promise<void>;
2027
1753
  /**
2028
- * Finalize the reconstructed message before upsert — e.g. strip internal
2029
- * parts or resolve the persist-target id. Return `null` to skip persistence
2030
- * entirely (e.g. an empty structural-only message).
1754
+ * Report a throw from the caller's `shouldKeepRecovering` hook. Optional: a
1755
+ * host that does not surface this diagnostic omits it (the engine swallows the
1756
+ * report).
2031
1757
  */
2032
- prepare: (message: TMessage) => TMessage | null;
1758
+ onShouldKeepRecoveringError?(error: unknown): void;
2033
1759
  /**
2034
- * Combine an existing row with the reconstructed message when a row already
2035
- * owns the id (replace, or reconcile partials).
1760
+ * Terminalize a given-up recovery turn: deliver the exhaustion notification
1761
+ * plus the package-owned terminal record / banner / submission writes. A thin
1762
+ * pass-through to the package's `_exhaustChatRecovery` (which composes
1763
+ * {@link runChatRecoveryExhaustion}). Driven by
1764
+ * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}; the engine owns the
1765
+ * surrounding read → re-entry-guard → build → terminalize → seal sequence, the
1766
+ * package owns the terminal writes (uniformly broadcast-first; their set
1767
+ * differs — `Think` also writes a submission row).
2036
1768
  */
2037
- merge: (existing: TMessage, incoming: TMessage) => TMessage;
1769
+ exhaustChatRecovery(
1770
+ incident: ChatRecoveryIncident,
1771
+ config: ResolvedChatRecoveryConfig,
1772
+ partial: RecoveryPartial,
1773
+ streamId: string,
1774
+ createdAt: number
1775
+ ): Promise<void>;
1776
+ /**
1777
+ * Resolve the orphaned stream identity for a (recovery-root) request id —
1778
+ * `streamId` is `""` when no stream metadata survives. Drives BOTH the wake
1779
+ * path (which consumes the full {@link ResolvedRecoveryStream}) and the
1780
+ * give-up path (which reads only `.streamId`). A thin pass-through to the
1781
+ * package's stream-metadata lookup: the newest row keyed by the request id,
1782
+ * else the live active stream.
1783
+ */
1784
+ resolveRecoveryStream(requestId: string): ResolvedRecoveryStream;
1785
+ /** Reconstruct the partial text/parts buffered for `streamId`. */
1786
+ getPartialStreamText(streamId: string): RecoveryPartial;
1787
+ /**
1788
+ * The in-flight recovery-root request id, consulted as a fallback in the
1789
+ * give-up root-id chain when the payload carries no `originalRequestId` /
1790
+ * `recoveredRequestId` and no incident record survives. `undefined` when no
1791
+ * recovery chain is active. (`AIChatAgent` and `Think` both back this with
1792
+ * `_activeChatRecoveryRootRequestId`.)
1793
+ */
1794
+ activeChatRecoveryRootRequestId(): string | undefined;
1795
+ /**
1796
+ * Report a tolerated best-effort bookkeeping failure during give-up: the
1797
+ * incident `"read"` (before synthesizing) or the sealing `"seal"` write
1798
+ * (after terminalization). Neither aborts terminalization — see
1799
+ * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.
1800
+ */
1801
+ onGiveUpBookkeepingError(phase: "read" | "seal", error: unknown): void;
2038
1802
  }
2039
- /**
2040
- * Reconstruct a message from `chunks` and upsert it via the store. Returns
2041
- * `true` when a write happened (so a caller that broadcasts after — Think — can
2042
- * gate its broadcast on it), `false` when there was nothing to persist (no
2043
- * parts, or `prepare` returned `null`).
2044
- */
2045
- declare function persistReconstructedOrphan<
2046
- TMessage extends UIMessage = UIMessage
2047
- >(
2048
- chunks: ReadonlyArray<{
2049
- body: string;
2050
- }>,
2051
- options: PersistReconstructedOrphanOptions<TMessage>
2052
- ): Promise<boolean>;
2053
- //#endregion
2054
- //#region src/chat/recovery.d.ts
2055
- /**
2056
- * The minimal transcript-tail shape {@link createChatFiberSnapshot} reads to
2057
- * derive the snapshot's `latest*Id` markers. Deliberately NOT `UIMessage`: the
2058
- * snapshot only ever needs each message's `id` + `role`, so any host transcript
2059
- * (AI SDK `UIMessage[]`, `Think`'s session leaves, or the pi adapter's plain
2060
- * `AgentMessage[]`) satisfies it structurally. Keeping this off `UIMessage` is
2061
- * the Phase-5 genericity seam — the snapshot builder must not couple to the AI
2062
- * SDK message shape.
2063
- */
2064
- interface SnapshotMessage {
2065
- id?: string;
2066
- role: string;
1803
+ /** Resolved orphaned-stream identity for a recovered chat turn. */
1804
+ interface ResolvedRecoveryStream {
1805
+ /** The orphaned stream id, or `""` when no stream metadata survives. */
1806
+ streamId: string;
1807
+ /**
1808
+ * Whether the orphaned stream is still the live in-flight stream (so its
1809
+ * partial has not already been persisted + completed by an ACK-driven
1810
+ * reconnect). Gates persistence and stream completion.
1811
+ */
1812
+ streamStillActive: boolean;
1813
+ /**
1814
+ * The stream metadata row's lifecycle status, when the host tracks it
1815
+ * (`Think`). `undefined` for hosts that do not model terminal streams
1816
+ * (`AIChatAgent`) — those keep every terminal-stream branch dead, per the
1817
+ * "substrate capabilities are optional" decision in the RFC.
1818
+ */
1819
+ streamStatus?: ChatStreamStatus;
2067
1820
  }
2068
- type ChatFiberSnapshot<Kind extends string = string> = {
2069
- kind: Kind;
2070
- version: 1;
1821
+ /** Input to {@link ChatFiberWakeHooks.classifyRecoveredTurn}. */
1822
+ interface ClassifyRecoveredTurnInput {
1823
+ snapshot: ChatFiberSnapshot | null;
2071
1824
  requestId: string;
2072
- recoveryRootRequestId?: string;
2073
- continuation: boolean;
2074
- latestMessageId?: string;
2075
- latestMessageRole?: string;
2076
- latestUserMessageId?: string;
2077
- startedAt: number;
2078
- lastBody?: Record<string, unknown>;
2079
- lastClientTools?: ClientToolSchema[];
2080
- };
2081
- declare function createChatFiberSnapshot<Kind extends string>({
2082
- kind,
2083
- requestId,
2084
- recoveryRootRequestId,
2085
- continuation,
2086
- messages,
2087
- lastBody,
2088
- lastClientTools
2089
- }: {
2090
- kind: Kind;
1825
+ streamId: string;
1826
+ partial: RecoveryPartial;
1827
+ streamStillActive: boolean;
1828
+ streamStatus?: ChatStreamStatus;
1829
+ }
1830
+ /** Input to {@link ChatFiberWakeHooks.invokeOnChatRecovery}. */
1831
+ interface InvokeOnChatRecoveryInput {
1832
+ incident: ChatRecoveryIncident;
1833
+ recoveryKind: ChatRecoveryKind;
1834
+ recoveryRootRequestId: string;
2091
1835
  requestId: string;
2092
- recoveryRootRequestId?: string;
2093
- continuation: boolean;
2094
- messages: ReadonlyArray<SnapshotMessage>;
2095
- lastBody?: Record<string, unknown>;
2096
- lastClientTools?: ClientToolSchema[];
2097
- }): ChatFiberSnapshot<Kind>;
2098
- declare function wrapChatFiberSnapshot<Kind extends string>(
2099
- key: string,
2100
- snapshot: ChatFiberSnapshot<Kind>,
2101
- user: unknown | null
2102
- ): Record<string, unknown>;
2103
- declare function unwrapChatFiberSnapshot<Kind extends string>(
2104
- key: string,
2105
- value: unknown,
2106
- expectedKind?: Kind
2107
- ): {
2108
- snapshot: ChatFiberSnapshot<Kind> | null;
2109
- user: unknown | null;
2110
- };
2111
- //#endregion
2112
- //#region src/chat/recovery-incident.d.ts
2113
- /**
2114
- * Whether a recovery is retrying an unanswered user turn or continuing a
2115
- * partial assistant turn. Intentionally NOT part of the incident identity (see
2116
- * {@link chatRecoveryIncidentId}).
2117
- */
2118
- type ChatRecoveryKind = "retry" | "continue";
1836
+ streamId: string;
1837
+ partial: RecoveryPartial;
1838
+ snapshot: ChatFiberSnapshot | null;
1839
+ recoveryData: unknown;
1840
+ createdAt: number;
1841
+ }
1842
+ /** Input to {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}. */
1843
+ interface PersistOrphanedPartialInput {
1844
+ streamId: string;
1845
+ streamStillActive: boolean;
1846
+ streamStatus?: ChatStreamStatus;
1847
+ snapshot: ChatFiberSnapshot | null;
1848
+ }
1849
+ /** Input to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}. */
1850
+ interface DispatchRecoveredTurnInput<TClassify> {
1851
+ incident: ChatRecoveryIncident;
1852
+ config: ResolvedChatRecoveryConfig;
1853
+ recoveryKind: ChatRecoveryKind;
1854
+ options: ChatRecoveryOptions;
1855
+ snapshot: ChatFiberSnapshot | null;
1856
+ requestId: string;
1857
+ recoveryRootRequestId: string;
1858
+ streamId: string;
1859
+ streamStatus?: ChatStreamStatus;
1860
+ /** The package-specific classification detail produced by `classifyRecoveredTurn`. */
1861
+ detail: TClassify;
1862
+ }
2119
1863
  /**
2120
- * Durable per-incident recovery record.
1864
+ * The wake-dispatch host operations the engine drives when an interrupted CHAT
1865
+ * fiber is detected on restart — the divergent organs the frame-collapse map
1866
+ * flagged. Kept SEPARATE from {@link ChatRecoveryAdapter} (and passed per call to
1867
+ * {@link ChatRecoveryEngine.handleChatFiberRecovery}) so the incident/give-up
1868
+ * adapter stays focused, and generic over `TClassify` so the
1869
+ * `classifyRecoveredTurn` → `dispatchRecoveredTurn` handoff is type-safe without a
1870
+ * class-level generic.
2121
1871
  *
2122
- * PERSISTED CONTRACT — this shape round-trips across deploys (including the
2123
- * deploy that ships the shared engine, which is itself a deploy-mid-recovery).
2124
- * Fields are added as optional so older persisted incidents keep recovering.
1872
+ * The engine owns the wake LIFECYCLE (gate → parse → unwrap → stream → partial →
1873
+ * classify → begin-incident → exhausted-branch → onChatRecovery → persist →
1874
+ * complete → dispatch → catch→failed) and the shared persist clause; these hooks
1875
+ * own the package-specific I/O and the retry/continue/skip decision.
2125
1876
  */
2126
- type ChatRecoveryIncident = {
2127
- incidentId: string;
2128
- requestId: string /** Stable request ID for the whole continuation chain (the recovery root). */;
2129
- recoveryRootRequestId?: string;
2130
- recoveryKind: ChatRecoveryKind;
2131
- attempt: number;
2132
- maxAttempts: number;
2133
- status:
2134
- | "detected"
2135
- | "scheduled"
2136
- | "attempting"
2137
- | "completed"
2138
- | "skipped"
2139
- | "exhausted"
2140
- | "failed";
2141
- firstSeenAt: number;
2142
- lastAttemptAt: number;
1877
+ interface ChatFiberWakeHooks<TClassify> {
1878
+ /** The chat-fiber name prefix (`CHAT_FIBER_NAME + ":"`) gating the wake path. */
1879
+ chatFiberPrefix(): string;
1880
+ /** Decode the fiber snapshot into the recovery snapshot + checkpointed user data. */
1881
+ unwrapRecoverySnapshot(ctx: FiberRecoveryContext$1): {
1882
+ snapshot: ChatFiberSnapshot | null;
1883
+ recoveryData: unknown;
1884
+ };
2143
1885
  /**
2144
- * Epoch ms of the last attempt that observed forward progress. The recovery
2145
- * budget is keyed to this (`now - lastProgressAt > noProgressTimeoutMs`), so a
2146
- * turn that keeps producing content survives churn indefinitely while a
2147
- * genuinely stuck turn is sealed within the window (#1637). Optional for
2148
- * backward-compat — falls back to `firstSeenAt`.
1886
+ * Classify the recovered turn as a `retry` or `continue` and return any
1887
+ * package-specific detail the dispatch decision needs (e.g. the pre-stream
1888
+ * retry target id). Runs before the incident is opened.
2149
1889
  */
2150
- lastProgressAt?: number;
2151
- reason?: string;
1890
+ classifyRecoveredTurn(input: ClassifyRecoveredTurnInput):
1891
+ | {
1892
+ recoveryKind: ChatRecoveryKind;
1893
+ detail: TClassify;
1894
+ }
1895
+ | Promise<{
1896
+ recoveryKind: ChatRecoveryKind;
1897
+ detail: TClassify;
1898
+ }>;
2152
1899
  /**
2153
- * High-water mark of the durable, monotonic recovery-progress counter
2154
- * observed for this incident. Distinguishes a turn making forward progress
2155
- * but repeatedly interrupted by isolate resets (deploys) — which must NOT
2156
- * exhaust the budget — from one that genuinely fails to advance. Sourced from
2157
- * a persisted counter, never the compactable transcript (#1628).
1900
+ * Build the package's `ChatRecoveryContext` and invoke the user `onChatRecovery`
1901
+ * hook, returning its (defaulted) options. The engine wraps this in the
1902
+ * incident `failed`-on-throw guard. Optional: a host with no user
1903
+ * `onChatRecovery` surface (e.g. the pi fixture) omits it and the engine
1904
+ * proceeds with empty options (`{}`).
2158
1905
  */
2159
- progress?: number;
1906
+ invokeOnChatRecovery?(
1907
+ input: InvokeOnChatRecoveryInput
1908
+ ): Promise<ChatRecoveryOptions | void>;
2160
1909
  /**
2161
- * Value of the durable progress counter when this incident opened. The
2162
- * runaway-loop work budget is `progress - workBaseline`, compared against
2163
- * `maxRecoveryWork`. Optional for backward-compat — a missing baseline is
2164
- * treated as the current marker (zero work so far), so an in-flight incident
2165
- * from an older build is never falsely sealed.
1910
+ * The BASE persist gate: whether the orphaned partial is eligible to be
1911
+ * materialized at all (live stream, or terminal-but-not-yet-persisted). The
1912
+ * engine ANDs this with the shared `options.persist !== false ||
1913
+ * partial.hasSettledToolResults` clause, so settled work is never dropped.
2166
1914
  */
2167
- workBaseline?: number;
1915
+ shouldPersistOrphanedPartial(
1916
+ input: PersistOrphanedPartialInput
1917
+ ): boolean | Promise<boolean>;
1918
+ /** Materialize the orphaned stream's partial into a persisted assistant message. */
1919
+ persistOrphanedStream(streamId: string): Promise<void>;
1920
+ /** Mark the (still-active) recovered stream complete and schedule cleanup. */
1921
+ completeRecoveredStream(streamId: string): void | Promise<void>;
2168
1922
  /**
2169
- * Count of recovery attempts for this incident that ended in a Durable Object
2170
- * memory-limit reset (the isolate exceeded its 128 MB limit — see
2171
- * `isDurableObjectMemoryLimitReset`). An OOM is a poison signal: re-running the
2172
- * same memory-heavy turn deterministically re-OOMs, so unlike a deploy/eviction
2173
- * it must NOT credit forward progress and must be bounded by a tight,
2174
- * OOM-specific budget (`maxOomRetries`) rather than the generic attempt cap
2175
- * (which resets on progress). Bumped by `ChatRecoveryEngine.recordOomAndDecide`
2176
- * when a recovery callback observes an OOM; exceeding `maxOomRetries` seals the
2177
- * incident with `reason="out_of_memory"` (#1825). Optional for backward-compat.
1923
+ * The retry/continue/skip DECISION — the package-owned core. Runs after persist
1924
+ * + complete; owns the leaf/submission computation, the schedule calls (via
1925
+ * {@link ChatRecoveryEngine.scheduleRecovery}), the skip transitions, and any
1926
+ * package-specific terminal/broadcast writes.
2178
1927
  */
2179
- oomAttempts?: number;
2180
- };
2181
- declare const CHAT_RECOVERY_INCIDENT_KEY_PREFIX = "cf:chat-recovery:incident:";
2182
- /**
2183
- * Durable, monotonic forward-progress counter for recovery budget resets.
2184
- * Bumped at production time when new content is streamed, so it reflects
2185
- * genuinely new content and is immune to reconnects/re-persists; never
2186
- * recomputed from the (compactable) transcript.
2187
- */
2188
- declare const CHAT_RECOVERY_PROGRESS_KEY = "cf:chat-recovery:progress";
2189
- /**
2190
- * Durable record of an in-progress recovery so a "recovering…" status (#1620)
2191
- * can be broadcast live and survive the set/clear happening in different
2192
- * isolates (a continuation runs in a later alarm invocation).
2193
- */
2194
- declare const CHAT_RECOVERING_KEY = "cf:chat:recovering";
2195
- /**
2196
- * Durable record of the last turn that ended in a terminal error / abandoned
2197
- * recovery (#1645). Replayed on the next reconnect via the resume handshake;
2198
- * cleared when a later turn supersedes it.
2199
- */
2200
- declare const CHAT_LAST_TERMINAL_KEY = "cf:chat:last-terminal";
1928
+ dispatchRecoveredTurn(
1929
+ input: DispatchRecoveredTurnInput<TClassify>
1930
+ ): Promise<void>;
1931
+ }
2201
1932
  /**
2202
- * Secondary backstop only. The primary recovery bound is the no-progress wall
2203
- * clock; with alarm debounce this cap rarely binds (it catches a pathological
2204
- * tight alarm-loop). Kept high so the no-progress window seals first under
2205
- * normal deploy cadence (#1637).
1933
+ * Drives the shared recovery orchestration over a {@link ChatRecoveryAdapter}.
1934
+ * The incident *budget math* lives in the pure `evaluateChatRecoveryIncident`;
1935
+ * this class owns the surrounding sequence and its ordering invariants.
2206
1936
  */
2207
- declare const DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS = 10;
2208
- /**
2209
- * Runaway-loop guard default — the framework-imposed backstop on cumulative
2210
- * recovery WORK (produced content/tool units) since an incident opened.
2211
- *
2212
- * Originally `Infinity` (rfc-chat-recovery-work-budget): the SDK shipped the
2213
- * *mechanism* but no default cap, so a progressing turn was never terminated on
2214
- * its own. Production issue #1825 showed that this is a footgun: an isolate that
2215
- * OOMs mid-stream still credits a little progress before it dies, which resets
2216
- * BOTH progress-keyed bounds (the attempt cap and the no-progress window) on
2217
- * every wake — and a fast crash loop (each attempt inside the alarm-debounce
2218
- * window) pins the attempt counter too. With `maxRecoveryWork = Infinity` the
2219
- * ONLY instrument whose meter still climbs across such a loop is disabled, so
2220
- * recovery re-runs the turn (and its LLM calls) forever.
1937
+ declare class ChatRecoveryEngine {
1938
+ private readonly adapter;
1939
+ constructor(adapter: ChatRecoveryAdapter);
1940
+ /**
1941
+ * Open or re-evaluate the recovery incident for `input`, persist the result,
1942
+ * and broadcast its lifecycle events. Returns the incident, the resolved
1943
+ * config, and whether the budget is now exhausted.
1944
+ */
1945
+ /**
1946
+ * Dispatch a recovered fiber to the package's non-chat handler (the
1947
+ * messenger/workflow seam) before any chat-recovery processing. Returns `true`
1948
+ * when the package consumed the fiber — the caller must then skip chat
1949
+ * recovery for it. The engine owns the *ordering* (this runs before the
1950
+ * chat-fiber gate); the *behavior* is adapter-owned. No-op (`false`) when the
1951
+ * adapter omits {@link ChatRecoveryAdapter.tryHandleNonChatFiberRecovery}.
1952
+ */
1953
+ handleNonChatFiber(ctx: FiberRecoveryContext$1): Promise<boolean>;
1954
+ /**
1955
+ * The shared wake-recovery LIFECYCLE for an interrupted chat fiber. Both
1956
+ * packages drove this exact frame; the divergent organs are the
1957
+ * {@link ChatFiberWakeHooks}. In order:
1958
+ *
1959
+ * 1. non-chat dispatch ({@link handleNonChatFiber}) FIRST, then the chat-fiber
1960
+ * name gate — a non-chat fiber is never misread as an orphaned chat turn;
1961
+ * 2. parse the request id, unwrap the snapshot, resolve the orphaned stream +
1962
+ * reconstruct its partial;
1963
+ * 3. classify the turn (retry/continue + package detail) and open the incident;
1964
+ * 4. if the budget is already exhausted, persist the settled partial (so
1965
+ * non-idempotent tool results are not discarded — #1631) and terminalize
1966
+ * BEFORE consulting `onChatRecovery`;
1967
+ * 5. otherwise, inside a `failed`-on-throw guard: invoke `onChatRecovery`,
1968
+ * apply the shared persist gate (base eligibility AND `persist !== false ||
1969
+ * settled tool results`), complete the live stream, then hand the
1970
+ * retry/continue/skip DECISION to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}.
1971
+ *
1972
+ * Returns `true` when the fiber was a chat (or non-chat) recovery the engine
1973
+ * handled, `false` when it was not a chat fiber (the caller keeps looking). Any
1974
+ * throw after the incident opens flips it to `failed` so it is never left
1975
+ * leaking in `attempting`.
1976
+ */
1977
+ handleChatFiberRecovery<TClassify>(
1978
+ ctx: FiberRecoveryContext$1,
1979
+ wake: ChatFiberWakeHooks<TClassify>
1980
+ ): Promise<boolean>;
1981
+ /**
1982
+ * The shared persist gate: base eligibility (the package's
1983
+ * {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}) AND the
1984
+ * never-drop-settled-work clause `options.persist !== false ||
1985
+ * partial.hasSettledToolResults`. `options: undefined` (the exhausted branch)
1986
+ * collapses the clause to the base gate. The clause lives here — not in each
1987
+ * package — because settled-work preservation is a cross-package invariant
1988
+ * (#1631), and the codec (not the engine) decides whether a partial carries
1989
+ * settled tool work, so the engine stays wire-vocabulary-agnostic.
1990
+ */
1991
+ private _shouldPersistOrphanedPartial;
1992
+ beginIncident(
1993
+ input: BeginChatRecoveryIncidentInput
1994
+ ): Promise<BeginChatRecoveryIncidentResult>;
1995
+ /**
1996
+ * Schedule a recovery continuation/retry: the transition + emit + enqueue
1997
+ * triplet both packages repeat at every fiber-recovery and stall-routing
1998
+ * decision. In order:
1999
+ *
2000
+ * 1. transition the incident to `scheduled` (persist + drive the #1620
2001
+ * "recovering…" status) via {@link updateIncident};
2002
+ * 2. emit `chat:recovery:scheduled`; and
2003
+ * 3. enqueue the callback through the adapter's transport — a Task run on
2004
+ * a root agent, an idempotent schedule on the routed fallback.
2005
+ *
2006
+ * `recoveryKind` is passed explicitly (not read off the incident) because a
2007
+ * caller can legitimately report a different kind than the incident was opened
2008
+ * with — e.g. `AIChatAgent`'s lost-partial branch opens a `continue` incident
2009
+ * but schedules (and reports) a `retry`. `requestId` always matches
2010
+ * `incident.requestId` (the evaluation rewrites it to the current attempt), so
2011
+ * it is read from the incident.
2012
+ */
2013
+ scheduleRecovery(input: {
2014
+ incident: ChatRecoveryIncident;
2015
+ recoveryKind: ChatRecoveryKind;
2016
+ callback: ChatRecoveryScheduleCallback;
2017
+ data: Record<string, unknown>;
2018
+ reason?: ChatRecoveryScheduleReason;
2019
+ }): Promise<void>;
2020
+ /**
2021
+ * Reschedule a recovery continuation/retry that timed out waiting for stable
2022
+ * state, from INSIDE the currently-executing recovery attempt (a
2023
+ * `__cf_internal_chat_recovery` Task run for a root agent, the routed
2024
+ * one-shot schedule row for a dynamic agent). Reads the
2025
+ * incident; if it is still under the attempt cap, bumps `attempt`, marks it
2026
+ * `scheduled` with `reason:"stable_timeout_retry"`, and issues a separate
2027
+ * delayed attempt. It must not join the currently executing attempt: Tasks
2028
+ * enqueue an unkeyed chained run, while the routed fallback creates a
2029
+ * non-idempotent schedule.
2030
+ *
2031
+ * Returns `true` when a retry was scheduled, `false` when there is no incident
2032
+ * (no id / record gone) or the attempt budget is already spent — in which case
2033
+ * the caller falls through to the give-up path. Deliberately bypasses the
2034
+ * `evaluateChatRecoveryIncident` budget (this is a coarse stable-state retry,
2035
+ * not a fresh interruption) and {@link updateIncident} (no `scheduled` event /
2036
+ * recovering-flag churn on a same-turn reschedule).
2037
+ */
2038
+ rescheduleAfterStableTimeout(input: {
2039
+ incidentId: string | undefined;
2040
+ callback: ChatRecoveryScheduleCallback;
2041
+ data: Record<string, unknown> | undefined;
2042
+ fallbackMaxAttempts: number;
2043
+ }): Promise<boolean>;
2044
+ /**
2045
+ * Record that a recovery callback observed a Durable Object memory-limit reset
2046
+ * (the isolate exceeded its 128 MB limit — `isDurableObjectMemoryLimitReset`)
2047
+ * and decide what to do next (#1825).
2048
+ *
2049
+ * Bumps the incident's durable `oomAttempts` counter, then:
2050
+ * - if it is still within `maxOomRetries`, issues a delayed, NON-idempotent
2051
+ * reschedule of the SAME callback (same machinery as
2052
+ * {@link rescheduleAfterStableTimeout}: the executing attempt settles
2053
+ * only after the callback returns, so an idempotent reschedule would
2054
+ * dedup onto that doomed attempt) and returns `"rescheduled"`. The small
2055
+ * delay lets a transient memory spike clear before the re-run;
2056
+ * - otherwise leaves the incremented count persisted (so a begin-path
2057
+ * re-evaluation agrees) and returns `"exhausted"` — the caller then
2058
+ * terminalizes via the give-up path with `reason="out_of_memory"`.
2059
+ *
2060
+ * Returns `"exhausted"` when there is no incident to track against (no id /
2061
+ * record gone): an OOM we cannot bound must seal rather than loop. Unlike a
2062
+ * stable-state retry this is gated by the OOM-specific budget, NOT the generic
2063
+ * attempt cap — re-running an OOM streams a little "progress" that would
2064
+ * otherwise reset the attempt cap forever (the #1825 loop).
2065
+ */
2066
+ recordOomAndDecide(input: {
2067
+ incidentId: string | undefined;
2068
+ callback: ChatRecoveryScheduleCallback;
2069
+ data: Record<string, unknown> | undefined;
2070
+ maxOomRetries: number;
2071
+ }): Promise<"rescheduled" | "exhausted">;
2072
+ /**
2073
+ * Give up on a recovery turn whose retry budget drained, terminalizing it so
2074
+ * it can never become an eternal spinner (#1645). The shared spine both
2075
+ * packages repeated verbatim:
2076
+ *
2077
+ * 1. resolve config + the incident key from `data.incidentId`;
2078
+ * 2. best-effort READ the stored incident — a failed read is tolerated
2079
+ * (reported via `onGiveUpBookkeepingError("read", …)`) and the incident is
2080
+ * synthesized, because the read backs only the re-entry guard, not the
2081
+ * terminal UX;
2082
+ * 3. re-entry guard: a `stored.status === "exhausted"` record means
2083
+ * terminalization already fired, so a duplicate stale alarm returns without
2084
+ * re-broadcasting the banner;
2085
+ * 4. build the exhausted incident (reuse `stored`, or synthesize a minimal one
2086
+ * so a swept/missing record STILL terminalizes through `onExhausted`);
2087
+ * 5. resolve the orphaned stream id + partial;
2088
+ * 6. terminalize via `exhaustChatRecovery` — BEFORE sealing. The terminal
2089
+ * writes can reject with a platform transient in the deploy/storage window
2090
+ * a give-up runs in (#1730); letting that throw propagate is deliberate, so
2091
+ * the current recovery attempt (a Task run on a root agent, a one-shot
2092
+ * schedule row on the routed fallback) defers and the WHOLE give-up
2093
+ * re-runs on a healthy isolate. Sealing first would arm the
2094
+ * re-entry guard and turn that re-run into a no-op, dropping the durable
2095
+ * terminal record. The re-run is idempotent (terminal writes overwrite the
2096
+ * same key); a second banner is the documented at-least-once edge; and
2097
+ * 7. best-effort SEAL write so the re-entry guard sees `exhausted` on a
2098
+ * duplicate alarm — a failed seal (reported via
2099
+ * `onGiveUpBookkeepingError("seal", …)`) costs at most one re-delivered
2100
+ * banner.
2101
+ *
2102
+ * The two packages diverged only in parameters the caller supplies:
2103
+ * `reason` (`Think` passes `stable_timeout` | `recovery_error`; `AIChatAgent`
2104
+ * always `stable_timeout`) and the root-id chain (`Think` includes
2105
+ * `recoveredRequestId`; `AIChatAgent` never sets it, so the unified chain
2106
+ * collapses identically). Exactly-once terminalization rests on the re-entry
2107
+ * guard alone in `AIChatAgent`; `Think` additionally short-circuits duplicate
2108
+ * alarms earlier in its durable-submission layer.
2109
+ */
2110
+ exhaustRecoveryGiveUp(input: {
2111
+ callback: ChatRecoveryScheduleCallback;
2112
+ data:
2113
+ | {
2114
+ incidentId?: string;
2115
+ originalRequestId?: string;
2116
+ recoveredRequestId?: string;
2117
+ }
2118
+ | undefined;
2119
+ reason: string;
2120
+ }): Promise<void>;
2121
+ /**
2122
+ * Apply a status transition to the recovery incident `incidentId`:
2123
+ *
2124
+ * - `completed` → drop the record (terminal, never retried);
2125
+ * - any other status → persist the new status (and `reason`), so the attempt
2126
+ * budget survives restarts until the TTL sweep reclaims it;
2127
+ * - emit the matching `completed`/`skipped`/`failed` lifecycle event; and
2128
+ * - drive the live "recovering…" status (#1620): `scheduled` marks it active
2129
+ * (keyed by the recovery-root request id), terminal states clear it.
2130
+ *
2131
+ * No-op when `incidentId` is undefined or the record is already gone. This is
2132
+ * the transition twin of {@link beginIncident}: all I/O is adapter-owned, the
2133
+ * engine owns only the state-machine shape.
2134
+ */
2135
+ updateIncident(
2136
+ incidentId: string | undefined,
2137
+ status: ChatRecoveryIncident["status"],
2138
+ reason?: string
2139
+ ): Promise<void>;
2140
+ }
2141
+ /**
2142
+ * The complete give-up choreography from a single call: build the exhausted
2143
+ * context, fire the shared notification ({@link notifyChatRecoveryExhausted}),
2144
+ * then hand that context to the host's `terminalize` step. Folds the
2145
+ * `buildChatRecoveryExhaustedContext` → `notifyChatRecoveryExhausted` → host
2146
+ * terminalize sequence that every host's `_exhaustChatRecovery` repeated.
2221
2147
  *
2222
- * A finite default closes that loop out of the box: work climbs regardless of
2223
- * debounce/progress resets, so a content-emitting runaway is always sealed with
2224
- * `reason="work_budget_exceeded"`. The value is deliberately generous — it
2225
- * bounds wasted re-run cost without clipping a normal interrupted turn (work
2226
- * only accrues from the first interruption until the turn completes, after which
2227
- * the incident is deleted). A very long agentic turn under heavy interruption
2228
- * that legitimately needs more should raise `maxRecoveryWork` (or set it to
2229
- * `Infinity` to restore the pre-#1825 unbounded behavior).
2148
+ * What this OWNS (the invariant, so it cannot drift per host):
2149
+ * - the notification ALWAYS runs before any terminal write, and
2150
+ * - a throwing `onExhausted` can NEVER block terminal delivery — it is swallowed
2151
+ * via `onError` (a tested invariant in both published packages).
2152
+ *
2153
+ * What it deliberately does NOT own: the terminal-record / broadcast /
2154
+ * recovering-clear writes — their exact set diverges per host (both
2155
+ * `AIChatAgent` and `Think` broadcast the banner first so it survives a storage
2156
+ * write that rejects mid-deploy; `Think` additionally writes a submission row)
2157
+ * — see {@link ChatRecoveryAdapter.exhaustChatRecovery}. The host expresses
2158
+ * those writes inside `terminalize`. A `terminalize` that throws DOES propagate,
2159
+ * so the whole give-up re-runs on a healthy isolate (#1730); see
2160
+ * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.
2161
+ *
2162
+ * `partialParts` is passed explicitly (not derived from a `RecoveryPartial`) so a
2163
+ * foreign-vocabulary host can pass `[]` rather than fabricate AI-SDK parts — the
2164
+ * engine seam stays parts-vocabulary-agnostic.
2230
2165
  */
2231
- declare const DEFAULT_CHAT_RECOVERY_MAX_WORK = 1000;
2166
+ declare function runChatRecoveryExhaustion(
2167
+ input: {
2168
+ incident: ChatRecoveryIncident;
2169
+ config: ResolvedChatRecoveryConfig;
2170
+ partialText: string;
2171
+ partialParts: ChatRecoveryExhaustedContext["partialParts"];
2172
+ streamId: string;
2173
+ createdAt: number;
2174
+ },
2175
+ hooks: {
2176
+ emit: (ctx: ChatRecoveryExhaustedContext) => void;
2177
+ onExhausted?: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;
2178
+ onError: (error: unknown) => void;
2179
+ terminalize: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;
2180
+ }
2181
+ ): Promise<void>;
2182
+ //#endregion
2183
+ //#region src/chat/recovery-task.d.ts
2184
+ /** Reserved Task definition shared by the chat hosts. */
2185
+ declare const CHAT_RECOVERY_TASK_NAME = "__cf_internal_chat_recovery";
2186
+ /** Input persisted for one recovery continuation attempt. */
2187
+ type ChatRecoveryTaskInput = {
2188
+ /** Host continuation to dispatch. */ readonly callback: ChatRecoveryScheduleCallback /** Continuation context owned by the chat host. */;
2189
+ readonly data: Record<
2190
+ string,
2191
+ unknown
2192
+ > /** Durable delay before dispatch, in seconds. */;
2193
+ readonly delaySeconds: number;
2194
+ };
2195
+ /** Why a recovery attempt is being enqueued. */
2196
+ type ChatRecoveryTaskReason = "initial" | "stable_timeout_retry" | "redefer";
2197
+ /** The host's bounded entry point for each continuation callback. */
2198
+ type ChatRecoveryTaskHooks = Record<
2199
+ ChatRecoveryScheduleCallback,
2200
+ (data: Record<string, unknown>) => Promise<void>
2201
+ >;
2202
+ /** How a bounded recovery callback hands its model turn to the alarm domain. */
2203
+ type ChatRecoveryHandoff = {
2204
+ /**
2205
+ * Start the detached continuation. It calls `onTurnStarted` once the model
2206
+ * turn begins — the point after which the bounded callback returns.
2207
+ */
2208
+ readonly detached: (
2209
+ onTurnStarted: () => void
2210
+ ) => Promise<void> /** Keep the detached turn inside the current alarm's breaker domain. */;
2211
+ readonly track: (turn: Promise<void>) => void;
2212
+ /**
2213
+ * Enqueue exactly one replacement attempt for a detached platform
2214
+ * failure. `dedupeKey` is stable across every retried call for the same
2215
+ * failure (see {@link dispatchChatRecoveryToHandoff}): acceptance may
2216
+ * throw after already durably creating the run — most likely on the
2217
+ * wake-mirror push, not the run insert itself — so a naive retry of an
2218
+ * unkeyed enqueue risks creating another one. Pass it straight through as
2219
+ * the run's `runId` so a retry joins that same row instead.
2220
+ */
2221
+ readonly redefer: (
2222
+ dedupeKey: string
2223
+ ) => Promise<void> /** Report a detached failure the turn's own bookkeeping already handled. */;
2224
+ readonly onDetachedError: (error: unknown) => void;
2225
+ };
2232
2226
  /**
2233
- * Tight, OOM-specific retry budget (#1825). A Durable Object memory-limit reset
2234
- * (`isDurableObjectMemoryLimitReset`) is usually deterministic — the turn's
2235
- * working set no longer fits in the isolate's 128 MB — so re-running it re-OOMs.
2236
- * But a single OOM CAN be a transient spike (the isolate's 128 MB is shared
2237
- * across the global scope / noisy neighbors), so recovery retries a small number
2238
- * of times before sealing with `reason="out_of_memory"` rather than abandoning a
2239
- * turn that one more attempt might have completed. Far tighter than the generic
2240
- * `maxRecoveryWork` backstop because an OOM is attributable and re-running it is
2241
- * expensive (it re-runs the model). Counts attempts that ended in an OOM, not
2242
- * total attempts, so a turn interrupted by deploys (no OOM) is unaffected.
2227
+ * Run a queue-driven recovery callback up to its model handoff, then return.
2228
+ *
2229
+ * The recovered turn can legitimately run for a long time, and awaiting it
2230
+ * would hold the Lifecycle job loop, starving every other job on the object.
2231
+ * A failure before the handoff rejects here, so the executing Task run (or
2232
+ * compatibility schedule row) keeps ownership and the driver's
2233
+ * platform-failure deferral applies (#1730). After the handoff the turn is
2234
+ * detached alarm work: a platform failure enqueues one replacement attempt,
2235
+ * retried a few times since the completed Task no longer owns this incident
2236
+ * and a failure here would otherwise abandon it silently, and any other
2237
+ * failure belongs to the turn's own incident bookkeeping.
2238
+ */
2239
+ declare function dispatchChatRecoveryToHandoff(
2240
+ handoff: ChatRecoveryHandoff
2241
+ ): Promise<void>;
2242
+ /**
2243
+ * Build run options for one recovery attempt.
2244
+ *
2245
+ * Initial detection joins an existing in-flight attempt for the same incident
2246
+ * and callback. Chained retries are otherwise unkeyed because each one is
2247
+ * enqueued while the preceding run still exists — a genuinely new attempt,
2248
+ * not a retry of this same enqueue. `dedupeKey`, when supplied, keys this
2249
+ * specific enqueue call by `runId` instead: every retry of one failed
2250
+ * `redefer` (see {@link dispatchChatRecoveryToHandoff}) reuses the same key,
2251
+ * so a rejected-but-already-inserted attempt is joined rather than
2252
+ * duplicated. Non-retention releases the initial key when the run settles.
2253
+ */
2254
+ declare function chatRecoveryTaskRunOptions(
2255
+ input: ChatRecoveryTaskInput,
2256
+ reason: ChatRecoveryTaskReason,
2257
+ dedupeKey?: string
2258
+ ): TaskRunOptions;
2259
+ /** Build the shared recovery Task handler for one chat host. */
2260
+ declare function createChatRecoveryTaskDefinition(
2261
+ hooks: ChatRecoveryTaskHooks
2262
+ ): (input: unknown, step: TaskStep) => Promise<void>;
2263
+ //#endregion
2264
+ //#region src/chat/sql-batch.d.ts
2265
+ /**
2266
+ * Helpers for building batched SQLite statements that run through the Agent's
2267
+ * `sql` tagged template (which interleaves a `?` placeholder between every
2268
+ * string fragment). Used to collapse per-row INSERT/DELETE loops into a small
2269
+ * number of multi-row statements.
2270
+ *
2271
+ * SQLite (Durable Object / D1) caps bound parameters at 100 per query, so
2272
+ * callers must chunk their inputs to stay within {@link MAX_BOUND_PARAMS}.
2273
+ * See https://developers.cloudflare.com/d1/platform/limits/
2243
2274
  */
2244
- declare const DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES = 3;
2245
- declare const DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS = 10000;
2275
+ /** Maximum bound parameters allowed in a single SQLite (DO / D1) query. */
2276
+ declare const MAX_BOUND_PARAMS = 100;
2246
2277
  /**
2247
- * Delay before retrying a recovery that timed out waiting for stable state.
2248
- * Gives an actively-churning isolate (e.g. a deploy in flight) time to settle.
2278
+ * Build a TemplateStringsArray for a single-column `IN (...)` clause. Produces
2279
+ * fragments for:
2280
+ * `${prefix}(?, ?, ...)`
2281
+ *
2282
+ * @throws if `count` is less than 1.
2249
2283
  */
2250
- declare const CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS = 3;
2251
- declare const DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE =
2252
- "The assistant was interrupted and could not recover. Please try again.";
2284
+ declare function buildInClauseStrings(
2285
+ prefix: string,
2286
+ count: number
2287
+ ): TemplateStringsArray;
2288
+ //#endregion
2289
+ //#region src/chat/protocol.d.ts
2253
2290
  /**
2254
- * Incidents that have not seen a new attempt within this window are assumed
2255
- * abandoned and swept so durable storage does not grow without bound.
2291
+ * Wire protocol message type constants for the cf_agent_chat_* protocol.
2292
+ *
2293
+ * These are the string values used on the wire between agent servers and
2294
+ * clients. Both @cloudflare/ai-chat (via its MessageType enum) and
2295
+ * @cloudflare/think use these values.
2256
2296
  */
2257
- declare const CHAT_RECOVERY_INCIDENT_TTL_MS: number;
2258
- /** Max keys per Durable Object KV `delete([...])` call. */
2259
- declare const KV_DELETE_MAX_KEYS = 128;
2297
+ declare const STREAM_RESUME_NONE_REASONS: {
2298
+ /** No active, pending, or terminal stream exists for this agent. */ readonly IDLE: "idle" /** An active tool continuation is owned by another live connection. */;
2299
+ readonly CONTINUATION_OWNED: "continuation-owned";
2300
+ };
2301
+ type StreamResumeNoneReason =
2302
+ (typeof STREAM_RESUME_NONE_REASONS)[keyof typeof STREAM_RESUME_NONE_REASONS];
2303
+ declare const CHAT_MESSAGE_TYPES: {
2304
+ readonly CHAT_MESSAGES: "cf_agent_chat_messages";
2305
+ readonly USE_CHAT_REQUEST: "cf_agent_use_chat_request";
2306
+ readonly USE_CHAT_RESPONSE: "cf_agent_use_chat_response";
2307
+ readonly CHAT_CLEAR: "cf_agent_chat_clear";
2308
+ readonly CHAT_REQUEST_CANCEL: "cf_agent_chat_request_cancel";
2309
+ readonly STREAM_RESUMING: "cf_agent_stream_resuming";
2310
+ readonly STREAM_RESUME_ACK: "cf_agent_stream_resume_ack";
2311
+ readonly STREAM_RESUME_REQUEST: "cf_agent_stream_resume_request";
2312
+ readonly STREAM_RESUME_NONE: "cf_agent_stream_resume_none";
2313
+ readonly STREAM_PENDING: "cf_agent_stream_pending";
2314
+ readonly TOOL_RESULT: "cf_agent_tool_result";
2315
+ readonly TOOL_APPROVAL: "cf_agent_tool_approval";
2316
+ readonly MESSAGE_UPDATED: "cf_agent_message_updated";
2317
+ readonly CHAT_RECOVERING: "cf_agent_chat_recovering";
2318
+ };
2319
+ //#endregion
2320
+ //#region src/chat/wire-types.d.ts
2260
2321
  /**
2261
- * PRIMARY recovery bound (#1637): seal an incident that has made no forward
2262
- * progress for this long. Keyed to `lastProgressAt`, which resets on every
2263
- * progress-bearing attempt — so a turn that keeps producing content survives
2264
- * deploy churn indefinitely, while a genuinely stuck turn dies within 5 min.
2322
+ * Enum for message types to improve type safety and maintainability
2265
2323
  */
2266
- declare const DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS: number;
2324
+ declare enum MessageType {
2325
+ CF_AGENT_CHAT_MESSAGES = "cf_agent_chat_messages",
2326
+ CF_AGENT_USE_CHAT_REQUEST = "cf_agent_use_chat_request",
2327
+ CF_AGENT_USE_CHAT_RESPONSE = "cf_agent_use_chat_response",
2328
+ CF_AGENT_CHAT_CLEAR = "cf_agent_chat_clear",
2329
+ CF_AGENT_CHAT_REQUEST_CANCEL = "cf_agent_chat_request_cancel",
2330
+ /** Sent by server when client connects and there's an active stream to resume */
2331
+ CF_AGENT_STREAM_RESUMING = "cf_agent_stream_resuming",
2332
+ /** Sent by client to acknowledge stream resuming notification and request chunks */
2333
+ CF_AGENT_STREAM_RESUME_ACK = "cf_agent_stream_resume_ack",
2334
+ /** Sent by client after message handler is ready, requesting stream resume check */
2335
+ CF_AGENT_STREAM_RESUME_REQUEST = "cf_agent_stream_resume_request",
2336
+ /** Sent by server when client requests resume but no active stream exists */
2337
+ CF_AGENT_STREAM_RESUME_NONE = "cf_agent_stream_resume_none",
2338
+ /**
2339
+ * Sent by server when a turn is accepted but its resumable stream has not
2340
+ * started yet (queued / debouncing / waiting on MCP / async setup). Tells a
2341
+ * reconnecting client to keep waiting rather than resolve its resume probe to
2342
+ * "no stream". Resolved by a later `CF_AGENT_STREAM_RESUMING` (stream started)
2343
+ * or `CF_AGENT_STREAM_RESUME_NONE` (settled without streaming). See #1784.
2344
+ */
2345
+ CF_AGENT_STREAM_PENDING = "cf_agent_stream_pending",
2346
+ /** Client sends tool result to server (for client-side tools) */
2347
+ CF_AGENT_TOOL_RESULT = "cf_agent_tool_result",
2348
+ /** Server notifies client that a message was updated (e.g., tool result applied) */
2349
+ CF_AGENT_MESSAGE_UPDATED = "cf_agent_message_updated",
2350
+ /** Client sends tool approval response to server (for tools with needsApproval) */
2351
+ CF_AGENT_TOOL_APPROVAL = "cf_agent_tool_approval",
2352
+ /**
2353
+ * Server→client progress hint: a durable chat turn is being recovered
2354
+ * (interrupted by a deploy/eviction or a stream-stall watchdog abort and now
2355
+ * resuming). Sent when a recovery continuation is scheduled and cleared on
2356
+ * every terminal outcome. (`@cloudflare/think` also replays it on connect;
2357
+ * `@cloudflare/ai-chat` broadcasts the live signal only — see #1645.)
2358
+ * Backward-compatible — clients that don't understand it ignore it. See #1620.
2359
+ */
2360
+ CF_AGENT_CHAT_RECOVERING = "cf_agent_chat_recovering"
2361
+ }
2362
+ /**
2363
+ * Types of messages sent from the Agent to clients
2364
+ */
2365
+ type OutgoingMessage<ChatMessage extends UIMessage = UIMessage> =
2366
+ | {
2367
+ /** Indicates this message is a command to clear chat history */ type: MessageType.CF_AGENT_CHAT_CLEAR;
2368
+ }
2369
+ | {
2370
+ /** Indicates this message contains updated chat messages */ type: MessageType.CF_AGENT_CHAT_MESSAGES /** Array of chat messages */;
2371
+ messages: readonly ChatMessage[];
2372
+ }
2373
+ | {
2374
+ /** Indicates this message is a response to a chat request */ type: MessageType.CF_AGENT_USE_CHAT_RESPONSE /** Unique ID of the request this response corresponds to */;
2375
+ id: string /** Content body of the response */;
2376
+ body: string /** Whether this is the final chunk of the response */;
2377
+ done: boolean /** Whether this response contains an error */;
2378
+ error?: boolean /** Whether this is a continuation (append to last assistant message) */;
2379
+ continuation?: boolean /** Whether this chunk is being replayed from storage (stream resumption) */;
2380
+ replay?: boolean /** Signals that replay of stored chunks is complete (stream is still active) */;
2381
+ replayComplete?: boolean;
2382
+ }
2383
+ | {
2384
+ /** Indicates the server is resuming an active stream */ type: MessageType.CF_AGENT_STREAM_RESUMING /** The request ID of the stream being resumed */;
2385
+ id: string /** Present when this offer directly answers a client resume probe. */;
2386
+ probeId?: string;
2387
+ }
2388
+ | {
2389
+ /** Server notifies client that a message was updated (e.g., tool result applied) */ type: MessageType.CF_AGENT_MESSAGE_UPDATED /** The updated message */;
2390
+ message: ChatMessage;
2391
+ }
2392
+ | {
2393
+ /** Server responds to a resume request with no stream for this client. */ type: MessageType.CF_AGENT_STREAM_RESUME_NONE;
2394
+ /**
2395
+ * Why no stream was offered. Only `idle` proves global inactivity;
2396
+ * omitted by older servers and by non-authoritative delayed releases.
2397
+ */
2398
+ reason?: StreamResumeNoneReason /** Correlates an authoritative response to its client resume probe. */;
2399
+ probeId?: string;
2400
+ }
2401
+ | {
2402
+ /**
2403
+ * Server signals an accepted turn whose resumable stream has not started
2404
+ * yet — the client should keep waiting for `STREAM_RESUMING` (or a later
2405
+ * `STREAM_RESUME_NONE`) rather than give up. See #1784.
2406
+ */
2407
+ type: MessageType.CF_AGENT_STREAM_PENDING /** The accepted request id, when known. */;
2408
+ id?: string /** Correlates a direct keep-waiting response to its client probe. */;
2409
+ probeId?: string;
2410
+ }
2411
+ | {
2412
+ /**
2413
+ * Progress hint: a durable chat turn is being recovered (`recovering:
2414
+ * true`) or recovery has resolved (`recovering: false`). Purely advisory;
2415
+ * a client renders a "recovering…" indicator while true.
2416
+ */
2417
+ type: MessageType.CF_AGENT_CHAT_RECOVERING /** Whether recovery is in progress (true) or has resolved (false). */;
2418
+ recovering: boolean /** The recovery-root request id of the turn being recovered, if known. */;
2419
+ id?: string;
2420
+ };
2421
+ /**
2422
+ * Types of messages sent from clients to the Agent
2423
+ */
2424
+ type IncomingMessage<ChatMessage extends UIMessage = UIMessage> =
2425
+ | {
2426
+ /** Indicates this message is a command to clear chat history */ type: MessageType.CF_AGENT_CHAT_CLEAR;
2427
+ }
2428
+ | {
2429
+ /** Indicates this message is a request to the chat API */ type: MessageType.CF_AGENT_USE_CHAT_REQUEST /** Unique ID for this request */;
2430
+ id: string /** Request initialization options */;
2431
+ init: Pick<
2432
+ RequestInit,
2433
+ | "method"
2434
+ | "keepalive"
2435
+ | "headers"
2436
+ | "body"
2437
+ | "redirect"
2438
+ | "integrity"
2439
+ | "credentials"
2440
+ | "mode"
2441
+ | "referrer"
2442
+ | "referrerPolicy"
2443
+ | "window"
2444
+ >;
2445
+ }
2446
+ | {
2447
+ /** Indicates this message contains updated chat messages */ type: MessageType.CF_AGENT_CHAT_MESSAGES /** Array of chat messages */;
2448
+ messages: ChatMessage[];
2449
+ }
2450
+ | {
2451
+ /** Indicates the user wants to stop generation of this message */ type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL;
2452
+ id: string;
2453
+ }
2454
+ | {
2455
+ /** Client acknowledges stream resuming notification and is ready to receive chunks */ type: MessageType.CF_AGENT_STREAM_RESUME_ACK /** The request ID of the stream being resumed */;
2456
+ id: string;
2457
+ }
2458
+ | {
2459
+ /** Client requests stream resume check after message handler is registered */ type: MessageType.CF_AGENT_STREAM_RESUME_REQUEST /** Opaque correlation id echoed by direct server responses. */;
2460
+ probeId?: string;
2461
+ }
2462
+ | {
2463
+ /** Client sends tool result to server (for client-side tools) */ type: MessageType.CF_AGENT_TOOL_RESULT /** The tool call ID this result is for */;
2464
+ toolCallId: string /** The name of the tool */;
2465
+ toolName: string /** The output from the tool execution */;
2466
+ output: unknown /** Override the tool part state (e.g. "output-error" for custom denial) */;
2467
+ state?:
2468
+ | "output-available"
2469
+ | "output-error" /** Error message when state is "output-error" */;
2470
+ errorText?: string /** Whether server should auto-continue the conversation after applying result */;
2471
+ autoContinue?: boolean /** Client tool schemas for continuation (client is source of truth) */;
2472
+ clientTools?: Array<{
2473
+ name: string;
2474
+ description?: string;
2475
+ parameters?: JSONSchema7;
2476
+ }>;
2477
+ }
2478
+ | {
2479
+ /** Client sends tool approval response to server (for tools with needsApproval) */ type: MessageType.CF_AGENT_TOOL_APPROVAL /** The tool call ID this approval is for */;
2480
+ toolCallId: string /** Whether the tool execution was approved */;
2481
+ approved: boolean /** Whether server should auto-continue the conversation after applying approval */;
2482
+ autoContinue?: boolean;
2483
+ };
2484
+ //#endregion
2485
+ //#region src/chat/connection.d.ts
2267
2486
  /**
2268
- * Alarm debounce: recovery alarms bunched within this window collapse into a
2269
- * single attempt. A deploy rollout drops/reconnects the socket several times
2270
- * over ~11–22s; without this, one logical deploy would burn several attempts.
2487
+ * Connection I/O — shared WebSocket send guard for chat agents.
2488
+ *
2489
+ * `@internal` — sibling-package support for `@cloudflare/ai-chat` and
2490
+ * `@cloudflare/think`, not a public API. See
2491
+ * `design/rfc-chat-recovery-foundation.md`.
2492
+ *
2493
+ * Both packages (and `continuation-state`) hand-maintained byte-identical
2494
+ * copies of `sendIfOpen` / `isWebSocketClosedSendError`; this is the single
2495
+ * shared implementation.
2271
2496
  */
2272
- declare const CHAT_RECOVERY_ALARM_DEBOUNCE_MS: number;
2273
2497
  /**
2274
- * Staleness bound for the live "recovering…" flag (#1620). A flag older than
2275
- * this is treated as abandoned so it can neither pin the indicator on forever
2276
- * nor suppress a genuinely-new recovering signal. NOT a recovery budget.
2498
+ * Minimal connection interface for sending WebSocket messages. Matches the
2499
+ * `Connection` type from `agents` without importing it: `Connection` extends
2500
+ * `WebSocket` with its own `send` overload, so it is structurally assignable.
2277
2501
  */
2278
- declare const CHAT_RECOVERING_FLAG_TTL_MS: number;
2502
+ interface ChatConnection {
2503
+ readonly id: string;
2504
+ send(message: string): void;
2505
+ }
2279
2506
  /**
2280
- * Resolve a raw `chatRecovery` config field into the fully-defaulted form the
2281
- * engine reasons about. Durable recovery is always enabled; a legacy runtime
2282
- * `false` value from previously compiled JavaScript safely receives defaults.
2507
+ * Send a message on a connection, swallowing the specific
2508
+ * "send after close" error a racing disconnect produces. Returns `true` if the
2509
+ * send went out, `false` if the socket was already closed. Any other error
2510
+ * rethrows.
2283
2511
  */
2284
- declare function resolveChatRecoveryConfig(
2285
- raw: ChatRecoveryConfig | undefined
2286
- ): ResolvedChatRecoveryConfig;
2512
+ declare function sendIfOpen(
2513
+ connection: ChatConnection,
2514
+ message: string
2515
+ ): boolean;
2516
+ //#endregion
2517
+ //#region src/chat/continuation-state.d.ts
2287
2518
  /**
2288
- * Sweep recovery incidents inactive past the TTL from durable storage. Lists by
2289
- * the incident key prefix, selects stale keys (`selectStaleIncidentKeys`), and
2290
- * batch-deletes them — the DO KV `delete([...])` accepts up to
2291
- * `KV_DELETE_MAX_KEYS` per call, collapsing N awaited round-trips into
2292
- * ceil(N / 128). Shared by `AIChatAgent` and `Think` so the sweep policy lives in
2293
- * one place. See `design/rfc-chat-recovery-foundation.md`.
2519
+ * Minimal connection interface for sending WebSocket messages. Alias of the
2520
+ * shared {@link ChatConnection} — kept as a named export for back-compat with
2521
+ * existing `ContinuationConnection` consumers.
2294
2522
  */
2295
- declare function sweepStaleChatRecoveryIncidents(
2296
- storage: Pick<DurableObjectStorage, "list" | "delete">,
2297
- now: number
2298
- ): Promise<void>;
2523
+ type ContinuationConnection = ChatConnection;
2524
+ interface ContinuationPending<
2525
+ TConnection extends ContinuationConnection = ContinuationConnection
2526
+ > {
2527
+ connection: TConnection;
2528
+ connectionId: string | null;
2529
+ requestId: string;
2530
+ clientTools?: ClientToolSchema[];
2531
+ body?: Record<string, unknown>;
2532
+ errorPrefix: string | null;
2533
+ prerequisite: Promise<boolean> | null;
2534
+ pastCoalesce: boolean;
2535
+ }
2536
+ interface ContinuationDeferred<
2537
+ TConnection extends ContinuationConnection = ContinuationConnection
2538
+ > {
2539
+ connection: TConnection;
2540
+ connectionId: string | null;
2541
+ clientTools?: ClientToolSchema[];
2542
+ body?: Record<string, unknown>;
2543
+ errorPrefix: string;
2544
+ prerequisite: Promise<boolean> | null;
2545
+ }
2546
+ declare class ContinuationState<
2547
+ TConnection extends ContinuationConnection = ContinuationConnection
2548
+ > {
2549
+ pending: ContinuationPending<TConnection> | null;
2550
+ deferred: ContinuationDeferred<TConnection> | null;
2551
+ activeRequestId: string | null;
2552
+ activeConnectionId: string | null;
2553
+ awaitingConnections: Map<string, TConnection>;
2554
+ /** Clear pending state and awaiting connections (without sending RESUME_NONE). */
2555
+ clearPending(): void;
2556
+ clearDeferred(): void;
2557
+ clearAll(): void;
2558
+ /**
2559
+ * Mark a connection as no longer available without canceling the
2560
+ * continuation it initiated.
2561
+ */
2562
+ releaseConnection(connectionId: string): void;
2563
+ /**
2564
+ * Send STREAM_RESUME_NONE to all connections waiting for a
2565
+ * continuation stream to start, then clear the map.
2566
+ */
2567
+ sendResumeNone(): void;
2568
+ /**
2569
+ * Flush awaiting connections by notifying each one via the provided
2570
+ * callback (typically sends STREAM_RESUMING), then clear.
2571
+ */
2572
+ flushAwaitingConnections(notify: (conn: TConnection) => void): void;
2573
+ /**
2574
+ * Transition pending → active. Called when the continuation stream
2575
+ * actually starts. Moves request/connection IDs to active slots,
2576
+ * clears pending fields.
2577
+ */
2578
+ activatePending(): void;
2579
+ /**
2580
+ * Transition deferred → pending. Called when a continuation turn
2581
+ * completes and there's a deferred follow-up waiting.
2582
+ *
2583
+ * Returns the new pending state (so the host can enqueue the turn),
2584
+ * or null if there was nothing deferred.
2585
+ */
2586
+ activateDeferred(
2587
+ generateRequestId: () => string
2588
+ ): ContinuationPending<TConnection> | null;
2589
+ }
2590
+ //#endregion
2591
+ //#region src/chat/pre-stream-turns.d.ts
2592
+ declare class PreStreamTurns<
2593
+ TConnection extends ChatConnection = ChatConnection
2594
+ > {
2595
+ /**
2596
+ * Accepted-but-not-yet-streamed request ids. A turn enters on `begin()` and
2597
+ * leaves on `settle()`; the set being non-empty means "pre-stream work is in
2598
+ * flight", which gates parking and the eventual `resume_none` release.
2599
+ */
2600
+ private readonly _accepted;
2601
+ /** Connections parked waiting for a stream to start. */
2602
+ readonly awaitingConnections: Map<string, TConnection>;
2603
+ /** The most recently accepted pre-stream request id (for the keep-waiting frame). */
2604
+ private _latestRequestId;
2605
+ /** Mark a freshly-accepted turn as in flight (pre-stream). */
2606
+ begin(requestId: string): void;
2607
+ /**
2608
+ * Mark an accepted turn as settled. Returns `true` when no accepted turn
2609
+ * remains in flight (the caller should release parked connections if no
2610
+ * stream is active).
2611
+ */
2612
+ settle(requestId: string): boolean;
2613
+ /** Whether any accepted turn is still pre-stream. */
2614
+ hasInFlight(): boolean;
2615
+ /** The request id to advertise in the keep-waiting frame, if known. */
2616
+ get latestRequestId(): string | null;
2617
+ /**
2618
+ * Park a reconnecting connection and tell it to keep waiting (so its
2619
+ * transport does not resolve `reconnectToStream` early). No-op when nothing
2620
+ * is in flight. Parked connections are deliberately NOT added to the host's
2621
+ * `pendingResumeConnections` — they must keep receiving any live broadcast —
2622
+ * until the host flushes them through `notifyStreamResuming` on stream start.
2623
+ */
2624
+ park(connection: TConnection, probeId?: string): boolean;
2625
+ /** Drop a single connection (e.g. on socket close) without releasing others. */
2626
+ release(connectionId: string): void;
2627
+ /**
2628
+ * A stream has started: hand every parked connection to `notify` (the host's
2629
+ * `notifyStreamResuming`, which sends `STREAM_RESUMING` and excludes the
2630
+ * connection from live broadcast until it ACKs), then clear the awaiting map.
2631
+ * The accepted set is untouched — the turn is still running.
2632
+ */
2633
+ flushOnStreamStart(notify: (connection: TConnection) => void): void;
2634
+ /**
2635
+ * Release every parked connection with `STREAM_RESUME_NONE` (the turn settled
2636
+ * without ever starting a stream) and clear the awaiting map. Safe to call
2637
+ * when the map is empty (no-op), so the host can call it liberally from a
2638
+ * turn-settle path.
2639
+ */
2640
+ releaseAwaiting(): void;
2641
+ /** Drop all state (chat clear / destroy). Does not send any frames. */
2642
+ reset(): void;
2643
+ }
2644
+ //#endregion
2645
+ //#region src/chat/auto-continuation-controller.d.ts
2299
2646
  /**
2300
- * List the persisted recovery incidents that are still live (status
2301
- * `detected` / `scheduled` / `attempting`) — i.e. NOT yet terminalized
2302
- * (`exhausted` / `failed`). Used by the alarm-boundary OOM circuit breaker
2303
- * (#1825) to find the incident(s) it must seal when the in-DO budgets could not.
2304
- * Lists by the incident key prefix so the storage layout stays encapsulated.
2647
+ * The data a host supplies to schedule (or re-target) a pending/deferred
2648
+ * auto-continuation. Mirrors the fields a host writes onto
2649
+ * {@link ContinuationState.pending} — the host owns where the values come from
2650
+ * (e.g. Think hardcodes a fixed `errorPrefix` and `body: undefined`; ai-chat
2651
+ * threads them per tool-result event).
2305
2652
  */
2306
- declare function listActiveChatRecoveryIncidents(
2307
- storage: Pick<DurableObjectStorage, "list">
2308
- ): Promise<
2309
- {
2310
- key: string;
2311
- incident: ChatRecoveryIncident;
2312
- }[]
2313
- >;
2653
+ interface ContinuationSpec<
2654
+ TConnection extends ContinuationConnection = ContinuationConnection
2655
+ > {
2656
+ connection: TConnection;
2657
+ clientTools: ClientToolSchema[] | undefined;
2658
+ body: Record<string, unknown> | undefined;
2659
+ errorPrefix: string;
2660
+ }
2314
2661
  /**
2315
- * Summarize a child agent's persisted recovery incidents for the parent's
2316
- * agent-tool reattach decision: `"in-progress"` if any incident is still live
2317
- * (detected/scheduled/attempting), else `"failed"` if any terminalized
2318
- * (exhausted/failed), else `"none"`. In-progress takes precedence so a parent
2319
- * never gives up on a child that is still recovering. Shared by `AIChatAgent`
2320
- * and `Think`. See `design/rfc-chat-recovery-foundation.md`.
2662
+ * Host substrate the controller parameterizes over. Implemented by the agent
2663
+ * (typically via a small adapter object capturing `this`).
2321
2664
  */
2322
- declare function classifyAgentToolChildRecovery(
2323
- storage: Pick<DurableObjectStorage, "list">
2324
- ): Promise<"in-progress" | "failed" | "none">;
2665
+ interface AutoContinuationHost<
2666
+ TConnection extends ContinuationConnection = ContinuationConnection
2667
+ > {
2668
+ /** Shared continuation state (pending/deferred/awaiting connections). */
2669
+ readonly continuation: ContinuationState<TConnection>;
2670
+ /** Generate a request id for a freshly-created continuation turn. */
2671
+ generateRequestId(): string;
2672
+ /**
2673
+ * `true` while an assistant turn is streaming — the parallel tool batch can
2674
+ * still grow with tool calls the model hasn't emitted yet, so no completeness
2675
+ * check is meaningful. (`_streamingAssistant !== null` in Think;
2676
+ * `_streamingTurnActive` in ai-chat.)
2677
+ */
2678
+ isStreamActive(): boolean;
2679
+ /** `true` while a tool-result/approval apply is in flight. */
2680
+ hasPendingInteraction(): boolean;
2681
+ /**
2682
+ * `true` when the latest assistant message is mid-batch (a settled tool
2683
+ * result beside an unanswered tool call/approval — the #1649 signature).
2684
+ */
2685
+ hasIncompleteToolBatch(): boolean;
2686
+ /**
2687
+ * Drain every in-flight tool-result/approval apply (including any enqueued
2688
+ * while draining) so the subsequent completeness re-check sees every result
2689
+ * that has already arrived. Bounded by real apply activity, never a timer.
2690
+ */
2691
+ drainInteractionApplies(): Promise<void>;
2692
+ /** Hold the isolate alive for the duration of `fn` (alarm heartbeats). */
2693
+ keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>;
2694
+ /**
2695
+ * Run the continuation turn for the current {@link ContinuationState.pending}.
2696
+ * Each host's inference/reply pipeline (Think: `_turnQueue.enqueue` +
2697
+ * `_runInferenceLoop`; ai-chat: `_runExclusiveChatTurn` + `onChatMessage`).
2698
+ * Reads everything it needs from `continuation.pending`, so it takes no args.
2699
+ */
2700
+ fire(): void;
2701
+ }
2702
+ declare class AutoContinuationController<
2703
+ TConnection extends ContinuationConnection = ContinuationConnection
2704
+ > {
2705
+ private readonly host;
2706
+ /**
2707
+ * Small debounce window to batch adjacent client-side tool results/approvals
2708
+ * into a single server continuation barrier check (#1650).
2709
+ */
2710
+ static readonly COALESCE_MS = 50;
2711
+ /**
2712
+ * Coalesce/debounce timer for the event-driven barrier (#1650). Each tool
2713
+ * result/approval re-arms it; on fire it runs {@link fireWhenStable}.
2714
+ */
2715
+ private _timer;
2716
+ /**
2717
+ * Double-fire guard (#1650). Ensures only one in-flight apply-drain runs;
2718
+ * that drain re-checks completeness on completion before firing. A sibling
2719
+ * that re-arms the coalesce timer during a drain is absorbed by the
2720
+ * in-progress drain rather than starting its own.
2721
+ */
2722
+ private _barrierActive;
2723
+ constructor(host: AutoContinuationHost<TConnection>);
2724
+ /**
2725
+ * Schedule an auto-continuation for a tool result/approval that opted in with
2726
+ * `autoContinue` (#1650). Coalesces rapid sibling results into a single
2727
+ * continuation via the debounce timer; the actual fire is gated by
2728
+ * {@link fireWhenStable}. If a continuation is already running
2729
+ * (`pastCoalesce`), the new result is stored as the deferred follow-up
2730
+ * instead of re-arming.
2731
+ */
2732
+ schedule(spec: ContinuationSpec<TConnection>): void;
2733
+ /**
2734
+ * Re-arm the barrier for a result/approval that arrived WITHOUT `autoContinue`
2735
+ * (#1650). A standalone errored result declines to continue on its own, but in
2736
+ * a parallel batch a SIBLING may already have opted in — and this result can
2737
+ * be the one that completes the batch, so we must re-run the barrier check.
2738
+ * Unlike {@link schedule} this NEVER creates a pending continuation, and
2739
+ * no-ops once the continuation is running (`pastCoalesce`).
2740
+ */
2741
+ rearmForBatch(): void;
2742
+ /** (Re)arm the coalesce timer; on fire, run {@link fireWhenStable}. */
2743
+ armTimer(): void;
2744
+ /**
2745
+ * Fire an auto-continuation, but only once the model's parallel tool-call
2746
+ * batch is fully answered (#1649) and no assistant turn is mid-stream (#1650).
2747
+ * The barrier is event-driven with NO orphan timeout: when the batch is still
2748
+ * incomplete we drain the in-flight applies, re-check, and — if still
2749
+ * incomplete — return WITHOUT firing and WITHOUT holding the isolate, leaving
2750
+ * `continuation.pending` in place. The next sibling's result re-arms the
2751
+ * coalesce timer and re-runs this check; the continuation fires once the final
2752
+ * sibling lands. A true orphan (a sibling that never arrives) simply never
2753
+ * auto-continues — a later user turn / chat recovery repairs the transcript.
2754
+ */
2755
+ fireWhenStable(): void;
2756
+ /**
2757
+ * Transition the deferred follow-up (stored while a continuation was running)
2758
+ * to pending and re-run the barrier — its batch may still be incomplete (or a
2759
+ * stream active), in which case it parks and re-arms instead of firing blind.
2760
+ */
2761
+ activateDeferredAndReschedule(): void;
2762
+ /**
2763
+ * Cancel any still-armed coalesce timer. Called on the fire path so a sibling
2764
+ * result that re-armed it during a barrier wait can't fire a duplicate
2765
+ * continuation after this one starts (#1649 / #1650).
2766
+ */
2767
+ cancelTimer(): void;
2768
+ /**
2769
+ * `true` when the barrier is going to fire on its own — its coalesce timer is
2770
+ * still pending or its completeness drain is in progress. The host combines
2771
+ * this with its own pending/`pastCoalesce` checks to decide idle/stable.
2772
+ */
2773
+ isArmed(): boolean;
2774
+ /**
2775
+ * Tear down the controller-owned barrier state (timer + double-fire guard).
2776
+ * Scoped to ONLY this controller's fields — the host clears the rest of its
2777
+ * turn state (stream gate, interaction tail, continuation data) separately.
2778
+ */
2779
+ reset(): void;
2780
+ }
2781
+ //#endregion
2782
+ //#region src/chat/abort-registry.d.ts
2325
2783
  /**
2326
- * Read the durable monotonic recovery-progress counter (0 when unset). The value
2327
- * feeds the no-progress budget decision; shared by `AIChatAgent` and `Think`.
2784
+ * AbortRegistry — manages per-request AbortControllers.
2785
+ *
2786
+ * Shared between AIChatAgent and Think for chat turn cancellation.
2787
+ * Each request gets its own AbortController keyed by request ID.
2788
+ * Controllers are created lazily on first signal access.
2328
2789
  */
2329
- declare function readChatRecoveryProgress(
2330
- storage: Pick<DurableObjectStorage, "get">
2331
- ): Promise<number>;
2790
+ declare class AbortRegistry {
2791
+ private controllers;
2792
+ /**
2793
+ * Get or create an AbortController for the given ID and return its signal.
2794
+ * Creates the controller lazily on first access.
2795
+ */
2796
+ getSignal(id: string): AbortSignal | undefined;
2797
+ /**
2798
+ * Get the signal for an existing controller without creating one.
2799
+ * Returns undefined if no controller exists for this ID.
2800
+ */
2801
+ getExistingSignal(id: string): AbortSignal | undefined;
2802
+ /**
2803
+ * Cancel a specific request by aborting its controller. Optionally
2804
+ * propagate a reason — surfaces as `signal.reason` on the registry's
2805
+ * controller and through any `AbortError` it produces downstream.
2806
+ */
2807
+ cancel(id: string, reason?: unknown): void;
2808
+ /** Remove a controller after the request completes. */
2809
+ remove(id: string): void;
2810
+ /**
2811
+ * Abort all pending requests and clear the registry. Optionally propagate a
2812
+ * reason — surfaces as `signal.reason` on each controller and through any
2813
+ * `AbortError` it produces downstream, exactly like {@link cancel}.
2814
+ */
2815
+ destroyAll(reason?: unknown): void;
2816
+ /** Check if a controller exists for the given ID. */
2817
+ has(id: string): boolean;
2818
+ /** Number of tracked controllers. */
2819
+ get size(): number;
2820
+ /**
2821
+ * Link an external `AbortSignal` to the controller for `id`. When the
2822
+ * external signal aborts, the registry's controller is cancelled —
2823
+ * propagating the abort reason — exactly the same way an internal
2824
+ * cancel would (e.g. via a `chat-request-cancel` WebSocket message).
2825
+ *
2826
+ * This is the integration point for callers that drive a chat turn
2827
+ * programmatically and want to cancel it from outside without knowing
2828
+ * the internally-generated request id (e.g. the helper-as-sub-agent
2829
+ * pattern, where a parent's `AbortSignal` from the AI SDK tool
2830
+ * `execute` needs to land inside a `Think.saveMessages` call running
2831
+ * on a child DO).
2832
+ *
2833
+ * Behavior:
2834
+ *
2835
+ * - Passing `undefined` is a no-op and returns a no-op detacher, so
2836
+ * callers can unconditionally call this with `options?.signal`.
2837
+ * - If the external signal is already aborted, the registry's
2838
+ * controller is created (if needed) and cancelled synchronously.
2839
+ * - Otherwise a one-shot `abort` listener is attached. The returned
2840
+ * function detaches it.
2841
+ *
2842
+ * **Always call the returned detacher in a `finally` block** — the
2843
+ * external signal may outlive the request (a parent chat turn that
2844
+ * drives many helper turns reuses one signal across all of them) and
2845
+ * leaving listeners attached pins closures and grows the listener
2846
+ * list on each turn.
2847
+ *
2848
+ * @returns A detacher function. Call it after the request finishes
2849
+ * (success or failure) to remove the abort listener from `signal`.
2850
+ */
2851
+ linkExternal(id: string, signal: AbortSignal | undefined): () => void;
2852
+ }
2853
+ //#endregion
2854
+ //#region src/chat/async-helpers.d.ts
2332
2855
  /**
2333
- * Advance the durable recovery-progress counter by one. Called when genuinely new
2334
- * content is durably flushed (real, reconnect-immune forward progress); shared by
2335
- * `AIChatAgent` and `Think`.
2856
+ * @internal Small async control-flow helpers shared by the chat hosts
2857
+ * (`@cloudflare/ai-chat` and `@cloudflare/think`) — not a public API. Extracted
2858
+ * so the host idle/stable waits and the interaction-apply completeness drain
2859
+ * stay byte-identical across both. See `design/chat-shared-layer.md`.
2336
2860
  */
2337
- declare function bumpChatRecoveryProgress(
2338
- storage: Pick<DurableObjectStorage, "get" | "put">
2339
- ): Promise<void>;
2340
2861
  /**
2341
- * Throttle window for crediting a parent turn's recovery progress from forwarded
2342
- * sub-agent (agent-tool) stream chunks (N9). Forwarding a child's chunks IS
2343
- * forward progress for the parent, but the credit must not write storage per
2344
- * token.
2862
+ * Sentinel returned by {@link awaitWithDeadline} when the deadline elapses
2863
+ * before the awaited promise settles. A single shared symbol so both hosts
2864
+ * compare against the same identity.
2345
2865
  */
2346
- declare const AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS = 5000;
2866
+ declare const TIMED_OUT: unique symbol;
2347
2867
  /**
2348
- * Per-isolate throttle gate for agent-tool stream-progress crediting (N9). The
2349
- * `_lastBumpAt` clock is in-memory, so it resets per isolate and the first
2350
- * forwarded chunk after a restart always credits. `shouldCredit(now)` returns
2351
- * `true` at most once per `AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS` window and
2352
- * records the time on each credit. Shared by `AIChatAgent` and `Think`.
2868
+ * Await `promise`, but give up and resolve to {@link TIMED_OUT} once `deadline`
2869
+ * (an absolute `Date.now()` ms timestamp) passes. A `null` deadline waits
2870
+ * indefinitely (the promise is returned unchanged). The timeout timer is always
2871
+ * cleared so it can't pin the isolate awake past resolution.
2353
2872
  */
2354
- declare class AgentToolStreamProgressThrottle {
2355
- private _lastBumpAt;
2356
- shouldCredit(now: number): boolean;
2357
- }
2873
+ declare function awaitWithDeadline<T>(
2874
+ promise: Promise<T>,
2875
+ deadline: number | null
2876
+ ): Promise<T | typeof TIMED_OUT>;
2358
2877
  /**
2359
- * Throttle window for crediting recovery progress from mid-segment streaming
2360
- * content (text/reasoning/tool-input deltas). A milestone chunk credits
2361
- * unconditionally; deltas credit at most once per window so a long single
2362
- * segment registers forward progress across crashes without writing storage per
2363
- * token. 5s is far finer than the 300s no-progress budget, so any crash gap
2364
- * longer than this window over an actively-streaming segment still credits.
2878
+ * Drain the host's interaction-apply chain so a subsequent completeness check
2879
+ * (e.g. `hasIncompleteToolBatch`) sees every tool result that has ALREADY
2880
+ * arrived.
2881
+ *
2882
+ * Bounded by real apply activity (a storage write each), never a fixed timer:
2883
+ * `getTail` is re-read after every await because a sibling can extend the tail
2884
+ * mid-drain, and the loop stops once the tail stops advancing. Bails early when
2885
+ * `hasPending()` goes false (the pending continuation was cleared by a chat
2886
+ * clear / turn reset) so a stale drain can't hold the isolate awake.
2887
+ */
2888
+ declare function drainInteractionApplies(
2889
+ hasPending: () => boolean,
2890
+ getTail: () => Promise<unknown>
2891
+ ): Promise<void>;
2892
+ //#endregion
2893
+ //#region src/chat/tool-state.d.ts
2894
+ /**
2895
+ * Tool State — shared update builders and applicator for tool part state changes.
2896
+ *
2897
+ * Used by both AIChatAgent and Think to apply tool results and approvals
2898
+ * to message parts. Each agent handles find-message, persist, and broadcast
2899
+ * in their own way; this module provides the state matching and update logic.
2365
2900
  */
2366
- declare const CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS = 5000;
2367
2901
  /**
2368
- * Per-isolate throttle gate for crediting recovery progress from mid-segment
2369
- * streaming-content chunks — the delta arm of {@link shouldCreditStreamProgress}.
2370
- * The `_lastBumpAt` clock is in-memory, so it resets per isolate and the first
2371
- * delta after a restart always credits. Shared by `AIChatAgent` and `Think`.
2902
+ * Describes an update to apply to a tool part.
2372
2903
  */
2373
- declare class StreamProgressCreditThrottle {
2374
- private _lastBumpAt;
2375
- shouldCredit(now: number): boolean;
2376
- }
2377
- /** Durable record of the last turn that ended in a terminal error (#1645). */
2378
- type ChatTerminalRecord = {
2379
- requestId: string;
2380
- body: string;
2904
+ type ToolPartUpdate = {
2905
+ toolCallId: string;
2906
+ matchStates: string[];
2907
+ apply: (part: Record<string, unknown>) => Record<string, unknown>;
2381
2908
  };
2382
2909
  /**
2383
- * Persist a durable record of the last terminal turn so a client that
2384
- * (re)connects after the turn ended still learns its outcome (#1645). Kept
2385
- * until a later turn supersedes it ({@link clearChatTerminal}); a single record
2386
- * is sufficient because only the most recent terminal is relevant.
2910
+ * Apply a tool part update to a parts array.
2911
+ * Finds the first part matching `update.toolCallId` in one of `update.matchStates`,
2912
+ * applies the update immutably, and returns the new parts array with the index.
2913
+ *
2914
+ * Returns `null` if no matching part was found.
2387
2915
  */
2388
- declare function recordChatTerminal(
2389
- storage: Pick<DurableObjectStorage, "put">,
2390
- requestId: string,
2391
- body: string
2392
- ): Promise<void>;
2393
- /** Clear the durable terminal record once a later turn supersedes it (#1645). */
2394
- declare function clearChatTerminal(
2395
- storage: Pick<DurableObjectStorage, "delete">
2396
- ): Promise<void>;
2397
- /** Read the pending terminal record, or `null` if none is stored (#1645). */
2398
- declare function pendingChatTerminal(
2399
- storage: Pick<DurableObjectStorage, "get">
2400
- ): Promise<ChatTerminalRecord | null>;
2916
+ declare function applyToolUpdate(
2917
+ parts: Array<Record<string, unknown>>,
2918
+ update: ToolPartUpdate
2919
+ ): {
2920
+ parts: Array<Record<string, unknown>>;
2921
+ index: number;
2922
+ } | null;
2401
2923
  /**
2402
- * Build the on-connect "recovering…" replay frame (#1620), or `null` when no
2403
- * (non-stale) recovery is in progress. A client that connects between recovery
2404
- * attempts (no active stream) reads the turn as working rather than frozen. A
2405
- * record older than the flag TTL is treated as abandoned (its terminal-clear
2406
- * never ran) and skipped, so a dead recovery can't show "recovering…" forever.
2407
- * `messageType` is the package's recovering wire-type enum.
2924
+ * Build an update descriptor for applying a tool result.
2925
+ *
2926
+ * Matches parts in `input-available`, `approval-requested`, or `approval-responded` state.
2927
+ * Sets state to `output-available` (with output) or `output-error` (with errorText).
2408
2928
  */
2409
- declare function buildChatRecoveringFrame(
2410
- storage: Pick<DurableObjectStorage, "get">,
2411
- messageType: string,
2412
- now: number
2413
- ): Promise<Record<string, unknown> | null>;
2929
+ declare function toolResultUpdate(
2930
+ toolCallId: string,
2931
+ output: unknown,
2932
+ overrideState?: "output-error",
2933
+ errorText?: string
2934
+ ): ToolPartUpdate;
2414
2935
  /**
2415
- * Set or clear the live "recovering…" status (#1620). Persists a durable record
2416
- * (so set/clear stay consistent across the isolates a recovery spans) and
2417
- * broadcasts a recovering frame — but only on a genuine transition, so a
2418
- * deploy/reconnect storm (which re-detects recovery many times) doesn't spam
2419
- * the wire. A flag older than the TTL is stale: the owning incident was
2420
- * abandoned without a terminal (e.g. the DO went idle before recovery could
2421
- * resolve), so it is treated as not-recovering and can neither pin the
2422
- * indicator on forever nor suppress a genuinely-new recovering signal.
2423
- * `messageType` is the package's recovering wire-type enum; `broadcast` is the
2424
- * package's chat-broadcast wrapper.
2936
+ * Build an update descriptor for a terminal tool result that belongs to a
2937
+ * tool part in a *different* (earlier) assistant message than the one
2938
+ * currently being streamed.
2939
+ *
2940
+ * This is the "cross-message" case: an approved server tool executes during a
2941
+ * continuation stream, but its tool part lives in the assistant message that
2942
+ * originally requested it. `StreamAccumulator` surfaces this as a
2943
+ * `cross-message-tool-update` action because the accumulator only owns the
2944
+ * current turn's new content and cannot mutate a part from a prior message.
2945
+ *
2946
+ * Compared to {@link toolResultUpdate} this builder is deliberately more
2947
+ * defensive, mirroring the equivalent fallback in `@cloudflare/ai-chat`:
2948
+ *
2949
+ * - It matches the broad set of pre-terminal **and** terminal states, so a
2950
+ * provider that replays the entire prior tool round-trip during a
2951
+ * continuation (notably the OpenAI Responses API — issue #1404) still
2952
+ * resolves to the same part instead of silently missing it.
2953
+ * - It is **first-write-wins**: a chunk arriving for a tool that already holds
2954
+ * a terminal result is treated as a replay and the existing output is never
2955
+ * overwritten. In that case `apply` returns the *same part reference*, which
2956
+ * callers use as an idempotent-no-op signal to skip the durable write and a
2957
+ * redundant `MESSAGE_UPDATED` broadcast.
2958
+ * - It preserves a streamed `preliminary` flag when one is present, otherwise
2959
+ * marks the result final (`preliminary: false`).
2425
2960
  */
2426
- declare function setChatRecovering(
2427
- active: boolean,
2428
- requestId: string | undefined,
2429
- deps: {
2430
- storage: Pick<DurableObjectStorage, "get" | "put" | "delete">;
2431
- messageType: string;
2432
- broadcast: (frame: Record<string, unknown>) => void;
2433
- now: number;
2434
- }
2435
- ): Promise<void>;
2961
+ declare function crossMessageToolResultUpdate(
2962
+ toolCallId: string,
2963
+ updateType: "output-available" | "output-error",
2964
+ output?: unknown,
2965
+ errorText?: string,
2966
+ preliminary?: boolean
2967
+ ): ToolPartUpdate;
2436
2968
  /**
2437
- * Observability event produced by an incident evaluation or a status
2438
- * transition, emitted by the caller. The `detected`/`attempt` events come from
2439
- * the budget evaluation (begin path); the `scheduled` event comes from
2440
- * `ChatRecoveryEngine.scheduleRecovery`; the `completed`/`skipped`/`failed`
2441
- * events come from `ChatRecoveryEngine.updateIncident`. `reason` is carried only
2442
- * by the `skipped`/`failed` transitions that record a cause.
2969
+ * Build an update descriptor that replaces the output of a *paused durable
2970
+ * execution* tool part (e.g. a codemode runtime tool that paused for
2971
+ * approval).
2972
+ *
2973
+ * A paused execution completes its tool call normally — the part is already
2974
+ * `output-available` with an output of `{ status: "paused", executionId }`.
2975
+ * When the host later approves/rejects the execution, the new outcome
2976
+ * (completed / rejected / paused-again) must replace that output in place.
2977
+ *
2978
+ * Matching is deliberately narrow and idempotent:
2979
+ *
2980
+ * - only `output-available` parts are considered;
2981
+ * - the existing output must be a paused-execution object carrying the same
2982
+ * `executionId` — anything else (already replaced, different execution)
2983
+ * returns the *same part reference*, which callers treat as a no-op signal
2984
+ * (skip persist + broadcast), mirroring {@link crossMessageToolResultUpdate}.
2443
2985
  */
2444
- type ChatRecoveryIncidentEvent = {
2445
- type:
2446
- | "chat:recovery:detected"
2447
- | "chat:recovery:attempt"
2448
- | "chat:recovery:scheduled"
2449
- | "chat:recovery:completed"
2450
- | "chat:recovery:skipped"
2451
- | "chat:recovery:failed";
2452
- incidentId: string;
2453
- requestId: string;
2454
- attempt: number;
2455
- maxAttempts: number;
2456
- recoveryKind: ChatRecoveryKind;
2457
- reason?: string;
2458
- };
2459
- type EvaluateChatRecoveryIncidentInput = {
2460
- /** Recovery identity for this turn. */ identity: {
2461
- requestId: string;
2462
- recoveryRootRequestId?: string | null;
2463
- latestUserMessageId?: string | null;
2464
- recoveryKind: ChatRecoveryKind;
2465
- } /** Fully-resolved recovery config. */;
2466
- config: ResolvedChatRecoveryConfig /** The existing incident for this identity, or `null` if this is fresh. */;
2467
- existing: ChatRecoveryIncident | null /** Current value of the durable monotonic progress counter. */;
2468
- currentProgress: number;
2469
- /**
2470
- * Whether the turn is parked on a pending CLIENT interaction (an
2471
- * `input-available` client-tool part or an `approval-requested` part). Such a
2472
- * turn is waiting on the human, not stuck, so it is budget-free.
2473
- */
2474
- awaitingClientInteraction: boolean /** Injected clock (epoch ms) for deterministic tests. */;
2475
- now: number;
2476
- /**
2477
- * Invoked when `config.shouldKeepRecovering` throws. Lets each package keep
2478
- * its own log prefix. A throwing predicate is treated as "keep recovering".
2479
- */
2480
- onShouldKeepRecoveringError?: (error: unknown) => void;
2481
- };
2482
- type EvaluateChatRecoveryIncidentResult = {
2483
- /** The next incident record to persist. */ incident: ChatRecoveryIncident /** Whether this incident is now sealed as exhausted. */;
2484
- exhausted: boolean /** Observability events to emit, in order. */;
2485
- events: ChatRecoveryIncidentEvent[];
2486
- };
2487
- //#endregion
2488
- //#region src/chat/recovery-engine.d.ts
2489
- /** The scheduled-callback entrypoints a recovery schedule can target. */
2490
- type ChatRecoveryScheduleCallback =
2491
- | "_chatRecoveryContinue"
2492
- | "_chatRecoveryRetry";
2986
+ declare function pausedExecutionUpdate(
2987
+ toolCallId: string,
2988
+ executionId: string,
2989
+ output: unknown
2990
+ ): ToolPartUpdate;
2493
2991
  /**
2494
- * Why a recovery callback is being scheduled. The idempotency of the underlying
2495
- * `schedule()` call depends ONLY on this:
2496
- *
2497
- * - `"initial"` — the first schedule of a continuation/retry when an interrupted
2498
- * turn is detected on wake. A deploy rollout drops/reconnects the socket
2499
- * several times, re-triggering detection; idempotent scheduling (dedup on
2500
- * callback + payload) collapses that storm into a single enqueued continuation
2501
- * instead of N duplicates.
2992
+ * Build an update descriptor for applying a tool approval.
2502
2993
  *
2503
- * - `"stable_timeout_retry"` — a reschedule issued from INSIDE the currently-
2504
- * executing one-shot schedule row (a continuation that timed out waiting for
2505
- * stable state). `alarm()` deletes that row only AFTER the callback returns,
2506
- * so an idempotent reschedule would dedup onto the doomed row and be deleted
2507
- * with it — the retry would never fire. A fresh (non-idempotent) delayed row
2508
- * survives the deletion.
2994
+ * Matches parts in `input-available` or `approval-requested` state.
2995
+ * Sets state to `approval-responded` (if approved) or `output-denied` (if denied).
2509
2996
  */
2510
- type ChatRecoveryScheduleReason = "initial" | "stable_timeout_retry";
2997
+ declare function toolApprovalUpdate(
2998
+ toolCallId: string,
2999
+ approved: boolean
3000
+ ): ToolPartUpdate;
3001
+ /** A minimal message shape for the leaf tool/interaction scans. */
3002
+ type ToolBatchMessage = {
3003
+ role: string;
3004
+ parts: ReadonlyArray<unknown>;
3005
+ };
3006
+ /** Extract a tool part's name from its `tool-<name>` / `dynamic-tool` shape. */
2511
3007
  /**
2512
- * A reconstructed orphaned-stream partial. The engine seam is deliberately
2513
- * **wire-vocabulary-agnostic**: `text` is the accumulated assistant text and
2514
- * `parts` is OPAQUE to the engine (`unknown[]`) — each host casts it back to its
2515
- * own message-part vocabulary (AI SDK `UIMessage` parts, AG-UI tool parts, …).
2516
- * The single fact the engine needs about parts — does the partial carry settled
2517
- * (non-idempotent) tool work that must survive a `{ persist: false }` recovery
2518
- * (#1631)? — is precomputed by the {@link ChatRecoveryCodec} as
2519
- * `hasSettledToolResults`. So the engine never imports a part vocabulary; the
2520
- * codec owns it (see `partialHasSettledToolResults` in `recovery-codec.ts` for
2521
- * the AI SDK codec's implementation of that predicate).
3008
+ * Whether a part is still awaiting a CLIENT interaction that can genuinely
3009
+ * arrive after a restart: an `approval-requested` part (a reconnecting client
3010
+ * replays the approval) or an `input-available` part for a CLIENT tool (the SPA
3011
+ * replays the `tool-result`). A SERVER tool's `input-available` is NOT pending —
3012
+ * its `execute()` died with the isolate.
3013
+ */
3014
+ declare function partAwaitsClientInteraction(
3015
+ part: unknown,
3016
+ clientResolvable: Set<string>
3017
+ ): boolean;
3018
+ /**
3019
+ * Names of the CLIENT-resolvable tools — the client-provided schemas from the
3020
+ * last request, which have no server `execute`. An interrupted `input-available`
3021
+ * part for one of these can still be resolved by the client replaying a
3022
+ * `tool-result`; a server tool's cannot.
3023
+ */
3024
+ declare function clientResolvableToolNames(
3025
+ tools:
3026
+ | ReadonlyArray<
3027
+ | {
3028
+ name?: string;
3029
+ }
3030
+ | null
3031
+ | undefined
3032
+ >
3033
+ | undefined
3034
+ ): Set<string>;
3035
+ /**
3036
+ * `true` when the latest assistant message is mid-batch: it carries at least
3037
+ * one settled tool result AND at least one tool call/approval still awaiting a
3038
+ * client result. That is the #1649 signature — the model fanned out parallel
3039
+ * tool calls and only some have been answered. Scoped to the leaf (the step the
3040
+ * continuation answers) so an unrelated dangling tool in an earlier message
3041
+ * doesn't block a legitimate follow-up continuation.
2522
3042
  */
2523
- type RecoveryPartial = {
2524
- text: string;
2525
- parts: unknown[];
2526
- hasSettledToolResults: boolean;
2527
- };
2528
- /** Lifecycle status of a recovered stream's metadata row. */
2529
- type ChatStreamStatus = "streaming" | "completed" | "error";
3043
+ declare function hasIncompleteToolBatch(
3044
+ messages: ReadonlyArray<ToolBatchMessage>
3045
+ ): boolean;
3046
+ //#endregion
3047
+ //#region src/chat/parse-protocol.d.ts
2530
3048
  /**
2531
- * Resolve the `schedule()` idempotency option for a recovery schedule. Single
2532
- * source of truth for both packages; see {@link ChatRecoveryScheduleReason} for
2533
- * the rationale behind each case.
3049
+ * Protocol Message Parser — typed parsing of cf_agent_chat_* WebSocket messages.
2534
3050
  *
2535
- * This is a cutover invariant: flipping either case silently breaks deploy-storm
2536
- * dedup (initial) or stalls stable-timeout retries (reschedule), and neither is
2537
- * caught by a type error — only by the recovery suites.
3051
+ * Parses raw WebSocket messages into a discriminated union of protocol events.
3052
+ * Both AIChatAgent and Think can use this instead of manual JSON.parse + type checking.
2538
3053
  */
2539
- declare function chatRecoverySchedulePolicy(
2540
- reason: ChatRecoveryScheduleReason
2541
- ): {
2542
- idempotent: boolean;
2543
- };
2544
- /** Identity + context for opening (or re-evaluating) a recovery incident. */
2545
- interface BeginChatRecoveryIncidentInput {
2546
- requestId: string;
2547
- recoveryRootRequestId?: string | null;
2548
- latestUserMessageId?: string | null;
2549
- recoveryKind: ChatRecoveryKind;
2550
- /** Test-only clock injection for deterministic debounce/window timing. */
2551
- nowMs?: number;
2552
- }
2553
- interface BeginChatRecoveryIncidentResult {
2554
- incident: ChatRecoveryIncident;
2555
- config: ResolvedChatRecoveryConfig;
2556
- exhausted: boolean;
2557
- }
2558
3054
  /**
2559
- * Package-specific host operations the engine drives during incident
2560
- * orchestration. Every method is a thin pass-through to the package's existing
2561
- * storage / clock / event / interaction primitives — the engine owns only the
2562
- * *sequence*, not the I/O.
3055
+ * Discriminated union of all incoming chat protocol events.
3056
+ *
3057
+ * Each agent handles the events it cares about and ignores the rest.
3058
+ * Returns `null` for non-JSON messages or unrecognized types.
2563
3059
  */
2564
- interface ChatRecoveryAdapter {
2565
- /** Resolve the effective recovery config (defaults + caller overrides). */
2566
- resolveConfig(): ResolvedChatRecoveryConfig;
2567
- /** Wall clock; only consulted when the input carries no test `nowMs`. */
2568
- now(): number;
2569
- /** Evict incidents past the TTL. Runs before the existing-record read. */
2570
- sweepStaleIncidents(now: number): Promise<void>;
2571
- /** Read the persisted incident for `key`, or `null` if none. */
2572
- getIncident(key: string): Promise<ChatRecoveryIncident | null>;
2573
- /**
2574
- * Optional: rehydrate any state the interaction predicate depends on. Invoked
2575
- * after the existing-incident read and BEFORE `isAwaitingClientInteraction`.
2576
- * `Think` uses this to restore client tools from durable storage on a cold
2577
- * boot-recovery wake (so a HITL turn is not misread as stuck); `AIChatAgent`
2578
- * has no such state and omits it.
2579
- */
2580
- ensureInteractionStateLoaded?(): void;
2581
- /**
2582
- * Optional: give the package a chance to handle a NON-chat fiber before chat
2583
- * recovery inspects it. Returns `true` if the package fully consumed the
2584
- * fiber, in which case the engine tells the caller to skip chat-recovery
2585
- * processing for it. `Think` uses this for its messenger/workflow reply fibers
2586
- * (`think:messenger-reply`); `AIChatAgent` has no non-chat fibers and omits it
2587
- * (the engine then treats every recovered fiber as a chat-recovery candidate).
2588
- *
2589
- * Ordering invariant: the engine dispatches this FIRST, before the
2590
- * chat-fiber-name gate, so a non-chat fiber is never misclassified as an
2591
- * orphaned chat turn.
2592
- */
2593
- tryHandleNonChatFiberRecovery?(ctx: FiberRecoveryContext): Promise<boolean>;
2594
- /** Monotonic forward-progress marker for the no-progress budget. */
2595
- readProgress(): Promise<number>;
2596
- /**
2597
- * Whether the turn is parked on a pending CLIENT interaction (waiting on the
2598
- * human, not stuck). When true the engine keeps the incident budget-free.
2599
- * Optional: a host with no client-interaction/HITL substrate (e.g. the pi
2600
- * fixture) omits it and the engine treats the turn as never parked (`false`).
2601
- */
2602
- isAwaitingClientInteraction?(): boolean;
2603
- /** Persist the evaluated incident under `key`. */
2604
- putIncident(key: string, incident: ChatRecoveryIncident): Promise<void>;
2605
- /**
2606
- * Delete the incident record under `key`. The engine calls this on the
2607
- * terminal `completed` transition (a completed recovery is never retried, so
2608
- * its record is dropped rather than left in storage forever).
2609
- */
2610
- deleteIncident(key: string): Promise<void>;
2611
- /** Broadcast a lifecycle event produced by the evaluation or a transition. */
2612
- emitRecoveryEvent(event: ChatRecoveryIncidentEvent): void;
2613
- /**
2614
- * Enqueue a recovery callback. A thin pass-through to the package's
2615
- * `schedule(delaySeconds, callback, data, chatRecoverySchedulePolicy(reason))`
2616
- * — the engine owns the surrounding orchestration (the transition + emit for
2617
- * the initial schedule in {@link ChatRecoveryEngine.scheduleRecovery}, the
2618
- * attempt bump for {@link ChatRecoveryEngine.rescheduleAfterStableTimeout});
2619
- * the package owns the Durable Object alarm write and the payload shape.
2620
- * `reason` selects the idempotency policy and `delaySeconds` the alarm delay
2621
- * (`0` for the initial enqueue, `CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS` for
2622
- * a stable-timeout reschedule).
2623
- */
2624
- scheduleRecovery(
2625
- callback: ChatRecoveryScheduleCallback,
2626
- data: Record<string, unknown>,
2627
- reason: ChatRecoveryScheduleReason,
2628
- delaySeconds: number
2629
- ): Promise<void>;
2630
- /**
2631
- * Set or clear the live "recovering…" status (#1620). The engine calls this on
2632
- * the incident transitions: `scheduled` → active (keyed by the recovery-root
2633
- * request id, falling back to the incident's request id), and
2634
- * `completed`/`skipped`/`failed` → cleared. The package owns the underlying
2635
- * staleness / idempotency / broadcast I/O.
2636
- */
2637
- setRecovering(active: boolean, requestId?: string): Promise<void>;
2638
- /**
2639
- * Report a throw from the caller's `shouldKeepRecovering` hook. Optional: a
2640
- * host that does not surface this diagnostic omits it (the engine swallows the
2641
- * report).
2642
- */
2643
- onShouldKeepRecoveringError?(error: unknown): void;
2644
- /**
2645
- * Terminalize a given-up recovery turn: deliver the exhaustion notification
2646
- * plus the package-owned terminal record / banner / submission writes. A thin
2647
- * pass-through to the package's `_exhaustChatRecovery` (which composes
2648
- * {@link runChatRecoveryExhaustion}). Driven by
2649
- * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}; the engine owns the
2650
- * surrounding read → re-entry-guard → build → terminalize → seal sequence, the
2651
- * package owns the terminal writes (uniformly broadcast-first; their set
2652
- * differs — `Think` also writes a submission row).
2653
- */
2654
- exhaustChatRecovery(
2655
- incident: ChatRecoveryIncident,
2656
- config: ResolvedChatRecoveryConfig,
2657
- partial: RecoveryPartial,
2658
- streamId: string,
2659
- createdAt: number
2660
- ): Promise<void>;
2661
- /**
2662
- * Resolve the orphaned stream identity for a (recovery-root) request id —
2663
- * `streamId` is `""` when no stream metadata survives. Drives BOTH the wake
2664
- * path (which consumes the full {@link ResolvedRecoveryStream}) and the
2665
- * give-up path (which reads only `.streamId`). A thin pass-through to the
2666
- * package's stream-metadata lookup: the newest row keyed by the request id,
2667
- * else the live active stream.
2668
- */
2669
- resolveRecoveryStream(requestId: string): ResolvedRecoveryStream;
2670
- /** Reconstruct the partial text/parts buffered for `streamId`. */
2671
- getPartialStreamText(streamId: string): RecoveryPartial;
2672
- /**
2673
- * The in-flight recovery-root request id, consulted as a fallback in the
2674
- * give-up root-id chain when the payload carries no `originalRequestId` /
2675
- * `recoveredRequestId` and no incident record survives. `undefined` when no
2676
- * recovery chain is active. (`AIChatAgent` and `Think` both back this with
2677
- * `_activeChatRecoveryRootRequestId`.)
2678
- */
2679
- activeChatRecoveryRootRequestId(): string | undefined;
2680
- /**
2681
- * Report a tolerated best-effort bookkeeping failure during give-up: the
2682
- * incident `"read"` (before synthesizing) or the sealing `"seal"` write
2683
- * (after terminalization). Neither aborts terminalization — see
2684
- * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.
2685
- */
2686
- onGiveUpBookkeepingError(phase: "read" | "seal", error: unknown): void;
2687
- }
2688
- /** Resolved orphaned-stream identity for a recovered chat turn. */
2689
- interface ResolvedRecoveryStream {
2690
- /** The orphaned stream id, or `""` when no stream metadata survives. */
2691
- streamId: string;
2692
- /**
2693
- * Whether the orphaned stream is still the live in-flight stream (so its
2694
- * partial has not already been persisted + completed by an ACK-driven
2695
- * reconnect). Gates persistence and stream completion.
2696
- */
2697
- streamStillActive: boolean;
2698
- /**
2699
- * The stream metadata row's lifecycle status, when the host tracks it
2700
- * (`Think`). `undefined` for hosts that do not model terminal streams
2701
- * (`AIChatAgent`) — those keep every terminal-stream branch dead, per the
2702
- * "substrate capabilities are optional" decision in the RFC.
2703
- */
2704
- streamStatus?: ChatStreamStatus;
2705
- }
2706
- /** Input to {@link ChatFiberWakeHooks.classifyRecoveredTurn}. */
2707
- interface ClassifyRecoveredTurnInput {
2708
- snapshot: ChatFiberSnapshot | null;
2709
- requestId: string;
2710
- streamId: string;
2711
- partial: RecoveryPartial;
2712
- streamStillActive: boolean;
2713
- streamStatus?: ChatStreamStatus;
2714
- }
2715
- /** Input to {@link ChatFiberWakeHooks.invokeOnChatRecovery}. */
2716
- interface InvokeOnChatRecoveryInput {
2717
- incident: ChatRecoveryIncident;
2718
- recoveryKind: ChatRecoveryKind;
2719
- recoveryRootRequestId: string;
2720
- requestId: string;
2721
- streamId: string;
2722
- partial: RecoveryPartial;
2723
- snapshot: ChatFiberSnapshot | null;
2724
- recoveryData: unknown;
2725
- createdAt: number;
2726
- }
2727
- /** Input to {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}. */
2728
- interface PersistOrphanedPartialInput {
2729
- streamId: string;
2730
- streamStillActive: boolean;
2731
- streamStatus?: ChatStreamStatus;
2732
- snapshot: ChatFiberSnapshot | null;
2733
- }
2734
- /** Input to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}. */
2735
- interface DispatchRecoveredTurnInput<TClassify> {
2736
- incident: ChatRecoveryIncident;
2737
- config: ResolvedChatRecoveryConfig;
2738
- recoveryKind: ChatRecoveryKind;
2739
- options: ChatRecoveryOptions;
2740
- snapshot: ChatFiberSnapshot | null;
2741
- requestId: string;
2742
- recoveryRootRequestId: string;
2743
- streamId: string;
2744
- streamStatus?: ChatStreamStatus;
2745
- /** The package-specific classification detail produced by `classifyRecoveredTurn`. */
2746
- detail: TClassify;
2747
- }
3060
+ type ChatProtocolEvent =
3061
+ | {
3062
+ type: "chat-request";
3063
+ id: string;
3064
+ init: {
3065
+ method?: string;
3066
+ body?: string;
3067
+ [key: string]: unknown;
3068
+ };
3069
+ }
3070
+ | {
3071
+ type: "clear";
3072
+ }
3073
+ | {
3074
+ type: "cancel";
3075
+ id: string;
3076
+ }
3077
+ | {
3078
+ type: "tool-result";
3079
+ toolCallId: string;
3080
+ toolName: string;
3081
+ output: unknown;
3082
+ state?: string;
3083
+ errorText?: string;
3084
+ autoContinue?: boolean;
3085
+ clientTools?: Array<{
3086
+ name: string;
3087
+ description?: string;
3088
+ parameters?: unknown;
3089
+ }>;
3090
+ }
3091
+ | {
3092
+ type: "tool-approval";
3093
+ toolCallId: string;
3094
+ approved: boolean;
3095
+ autoContinue?: boolean;
3096
+ }
3097
+ | {
3098
+ type: "stream-resume-request";
3099
+ probeId?: string;
3100
+ }
3101
+ | {
3102
+ type: "stream-resume-ack";
3103
+ id: string;
3104
+ }
3105
+ | {
3106
+ type: "messages";
3107
+ messages: unknown[];
3108
+ };
2748
3109
  /**
2749
- * The wake-dispatch host operations the engine drives when an interrupted CHAT
2750
- * fiber is detected on restart — the divergent organs the frame-collapse map
2751
- * flagged. Kept SEPARATE from {@link ChatRecoveryAdapter} (and passed per call to
2752
- * {@link ChatRecoveryEngine.handleChatFiberRecovery}) so the incident/give-up
2753
- * adapter stays focused, and generic over `TClassify` so the
2754
- * `classifyRecoveredTurn` → `dispatchRecoveredTurn` handoff is type-safe without a
2755
- * class-level generic.
3110
+ * Parse a raw WebSocket message string into a typed protocol event.
2756
3111
  *
2757
- * The engine owns the wake LIFECYCLE (gate → parse → unwrap → stream → partial →
2758
- * classify → begin-incident → exhausted-branch → onChatRecovery → persist →
2759
- * complete → dispatch → catch→failed) and the shared persist clause; these hooks
2760
- * own the package-specific I/O and the retry/continue/skip decision.
3112
+ * Returns `null` if the message is not valid JSON or not a recognized
3113
+ * protocol message type. Callers should fall through to the user's
3114
+ * `onMessage` handler when `null` is returned.
3115
+ *
3116
+ * @example
3117
+ * ```typescript
3118
+ * const event = parseProtocolMessage(rawMessage);
3119
+ * if (!event) return userOnMessage(connection, rawMessage);
3120
+ *
3121
+ * switch (event.type) {
3122
+ * case "chat-request": { ... }
3123
+ * case "clear": { ... }
3124
+ * case "tool-result": { ... }
3125
+ * }
3126
+ * ```
2761
3127
  */
2762
- interface ChatFiberWakeHooks<TClassify> {
2763
- /** The chat-fiber name prefix (`CHAT_FIBER_NAME + ":"`) gating the wake path. */
2764
- chatFiberPrefix(): string;
2765
- /** Decode the fiber snapshot into the recovery snapshot + checkpointed user data. */
2766
- unwrapRecoverySnapshot(ctx: FiberRecoveryContext): {
2767
- snapshot: ChatFiberSnapshot | null;
2768
- recoveryData: unknown;
2769
- };
2770
- /**
2771
- * Classify the recovered turn as a `retry` or `continue` and return any
2772
- * package-specific detail the dispatch decision needs (e.g. the pre-stream
2773
- * retry target id). Runs before the incident is opened.
2774
- */
2775
- classifyRecoveredTurn(input: ClassifyRecoveredTurnInput):
2776
- | {
2777
- recoveryKind: ChatRecoveryKind;
2778
- detail: TClassify;
2779
- }
2780
- | Promise<{
2781
- recoveryKind: ChatRecoveryKind;
2782
- detail: TClassify;
2783
- }>;
2784
- /**
2785
- * Build the package's `ChatRecoveryContext` and invoke the user `onChatRecovery`
2786
- * hook, returning its (defaulted) options. The engine wraps this in the
2787
- * incident `failed`-on-throw guard. Optional: a host with no user
2788
- * `onChatRecovery` surface (e.g. the pi fixture) omits it and the engine
2789
- * proceeds with empty options (`{}`).
2790
- */
2791
- invokeOnChatRecovery?(
2792
- input: InvokeOnChatRecoveryInput
2793
- ): Promise<ChatRecoveryOptions | void>;
2794
- /**
2795
- * The BASE persist gate: whether the orphaned partial is eligible to be
2796
- * materialized at all (live stream, or terminal-but-not-yet-persisted). The
2797
- * engine ANDs this with the shared `options.persist !== false ||
2798
- * partial.hasSettledToolResults` clause, so settled work is never dropped.
2799
- */
2800
- shouldPersistOrphanedPartial(
2801
- input: PersistOrphanedPartialInput
2802
- ): boolean | Promise<boolean>;
2803
- /** Materialize the orphaned stream's partial into a persisted assistant message. */
2804
- persistOrphanedStream(streamId: string): Promise<void>;
2805
- /** Mark the (still-active) recovered stream complete and schedule cleanup. */
2806
- completeRecoveredStream(streamId: string): void | Promise<void>;
2807
- /**
2808
- * The retry/continue/skip DECISION — the package-owned core. Runs after persist
2809
- * + complete; owns the leaf/submission computation, the schedule calls (via
2810
- * {@link ChatRecoveryEngine.scheduleRecovery}), the skip transitions, and any
2811
- * package-specific terminal/broadcast writes.
2812
- */
2813
- dispatchRecoveredTurn(
2814
- input: DispatchRecoveredTurnInput<TClassify>
2815
- ): Promise<void>;
2816
- }
3128
+ declare function parseProtocolMessage(raw: string): ChatProtocolEvent | null;
3129
+ //#endregion
3130
+ //#region src/chat/message-reconciler.d.ts
2817
3131
  /**
2818
- * Drives the shared recovery orchestration over a {@link ChatRecoveryAdapter}.
2819
- * The incident *budget math* lives in the pure `evaluateChatRecoveryIncident`;
2820
- * this class owns the surrounding sequence and its ordering invariants.
3132
+ * Reconcile incoming client messages against server state.
3133
+ *
3134
+ * 1. Merges server-known tool outputs into incoming messages that still
3135
+ * show stale states (input-available, approval-requested, approval-responded)
3136
+ * 2. Reconciles assistant IDs: exact match → content-key match → toolCallId match
3137
+ *
3138
+ * @param incoming - Messages from the client
3139
+ * @param serverMessages - Current server-side messages (source of truth)
3140
+ * @param sanitizeForContentKey - Function to sanitize a message before computing
3141
+ * its content key (typically strips ephemeral provider metadata)
3142
+ * @returns Reconciled messages ready for persistence
2821
3143
  */
2822
- declare class ChatRecoveryEngine {
2823
- private readonly adapter;
2824
- constructor(adapter: ChatRecoveryAdapter);
2825
- /**
2826
- * Open or re-evaluate the recovery incident for `input`, persist the result,
2827
- * and broadcast its lifecycle events. Returns the incident, the resolved
2828
- * config, and whether the budget is now exhausted.
2829
- */
2830
- /**
2831
- * Dispatch a recovered fiber to the package's non-chat handler (the
2832
- * messenger/workflow seam) before any chat-recovery processing. Returns `true`
2833
- * when the package consumed the fiber — the caller must then skip chat
2834
- * recovery for it. The engine owns the *ordering* (this runs before the
2835
- * chat-fiber gate); the *behavior* is adapter-owned. No-op (`false`) when the
2836
- * adapter omits {@link ChatRecoveryAdapter.tryHandleNonChatFiberRecovery}.
2837
- */
2838
- handleNonChatFiber(ctx: FiberRecoveryContext): Promise<boolean>;
2839
- /**
2840
- * The shared wake-recovery LIFECYCLE for an interrupted chat fiber. Both
2841
- * packages drove this exact frame; the divergent organs are the
2842
- * {@link ChatFiberWakeHooks}. In order:
2843
- *
2844
- * 1. non-chat dispatch ({@link handleNonChatFiber}) FIRST, then the chat-fiber
2845
- * name gate — a non-chat fiber is never misread as an orphaned chat turn;
2846
- * 2. parse the request id, unwrap the snapshot, resolve the orphaned stream +
2847
- * reconstruct its partial;
2848
- * 3. classify the turn (retry/continue + package detail) and open the incident;
2849
- * 4. if the budget is already exhausted, persist the settled partial (so
2850
- * non-idempotent tool results are not discarded — #1631) and terminalize
2851
- * BEFORE consulting `onChatRecovery`;
2852
- * 5. otherwise, inside a `failed`-on-throw guard: invoke `onChatRecovery`,
2853
- * apply the shared persist gate (base eligibility AND `persist !== false ||
2854
- * settled tool results`), complete the live stream, then hand the
2855
- * retry/continue/skip DECISION to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}.
2856
- *
2857
- * Returns `true` when the fiber was a chat (or non-chat) recovery the engine
2858
- * handled, `false` when it was not a chat fiber (the caller keeps looking). Any
2859
- * throw after the incident opens flips it to `failed` so it is never left
2860
- * leaking in `attempting`.
2861
- */
2862
- handleChatFiberRecovery<TClassify>(
2863
- ctx: FiberRecoveryContext,
2864
- wake: ChatFiberWakeHooks<TClassify>
2865
- ): Promise<boolean>;
2866
- /**
2867
- * The shared persist gate: base eligibility (the package's
2868
- * {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}) AND the
2869
- * never-drop-settled-work clause `options.persist !== false ||
2870
- * partial.hasSettledToolResults`. `options: undefined` (the exhausted branch)
2871
- * collapses the clause to the base gate. The clause lives here — not in each
2872
- * package — because settled-work preservation is a cross-package invariant
2873
- * (#1631), and the codec (not the engine) decides whether a partial carries
2874
- * settled tool work, so the engine stays wire-vocabulary-agnostic.
2875
- */
2876
- private _shouldPersistOrphanedPartial;
2877
- beginIncident(
2878
- input: BeginChatRecoveryIncidentInput
2879
- ): Promise<BeginChatRecoveryIncidentResult>;
2880
- /**
2881
- * Schedule a recovery continuation/retry: the transition + emit + enqueue
2882
- * triplet both packages repeat at every fiber-recovery and stall-routing
2883
- * decision. In order:
2884
- *
2885
- * 1. transition the incident to `scheduled` (persist + drive the #1620
2886
- * "recovering…" status) via {@link updateIncident};
2887
- * 2. emit `chat:recovery:scheduled`; and
2888
- * 3. enqueue the callback through the adapter's idempotent schedule.
2889
- *
2890
- * `recoveryKind` is passed explicitly (not read off the incident) because a
2891
- * caller can legitimately report a different kind than the incident was opened
2892
- * with — e.g. `AIChatAgent`'s lost-partial branch opens a `continue` incident
2893
- * but schedules (and reports) a `retry`. `requestId` always matches
2894
- * `incident.requestId` (the evaluation rewrites it to the current attempt), so
2895
- * it is read from the incident.
2896
- */
2897
- scheduleRecovery(input: {
2898
- incident: ChatRecoveryIncident;
2899
- recoveryKind: ChatRecoveryKind;
2900
- callback: ChatRecoveryScheduleCallback;
2901
- data: Record<string, unknown>;
2902
- reason?: ChatRecoveryScheduleReason;
2903
- }): Promise<void>;
3144
+ declare function reconcileMessages(
3145
+ incoming: UIMessage[],
3146
+ serverMessages: readonly UIMessage[],
3147
+ sanitizeForContentKey?: (message: UIMessage) => UIMessage
3148
+ ): UIMessage[];
3149
+ /**
3150
+ * For a single message, resolve its ID by matching toolCallId against server state.
3151
+ * Prevents duplicate DB rows when client IDs differ from server IDs.
3152
+ * Tool call IDs are unique per conversation, so matching is safe regardless of state.
3153
+ */
3154
+ declare function resolveToolMergeId(
3155
+ message: UIMessage,
3156
+ serverMessages: readonly UIMessage[]
3157
+ ): UIMessage;
3158
+ /**
3159
+ * Merge a freshly-reconstructed orphaned partial onto the assistant message
3160
+ * that already owns its target id (the orphan-persist **(c)** step).
3161
+ *
3162
+ * Used by hosts whose store can hold an assistant row for the SAME id BEFORE
3163
+ * the stream finalizes — e.g. an early persist at tool-approval time, or a
3164
+ * continuation resuming the prior assistant message. On recovery the engine
3165
+ * replays the same chunks, so a naive append would leave two parts per tool
3166
+ * call. The merge therefore:
3167
+ *
3168
+ * - keeps ALL existing parts (the persisted row is authoritative for tool
3169
+ * parts that had a client result applied IN PLACE — that result lives only
3170
+ * in storage, never in the chunk stream, so a whole-message replace would
3171
+ * clobber it);
3172
+ * - appends only the reconstructed parts whose `toolCallId` is NOT already
3173
+ * present (dedup by tool-call identity);
3174
+ * - overlays the incoming metadata onto the existing metadata (incoming wins
3175
+ * on conflicts), falling back to whichever side is present.
3176
+ *
3177
+ * The result carries the INCOMING message's id/role (the caller has already
3178
+ * resolved the incoming id to the existing row's id via the (b) target-id
3179
+ * step), so it is safe to write straight back through `updateMessage`.
3180
+ *
3181
+ * Hosts whose orphan persist only ever runs at stream finalize (no early/
3182
+ * mid-stream row for the same id) never hit the merge branch and don't need
3183
+ * this — a plain append/replace is already dedup-safe because the shared
3184
+ * reconstruction (`StreamAccumulator` / `applyChunkToParts`) is idempotent by
3185
+ * `toolCallId`.
3186
+ */
3187
+ declare function reconcileOrphanPartial(
3188
+ existing: UIMessage,
3189
+ incoming: UIMessage
3190
+ ): UIMessage;
3191
+ //#endregion
3192
+ //#region src/chat/repair-transcript.d.ts
3193
+ /**
3194
+ * Whether a tool part already has a settled result the provider accepts, so it
3195
+ * must NOT be re-repaired into an errored result.
3196
+ *
3197
+ * Single source of truth for the terminal tool states. Mirrors the AI SDK's
3198
+ * terminal states: `convertToModelMessages` emits a `tool-result` for
3199
+ * `output-available`, `output-error`, AND `output-denied` (a user-denied
3200
+ * approval — its denial reason becomes the tool-result). Omitting any of these
3201
+ * makes repair re-flip the part every turn — clobbering a real `errorText` /
3202
+ * denial with the generic "interrupted" message.
3203
+ */
3204
+ declare function toolPartHasSettledResult(
3205
+ record: Record<string, unknown>
3206
+ ): boolean;
3207
+ interface RepairInterruptedToolPartsOptions {
2904
3208
  /**
2905
- * Reschedule a recovery continuation/retry that timed out waiting for stable
2906
- * state, INSIDE the currently-executing one-shot schedule row. Reads the
2907
- * incident; if it is still under the attempt cap, bumps `attempt`, marks it
2908
- * `scheduled` with `reason:"stable_timeout_retry"`, and issues a delayed,
2909
- * NON-idempotent schedule (`alarm()` deletes the executing row only after this
2910
- * returns, so an idempotent reschedule would dedup onto that doomed row and
2911
- * never fire — see {@link chatRecoverySchedulePolicy}).
2912
- *
2913
- * Returns `true` when a retry was scheduled, `false` when there is no incident
2914
- * (no id / record gone) or the attempt budget is already spent — in which case
2915
- * the caller falls through to the give-up path. Deliberately bypasses the
2916
- * `evaluateChatRecoveryIncident` budget (this is a coarse stable-state retry,
2917
- * not a fresh interruption) and {@link updateIncident} (no `scheduled` event /
2918
- * recovering-flag churn on a same-turn reschedule).
3209
+ * Decide the replacement for an interrupted tool part (no settled result, not
3210
+ * `approval-responded`). Its `input` has already been normalized to a valid
3211
+ * object. The default host behavior flips it to an errored tool-result; hosts
3212
+ * expose this as an overridable `repairInterruptedToolPart` hook so a subclass
3213
+ * can, e.g., convert an interrupted client-resolved tool into a text part.
2919
3214
  */
2920
- rescheduleAfterStableTimeout(input: {
2921
- incidentId: string | undefined;
2922
- callback: ChatRecoveryScheduleCallback;
2923
- data: Record<string, unknown> | undefined;
2924
- fallbackMaxAttempts: number;
2925
- }): Promise<boolean>;
3215
+ repairPart: (part: UIMessage["parts"][number]) => UIMessage["parts"][number];
2926
3216
  /**
2927
- * Record that a recovery callback observed a Durable Object memory-limit reset
2928
- * (the isolate exceeded its 128 MB limit — `isDurableObjectMemoryLimitReset`)
2929
- * and decide what to do next (#1825).
2930
- *
2931
- * Bumps the incident's durable `oomAttempts` counter, then:
2932
- * - if it is still within `maxOomRetries`, issues a delayed, NON-idempotent
2933
- * reschedule of the SAME callback (same machinery as
2934
- * {@link rescheduleAfterStableTimeout}: the executing one-shot row is
2935
- * deleted only after the callback returns, so an idempotent reschedule
2936
- * would dedup onto that doomed row) and returns `"rescheduled"`. The small
2937
- * delay lets a transient memory spike clear before the re-run;
2938
- * - otherwise leaves the incremented count persisted (so a begin-path
2939
- * re-evaluation agrees) and returns `"exhausted"` — the caller then
2940
- * terminalizes via the give-up path with `reason="out_of_memory"`.
2941
- *
2942
- * Returns `"exhausted"` when there is no incident to track against (no id /
2943
- * record gone): an OOM we cannot bound must seal rather than loop. Unlike a
2944
- * stable-state retry this is gated by the OOM-specific budget, NOT the generic
2945
- * attempt cap — re-running an OOM streams a little "progress" that would
2946
- * otherwise reset the attempt cap forever (the #1825 loop).
3217
+ * Whether a tool part already carries a settled result (defaults to
3218
+ * {@link toolPartHasSettledResult}).
2947
3219
  */
2948
- recordOomAndDecide(input: {
2949
- incidentId: string | undefined;
2950
- callback: ChatRecoveryScheduleCallback;
2951
- data: Record<string, unknown> | undefined;
2952
- maxOomRetries: number;
2953
- }): Promise<"rescheduled" | "exhausted">;
3220
+ isSettled?: (record: Record<string, unknown>) => boolean;
2954
3221
  /**
2955
- * Give up on a recovery turn whose retry budget drained, terminalizing it so
2956
- * it can never become an eternal spinner (#1645). The shared spine both
2957
- * packages repeated verbatim:
2958
- *
2959
- * 1. resolve config + the incident key from `data.incidentId`;
2960
- * 2. best-effort READ the stored incident — a failed read is tolerated
2961
- * (reported via `onGiveUpBookkeepingError("read", …)`) and the incident is
2962
- * synthesized, because the read backs only the re-entry guard, not the
2963
- * terminal UX;
2964
- * 3. re-entry guard: a `stored.status === "exhausted"` record means
2965
- * terminalization already fired, so a duplicate stale alarm returns without
2966
- * re-broadcasting the banner;
2967
- * 4. build the exhausted incident (reuse `stored`, or synthesize a minimal one
2968
- * so a swept/missing record STILL terminalizes through `onExhausted`);
2969
- * 5. resolve the orphaned stream id + partial;
2970
- * 6. terminalize via `exhaustChatRecovery` — BEFORE sealing. The terminal
2971
- * writes can reject with a platform transient in the deploy/storage window
2972
- * a give-up runs in (#1730); letting that throw propagate is deliberate, so
2973
- * `Agent._executeScheduleCallback` defers the one-shot row and the WHOLE
2974
- * give-up re-runs on a healthy isolate. Sealing first would arm the
2975
- * re-entry guard and turn that re-run into a no-op, dropping the durable
2976
- * terminal record. The re-run is idempotent (terminal writes overwrite the
2977
- * same key); a second banner is the documented at-least-once edge; and
2978
- * 7. best-effort SEAL write so the re-entry guard sees `exhausted` on a
2979
- * duplicate alarm — a failed seal (reported via
2980
- * `onGiveUpBookkeepingError("seal", …)`) costs at most one re-delivered
2981
- * banner.
2982
- *
2983
- * The two packages diverged only in parameters the caller supplies:
2984
- * `reason` (`Think` passes `stable_timeout` | `recovery_error`; `AIChatAgent`
2985
- * always `stable_timeout`) and the root-id chain (`Think` includes
2986
- * `recoveredRequestId`; `AIChatAgent` never sets it, so the unified chain
2987
- * collapses identically). Exactly-once terminalization rests on the re-entry
2988
- * guard alone in `AIChatAgent`; `Think` additionally short-circuits duplicate
2989
- * alarms earlier in its durable-submission layer.
3222
+ * Normalize a tool part's `input` (defaults to the shared
3223
+ * {@link normalizeToolInput}).
2990
3224
  */
2991
- exhaustRecoveryGiveUp(input: {
2992
- callback: ChatRecoveryScheduleCallback;
2993
- data:
2994
- | {
2995
- incidentId?: string;
2996
- originalRequestId?: string;
2997
- recoveredRequestId?: string;
2998
- }
2999
- | undefined;
3000
- reason: string;
3001
- }): Promise<void>;
3225
+ normalizeInput?: (input: unknown) => {
3226
+ input: unknown;
3227
+ changed: boolean;
3228
+ };
3002
3229
  /**
3003
- * Apply a status transition to the recovery incident `incidentId`:
3004
- *
3005
- * - `completed` → drop the record (terminal, never retried);
3006
- * - any other status → persist the new status (and `reason`), so the attempt
3007
- * budget survives restarts until the TTL sweep reclaims it;
3008
- * - emit the matching `completed`/`skipped`/`failed` lifecycle event; and
3009
- * - drive the live "recovering…" status (#1620): `scheduled` marks it active
3010
- * (keyed by the recovery-root request id), terminal states clear it.
3011
- *
3012
- * No-op when `incidentId` is undefined or the record is already gone. This is
3013
- * the transition twin of {@link beginIncident}: all I/O is adapter-owned, the
3014
- * engine owns only the state-machine shape.
3230
+ * Whether an interrupted tool part (no settled result, not
3231
+ * `approval-responded`) should be repaired at all. Defaults to `true` (repair
3232
+ * everything, like Think — which converts even client tools via its
3233
+ * `repairPart` override). A host whose default `repairPart` errors the part
3234
+ * (ai-chat) passes this to SKIP a part still legitimately awaiting a CLIENT
3235
+ * interaction (an `input-available` client tool or an `approval-requested`
3236
+ * part the user may still answer) so it is left verbatim rather than clobbered
3237
+ * with an error. Skipped parts are not counted in `removedToolCalls`.
3015
3238
  */
3016
- updateIncident(
3017
- incidentId: string | undefined,
3018
- status: ChatRecoveryIncident["status"],
3019
- reason?: string
3020
- ): Promise<void>;
3239
+ shouldRepair?: (part: UIMessage["parts"][number]) => boolean;
3240
+ }
3241
+ interface RepairInterruptedToolPartsResult {
3242
+ /** A new messages array; unchanged messages keep their original reference. */
3243
+ messages: UIMessage[];
3244
+ /** Count of interrupted tool calls flipped to a repaired shape. */
3245
+ removedToolCalls: number;
3246
+ /** Count of tool parts whose malformed `input` was normalized. */
3247
+ normalizedInputs: number;
3248
+ /** The tool-call ids that were repaired. */
3249
+ toolCallIds: string[];
3021
3250
  }
3022
3251
  /**
3023
- * The complete give-up choreography from a single call: build the exhausted
3024
- * context, fire the shared notification ({@link notifyChatRecoveryExhausted}),
3025
- * then hand that context to the host's `terminalize` step. Folds the
3026
- * `buildChatRecoveryExhaustedContext` → `notifyChatRecoveryExhausted` → host
3027
- * terminalize sequence that every host's `_exhaustChatRecovery` repeated.
3028
- *
3029
- * What this OWNS (the invariant, so it cannot drift per host):
3030
- * - the notification ALWAYS runs before any terminal write, and
3031
- * - a throwing `onExhausted` can NEVER block terminal delivery — it is swallowed
3032
- * via `onError` (a tested invariant in both published packages).
3252
+ * Repair interrupted tool calls and normalize malformed tool inputs across a
3253
+ * transcript. Behavior mirrors `@cloudflare/think`'s original
3254
+ * `_repairToolTranscriptParts`:
3033
3255
  *
3034
- * What it deliberately does NOT own: the terminal-record / broadcast /
3035
- * recovering-clear writes — their exact set diverges per host (both
3036
- * `AIChatAgent` and `Think` broadcast the banner first so it survives a storage
3037
- * write that rejects mid-deploy; `Think` additionally writes a submission row)
3038
- * — see {@link ChatRecoveryAdapter.exhaustChatRecovery}. The host expresses
3039
- * those writes inside `terminalize`. A `terminalize` that throws DOES propagate,
3040
- * so the whole give-up re-runs on a healthy isolate (#1730); see
3041
- * {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.
3256
+ * - a tool part with NO settled result and state `approval-responded` is kept
3257
+ * verbatim (an approved server tool waiting for its continuation to run
3258
+ * `execute()` — not abandoned);
3259
+ * - a tool part with NO settled result for which `shouldRepair` returns false
3260
+ * is kept verbatim (a part still awaiting a CLIENT interaction; see option);
3261
+ * - any other tool part with no settled result is normalized then handed to
3262
+ * `repairPart` (default: flipped to an errored result);
3263
+ * - a tool part WITH a settled result only has its `input` normalized.
3042
3264
  *
3043
- * `partialParts` is passed explicitly (not derived from a `RecoveryPartial`) so a
3044
- * foreign-vocabulary host can pass `[]` rather than fabricate AI-SDK parts — the
3045
- * engine seam stays parts-vocabulary-agnostic.
3265
+ * Messages with no changed part keep their original object reference so callers
3266
+ * can cheaply detect what to persist.
3046
3267
  */
3047
- declare function runChatRecoveryExhaustion(
3048
- input: {
3049
- incident: ChatRecoveryIncident;
3050
- config: ResolvedChatRecoveryConfig;
3051
- partialText: string;
3052
- partialParts: ChatRecoveryExhaustedContext["partialParts"];
3053
- streamId: string;
3054
- createdAt: number;
3055
- },
3056
- hooks: {
3057
- emit: (ctx: ChatRecoveryExhaustedContext) => void;
3058
- onExhausted?: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;
3059
- onError: (error: unknown) => void;
3060
- terminalize: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;
3061
- }
3062
- ): Promise<void>;
3268
+ declare function repairInterruptedToolParts(
3269
+ messages: UIMessage[],
3270
+ options: RepairInterruptedToolPartsOptions
3271
+ ): RepairInterruptedToolPartsResult;
3272
+ //#endregion
3273
+ //#region src/chat/orphan-store.d.ts
3274
+ interface OrphanPersistStore<
3275
+ M extends {
3276
+ id: string;
3277
+ } = UIMessage
3278
+ > {
3279
+ /** Read the stored message with this id, or `null` if none exists. */
3280
+ getMessage(id: string): M | null | Promise<M | null>;
3281
+ /**
3282
+ * Append a new message. `parentId` is honored by tree-structured stores
3283
+ * (`undefined` → attach to the latest leaf); flat-array stores ignore it.
3284
+ */
3285
+ appendMessage(message: M, parentId?: string | null): void | Promise<void>;
3286
+ /** Replace the stored message that owns `message.id`. */
3287
+ updateMessage(message: M): void | Promise<void>;
3288
+ }
3289
+ //#endregion
3290
+ //#region src/chat/orphan-persist.d.ts
3291
+ interface PersistReconstructedOrphanOptions<
3292
+ TMessage extends UIMessage = UIMessage
3293
+ > {
3294
+ /** The store seam to upsert through (a `SessionProvider` write-subset). */
3295
+ store: OrphanPersistStore<TMessage>;
3296
+ /**
3297
+ * Id for the reconstructed message when the stream carried no provider
3298
+ * `start.messageId` to adopt. The accumulator still adopts a provider id when
3299
+ * present.
3300
+ */
3301
+ fallbackId: string;
3302
+ /**
3303
+ * Finalize the reconstructed message before upsert — e.g. strip internal
3304
+ * parts or resolve the persist-target id. Return `null` to skip persistence
3305
+ * entirely (e.g. an empty structural-only message).
3306
+ */
3307
+ prepare: (message: TMessage) => TMessage | null;
3308
+ /**
3309
+ * Combine an existing row with the reconstructed message when a row already
3310
+ * owns the id (replace, or reconcile partials).
3311
+ */
3312
+ merge: (existing: TMessage, incoming: TMessage) => TMessage;
3313
+ }
3314
+ /**
3315
+ * Reconstruct a message from `chunks` and upsert it via the store. Returns
3316
+ * `true` when a write happened (so a caller that broadcasts after — Think — can
3317
+ * gate its broadcast on it), `false` when there was nothing to persist (no
3318
+ * parts, or `prepare` returned `null`).
3319
+ */
3320
+ declare function persistReconstructedOrphan<
3321
+ TMessage extends UIMessage = UIMessage
3322
+ >(
3323
+ chunks: ReadonlyArray<{
3324
+ body: string;
3325
+ }>,
3326
+ options: PersistReconstructedOrphanOptions<TMessage>
3327
+ ): Promise<boolean>;
3063
3328
  //#endregion
3064
3329
  //#region src/chat/recovery-codec.d.ts
3065
3330
  /**
@@ -3100,11 +3365,12 @@ interface ProgressCreditThrottle {
3100
3365
  shouldCredit(now: number): boolean;
3101
3366
  }
3102
3367
  /**
3103
- * The single, host-agnostic rule for crediting recovery forward progress from a
3104
- * stored stream chunk — the convergence of what `AIChatAgent` and `Think`
3105
- * previously each decided on their own (ai-chat keyed on chunk type only; Think
3106
- * keyed on its flush cadence). Both hosts now call this at chunk-store time so
3107
- * the bump TIMING is identical:
3368
+ * The host-agnostic rule for crediting recovery forward progress from a stored
3369
+ * stream chunk, from when the marker was a counter bumped per chunk. Neither
3370
+ * host bumps a counter any more: the marker is derived from the stream log
3371
+ * (`ResumableStream.progressMarker`), so a chunk counts once its segment is
3372
+ * durably flushed. The rule stays exported for consumers that still keep a
3373
+ * counter of their own:
3108
3374
  *
3109
3375
  * - a **milestone** ({@link ChatRecoveryCodec.isProgressChunk}) always credits;
3110
3376
  * - **streaming content** ({@link ChatRecoveryCodec.isStreamingContentChunk})
@@ -3277,6 +3543,37 @@ declare function iterateWithStallWatchdog<T>(
3277
3543
  onStall: () => void
3278
3544
  ): AsyncGenerator<T>;
3279
3545
  //#endregion
3546
+ //#region src/chat/truncate-older-messages.d.ts
3547
+ interface TruncateOptions {
3548
+ /** Number of recent messages to keep intact (default: 4) */
3549
+ keepRecent?: number;
3550
+ /** Max chars for tool outputs in older messages (default: 500) */
3551
+ maxToolOutputChars?: number;
3552
+ /** Max chars for text parts in older messages (default: 10000) */
3553
+ maxTextChars?: number;
3554
+ }
3555
+ /**
3556
+ * Truncate tool outputs and long text in older messages.
3557
+ * Returns a new array — input messages are not mutated.
3558
+ *
3559
+ * Recent messages (last `keepRecent`) are left intact.
3560
+ * Older messages get tool outputs and long text truncated. Structured tool
3561
+ * outputs are truncated in place instead of being replaced by raw strings.
3562
+ *
3563
+ * Use in assembleContext() before sending to the LLM:
3564
+ * ```typescript
3565
+ * async assembleContext() {
3566
+ * const history = this.sessions.getHistory(this._sessionId);
3567
+ * const truncated = truncateOlderMessages(history);
3568
+ * return convertToModelMessages(truncated);
3569
+ * }
3570
+ * ```
3571
+ */
3572
+ declare function truncateOlderMessages(
3573
+ messages: SessionMessage[],
3574
+ options?: TruncateOptions
3575
+ ): SessionMessage[];
3576
+ //#endregion
3280
3577
  export {
3281
3578
  AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS,
3282
3579
  AbortRegistry,
@@ -3305,6 +3602,7 @@ export {
3305
3602
  CHAT_RECOVERY_INCIDENT_TTL_MS,
3306
3603
  CHAT_RECOVERY_PROGRESS_KEY,
3307
3604
  CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS,
3605
+ CHAT_RECOVERY_TASK_NAME,
3308
3606
  CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS,
3309
3607
  type ChatConnection,
3310
3608
  type ChatFiberSnapshot,
@@ -3316,6 +3614,7 @@ export {
3316
3614
  type ChatRecoveryContext,
3317
3615
  ChatRecoveryEngine,
3318
3616
  type ChatRecoveryExhaustedContext,
3617
+ type ChatRecoveryHandoff,
3319
3618
  type ChatRecoveryIncident,
3320
3619
  type ChatRecoveryIncidentEvent,
3321
3620
  type ChatRecoveryKind,
@@ -3323,10 +3622,15 @@ export {
3323
3622
  type ChatRecoveryProgressContext,
3324
3623
  type ChatRecoveryScheduleCallback,
3325
3624
  type ChatRecoveryScheduleReason,
3625
+ type ChatRecoveryTaskHooks,
3626
+ type ChatRecoveryTaskInput,
3627
+ type ChatRecoveryTaskReason,
3326
3628
  type ChatResponseResult,
3327
3629
  ChatStreamStalledError,
3328
3630
  type ChatStreamStatus,
3329
3631
  type ChatTerminalRecord,
3632
+ type ChatTurnClosureEntry,
3633
+ type ChatTurnTaskHooks,
3330
3634
  type ChunkAction,
3331
3635
  type ChunkResult,
3332
3636
  type ClassifyRecoveredTurnInput,
@@ -3374,7 +3678,6 @@ export {
3374
3678
  ResumableStream,
3375
3679
  ResumeHandshake,
3376
3680
  type ResumeHandshakeHost,
3377
- STREAM_CLEANUP_DELAY_SECONDS,
3378
3681
  STREAM_RESUME_NONE_REASONS,
3379
3682
  type SaveMessagesOptions,
3380
3683
  type SaveMessagesResult,
@@ -3392,6 +3695,7 @@ export {
3392
3695
  type TextSegmentEvent,
3393
3696
  TextSegmentJoiner,
3394
3697
  type ToolPartUpdate,
3698
+ type TruncateOptions,
3395
3699
  TurnQueue,
3396
3700
  type TurnResult,
3397
3701
  aiSdkRecoveryCodec,
@@ -3404,19 +3708,23 @@ export {
3404
3708
  buildInClauseStrings,
3405
3709
  bumpChatRecoveryProgress,
3406
3710
  byteLength,
3407
- chatRecoverySchedulePolicy,
3711
+ chatRecoveryTaskRunOptions,
3408
3712
  classifyAgentToolChildRecovery,
3409
- cleanupStreamBuffers,
3410
3713
  clearChatTerminal,
3411
3714
  clientResolvableToolNames,
3412
3715
  createAgentToolEventState,
3413
3716
  createChatFiberSnapshot,
3717
+ createChatRecoveryTaskDefinition,
3718
+ createChatStreams,
3719
+ createChatTurnTaskDefinition,
3414
3720
  createToolsFromClientSchemas,
3415
3721
  crossMessageToolResultUpdate,
3722
+ dispatchChatRecoveryToHandoff,
3416
3723
  drainInteractionApplies,
3417
3724
  enforceRowSizeLimit,
3418
3725
  hasIncompleteToolBatch,
3419
3726
  interceptAgentToolBroadcast,
3727
+ isPlatformFailure,
3420
3728
  isReplayChunk,
3421
3729
  iterateWithStallWatchdog,
3422
3730
  listActiveChatRecoveryIncidents,
@@ -3442,6 +3750,7 @@ export {
3442
3750
  toolApprovalUpdate,
3443
3751
  toolPartHasSettledResult,
3444
3752
  toolResultUpdate,
3753
+ truncateOlderMessages,
3445
3754
  unwrapChatFiberSnapshot,
3446
3755
  wrapChatFiberSnapshot
3447
3756
  };