agents 0.23.0 → 0.25.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 (187) hide show
  1. package/dist/{agent-routing-DE5zmCQ8.d.ts → agent-routing-0DR0Tk2v.d.ts} +270 -190
  2. package/dist/agent-routing.d.ts +1 -1
  3. package/dist/agent-routing.js.map +1 -1
  4. package/dist/agent-tool-types.d.ts +13 -7
  5. package/dist/agent-tool-types.js +23 -1
  6. package/dist/agent-tool-types.js.map +1 -1
  7. package/dist/{agent-tools-y7zLfw4Q.js → agent-tools-CdJC-8Dc.js} +72 -9
  8. package/dist/agent-tools-CdJC-8Dc.js.map +1 -0
  9. package/dist/{agent-tools-DtXMTDGM.d.ts → agent-tools-YrATZJu2.d.ts} +29 -15
  10. package/dist/agent-tools.d.ts +6 -4
  11. package/dist/agent-tools.js.map +1 -1
  12. package/dist/ai-fm7xx1BK.js +312 -0
  13. package/dist/ai-fm7xx1BK.js.map +1 -0
  14. package/dist/base64-redaction-C1Uu29t5.js +59 -0
  15. package/dist/base64-redaction-C1Uu29t5.js.map +1 -0
  16. package/dist/browser/ai-sdk.d.ts +90 -0
  17. package/dist/browser/ai-sdk.js +51 -0
  18. package/dist/browser/ai-sdk.js.map +1 -0
  19. package/dist/browser/ai.d.ts +4 -4
  20. package/dist/browser/ai.js +1 -347
  21. package/dist/browser/index.d.ts +81 -79
  22. package/dist/browser/index.js +361 -2
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/browser/tanstack-ai.d.ts +96 -1
  25. package/dist/browser/tanstack-ai.js +62 -2
  26. package/dist/browser/tanstack-ai.js.map +1 -1
  27. package/dist/browser-DE3d-RLk.d.ts +275 -0
  28. package/dist/browser-tool-CQ10e6Kj.js +480 -0
  29. package/dist/browser-tool-CQ10e6Kj.js.map +1 -0
  30. package/dist/browser-tool-DuNdlwZ9.d.ts +73 -0
  31. package/dist/{capability-BjSKYpzg.js → capability-B4WbF81e.js} +2 -1
  32. package/dist/capability-B4WbF81e.js.map +1 -0
  33. package/dist/{capability-runner-Be_-PLR1.d.ts → capability-runner-DBAnGr6L.d.ts} +45 -15
  34. package/dist/chat/index.d.ts +341 -232
  35. package/dist/chat/index.js +602 -72
  36. package/dist/chat/index.js.map +1 -1
  37. package/dist/chat/react.d.ts +42 -2
  38. package/dist/chat/react.js +191 -35
  39. package/dist/chat/react.js.map +1 -1
  40. package/dist/chat/transport.d.ts +1 -1
  41. package/dist/chat/transport.js +1 -1
  42. package/dist/chat-sdk/index.d.ts +17 -3
  43. package/dist/chat-sdk/index.js +28 -2
  44. package/dist/chat-sdk/index.js.map +1 -1
  45. package/dist/client-4OlMIJx-.js +591 -0
  46. package/dist/client-4OlMIJx-.js.map +1 -0
  47. package/dist/{client-jagG8a9_.js → client-fyQrLGkD.js} +2 -2
  48. package/dist/{client-jagG8a9_.js.map → client-fyQrLGkD.js.map} +1 -1
  49. package/dist/client-je3koD9Z.d.ts +446 -0
  50. package/dist/client.d.ts +29 -283
  51. package/dist/client.js +2 -274
  52. package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
  53. package/dist/connector-58XDGvc3.js.map +1 -0
  54. package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
  55. package/dist/context/index.d.ts +29 -0
  56. package/dist/context/index.js +64 -17
  57. package/dist/context/index.js.map +1 -1
  58. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  59. package/dist/current-agent-vAk_5JMt.d.ts +93 -0
  60. package/dist/{current-agent-Da_C9a3b.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +26 -127
  61. package/dist/harnesses/pi/index.d.ts +348 -0
  62. package/dist/harnesses/pi/index.js +754 -0
  63. package/dist/harnesses/pi/index.js.map +1 -0
  64. package/dist/images-iiTDSyRu.js +1631 -0
  65. package/dist/images-iiTDSyRu.js.map +1 -0
  66. package/dist/index-BreL0YrQ.d.ts +78 -0
  67. package/dist/index-kbSUIer3.d.ts +343 -0
  68. package/dist/index.d.ts +43 -41
  69. package/dist/index.js +2 -2
  70. package/dist/internal_context.d.ts +1 -1
  71. package/dist/lifecycle/index.d.ts +19 -17
  72. package/dist/lifecycle/index.js +2 -2
  73. package/dist/{lifecycle-CMRGjZdw.js → lifecycle-CFu6OV03.js} +73 -29
  74. package/dist/lifecycle-CFu6OV03.js.map +1 -0
  75. package/dist/mcp/client/index.d.ts +20 -20
  76. package/dist/mcp/client/index.js +1 -1
  77. package/dist/mcp/client/x402.js +21 -19
  78. package/dist/mcp/client/x402.js.map +1 -1
  79. package/dist/mcp/index.d.ts +35 -35
  80. package/dist/mcp/index.js +2 -2
  81. package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
  82. package/dist/message-builder-DAl9AhHH.js.map +1 -0
  83. package/dist/models/ai-sdk/index.d.ts +353 -0
  84. package/dist/models/ai-sdk/index.js +2443 -0
  85. package/dist/models/ai-sdk/index.js.map +1 -0
  86. package/dist/models/pi-ai/index.d.ts +313 -0
  87. package/dist/models/pi-ai/index.js +1795 -0
  88. package/dist/models/pi-ai/index.js.map +1 -0
  89. package/dist/observability/ai/index.js +20 -4
  90. package/dist/observability/ai/index.js.map +1 -1
  91. package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
  92. package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
  93. package/dist/queue/index.d.ts +123 -0
  94. package/dist/queue/index.js +2 -0
  95. package/dist/queue-BB-zfIYF.js +406 -0
  96. package/dist/queue-BB-zfIYF.js.map +1 -0
  97. package/dist/react.d.ts +31 -13
  98. package/dist/react.js +190 -71
  99. package/dist/react.js.map +1 -1
  100. package/dist/{retries-D9Ds-1lz.d.ts → retries-CV6AkXmQ.d.ts} +23 -8
  101. package/dist/retries.d.ts +12 -8
  102. package/dist/retries.js +19 -1
  103. package/dist/retries.js.map +1 -1
  104. package/dist/routing/index.d.ts +2 -2
  105. package/dist/routing/index.js +1 -1
  106. package/dist/{scheduler-DD9NdYbF.js → scheduler-DZ3xQfeK.js} +4 -11
  107. package/dist/scheduler-DZ3xQfeK.js.map +1 -0
  108. package/dist/{scheduler-Dwh85ZGl.d.ts → scheduler-RdACNXDP.d.ts} +6 -6
  109. package/dist/schedules/index.d.ts +1 -1
  110. package/dist/schedules/index.js +1 -1
  111. package/dist/serializable.d.ts +1 -1
  112. package/dist/sessions/index.d.ts +70 -3
  113. package/dist/sessions/index.js +276 -67
  114. package/dist/sessions/index.js.map +1 -1
  115. package/dist/skills/index.d.ts +29 -314
  116. package/dist/socket-address.d.ts +16 -0
  117. package/dist/socket-address.js +19 -0
  118. package/dist/socket-address.js.map +1 -0
  119. package/dist/{src-DlSHshb2.js → src-BZeu-SCD.js} +541 -500
  120. package/dist/src-BZeu-SCD.js.map +1 -0
  121. package/dist/state/index.d.ts +6 -0
  122. package/dist/state/index.js +137 -0
  123. package/dist/state/index.js.map +1 -0
  124. package/dist/streams/index.d.ts +1 -1
  125. package/dist/streams/index.js +1 -1
  126. package/dist/{streams-D6tJ0NN9.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
  127. package/dist/{streams-DZKgAj9b.js → streams-DW-UCOUc.js} +9 -2
  128. package/dist/streams-DW-UCOUc.js.map +1 -0
  129. package/dist/sub-routing.d.ts +12 -12
  130. package/dist/tasks/index.d.ts +1 -1
  131. package/dist/tasks/index.js +1 -1
  132. package/dist/{tasks-ylZgBjhj.js → tasks-68DESxf0.js} +3 -3
  133. package/dist/{tasks-ylZgBjhj.js.map → tasks-68DESxf0.js.map} +1 -1
  134. package/dist/{tasks-BRJ5zgya.d.ts → tasks-9YxYtByh.d.ts} +5 -5
  135. package/dist/transport-PfFnt-Ld.d.ts +525 -0
  136. package/dist/transport-protocol-BxyngQ_R.js +25 -0
  137. package/dist/transport-protocol-BxyngQ_R.js.map +1 -0
  138. package/dist/transport-protocol-ZTew5Wso.d.ts +27 -0
  139. package/dist/types-BA4anrjI.d.ts +70 -0
  140. package/dist/voice/index.d.ts +2 -2
  141. package/dist/websockets/index.d.ts +195 -69
  142. package/dist/websockets/index.js +3 -2
  143. package/dist/websockets-Ci-LZFHH.js +1030 -0
  144. package/dist/websockets-Ci-LZFHH.js.map +1 -0
  145. package/dist/wire-types-CnMt6_HR.js.map +1 -1
  146. package/dist/wire-types-CrjKvDyi.d.ts +300 -0
  147. package/dist/workflow-types.d.ts +25 -25
  148. package/dist/workflows.d.ts +21 -21
  149. package/dist/workflows.js +1 -1
  150. package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
  151. package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
  152. package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
  153. package/docs/agent-class.md +2 -2
  154. package/docs/agent-tools.md +29 -4
  155. package/docs/browse-the-web.md +87 -5
  156. package/docs/chat-agents.md +124 -16
  157. package/docs/client-sdk.md +30 -0
  158. package/docs/context.md +27 -0
  159. package/docs/durable-execution.md +2 -0
  160. package/docs/harnesses/pi.md +91 -0
  161. package/docs/human-in-the-loop.md +4 -0
  162. package/docs/index.md +4 -1
  163. package/docs/lifecycle.md +114 -23
  164. package/docs/models-pi-ai.md +325 -0
  165. package/docs/models.md +504 -0
  166. package/docs/queue.md +135 -241
  167. package/docs/resumable-streaming.md +2 -0
  168. package/docs/retries.md +1 -1
  169. package/docs/routing.md +13 -13
  170. package/docs/sessions.md +36 -1
  171. package/docs/state.md +1 -0
  172. package/docs/sub-agents.md +12 -3
  173. package/package.json +51 -3
  174. package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
  175. package/dist/browser/ai.js.map +0 -1
  176. package/dist/capability-BjSKYpzg.js.map +0 -1
  177. package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
  178. package/dist/client.js.map +0 -1
  179. package/dist/connector-CptFKzRh.js.map +0 -1
  180. package/dist/lifecycle-CMRGjZdw.js.map +0 -1
  181. package/dist/message-builder-BymO4N_D.js.map +0 -1
  182. package/dist/scheduler-DD9NdYbF.js.map +0 -1
  183. package/dist/src-DlSHshb2.js.map +0 -1
  184. package/dist/streams-DZKgAj9b.js.map +0 -1
  185. package/dist/websockets-DUfRHPRq.js +0 -502
  186. package/dist/websockets-DUfRHPRq.js.map +0 -1
  187. package/dist/ws-chat-transport-rWwta645.js.map +0 -1
@@ -1,6 +1,6 @@
1
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
2
  import { t as _classPrivateMethodInitSpec } from "../classPrivateMethodInitSpec-qMjJ6sHQ.js";
3
+ import { t as LifecycleCapability } from "../capability-B4WbF81e.js";
4
4
  import { SqlError } from "../sql-error.js";
5
5
  import { n as sanitizeMessage, t as byteLength } from "../sanitize-D9TujEK8.js";
6
6
  import { i as estimatedDataUrlBytes, n as estimateMessageTokens, r as estimateStringTokens, t as estimateAttachmentTokens } from "../tokens-nHAKcN6M.js";
@@ -396,22 +396,46 @@ function extractAttachments(message) {
396
396
  };
397
397
  if (message.parts.length === 0) return {
398
398
  message,
399
- attachments
399
+ attachments,
400
+ references: []
400
401
  };
401
402
  const parts = walk(message.parts, 0);
403
+ const references = [...pointersOf(parts)];
402
404
  if (parts === message.parts) return {
403
405
  message,
404
- attachments
406
+ attachments,
407
+ references
405
408
  };
406
409
  return {
407
410
  message: {
408
411
  ...message,
409
412
  parts
410
413
  },
411
- attachments
414
+ attachments,
415
+ references
412
416
  };
413
417
  }
414
418
  /**
419
+ * The attachment addresses a read of this stored form restores. It walks
420
+ * exactly as {@link resolveAttachments} does, so a message holds a reference
421
+ * for every pointer a read would put back and for nothing else.
422
+ */
423
+ function pointersOf(value, into = /* @__PURE__ */ new Set(), depth = 0) {
424
+ if (depth > MAX_WALK_DEPTH || value === null || typeof value !== "object") return into;
425
+ if (Array.isArray(value)) {
426
+ for (const entry of value) pointersOf(entry, into, depth + 1);
427
+ return into;
428
+ }
429
+ const record = value;
430
+ const hash = parseAttachmentUrl(record.url) ?? parseAttachmentUrl(record.data);
431
+ if (hash) {
432
+ into.add(hash);
433
+ return into;
434
+ }
435
+ for (const entry of Object.values(record)) pointersOf(entry, into, depth + 1);
436
+ return into;
437
+ }
438
+ /**
415
439
  * Put payloads back inline, undoing extraction exactly.
416
440
  *
417
441
  * A pointer whose bytes cannot be loaded is left as it is: an unresolvable
@@ -747,6 +771,38 @@ const NEWEST_FIRST_WINDOW_ROWS = 8;
747
771
  * recent rows only, and `getRecentHistory` reports that as truncated.
748
772
  */
749
773
  const MAX_PATH_DEPTH = 1e4;
774
+ /**
775
+ * Hidden overlay rows: a stored `compaction_<id>` row whose `<id>` names one
776
+ * of the session's own compaction records, so it duplicates an overlay the
777
+ * session renders itself. Writes refuse overlays, but sessions written before
778
+ * that guard (#1984) can hold one, usually with later rows parented to it.
779
+ * Reads skip such a row and walk through it, so its children take its place.
780
+ * A `compaction_` row with no matching record, such as a summary imported
781
+ * from another session's history, is the only copy of that context and stays
782
+ * visible.
783
+ */
784
+ function hasOverlayPrefix(id) {
785
+ return id.startsWith(COMPACTION_PREFIX);
786
+ }
787
+ /**
788
+ * SQL twin of the hidden-overlay test for the row id in `idColumn` of the
789
+ * session in `sessionExpr`. The record lookup runs only for prefixed ids.
790
+ * Both must be table-qualified: inside the lookup an unqualified `id` or
791
+ * `session_id` binds to the compactions table, not the row being tested.
792
+ */
793
+ function hiddenOverlayRowSql(idColumn, sessionExpr) {
794
+ return `(substr(${idColumn}, 1, 11) = '${COMPACTION_PREFIX}'
795
+ AND EXISTS (SELECT 1 FROM cf_agents_session_compactions oc
796
+ WHERE oc.session_id = ${sessionExpr}
797
+ AND oc.id = substr(${idColumn}, 12)))`;
798
+ }
799
+ /**
800
+ * Digest of a message's stored form, stamped on the row by every write, so an
801
+ * update can decide "unchanged" without reading the stored content back.
802
+ */
803
+ function contentDigest(json) {
804
+ return createHash("sha256").update(json, "utf8").digest("hex");
805
+ }
750
806
  var _reservedMetadataKeys = /* @__PURE__ */ new WeakMap();
751
807
  var _listeners = /* @__PURE__ */ new WeakMap();
752
808
  var _tails = /* @__PURE__ */ new WeakMap();
@@ -782,8 +838,10 @@ var SessionsCore = class {
782
838
  content_chunks INTEGER NOT NULL DEFAULT 0,
783
839
  token_estimate INTEGER NOT NULL DEFAULT 0,
784
840
  created_at INTEGER NOT NULL,
841
+ content_hash TEXT,
785
842
  PRIMARY KEY (session_id, id)
786
843
  ) WITHOUT ROWID`, []);
844
+ _assertClassBrand(_SessionsCore_brand, this, _addMessageContentHashColumn).call(this);
787
845
  this.io.sqlWrite(`CREATE TABLE IF NOT EXISTS cf_agents_session_message_chunks (
788
846
  session_id TEXT NOT NULL,
789
847
  id TEXT NOT NULL,
@@ -923,12 +981,15 @@ var SessionsCore = class {
923
981
  return _assertClassBrand(_SessionsCore_brand, this, _tail).call(this, sessionId).leafId;
924
982
  }
925
983
  getLatestLeaf(sessionId) {
926
- const leafId = this.latestLeafId(sessionId);
984
+ let leafId = this.latestLeafId(sessionId);
985
+ while (leafId !== null && _assertClassBrand(_SessionsCore_brand, this, _hiddenOverlayIds).call(this, sessionId, [leafId]).has(leafId)) {
986
+ const [row] = this.io.sql("SELECT parent_id FROM cf_agents_session_messages WHERE session_id = ? AND id = ?", [sessionId, leafId]);
987
+ leafId = row?.parent_id ?? null;
988
+ }
927
989
  return leafId ? this.getMessage(sessionId, leafId) : null;
928
990
  }
929
991
  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]);
992
+ const rows = _assertClassBrand(_SessionsCore_brand, this, _childRows).call(this, sessionId, messageId);
932
993
  const continued = _assertClassBrand(_SessionsCore_brand, this, _continuations).call(this, sessionId, rows.filter((row) => row.content_chunks > 0).map((row) => row.id));
933
994
  const result = [];
934
995
  for (const row of rows) {
@@ -946,38 +1007,7 @@ var SessionsCore = class {
946
1007
  * budget over these rows bounds real hydrated memory.
947
1008
  */
948
1009
  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
- ]);
1010
+ return _assertClassBrand(_SessionsCore_brand, this, _pathRows).call(this, sessionId, leafId).rows;
981
1011
  }
982
1012
  /**
983
1013
  * Stream the path ending at `leafId` (default: active leaf), root → leaf
@@ -1010,7 +1040,7 @@ var SessionsCore = class {
1010
1040
  * skipped, showing the raw recent messages.
1011
1041
  */
1012
1042
  async getRecentHistory(sessionId, maxContentBytes, leafId) {
1013
- const stats = this.pathRowStats(sessionId, leafId);
1043
+ const { rows: stats, walked, oldestId } = _assertClassBrand(_SessionsCore_brand, this, _pathRows).call(this, sessionId, leafId);
1014
1044
  if (stats.length === 0) return {
1015
1045
  messages: [],
1016
1046
  truncated: false,
@@ -1027,7 +1057,7 @@ var SessionsCore = class {
1027
1057
  }
1028
1058
  const messages = [];
1029
1059
  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);
1060
+ const capped = walked > MAX_PATH_DEPTH && oldestId !== null && _assertClassBrand(_SessionsCore_brand, this, _hasParent).call(this, sessionId, oldestId);
1031
1061
  return {
1032
1062
  messages,
1033
1063
  truncated: start > 0 || capped,
@@ -1044,7 +1074,7 @@ var SessionsCore = class {
1044
1074
  const leafId = this.latestLeafId(sessionId);
1045
1075
  const memo = _classPrivateFieldGet2(_pathTokens, this).get(sessionId);
1046
1076
  if (memo && memo.leafId === leafId) return Math.max(0, Math.ceil(memo.total));
1047
- const stats = this.pathRowStats(sessionId);
1077
+ const { rows: stats, walked } = _assertClassBrand(_SessionsCore_brand, this, _pathRows).call(this, sessionId);
1048
1078
  const counted = new Set(stats.map((row) => row.id));
1049
1079
  let tokens = stats.reduce((sum, row) => sum + row.tokenEstimate, 0);
1050
1080
  for (const span of planOverlays(stats.map((row) => row.id), this.getCompactions(sessionId))) {
@@ -1058,7 +1088,7 @@ var SessionsCore = class {
1058
1088
  leafId,
1059
1089
  counted,
1060
1090
  total: tokens,
1061
- depth: stats.length
1091
+ depth: walked
1062
1092
  });
1063
1093
  return Math.max(0, Math.ceil(tokens));
1064
1094
  }
@@ -1109,14 +1139,15 @@ var SessionsCore = class {
1109
1139
  let parent;
1110
1140
  if (parentId === void 0) parent = tail.leafId;
1111
1141
  else parent = parentId && this.exists(sessionId, parentId) ? parentId : null;
1112
- const { message: staged, attachments } = extractAttachments(message);
1113
- const slices = splitContent(JSON.stringify(staged));
1142
+ const { message: staged, attachments, references } = extractAttachments(message);
1143
+ const json = JSON.stringify(staged);
1144
+ const slices = splitContent(json);
1114
1145
  const seq = tail.nextSeq;
1115
1146
  this.io.transaction(() => {
1116
1147
  for (const attachment of attachments) _classPrivateFieldGet2(_attachments, this).put(attachment.payload, attachment.hash);
1117
1148
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1149
+ (session_id, id, seq, parent_id, role, content, content_chunks, token_estimate, created_at, content_hash)
1150
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1120
1151
  sessionId,
1121
1152
  message.id,
1122
1153
  seq,
@@ -1125,10 +1156,11 @@ var SessionsCore = class {
1125
1156
  slices[0],
1126
1157
  slices.length - 1,
1127
1158
  tokenEstimate,
1128
- Date.now()
1159
+ Date.now(),
1160
+ contentDigest(json)
1129
1161
  ]);
1130
1162
  _assertClassBrand(_SessionsCore_brand, this, _writeContinuations).call(this, sessionId, message.id, slices);
1131
- _classPrivateFieldGet2(_attachments, this).addRefs(sessionId, message.id, attachments.map((attachment) => attachment.hash));
1163
+ _classPrivateFieldGet2(_attachments, this).addRefs(sessionId, message.id, references);
1132
1164
  _assertClassBrand(_SessionsCore_brand, this, _indexFts).call(this, sessionId, staged, false);
1133
1165
  });
1134
1166
  _classPrivateFieldGet2(_tails, this).set(sessionId, {
@@ -1155,26 +1187,38 @@ var SessionsCore = class {
1155
1187
  /**
1156
1188
  * Durable update of an existing row. An identical row writes nothing: no
1157
1189
  * 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.
1190
+ * FULL content, through the digest the last write stamped, so neither the
1191
+ * payload nor a continuation row is read. A row without a digest compares
1192
+ * the reassembled content once and is stamped.
1159
1193
  */
1160
1194
  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]);
1195
+ const oldRows = this.io.sql("SELECT content_chunks, token_estimate, content_hash FROM cf_agents_session_messages WHERE session_id = ? AND id = ?", [sessionId, message.id]);
1162
1196
  if (oldRows.length === 0) return "missing";
1163
1197
  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);
1198
+ const { message: staged, attachments, references } = extractAttachments(message);
1166
1199
  const json = JSON.stringify(staged);
1167
- if (oldContent === json) return "unchanged";
1200
+ const digest = contentDigest(json);
1201
+ if (old.content_hash !== null) {
1202
+ if (old.content_hash === digest) return "unchanged";
1203
+ } else if (_assertClassBrand(_SessionsCore_brand, this, _content).call(this, sessionId, message.id) === json) {
1204
+ this.io.sqlWrite("UPDATE cf_agents_session_messages SET content_hash = ? WHERE session_id = ? AND id = ?", [
1205
+ digest,
1206
+ sessionId,
1207
+ message.id
1208
+ ]);
1209
+ return "unchanged";
1210
+ }
1168
1211
  const slices = splitContent(json);
1169
1212
  this.io.transaction(() => {
1170
1213
  for (const attachment of attachments) _classPrivateFieldGet2(_attachments, this).put(attachment.payload, attachment.hash);
1171
1214
  this.io.sqlWrite(`UPDATE cf_agents_session_messages
1172
- SET role = ?, content = ?, content_chunks = ?, token_estimate = ?
1215
+ SET role = ?, content = ?, content_chunks = ?, token_estimate = ?, content_hash = ?
1173
1216
  WHERE session_id = ? AND id = ?`, [
1174
1217
  message.role,
1175
1218
  slices[0],
1176
1219
  slices.length - 1,
1177
1220
  tokenEstimate,
1221
+ digest,
1178
1222
  sessionId,
1179
1223
  message.id
1180
1224
  ]);
@@ -1185,7 +1229,7 @@ var SessionsCore = class {
1185
1229
  slices.length - 1
1186
1230
  ]);
1187
1231
  _assertClassBrand(_SessionsCore_brand, this, _writeContinuations).call(this, sessionId, message.id, slices);
1188
- _classPrivateFieldGet2(_attachments, this).replaceRefs(sessionId, message.id, attachments.map((attachment) => attachment.hash));
1232
+ _classPrivateFieldGet2(_attachments, this).replaceRefs(sessionId, message.id, references);
1189
1233
  _assertClassBrand(_SessionsCore_brand, this, _indexFts).call(this, sessionId, staged, true);
1190
1234
  });
1191
1235
  const memo = _classPrivateFieldGet2(_pathTokens, this).get(sessionId);
@@ -1316,6 +1360,7 @@ var SessionsCore = class {
1316
1360
  INNER JOIN cf_agents_session_messages m
1317
1361
  ON m.session_id = f.session_id AND m.id = f.id
1318
1362
  WHERE cf_agents_session_fts MATCH ? AND f.session_id = ?
1363
+ AND NOT ${hiddenOverlayRowSql("f.id", "f.session_id")}
1319
1364
  ORDER BY rank LIMIT ?`, [
1320
1365
  sanitized,
1321
1366
  sessionId,
@@ -1332,15 +1377,16 @@ var SessionsCore = class {
1332
1377
  */
1333
1378
  /** Returns `false` when the id already exists and nothing was written. */
1334
1379
  importMessage(sessionId, message, options) {
1335
- const { message: staged, attachments } = extractAttachments(message);
1336
- const slices = splitContent(JSON.stringify(staged));
1380
+ const { message: staged, attachments, references } = extractAttachments(message);
1381
+ const json = JSON.stringify(staged);
1382
+ const slices = splitContent(json);
1337
1383
  const tail = _assertClassBrand(_SessionsCore_brand, this, _tail).call(this, sessionId);
1338
1384
  let inserted = 0;
1339
1385
  this.io.transaction(() => {
1340
1386
  for (const attachment of attachments) _classPrivateFieldGet2(_attachments, this).put(attachment.payload, attachment.hash);
1341
1387
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1388
+ (session_id, id, seq, parent_id, role, content, content_chunks, token_estimate, created_at, content_hash)
1389
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1344
1390
  sessionId,
1345
1391
  message.id,
1346
1392
  tail.nextSeq,
@@ -1349,11 +1395,12 @@ var SessionsCore = class {
1349
1395
  slices[0],
1350
1396
  slices.length - 1,
1351
1397
  this.estimateRowTokens(message),
1352
- options.createdAt
1398
+ options.createdAt,
1399
+ contentDigest(json)
1353
1400
  ]);
1354
1401
  if (inserted === 0) return;
1355
1402
  _assertClassBrand(_SessionsCore_brand, this, _writeContinuations).call(this, sessionId, message.id, slices);
1356
- _classPrivateFieldGet2(_attachments, this).addRefs(sessionId, message.id, attachments.map((attachment) => attachment.hash));
1403
+ _classPrivateFieldGet2(_attachments, this).addRefs(sessionId, message.id, references);
1357
1404
  _assertClassBrand(_SessionsCore_brand, this, _indexFts).call(this, sessionId, staged, false);
1358
1405
  });
1359
1406
  if (inserted === 0) return false;
@@ -1365,6 +1412,15 @@ var SessionsCore = class {
1365
1412
  return true;
1366
1413
  }
1367
1414
  };
1415
+ /**
1416
+ * Add the digest column to a message table that predates it. Nullable, so
1417
+ * no existing row is rewritten; a `null` digest falls back to reading the
1418
+ * row back once.
1419
+ */
1420
+ function _addMessageContentHashColumn() {
1421
+ if (this.io.sql("SELECT name FROM pragma_table_info('cf_agents_session_messages')", []).map((row) => row.name).includes("content_hash")) return;
1422
+ this.io.sqlWrite("ALTER TABLE cf_agents_session_messages ADD COLUMN content_hash TEXT", []);
1423
+ }
1368
1424
  function _tableExists(name) {
1369
1425
  return this.io.sql("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?", [name]).length > 0;
1370
1426
  }
@@ -1448,6 +1504,74 @@ function _resolveLeafId(sessionId, leafId) {
1448
1504
  return this.latestLeafId(sessionId);
1449
1505
  }
1450
1506
  /**
1507
+ * Which of `ids` are hidden overlay rows. Reads the session's compaction
1508
+ * records only when some id carries the prefix.
1509
+ */
1510
+ function _hiddenOverlayIds(sessionId, ids) {
1511
+ if (!ids.some(hasOverlayPrefix)) return /* @__PURE__ */ new Set();
1512
+ const overlays = new Set(this.getCompactions(sessionId).map((compaction) => `${COMPACTION_PREFIX}${compaction.id}`));
1513
+ return new Set(ids.filter((id) => overlays.has(id)));
1514
+ }
1515
+ /** Children of a row in `seq` order, hidden overlay rows replaced by theirs. */
1516
+ function _childRows(sessionId, parentId) {
1517
+ const rows = this.io.sql(`SELECT id, seq, content, content_chunks FROM cf_agents_session_messages
1518
+ WHERE session_id = ? AND parent_id = ? ORDER BY seq ASC`, [sessionId, parentId]);
1519
+ const hidden = _assertClassBrand(_SessionsCore_brand, this, _hiddenOverlayIds).call(this, sessionId, rows.map((row) => row.id));
1520
+ if (hidden.size === 0) return rows;
1521
+ return rows.flatMap((row) => hidden.has(row.id) ? _assertClassBrand(_SessionsCore_brand, this, _childRows).call(this, sessionId, row.id) : [row]).sort((a, b) => a.seq - b.seq);
1522
+ }
1523
+ /**
1524
+ * `pathRowStats` plus what the walk itself saw: the path cap counts stored
1525
+ * rows, hidden overlay rows included, so whether the walk was cut short
1526
+ * is judged from the unfiltered path.
1527
+ */
1528
+ function _pathRows(sessionId, leafId) {
1529
+ const leaf = _assertClassBrand(_SessionsCore_brand, this, _resolveLeafId).call(this, sessionId, leafId);
1530
+ if (!leaf) return {
1531
+ rows: [],
1532
+ walked: 0,
1533
+ oldestId: null
1534
+ };
1535
+ const walked = this.io.sql(`WITH RECURSIVE path(id, parent_id, depth) AS (
1536
+ SELECT id, parent_id, 0 FROM cf_agents_session_messages
1537
+ WHERE session_id = ? AND id = ?
1538
+ UNION ALL
1539
+ SELECT m.id, m.parent_id, p.depth + 1 FROM cf_agents_session_messages m
1540
+ JOIN path p ON m.id = p.parent_id
1541
+ WHERE m.session_id = ? AND p.depth < ${MAX_PATH_DEPTH}
1542
+ )
1543
+ SELECT path.id AS id, am.role AS role,
1544
+ LENGTH(CAST(am.content AS BLOB)) + CASE WHEN am.content_chunks = 0 THEN 0
1545
+ ELSE COALESCE((
1546
+ SELECT SUM(LENGTH(CAST(c.content AS BLOB)))
1547
+ FROM cf_agents_session_message_chunks c
1548
+ WHERE c.session_id = am.session_id AND c.id = am.id
1549
+ ), 0) END
1550
+ + COALESCE((
1551
+ SELECT SUM((meta.bytes + 2) / 3 * 4)
1552
+ FROM cf_agents_session_attachment_refs r
1553
+ JOIN cf_agents_session_attachment_meta meta ON meta.hash = r.hash
1554
+ WHERE r.session_id = am.session_id AND r.message_id = am.id
1555
+ ), 0) AS bytes,
1556
+ am.token_estimate AS tokenEstimate,
1557
+ ${hiddenOverlayRowSql("path.id", "am.session_id")} AS hidden
1558
+ FROM path JOIN cf_agents_session_messages am
1559
+ ON am.session_id = ? AND am.id = path.id
1560
+ ORDER BY path.depth DESC`, [
1561
+ sessionId,
1562
+ leaf,
1563
+ sessionId,
1564
+ sessionId
1565
+ ]);
1566
+ const rows = [];
1567
+ for (const { hidden, ...row } of walked) if (!hidden) rows.push(row);
1568
+ return {
1569
+ rows,
1570
+ walked: walked.length,
1571
+ oldestId: walked[0]?.id ?? null
1572
+ };
1573
+ }
1574
+ /**
1451
1575
  * The active branch path as ids alone, root → leaf. The cheapest walk the
1452
1576
  * tree allows — one row per step, following `parent_id` by primary key —
1453
1577
  * for readers that will hydrate only a few rows and do not need the sizes
@@ -1464,9 +1588,12 @@ function _pathIds(sessionId, leafId) {
1464
1588
  JOIN path p ON m.id = p.parent_id
1465
1589
  WHERE m.session_id = ? AND p.depth < ${MAX_PATH_DEPTH}
1466
1590
  )
1467
- SELECT id FROM path ORDER BY depth DESC`, [
1591
+ SELECT path.id AS id FROM path
1592
+ WHERE NOT ${hiddenOverlayRowSql("path.id", "?")}
1593
+ ORDER BY depth DESC`, [
1468
1594
  sessionId,
1469
1595
  leaf,
1596
+ sessionId,
1470
1597
  sessionId
1471
1598
  ]).map((row) => row.id);
1472
1599
  }
@@ -1569,6 +1696,7 @@ async function* _streamStats(sessionId, stats, signal, newestFirst = false, plan
1569
1696
  async function* _walkFromLeaf(sessionId, leafId, signal) {
1570
1697
  const compactions = this.getCompactions(sessionId);
1571
1698
  const spanEnds = new Set(compactions.map((c) => c.toMessageId));
1699
+ const hiddenIds = new Set(compactions.map((c) => `${COMPACTION_PREFIX}${c.id}`));
1572
1700
  let next = _assertClassBrand(_SessionsCore_brand, this, _resolveLeafId).call(this, sessionId, leafId);
1573
1701
  let depth = 0;
1574
1702
  while (next !== null && depth <= MAX_PATH_DEPTH) {
@@ -1580,9 +1708,11 @@ async function* _walkFromLeaf(sessionId, leafId, signal) {
1580
1708
  const [row] = this.io.sql(`SELECT parent_id, content, content_chunks FROM cf_agents_session_messages
1581
1709
  WHERE session_id = ? AND id = ?`, [sessionId, next]);
1582
1710
  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);
1711
+ if (!hiddenIds.has(next)) {
1712
+ const json = row.content_chunks === 0 ? row.content : row.content + (_assertClassBrand(_SessionsCore_brand, this, _continuations).call(this, sessionId, [next]).get(next) ?? "");
1713
+ const parsed = _assertClassBrand(_SessionsCore_brand, this, _parse).call(this, json);
1714
+ if (parsed) yield _assertClassBrand(_SessionsCore_brand, this, _inline).call(this, parsed);
1715
+ }
1586
1716
  next = row.parent_id;
1587
1717
  depth++;
1588
1718
  }
@@ -1649,6 +1779,50 @@ function _parse(json) {
1649
1779
  return null;
1650
1780
  }
1651
1781
  //#endregion
1782
+ //#region src/sessions/mirror.ts
1783
+ /**
1784
+ * The default reduction, per event:
1785
+ *
1786
+ * - `append` of a new row: replace the cached entry with its id, or push it.
1787
+ * A duplicate append (`inserted: false`) changed nothing.
1788
+ * - `update`: replace the cached entry with its id; a row the cache does not
1789
+ * hold stays out.
1790
+ * - `delete`: drop the removed ids. `clear`: empty the cache.
1791
+ * - `import`, `compact`, `compaction`: nothing, since no in-place patch
1792
+ * expresses them. A host that must react intercepts them.
1793
+ */
1794
+ function mirrorSessionChanges(subscribe, sessionId, options) {
1795
+ const transform = options.transform ?? ((message) => message);
1796
+ return subscribe(async (event) => {
1797
+ if (event.sessionId !== sessionId) return;
1798
+ if (await options.intercept?.(event)) return;
1799
+ switch (event.type) {
1800
+ case "append":
1801
+ case "update": {
1802
+ if (event.type === "append" && !event.inserted) return;
1803
+ const cache = options.get();
1804
+ const index = cache.findIndex((m) => m.id === event.message.id);
1805
+ if (index === -1 && event.type === "update") return;
1806
+ const message = transform(event.message);
1807
+ const previous = index === -1 ? void 0 : cache[index];
1808
+ if (index === -1) cache.push(message);
1809
+ else cache[index] = message;
1810
+ options.onApplied?.(event, message, previous);
1811
+ return;
1812
+ }
1813
+ case "delete": {
1814
+ const removed = new Set(event.messageIds);
1815
+ options.set(options.get().filter((m) => !removed.has(m.id)));
1816
+ return;
1817
+ }
1818
+ case "clear":
1819
+ options.set([]);
1820
+ return;
1821
+ default: return;
1822
+ }
1823
+ });
1824
+ }
1825
+ //#endregion
1652
1826
  //#region src/sessions/handle.ts
1653
1827
  var _core$1 = /* @__PURE__ */ new WeakMap();
1654
1828
  var _ready = /* @__PURE__ */ new WeakMap();
@@ -1682,6 +1856,13 @@ var Session = class {
1682
1856
  return this;
1683
1857
  }
1684
1858
  /**
1859
+ * Keep an in-memory transcript in step with this session's change feed,
1860
+ * so write sites never hand-patch it. Returns the unsubscribe function.
1861
+ */
1862
+ mirror(options) {
1863
+ return mirrorSessionChanges((listener) => _classPrivateFieldGet2(_core$1, this).subscribe(listener), this.sessionId, options);
1864
+ }
1865
+ /**
1685
1866
  * Stream the active branch path root → leaf (leaf → root with
1686
1867
  * `newestFirst`) with compaction overlays applied. Peak memory is one
1687
1868
  * bounded content window, never the whole transcript, and a consumer that
@@ -1768,9 +1949,18 @@ var Session = class {
1768
1949
  /**
1769
1950
  * Append one message. Idempotent on id: a repeated append returns the row
1770
1951
  * already stored and dispatches an `append` event with `inserted: false`.
1952
+ *
1953
+ * A compaction overlay (a `compaction_` id) is derived on read and is never
1954
+ * stored. Clients post their transcript back after every compaction, so
1955
+ * this and every other write drops one silently: nothing is written and no
1956
+ * event is dispatched.
1771
1957
  */
1772
1958
  async appendMessage(message, options = {}) {
1773
1959
  await _classPrivateFieldGet2(_ready, this).call(this);
1960
+ if (isCompactionMessage(message)) return {
1961
+ inserted: false,
1962
+ message
1963
+ };
1774
1964
  const prepared = _assertClassBrand(_Session_brand, this, _prepare).call(this, message, options.source);
1775
1965
  const result = _classPrivateFieldGet2(_core$1, this).append(this.sessionId, prepared.message, options.parentId, prepared.tokenEstimate);
1776
1966
  await _classPrivateFieldGet2(_core$1, this).notify({
@@ -1786,10 +1976,12 @@ var Session = class {
1786
1976
  /**
1787
1977
  * Update one stored row. Returns the stored form, or `null` when the id is
1788
1978
  * not in this session. An unchanged message writes nothing and dispatches
1789
- * no event.
1979
+ * no event. A compaction overlay is never a stored row, so it reports
1980
+ * `null`.
1790
1981
  */
1791
1982
  async updateMessage(message, options = {}) {
1792
1983
  await _classPrivateFieldGet2(_ready, this).call(this);
1984
+ if (isCompactionMessage(message)) return null;
1793
1985
  const prepared = _assertClassBrand(_Session_brand, this, _prepare).call(this, message, options.source);
1794
1986
  const outcome = _classPrivateFieldGet2(_core$1, this).update(this.sessionId, prepared.message, prepared.tokenEstimate);
1795
1987
  if (outcome === "missing") return null;
@@ -1817,6 +2009,13 @@ var Session = class {
1817
2009
  return {
1818
2010
  abandon: () => _classPrivateFieldGet2(_core$1, this).forgetCaches(this.sessionId),
1819
2011
  upsert: (message, options = {}) => {
2012
+ if (isCompactionMessage(message)) return {
2013
+ result: {
2014
+ inserted: false,
2015
+ message
2016
+ },
2017
+ after: () => Promise.resolve()
2018
+ };
1820
2019
  const prepared = _assertClassBrand(_Session_brand, this, _prepare).call(this, message, options.source);
1821
2020
  if (!_classPrivateFieldGet2(_core$1, this).exists(this.sessionId, message.id)) {
1822
2021
  const result = _classPrivateFieldGet2(_core$1, this).append(this.sessionId, prepared.message, options.parentId, prepared.tokenEstimate);
@@ -1851,6 +2050,10 @@ var Session = class {
1851
2050
  }
1852
2051
  async upsertMessage(message, options = {}) {
1853
2052
  await _classPrivateFieldGet2(_ready, this).call(this);
2053
+ if (isCompactionMessage(message)) return {
2054
+ inserted: false,
2055
+ message
2056
+ };
1854
2057
  if (!_classPrivateFieldGet2(_core$1, this).exists(this.sessionId, message.id)) return this.appendMessage(message, options);
1855
2058
  return {
1856
2059
  inserted: false,
@@ -1863,6 +2066,12 @@ var Session = class {
1863
2066
  * `import` change event so a host cache can mark itself stale; it is not
1864
2067
  * an `append`, so a cache does not patch itself per imported row, and an
1865
2068
  * id that already exists writes nothing and dispatches nothing.
2069
+ *
2070
+ * Unlike the other writes, a `compaction_` id is imported as given: a
2071
+ * summary copied from another session's `history()` is the only record of
2072
+ * the context it replaced, and later rows may be parented to it. History
2073
+ * reads hide such a row only when it duplicates one of this session's own
2074
+ * compaction overlays.
1866
2075
  */
1867
2076
  async importMessage(message, options) {
1868
2077
  await _classPrivateFieldGet2(_ready, this).call(this);