@ably/ai-transport 0.0.1 → 0.2.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 (167) hide show
  1. package/README.md +114 -116
  2. package/dist/ably-ai-transport.js +1743 -961
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +117 -39
  7. package/dist/core/agent.d.ts +29 -0
  8. package/dist/core/codec/decoder.d.ts +20 -23
  9. package/dist/core/codec/encoder.d.ts +11 -8
  10. package/dist/core/codec/index.d.ts +1 -2
  11. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  12. package/dist/core/codec/types.d.ts +410 -101
  13. package/dist/core/transport/agent-session.d.ts +10 -0
  14. package/dist/core/transport/branch-chain.d.ts +43 -0
  15. package/dist/core/transport/client-session.d.ts +13 -0
  16. package/dist/core/transport/decode-fold.d.ts +47 -0
  17. package/dist/core/transport/headers.d.ts +97 -17
  18. package/dist/core/transport/index.d.ts +5 -3
  19. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  20. package/dist/core/transport/invocation.d.ts +74 -0
  21. package/dist/core/transport/load-conversation.d.ts +128 -0
  22. package/dist/core/transport/load-history.d.ts +39 -0
  23. package/dist/core/transport/pipe-stream.d.ts +9 -8
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +435 -0
  26. package/dist/core/transport/types/agent.d.ts +353 -0
  27. package/dist/core/transport/types/client.d.ts +168 -0
  28. package/dist/core/transport/types/shared.d.ts +24 -0
  29. package/dist/core/transport/types/tree.d.ts +315 -0
  30. package/dist/core/transport/types/view.d.ts +222 -0
  31. package/dist/core/transport/types.d.ts +13 -402
  32. package/dist/core/transport/view.d.ts +354 -0
  33. package/dist/errors.d.ts +37 -9
  34. package/dist/index.d.ts +6 -6
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +1164 -645
  37. package/dist/react/ably-ai-transport-react.js.map +1 -1
  38. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  39. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  40. package/dist/react/contexts/client-session-context.d.ts +36 -0
  41. package/dist/react/contexts/client-session-provider.d.ts +53 -0
  42. package/dist/react/create-session-hooks.d.ts +116 -0
  43. package/dist/react/index.d.ts +16 -10
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +20 -11
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +23 -0
  48. package/dist/react/use-tree.d.ts +35 -0
  49. package/dist/react/use-view.d.ts +110 -0
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
  52. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  53. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  55. package/dist/vercel/codec/decoder.d.ts +5 -18
  56. package/dist/vercel/codec/encoder.d.ts +6 -36
  57. package/dist/vercel/codec/events.d.ts +51 -0
  58. package/dist/vercel/codec/index.d.ts +24 -12
  59. package/dist/vercel/codec/reducer.d.ts +144 -0
  60. package/dist/vercel/codec/tool-transitions.d.ts +50 -0
  61. package/dist/vercel/index.d.ts +4 -2
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
  63. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  64. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +70 -1
  65. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  66. package/dist/vercel/react/contexts/chat-transport-context.d.ts +33 -0
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
  68. package/dist/vercel/react/index.d.ts +4 -0
  69. package/dist/vercel/react/use-chat-transport.d.ts +66 -21
  70. package/dist/vercel/react/use-message-sync.d.ts +31 -12
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +71 -30
  73. package/dist/vercel/transport/index.d.ts +25 -18
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +47 -34
  77. package/src/constants.ts +126 -47
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +115 -58
  81. package/src/core/codec/index.ts +13 -6
  82. package/src/core/codec/lifecycle-tracker.ts +10 -9
  83. package/src/core/codec/types.ts +438 -106
  84. package/src/core/transport/agent-session.ts +1344 -0
  85. package/src/core/transport/branch-chain.ts +58 -0
  86. package/src/core/transport/client-session.ts +775 -0
  87. package/src/core/transport/decode-fold.ts +91 -0
  88. package/src/core/transport/headers.ts +182 -19
  89. package/src/core/transport/index.ts +29 -22
  90. package/src/core/transport/internal/bounded-map.ts +27 -0
  91. package/src/core/transport/invocation.ts +98 -0
  92. package/src/core/transport/load-conversation.ts +355 -0
  93. package/src/core/transport/load-history.ts +269 -0
  94. package/src/core/transport/pipe-stream.ts +58 -40
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +1167 -0
  97. package/src/core/transport/types/agent.ts +407 -0
  98. package/src/core/transport/types/client.ts +211 -0
  99. package/src/core/transport/types/shared.ts +27 -0
  100. package/src/core/transport/types/tree.ts +344 -0
  101. package/src/core/transport/types/view.ts +259 -0
  102. package/src/core/transport/types.ts +13 -527
  103. package/src/core/transport/view.ts +1271 -0
  104. package/src/errors.ts +42 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +55 -39
  107. package/src/logger.ts +14 -1
  108. package/src/react/contexts/client-session-context.ts +41 -0
  109. package/src/react/contexts/client-session-provider.tsx +186 -0
  110. package/src/react/create-session-hooks.ts +141 -0
  111. package/src/react/index.ts +27 -10
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +47 -19
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +72 -0
  116. package/src/react/use-tree.ts +84 -0
  117. package/src/react/use-view.ts +275 -0
  118. package/src/react/vite.config.ts +4 -1
  119. package/src/utils.ts +63 -45
  120. package/src/vercel/codec/decoder.ts +336 -255
  121. package/src/vercel/codec/encoder.ts +348 -196
  122. package/src/vercel/codec/events.ts +87 -0
  123. package/src/vercel/codec/index.ts +59 -14
  124. package/src/vercel/codec/reducer.ts +977 -0
  125. package/src/vercel/codec/tool-transitions.ts +122 -0
  126. package/src/vercel/index.ts +7 -3
  127. package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
  128. package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
  129. package/src/vercel/react/index.ts +13 -1
  130. package/src/vercel/react/use-chat-transport.ts +162 -42
  131. package/src/vercel/react/use-message-sync.ts +121 -22
  132. package/src/vercel/react/vite.config.ts +4 -2
  133. package/src/vercel/run-end-reason.ts +78 -0
  134. package/src/vercel/transport/chat-transport.ts +553 -113
  135. package/src/vercel/transport/index.ts +40 -28
  136. package/src/vercel/transport/run-output-stream.ts +170 -0
  137. package/src/version.ts +2 -0
  138. package/dist/core/transport/client-transport.d.ts +0 -10
  139. package/dist/core/transport/conversation-tree.d.ts +0 -9
  140. package/dist/core/transport/decode-history.d.ts +0 -41
  141. package/dist/core/transport/server-transport.d.ts +0 -7
  142. package/dist/core/transport/stream-router.d.ts +0 -19
  143. package/dist/core/transport/turn-manager.d.ts +0 -34
  144. package/dist/react/use-active-turns.d.ts +0 -8
  145. package/dist/react/use-client-transport.d.ts +0 -7
  146. package/dist/react/use-conversation-tree.d.ts +0 -20
  147. package/dist/react/use-edit.d.ts +0 -7
  148. package/dist/react/use-history.d.ts +0 -19
  149. package/dist/react/use-messages.d.ts +0 -7
  150. package/dist/react/use-regenerate.d.ts +0 -7
  151. package/dist/react/use-send.d.ts +0 -7
  152. package/dist/vercel/codec/accumulator.d.ts +0 -21
  153. package/src/core/transport/client-transport.ts +0 -959
  154. package/src/core/transport/conversation-tree.ts +0 -434
  155. package/src/core/transport/decode-history.ts +0 -337
  156. package/src/core/transport/server-transport.ts +0 -458
  157. package/src/core/transport/stream-router.ts +0 -118
  158. package/src/core/transport/turn-manager.ts +0 -147
  159. package/src/react/use-active-turns.ts +0 -61
  160. package/src/react/use-client-transport.ts +0 -37
  161. package/src/react/use-conversation-tree.ts +0 -71
  162. package/src/react/use-edit.ts +0 -24
  163. package/src/react/use-history.ts +0 -111
  164. package/src/react/use-messages.ts +0 -32
  165. package/src/react/use-regenerate.ts +0 -24
  166. package/src/react/use-send.ts +0 -25
  167. package/src/vercel/codec/accumulator.ts +0 -603
@@ -1,434 +0,0 @@
1
- /**
2
- * ConversationTree — materializes a branching conversation from a flat
3
- * oplog of Ably messages using serial-first ordering.
4
- *
5
- * Serial order (the total order assigned by Ably) is the primary mechanism
6
- * for linear message sequences. `x-ably-parent` and `x-ably-fork-of` headers
7
- * are only structurally meaningful at branch points — where the user is
8
- * interacting with a visible message and the client always has it loaded.
9
- *
10
- * `upsert()` is the sole mutation method. Messages can arrive in any order
11
- * (live subscription, history pages, seed data) and the tree produces the
12
- * correct `flatten()` output once all messages are present.
13
- *
14
- * The tree owns conversation state. `flatten()` returns the linear message
15
- * list for the currently selected branches — this is what the transport's
16
- * `getMessages()` delegates to.
17
- */
18
-
19
- import { HEADER_FORK_OF, HEADER_PARENT } from '../../constants.js';
20
- import type { Logger } from '../../logger.js';
21
- import type { ConversationNode, ConversationTree } from './types.js';
22
-
23
- // ---------------------------------------------------------------------------
24
- // Internal node type
25
- // ---------------------------------------------------------------------------
26
-
27
- interface InternalNode<TMessage> {
28
- node: ConversationNode<TMessage>;
29
- /** Insertion sequence — tiebreaker for null-serial messages. */
30
- insertSeq: number;
31
- }
32
-
33
- // ---------------------------------------------------------------------------
34
- // Implementation
35
- // ---------------------------------------------------------------------------
36
-
37
- // Spec: AIT-CT13
38
- class DefaultConversationTree<TMessage> implements ConversationTree<TMessage> {
39
- /** All nodes indexed by msgId (x-ably-msg-id). */
40
- private readonly _nodeIndex = new Map<string, InternalNode<TMessage>>();
41
-
42
- /** Secondary index: codec message key to msgId. Bridges UIMessage.id to x-ably-msg-id. */
43
- private readonly _codecKeyIndex = new Map<string, string>();
44
-
45
- /**
46
- * All nodes sorted by serial (lexicographic). Null-serial messages
47
- * (optimistic inserts, seed data) sort after all serial-bearing messages,
48
- * ordered among themselves by insertion sequence.
49
- */
50
- private readonly _sortedList: InternalNode<TMessage>[] = [];
51
-
52
- /**
53
- * Parent index: parentId to set of child msgIds.
54
- * Nodes with no parent are indexed under the key `null`.
55
- */
56
- private readonly _parentIndex = new Map<string | undefined, Set<string>>();
57
-
58
- /**
59
- * Selected sibling index at each fork point, keyed by the msgId of
60
- * the first sibling in the group (the fork target). Default: last.
61
- */
62
- private readonly _selections = new Map<string, number>();
63
-
64
- private readonly _getKey: (message: TMessage) => string;
65
- private readonly _logger: Logger;
66
-
67
- /** Monotonically increasing counter for insertion sequence. */
68
- private _seqCounter = 0;
69
-
70
- constructor(getKey: (message: TMessage) => string, logger: Logger) {
71
- this._getKey = getKey;
72
- this._logger = logger;
73
- }
74
-
75
- // -------------------------------------------------------------------------
76
- // Sorted list maintenance
77
- // -------------------------------------------------------------------------
78
-
79
- /**
80
- * Compare two nodes for sorted list ordering.
81
- * Serial-bearing nodes sort by serial (lexicographic).
82
- * Null-serial nodes sort after all serial-bearing nodes.
83
- * Among null-serial nodes, sort by insertion sequence.
84
- * @param a - First node to compare.
85
- * @param b - Second node to compare.
86
- * @returns Negative if a sorts before b, positive if after, zero if equal.
87
- */
88
- // Spec: AIT-CT13a
89
- private _compareNodes(a: InternalNode<TMessage>, b: InternalNode<TMessage>): number {
90
- const sa = a.node.serial;
91
- const sb = b.node.serial;
92
- if (sa === undefined && sb === undefined) return a.insertSeq - b.insertSeq;
93
- if (sa === undefined) return 1; // a sorts after serial-bearing b
94
- if (sb === undefined) return -1; // b sorts after serial-bearing a
95
- if (sa < sb) return -1;
96
- if (sa > sb) return 1;
97
- return a.insertSeq - b.insertSeq; // same serial: preserve insertion order
98
- }
99
-
100
- /**
101
- * Insert a node into sortedList at the correct position via binary search.
102
- * @param internal - The node to insert.
103
- */
104
- private _insertSorted(internal: InternalNode<TMessage>): void {
105
- const serial = internal.node.serial;
106
-
107
- // Fast path: null-serial always appends to end (among other null-serials)
108
- if (serial === undefined) {
109
- this._sortedList.push(internal);
110
- return;
111
- }
112
-
113
- // Binary search for insertion point among serial-bearing nodes.
114
- let lo = 0;
115
- let hi = this._sortedList.length;
116
- while (lo < hi) {
117
- const mid = (lo + hi) >>> 1;
118
- const midNode = this._sortedList[mid];
119
- if (!midNode) break; // unreachable: mid is always in bounds
120
- if (this._compareNodes(midNode, internal) <= 0) {
121
- lo = mid + 1;
122
- } else {
123
- hi = mid;
124
- }
125
- }
126
- this._sortedList.splice(lo, 0, internal);
127
- }
128
-
129
- /**
130
- * Remove a node from sortedList.
131
- * @param internal - The node to remove.
132
- */
133
- private _removeSorted(internal: InternalNode<TMessage>): void {
134
- const idx = this._sortedList.indexOf(internal);
135
- if (idx !== -1) this._sortedList.splice(idx, 1);
136
- }
137
-
138
- // -------------------------------------------------------------------------
139
- // Parent index maintenance
140
- // -------------------------------------------------------------------------
141
-
142
- private _addToParentIndex(parentId: string | undefined, msgId: string): void {
143
- let set = this._parentIndex.get(parentId);
144
- if (!set) {
145
- set = new Set();
146
- this._parentIndex.set(parentId, set);
147
- }
148
- set.add(msgId);
149
- }
150
-
151
- private _removeFromParentIndex(parentId: string | undefined, msgId: string): void {
152
- const set = this._parentIndex.get(parentId);
153
- if (set) {
154
- set.delete(msgId);
155
- if (set.size === 0) this._parentIndex.delete(parentId);
156
- }
157
- }
158
-
159
- // -------------------------------------------------------------------------
160
- // Sibling grouping
161
- // -------------------------------------------------------------------------
162
-
163
- /**
164
- * Get the sibling group that `msgId` belongs to.
165
- *
166
- * A sibling group is: the original message + all messages whose `forkOf`
167
- * points to the original (or transitively to a sibling). We find the
168
- * group root by following `forkOf` chains to the earliest ancestor that
169
- * has no `forkOf` (or whose `forkOf` target doesn't share the same parent).
170
- * @param msgId - The msg-id to look up the sibling group for.
171
- * @returns The ordered list of sibling nodes.
172
- */
173
- // Spec: AIT-CT13b
174
- private _getSiblingGroup(msgId: string): ConversationNode<TMessage>[] {
175
- const entry = this._nodeIndex.get(msgId);
176
- if (!entry) return [];
177
-
178
- // Find the "original" — the message at the root of the fork chain
179
- // that shares the same parentId. Guard against cycles in forkOf chains.
180
- let original = entry.node;
181
- const visitedGroup = new Set<string>([original.msgId]);
182
- while (original.forkOf) {
183
- if (visitedGroup.has(original.forkOf)) break; // cycle guard
184
- const forkTarget = this._nodeIndex.get(original.forkOf);
185
- if (!forkTarget || forkTarget.node.parentId !== original.parentId) break;
186
- original = forkTarget.node;
187
- visitedGroup.add(original.msgId);
188
- }
189
-
190
- // Collect all siblings: nodes with the same parentId that either
191
- // ARE the original, or have a forkOf chain leading to the original.
192
- const parentId = original.parentId;
193
- const originalId = original.msgId;
194
- const siblings: InternalNode<TMessage>[] = [];
195
-
196
- const candidateIds = this._parentIndex.get(parentId);
197
- if (candidateIds) {
198
- for (const childId of candidateIds) {
199
- const childEntry = this._nodeIndex.get(childId);
200
- if (childEntry && this._isSiblingOf(childEntry.node, originalId)) {
201
- siblings.push(childEntry);
202
- }
203
- }
204
- }
205
-
206
- // Sort by Ably serial (lexicographic). Messages without a serial
207
- // (optimistic inserts before server relay) sort after all serial-bearing
208
- // siblings — they represent the user's most recent action.
209
- siblings.sort((a, b) => this._compareNodes(a, b));
210
- return siblings.map((s) => s.node);
211
- }
212
-
213
- /**
214
- * Check if `node` belongs to the sibling group rooted at `originalId`.
215
- * A node is a sibling if it IS the original or its forkOf chain leads
216
- * to the original (with the same parentId).
217
- * @param node - The node to check.
218
- * @param originalId - The group root to match against.
219
- * @returns True if the node belongs to the sibling group.
220
- */
221
- private _isSiblingOf(node: ConversationNode<TMessage>, originalId: string): boolean {
222
- if (node.msgId === originalId) return true;
223
- let current = node;
224
- const visited = new Set<string>([current.msgId]);
225
- while (current.forkOf) {
226
- if (current.forkOf === originalId) return true;
227
- if (visited.has(current.forkOf)) break; // cycle guard
228
- const target = this._nodeIndex.get(current.forkOf);
229
- if (!target) break;
230
- current = target.node;
231
- visited.add(current.msgId);
232
- }
233
- return false;
234
- }
235
-
236
- /**
237
- * Get the "group root" msgId for a sibling group — the original message
238
- * that all forks trace back to.
239
- * @param msgId - Any msg-id in the sibling group.
240
- * @returns The msg-id of the group root.
241
- */
242
- private _getGroupRoot(msgId: string): string {
243
- const entry = this._nodeIndex.get(msgId);
244
- if (!entry) return msgId;
245
-
246
- let current = entry.node;
247
- const visited = new Set<string>([current.msgId]);
248
- while (current.forkOf) {
249
- if (visited.has(current.forkOf)) break; // cycle guard
250
- const forkTarget = this._nodeIndex.get(current.forkOf);
251
- if (!forkTarget || forkTarget.node.parentId !== current.parentId) break;
252
- current = forkTarget.node;
253
- visited.add(current.msgId);
254
- }
255
- return current.msgId;
256
- }
257
-
258
- // -------------------------------------------------------------------------
259
- // Public query methods
260
- // -------------------------------------------------------------------------
261
-
262
- flatten(): TMessage[] {
263
- const result: TMessage[] = [];
264
- const currentPath = new Set<string>();
265
- // Track which sibling groups we've already resolved to avoid
266
- // re-resolving for every member of the group.
267
- const resolvedGroups = new Map<string, string>(); // groupRootId → selected msgId
268
-
269
- for (const internal of this._sortedList) {
270
- const node = internal.node;
271
- const { msgId, parentId } = node;
272
-
273
- // Step 1: Check parent reachability.
274
- if (parentId !== undefined && !currentPath.has(parentId)) {
275
- continue;
276
- }
277
-
278
- // Step 2: Check sibling selection.
279
- const group = this._getSiblingGroup(msgId);
280
- if (group.length > 1) {
281
- const groupRootId = this._getGroupRoot(msgId);
282
- let selectedId = resolvedGroups.get(groupRootId);
283
- if (selectedId === undefined) {
284
- const selectedIdx = this._selections.get(groupRootId) ?? group.length - 1;
285
- const clamped = Math.max(0, Math.min(selectedIdx, group.length - 1));
286
- const selected = group[clamped];
287
- if (!selected) break; // unreachable: clamped is always in bounds
288
- selectedId = selected.msgId;
289
- resolvedGroups.set(groupRootId, selectedId);
290
- }
291
- if (msgId !== selectedId) {
292
- continue;
293
- }
294
- }
295
-
296
- currentPath.add(msgId);
297
- result.push(node.message);
298
- }
299
-
300
- return result;
301
- }
302
-
303
- getSiblings(msgId: string): TMessage[] {
304
- return this._getSiblingGroup(msgId).map((n) => n.message);
305
- }
306
-
307
- hasSiblings(msgId: string): boolean {
308
- return this._getSiblingGroup(msgId).length > 1;
309
- }
310
-
311
- getSelectedIndex(msgId: string): number {
312
- const group = this._getSiblingGroup(msgId);
313
- if (group.length <= 1) return 0;
314
- const groupRootId = this._getGroupRoot(msgId);
315
- const stored = this._selections.get(groupRootId);
316
- if (stored !== undefined) return Math.max(0, Math.min(stored, group.length - 1));
317
- return group.length - 1; // default: latest
318
- }
319
-
320
- // Spec: AIT-CT13c
321
- select(msgId: string, index: number): void {
322
- this._logger.debug('ConversationTree.select();', { msgId, index });
323
- const group = this._getSiblingGroup(msgId);
324
- if (group.length <= 1) return;
325
- const groupRootId = this._getGroupRoot(msgId);
326
- this._selections.set(groupRootId, Math.max(0, Math.min(index, group.length - 1)));
327
- }
328
-
329
- getNode(msgId: string): ConversationNode<TMessage> | undefined {
330
- return this._nodeIndex.get(msgId)?.node;
331
- }
332
-
333
- getNodeByKey(key: string): ConversationNode<TMessage> | undefined {
334
- const msgId = this._codecKeyIndex.get(key);
335
- if (!msgId) return undefined;
336
- return this._nodeIndex.get(msgId)?.node;
337
- }
338
-
339
- getHeaders(msgId: string): Record<string, string> | undefined {
340
- return this._nodeIndex.get(msgId)?.node.headers;
341
- }
342
-
343
- // -------------------------------------------------------------------------
344
- // Mutation
345
- // -------------------------------------------------------------------------
346
-
347
- upsert(msgId: string, message: TMessage, headers: Record<string, string>, serial?: string): void {
348
- const parentId = headers[HEADER_PARENT] ?? undefined;
349
- const forkOf = headers[HEADER_FORK_OF] ?? undefined;
350
-
351
- // Maintain codec key → msgId secondary index
352
- this._codecKeyIndex.set(this._getKey(message), msgId);
353
-
354
- const existing = this._nodeIndex.get(msgId);
355
- if (existing) {
356
- // Update in place — message content may have changed (e.g. streaming).
357
- // Only update headers if the new headers are non-empty (prevents
358
- // streaming updates from erasing canonical headers).
359
- existing.node.message = message;
360
- if (Object.keys(headers).length > 0) {
361
- existing.node.headers = { ...headers };
362
- }
363
- // Spec: AIT-CT13d
364
- // Promote serial: optimistic (null) → server-assigned on relay.
365
- if (serial && !existing.node.serial) {
366
- this._logger.debug('ConversationTree.upsert(); promoting serial', { msgId, serial });
367
- existing.node.serial = serial;
368
- // Re-sort: remove from current position, re-insert at correct position.
369
- this._removeSorted(existing);
370
- this._insertSorted(existing);
371
- }
372
- return;
373
- }
374
-
375
- this._logger.trace('ConversationTree.upsert(); inserting new node', { msgId, parentId, forkOf });
376
-
377
- const node: ConversationNode<TMessage> = {
378
- message,
379
- msgId,
380
- parentId,
381
- forkOf,
382
- headers: { ...headers },
383
- serial,
384
- };
385
-
386
- const internal: InternalNode<TMessage> = { node, insertSeq: this._seqCounter++ };
387
- this._nodeIndex.set(msgId, internal);
388
- this._addToParentIndex(parentId, msgId);
389
- this._insertSorted(internal);
390
- }
391
-
392
- delete(msgId: string): void {
393
- const entry = this._nodeIndex.get(msgId);
394
- if (!entry) return;
395
-
396
- this._logger.debug('ConversationTree.delete();', { msgId });
397
-
398
- const { node } = entry;
399
-
400
- // Clean up secondary index
401
- const codecKey = this._getKey(node.message);
402
- if (this._codecKeyIndex.get(codecKey) === msgId) {
403
- this._codecKeyIndex.delete(codecKey);
404
- }
405
-
406
- // Remove from parent index
407
- this._removeFromParentIndex(node.parentId, msgId);
408
-
409
- // Remove from sorted list
410
- this._removeSorted(entry);
411
-
412
- // Remove from primary index
413
- this._nodeIndex.delete(msgId);
414
- this._selections.delete(msgId);
415
-
416
- // Children are NOT deleted — they become unreachable in flatten()
417
- // because their parent is no longer on the active path.
418
- }
419
- }
420
-
421
- // ---------------------------------------------------------------------------
422
- // Factory
423
- // ---------------------------------------------------------------------------
424
-
425
- /**
426
- * Create a ConversationTree that materializes branching history from a flat oplog.
427
- * @param getKey - Codec function that returns a stable key for a domain message.
428
- * @param logger - Logger for diagnostic output.
429
- * @returns A new {@link ConversationTree} instance.
430
- */
431
- export const createConversationTree = <TMessage>(
432
- getKey: (message: TMessage) => string,
433
- logger: Logger,
434
- ): ConversationTree<TMessage> => new DefaultConversationTree(getKey, logger);