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