@flamingo-stack/openframe-frontend-core 0.0.508 → 0.0.509

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 (306) hide show
  1. package/dist/chat-protocol/decode.d.ts +58 -0
  2. package/dist/chat-protocol/decode.d.ts.map +1 -0
  3. package/dist/chat-protocol/encode.d.ts +44 -0
  4. package/dist/chat-protocol/encode.d.ts.map +1 -0
  5. package/dist/chat-protocol/env-flag.d.ts +35 -0
  6. package/dist/chat-protocol/env-flag.d.ts.map +1 -0
  7. package/dist/chat-protocol/events.d.ts +216 -0
  8. package/dist/chat-protocol/events.d.ts.map +1 -0
  9. package/dist/chat-protocol/frames.d.ts +213 -0
  10. package/dist/chat-protocol/frames.d.ts.map +1 -0
  11. package/dist/chat-protocol/index.cjs +506 -0
  12. package/dist/chat-protocol/index.cjs.map +1 -0
  13. package/dist/chat-protocol/index.d.ts +18 -0
  14. package/dist/chat-protocol/index.d.ts.map +1 -0
  15. package/dist/chat-protocol/index.js +490 -0
  16. package/dist/chat-protocol/index.js.map +1 -0
  17. package/dist/chat-protocol/ip-normalize.d.ts +44 -0
  18. package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
  19. package/dist/chat-protocol/nats-decoder.d.ts +26 -0
  20. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
  21. package/dist/{chunk-3SQ5KXHQ.cjs → chunk-2T4GTV25.cjs} +9 -9
  22. package/dist/{chunk-3SQ5KXHQ.cjs.map → chunk-2T4GTV25.cjs.map} +1 -1
  23. package/dist/{chunk-CWAOV2FK.cjs → chunk-3CGZPAGR.cjs} +3 -3
  24. package/dist/{chunk-CWAOV2FK.cjs.map → chunk-3CGZPAGR.cjs.map} +1 -1
  25. package/dist/{chunk-TLMJHMXJ.js → chunk-3QSR22IC.js} +12 -12
  26. package/dist/chunk-3QSR22IC.js.map +1 -0
  27. package/dist/{chunk-TWYNR4TZ.cjs → chunk-4G5TLHJD.cjs} +7141 -4799
  28. package/dist/chunk-4G5TLHJD.cjs.map +1 -0
  29. package/dist/{chunk-ADPMHWOE.js → chunk-72XAID7Y.js} +4 -4
  30. package/dist/{chunk-PAGKRNWK.js → chunk-7WZHBQ4J.js} +2 -2
  31. package/dist/{chunk-OCEKO5CW.js → chunk-AEELJC4N.js} +12106 -9764
  32. package/dist/chunk-AEELJC4N.js.map +1 -0
  33. package/dist/{chunk-Z42BGM6Q.cjs → chunk-BT2WWDFP.cjs} +5 -5
  34. package/dist/{chunk-Z42BGM6Q.cjs.map → chunk-BT2WWDFP.cjs.map} +1 -1
  35. package/dist/{chunk-7LEL3RIX.cjs → chunk-C7OY3PQP.cjs} +11 -11
  36. package/dist/{chunk-7LEL3RIX.cjs.map → chunk-C7OY3PQP.cjs.map} +1 -1
  37. package/dist/{chunk-RHGGEKPQ.cjs → chunk-CIOLOQKQ.cjs} +4 -4
  38. package/dist/{chunk-RHGGEKPQ.cjs.map → chunk-CIOLOQKQ.cjs.map} +1 -1
  39. package/dist/{chunk-TSZHM74B.js → chunk-DC2TKS7C.js} +2 -2
  40. package/dist/{chunk-M2GOR3XQ.cjs → chunk-DYPYR7FR.cjs} +87 -87
  41. package/dist/{chunk-M2GOR3XQ.cjs.map → chunk-DYPYR7FR.cjs.map} +1 -1
  42. package/dist/{chunk-I64ABCDX.js → chunk-I2B6X77L.js} +2 -2
  43. package/dist/{chunk-NIZDKTGL.cjs → chunk-K5MSCMDK.cjs} +37 -37
  44. package/dist/{chunk-NIZDKTGL.cjs.map → chunk-K5MSCMDK.cjs.map} +1 -1
  45. package/dist/{chunk-DTYRYB2N.js → chunk-K6QTZ7CZ.js} +2 -2
  46. package/dist/{chunk-4LTMXDUS.js → chunk-KBN7PMFT.js} +2 -2
  47. package/dist/{chunk-TBMV7I5N.js → chunk-KXZO2WZX.js} +2 -2
  48. package/dist/{chunk-QTYZMP6D.cjs → chunk-LBAKLT6T.cjs} +62 -62
  49. package/dist/chunk-LBAKLT6T.cjs.map +1 -0
  50. package/dist/{chunk-KEBYLU3U.js → chunk-LFUPI7VX.js} +2 -2
  51. package/dist/{chunk-LDTR4IVZ.cjs → chunk-M37PBG4N.cjs} +31 -31
  52. package/dist/{chunk-LDTR4IVZ.cjs.map → chunk-M37PBG4N.cjs.map} +1 -1
  53. package/dist/{chunk-WA7RR64F.js → chunk-PO5O654H.js} +6 -5
  54. package/dist/chunk-PO5O654H.js.map +1 -0
  55. package/dist/{chunk-WK4N5VBX.cjs → chunk-QJJISLPA.cjs} +28 -27
  56. package/dist/chunk-QJJISLPA.cjs.map +1 -0
  57. package/dist/{chunk-2F44PLTV.cjs → chunk-T7MCSA55.cjs} +26 -26
  58. package/dist/{chunk-2F44PLTV.cjs.map → chunk-T7MCSA55.cjs.map} +1 -1
  59. package/dist/{chunk-WJQHJD7J.js → chunk-TWSKTJHW.js} +2 -2
  60. package/dist/{chunk-ZSQHZYCO.cjs → chunk-UQDMJ6DH.cjs} +13 -13
  61. package/dist/{chunk-ZSQHZYCO.cjs.map → chunk-UQDMJ6DH.cjs.map} +1 -1
  62. package/dist/{chunk-TZRUCD56.js → chunk-V3ZAAFE3.js} +5 -5
  63. package/dist/{chunk-LRGHJPET.js → chunk-XGIFOHKE.js} +2 -2
  64. package/dist/{chunk-POOMO3PA.cjs → chunk-YQEPRYT5.cjs} +7 -7
  65. package/dist/{chunk-POOMO3PA.cjs.map → chunk-YQEPRYT5.cjs.map} +1 -1
  66. package/dist/components/case-studies/index.cjs +8 -8
  67. package/dist/components/case-studies/index.js +2 -2
  68. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  69. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  70. package/dist/components/chat/hooks/index.d.ts +0 -1
  71. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  72. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  73. package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
  74. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  75. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  76. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
  77. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  78. package/dist/components/chat/index.cjs +10 -2
  79. package/dist/components/chat/index.cjs.map +1 -1
  80. package/dist/components/chat/index.d.ts +1 -0
  81. package/dist/components/chat/index.d.ts.map +1 -1
  82. package/dist/components/chat/index.js +21 -13
  83. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  84. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  85. package/dist/components/chat/stream/chat-stream-reducer.d.ts +380 -0
  86. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  87. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  88. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  89. package/dist/components/chat/stream/index.d.ts +17 -0
  90. package/dist/components/chat/stream/index.d.ts.map +1 -0
  91. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  92. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  93. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  94. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  95. package/dist/components/chat/types/api.types.d.ts +1 -81
  96. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  97. package/dist/components/chat/types/processing.types.d.ts +2 -93
  98. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  99. package/dist/components/chat/types/unified-chat-state.types.d.ts +15 -0
  100. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  101. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  102. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  103. package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
  104. package/dist/components/chat/utils/index.d.ts +2 -3
  105. package/dist/components/chat/utils/index.d.ts.map +1 -1
  106. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  107. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  108. package/dist/components/contact/index.cjs +3 -3
  109. package/dist/components/contact/index.js +2 -2
  110. package/dist/components/docs/index.cjs +5 -5
  111. package/dist/components/docs/index.js +4 -4
  112. package/dist/components/embeds/index.cjs +3 -3
  113. package/dist/components/embeds/index.js +2 -2
  114. package/dist/components/faq/index.cjs +3 -3
  115. package/dist/components/faq/index.js +2 -2
  116. package/dist/components/features/index.cjs +2 -2
  117. package/dist/components/features/index.js +1 -1
  118. package/dist/components/help-center-pages/index.cjs +22 -22
  119. package/dist/components/help-center-pages/index.js +13 -13
  120. package/dist/components/index.cjs +174 -132
  121. package/dist/components/index.cjs.map +1 -1
  122. package/dist/components/index.js +65 -23
  123. package/dist/components/index.js.map +1 -1
  124. package/dist/components/meeting-scheduler/index.cjs +34 -34
  125. package/dist/components/meeting-scheduler/index.js +3 -3
  126. package/dist/components/navigation/index.cjs +2 -2
  127. package/dist/components/navigation/index.js +1 -1
  128. package/dist/components/onboarding-guides/index.cjs +5 -5
  129. package/dist/components/onboarding-guides/index.js +4 -4
  130. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  131. package/dist/components/related-content/index.cjs +3 -3
  132. package/dist/components/related-content/index.js +2 -2
  133. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  134. package/dist/components/tickets/index.cjs +6 -6
  135. package/dist/components/tickets/index.js +5 -5
  136. package/dist/components/ui/index.cjs +44 -2
  137. package/dist/components/ui/index.cjs.map +1 -1
  138. package/dist/components/ui/index.d.ts +1 -2
  139. package/dist/components/ui/index.d.ts.map +1 -1
  140. package/dist/components/ui/index.js +55 -13
  141. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  142. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  143. package/dist/components/ui/markdown/engine.d.ts +99 -0
  144. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  145. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  146. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  147. package/dist/components/ui/markdown/index.d.ts +18 -0
  148. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  149. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  150. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  151. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  152. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  153. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  154. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  155. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  156. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  157. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  158. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  159. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  160. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  161. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  162. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  163. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  164. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  165. package/dist/index.cjs +60 -2
  166. package/dist/index.cjs.map +1 -1
  167. package/dist/index.js +71 -13
  168. package/dist/utils/index.cjs +169 -40
  169. package/dist/utils/index.cjs.map +1 -1
  170. package/dist/utils/index.d.ts +1 -0
  171. package/dist/utils/index.d.ts.map +1 -1
  172. package/dist/utils/index.js +162 -41
  173. package/dist/utils/index.js.map +1 -1
  174. package/dist/utils/markdown-fences.d.ts +42 -0
  175. package/dist/utils/markdown-fences.d.ts.map +1 -0
  176. package/dist/utils/markdown-heading-id.d.ts +85 -0
  177. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  178. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  179. package/package.json +7 -1
  180. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +319 -0
  181. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  182. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  183. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  184. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +237 -0
  185. package/src/chat-protocol/decode.ts +278 -0
  186. package/src/chat-protocol/encode.ts +71 -0
  187. package/src/chat-protocol/env-flag.ts +39 -0
  188. package/src/chat-protocol/events.ts +254 -0
  189. package/src/chat-protocol/frames.ts +245 -0
  190. package/src/chat-protocol/index.ts +21 -0
  191. package/src/chat-protocol/ip-normalize.ts +146 -0
  192. package/src/chat-protocol/nats-decoder.ts +264 -0
  193. package/src/components/chat/.chat-message-list.md +10 -6
  194. package/src/components/chat/__tests__/chat-message-list.test.tsx +450 -9
  195. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  196. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +250 -0
  197. package/src/components/chat/chat-message-enhanced.tsx +122 -20
  198. package/src/components/chat/chat-message-list.tsx +515 -133
  199. package/src/components/chat/embeddable-chat.tsx +6 -0
  200. package/src/components/chat/hooks/.index.md +30 -33
  201. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  202. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  203. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  204. package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +92 -0
  205. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  206. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  207. package/src/components/chat/hooks/index.ts +0 -1
  208. package/src/components/chat/hooks/use-chat.ts +8 -0
  209. package/src/components/chat/hooks/use-chunk-catchup.ts +26 -1
  210. package/src/components/chat/hooks/use-nats-chat-adapter.ts +254 -825
  211. package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
  212. package/src/components/chat/index.ts +5 -0
  213. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +944 -0
  214. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +806 -0
  215. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +467 -0
  216. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +1330 -0
  217. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  218. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +105 -0
  219. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  220. package/src/components/chat/stream/chat-stream-reducer.ts +1928 -0
  221. package/src/components/chat/stream/delta-batcher.ts +168 -0
  222. package/src/components/chat/stream/index.ts +55 -0
  223. package/src/components/chat/stream/message-mutations.ts +340 -0
  224. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  225. package/src/components/chat/thinking-display.tsx +1 -1
  226. package/src/components/chat/types/.api.types.md +39 -49
  227. package/src/components/chat/types/.processing.types.md +29 -58
  228. package/src/components/chat/types/.unified-chat-state.types.md +2 -2
  229. package/src/components/chat/types/api.types.ts +6 -73
  230. package/src/components/chat/types/processing.types.ts +11 -53
  231. package/src/components/chat/types/unified-chat-state.types.ts +16 -0
  232. package/src/components/chat/utils/.extract-incomplete-message-state.md +21 -3
  233. package/src/components/chat/utils/.history-merge.md +44 -3
  234. package/src/components/chat/utils/.index.md +31 -40
  235. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  236. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  237. package/src/components/chat/utils/__tests__/extract-incomplete-message-state.test.ts +106 -0
  238. package/src/components/chat/utils/__tests__/history-merge.test.ts +160 -0
  239. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +61 -0
  240. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
  241. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  242. package/src/components/chat/utils/extract-incomplete-message-state.ts +93 -6
  243. package/src/components/chat/utils/history-merge.ts +85 -5
  244. package/src/components/chat/utils/index.ts +5 -8
  245. package/src/components/chat/utils/process-historical-messages.ts +361 -385
  246. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  247. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  248. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  249. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  250. package/src/components/ui/__tests__/markdown-parity.test.tsx +2540 -0
  251. package/src/components/ui/index.ts +1 -2
  252. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +176 -0
  253. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +151 -0
  254. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +354 -0
  255. package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +118 -0
  256. package/src/components/ui/markdown/__tests__/streaming.test.tsx +436 -0
  257. package/src/components/ui/markdown/base-components.tsx +360 -0
  258. package/src/components/ui/markdown/engine.tsx +315 -0
  259. package/src/components/ui/markdown/heading-ids.ts +239 -0
  260. package/src/components/ui/markdown/index.ts +49 -0
  261. package/src/components/ui/markdown/mermaid-diagram.tsx +323 -0
  262. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  263. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  264. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  265. package/src/components/ui/markdown/sanitize.ts +3584 -0
  266. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  267. package/src/components/ui/markdown/streaming.ts +390 -0
  268. package/src/components/ui/markdown/text-size.ts +106 -0
  269. package/src/components/ui/release-changelog-section.tsx +1 -1
  270. package/src/components/ui/ticket-info-section.tsx +1 -1
  271. package/src/utils/index.ts +1 -0
  272. package/src/utils/markdown-fences.ts +127 -0
  273. package/src/utils/markdown-heading-id.ts +348 -0
  274. package/src/utils/markdown-section-extractor.ts +41 -56
  275. package/dist/chunk-OCEKO5CW.js.map +0 -1
  276. package/dist/chunk-QTYZMP6D.cjs.map +0 -1
  277. package/dist/chunk-TLMJHMXJ.js.map +0 -1
  278. package/dist/chunk-TWYNR4TZ.cjs.map +0 -1
  279. package/dist/chunk-WA7RR64F.js.map +0 -1
  280. package/dist/chunk-WK4N5VBX.cjs.map +0 -1
  281. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  282. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  283. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  284. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  285. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  286. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  287. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  288. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  289. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  290. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
  291. package/src/components/chat/utils/.chunk-parser.md +0 -62
  292. package/src/components/chat/utils/chunk-parser.ts +0 -262
  293. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  294. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  295. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  296. /package/dist/{chunk-ADPMHWOE.js.map → chunk-72XAID7Y.js.map} +0 -0
  297. /package/dist/{chunk-PAGKRNWK.js.map → chunk-7WZHBQ4J.js.map} +0 -0
  298. /package/dist/{chunk-TSZHM74B.js.map → chunk-DC2TKS7C.js.map} +0 -0
  299. /package/dist/{chunk-I64ABCDX.js.map → chunk-I2B6X77L.js.map} +0 -0
  300. /package/dist/{chunk-DTYRYB2N.js.map → chunk-K6QTZ7CZ.js.map} +0 -0
  301. /package/dist/{chunk-4LTMXDUS.js.map → chunk-KBN7PMFT.js.map} +0 -0
  302. /package/dist/{chunk-TBMV7I5N.js.map → chunk-KXZO2WZX.js.map} +0 -0
  303. /package/dist/{chunk-KEBYLU3U.js.map → chunk-LFUPI7VX.js.map} +0 -0
  304. /package/dist/{chunk-WJQHJD7J.js.map → chunk-TWSKTJHW.js.map} +0 -0
  305. /package/dist/{chunk-TZRUCD56.js.map → chunk-V3ZAAFE3.js.map} +0 -0
  306. /package/dist/{chunk-LRGHJPET.js.map → chunk-XGIFOHKE.js.map} +0 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Phase-0 GOLDEN CHARACTERIZATION TESTS — `MessageSegmentAccumulator`.
3
+ *
4
+ * Pins the CURRENT segment-accumulation semantics before the stream-reader
5
+ * unification. Each test drives the accumulator through a recorded event
6
+ * sequence and snapshots the final segment array. Behaviors captured here
7
+ * (including the implicit approval-flip on tool execution and the batch
8
+ * no-downgrade rule) are the recorded baseline the future reducer must match.
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest'
12
+ import { createMessageSegmentAccumulator } from '../message-segment-accumulator'
13
+ import type { ToolExecutionSegment, PendingToolCallData } from '../../types'
14
+
15
+ const executing = (
16
+ execId: string | undefined,
17
+ overrides: Partial<ToolExecutionSegment['data']> = {},
18
+ ): ToolExecutionSegment => ({
19
+ type: 'tool_execution',
20
+ data: {
21
+ type: 'EXECUTING_TOOL',
22
+ integratedToolType: 'TACTICAL_RMM',
23
+ toolFunction: 'run_script',
24
+ toolTitle: 'Run cleanup script',
25
+ parameters: { script: 'cleanup.sh' },
26
+ ...(execId ? { toolExecutionRequestId: execId } : {}),
27
+ ...overrides,
28
+ },
29
+ })
30
+
31
+ const executed = (
32
+ execId: string | undefined,
33
+ overrides: Partial<ToolExecutionSegment['data']> = {},
34
+ ): ToolExecutionSegment => ({
35
+ type: 'tool_execution',
36
+ data: {
37
+ type: 'EXECUTED_TOOL',
38
+ integratedToolType: 'TACTICAL_RMM',
39
+ toolFunction: 'run_script',
40
+ // Backend omits toolTitle on EXECUTED_TOOL — the accumulator restores it.
41
+ result: 'Freed 2.3 GB',
42
+ success: true,
43
+ ...(execId ? { toolExecutionRequestId: execId } : {}),
44
+ ...overrides,
45
+ },
46
+ })
47
+
48
+ const batchToolCalls: PendingToolCallData[] = [
49
+ {
50
+ toolExecutionRequestId: 'exec-a',
51
+ toolName: 'create_ticket',
52
+ toolTitle: 'Create ticket',
53
+ toolExplanation: 'Opens a support ticket',
54
+ requiresApproval: true,
55
+ approvalType: 'CLIENT',
56
+ toolCallArguments: { subject: 'Printer down' },
57
+ },
58
+ {
59
+ toolExecutionRequestId: 'exec-b',
60
+ toolName: 'read_kb',
61
+ requiresApproval: false,
62
+ approvalType: null,
63
+ toolCallArguments: null,
64
+ },
65
+ ]
66
+
67
+ describe('MessageSegmentAccumulator — golden sequences', () => {
68
+ it('coalesces consecutive text deltas into one text segment', () => {
69
+ const acc = createMessageSegmentAccumulator()
70
+ acc.appendText('Hello')
71
+ acc.appendText(', ')
72
+ acc.appendText('world')
73
+ expect(acc.getSegments()).toMatchSnapshot()
74
+ })
75
+
76
+ it('coalesces thinking deltas, and text after thinking starts a NEW segment', () => {
77
+ const acc = createMessageSegmentAccumulator()
78
+ acc.appendThinking('Let me think')
79
+ acc.appendThinking('… ok.')
80
+ acc.appendText('Answer: ')
81
+ acc.appendText('42')
82
+ // A late thinking delta after text starts ANOTHER thinking segment
83
+ // (append-to-last only merges into a trailing segment of the same type).
84
+ acc.appendThinking('post-answer thought')
85
+ expect(acc.getSegments()).toMatchSnapshot()
86
+ })
87
+
88
+ it('merges EXECUTING_TOOL → EXECUTED_TOOL in place by toolExecutionRequestId (title restored)', () => {
89
+ const acc = createMessageSegmentAccumulator()
90
+ acc.appendText('Running a cleanup now.')
91
+ acc.addToolExecution(executing('exec-1'))
92
+ acc.addToolExecution(executed('exec-1'))
93
+ expect(acc.getSegments()).toMatchSnapshot()
94
+ })
95
+
96
+ it('pairs by (integratedToolType, toolFunction) when no exec id is present (older backends)', () => {
97
+ const acc = createMessageSegmentAccumulator()
98
+ acc.addToolExecution(executing(undefined))
99
+ acc.addToolExecution(executed(undefined))
100
+ expect(acc.getSegments()).toMatchSnapshot()
101
+ })
102
+
103
+ it('EXECUTED with no prior EXECUTING lands as a standalone completed segment', () => {
104
+ const acc = createMessageSegmentAccumulator()
105
+ acc.addToolExecution(executed('exec-orphan'))
106
+ expect(acc.getSegments()).toMatchSnapshot()
107
+ })
108
+
109
+ it('CHARACTERIZATION: a LATE standalone EXECUTING after its EXECUTED pushes a NEW executing segment (no in-place downgrade, but a duplicate card appears)', () => {
110
+ // The no-downgrade rule is enforced INSIDE approval batches
111
+ // (applyExecutionToBatch); for standalone segments a redelivered
112
+ // EXECUTING after EXECUTED does NOT downgrade the merged segment —
113
+ // it appends a fresh EXECUTING segment. Recording as-is, see
114
+ // unification plan.
115
+ const acc = createMessageSegmentAccumulator()
116
+ acc.addToolExecution(executing('exec-1'))
117
+ acc.addToolExecution(executed('exec-1'))
118
+ acc.addToolExecution(executing('exec-1'))
119
+ expect(acc.getSegments()).toMatchSnapshot()
120
+ })
121
+
122
+ it('single approval request → updateApprovalStatus flips status and stamps resolvedByName only on batches', () => {
123
+ const acc = createMessageSegmentAccumulator({
124
+ onApprove: async () => {},
125
+ onReject: async () => {},
126
+ })
127
+ acc.addApprovalRequest('req-1', 'systemctl restart nats', 'Restart broker', 'CLIENT')
128
+ acc.updateApprovalStatus('req-1', 'approved', 'Jane Admin')
129
+ expect(acc.getSegments()).toMatchSnapshot()
130
+ })
131
+
132
+ it('tracked (pending) approval + processApprovalResult appends a resolved segment', () => {
133
+ const acc = createMessageSegmentAccumulator()
134
+ acc.trackApprovalRequest('req-2', {
135
+ command: 'rm -rf /tmp/cache',
136
+ explanation: 'Clear caches',
137
+ approvalType: 'CLIENT',
138
+ })
139
+ const result = acc.processApprovalResult('req-2', false, 'CLIENT')
140
+ expect({
141
+ segments: acc.getSegments(),
142
+ pendingData: result?.pendingData ?? null,
143
+ hasPendingAfter: acc.hasPendingApprovals(),
144
+ }).toMatchSnapshot()
145
+ })
146
+
147
+ it('batch approval with per-tool executions merged into the batch (no standalone segments)', () => {
148
+ const acc = createMessageSegmentAccumulator()
149
+ acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls)
150
+ acc.updateApprovalStatus('batch-1', 'approved', 'Jane Admin')
151
+ // Executions matching the batch's toolExecutionRequestIds merge into
152
+ // `executions` instead of pushing standalone tool_execution segments.
153
+ acc.addToolExecution(executing('exec-a', { toolFunction: 'create_ticket' }))
154
+ acc.addToolExecution(
155
+ executed('exec-a', { toolFunction: 'create_ticket', result: 'Ticket #77 created' }),
156
+ )
157
+ acc.addToolExecution(executing('exec-b', { toolFunction: 'read_kb' }))
158
+ expect(acc.getSegments()).toMatchSnapshot()
159
+ })
160
+
161
+ it('batch no-downgrade rule: a redelivered EXECUTING after EXECUTED keeps the done slot', () => {
162
+ const acc = createMessageSegmentAccumulator()
163
+ acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'approved')
164
+ acc.addToolExecution(executed('exec-a', { result: 'done!' }))
165
+ // Late/redelivered EXECUTING for the same exec id — must NOT flip
166
+ // the slot back to 'executing'.
167
+ acc.addToolExecution(executing('exec-a'))
168
+ expect(acc.getSegments()).toMatchSnapshot()
169
+ })
170
+
171
+ it('addApprovalBatch upserts by approvalRequestId (replay path produces ONE batch segment)', () => {
172
+ const acc = createMessageSegmentAccumulator()
173
+ acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'pending')
174
+ acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'approved', {
175
+ 'exec-a': { status: 'done', result: 'ok', success: true },
176
+ }, 'Jane Admin')
177
+ expect(acc.getSegments()).toMatchSnapshot()
178
+ })
179
+
180
+ it('implicit resolvePendingApprovalForExecution: a tool execution flips the LATEST pending single approval (NATS-observer behavior)', () => {
181
+ // An observer (technician mirroring a client chat) may never receive
182
+ // APPROVAL_RESULT — only the tool's EXECUTING/EXECUTED events. The
183
+ // accumulator treats a tool execution as implicit approval of the most
184
+ // recent still-pending single approval gate. Pinning explicitly.
185
+ const acc = createMessageSegmentAccumulator()
186
+ acc.addApprovalRequest('req-old', 'echo old', undefined, 'CLIENT', 'rejected')
187
+ acc.addApprovalRequest('req-1', 'systemctl restart nats', undefined, 'CLIENT', 'pending')
188
+ acc.addToolExecution(executing('exec-9'))
189
+ expect(acc.getSegments()).toMatchSnapshot()
190
+ })
191
+
192
+ it('implicit approval flips ONLY the latest pending gate, earlier pending gates stay pending', () => {
193
+ const acc = createMessageSegmentAccumulator()
194
+ acc.addApprovalRequest('req-1', 'first', undefined, 'CLIENT', 'pending')
195
+ acc.addApprovalRequest('req-2', 'second', undefined, 'CLIENT', 'pending')
196
+ acc.addToolExecution(executing('exec-1'))
197
+ expect(acc.getSegments()).toMatchSnapshot()
198
+ })
199
+
200
+ it('implicit approval does NOT touch approval_batch segments (handled by applyExecutionToBatch only)', () => {
201
+ const acc = createMessageSegmentAccumulator()
202
+ acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'pending')
203
+ // exec id NOT in the batch → standalone segment path; batch stays pending.
204
+ acc.addToolExecution(executing('exec-unrelated'))
205
+ expect(acc.getSegments()).toMatchSnapshot()
206
+ })
207
+
208
+ it('context compaction start → complete replaces the started segment in place', () => {
209
+ const acc = createMessageSegmentAccumulator()
210
+ acc.appendText('Before compaction')
211
+ acc.addContextCompaction()
212
+ acc.completeContextCompaction('Summarized 34 messages')
213
+ acc.appendText('After compaction')
214
+ expect(acc.getSegments()).toMatchSnapshot()
215
+ })
216
+
217
+ it('completeContextCompaction with no started segment pushes a completed one', () => {
218
+ const acc = createMessageSegmentAccumulator()
219
+ acc.completeContextCompaction(undefined)
220
+ expect(acc.getSegments()).toMatchSnapshot()
221
+ })
222
+
223
+ it('error segments append verbatim', () => {
224
+ const acc = createMessageSegmentAccumulator()
225
+ acc.appendText('partial answer')
226
+ acc.addError('Agent crashed', 'boom')
227
+ expect(acc.getSegments()).toMatchSnapshot()
228
+ })
229
+
230
+ it('flushPendingApprovals emits pending cards for every tracked-but-unresolved approval', () => {
231
+ const acc = createMessageSegmentAccumulator()
232
+ acc.trackApprovalRequest('req-1', { command: 'cmd-1', approvalType: 'CLIENT' })
233
+ acc.trackApprovalRequest('req-2', { command: 'cmd-2', explanation: 'why', approvalType: 'ADMIN' })
234
+ expect(acc.flushPendingApprovals()).toMatchSnapshot()
235
+ })
236
+
237
+ it('replaySegments reproduces a mixed recorded segment array (idempotent replay)', () => {
238
+ const acc = createMessageSegmentAccumulator()
239
+ acc.appendThinking('hmm')
240
+ acc.appendText('Sure — ')
241
+ acc.addToolExecution(executing('exec-1'))
242
+ acc.addToolExecution(executed('exec-1'))
243
+ acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'approved', {
244
+ 'exec-a': { status: 'done', result: 'ok', success: true },
245
+ })
246
+ acc.appendText('done.')
247
+ const recorded = acc.getSegments()
248
+
249
+ const replayed = createMessageSegmentAccumulator().replaySegments(recorded)
250
+ expect(replayed).toMatchSnapshot()
251
+ })
252
+
253
+ it('initializeWithState continues an incomplete message across reconnect', () => {
254
+ const first = createMessageSegmentAccumulator()
255
+ first.appendText('partial ')
256
+ first.addToolExecution(executing('exec-1'))
257
+ const state = first.getState()
258
+
259
+ const resumed = createMessageSegmentAccumulator()
260
+ resumed.initializeWithState({
261
+ existingSegments: state.segments,
262
+ pendingApprovals: state.pendingApprovals,
263
+ executingTools: state.executingTools,
264
+ })
265
+ // The EXECUTED for the pre-reconnect EXECUTING merges into it.
266
+ resumed.addToolExecution(executed('exec-1'))
267
+ resumed.appendText('and the rest.')
268
+ expect(resumed.getSegments()).toMatchSnapshot()
269
+ })
270
+ })
@@ -9,6 +9,18 @@ import type {
9
9
  PendingApproval,
10
10
  ExecutingToolState,
11
11
  } from '../types'
12
+ import type { InitializeExtras } from '../stream/chat-stream-reducer'
13
+
14
+ /**
15
+ * What the realtime accumulator needs to RESUME an unfinished turn.
16
+ *
17
+ * DERIVED, not restated: this is exactly the reducer's `InitializeExtras`
18
+ * minus `escalatedApprovals` (which only the live stream can produce — it has
19
+ * no historical representation to extract). Hosts pass the result straight
20
+ * into `reducer.initializeWithState(messages, extras)`, so the two must not
21
+ * drift; a structural coincidence is not a contract.
22
+ */
23
+ export type IncompleteMessageState = Omit<InitializeExtras, 'escalatedApprovals'>
12
24
 
13
25
  /**
14
26
  * Extract incomplete message state from the last historical assistant message
@@ -16,11 +28,7 @@ import type {
16
28
  */
17
29
  export function extractIncompleteMessageState(
18
30
  lastMessage: ProcessedMessage | undefined
19
- ): {
20
- existingSegments?: MessageSegment[]
21
- pendingApprovals?: Map<string, PendingApproval>
22
- executingTools?: Map<string, ExecutingToolState>
23
- } | undefined {
31
+ ): IncompleteMessageState | undefined {
24
32
  if (!lastMessage || lastMessage.role !== 'assistant' || typeof lastMessage.content === 'string') {
25
33
  return undefined
26
34
  }
@@ -29,8 +37,15 @@ export function extractIncompleteMessageState(
29
37
  const pendingApprovals = new Map<string, PendingApproval>()
30
38
  const executingTools = new Map<string, ExecutingToolState>()
31
39
  let hasIncompleteState = false
40
+ // "Incomplete" and "the agent is working" are DIFFERENT questions, and only
41
+ // the second one drives the activity indicator. A pending approval is the
42
+ // clearest case of incomplete-but-idle: the turn is unfinished precisely
43
+ // BECAUSE the agent is blocked on the user, and spinning there claims work
44
+ // that is not happening. Everything below is the complement of that.
45
+ let agentBusy = false
32
46
 
33
- segments.forEach(segment => {
47
+ segments.forEach((segment, index) => {
48
+ const isLast = index === segments.length - 1
34
49
  switch (segment.type) {
35
50
  case 'tool_execution':
36
51
  if (segment.data.type === 'EXECUTING_TOOL') {
@@ -45,6 +60,8 @@ export function extractIncompleteMessageState(
45
60
  parameters: segment.data.parameters,
46
61
  })
47
62
  hasIncompleteState = true
63
+ // An EXECUTING with no result: the tool is running right now.
64
+ agentBusy = true
48
65
  }
49
66
  break
50
67
 
@@ -56,6 +73,17 @@ export function extractIncompleteMessageState(
56
73
  approvalType: segment.data.approvalType || 'CLIENT',
57
74
  })
58
75
  hasIncompleteState = true
76
+ } else if (isLast && segment.status === 'approved') {
77
+ // The user said yes and the agent has produced NOTHING since — it is
78
+ // executing the command. Position is the only signal here: unlike a
79
+ // batch, a single request carries no execution record, so an approved
80
+ // request that already ran is recognised by the work that follows it.
81
+ //
82
+ // This also has to mark the tail incomplete, or the state never
83
+ // reaches the reducer at all — and the continuation, when it replays,
84
+ // must merge into THIS bubble rather than open a second one.
85
+ hasIncompleteState = true
86
+ agentBusy = true
59
87
  }
60
88
  break
61
89
 
@@ -72,12 +100,20 @@ export function extractIncompleteMessageState(
72
100
  if (segment.status !== 'rejected' && !allDone) {
73
101
  hasIncompleteState = true
74
102
  }
103
+ // Approved with tool calls still outstanding — the agent is running
104
+ // them. The execution record makes this position-independent: once
105
+ // every call is `done` the batch stops claiming work.
106
+ if (segment.status === 'approved' && !allDone) {
107
+ agentBusy = true
108
+ }
75
109
  break
76
110
  }
77
111
 
78
112
  case 'context_compaction':
79
113
  if (segment.status === 'started') {
80
114
  hasIncompleteState = true
115
+ // Compaction is the agent's own work, not a wait on the user.
116
+ agentBusy = true
81
117
  }
82
118
  break
83
119
  }
@@ -91,5 +127,56 @@ export function extractIncompleteMessageState(
91
127
  existingSegments: segments,
92
128
  pendingApprovals: pendingApprovals.size > 0 ? pendingApprovals : undefined,
93
129
  executingTools: executingTools.size > 0 ? executingTools : undefined,
130
+ ...(agentBusy ? { agentBusy: true } : {}),
131
+ }
132
+ }
133
+
134
+ /**
135
+ * THREAD-level generalization of `extractIncompleteMessageState`: given a
136
+ * processed thread, look at the TRAILING RUN of consecutive assistant
137
+ * messages (not just the very last row), flatten it to one segment list, and
138
+ * ask the single-message extractor whether that tail is unfinished.
139
+ *
140
+ * Why the run and not the last row: the backend can split one logical turn
141
+ * across several assistant messages (a preamble bubble, then the approval
142
+ * card, then the continuation). The unfinished artifact — a pending approval,
143
+ * an EXECUTING_TOOL with no result, an open compaction — can therefore sit in
144
+ * an EARLIER bubble of the same trailing run, and a last-row-only check
145
+ * reports "complete" while the turn is very much still in flight.
146
+ *
147
+ * SSOT for every "is the tail incomplete?" question — the NATS adapter (which
148
+ * used to inline the single-row check) and hosts replaying a stored thread
149
+ * both call this instead of re-deriving the walk.
150
+ *
151
+ * A trailing non-assistant row (user / error) means the last turn closed →
152
+ * `undefined`.
153
+ */
154
+ export function extractIncompleteTailState(
155
+ messages: readonly ProcessedMessage[] | undefined,
156
+ ): IncompleteMessageState | undefined {
157
+ if (!messages || messages.length === 0) return undefined
158
+
159
+ // Walk back over the trailing consecutive assistant run.
160
+ let start = messages.length
161
+ while (start > 0 && messages[start - 1]?.role === 'assistant') start -= 1
162
+ if (start === messages.length) return undefined
163
+
164
+ const segments: MessageSegment[] = []
165
+ for (let i = start; i < messages.length; i += 1) {
166
+ const content = messages[i]?.content
167
+ // A string-content assistant row is a plain-text bubble with nothing to
168
+ // resume; skip it rather than aborting — a later row in the same run may
169
+ // still hold the unfinished segment.
170
+ if (Array.isArray(content)) segments.push(...(content as MessageSegment[]))
94
171
  }
172
+ if (segments.length === 0) return undefined
173
+
174
+ // Reuse the single-message extractor verbatim by handing it a synthetic
175
+ // assistant row carrying the flattened run — one incompleteness rule set,
176
+ // not two.
177
+ return extractIncompleteMessageState({
178
+ ...messages[messages.length - 1],
179
+ role: 'assistant',
180
+ content: segments,
181
+ } as ProcessedMessage)
95
182
  }
@@ -1,4 +1,9 @@
1
- import type { ApprovalBatchSegment, MessageContent } from '../types'
1
+ import type {
2
+ ApprovalBatchSegment,
3
+ ApprovalRequestSegment,
4
+ MessageContent,
5
+ ToolExecutionSegment,
6
+ } from '../types'
2
7
 
3
8
  /**
4
9
  * Reconciliation between persisted dialog history (processed GraphQL pages)
@@ -77,6 +82,50 @@ function assistantAnswerText(content: MessageContent): string {
77
82
  return parts.join('\n').trim()
78
83
  }
79
84
 
85
+ /**
86
+ * Backend request ids carried by a turn's segments — tool-call ids, approval
87
+ * request ids, batch ids.
88
+ *
89
+ * These identify the TURN, which is what lets the merge recognise history's
90
+ * persisted row and a live synthetic as two copies of the same thing when
91
+ * their IDS DIFFER. Ids only converge through adoption, and adoption only
92
+ * happens on a replay (a reload re-streams the turn into the persisted row);
93
+ * a refetch that lands mid-stream has no replay, so the live copy keeps the
94
+ * synthetic id the reducer minted.
95
+ *
96
+ * Empty for a turn that has produced only text — callers must treat an empty
97
+ * set as "no signal", never as a match.
98
+ */
99
+ function turnRequestKeys(content: MessageContent): Set<string> {
100
+ const keys = new Set<string>()
101
+ if (!Array.isArray(content)) return keys
102
+ for (const seg of content) {
103
+ if (seg.type === 'tool_execution') {
104
+ const id = (seg as ToolExecutionSegment).data?.toolExecutionRequestId
105
+ if (id) keys.add(id)
106
+ } else if (seg.type === 'approval_request') {
107
+ const id = (seg as ApprovalRequestSegment).data?.requestId
108
+ if (id) keys.add(id)
109
+ } else if (seg.type === 'approval_batch') {
110
+ const data = (seg as ApprovalBatchSegment).data
111
+ if (data?.approvalRequestId) keys.add(data.approvalRequestId)
112
+ for (const call of data?.toolCalls ?? []) {
113
+ if (call?.toolExecutionRequestId) keys.add(call.toolExecutionRequestId)
114
+ }
115
+ }
116
+ }
117
+ return keys
118
+ }
119
+
120
+ /** Whether `content` carries any of `keys` — see `turnRequestKeys`. */
121
+ function sharesRequestKey(content: MessageContent, keys: ReadonlySet<string>): boolean {
122
+ if (keys.size === 0) return false
123
+ for (const key of turnRequestKeys(content)) {
124
+ if (keys.has(key)) return true
125
+ }
126
+ return false
127
+ }
128
+
80
129
  /** Flattens DESC-sorted message pages (newest page first, newest message
81
130
  * first within a page) into one chronological list. */
82
131
  export function flattenMessagePagesChronological<T>(pages: readonly { messages: readonly T[] }[] | undefined): T[] {
@@ -250,13 +299,32 @@ export function mergeHistoryWithRealtime<M extends MergeableChatMessage>(input:
250
299
  ) {
251
300
  const persistedSize = historyTrailingAssistant.content.length
252
301
  const persistedSeq = historyTrailingAssistant.streamSeq ?? 0
302
+ // Turn identity for a twin that has NOT adopted the persisted id. Adoption
303
+ // only happens on a REPLAY (a reload re-streams the turn from its
304
+ // MESSAGE_START into the persisted row); a refetch that lands while the
305
+ // turn is still streaming has no replay, so the live copy keeps the
306
+ // synthetic id the reducer minted and the id test above can never match.
307
+ // Both copies do carry the backend's per-call request ids, which identify
308
+ // the TURN — the same trick the approval-batch branch plays with
309
+ // `approvalRequestId`, widened to tool calls.
310
+ const trailingKeys = turnRequestKeys(historyTrailingAssistant.content)
311
+ const isSameTurn = (m: M): boolean =>
312
+ m.id === historyTrailingAssistant.id ||
313
+ (trailingKeys.size > 0 && sharesRequestKey(m.content, trailingKeys))
253
314
  const richerTwin = existingMessages.find(
254
315
  (m) =>
255
316
  m !== historyTrailingAssistant &&
256
- m.id === historyTrailingAssistant.id &&
257
317
  m.role === 'assistant' &&
258
318
  Array.isArray(m.content) &&
259
- (m.content.length > persistedSize ||
319
+ isSameTurn(m) &&
320
+ // A STILL-STREAMING twin wins outright: it is the copy the chunks are
321
+ // landing in, so "richer" is a property it is about to have anyway,
322
+ // and history's snapshot of a turn in flight is stale by construction.
323
+ // Without this the same turn rendered TWICE — history's partial copy
324
+ // beside the live one, their tool rows disagreeing (one pending, one
325
+ // failed) because each stopped at a different chunk.
326
+ (m.id === streamingMessageId ||
327
+ m.content.length > persistedSize ||
260
328
  (m.content.length === persistedSize && typeof m.streamSeq === 'number' && m.streamSeq > persistedSeq)),
261
329
  )
262
330
  if (richerTwin) {
@@ -384,8 +452,20 @@ export function mergeHistoryWithRealtime<M extends MergeableChatMessage>(input:
384
452
  if (typeof m.streamSeq === 'number' && anyHistoryRowSeq) {
385
453
  const roleMax = historyMaxSeqByRole.get(m.role) ?? 0
386
454
  covered = roleMax > 0 && roleMax >= m.streamSeq
387
- } else if (typeof m.streamSeq === 'number' && historyMaxStreamSeq > 0) {
388
- covered = historyMaxStreamSeq >= m.streamSeq
455
+ } else if (typeof m.streamSeq === 'number') {
456
+ // This synthetic carries its own seq, but no history row does. Decide
457
+ // against the global when it exists — and when it does NOT
458
+ // (`historyMaxStreamSeq === 0`), that is not "unknown, guess by clock":
459
+ // it is positive evidence that nothing in the snapshot has reached any
460
+ // seq at all, so the snapshot cannot contain this turn. KEEP it.
461
+ //
462
+ // This is the mid-stream refetch: the only persisted row of a turn in
463
+ // flight is the user MESSAGE_REQUEST, which the backend does not stamp,
464
+ // so the max is 0 while live bubbles keep arriving. Falling through to
465
+ // wall-clock there judged every one of them "older than the fetch
466
+ // instant, therefore persisted" and dropped all but the single
467
+ // `streamingMessageId` — the thread collapsed to the user's prompt.
468
+ covered = historyMaxStreamSeq > 0 && historyMaxStreamSeq >= m.streamSeq
389
469
  } else {
390
470
  covered =
391
471
  historyCoversRealtime !== null
@@ -3,14 +3,9 @@
3
3
  * Export all utilities for processing chat messages
4
4
  */
5
5
 
6
- // Chunk parsing utilities
7
- export {
8
- parseChunkToAction,
9
- isControlChunk,
10
- isErrorChunk,
11
- isMetadataChunk,
12
- extractTextFromChunk,
13
- } from './chunk-parser'
6
+ // NOTE: NATS chunk parsing lives in `chat-protocol/nats-decoder`
7
+ // (`decodeNatsChunk`) — the legacy `chunk-parser` module was deleted once the
8
+ // reducer became the single reading path.
14
9
 
15
10
  // Segment accumulator
16
11
  export {
@@ -24,11 +19,13 @@ export {
24
19
  processHistoricalMessages,
25
20
  extractErrorMessages,
26
21
  processHistoricalMessagesWithErrors,
22
+ decodeHistoricalMessageData,
27
23
  } from './process-historical-messages'
28
24
 
29
25
  // Incomplete message state extraction
30
26
  export {
31
27
  extractIncompleteMessageState,
28
+ extractIncompleteTailState,
32
29
  } from './extract-incomplete-message-state'
33
30
 
34
31
  // History <-> realtime reconciliation