agents 0.21.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 (258) hide show
  1. package/README.md +21 -1
  2. package/dist/{agent-tool-types-CzGGB-20.d.ts → agent-routing-DE5zmCQ8.d.ts} +1844 -1336
  3. package/dist/agent-routing.d.ts +14 -0
  4. package/dist/agent-routing.js +187 -0
  5. package/dist/agent-routing.js.map +1 -0
  6. package/dist/agent-tool-types.d.ts +26 -26
  7. package/dist/{agent-tools-zR2d5uij.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
  8. package/dist/agent-tools.d.ts +21 -21
  9. package/dist/agent-tools.js +2 -1
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/browser/ai.js +6 -2
  12. package/dist/browser/ai.js.map +1 -1
  13. package/dist/browser/tanstack-ai.js.map +1 -1
  14. package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
  15. package/dist/callable-decorator.d.ts +18 -0
  16. package/dist/callable-decorator.js +71 -0
  17. package/dist/callable-decorator.js.map +1 -0
  18. package/dist/capability-BjSKYpzg.js +42 -0
  19. package/dist/capability-BjSKYpzg.js.map +1 -0
  20. package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
  21. package/dist/channel-Bnm4S7T2.d.ts +491 -0
  22. package/dist/channels/ai-sdk.d.ts +46 -0
  23. package/dist/channels/ai-sdk.js +120 -0
  24. package/dist/channels/ai-sdk.js.map +1 -0
  25. package/dist/channels/email.d.ts +95 -0
  26. package/dist/channels/email.js +323 -0
  27. package/dist/channels/email.js.map +1 -0
  28. package/dist/channels/index.d.ts +233 -0
  29. package/dist/channels/index.js +608 -0
  30. package/dist/channels/index.js.map +1 -0
  31. package/dist/channels/slack.d.ts +140 -0
  32. package/dist/channels/slack.js +614 -0
  33. package/dist/channels/slack.js.map +1 -0
  34. package/dist/channels/tanstack-ai.d.ts +39 -0
  35. package/dist/channels/tanstack-ai.js +17 -0
  36. package/dist/channels/tanstack-ai.js.map +1 -0
  37. package/dist/channels/telegram.d.ts +106 -0
  38. package/dist/channels/telegram.js +427 -0
  39. package/dist/channels/telegram.js.map +1 -0
  40. package/dist/channels/voice.d.ts +45 -0
  41. package/dist/channels/voice.js +122 -0
  42. package/dist/channels/voice.js.map +1 -0
  43. package/dist/chat/index.d.ts +2328 -2015
  44. package/dist/chat/index.js +891 -521
  45. package/dist/chat/index.js.map +1 -1
  46. package/dist/chat/react.d.ts +14 -1
  47. package/dist/chat/react.js +82 -52
  48. package/dist/chat/react.js.map +1 -1
  49. package/dist/chat/transport.js +1 -1
  50. package/dist/chat-sdk/index.d.ts +7 -7
  51. package/dist/chat-sdk/index.js +1 -1
  52. package/dist/{client-zqKcsyFa.js → client-jagG8a9_.js} +129 -37
  53. package/dist/client-jagG8a9_.js.map +1 -0
  54. package/dist/client.d.ts +1 -1
  55. package/dist/client.js +1 -1
  56. package/dist/{cloudflare-BduZwmYK.js → cloudflare-Dzvc7V2N.js} +10 -3
  57. package/dist/{cloudflare-BduZwmYK.js.map → cloudflare-Dzvc7V2N.js.map} +1 -1
  58. package/dist/context/index.d.ts +216 -0
  59. package/dist/context/index.js +454 -0
  60. package/dist/context/index.js.map +1 -0
  61. package/dist/current-agent-Da_C9a3b.d.ts +266 -0
  62. package/dist/current-agent-DhoDkSnH.js +51 -0
  63. package/dist/current-agent-DhoDkSnH.js.map +1 -0
  64. package/dist/diagnostics-BzvaX2UT.js +45 -0
  65. package/dist/diagnostics-BzvaX2UT.js.map +1 -0
  66. package/dist/diagnostics-C4jcz3VK.js +360 -0
  67. package/dist/diagnostics-C4jcz3VK.js.map +1 -0
  68. package/dist/{do-oauth-client-provider-VTZj2VtM.d.ts → do-oauth-client-provider-Tmf1vgKz.d.ts} +2 -2
  69. package/dist/{email-CL27preh.d.ts → email-7TatiTnl.d.ts} +38 -9
  70. package/dist/email-send.d.ts +15 -0
  71. package/dist/email-send.js +32 -0
  72. package/dist/email-send.js.map +1 -0
  73. package/dist/email.d.ts +14 -10
  74. package/dist/email.js.map +1 -1
  75. package/dist/{handler-stateless-C_bo-Ytq.d.ts → handler-stateless-DxYpJ_XF.d.ts} +3 -3
  76. package/dist/{handler-stateless-CIkKPETH.js → handler-stateless-VvrWSAVA.js} +5 -5
  77. package/dist/handler-stateless-VvrWSAVA.js.map +1 -0
  78. package/dist/index-BB0kqhIz.d.ts +101 -0
  79. package/dist/index-XDkuQ7zm.d.ts +89 -0
  80. package/dist/{index-BRnybD6X.d.ts → index-YSKgfgg9.d.ts} +21 -31
  81. package/dist/index.d.ts +113 -101
  82. package/dist/index.js +11 -7234
  83. package/dist/ingress-BfetZbMO.js +83 -0
  84. package/dist/ingress-BfetZbMO.js.map +1 -0
  85. package/dist/internal-CYlgHl1l.js +59 -0
  86. package/dist/internal-CYlgHl1l.js.map +1 -0
  87. package/dist/internal_context-BlxFEWfn.d.ts +19 -0
  88. package/dist/internal_context.d.ts +10 -4
  89. package/dist/internal_context.js +1 -10
  90. package/dist/{client-invoker-BNSZxAkv.d.ts → invoker-CG0_p_Wq.d.ts} +2 -2
  91. package/dist/{client-invoker-VNZ7X0nn.js → invoker-CHMnoxIA.js} +2 -2
  92. package/dist/invoker-CHMnoxIA.js.map +1 -0
  93. package/dist/lifecycle/index.d.ts +66 -0
  94. package/dist/lifecycle/index.js +4 -0
  95. package/dist/lifecycle-CMRGjZdw.js +1299 -0
  96. package/dist/lifecycle-CMRGjZdw.js.map +1 -0
  97. package/dist/mcp/{do-oauth-client-provider.d.ts → client/do-oauth-client-provider.d.ts} +1 -1
  98. package/dist/mcp/{do-oauth-client-provider.js → client/do-oauth-client-provider.js} +1 -1
  99. package/dist/mcp/client/do-oauth-client-provider.js.map +1 -0
  100. package/dist/mcp/client/index.d.ts +42 -0
  101. package/dist/mcp/{client.js → client/index.js} +1 -1
  102. package/dist/mcp/{x402.d.ts → client/x402.d.ts} +2 -2
  103. package/dist/mcp/{x402.js → client/x402.js} +2 -2
  104. package/dist/mcp/client/x402.js.map +1 -0
  105. package/dist/mcp/index.d.ts +36 -36
  106. package/dist/mcp/index.js +14 -16
  107. package/dist/mcp/index.js.map +1 -1
  108. package/dist/mcp/{server.d.ts → server/index.d.ts} +1 -1
  109. package/dist/mcp/{server.js → server/index.js} +1 -1
  110. package/dist/observability/ai/index.js +50 -35
  111. package/dist/observability/ai/index.js.map +1 -1
  112. package/dist/observability/index.d.ts +4 -4
  113. package/dist/observability/index.js +3 -50
  114. package/dist/observability/index.js.map +1 -1
  115. package/dist/{protocol-Dqc2MQxo.js → protocol-B0nh6KNf.js} +19 -21
  116. package/dist/protocol-B0nh6KNf.js.map +1 -0
  117. package/dist/react.d.ts +4 -4
  118. package/dist/react.js +1 -1
  119. package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
  120. package/dist/retries.d.ts +8 -6
  121. package/dist/retries.js +13 -1
  122. package/dist/retries.js.map +1 -1
  123. package/dist/routing/index.d.ts +137 -0
  124. package/dist/routing/index.js +244 -0
  125. package/dist/routing/index.js.map +1 -0
  126. package/dist/sanitize-D9TujEK8.js +79 -0
  127. package/dist/sanitize-D9TujEK8.js.map +1 -0
  128. package/dist/schedule.d.ts +25 -94
  129. package/dist/schedule.js +1 -98
  130. package/dist/schedule.js.map +1 -1
  131. package/dist/scheduler-DD9NdYbF.js +665 -0
  132. package/dist/scheduler-DD9NdYbF.js.map +1 -0
  133. package/dist/scheduler-Dwh85ZGl.d.ts +223 -0
  134. package/dist/schedules/index.d.ts +22 -0
  135. package/dist/schedules/index.js +2 -0
  136. package/dist/schedules/parser.d.ts +79 -0
  137. package/dist/schedules/parser.js +103 -0
  138. package/dist/schedules/parser.js.map +1 -0
  139. package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
  140. package/dist/serializable.d.ts +1 -1
  141. package/dist/sessions/index.d.ts +441 -0
  142. package/dist/sessions/index.js +2063 -0
  143. package/dist/sessions/index.js.map +1 -0
  144. package/dist/skills/index.d.ts +99 -0
  145. package/dist/skills/index.js +254 -5
  146. package/dist/skills/index.js.map +1 -1
  147. package/dist/sql-error-CPY-GXyI.d.ts +12 -0
  148. package/dist/sql-error.d.ts +2 -0
  149. package/dist/sql-error.js +16 -0
  150. package/dist/sql-error.js.map +1 -0
  151. package/dist/src-DlSHshb2.js +6963 -0
  152. package/dist/src-DlSHshb2.js.map +1 -0
  153. package/dist/streams/index.d.ts +120 -0
  154. package/dist/streams/index.js +107 -0
  155. package/dist/streams/index.js.map +1 -0
  156. package/dist/streams-D6tJ0NN9.d.ts +370 -0
  157. package/dist/streams-DZKgAj9b.js +709 -0
  158. package/dist/streams-DZKgAj9b.js.map +1 -0
  159. package/dist/sub-routing.d.ts +12 -12
  160. package/dist/surface-bZZJqBka.js +17 -0
  161. package/dist/surface-bZZJqBka.js.map +1 -0
  162. package/dist/tasks/index.d.ts +64 -0
  163. package/dist/tasks/index.js +2 -0
  164. package/dist/tasks-BRJ5zgya.d.ts +517 -0
  165. package/dist/tasks-ylZgBjhj.js +1656 -0
  166. package/dist/tasks-ylZgBjhj.js.map +1 -0
  167. package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
  168. package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
  169. package/dist/text-stream-CpdiKrJB.js +272 -0
  170. package/dist/text-stream-CpdiKrJB.js.map +1 -0
  171. package/dist/tokens-nHAKcN6M.js +52 -0
  172. package/dist/tokens-nHAKcN6M.js.map +1 -0
  173. package/dist/tool-schema-CBjGPrsQ.js +31 -0
  174. package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
  175. package/dist/types-B7LojTe4.d.ts +202 -0
  176. package/dist/types-_Faxb570.d.ts +439 -0
  177. package/dist/voice/client.d.ts +226 -0
  178. package/dist/voice/client.js +932 -0
  179. package/dist/voice/client.js.map +1 -0
  180. package/dist/voice/errors.d.ts +43 -0
  181. package/dist/voice/errors.js +41 -0
  182. package/dist/voice/errors.js.map +1 -0
  183. package/dist/voice/index.d.ts +271 -0
  184. package/dist/voice/index.js +1812 -0
  185. package/dist/voice/index.js.map +1 -0
  186. package/dist/voice/react.d.ts +167 -0
  187. package/dist/voice/react.js +234 -0
  188. package/dist/voice/react.js.map +1 -0
  189. package/dist/voice/sfu.d.ts +71 -0
  190. package/dist/voice/sfu.js +157 -0
  191. package/dist/voice/sfu.js.map +1 -0
  192. package/dist/voice/text.d.ts +6 -0
  193. package/dist/voice/text.js +2 -0
  194. package/dist/voice/types.d.ts +58 -0
  195. package/dist/voice/types.js +18 -0
  196. package/dist/voice/types.js.map +1 -0
  197. package/dist/voice/workers-ai.d.ts +136 -0
  198. package/dist/voice/workers-ai.js +568 -0
  199. package/dist/voice/workers-ai.js.map +1 -0
  200. package/dist/websockets/index.d.ts +192 -0
  201. package/dist/websockets/index.js +2 -0
  202. package/dist/websockets-DUfRHPRq.js +502 -0
  203. package/dist/websockets-DUfRHPRq.js.map +1 -0
  204. package/dist/workflow-types.d.ts +25 -25
  205. package/dist/workflows.d.ts +22 -22
  206. package/dist/workflows.js +2 -1
  207. package/dist/workflows.js.map +1 -1
  208. package/dist/{ws-chat-transport-CIoOBbO7.js → ws-chat-transport-rWwta645.js} +152 -15
  209. package/dist/ws-chat-transport-rWwta645.js.map +1 -0
  210. package/docs/agent-class.md +29 -87
  211. package/docs/agent-tools.md +2 -1
  212. package/docs/channels.md +323 -0
  213. package/docs/chat-agents.md +19 -25
  214. package/docs/context.md +131 -0
  215. package/docs/durable-execution.md +1 -1
  216. package/docs/http-websockets.md +1 -11
  217. package/docs/human-in-the-loop.md +1 -1
  218. package/docs/index.md +16 -12
  219. package/docs/lifecycle.md +370 -0
  220. package/docs/long-running-agents.md +4 -6
  221. package/docs/mcp-client.md +55 -0
  222. package/docs/mcp-servers.md +5 -1
  223. package/docs/observability.md +11 -11
  224. package/docs/resumable-streaming.md +2 -2
  225. package/docs/routing.md +105 -0
  226. package/docs/scheduling.md +175 -15
  227. package/docs/server-driven-messages.md +1 -1
  228. package/docs/sessions.md +237 -871
  229. package/docs/streams.md +213 -0
  230. package/docs/sub-agents.md +185 -125
  231. package/docs/tasks.md +246 -0
  232. package/docs/voice.md +745 -0
  233. package/package.json +144 -33
  234. package/dist/cli/index.js +0 -26
  235. package/dist/cli/index.js.map +0 -1
  236. package/dist/client-invoker-VNZ7X0nn.js.map +0 -1
  237. package/dist/client-zqKcsyFa.js.map +0 -1
  238. package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
  239. package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
  240. package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
  241. package/dist/experimental/memory/session/index.d.ts +0 -670
  242. package/dist/experimental/memory/session/index.js +0 -2374
  243. package/dist/experimental/memory/session/index.js.map +0 -1
  244. package/dist/experimental/memory/utils/index.d.ts +0 -96
  245. package/dist/experimental/memory/utils/index.js +0 -79
  246. package/dist/experimental/memory/utils/index.js.map +0 -1
  247. package/dist/handler-stateless-CIkKPETH.js.map +0 -1
  248. package/dist/index.js.map +0 -1
  249. package/dist/internal_context-Dg4Cgjcu.d.ts +0 -37
  250. package/dist/internal_context.js.map +0 -1
  251. package/dist/mcp/client.d.ts +0 -42
  252. package/dist/mcp/do-oauth-client-provider.js.map +0 -1
  253. package/dist/mcp/x402.js.map +0 -1
  254. package/dist/protocol-Dqc2MQxo.js.map +0 -1
  255. package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
  256. package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
  257. package/dist/ws-chat-transport-CIoOBbO7.js.map +0 -1
  258. /package/dist/{cli/index.d.ts → index-BVVgDSdq.d.ts} +0 -0
@@ -0,0 +1,2063 @@
1
+ import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../classPrivateFieldGet2-DZBYAB34.js";
2
+ import { t as LifecycleCapability } from "../capability-BjSKYpzg.js";
3
+ import { t as _classPrivateMethodInitSpec } from "../classPrivateMethodInitSpec-qMjJ6sHQ.js";
4
+ import { SqlError } from "../sql-error.js";
5
+ import { n as sanitizeMessage, t as byteLength } from "../sanitize-D9TujEK8.js";
6
+ import { i as estimatedDataUrlBytes, n as estimateMessageTokens, r as estimateStringTokens, t as estimateAttachmentTokens } from "../tokens-nHAKcN6M.js";
7
+ import { createHash } from "node:crypto";
8
+ //#region src/sessions/chunking.ts
9
+ /**
10
+ * Row chunking for the Sessions capability.
11
+ *
12
+ * SQLite on a Durable Object caps how many bytes one row may hold, so a
13
+ * message whose serialized JSON exceeds the budget is split across the
14
+ * message row and numbered continuation rows. Reads concatenate the slices
15
+ * back into the original string, so a round-trip is exact and nothing is
16
+ * ever truncated or too large to store.
17
+ *
18
+ * The split is on BYTES, not characters: SQLite's limit is a byte limit and
19
+ * one character can be up to four of them. A boundary is never placed
20
+ * between a high surrogate and its low surrogate either, because a lone
21
+ * surrogate is not valid UTF-8 and would not survive the round-trip.
22
+ * `splitContent(s).join("")` always equals `s`.
23
+ */
24
+ /**
25
+ * Serialized byte ceiling for one stored row. A message larger than this is
26
+ * split across continuation rows; the overwhelmingly common message fits in
27
+ * one row and costs exactly one billed row write, as it always has.
28
+ */
29
+ const MAX_INLINE_ROW_BYTES = 1536 * 1024;
30
+ /**
31
+ * Split a serialized message into row-sized slices, root slice first.
32
+ *
33
+ * Always returns at least one slice, and never an empty slice unless the
34
+ * input itself is empty. Slice 0 lives in the message row; the rest become
35
+ * continuation rows numbered from 1.
36
+ */
37
+ function splitContent(json, budget = MAX_INLINE_ROW_BYTES) {
38
+ if (json.length === 0) return [""];
39
+ const slices = [];
40
+ let start = 0;
41
+ let bytes = 0;
42
+ let index = 0;
43
+ while (index < json.length) {
44
+ const code = json.charCodeAt(index);
45
+ let width;
46
+ let step = 1;
47
+ if (code < 128) width = 1;
48
+ else if (code < 2048) width = 2;
49
+ else if (code >= 55296 && code <= 56319 && index + 1 < json.length) {
50
+ const low = json.charCodeAt(index + 1);
51
+ if (low >= 56320 && low <= 57343) {
52
+ width = 4;
53
+ step = 2;
54
+ } else width = 3;
55
+ } else width = 3;
56
+ if (bytes > 0 && bytes + width > budget) {
57
+ slices.push(json.slice(start, index));
58
+ start = index;
59
+ bytes = 0;
60
+ }
61
+ bytes += width;
62
+ index += step;
63
+ }
64
+ slices.push(json.slice(start));
65
+ return slices;
66
+ }
67
+ //#endregion
68
+ //#region src/sessions/attachment-store.ts
69
+ /**
70
+ * Content-addressed byte storage for session attachments.
71
+ *
72
+ * An attachment is a typed media payload that never belongs inside a message
73
+ * row: an image, an audio clip, a PDF. The store holds the raw bytes under
74
+ * their SHA-256 and hands back a content address; the message keeps only a
75
+ * pointer part. Payloads larger than one SQLite row are split across numbered
76
+ * chunk rows, the same way an oversized message is.
77
+ *
78
+ * Everything here is synchronous, so bytes and the message row that points at
79
+ * them commit in ONE transaction. There is no window in which a stored pointer
80
+ * has no bytes behind it, and no cleanup pass for half-written payloads.
81
+ *
82
+ * Content addressing buys idempotency — a replayed append re-derives the same
83
+ * address and stores nothing new. It is not a space-saving claim: two messages
84
+ * that happen to carry the same image share a record, but nothing in the design
85
+ * depends on that being common.
86
+ */
87
+ /**
88
+ * SQLite window for one attachment chunk. Attachments are immutable, so a
89
+ * larger row means fewer billed writes; this leaves headroom below SQLite's
90
+ * 2 MiB ceiling for the row key and record overhead.
91
+ */
92
+ const ATTACHMENT_CHUNK_BYTES = MAX_INLINE_ROW_BYTES;
93
+ /** The content address of a payload. Pure: no storage access. */
94
+ function hashPayload(bytes) {
95
+ return createHash("sha256").update(bytes).digest("hex");
96
+ }
97
+ /** A standalone copy of a slice, since `SqlParam` accepts only `ArrayBuffer`. */
98
+ function chunkBuffer(bytes, start, end) {
99
+ return bytes.slice(start, end).buffer;
100
+ }
101
+ var _io = /* @__PURE__ */ new WeakMap();
102
+ var _AttachmentStore_brand = /* @__PURE__ */ new WeakSet();
103
+ var AttachmentStore = class {
104
+ constructor(io) {
105
+ _classPrivateMethodInitSpec(this, _AttachmentStore_brand);
106
+ _classPrivateFieldInitSpec(this, _io, void 0);
107
+ _classPrivateFieldSet2(_io, this, io);
108
+ }
109
+ ensureTables() {
110
+ _classPrivateFieldGet2(_io, this).sqlWrite(`CREATE TABLE IF NOT EXISTS cf_agents_session_attachment_meta (
111
+ hash TEXT PRIMARY KEY,
112
+ bytes INTEGER NOT NULL,
113
+ media_type TEXT NOT NULL,
114
+ chunks INTEGER NOT NULL
115
+ ) WITHOUT ROWID`, []);
116
+ _classPrivateFieldGet2(_io, this).sqlWrite(`CREATE TABLE IF NOT EXISTS cf_agents_session_attachment_chunks (
117
+ hash TEXT NOT NULL,
118
+ idx INTEGER NOT NULL,
119
+ data BLOB NOT NULL,
120
+ PRIMARY KEY (hash, idx)
121
+ ) WITHOUT ROWID`, []);
122
+ _classPrivateFieldGet2(_io, this).sqlWrite(`CREATE TABLE IF NOT EXISTS cf_agents_session_attachment_refs (
123
+ session_id TEXT NOT NULL,
124
+ message_id TEXT NOT NULL,
125
+ hash TEXT NOT NULL,
126
+ PRIMARY KEY (session_id, message_id, hash)
127
+ ) WITHOUT ROWID`, []);
128
+ }
129
+ /**
130
+ * Store one payload under its precomputed address. Idempotent: bytes that
131
+ * are already present cost a single read and no writes.
132
+ *
133
+ * Call inside the caller's transaction so the payload and the message that
134
+ * references it commit together. The address is passed in rather than
135
+ * derived so hashing stays outside the transaction.
136
+ */
137
+ put(payload, hash) {
138
+ const { bytes, mediaType } = payload;
139
+ if (_classPrivateFieldGet2(_io, this).sql("SELECT hash FROM cf_agents_session_attachment_meta WHERE hash = ?", [hash]).length > 0) return;
140
+ let chunks = 0;
141
+ for (let start = 0; start < bytes.length; start += ATTACHMENT_CHUNK_BYTES) {
142
+ const end = Math.min(start + ATTACHMENT_CHUNK_BYTES, bytes.length);
143
+ _classPrivateFieldGet2(_io, this).sqlWrite("INSERT INTO cf_agents_session_attachment_chunks (hash, idx, data) VALUES (?, ?, ?)", [
144
+ hash,
145
+ chunks,
146
+ chunkBuffer(bytes, start, end)
147
+ ]);
148
+ chunks++;
149
+ }
150
+ _classPrivateFieldGet2(_io, this).sqlWrite(`INSERT INTO cf_agents_session_attachment_meta (hash, bytes, media_type, chunks)
151
+ VALUES (?, ?, ?, ?)`, [
152
+ hash,
153
+ bytes.length,
154
+ mediaType,
155
+ chunks
156
+ ]);
157
+ }
158
+ /** Read one payload back, or `undefined` when the address is unknown. */
159
+ get(hash) {
160
+ const [meta] = _classPrivateFieldGet2(_io, this).sql("SELECT bytes, media_type, chunks FROM cf_agents_session_attachment_meta WHERE hash = ?", [hash]);
161
+ if (!meta) return void 0;
162
+ const out = new Uint8Array(meta.bytes);
163
+ if (meta.chunks > 0) {
164
+ const rows = _classPrivateFieldGet2(_io, this).sql("SELECT idx, data FROM cf_agents_session_attachment_chunks WHERE hash = ? ORDER BY idx", [hash]);
165
+ let offset = 0;
166
+ for (const row of rows) {
167
+ const slice = new Uint8Array(row.data);
168
+ out.set(slice, offset);
169
+ offset += slice.length;
170
+ }
171
+ }
172
+ return {
173
+ mediaType: meta.media_type,
174
+ bytes: out
175
+ };
176
+ }
177
+ /** Record that one message references these payloads. */
178
+ addRefs(sessionId, messageId, hashes) {
179
+ for (const hash of hashes) _classPrivateFieldGet2(_io, this).sqlWrite(`INSERT OR IGNORE INTO cf_agents_session_attachment_refs
180
+ (session_id, message_id, hash) VALUES (?, ?, ?)`, [
181
+ sessionId,
182
+ messageId,
183
+ hash
184
+ ]);
185
+ }
186
+ /**
187
+ * Point one message at exactly `hashes`, adding and dropping references to
188
+ * match, and collect whatever that orphaned. Call after the new payloads are
189
+ * stored, so a hash the message still uses is never briefly unreferenced.
190
+ */
191
+ replaceRefs(sessionId, messageId, hashes) {
192
+ const current = _classPrivateFieldGet2(_io, this).sql(`SELECT hash FROM cf_agents_session_attachment_refs
193
+ WHERE session_id = ? AND message_id = ?`, [sessionId, messageId]);
194
+ const wanted = new Set(hashes);
195
+ const held = new Set(current.map((row) => row.hash));
196
+ const dropped = [];
197
+ for (const hash of held) {
198
+ if (wanted.has(hash)) continue;
199
+ _classPrivateFieldGet2(_io, this).sqlWrite(`DELETE FROM cf_agents_session_attachment_refs
200
+ WHERE session_id = ? AND message_id = ? AND hash = ?`, [
201
+ sessionId,
202
+ messageId,
203
+ hash
204
+ ]);
205
+ dropped.push(hash);
206
+ }
207
+ this.addRefs(sessionId, messageId, hashes.filter((hash) => !held.has(hash)));
208
+ if (dropped.length > 0) _assertClassBrand(_AttachmentStore_brand, this, _collect).call(this, dropped);
209
+ }
210
+ /**
211
+ * Drop the references held by the given messages and collect any payload
212
+ * that no longer has a reader.
213
+ */
214
+ releaseMessages(sessionId, messageIds) {
215
+ if (messageIds.length === 0) return;
216
+ const ids = JSON.stringify(messageIds);
217
+ const orphanCandidates = _classPrivateFieldGet2(_io, this).sql(`SELECT DISTINCT hash FROM cf_agents_session_attachment_refs
218
+ WHERE session_id = ? AND message_id IN (SELECT value FROM json_each(?))`, [sessionId, ids]);
219
+ if (orphanCandidates.length === 0) return;
220
+ _classPrivateFieldGet2(_io, this).sqlWrite(`DELETE FROM cf_agents_session_attachment_refs
221
+ WHERE session_id = ? AND message_id IN (SELECT value FROM json_each(?))`, [sessionId, ids]);
222
+ _assertClassBrand(_AttachmentStore_brand, this, _collect).call(this, orphanCandidates.map((row) => row.hash));
223
+ }
224
+ /** Drop every reference held by one session and collect what it orphaned. */
225
+ releaseSession(sessionId) {
226
+ const candidates = _classPrivateFieldGet2(_io, this).sql("SELECT DISTINCT hash FROM cf_agents_session_attachment_refs WHERE session_id = ?", [sessionId]);
227
+ if (candidates.length === 0) return;
228
+ _classPrivateFieldGet2(_io, this).sqlWrite("DELETE FROM cf_agents_session_attachment_refs WHERE session_id = ?", [sessionId]);
229
+ _assertClassBrand(_AttachmentStore_brand, this, _collect).call(this, candidates.map((row) => row.hash));
230
+ }
231
+ };
232
+ /** Delete payloads that no reference points at any more. */
233
+ function _collect(hashes) {
234
+ for (const hash of hashes) {
235
+ if (_classPrivateFieldGet2(_io, this).sql("SELECT hash FROM cf_agents_session_attachment_refs WHERE hash = ? LIMIT 1", [hash]).length > 0) continue;
236
+ _classPrivateFieldGet2(_io, this).sqlWrite("DELETE FROM cf_agents_session_attachment_chunks WHERE hash = ?", [hash]);
237
+ _classPrivateFieldGet2(_io, this).sqlWrite("DELETE FROM cf_agents_session_attachment_meta WHERE hash = ?", [hash]);
238
+ }
239
+ }
240
+ //#endregion
241
+ //#region src/sessions/attachment-ingest.ts
242
+ /**
243
+ * Which parts of a message become attachments, and how they come back.
244
+ *
245
+ * The rule is typed, not size-based: a part that DECLARES a non-text media
246
+ * type and carries its payload inline is an attachment, whatever its size.
247
+ * Text, reasoning and plain tool output are never extracted — they stay in the
248
+ * row and, if they are too large for one, chunk across continuation rows.
249
+ *
250
+ * That distinction is the whole point. Sizing the rule off bytes would make a
251
+ * message's stored shape depend on how big an image happened to be, which is
252
+ * how the previous design ended up as a rescue mechanism that competed with
253
+ * row chunking. Extraction here is uniform, so a reader never has to ask why
254
+ * one image is a pointer and another is inline.
255
+ *
256
+ * Extraction is lossless: bytes move to the attachment store and a read puts
257
+ * them back verbatim. Nothing that shapes what a model SEES belongs here — a
258
+ * cap on tool output discards content and so lives on the read path, in
259
+ * `agents/context`, where it can change without having destroyed anything.
260
+ */
261
+ /** Pointer scheme written into a stored part in place of its payload. */
262
+ const ATTACHMENT_URL_PREFIX = "attachment:sha256:";
263
+ /** Hostile or deeply nested tool output stops here rather than recursing forever. */
264
+ const MAX_WALK_DEPTH = 8;
265
+ /** Build the pointer for a content address. */
266
+ function attachmentUrl(hash) {
267
+ return `${ATTACHMENT_URL_PREFIX}${hash}`;
268
+ }
269
+ /** The content address in a pointer, or `null` when the value is not one. */
270
+ function parseAttachmentUrl(url) {
271
+ if (typeof url !== "string" || !url.startsWith(ATTACHMENT_URL_PREFIX)) return null;
272
+ const hash = url.slice(18);
273
+ return /^[0-9a-f]{64}$/.test(hash) ? hash : null;
274
+ }
275
+ /** Parse a base64 `data:` URL. Non-base64 data URLs are left alone. */
276
+ function parseDataUrl(url) {
277
+ if (!url.startsWith("data:")) return null;
278
+ const comma = url.indexOf(",");
279
+ if (comma < 0) return null;
280
+ const header = url.slice(5, comma);
281
+ if (!header.endsWith(";base64")) return null;
282
+ return {
283
+ mediaType: header.slice(0, -7) || "application/octet-stream",
284
+ payload: url.slice(comma + 1)
285
+ };
286
+ }
287
+ /**
288
+ * Text payloads stay in the message. They chunk perfectly well, they are what
289
+ * FTS indexes, and moving them out would put prose behind a pointer for no gain.
290
+ */
291
+ function isTextMediaType(mediaType) {
292
+ return mediaType.startsWith("text/");
293
+ }
294
+ function decodeBase64(payload) {
295
+ const compact = /[\t\n\f\r ]/.test(payload) ? payload.replace(/[\t\n\f\r ]/g, "") : payload;
296
+ if (compact.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(compact)) return null;
297
+ let binary;
298
+ try {
299
+ binary = atob(compact);
300
+ } catch {
301
+ return null;
302
+ }
303
+ const bytes = new Uint8Array(binary.length);
304
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
305
+ return bytes;
306
+ }
307
+ function encodeBase64(bytes) {
308
+ const step = 32768;
309
+ let binary = "";
310
+ for (let i = 0; i < bytes.length; i += step) binary += String.fromCharCode(...bytes.subarray(i, i + step));
311
+ return btoa(binary);
312
+ }
313
+ /** Rebuild the exact `data:` URL a payload was extracted from. */
314
+ function dataUrl(mediaType, bytes) {
315
+ return `data:${mediaType};base64,${encodeBase64(bytes)}`;
316
+ }
317
+ /**
318
+ * A value carrying inline media: anything that declares a `mediaType` and
319
+ * holds its bytes in a `url` data URL or a base64 `data` string. Covers AI SDK
320
+ * file parts and the media entries tool results return, without either shape
321
+ * being named here.
322
+ */
323
+ function inlineMediaOf(value) {
324
+ const declared = typeof value.mediaType === "string" ? value.mediaType : null;
325
+ const url = value.url;
326
+ if (typeof url === "string") {
327
+ const parsed = parseDataUrl(url);
328
+ if (!parsed) return null;
329
+ const mediaType = declared ?? parsed.mediaType;
330
+ if (isTextMediaType(mediaType)) return null;
331
+ const bytes = decodeBase64(parsed.payload);
332
+ return bytes ? {
333
+ field: "url",
334
+ mediaType,
335
+ bytes
336
+ } : null;
337
+ }
338
+ if (declared && typeof value.data === "string" && !isTextMediaType(declared)) {
339
+ const bytes = decodeBase64(value.data);
340
+ return bytes ? {
341
+ field: "data",
342
+ mediaType: declared,
343
+ bytes
344
+ } : null;
345
+ }
346
+ return null;
347
+ }
348
+ /**
349
+ * Replace inline media with pointers, collecting the payloads to store.
350
+ *
351
+ * Returns the original message by reference when nothing was extracted, so the
352
+ * overwhelmingly common text-only write allocates nothing and pays only a walk.
353
+ */
354
+ function extractAttachments(message) {
355
+ const attachments = [];
356
+ const seen = /* @__PURE__ */ new Set();
357
+ const walk = (value, depth) => {
358
+ if (depth > MAX_WALK_DEPTH || value === null || typeof value !== "object") return value;
359
+ if (Array.isArray(value)) {
360
+ let changed = false;
361
+ const next = value.map((entry) => {
362
+ const walked = walk(entry, depth + 1);
363
+ if (walked !== entry) changed = true;
364
+ return walked;
365
+ });
366
+ return changed ? next : value;
367
+ }
368
+ const record = value;
369
+ const media = inlineMediaOf(record);
370
+ if (media) {
371
+ const hash = hashPayload(media.bytes);
372
+ if (!seen.has(hash)) {
373
+ seen.add(hash);
374
+ attachments.push({
375
+ hash,
376
+ payload: {
377
+ bytes: media.bytes,
378
+ mediaType: media.mediaType
379
+ }
380
+ });
381
+ }
382
+ return {
383
+ ...record,
384
+ mediaType: media.mediaType,
385
+ [media.field]: attachmentUrl(hash)
386
+ };
387
+ }
388
+ let changed = false;
389
+ const next = {};
390
+ for (const [key, entry] of Object.entries(record)) {
391
+ const walked = walk(entry, depth + 1);
392
+ if (walked !== entry) changed = true;
393
+ next[key] = walked;
394
+ }
395
+ return changed ? next : value;
396
+ };
397
+ if (message.parts.length === 0) return {
398
+ message,
399
+ attachments
400
+ };
401
+ const parts = walk(message.parts, 0);
402
+ if (parts === message.parts) return {
403
+ message,
404
+ attachments
405
+ };
406
+ return {
407
+ message: {
408
+ ...message,
409
+ parts
410
+ },
411
+ attachments
412
+ };
413
+ }
414
+ /**
415
+ * Put payloads back inline, undoing extraction exactly.
416
+ *
417
+ * A pointer whose bytes cannot be loaded is left as it is: an unresolvable
418
+ * pointer is a truthful record that the reference survived its payload, which
419
+ * only a bug could produce, and inventing a placeholder would hide it.
420
+ */
421
+ function resolveAttachments(message, load) {
422
+ const walk = (value, depth) => {
423
+ if (depth > MAX_WALK_DEPTH || value === null || typeof value !== "object") return value;
424
+ if (Array.isArray(value)) {
425
+ let changed = false;
426
+ const next = value.map((entry) => {
427
+ const walked = walk(entry, depth + 1);
428
+ if (walked !== entry) changed = true;
429
+ return walked;
430
+ });
431
+ return changed ? next : value;
432
+ }
433
+ const record = value;
434
+ const urlHash = parseAttachmentUrl(record.url);
435
+ if (urlHash) {
436
+ const loaded = load(urlHash);
437
+ if (!loaded) return value;
438
+ return {
439
+ ...record,
440
+ url: dataUrl(loaded.mediaType, loaded.bytes)
441
+ };
442
+ }
443
+ const dataHash = parseAttachmentUrl(record.data);
444
+ if (dataHash) {
445
+ const loaded = load(dataHash);
446
+ if (!loaded) return value;
447
+ return {
448
+ ...record,
449
+ data: encodeBase64(loaded.bytes)
450
+ };
451
+ }
452
+ let changed = false;
453
+ const next = {};
454
+ for (const [key, entry] of Object.entries(record)) {
455
+ const walked = walk(entry, depth + 1);
456
+ if (walked !== entry) changed = true;
457
+ next[key] = walked;
458
+ }
459
+ return changed ? next : value;
460
+ };
461
+ const parts = walk(message.parts, 0);
462
+ return parts === message.parts ? message : {
463
+ ...message,
464
+ parts
465
+ };
466
+ }
467
+ //#endregion
468
+ //#region src/sessions/compaction-helpers.ts
469
+ /** Prefix for all compaction messages (overlays and summaries) */
470
+ const COMPACTION_PREFIX = "compaction_";
471
+ /** Head messages kept verbatim so the conversation's opening survives. */
472
+ const PROTECT_HEAD = 3;
473
+ /** Tail messages kept verbatim regardless of the token budget. */
474
+ const MIN_TAIL_MESSAGES = 2;
475
+ /** Check if a message is a compaction message */
476
+ function isCompactionMessage(msg) {
477
+ return msg.id.startsWith(COMPACTION_PREFIX);
478
+ }
479
+ /**
480
+ * Check if a message contains tool invocations.
481
+ */
482
+ function hasToolCalls(msg) {
483
+ return msg.parts.some((p) => p.type.startsWith("tool-") || p.type === "dynamic-tool");
484
+ }
485
+ /**
486
+ * Get tool call IDs from a message's parts.
487
+ */
488
+ function getToolCallIds(msg) {
489
+ const ids = /* @__PURE__ */ new Set();
490
+ for (const part of msg.parts) if ((part.type.startsWith("tool-") || part.type === "dynamic-tool") && "toolCallId" in part) ids.add(part.toolCallId);
491
+ return ids;
492
+ }
493
+ /**
494
+ * Check if a message is a tool result referencing a specific call ID.
495
+ */
496
+ function isToolResultFor(msg, callIds) {
497
+ return msg.parts.some((p) => (p.type.startsWith("tool-") || p.type === "dynamic-tool") && "toolCallId" in p && callIds.has(p.toolCallId));
498
+ }
499
+ /**
500
+ * Align a boundary index forward to avoid splitting tool call/result groups.
501
+ * If the boundary falls between an assistant message with tool calls and its
502
+ * tool results, move it forward past the results.
503
+ */
504
+ function alignBoundaryForward(messages, idx) {
505
+ if (idx <= 0 || idx >= messages.length) return idx;
506
+ const prev = messages[idx - 1];
507
+ if (prev.role === "assistant" && hasToolCalls(prev)) {
508
+ const callIds = getToolCallIds(prev);
509
+ while (idx < messages.length && isToolResultFor(messages[idx], callIds)) idx++;
510
+ }
511
+ return idx;
512
+ }
513
+ /**
514
+ * Align a boundary index backward to avoid splitting tool call/result groups.
515
+ * If the boundary falls in the middle of tool results, move it backward to
516
+ * include the assistant message that made the calls.
517
+ */
518
+ function alignBoundaryBackward(messages, idx) {
519
+ if (idx <= 0 || idx >= messages.length) return idx;
520
+ while (idx > 0) {
521
+ const msg = messages[idx];
522
+ if (msg.role === "assistant" && hasToolCalls(msg)) break;
523
+ const prev = messages[idx - 1];
524
+ if (prev.role === "assistant" && hasToolCalls(prev)) {
525
+ if (isToolResultFor(msg, getToolCallIds(prev))) {
526
+ idx--;
527
+ continue;
528
+ }
529
+ }
530
+ break;
531
+ }
532
+ return idx;
533
+ }
534
+ /**
535
+ * Find the compression end boundary using a token budget for the tail.
536
+ * Walks backward from the end, accumulating tokens until budget is reached.
537
+ * Returns the index where compression should stop (everything from this
538
+ * index onward is protected).
539
+ *
540
+ * @param messages All messages
541
+ * @param headEnd Index where the protected head ends (compression starts here)
542
+ * @param tailTokenBudget Maximum tokens to keep in the tail
543
+ * @param minTailMessages Minimum messages to protect in the tail (fallback)
544
+ */
545
+ function findTailCutByTokens(messages, headEnd, tailTokenBudget = 2e4, minTailMessages = 2) {
546
+ const n = messages.length;
547
+ let accumulated = 0;
548
+ let tokenCut = n;
549
+ for (let i = n - 1; i >= headEnd; i--) {
550
+ const msgTokens = estimateMessageTokens([messages[i]]);
551
+ if (accumulated + msgTokens > tailTokenBudget && tokenCut < n) break;
552
+ accumulated += msgTokens;
553
+ tokenCut = i;
554
+ }
555
+ const minCut = n - minTailMessages;
556
+ return alignBoundaryBackward(messages, minCut >= headEnd ? Math.min(tokenCut, minCut) : tokenCut);
557
+ }
558
+ function computeSummaryBudget(messages) {
559
+ const contentTokens = estimateMessageTokens(messages);
560
+ const budget = Math.floor(contentTokens * .2);
561
+ return Math.max(100, budget);
562
+ }
563
+ /**
564
+ * Build a prompt for LLM summarization of compressed messages.
565
+ *
566
+ * @param messages Messages to summarize
567
+ * @param previousSummary Previous summary for iterative updates (or null for first compaction)
568
+ * @param budget Target token count for the summary
569
+ */
570
+ function buildSummaryPrompt(messages, previousSummary, budget) {
571
+ const content = messages.map((msg) => {
572
+ const textParts = msg.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
573
+ const toolParts = msg.parts.filter((p) => p.type.startsWith("tool-") || p.type === "dynamic-tool").map((p) => {
574
+ const tp = p;
575
+ const parts = [`[Tool: ${tp.toolName ?? "unknown"}]`];
576
+ if (tp.input) parts.push(`Input: ${JSON.stringify(tp.input).slice(0, 500)}`);
577
+ if (tp.output) parts.push(`Output: ${JSON.stringify(tp.output).slice(0, 500)}`);
578
+ return parts.join("\n");
579
+ }).join("\n");
580
+ return `[${msg.role}]\n${textParts}${toolParts ? "\n" + toolParts : ""}`;
581
+ }).join("\n\n---\n\n");
582
+ if (previousSummary) return `You are updating a conversation summary. A previous summary exists below. New conversation turns have occurred since then and need to be incorporated.
583
+
584
+ PREVIOUS SUMMARY:
585
+ ${previousSummary}
586
+
587
+ NEW TURNS TO INCORPORATE:
588
+ ${content}
589
+
590
+ Update the summary. PRESERVE existing information that is still relevant. ADD new information. Remove information only if it is clearly obsolete.
591
+
592
+ ## Topic
593
+ [What the conversation is about]
594
+
595
+ ## Key Points
596
+ [Important information, decisions, and conclusions from the conversation]
597
+
598
+ ## Current State
599
+ [Where things stand now — what has been done, what is in progress]
600
+
601
+ ## Open Items
602
+ [Unresolved questions, pending tasks, or next steps discussed]
603
+
604
+ Target ~${budget} tokens. Be factual — only include information that was explicitly discussed in the conversation. Do NOT invent file paths, commands, or details that were not mentioned. Write only the summary body.`;
605
+ return `Create a concise summary of this conversation that preserves the important information for future context.
606
+
607
+ CONVERSATION TO SUMMARIZE:
608
+ ${content}
609
+
610
+ Use this structure:
611
+
612
+ ## Topic
613
+ [What the conversation is about]
614
+
615
+ ## Key Points
616
+ [Important information, decisions, and conclusions from the conversation]
617
+
618
+ ## Current State
619
+ [Where things stand now — what has been done, what is in progress]
620
+
621
+ ## Open Items
622
+ [Unresolved questions, pending tasks, or next steps discussed]
623
+
624
+ Target ~${budget} tokens. Be factual — only include information that was explicitly discussed in the conversation. Do NOT invent file paths, commands, or details that were not mentioned. Write only the summary body.`;
625
+ }
626
+ /**
627
+ * Reference compaction implementation.
628
+ *
629
+ * Implements the full hermes-style compaction algorithm:
630
+ * 1. Protect head messages (first N)
631
+ * 2. Protect tail by token budget (walk backward)
632
+ * 3. Align boundaries to tool call groups
633
+ * 4. Summarize middle section with LLM (structured format)
634
+ * 5. Iterative summary updates on subsequent compactions
635
+ *
636
+ * @example
637
+ * ```typescript
638
+ * import { createCompactFunction } from "agents/sessions";
639
+ *
640
+ * sessions
641
+ * .session()
642
+ * .onCompaction(
643
+ * createCompactFunction({
644
+ * summarize: (prompt) => generateText({ model, prompt }).then((r) => r.text)
645
+ * })
646
+ * )
647
+ * .compactAfter(100_000);
648
+ * ```
649
+ */
650
+ function createCompactFunction(opts) {
651
+ const keepRecentTokens = opts.keepRecentTokens ?? 2e4;
652
+ return async (messages) => {
653
+ if (messages.length <= 5) return null;
654
+ const compressStart = alignBoundaryForward(messages, PROTECT_HEAD);
655
+ const compressEnd = findTailCutByTokens(messages, compressStart, keepRecentTokens, MIN_TAIL_MESSAGES);
656
+ if (compressEnd <= compressStart) return null;
657
+ const middleMessages = messages.slice(compressStart, compressEnd).filter((m) => !isCompactionMessage(m));
658
+ if (middleMessages.length === 0) return null;
659
+ const existingCompaction = messages.find(isCompactionMessage);
660
+ const prompt = buildSummaryPrompt(middleMessages, existingCompaction ? existingCompaction.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n") : null, computeSummaryBudget(middleMessages));
661
+ const summary = await opts.summarize(prompt);
662
+ if (!summary.trim()) return null;
663
+ return {
664
+ fromMessageId: middleMessages[0].id,
665
+ toMessageId: middleMessages[middleMessages.length - 1].id,
666
+ summary
667
+ };
668
+ };
669
+ }
670
+ //#endregion
671
+ //#region src/sessions/overlays.ts
672
+ /**
673
+ * Compaction overlay planning shared by the read path and the stats
674
+ * derivation. Reproduces the selection semantics of the legacy
675
+ * `applyCompactions` exactly: walk the path root → leaf; at each position the
676
+ * newest overlay that starts here and ends at-or-after here on this branch
677
+ * wins; its span is skipped and later overlaps inside the span never apply.
678
+ */
679
+ /** Plan overlay spans over an ordered list of path message ids. */
680
+ function planOverlays(pathIds, compactions) {
681
+ if (compactions.length === 0) return [];
682
+ const indexById = new Map(pathIds.map((id, index) => [id, index]));
683
+ const spans = [];
684
+ let i = 0;
685
+ while (i < pathIds.length) {
686
+ const compaction = compactions.filter((compaction) => compaction.fromMessageId === pathIds[i] && (indexById.get(compaction.toMessageId) ?? -1) >= i).at(-1);
687
+ if (compaction) {
688
+ const endIndex = indexById.get(compaction.toMessageId) ?? -1;
689
+ if (endIndex >= i) {
690
+ spans.push({
691
+ startIndex: i,
692
+ endIndex,
693
+ compaction
694
+ });
695
+ i = endIndex + 1;
696
+ continue;
697
+ }
698
+ }
699
+ i++;
700
+ }
701
+ return spans;
702
+ }
703
+ /** The synthetic message an overlay span renders as. */
704
+ function overlayMessage(compaction) {
705
+ return {
706
+ id: `${COMPACTION_PREFIX}${compaction.id}`,
707
+ role: "assistant",
708
+ parts: [{
709
+ type: "text",
710
+ text: compaction.summary
711
+ }],
712
+ createdAt: /* @__PURE__ */ new Date()
713
+ };
714
+ }
715
+ //#endregion
716
+ //#region src/sessions/core.ts
717
+ /**
718
+ * @internal Storage engine behind the Sessions capability. One instance per
719
+ * capability, owning the `cf_agents_session_*` tables. All methods assume the
720
+ * caller has settled startup ordering (`lifecycle.ready()` for public API).
721
+ *
722
+ * Write economics: rows written cost ~1000× rows read on DO SQLite. Every
723
+ * table is WITHOUT ROWID with no secondary index, so one row write bills one
724
+ * row. State is derived from existing rows, never kept in counter rows, and
725
+ * an unchanged update writes nothing. The only in-memory state is the tail
726
+ * of each session (its leaf id and next `seq`), read once per object
727
+ * lifetime because finding it means scanning the session's rows.
728
+ */
729
+ /**
730
+ * Bounds for each content-hydration query on a history path. In workerd the
731
+ * SQLite allocator shares the isolate's memory budget with the JS heap, so
732
+ * oversized transient result sets surface as SQLITE_NOMEM (#1710). Chunks
733
+ * are bounded by BOTH row count and cumulative stored bytes.
734
+ */
735
+ const HISTORY_CONTENT_CHUNK_SIZE = 50;
736
+ const HISTORY_CONTENT_CHUNK_BYTES = 4 * 1024 * 1024;
737
+ /**
738
+ * Rows per content window on a newest-first read. Such a read walks the path
739
+ * by id alone — no per-row byte subqueries, so no byte-bounded chunking — and
740
+ * is typically stopped by its consumer within the first few messages, so a
741
+ * small fixed window keeps both the rows read and the memory held low.
742
+ */
743
+ const NEWEST_FIRST_WINDOW_ROWS = 8;
744
+ /**
745
+ * Deepest path a history read follows: the root row is depth 0, so a read
746
+ * returns at most this many rows plus one. A longer branch shows its most
747
+ * recent rows only, and `getRecentHistory` reports that as truncated.
748
+ */
749
+ const MAX_PATH_DEPTH = 1e4;
750
+ var _reservedMetadataKeys = /* @__PURE__ */ new WeakMap();
751
+ var _listeners = /* @__PURE__ */ new WeakMap();
752
+ var _tails = /* @__PURE__ */ new WeakMap();
753
+ var _pathTokens = /* @__PURE__ */ new WeakMap();
754
+ var _attachments = /* @__PURE__ */ new WeakMap();
755
+ var _tablesEnsured = /* @__PURE__ */ new WeakMap();
756
+ var _fts = /* @__PURE__ */ new WeakMap();
757
+ var _SessionsCore_brand = /* @__PURE__ */ new WeakSet();
758
+ var SessionsCore = class {
759
+ constructor(options, io) {
760
+ _classPrivateMethodInitSpec(this, _SessionsCore_brand);
761
+ _classPrivateFieldInitSpec(this, _reservedMetadataKeys, void 0);
762
+ _classPrivateFieldInitSpec(this, _listeners, /* @__PURE__ */ new Set());
763
+ _classPrivateFieldInitSpec(this, _tails, /* @__PURE__ */ new Map());
764
+ _classPrivateFieldInitSpec(this, _pathTokens, /* @__PURE__ */ new Map());
765
+ _classPrivateFieldInitSpec(this, _attachments, void 0);
766
+ _classPrivateFieldInitSpec(this, _tablesEnsured, false);
767
+ _classPrivateFieldInitSpec(this, _fts, false);
768
+ this.io = io;
769
+ _classPrivateFieldSet2(_attachments, this, new AttachmentStore(io));
770
+ _classPrivateFieldSet2(_reservedMetadataKeys, this, options.reservedMetadataKeys ?? []);
771
+ }
772
+ ensureTables() {
773
+ if (_classPrivateFieldGet2(_tablesEnsured, this)) return;
774
+ this.io.sqlWrite(`CREATE TABLE IF NOT EXISTS cf_agents_session_messages (
775
+ session_id TEXT NOT NULL,
776
+ id TEXT NOT NULL,
777
+ seq INTEGER NOT NULL,
778
+ parent_id TEXT,
779
+ type TEXT NOT NULL DEFAULT 'message',
780
+ role TEXT NOT NULL,
781
+ content TEXT NOT NULL,
782
+ content_chunks INTEGER NOT NULL DEFAULT 0,
783
+ token_estimate INTEGER NOT NULL DEFAULT 0,
784
+ created_at INTEGER NOT NULL,
785
+ PRIMARY KEY (session_id, id)
786
+ ) WITHOUT ROWID`, []);
787
+ this.io.sqlWrite(`CREATE TABLE IF NOT EXISTS cf_agents_session_message_chunks (
788
+ session_id TEXT NOT NULL,
789
+ id TEXT NOT NULL,
790
+ idx INTEGER NOT NULL,
791
+ content TEXT NOT NULL,
792
+ PRIMARY KEY (session_id, id, idx)
793
+ ) WITHOUT ROWID`, []);
794
+ this.io.sqlWrite(`CREATE TABLE IF NOT EXISTS cf_agents_session_compactions (
795
+ session_id TEXT NOT NULL,
796
+ id TEXT NOT NULL,
797
+ seq INTEGER NOT NULL,
798
+ summary TEXT NOT NULL,
799
+ from_message_id TEXT NOT NULL,
800
+ to_message_id TEXT NOT NULL,
801
+ created_at INTEGER NOT NULL,
802
+ PRIMARY KEY (session_id, id)
803
+ ) WITHOUT ROWID`, []);
804
+ this.io.sqlWrite(`CREATE TABLE IF NOT EXISTS cf_agents_session_config (
805
+ session_id TEXT NOT NULL,
806
+ key TEXT NOT NULL,
807
+ value TEXT NOT NULL,
808
+ PRIMARY KEY (session_id, key)
809
+ ) WITHOUT ROWID`, []);
810
+ _classPrivateFieldGet2(_attachments, this).ensureTables();
811
+ _classPrivateFieldSet2(_fts, this, _assertClassBrand(_SessionsCore_brand, this, _tableExists).call(this, "cf_agents_session_fts"));
812
+ _classPrivateFieldSet2(_tablesEnsured, this, true);
813
+ }
814
+ /**
815
+ * Lift the legacy `assistant_*` message and compaction tables.
816
+ *
817
+ * The copy is pure SQL, so SQLite streams it rather than materializing rows
818
+ * in the isolate. Each source is then verified row by row against its
819
+ * destination and DROPPED: keeping tombstones would leave every upgraded
820
+ * object holding its history twice inside the same 10 GB. A table whose
821
+ * verification fails is left in place with a `session:migration:incomplete`
822
+ * event, and the method returns false so the caller leaves the schema
823
+ * version unstamped and retries on a later start. `assistant_config`
824
+ * belongs to Think, which lifts and drops it itself.
825
+ */
826
+ migrateLegacy() {
827
+ _classPrivateFieldGet2(_pathTokens, this).clear();
828
+ let complete = true;
829
+ const drop = (name) => {
830
+ if (_assertClassBrand(_SessionsCore_brand, this, _tableExists).call(this, name)) this.io.sqlWrite(`DROP TABLE ${name}`, []);
831
+ };
832
+ /**
833
+ * Drop a lifted source only once every one of its rows has a copy holding
834
+ * the same payload. Matching on the key alone would accept a destination
835
+ * row that merely occupies the key.
836
+ */
837
+ const dropWhenCopied = (source, destination, payload) => {
838
+ const [counts] = this.io.sql(`SELECT
839
+ (SELECT COUNT(*) FROM ${source}) AS source,
840
+ (SELECT COUNT(*) FROM ${source} AS legacy
841
+ JOIN ${destination} AS lifted
842
+ ON lifted.session_id = legacy.session_id
843
+ AND lifted.id = legacy.id
844
+ AND lifted.${payload} = legacy.${payload}) AS copied`, []);
845
+ if (counts && counts.source === counts.copied) {
846
+ drop(source);
847
+ return;
848
+ }
849
+ complete = false;
850
+ this.io.emit("session:migration:incomplete", {
851
+ table: source,
852
+ source: counts?.source ?? 0,
853
+ copied: counts?.copied ?? 0
854
+ });
855
+ };
856
+ if (_assertClassBrand(_SessionsCore_brand, this, _tableExists).call(this, "assistant_messages")) {
857
+ this.io.sqlWrite(`INSERT OR IGNORE INTO cf_agents_session_messages
858
+ (session_id, id, seq, parent_id, role, content, token_estimate, created_at)
859
+ SELECT session_id, id,
860
+ ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at ASC, rowid ASC),
861
+ parent_id, role, content,
862
+ CAST(LENGTH(CAST(content AS BLOB)) / 4 AS INTEGER),
863
+ COALESCE(CAST(strftime('%s', created_at) AS INTEGER), 0) * 1000
864
+ FROM assistant_messages`, []);
865
+ if (_classPrivateFieldGet2(_fts, this)) _assertClassBrand(_SessionsCore_brand, this, _backfillMissingFtsRows).call(this);
866
+ dropWhenCopied("assistant_messages", "cf_agents_session_messages", "content");
867
+ }
868
+ if (_assertClassBrand(_SessionsCore_brand, this, _tableExists).call(this, "assistant_compactions")) {
869
+ this.io.sqlWrite(`INSERT OR IGNORE INTO cf_agents_session_compactions
870
+ (session_id, id, seq, summary, from_message_id, to_message_id, created_at)
871
+ SELECT session_id, id,
872
+ ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at ASC, rowid ASC),
873
+ summary, from_message_id, to_message_id,
874
+ COALESCE(CAST(strftime('%s', created_at) AS INTEGER), 0) * 1000
875
+ FROM assistant_compactions`, []);
876
+ dropWhenCopied("assistant_compactions", "cf_agents_session_compactions", "summary");
877
+ }
878
+ drop("assistant_sessions");
879
+ drop("assistant_fts");
880
+ return complete;
881
+ }
882
+ subscribe(listener) {
883
+ _classPrivateFieldGet2(_listeners, this).add(listener);
884
+ return () => _classPrivateFieldGet2(_listeners, this).delete(listener);
885
+ }
886
+ /**
887
+ * Dispatch after a durable write. A listener that throws must not turn a
888
+ * committed write into a rejected call, so failures are reported through
889
+ * telemetry and dispatch continues.
890
+ */
891
+ async notify(event) {
892
+ for (const listener of _classPrivateFieldGet2(_listeners, this)) try {
893
+ await listener(event);
894
+ } catch (error) {
895
+ const detail = error instanceof Error ? error.message : String(error);
896
+ console.warn(`[Sessions] change listener failed: ${detail}`);
897
+ this.io.emit("session:error", {
898
+ sessionId: event.sessionId,
899
+ event: event.type,
900
+ error: detail
901
+ });
902
+ }
903
+ }
904
+ exists(sessionId, id) {
905
+ return this.io.sql("SELECT id FROM cf_agents_session_messages WHERE session_id = ? AND id = ?", [sessionId, id]).length > 0;
906
+ }
907
+ getMessage(sessionId, id) {
908
+ const content = _assertClassBrand(_SessionsCore_brand, this, _content).call(this, sessionId, id);
909
+ const parsed = content === null ? null : _assertClassBrand(_SessionsCore_brand, this, _parse).call(this, content);
910
+ return parsed && _assertClassBrand(_SessionsCore_brand, this, _inline).call(this, parsed);
911
+ }
912
+ /**
913
+ * Drop the in-memory tail and token-total caches for a session. For a
914
+ * caller whose enclosing transaction rolled back after an append or update
915
+ * ran inside it: the rows are gone but the caches already moved. The next
916
+ * read re-derives both from storage.
917
+ */
918
+ forgetCaches(sessionId) {
919
+ _classPrivateFieldGet2(_tails, this).delete(sessionId);
920
+ _classPrivateFieldGet2(_pathTokens, this).delete(sessionId);
921
+ }
922
+ latestLeafId(sessionId) {
923
+ return _assertClassBrand(_SessionsCore_brand, this, _tail).call(this, sessionId).leafId;
924
+ }
925
+ getLatestLeaf(sessionId) {
926
+ const leafId = this.latestLeafId(sessionId);
927
+ return leafId ? this.getMessage(sessionId, leafId) : null;
928
+ }
929
+ getBranches(sessionId, messageId) {
930
+ const rows = this.io.sql(`SELECT id, content, content_chunks FROM cf_agents_session_messages
931
+ WHERE session_id = ? AND parent_id = ? ORDER BY seq ASC`, [sessionId, messageId]);
932
+ const continued = _assertClassBrand(_SessionsCore_brand, this, _continuations).call(this, sessionId, rows.filter((row) => row.content_chunks > 0).map((row) => row.id));
933
+ const result = [];
934
+ for (const row of rows) {
935
+ const parsed = _assertClassBrand(_SessionsCore_brand, this, _parse).call(this, row.content_chunks === 0 ? row.content : row.content + (continued.get(row.id) ?? ""));
936
+ if (parsed) result.push(_assertClassBrand(_SessionsCore_brand, this, _inline).call(this, parsed));
937
+ }
938
+ return result;
939
+ }
940
+ /**
941
+ * The active branch path as content-free rows, root → leaf. Recurses over
942
+ * (id, parent_id) only — carrying content through the recursive queue
943
+ * materializes the transcript several times inside SQLite (#1710).
944
+ * `bytes` counts the message row, its continuation rows, AND the payloads
945
+ * it points at, charged at the size they take once inlined, so a byte
946
+ * budget over these rows bounds real hydrated memory.
947
+ */
948
+ pathRowStats(sessionId, leafId) {
949
+ const leaf = _assertClassBrand(_SessionsCore_brand, this, _resolveLeafId).call(this, sessionId, leafId);
950
+ if (!leaf) return [];
951
+ return this.io.sql(`WITH RECURSIVE path(id, parent_id, depth) AS (
952
+ SELECT id, parent_id, 0 FROM cf_agents_session_messages
953
+ WHERE session_id = ? AND id = ?
954
+ UNION ALL
955
+ SELECT m.id, m.parent_id, p.depth + 1 FROM cf_agents_session_messages m
956
+ JOIN path p ON m.id = p.parent_id
957
+ WHERE m.session_id = ? AND p.depth < ${MAX_PATH_DEPTH}
958
+ )
959
+ SELECT path.id AS id, am.role AS role,
960
+ LENGTH(CAST(am.content AS BLOB)) + CASE WHEN am.content_chunks = 0 THEN 0
961
+ ELSE COALESCE((
962
+ SELECT SUM(LENGTH(CAST(c.content AS BLOB)))
963
+ FROM cf_agents_session_message_chunks c
964
+ WHERE c.session_id = am.session_id AND c.id = am.id
965
+ ), 0) END
966
+ + COALESCE((
967
+ SELECT SUM((meta.bytes + 2) / 3 * 4)
968
+ FROM cf_agents_session_attachment_refs r
969
+ JOIN cf_agents_session_attachment_meta meta ON meta.hash = r.hash
970
+ WHERE r.session_id = am.session_id AND r.message_id = am.id
971
+ ), 0) AS bytes,
972
+ am.token_estimate AS tokenEstimate
973
+ FROM path JOIN cf_agents_session_messages am
974
+ ON am.session_id = ? AND am.id = path.id
975
+ ORDER BY path.depth DESC`, [
976
+ sessionId,
977
+ leaf,
978
+ sessionId,
979
+ sessionId
980
+ ]);
981
+ }
982
+ /**
983
+ * Stream the path ending at `leafId` (default: active leaf), root → leaf
984
+ * (or leaf → root with `newestFirst`), compaction overlays collapsed. Peak
985
+ * memory is one bounded content window — never the whole transcript.
986
+ */
987
+ async *streamHistory(sessionId, options) {
988
+ if (options.newestFirst === true) {
989
+ yield* _assertClassBrand(_SessionsCore_brand, this, _walkFromLeaf).call(this, sessionId, options.leafId, options.signal);
990
+ return;
991
+ }
992
+ const stats = this.pathRowStats(sessionId, options.leafId);
993
+ if (stats.length === 0) return;
994
+ yield* _assertClassBrand(_SessionsCore_brand, this, _streamStats).call(this, sessionId, stats, options.signal);
995
+ }
996
+ async getHistory(sessionId, options) {
997
+ const messages = [];
998
+ for await (const message of this.streamHistory(sessionId, options)) messages.push(message);
999
+ return messages;
1000
+ }
1001
+ /**
1002
+ * Byte-budgeted read of the most recent messages on the active branch
1003
+ * path — the longest suffix whose stored size fits `maxContentBytes`.
1004
+ *
1005
+ * There is no message-count floor: one used to exist, and it admitted
1006
+ * rows regardless of size, so a window of media-heavy messages could
1007
+ * hydrate far past the limit meant to bound it. The newest message is
1008
+ * always returned even if it alone exceeds the budget, since returning
1009
+ * nothing is worse. Overlays whose anchors fall outside the window are
1010
+ * skipped, showing the raw recent messages.
1011
+ */
1012
+ async getRecentHistory(sessionId, maxContentBytes, leafId) {
1013
+ const stats = this.pathRowStats(sessionId, leafId);
1014
+ if (stats.length === 0) return {
1015
+ messages: [],
1016
+ truncated: false,
1017
+ totalContentBytes: 0
1018
+ };
1019
+ const totalContentBytes = stats.reduce((sum, row) => sum + row.bytes, 0);
1020
+ let start = stats.length - 1;
1021
+ let used = stats[start].bytes;
1022
+ while (start > 0) {
1023
+ const next = stats[start - 1].bytes;
1024
+ if (used + next > maxContentBytes) break;
1025
+ start--;
1026
+ used += next;
1027
+ }
1028
+ const messages = [];
1029
+ for await (const message of _assertClassBrand(_SessionsCore_brand, this, _streamStats).call(this, sessionId, stats.slice(start))) messages.push(message);
1030
+ const capped = stats.length > MAX_PATH_DEPTH && _assertClassBrand(_SessionsCore_brand, this, _hasParent).call(this, sessionId, stats[0].id);
1031
+ return {
1032
+ messages,
1033
+ truncated: start > 0 || capped,
1034
+ totalContentBytes
1035
+ };
1036
+ }
1037
+ /**
1038
+ * Heuristic token estimate for the active path with compaction overlays
1039
+ * applied: stamped per-row estimates, minus compacted spans, plus their
1040
+ * summaries. Derived from content-free rows on each call; it gates cheap
1041
+ * triggers only, and model-reported usage stays authoritative.
1042
+ */
1043
+ tokenEstimate(sessionId) {
1044
+ const leafId = this.latestLeafId(sessionId);
1045
+ const memo = _classPrivateFieldGet2(_pathTokens, this).get(sessionId);
1046
+ if (memo && memo.leafId === leafId) return Math.max(0, Math.ceil(memo.total));
1047
+ const stats = this.pathRowStats(sessionId);
1048
+ const counted = new Set(stats.map((row) => row.id));
1049
+ let tokens = stats.reduce((sum, row) => sum + row.tokenEstimate, 0);
1050
+ for (const span of planOverlays(stats.map((row) => row.id), this.getCompactions(sessionId))) {
1051
+ for (let i = span.startIndex; i <= span.endIndex; i++) {
1052
+ tokens -= stats[i].tokenEstimate;
1053
+ counted.delete(stats[i].id);
1054
+ }
1055
+ tokens += estimateStringTokens(span.compaction.summary);
1056
+ }
1057
+ _classPrivateFieldGet2(_pathTokens, this).set(sessionId, {
1058
+ leafId,
1059
+ counted,
1060
+ total: tokens,
1061
+ depth: stats.length
1062
+ });
1063
+ return Math.max(0, Math.ceil(tokens));
1064
+ }
1065
+ stripReservedMetadata(message) {
1066
+ if (_classPrivateFieldGet2(_reservedMetadataKeys, this).length === 0 || typeof message.metadata !== "object" || message.metadata === null || Array.isArray(message.metadata)) return message;
1067
+ const metadata = { ...message.metadata };
1068
+ let changed = false;
1069
+ for (const key of _classPrivateFieldGet2(_reservedMetadataKeys, this)) if (key in metadata) {
1070
+ delete metadata[key];
1071
+ changed = true;
1072
+ }
1073
+ if (!changed) return message;
1074
+ if (Object.keys(metadata).length > 0) return {
1075
+ ...message,
1076
+ metadata
1077
+ };
1078
+ const { metadata: _dropped, ...withoutMetadata } = message;
1079
+ return withoutMetadata;
1080
+ }
1081
+ /**
1082
+ * Stamped row estimate: the part heuristic over the message as written,
1083
+ * plus a weight per inline file payload so media never counts as zero.
1084
+ */
1085
+ estimateRowTokens(message) {
1086
+ let tokens = estimateMessageTokens([message]);
1087
+ for (const part of message.parts) {
1088
+ if (part.type !== "file") continue;
1089
+ if (typeof part.url === "string" && part.url.startsWith("data:")) tokens += estimateAttachmentTokens(part.mediaType ?? "application/octet-stream", estimatedDataUrlBytes(part.url));
1090
+ }
1091
+ return tokens;
1092
+ }
1093
+ /**
1094
+ * Durable append. The caller has already sanitized the message. Message,
1095
+ * continuation, attachment, and FTS rows commit in one synchronous SQLite
1096
+ * transaction. Returns the stored message: the input itself when it was
1097
+ * inserted (extraction is lossless, so the two are identical), or the row
1098
+ * already holding the id when it was not.
1099
+ */
1100
+ append(sessionId, message, parentId, tokenEstimate) {
1101
+ if (this.exists(sessionId, message.id)) {
1102
+ const existing = this.getMessage(sessionId, message.id);
1103
+ if (existing) return {
1104
+ inserted: false,
1105
+ message: existing
1106
+ };
1107
+ }
1108
+ const tail = _assertClassBrand(_SessionsCore_brand, this, _tail).call(this, sessionId);
1109
+ let parent;
1110
+ if (parentId === void 0) parent = tail.leafId;
1111
+ else parent = parentId && this.exists(sessionId, parentId) ? parentId : null;
1112
+ const { message: staged, attachments } = extractAttachments(message);
1113
+ const slices = splitContent(JSON.stringify(staged));
1114
+ const seq = tail.nextSeq;
1115
+ this.io.transaction(() => {
1116
+ for (const attachment of attachments) _classPrivateFieldGet2(_attachments, this).put(attachment.payload, attachment.hash);
1117
+ this.io.sqlWrite(`INSERT INTO cf_agents_session_messages
1118
+ (session_id, id, seq, parent_id, role, content, content_chunks, token_estimate, created_at)
1119
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1120
+ sessionId,
1121
+ message.id,
1122
+ seq,
1123
+ parent,
1124
+ message.role,
1125
+ slices[0],
1126
+ slices.length - 1,
1127
+ tokenEstimate,
1128
+ Date.now()
1129
+ ]);
1130
+ _assertClassBrand(_SessionsCore_brand, this, _writeContinuations).call(this, sessionId, message.id, slices);
1131
+ _classPrivateFieldGet2(_attachments, this).addRefs(sessionId, message.id, attachments.map((attachment) => attachment.hash));
1132
+ _assertClassBrand(_SessionsCore_brand, this, _indexFts).call(this, sessionId, staged, false);
1133
+ });
1134
+ _classPrivateFieldGet2(_tails, this).set(sessionId, {
1135
+ leafId: message.id,
1136
+ nextSeq: seq + 1
1137
+ });
1138
+ const memo = _classPrivateFieldGet2(_pathTokens, this).get(sessionId);
1139
+ if (memo) if (memo.leafId === parent && memo.depth <= MAX_PATH_DEPTH) {
1140
+ memo.leafId = message.id;
1141
+ memo.counted.add(message.id);
1142
+ memo.total += tokenEstimate;
1143
+ memo.depth += 1;
1144
+ } else _classPrivateFieldGet2(_pathTokens, this).delete(sessionId);
1145
+ this.io.emit("session:message:appended", {
1146
+ sessionId,
1147
+ messageId: message.id,
1148
+ tokenEstimate
1149
+ });
1150
+ return {
1151
+ inserted: true,
1152
+ message
1153
+ };
1154
+ }
1155
+ /**
1156
+ * Durable update of an existing row. An identical row writes nothing: no
1157
+ * row, no continuation, no FTS, no event. The no-op guard compares the
1158
+ * FULL reassembled content, not just the slice the message row holds.
1159
+ */
1160
+ update(sessionId, message, tokenEstimate) {
1161
+ const oldRows = this.io.sql("SELECT content, content_chunks, token_estimate FROM cf_agents_session_messages WHERE session_id = ? AND id = ?", [sessionId, message.id]);
1162
+ if (oldRows.length === 0) return "missing";
1163
+ const old = oldRows[0];
1164
+ const oldContent = old.content_chunks === 0 ? old.content : old.content + (_assertClassBrand(_SessionsCore_brand, this, _continuations).call(this, sessionId, [message.id]).get(message.id) ?? "");
1165
+ const { message: staged, attachments } = extractAttachments(message);
1166
+ const json = JSON.stringify(staged);
1167
+ if (oldContent === json) return "unchanged";
1168
+ const slices = splitContent(json);
1169
+ this.io.transaction(() => {
1170
+ for (const attachment of attachments) _classPrivateFieldGet2(_attachments, this).put(attachment.payload, attachment.hash);
1171
+ this.io.sqlWrite(`UPDATE cf_agents_session_messages
1172
+ SET role = ?, content = ?, content_chunks = ?, token_estimate = ?
1173
+ WHERE session_id = ? AND id = ?`, [
1174
+ message.role,
1175
+ slices[0],
1176
+ slices.length - 1,
1177
+ tokenEstimate,
1178
+ sessionId,
1179
+ message.id
1180
+ ]);
1181
+ if (old.content_chunks > slices.length - 1) this.io.sqlWrite(`DELETE FROM cf_agents_session_message_chunks
1182
+ WHERE session_id = ? AND id = ? AND idx > ?`, [
1183
+ sessionId,
1184
+ message.id,
1185
+ slices.length - 1
1186
+ ]);
1187
+ _assertClassBrand(_SessionsCore_brand, this, _writeContinuations).call(this, sessionId, message.id, slices);
1188
+ _classPrivateFieldGet2(_attachments, this).replaceRefs(sessionId, message.id, attachments.map((attachment) => attachment.hash));
1189
+ _assertClassBrand(_SessionsCore_brand, this, _indexFts).call(this, sessionId, staged, true);
1190
+ });
1191
+ const memo = _classPrivateFieldGet2(_pathTokens, this).get(sessionId);
1192
+ if (memo?.counted.has(message.id)) memo.total += tokenEstimate - (old.token_estimate ?? 0);
1193
+ this.io.emit("session:message:updated", {
1194
+ sessionId,
1195
+ messageId: message.id
1196
+ });
1197
+ return "updated";
1198
+ }
1199
+ /**
1200
+ * Delete rows, SPLICING children to their grandparent so a mid-chain
1201
+ * delete never decapitates older history. Only surviving boundary children
1202
+ * are rewired: a prefix delete writes one boundary child, not one child
1203
+ * per deleted message.
1204
+ */
1205
+ deleteMessages(sessionId, messageIds) {
1206
+ const uniqueIds = [...new Set(messageIds)];
1207
+ if (uniqueIds.length === 0) return;
1208
+ const ids = JSON.stringify(uniqueIds);
1209
+ this.io.transaction(() => {
1210
+ this.io.sqlWrite(`WITH RECURSIVE
1211
+ deleted(id) AS (SELECT value FROM json_each(?)),
1212
+ rewire(child_id, ancestor_id, depth) AS (
1213
+ SELECT child.id, child.parent_id, 0
1214
+ FROM cf_agents_session_messages AS child
1215
+ JOIN deleted ON deleted.id = child.parent_id
1216
+ WHERE child.session_id = ?
1217
+ AND child.id NOT IN (SELECT id FROM deleted)
1218
+ UNION ALL
1219
+ SELECT rewire.child_id, parent.parent_id, rewire.depth + 1
1220
+ FROM rewire
1221
+ JOIN cf_agents_session_messages AS parent
1222
+ ON parent.id = rewire.ancestor_id
1223
+ JOIN deleted ON deleted.id = parent.id
1224
+ WHERE parent.session_id = ? AND rewire.depth < 10000
1225
+ ),
1226
+ nearest(child_id, ancestor_id) AS (
1227
+ SELECT child_id, ancestor_id FROM rewire
1228
+ WHERE ancestor_id IS NULL
1229
+ OR ancestor_id NOT IN (SELECT id FROM deleted)
1230
+ )
1231
+ UPDATE cf_agents_session_messages
1232
+ SET parent_id = (
1233
+ SELECT nearest.ancestor_id FROM nearest
1234
+ WHERE nearest.child_id = cf_agents_session_messages.id
1235
+ )
1236
+ WHERE session_id = ?
1237
+ AND id IN (SELECT child_id FROM nearest)`, [
1238
+ ids,
1239
+ sessionId,
1240
+ sessionId,
1241
+ sessionId
1242
+ ]);
1243
+ this.io.sqlWrite(`DELETE FROM cf_agents_session_messages
1244
+ WHERE session_id = ? AND id IN (SELECT value FROM json_each(?))`, [sessionId, ids]);
1245
+ this.io.sqlWrite(`DELETE FROM cf_agents_session_message_chunks
1246
+ WHERE session_id = ? AND id IN (SELECT value FROM json_each(?))`, [sessionId, ids]);
1247
+ _classPrivateFieldGet2(_attachments, this).releaseMessages(sessionId, uniqueIds);
1248
+ if (_classPrivateFieldGet2(_fts, this)) this.io.sqlWrite(`DELETE FROM cf_agents_session_fts
1249
+ WHERE session_id = ? AND id IN (SELECT value FROM json_each(?))`, [sessionId, ids]);
1250
+ });
1251
+ _classPrivateFieldGet2(_tails, this).delete(sessionId);
1252
+ _classPrivateFieldGet2(_pathTokens, this).delete(sessionId);
1253
+ this.io.emit("session:messages:deleted", {
1254
+ sessionId,
1255
+ count: uniqueIds.length
1256
+ });
1257
+ }
1258
+ clearMessages(sessionId) {
1259
+ this.io.transaction(() => {
1260
+ this.io.sqlWrite("DELETE FROM cf_agents_session_messages WHERE session_id = ?", [sessionId]);
1261
+ this.io.sqlWrite("DELETE FROM cf_agents_session_message_chunks WHERE session_id = ?", [sessionId]);
1262
+ this.io.sqlWrite("DELETE FROM cf_agents_session_compactions WHERE session_id = ?", [sessionId]);
1263
+ _classPrivateFieldGet2(_attachments, this).releaseSession(sessionId);
1264
+ if (_classPrivateFieldGet2(_fts, this)) this.io.sqlWrite("DELETE FROM cf_agents_session_fts WHERE session_id = ?", [sessionId]);
1265
+ });
1266
+ _classPrivateFieldGet2(_tails, this).set(sessionId, {
1267
+ leafId: null,
1268
+ nextSeq: 1
1269
+ });
1270
+ _classPrivateFieldGet2(_pathTokens, this).delete(sessionId);
1271
+ this.io.emit("session:cleared", { sessionId });
1272
+ }
1273
+ addCompaction(sessionId, summary, fromMessageId, toMessageId) {
1274
+ const id = crypto.randomUUID();
1275
+ const now = Date.now();
1276
+ const seq = this.io.sql("SELECT COALESCE(MAX(seq), 0) + 1 AS seq FROM cf_agents_session_compactions WHERE session_id = ?", [sessionId])[0]?.seq ?? 1;
1277
+ this.io.sqlWrite(`INSERT INTO cf_agents_session_compactions
1278
+ (session_id, id, seq, summary, from_message_id, to_message_id, created_at)
1279
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
1280
+ sessionId,
1281
+ id,
1282
+ seq,
1283
+ summary,
1284
+ fromMessageId,
1285
+ toMessageId,
1286
+ now
1287
+ ]);
1288
+ _classPrivateFieldGet2(_pathTokens, this).delete(sessionId);
1289
+ this.io.emit("session:compacted", {
1290
+ sessionId,
1291
+ compactionId: id
1292
+ });
1293
+ return {
1294
+ id,
1295
+ summary,
1296
+ fromMessageId,
1297
+ toMessageId,
1298
+ createdAt: new Date(now).toISOString()
1299
+ };
1300
+ }
1301
+ getCompactions(sessionId) {
1302
+ return this.io.sql(`SELECT id, summary, from_message_id, to_message_id, created_at
1303
+ FROM cf_agents_session_compactions
1304
+ WHERE session_id = ? ORDER BY seq ASC`, [sessionId]).map((row) => ({
1305
+ id: row.id,
1306
+ summary: row.summary,
1307
+ fromMessageId: row.from_message_id,
1308
+ toMessageId: row.to_message_id,
1309
+ createdAt: new Date(row.created_at).toISOString()
1310
+ }));
1311
+ }
1312
+ search(sessionId, query, limit) {
1313
+ _assertClassBrand(_SessionsCore_brand, this, _ensureFts).call(this);
1314
+ const sanitized = `"${query.replace(/"/g, "\"\"")}"`;
1315
+ return this.io.sql(`SELECT f.id, f.role, f.content FROM cf_agents_session_fts f
1316
+ INNER JOIN cf_agents_session_messages m
1317
+ ON m.session_id = f.session_id AND m.id = f.id
1318
+ WHERE cf_agents_session_fts MATCH ? AND f.session_id = ?
1319
+ ORDER BY rank LIMIT ?`, [
1320
+ sanitized,
1321
+ sessionId,
1322
+ limit
1323
+ ]).map((row) => ({
1324
+ id: row.id,
1325
+ role: row.role,
1326
+ content: row.content
1327
+ }));
1328
+ }
1329
+ /**
1330
+ * Import one historical message verbatim (migrations, cross-DO moves):
1331
+ * explicit parent and timestamp, stamped estimate, no change-feed events.
1332
+ */
1333
+ /** Returns `false` when the id already exists and nothing was written. */
1334
+ importMessage(sessionId, message, options) {
1335
+ const { message: staged, attachments } = extractAttachments(message);
1336
+ const slices = splitContent(JSON.stringify(staged));
1337
+ const tail = _assertClassBrand(_SessionsCore_brand, this, _tail).call(this, sessionId);
1338
+ let inserted = 0;
1339
+ this.io.transaction(() => {
1340
+ for (const attachment of attachments) _classPrivateFieldGet2(_attachments, this).put(attachment.payload, attachment.hash);
1341
+ inserted = this.io.sqlWrite(`INSERT OR IGNORE INTO cf_agents_session_messages
1342
+ (session_id, id, seq, parent_id, role, content, content_chunks, token_estimate, created_at)
1343
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1344
+ sessionId,
1345
+ message.id,
1346
+ tail.nextSeq,
1347
+ options.parentId,
1348
+ message.role,
1349
+ slices[0],
1350
+ slices.length - 1,
1351
+ this.estimateRowTokens(message),
1352
+ options.createdAt
1353
+ ]);
1354
+ if (inserted === 0) return;
1355
+ _assertClassBrand(_SessionsCore_brand, this, _writeContinuations).call(this, sessionId, message.id, slices);
1356
+ _classPrivateFieldGet2(_attachments, this).addRefs(sessionId, message.id, attachments.map((attachment) => attachment.hash));
1357
+ _assertClassBrand(_SessionsCore_brand, this, _indexFts).call(this, sessionId, staged, false);
1358
+ });
1359
+ if (inserted === 0) return false;
1360
+ _classPrivateFieldGet2(_tails, this).set(sessionId, {
1361
+ leafId: message.id,
1362
+ nextSeq: tail.nextSeq + 1
1363
+ });
1364
+ _classPrivateFieldGet2(_pathTokens, this).delete(sessionId);
1365
+ return true;
1366
+ }
1367
+ };
1368
+ function _tableExists(name) {
1369
+ return this.io.sql("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?", [name]).length > 0;
1370
+ }
1371
+ /**
1372
+ * Build the FTS index on demand. Maintaining it costs an extra billed row
1373
+ * on every append, so it exists only on objects that have actually
1374
+ * searched; the first search pays a one-time SQL backfill instead.
1375
+ */
1376
+ function _ensureFts() {
1377
+ if (_classPrivateFieldGet2(_fts, this)) return;
1378
+ this.io.sqlWrite(`CREATE VIRTUAL TABLE cf_agents_session_fts
1379
+ USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')`, []);
1380
+ _classPrivateFieldSet2(_fts, this, true);
1381
+ _assertClassBrand(_SessionsCore_brand, this, _backfillMissingFtsRows).call(this);
1382
+ }
1383
+ /** Index rows that predate indexing, in SQL, without loading JSON into JS. */
1384
+ function _backfillMissingFtsRows() {
1385
+ this.io.sqlWrite(`INSERT INTO cf_agents_session_fts (id, session_id, role, content)
1386
+ SELECT m.id, m.session_id, m.role,
1387
+ group_concat(json_extract(part.value, '$.text'), ' ')
1388
+ FROM cf_agents_session_messages AS m
1389
+ JOIN json_each(
1390
+ CASE WHEN json_valid(m.content) THEN m.content ELSE '{"parts":[]}' END,
1391
+ '$.parts'
1392
+ ) AS part
1393
+ WHERE json_extract(part.value, '$.type') = 'text'
1394
+ AND COALESCE(json_extract(part.value, '$.text'), '') <> ''
1395
+ AND NOT EXISTS (
1396
+ SELECT 1 FROM cf_agents_session_fts AS existing
1397
+ WHERE existing.id = m.id AND existing.session_id = m.session_id
1398
+ )
1399
+ GROUP BY m.id, m.session_id, m.role`, []);
1400
+ }
1401
+ /**
1402
+ * Continuation slices for the given ids, keyed by id and already joined in
1403
+ * `idx` order. One query, and only for ids the caller knows have them.
1404
+ */
1405
+ function _continuations(sessionId, ids) {
1406
+ const joined = /* @__PURE__ */ new Map();
1407
+ if (ids.length === 0) return joined;
1408
+ const rows = this.io.sql(`SELECT id, content FROM cf_agents_session_message_chunks
1409
+ WHERE session_id = ? AND id IN (SELECT value FROM json_each(?))
1410
+ ORDER BY id ASC, idx ASC`, [sessionId, JSON.stringify([...ids])]);
1411
+ for (const row of rows) joined.set(row.id, (joined.get(row.id) ?? "") + row.content);
1412
+ return joined;
1413
+ }
1414
+ /** Reassemble one stored row, reading continuations only when it has any. */
1415
+ function _content(sessionId, id) {
1416
+ const rows = this.io.sql("SELECT content, content_chunks FROM cf_agents_session_messages WHERE session_id = ? AND id = ?", [sessionId, id]);
1417
+ if (rows.length === 0) return null;
1418
+ const row = rows[0];
1419
+ if (row.content_chunks === 0) return row.content;
1420
+ return row.content + (_assertClassBrand(_SessionsCore_brand, this, _continuations).call(this, sessionId, [id]).get(id) ?? "");
1421
+ }
1422
+ function _hasParent(sessionId, id) {
1423
+ const [row] = this.io.sql("SELECT parent_id FROM cf_agents_session_messages WHERE session_id = ? AND id = ?", [sessionId, id]);
1424
+ return row?.parent_id != null;
1425
+ }
1426
+ /**
1427
+ * The session's newest row. Children insert after their parents, so the
1428
+ * max-seq row is provably childless: it is the active leaf, and the next
1429
+ * append numbers from it. Read once per object lifetime, since the table
1430
+ * is keyed by id and finding the max means scanning the session's rows.
1431
+ */
1432
+ function _tail(sessionId) {
1433
+ const cached = _classPrivateFieldGet2(_tails, this).get(sessionId);
1434
+ if (cached) return cached;
1435
+ const [row] = this.io.sql("SELECT id, seq FROM cf_agents_session_messages WHERE session_id = ? ORDER BY seq DESC LIMIT 1", [sessionId]);
1436
+ const tail = row ? {
1437
+ leafId: row.id,
1438
+ nextSeq: row.seq + 1
1439
+ } : {
1440
+ leafId: null,
1441
+ nextSeq: 1
1442
+ };
1443
+ _classPrivateFieldGet2(_tails, this).set(sessionId, tail);
1444
+ return tail;
1445
+ }
1446
+ function _resolveLeafId(sessionId, leafId) {
1447
+ if (leafId) return this.exists(sessionId, leafId) ? leafId : null;
1448
+ return this.latestLeafId(sessionId);
1449
+ }
1450
+ /**
1451
+ * The active branch path as ids alone, root → leaf. The cheapest walk the
1452
+ * tree allows — one row per step, following `parent_id` by primary key —
1453
+ * for readers that will hydrate only a few rows and do not need the sizes
1454
+ * `pathRowStats` charges per row.
1455
+ */
1456
+ function _pathIds(sessionId, leafId) {
1457
+ const leaf = _assertClassBrand(_SessionsCore_brand, this, _resolveLeafId).call(this, sessionId, leafId);
1458
+ if (!leaf) return [];
1459
+ return this.io.sql(`WITH RECURSIVE path(id, parent_id, depth) AS (
1460
+ SELECT id, parent_id, 0 FROM cf_agents_session_messages
1461
+ WHERE session_id = ? AND id = ?
1462
+ UNION ALL
1463
+ SELECT m.id, m.parent_id, p.depth + 1 FROM cf_agents_session_messages m
1464
+ JOIN path p ON m.id = p.parent_id
1465
+ WHERE m.session_id = ? AND p.depth < ${MAX_PATH_DEPTH}
1466
+ )
1467
+ SELECT id FROM path ORDER BY depth DESC`, [
1468
+ sessionId,
1469
+ leaf,
1470
+ sessionId
1471
+ ]).map((row) => row.id);
1472
+ }
1473
+ /**
1474
+ * Split path rows into bounded hydration queries: at most `maxRows` rows,
1475
+ * and — when the rows carry sizes — at most `HISTORY_CONTENT_CHUNK_BYTES`
1476
+ * of stored content, with a single oversized row always standing alone.
1477
+ */
1478
+ function* _boundedStatsChunks(rows, maxRows = HISTORY_CONTENT_CHUNK_SIZE) {
1479
+ let start = 0;
1480
+ while (start < rows.length) {
1481
+ let end = start;
1482
+ let bytes = 0;
1483
+ while (end < rows.length && end - start < maxRows) {
1484
+ const nextBytes = rows[end].bytes;
1485
+ if (end > start && bytes + nextBytes > HISTORY_CONTENT_CHUNK_BYTES) break;
1486
+ bytes += nextBytes;
1487
+ end++;
1488
+ }
1489
+ yield rows.slice(start, end);
1490
+ start = end;
1491
+ }
1492
+ }
1493
+ /**
1494
+ * Fetch and parse one already-bounded content window. The common window
1495
+ * has no continuation rows at all, so the second query is issued only for
1496
+ * the ids that actually carry them and never runs otherwise.
1497
+ */
1498
+ function _contentByStats(sessionId, rows) {
1499
+ const result = /* @__PURE__ */ new Map();
1500
+ if (rows.length === 0) return result;
1501
+ const fetched = this.io.sql(`SELECT id, content, content_chunks FROM cf_agents_session_messages
1502
+ WHERE session_id = ? AND id IN (SELECT value FROM json_each(?))`, [sessionId, JSON.stringify(rows.map((row) => row.id))]);
1503
+ const continued = _assertClassBrand(_SessionsCore_brand, this, _continuations).call(this, sessionId, fetched.filter((row) => row.content_chunks > 0).map((row) => row.id));
1504
+ for (const row of fetched) {
1505
+ const parsed = _assertClassBrand(_SessionsCore_brand, this, _parse).call(this, row.content_chunks === 0 ? row.content : row.content + (continued.get(row.id) ?? ""));
1506
+ if (parsed) result.set(row.id, _assertClassBrand(_SessionsCore_brand, this, _inline).call(this, parsed));
1507
+ }
1508
+ return result;
1509
+ }
1510
+ /**
1511
+ * Stream a known path window without retaining earlier content chunks.
1512
+ *
1513
+ * The path is first cut into segments — a compaction overlay, or a run of
1514
+ * raw rows between overlays — in root → leaf order. `newestFirst` walks
1515
+ * those segments, and the rows inside each, from the leaf instead, in
1516
+ * small fixed windows, so the first content fetched is the newest and a
1517
+ * consumer that stops early never touches older rows.
1518
+ */
1519
+ async function* _streamStats(sessionId, stats, signal, newestFirst = false, plannedSpans) {
1520
+ const spans = plannedSpans ?? planOverlays(stats.map((row) => row.id), this.getCompactions(sessionId));
1521
+ const spanByStart = new Map(spans.map((span) => [span.startIndex, span]));
1522
+ const segments = [];
1523
+ let index = 0;
1524
+ while (index < stats.length) {
1525
+ const span = spanByStart.get(index);
1526
+ if (span) {
1527
+ segments.push({ overlay: span.compaction });
1528
+ index = span.endIndex + 1;
1529
+ continue;
1530
+ }
1531
+ let runEnd = index + 1;
1532
+ while (runEnd < stats.length && !spanByStart.has(runEnd)) runEnd++;
1533
+ segments.push({ rows: stats.slice(index, runEnd) });
1534
+ index = runEnd;
1535
+ }
1536
+ if (newestFirst) segments.reverse();
1537
+ for (const segment of segments) {
1538
+ if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("History read aborted");
1539
+ if ("overlay" in segment) {
1540
+ yield overlayMessage(segment.overlay);
1541
+ continue;
1542
+ }
1543
+ const rows = newestFirst ? [...segment.rows].reverse() : segment.rows;
1544
+ const windowRows = newestFirst ? NEWEST_FIRST_WINDOW_ROWS : HISTORY_CONTENT_CHUNK_SIZE;
1545
+ for (const chunk of _assertClassBrand(_SessionsCore_brand, this, _boundedStatsChunks).call(this, rows, windowRows)) {
1546
+ const content = _assertClassBrand(_SessionsCore_brand, this, _contentByStats).call(this, sessionId, chunk);
1547
+ for (const row of chunk) {
1548
+ const parsed = content.get(row.id);
1549
+ if (parsed) yield parsed;
1550
+ }
1551
+ if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("History read aborted");
1552
+ }
1553
+ }
1554
+ }
1555
+ /**
1556
+ * The path leaf → root as a chain of point reads: each row names its
1557
+ * parent, so the next read is known before the current message is
1558
+ * yielded, and a consumer that stops early has paid for exactly the rows it
1559
+ * saw.
1560
+ *
1561
+ * Compaction overlays are honored without planning them up front. An
1562
+ * overlay that applies to this branch ends at a row the walk reaches
1563
+ * before any row it covers, so the raw walk is exact until it lands on
1564
+ * some compaction's `toMessageId`. Only then is the remaining prefix read
1565
+ * as ids and planned root → leaf — the order overlay selection is defined
1566
+ * in — and streamed leaf-first with the overlays collapsed. A lookup that
1567
+ * stops in the messages after the last compaction never pays for that.
1568
+ */
1569
+ async function* _walkFromLeaf(sessionId, leafId, signal) {
1570
+ const compactions = this.getCompactions(sessionId);
1571
+ const spanEnds = new Set(compactions.map((c) => c.toMessageId));
1572
+ let next = _assertClassBrand(_SessionsCore_brand, this, _resolveLeafId).call(this, sessionId, leafId);
1573
+ let depth = 0;
1574
+ while (next !== null && depth <= MAX_PATH_DEPTH) {
1575
+ if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("History read aborted");
1576
+ if (spanEnds.has(next)) {
1577
+ yield* _assertClassBrand(_SessionsCore_brand, this, _streamOverlaidPrefix).call(this, sessionId, leafId, next, compactions, signal);
1578
+ return;
1579
+ }
1580
+ const [row] = this.io.sql(`SELECT parent_id, content, content_chunks FROM cf_agents_session_messages
1581
+ WHERE session_id = ? AND id = ?`, [sessionId, next]);
1582
+ if (!row) return;
1583
+ const json = row.content_chunks === 0 ? row.content : row.content + (_assertClassBrand(_SessionsCore_brand, this, _continuations).call(this, sessionId, [next]).get(next) ?? "");
1584
+ const parsed = _assertClassBrand(_SessionsCore_brand, this, _parse).call(this, json);
1585
+ if (parsed) yield _assertClassBrand(_SessionsCore_brand, this, _inline).call(this, parsed);
1586
+ next = row.parent_id;
1587
+ depth++;
1588
+ }
1589
+ }
1590
+ /**
1591
+ * The path from `fromId` down to the root, leaf-first with overlays
1592
+ * collapsed. Spans are planned over the WHOLE path (selection is
1593
+ * root → leaf and a chosen span suppresses overlaps inside it), then only
1594
+ * those ending at or before `fromId` apply: `fromId` is some compaction's
1595
+ * end, and a chosen span reaching past it would have ended at a row the
1596
+ * raw walk visited first.
1597
+ */
1598
+ async function* _streamOverlaidPrefix(sessionId, leafId, fromId, compactions, signal) {
1599
+ const ids = _assertClassBrand(_SessionsCore_brand, this, _pathIds).call(this, sessionId, leafId);
1600
+ const end = ids.indexOf(fromId);
1601
+ if (end === -1) return;
1602
+ const spans = planOverlays(ids, compactions).filter((span) => span.endIndex <= end);
1603
+ yield* _assertClassBrand(_SessionsCore_brand, this, _streamStats).call(this, sessionId, ids.slice(0, end + 1).map((id) => ({
1604
+ id,
1605
+ bytes: 0
1606
+ })), signal, true, spans);
1607
+ }
1608
+ /**
1609
+ * Write one message's slices: the row itself plus its continuation rows.
1610
+ * Callers run this inside their own transaction, so a message and its
1611
+ * continuations always commit together.
1612
+ */
1613
+ function _writeContinuations(sessionId, id, slices) {
1614
+ for (let idx = 1; idx < slices.length; idx++) this.io.sqlWrite(`INSERT OR REPLACE INTO cf_agents_session_message_chunks
1615
+ (session_id, id, idx, content) VALUES (?, ?, ?, ?)`, [
1616
+ sessionId,
1617
+ id,
1618
+ idx,
1619
+ slices[idx]
1620
+ ]);
1621
+ }
1622
+ /** Maintain the FTS row when the index exists; an unchanged text writes nothing. */
1623
+ function _indexFts(sessionId, message, replace) {
1624
+ if (!_classPrivateFieldGet2(_fts, this)) return;
1625
+ const text = message.parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join(" ");
1626
+ if (replace) {
1627
+ const existing = this.io.sql("SELECT content FROM cf_agents_session_fts WHERE id = ? AND session_id = ?", [message.id, sessionId]);
1628
+ if (existing.length > 0) {
1629
+ if (existing[0].content === text) return;
1630
+ this.io.sqlWrite("DELETE FROM cf_agents_session_fts WHERE id = ? AND session_id = ?", [message.id, sessionId]);
1631
+ }
1632
+ }
1633
+ if (text) this.io.sqlWrite("INSERT INTO cf_agents_session_fts (id, session_id, role, content) VALUES (?, ?, ?, ?)", [
1634
+ message.id,
1635
+ sessionId,
1636
+ message.role,
1637
+ text
1638
+ ]);
1639
+ }
1640
+ /** Put attachment payloads back inline, so a read returns what was written. */
1641
+ function _inline(message) {
1642
+ return resolveAttachments(message, (hash) => _classPrivateFieldGet2(_attachments, this).get(hash));
1643
+ }
1644
+ function _parse(json) {
1645
+ try {
1646
+ const message = JSON.parse(json);
1647
+ if (typeof message?.id === "string" && typeof message?.role === "string" && Array.isArray(message?.parts)) return message;
1648
+ } catch {}
1649
+ return null;
1650
+ }
1651
+ //#endregion
1652
+ //#region src/sessions/handle.ts
1653
+ var _core$1 = /* @__PURE__ */ new WeakMap();
1654
+ var _ready = /* @__PURE__ */ new WeakMap();
1655
+ var _compactionFn = /* @__PURE__ */ new WeakMap();
1656
+ var _tokenThreshold = /* @__PURE__ */ new WeakMap();
1657
+ var _Session_brand = /* @__PURE__ */ new WeakSet();
1658
+ var Session = class {
1659
+ /** @internal Constructed by the Sessions capability only. */
1660
+ constructor(sessionId, core, ready) {
1661
+ _classPrivateMethodInitSpec(this, _Session_brand);
1662
+ _classPrivateFieldInitSpec(this, _core$1, void 0);
1663
+ _classPrivateFieldInitSpec(this, _ready, void 0);
1664
+ _classPrivateFieldInitSpec(this, _compactionFn, null);
1665
+ _classPrivateFieldInitSpec(this, _tokenThreshold, void 0);
1666
+ this.sessionId = sessionId;
1667
+ _classPrivateFieldSet2(_core$1, this, core);
1668
+ _classPrivateFieldSet2(_ready, this, ready);
1669
+ }
1670
+ /** Register the function `compact()` calls to summarize a branch. */
1671
+ onCompaction(fn) {
1672
+ _classPrivateFieldSet2(_compactionFn, this, fn);
1673
+ return this;
1674
+ }
1675
+ /**
1676
+ * Auto-compact after an append once the estimated token count crosses the
1677
+ * threshold. Requires `onCompaction()`. The estimate is derived from the
1678
+ * stamped per-row estimates, never from the transcript.
1679
+ */
1680
+ compactAfter(tokenThreshold) {
1681
+ _classPrivateFieldSet2(_tokenThreshold, this, tokenThreshold);
1682
+ return this;
1683
+ }
1684
+ /**
1685
+ * Stream the active branch path root → leaf (leaf → root with
1686
+ * `newestFirst`) with compaction overlays applied. Peak memory is one
1687
+ * bounded content window, never the whole transcript, and a consumer that
1688
+ * breaks out early leaves the rows it never reached unread.
1689
+ */
1690
+ async *history(options = {}) {
1691
+ await _classPrivateFieldGet2(_ready, this).call(this);
1692
+ yield* _classPrivateFieldGet2(_core$1, this).streamHistory(this.sessionId, options);
1693
+ }
1694
+ /**
1695
+ * Stream history in bounded non-empty batches. Both message count and
1696
+ * serialized bytes bound each batch; a single large message is yielded by
1697
+ * itself.
1698
+ */
1699
+ async *historyBatches(options = {}) {
1700
+ const batchSize = Math.max(1, Math.floor(options.batchSize ?? 50));
1701
+ const maxBatchBytes = Math.max(1, Math.floor(options.maxBatchBytes ?? 4 * 1024 * 1024));
1702
+ let batch = [];
1703
+ let batchBytes = 0;
1704
+ for await (const message of this.history(options)) {
1705
+ const bytes = byteLength(JSON.stringify(message));
1706
+ if (batch.length > 0 && (batch.length >= batchSize || batchBytes + bytes > maxBatchBytes)) {
1707
+ yield batch;
1708
+ batch = [];
1709
+ batchBytes = 0;
1710
+ }
1711
+ batch.push(message);
1712
+ batchBytes += bytes;
1713
+ if (batch.length >= batchSize || batchBytes >= maxBatchBytes) {
1714
+ yield batch;
1715
+ batch = [];
1716
+ batchBytes = 0;
1717
+ }
1718
+ }
1719
+ if (batch.length > 0) yield batch;
1720
+ }
1721
+ /**
1722
+ * Materialize the whole selected path. Prefer `history()` or
1723
+ * `getRecentHistory()` inside a Durable Object: this holds every message
1724
+ * of the branch in memory at once.
1725
+ */
1726
+ async getHistory(options = {}) {
1727
+ await _classPrivateFieldGet2(_ready, this).call(this);
1728
+ return _classPrivateFieldGet2(_core$1, this).getHistory(this.sessionId, options);
1729
+ }
1730
+ /**
1731
+ * Byte-budgeted read of the most recent messages on the active branch path
1732
+ * (always at least the leaf). The budget counts each row, its continuation
1733
+ * rows, and the payloads it points at, so it bounds hydrated memory (#1710).
1734
+ */
1735
+ async getRecentHistory(maxContentBytes, options = {}) {
1736
+ await _classPrivateFieldGet2(_ready, this).call(this);
1737
+ return _classPrivateFieldGet2(_core$1, this).getRecentHistory(this.sessionId, maxContentBytes, options.leafId);
1738
+ }
1739
+ /**
1740
+ * Per-row stored sizes (row plus continuation rows and attachments) and
1741
+ * stamped token estimates for the active branch path (root → leaf)
1742
+ * WITHOUT loading message content.
1743
+ */
1744
+ async getHistoryRowStats(leafId) {
1745
+ await _classPrivateFieldGet2(_ready, this).call(this);
1746
+ return _classPrivateFieldGet2(_core$1, this).pathRowStats(this.sessionId, leafId);
1747
+ }
1748
+ async getMessage(id) {
1749
+ await _classPrivateFieldGet2(_ready, this).call(this);
1750
+ return _classPrivateFieldGet2(_core$1, this).getMessage(this.sessionId, id);
1751
+ }
1752
+ async getLatestLeaf() {
1753
+ await _classPrivateFieldGet2(_ready, this).call(this);
1754
+ return _classPrivateFieldGet2(_core$1, this).getLatestLeaf(this.sessionId);
1755
+ }
1756
+ async getBranches(messageId) {
1757
+ await _classPrivateFieldGet2(_ready, this).call(this);
1758
+ return _classPrivateFieldGet2(_core$1, this).getBranches(this.sessionId, messageId);
1759
+ }
1760
+ /**
1761
+ * Full-text search over this session's text parts. The index is built on
1762
+ * the first call and maintained from then on.
1763
+ */
1764
+ async search(query, options) {
1765
+ await _classPrivateFieldGet2(_ready, this).call(this);
1766
+ return _classPrivateFieldGet2(_core$1, this).search(this.sessionId, query, options?.limit ?? 20);
1767
+ }
1768
+ /**
1769
+ * Append one message. Idempotent on id: a repeated append returns the row
1770
+ * already stored and dispatches an `append` event with `inserted: false`.
1771
+ */
1772
+ async appendMessage(message, options = {}) {
1773
+ await _classPrivateFieldGet2(_ready, this).call(this);
1774
+ const prepared = _assertClassBrand(_Session_brand, this, _prepare).call(this, message, options.source);
1775
+ const result = _classPrivateFieldGet2(_core$1, this).append(this.sessionId, prepared.message, options.parentId, prepared.tokenEstimate);
1776
+ await _classPrivateFieldGet2(_core$1, this).notify({
1777
+ type: "append",
1778
+ sessionId: this.sessionId,
1779
+ message: result.message,
1780
+ parentId: options.parentId,
1781
+ inserted: result.inserted
1782
+ });
1783
+ if (result.inserted) await _assertClassBrand(_Session_brand, this, _maybeAutoCompact).call(this);
1784
+ return result;
1785
+ }
1786
+ /**
1787
+ * Update one stored row. Returns the stored form, or `null` when the id is
1788
+ * not in this session. An unchanged message writes nothing and dispatches
1789
+ * no event.
1790
+ */
1791
+ async updateMessage(message, options = {}) {
1792
+ await _classPrivateFieldGet2(_ready, this).call(this);
1793
+ const prepared = _assertClassBrand(_Session_brand, this, _prepare).call(this, message, options.source);
1794
+ const outcome = _classPrivateFieldGet2(_core$1, this).update(this.sessionId, prepared.message, prepared.tokenEstimate);
1795
+ if (outcome === "missing") return null;
1796
+ if (outcome === "updated") await _classPrivateFieldGet2(_core$1, this).notify({
1797
+ type: "update",
1798
+ sessionId: this.sessionId,
1799
+ message: prepared.message
1800
+ });
1801
+ return prepared.message;
1802
+ }
1803
+ /**
1804
+ * @internal Synchronous write aperture for same-isolate first-party
1805
+ * machinery that must commit a message inside a caller-owned SQLite
1806
+ * transaction — the stream cutover, where the finished message and the
1807
+ * discard of its stream rows land together. Bypasses readiness (the
1808
+ * caller owns startup ordering) and defers everything that follows a
1809
+ * write — the change-feed dispatch and auto-compaction: call the returned
1810
+ * `after()` once the transaction commits, or subscribers (the host's
1811
+ * message mirror) never hear about the write. If the transaction rolls
1812
+ * back after an `upsert` ran inside it, call `abandon()`: the write is
1813
+ * gone but the in-memory tail and token-total caches already moved.
1814
+ * Will break without notice; never use from application code.
1815
+ */
1816
+ __DO_NOT_USE_WILL_BREAK__sync() {
1817
+ return {
1818
+ abandon: () => _classPrivateFieldGet2(_core$1, this).forgetCaches(this.sessionId),
1819
+ upsert: (message, options = {}) => {
1820
+ const prepared = _assertClassBrand(_Session_brand, this, _prepare).call(this, message, options.source);
1821
+ if (!_classPrivateFieldGet2(_core$1, this).exists(this.sessionId, message.id)) {
1822
+ const result = _classPrivateFieldGet2(_core$1, this).append(this.sessionId, prepared.message, options.parentId, prepared.tokenEstimate);
1823
+ return {
1824
+ result,
1825
+ after: async () => {
1826
+ await _classPrivateFieldGet2(_core$1, this).notify({
1827
+ type: "append",
1828
+ sessionId: this.sessionId,
1829
+ message: result.message,
1830
+ parentId: options.parentId,
1831
+ inserted: result.inserted
1832
+ });
1833
+ if (result.inserted) await _assertClassBrand(_Session_brand, this, _maybeAutoCompact).call(this);
1834
+ }
1835
+ };
1836
+ }
1837
+ const outcome = _classPrivateFieldGet2(_core$1, this).update(this.sessionId, prepared.message, prepared.tokenEstimate);
1838
+ return {
1839
+ result: {
1840
+ inserted: false,
1841
+ message: prepared.message
1842
+ },
1843
+ after: () => outcome === "updated" ? _classPrivateFieldGet2(_core$1, this).notify({
1844
+ type: "update",
1845
+ sessionId: this.sessionId,
1846
+ message: prepared.message
1847
+ }) : Promise.resolve()
1848
+ };
1849
+ }
1850
+ };
1851
+ }
1852
+ async upsertMessage(message, options = {}) {
1853
+ await _classPrivateFieldGet2(_ready, this).call(this);
1854
+ if (!_classPrivateFieldGet2(_core$1, this).exists(this.sessionId, message.id)) return this.appendMessage(message, options);
1855
+ return {
1856
+ inserted: false,
1857
+ message: await this.updateMessage(message, { source: options.source }) ?? message
1858
+ };
1859
+ }
1860
+ /**
1861
+ * Import one historical message verbatim (migrations, cross-object moves):
1862
+ * explicit parent and timestamp. A row actually written dispatches an
1863
+ * `import` change event so a host cache can mark itself stale; it is not
1864
+ * an `append`, so a cache does not patch itself per imported row, and an
1865
+ * id that already exists writes nothing and dispatches nothing.
1866
+ */
1867
+ async importMessage(message, options) {
1868
+ await _classPrivateFieldGet2(_ready, this).call(this);
1869
+ if (!_classPrivateFieldGet2(_core$1, this).importMessage(this.sessionId, message, options)) return;
1870
+ await _classPrivateFieldGet2(_core$1, this).notify({
1871
+ type: "import",
1872
+ sessionId: this.sessionId,
1873
+ message,
1874
+ parentId: options.parentId
1875
+ });
1876
+ }
1877
+ async deleteMessages(messageIds) {
1878
+ await _classPrivateFieldGet2(_ready, this).call(this);
1879
+ _classPrivateFieldGet2(_core$1, this).deleteMessages(this.sessionId, messageIds);
1880
+ await _classPrivateFieldGet2(_core$1, this).notify({
1881
+ type: "delete",
1882
+ sessionId: this.sessionId,
1883
+ messageIds
1884
+ });
1885
+ }
1886
+ async clearMessages() {
1887
+ await _classPrivateFieldGet2(_ready, this).call(this);
1888
+ _classPrivateFieldGet2(_core$1, this).clearMessages(this.sessionId);
1889
+ await _classPrivateFieldGet2(_core$1, this).notify({
1890
+ type: "clear",
1891
+ sessionId: this.sessionId
1892
+ });
1893
+ }
1894
+ /**
1895
+ * Store an overlay directly. Dispatches a `compaction` change event: the
1896
+ * rows are untouched, but what a path read returns has changed.
1897
+ */
1898
+ async addCompaction(summary, fromMessageId, toMessageId) {
1899
+ await _classPrivateFieldGet2(_ready, this).call(this);
1900
+ const compaction = _classPrivateFieldGet2(_core$1, this).addCompaction(this.sessionId, summary, fromMessageId, toMessageId);
1901
+ await _classPrivateFieldGet2(_core$1, this).notify({
1902
+ type: "compaction",
1903
+ sessionId: this.sessionId,
1904
+ compaction
1905
+ });
1906
+ return compaction;
1907
+ }
1908
+ async getCompactions() {
1909
+ await _classPrivateFieldGet2(_ready, this).call(this);
1910
+ return _classPrivateFieldGet2(_core$1, this).getCompactions(this.sessionId);
1911
+ }
1912
+ /**
1913
+ * Run the registered compaction function and store the result as an
1914
+ * overlay. When `leafId` is given, compact that root-to-leaf branch instead
1915
+ * of the active branch. Requires `onCompaction()`. A compaction function
1916
+ * that throws is reported through the `session:error` capability event and
1917
+ * the call returns `null`.
1918
+ */
1919
+ async compact(leafId) {
1920
+ await _classPrivateFieldGet2(_ready, this).call(this);
1921
+ const fn = _classPrivateFieldGet2(_compactionFn, this);
1922
+ if (!fn) throw new Error("No compaction function registered. Call onCompaction() first.");
1923
+ const history = await _classPrivateFieldGet2(_core$1, this).getHistory(this.sessionId, { leafId });
1924
+ let result;
1925
+ try {
1926
+ result = await fn(history);
1927
+ } catch (error) {
1928
+ _classPrivateFieldGet2(_core$1, this).io.emit("session:error", {
1929
+ sessionId: this.sessionId,
1930
+ error: error instanceof Error ? error.message : String(error)
1931
+ });
1932
+ return null;
1933
+ }
1934
+ if (!result) return null;
1935
+ const historyIds = new Set(history.map((message) => message.id));
1936
+ if (!historyIds.has(result.toMessageId)) return null;
1937
+ const existing = _classPrivateFieldGet2(_core$1, this).getCompactions(this.sessionId).filter((compaction) => historyIds.has(`compaction_${compaction.id}`) || historyIds.has(compaction.fromMessageId) && historyIds.has(compaction.toMessageId));
1938
+ const fromId = existing.length > 0 ? existing[0].fromMessageId : result.fromMessageId;
1939
+ _classPrivateFieldGet2(_core$1, this).addCompaction(this.sessionId, result.summary, fromId, result.toMessageId);
1940
+ await _classPrivateFieldGet2(_core$1, this).notify({
1941
+ type: "compact",
1942
+ sessionId: this.sessionId
1943
+ });
1944
+ return {
1945
+ ...result,
1946
+ fromMessageId: fromId
1947
+ };
1948
+ }
1949
+ };
1950
+ /**
1951
+ * The shared write pipeline: sanitize provider metadata and strip reserved
1952
+ * metadata on client-source input. Content is never truncated and never
1953
+ * too large: a message that exceeds the row budget is split across
1954
+ * continuation rows by the durable write.
1955
+ */
1956
+ function _prepare(message, source) {
1957
+ let prepared = sanitizeMessage(message);
1958
+ if (source === "client") prepared = _classPrivateFieldGet2(_core$1, this).stripReservedMetadata(prepared);
1959
+ return {
1960
+ message: prepared,
1961
+ tokenEstimate: _classPrivateFieldGet2(_core$1, this).estimateRowTokens(prepared)
1962
+ };
1963
+ }
1964
+ /** Gate on the derived estimate, then compact. Failures are non-fatal. */
1965
+ async function _maybeAutoCompact() {
1966
+ const threshold = _classPrivateFieldGet2(_tokenThreshold, this);
1967
+ if (threshold == null || !_classPrivateFieldGet2(_compactionFn, this)) return;
1968
+ if (_classPrivateFieldGet2(_core$1, this).tokenEstimate(this.sessionId) <= threshold) return;
1969
+ try {
1970
+ await this.compact();
1971
+ } catch (error) {
1972
+ const detail = error instanceof Error ? error.message : String(error);
1973
+ console.warn(`[Sessions] auto-compaction failed: ${detail}`);
1974
+ _classPrivateFieldGet2(_core$1, this).io.emit("session:error", {
1975
+ sessionId: this.sessionId,
1976
+ error: detail
1977
+ });
1978
+ }
1979
+ }
1980
+ //#endregion
1981
+ //#region src/sessions/sessions.ts
1982
+ /**
1983
+ * Durable conversation history for Lifecycle Objects. `Sessions` owns the
1984
+ * `cf_agents_session_*` tables: tree-structured messages (branch
1985
+ * regeneration, latest-leaf paths), compaction overlays, and full-text
1986
+ * search whose index is built on first use. A message larger than one
1987
+ * SQLite row is split across continuation rows and reassembled on read, so
1988
+ * nothing is ever too large to store.
1989
+ *
1990
+ * Sessions consumes only the standard capability services — storage and
1991
+ * events. It needs no alarm, so it also works on facets (facets have
1992
+ * isolated SQLite but no independent alarm slot).
1993
+ *
1994
+ * @experimental The API surface may change before stabilizing.
1995
+ */
1996
+ const SESSIONS_SCHEMA_VERSION_KEY = "cf_agents:sessions_schema_version";
1997
+ const CURRENT_SESSIONS_SCHEMA_VERSION = 1;
1998
+ var _core = /* @__PURE__ */ new WeakMap();
1999
+ var _handles = /* @__PURE__ */ new WeakMap();
2000
+ /**
2001
+ * Durable conversation history for a Lifecycle Object.
2002
+ *
2003
+ * `session()` returns a per-session handle for reads (streamed, byte
2004
+ * budgeted), writes (sanitized, media offloaded), branch navigation, and
2005
+ * compaction overlays. `subscribe()` is the change feed a cache-owning host
2006
+ * mirrors.
2007
+ *
2008
+ * @experimental The API surface may change before stabilizing.
2009
+ */
2010
+ var Sessions = class extends LifecycleCapability {
2011
+ constructor(options = {}) {
2012
+ super("sessions");
2013
+ _classPrivateFieldInitSpec(this, _core, void 0);
2014
+ _classPrivateFieldInitSpec(this, _handles, /* @__PURE__ */ new Map());
2015
+ const exec = (query, params) => {
2016
+ try {
2017
+ return this.lifecycle.storage.sql.exec(query, ...params);
2018
+ } catch (cause) {
2019
+ throw new SqlError(query, cause);
2020
+ }
2021
+ };
2022
+ _classPrivateFieldSet2(_core, this, new SessionsCore(options, {
2023
+ sql: (query, params) => [...exec(query, params)],
2024
+ sqlWrite: (query, params) => exec(query, params).rowsWritten,
2025
+ transaction: (fn) => this.lifecycle.storage.transactionSync(fn),
2026
+ emit: (type, payload) => this.lifecycle.events.emit(type, payload)
2027
+ }));
2028
+ }
2029
+ /** Migrate session storage during Lifecycle startup. */
2030
+ async onStart() {
2031
+ const storage = this.lifecycle.storage;
2032
+ const version = await storage.get(SESSIONS_SCHEMA_VERSION_KEY) ?? 0;
2033
+ _classPrivateFieldGet2(_core, this).ensureTables();
2034
+ if (version < CURRENT_SESSIONS_SCHEMA_VERSION) {
2035
+ if (_classPrivateFieldGet2(_core, this).migrateLegacy()) await storage.put(SESSIONS_SCHEMA_VERSION_KEY, CURRENT_SESSIONS_SCHEMA_VERSION);
2036
+ }
2037
+ }
2038
+ /**
2039
+ * The handle for one session. The default (empty) id is the primary path:
2040
+ * in the one-object-per-conversation model a Durable Object holds exactly
2041
+ * one session. Handles are cached, so per-session configuration (the
2042
+ * compaction trigger) survives repeated calls.
2043
+ */
2044
+ session(sessionId = "") {
2045
+ const existing = _classPrivateFieldGet2(_handles, this).get(sessionId);
2046
+ if (existing) return existing;
2047
+ const handle = new Session(sessionId, _classPrivateFieldGet2(_core, this), () => this.lifecycle.ready());
2048
+ _classPrivateFieldGet2(_handles, this).set(sessionId, handle);
2049
+ return handle;
2050
+ }
2051
+ /**
2052
+ * Subscribe to the change feed: ordered dispatch after every durable
2053
+ * write, with the stored message. The host cache mirror lives here;
2054
+ * telemetry additionally flows through capability events.
2055
+ */
2056
+ subscribe(listener) {
2057
+ return _classPrivateFieldGet2(_core, this).subscribe(listener);
2058
+ }
2059
+ };
2060
+ //#endregion
2061
+ export { COMPACTION_PREFIX, Session, Sessions, createCompactFunction, isCompactionMessage };
2062
+
2063
+ //# sourceMappingURL=index.js.map