@assistant-ui/core 0.3.14 → 0.3.16

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 (341) hide show
  1. package/dist/adapters/voice.js +11 -0
  2. package/dist/adapters/voice.js.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/internal.d.ts +8 -2
  5. package/dist/internal.js +9 -3
  6. package/dist/model-context/frame/host.d.ts +1 -1
  7. package/dist/model-context/frame/host.d.ts.map +1 -1
  8. package/dist/model-context/frame/host.js +20 -5
  9. package/dist/model-context/frame/host.js.map +1 -1
  10. package/dist/model-context/frame/provider.d.ts +6 -0
  11. package/dist/model-context/frame/provider.d.ts.map +1 -1
  12. package/dist/model-context/frame/provider.js +126 -16
  13. package/dist/model-context/frame/provider.js.map +1 -1
  14. package/dist/model-context/frame/types.d.ts +4 -1
  15. package/dist/model-context/frame/types.d.ts.map +1 -1
  16. package/dist/model-context/frame/types.js.map +1 -1
  17. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  18. package/dist/model-context/interactable-composer-metadata.js +1 -1
  19. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  20. package/dist/model-context/registry.d.ts +2 -2
  21. package/dist/model-context/registry.d.ts.map +1 -1
  22. package/dist/model-context/registry.js +9 -28
  23. package/dist/model-context/registry.js.map +1 -1
  24. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  25. package/dist/react/client/InMemoryThreadList.js +24 -20
  26. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  27. package/dist/react/client/Interactables.d.ts.map +1 -1
  28. package/dist/react/client/Interactables.js +59 -177
  29. package/dist/react/client/Interactables.js.map +1 -1
  30. package/dist/react/client/RemoteThreadList.d.ts +1 -1
  31. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  32. package/dist/react/client/RemoteThreadList.js +117 -78
  33. package/dist/react/client/RemoteThreadList.js.map +1 -1
  34. package/dist/react/index.d.ts +2 -1
  35. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  36. package/dist/react/interactables-legacy/Interactables.js +56 -174
  37. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  38. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  39. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  40. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  41. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  42. package/dist/react/primitive-hooks/useActionBarCopy.js +7 -1
  43. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  44. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +3 -0
  45. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
  46. package/dist/react/primitive-hooks/useThreadIsEmpty.js +3 -0
  47. package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -1
  48. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +3 -0
  49. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
  50. package/dist/react/primitive-hooks/useThreadIsRunning.js +3 -0
  51. package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -1
  52. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  53. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  54. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  55. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  56. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +3 -1
  57. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  58. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +6 -0
  59. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  60. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +7 -1
  61. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  62. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +174 -64
  63. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  64. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  65. package/dist/react/runtimes/RemoteThreadResource.js +2 -0
  66. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  67. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  68. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  69. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  70. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  71. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js +1 -1
  72. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  73. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  74. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  75. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  76. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  77. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  78. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  79. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  80. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  81. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  82. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +38 -5
  83. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  84. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  85. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  86. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  87. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  88. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  89. package/dist/react/runtimes/external-message-converter.js +33 -233
  90. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  91. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  92. package/dist/runtime/api/composer-runtime.js.map +1 -1
  93. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  94. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  95. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  96. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  97. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  98. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  99. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  100. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  101. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  102. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  103. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  104. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  105. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  106. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  107. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  108. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  109. package/dist/runtime/internal.d.ts +2 -1
  110. package/dist/runtime/internal.js +2 -1
  111. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  112. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  113. package/dist/runtime/queue/message-queue.js +1 -1
  114. package/dist/runtime/queue/message-queue.js.map +1 -1
  115. package/dist/runtime/queue/queue-item.d.ts +12 -0
  116. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  117. package/dist/runtime/queue/queue-item.js +6 -0
  118. package/dist/runtime/queue/queue-item.js.map +1 -0
  119. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  120. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  121. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  122. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  123. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  124. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  125. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  126. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  127. package/dist/runtime/utils/auto-status.d.ts +6 -1
  128. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  129. package/dist/runtime/utils/auto-status.js +7 -1
  130. package/dist/runtime/utils/auto-status.js.map +1 -1
  131. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  132. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  133. package/dist/runtime/utils/external-message-conversion.js +221 -0
  134. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  135. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  136. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  137. package/dist/runtime/utils/external-store-message.js +2 -1
  138. package/dist/runtime/utils/external-store-message.js.map +1 -1
  139. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  140. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  141. package/dist/runtime/utils/message-repository-session.js +130 -0
  142. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  143. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  144. package/dist/runtime/utils/message-repository.js +30 -22
  145. package/dist/runtime/utils/message-repository.js.map +1 -1
  146. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  147. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  148. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  149. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  150. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  151. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  152. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  153. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  154. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  155. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  156. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  157. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  158. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  159. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +124 -20
  160. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  161. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +102 -0
  162. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -0
  163. package/dist/runtimes/inert/InertThreadRuntimeCore.js +156 -0
  164. package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -0
  165. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  166. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  167. package/dist/runtimes/local/local-thread-runtime-core.js +45 -8
  168. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  169. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +12 -60
  170. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  171. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +10 -145
  172. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  173. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +9 -0
  174. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -0
  175. package/dist/runtimes/remote-thread-list/adapter-changed.js +13 -0
  176. package/dist/runtimes/remote-thread-list/adapter-changed.js.map +1 -0
  177. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  178. package/dist/runtimes/remote-thread-list/empty-thread-core.js +16 -155
  179. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  180. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +2 -0
  181. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  182. package/dist/runtimes/remote-thread-list/optimistic-state.js +11 -0
  183. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  184. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  185. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  186. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  187. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  188. package/dist/runtimes/remote-thread-list/types.d.ts +3 -0
  189. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  190. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  191. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  192. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  193. package/dist/store/clients/external-thread.d.ts.map +1 -1
  194. package/dist/store/clients/external-thread.js +75 -100
  195. package/dist/store/clients/external-thread.js.map +1 -1
  196. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  197. package/dist/store/clients/single-thread-list.js +1 -1
  198. package/dist/store/clients/single-thread-list.js.map +1 -1
  199. package/dist/store/clients/suggestions.d.ts.map +1 -1
  200. package/dist/store/clients/suggestions.js +16 -7
  201. package/dist/store/clients/suggestions.js.map +1 -1
  202. package/dist/store/index.d.ts +4 -2
  203. package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
  204. package/dist/store/runtime-clients/handle-runtime-action.js +2 -0
  205. package/dist/store/runtime-clients/handle-runtime-action.js.map +1 -1
  206. package/dist/store/scope-registration.d.ts +1 -1
  207. package/dist/store/scopes/composer.d.ts +2 -1
  208. package/dist/store/scopes/composer.d.ts.map +1 -1
  209. package/dist/store/scopes/message.d.ts +1 -1
  210. package/dist/store/scopes/queue-item.d.ts +2 -9
  211. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  212. package/dist/store/scopes/queue-item.js +0 -6
  213. package/dist/types/message.d.ts +8 -4
  214. package/dist/types/message.d.ts.map +1 -1
  215. package/dist/types/message.js.map +1 -1
  216. package/dist/utils/data-url.d.ts +14 -1
  217. package/dist/utils/data-url.d.ts.map +1 -1
  218. package/dist/utils/data-url.js +13 -1
  219. package/dist/utils/data-url.js.map +1 -1
  220. package/dist/utils/invoke-user-callback.d.ts +5 -0
  221. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  222. package/dist/utils/invoke-user-callback.js +19 -0
  223. package/dist/utils/invoke-user-callback.js.map +1 -0
  224. package/dist/utils/normalizePartStatus.js.map +1 -1
  225. package/dist/utils/silent-runtime-action.d.ts +6 -0
  226. package/dist/utils/silent-runtime-action.d.ts.map +1 -0
  227. package/dist/utils/silent-runtime-action.js +7 -0
  228. package/dist/utils/silent-runtime-action.js.map +1 -0
  229. package/package.json +10 -10
  230. package/src/adapters/voice.test.ts +66 -1
  231. package/src/adapters/voice.ts +8 -0
  232. package/src/internal.ts +13 -0
  233. package/src/model-context/frame/host.test.ts +152 -9
  234. package/src/model-context/frame/host.ts +23 -5
  235. package/src/model-context/frame/provider.test.ts +601 -19
  236. package/src/model-context/frame/provider.ts +158 -20
  237. package/src/model-context/frame/types.ts +5 -0
  238. package/src/model-context/interactable-composer-metadata.ts +1 -1
  239. package/src/model-context/registry.test.ts +44 -0
  240. package/src/model-context/registry.ts +13 -35
  241. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  242. package/src/react/client/InMemoryThreadList.ts +7 -1
  243. package/src/react/client/Interactables.ts +19 -178
  244. package/src/react/client/RemoteThreadList.test.ts +292 -0
  245. package/src/react/client/RemoteThreadList.ts +119 -82
  246. package/src/react/interactables-legacy/Interactables.ts +20 -177
  247. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  248. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  249. package/src/react/primitive-hooks/useActionBarCopy.test.ts +15 -0
  250. package/src/react/primitive-hooks/useActionBarCopy.ts +13 -3
  251. package/src/react/primitive-hooks/useThreadIsEmpty.ts +3 -0
  252. package/src/react/primitive-hooks/useThreadIsRunning.ts +3 -0
  253. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +12 -0
  254. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.republish.test.ts +87 -0
  255. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +251 -76
  256. package/src/react/runtimes/RemoteThreadResource.ts +2 -0
  257. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  258. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +58 -0
  259. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.ts +3 -1
  260. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  261. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  262. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  263. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  264. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +98 -3
  265. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +68 -6
  266. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  267. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  268. package/src/react/runtimes/external-message-converter.ts +58 -481
  269. package/src/react/runtimes/useLocalRuntime.test.tsx +5 -5
  270. package/src/runtime/api/composer-runtime.ts +1 -1
  271. package/src/runtime/api/message-runtime.test.ts +28 -1
  272. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  273. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  274. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  275. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  276. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  277. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  278. package/src/runtime/internal.ts +5 -0
  279. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  280. package/src/runtime/queue/message-queue.ts +1 -4
  281. package/src/runtime/queue/queue-item.ts +10 -0
  282. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  283. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  284. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  285. package/src/runtime/utils/auto-status.ts +33 -0
  286. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  287. package/src/runtime/utils/external-message-conversion.ts +494 -0
  288. package/src/runtime/utils/external-store-message.ts +2 -0
  289. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  290. package/src/runtime/utils/message-repository-session.ts +176 -0
  291. package/src/runtime/utils/message-repository.ts +33 -22
  292. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  293. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  294. package/src/runtime/utils/thread-message-like.ts +2 -0
  295. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  296. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +179 -28
  297. package/src/runtimes/inert/InertThreadRuntimeCore.test.ts +253 -0
  298. package/src/runtimes/inert/InertThreadRuntimeCore.ts +232 -0
  299. package/src/runtimes/local/local-thread-runtime-core.test.ts +491 -1
  300. package/src/runtimes/local/local-thread-runtime-core.ts +86 -11
  301. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +17 -209
  302. package/src/runtimes/remote-thread-list/adapter-changed.ts +10 -0
  303. package/src/runtimes/remote-thread-list/empty-thread-core.ts +28 -221
  304. package/src/runtimes/remote-thread-list/optimistic-state.test.ts +18 -0
  305. package/src/runtimes/remote-thread-list/optimistic-state.ts +13 -0
  306. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  307. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  308. package/src/runtimes/remote-thread-list/types.ts +4 -0
  309. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  310. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  311. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  312. package/src/store/clients/external-thread.ts +17 -62
  313. package/src/store/clients/single-thread-list.ts +4 -1
  314. package/src/store/clients/suggestions.test.ts +31 -0
  315. package/src/store/clients/suggestions.ts +18 -19
  316. package/src/store/runtime-clients/handle-runtime-action.test.ts +40 -0
  317. package/src/store/runtime-clients/handle-runtime-action.ts +3 -0
  318. package/src/store/scopes/message.ts +1 -1
  319. package/src/store/scopes/queue-item.ts +2 -9
  320. package/src/tests/MessageRepository.test.ts +125 -0
  321. package/src/tests/RemoteThreadListThreadListRuntimeCore-errors.test.ts +2 -2
  322. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  323. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +8 -6
  324. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  325. package/src/tests/empty-thread-core.test.ts +6 -0
  326. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +184 -0
  327. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  328. package/src/tests/external-thread-attachments.test.tsx +173 -0
  329. package/src/tests/external-thread-isEmpty.test.tsx +100 -0
  330. package/src/tests/remote-thread-list-adapter-switch.test.ts +383 -0
  331. package/src/tests/single-thread-list-item.test.tsx +47 -0
  332. package/src/tests/thread-message-like.test.ts +39 -1
  333. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +67 -0
  334. package/src/types/message.ts +9 -4
  335. package/src/utils/data-url.test.ts +52 -1
  336. package/src/utils/data-url.ts +21 -0
  337. package/src/utils/invoke-user-callback.test.ts +82 -0
  338. package/src/utils/invoke-user-callback.ts +31 -0
  339. package/src/utils/normalizePartStatus.ts +1 -1
  340. package/src/utils/silent-runtime-action.ts +6 -0
  341. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -0,0 +1,494 @@
1
+ import type { ReadonlyJSONValue } from "assistant-stream/utils";
2
+ import type { ToolExecutionStatus } from "../../runtimes/tool-invocations/ToolInvocationTracker";
3
+ import type {
4
+ ThreadAssistantMessage,
5
+ ThreadMessage,
6
+ ToolCallMessagePart,
7
+ } from "../../types/message";
8
+ import type { MessageTiming } from "../../types/message";
9
+ import { generateErrorMessageId } from "../../utils/id";
10
+ import { isJSONValueEqual } from "../../utils/json/is-json-equal";
11
+ import {
12
+ getAutoStatus,
13
+ isAutoStatus,
14
+ isInterruptedToolCall,
15
+ isPendingToolCall,
16
+ } from "./auto-status";
17
+ import {
18
+ bindExternalStoreMessage,
19
+ FALLBACK_ID_PREFIX,
20
+ getExternalStoreMessages,
21
+ symbolInnerMessage,
22
+ } from "./external-store-message";
23
+ import {
24
+ fromThreadMessageLike,
25
+ type ThreadMessageLike,
26
+ } from "./thread-message-like";
27
+
28
+ export type JoinStrategy = "concat-content" | "none";
29
+
30
+ export type ExternalMessageConverterMessage =
31
+ | (ThreadMessageLike & {
32
+ readonly convertConfig?: {
33
+ readonly joinStrategy?: JoinStrategy;
34
+ };
35
+ })
36
+ | {
37
+ role: "tool";
38
+ toolCallId: string;
39
+ toolName?: string | undefined;
40
+ result: any;
41
+ artifact?: any;
42
+ isError?: boolean;
43
+ messages?: readonly ThreadMessage[];
44
+ };
45
+
46
+ export type ExternalMessageConverterMetadata = {
47
+ readonly toolStatuses?: Record<string, ToolExecutionStatus>;
48
+ readonly error?: ReadonlyJSONValue;
49
+ readonly messageTiming?: Record<string, MessageTiming>;
50
+ };
51
+
52
+ export type ExternalMessageConverterCallback<T> = (
53
+ message: T,
54
+ metadata: ExternalMessageConverterMetadata,
55
+ ) => ExternalMessageConverterMessage | ExternalMessageConverterMessage[];
56
+
57
+ export type ExternalMessageConverterCallbackResult<T> = {
58
+ input: T;
59
+ outputs: ExternalMessageConverterMessage[];
60
+ };
61
+
62
+ export type ExternalMessageConverterChunk<T> = {
63
+ inputs: T[];
64
+ outputs: ExternalMessageConverterMessage[];
65
+ };
66
+
67
+ type Mutable<T> = {
68
+ -readonly [P in keyof T]: T[P];
69
+ };
70
+
71
+ const stringifyForError = (value: unknown) => {
72
+ let text;
73
+ try {
74
+ text = value instanceof Error ? String(value) : JSON.stringify(value);
75
+ } catch {
76
+ /* circular */
77
+ }
78
+ text ??= String(value);
79
+ return text.length > 200 ? `${text.slice(0, 200)}…` : text;
80
+ };
81
+
82
+ const toCallbackOutputs = (
83
+ output: ExternalMessageConverterMessage | ExternalMessageConverterMessage[],
84
+ input: unknown,
85
+ ): ExternalMessageConverterMessage[] => {
86
+ const outputs = Array.isArray(output) ? output : [output];
87
+ for (const o of outputs) {
88
+ const valid =
89
+ typeof o === "object" &&
90
+ o !== null &&
91
+ (o.role === "tool" ||
92
+ ((o.role === "assistant" || o.role === "user" || o.role === "system") &&
93
+ (typeof o.content === "string" || Array.isArray(o.content))));
94
+ if (!valid)
95
+ throw new Error(
96
+ `External message converter: the converter callback returned an invalid message (${stringifyForError(o)}) for input ${stringifyForError(input)}. Return an empty array to skip a message.`,
97
+ );
98
+ }
99
+ // Providers can emit tool results without a usable id (react-langchain maps
100
+ // a missing tool_call_id to ""); converters must not throw on those, so the
101
+ // result is dropped like any other orphaned tool output.
102
+ return outputs.filter((o) => {
103
+ if (o.role !== "tool") return true;
104
+ if (typeof o.toolCallId === "string" && o.toolCallId.length > 0)
105
+ return true;
106
+ if (!warnedDroppedToolResults.has(o)) {
107
+ warnedDroppedToolResults.add(o);
108
+ console.warn(
109
+ `External message converter: dropping a tool result without a toolCallId (${stringifyForError(o)}) for input ${stringifyForError(input)}.`,
110
+ );
111
+ }
112
+ return false;
113
+ });
114
+ };
115
+
116
+ const warnedDroppedToolResults = new WeakSet<object>();
117
+
118
+ export const convertExternalMessageCallback = <T>(
119
+ input: T,
120
+ callback: ExternalMessageConverterCallback<T>,
121
+ metadata: ExternalMessageConverterMetadata,
122
+ ): ExternalMessageConverterCallbackResult<T> => ({
123
+ input,
124
+ outputs: toCallbackOutputs(callback(input, metadata), input),
125
+ });
126
+
127
+ const mergeInnerMessages = (existing: object, incoming: object) => ({
128
+ [symbolInnerMessage]: [
129
+ ...((existing as any)[symbolInnerMessage] ?? []),
130
+ ...((incoming as any)[symbolInnerMessage] ?? []),
131
+ ],
132
+ });
133
+
134
+ export const joinExternalMessages = (
135
+ messages: readonly ExternalMessageConverterMessage[],
136
+ ): ThreadMessageLike => {
137
+ const assistantMessage: Mutable<Omit<ThreadMessageLike, "metadata">> & {
138
+ content: Exclude<ThreadMessageLike["content"][0], string>[];
139
+ metadata?: Mutable<ThreadMessageLike["metadata"]>;
140
+ } = {
141
+ role: "assistant",
142
+ content: [],
143
+ };
144
+ for (const output of messages) {
145
+ if (output.role === "tool") {
146
+ const toolCallIdx = assistantMessage.content.findIndex(
147
+ (c) => c.type === "tool-call" && c.toolCallId === output.toolCallId,
148
+ );
149
+ // Ignore orphaned tool results so one bad tool message does not
150
+ // prevent rendering the rest of the conversation.
151
+ if (toolCallIdx !== -1) {
152
+ const toolCall = assistantMessage.content[
153
+ toolCallIdx
154
+ ]! as ToolCallMessagePart;
155
+ if (output.toolName != null) {
156
+ if (toolCall.toolName !== output.toolName)
157
+ throw new Error(
158
+ `Tool call name ${output.toolCallId} ${output.toolName} does not match existing tool call ${toolCall.toolName}`,
159
+ );
160
+ }
161
+ assistantMessage.content[toolCallIdx] = {
162
+ ...toolCall,
163
+ ...{
164
+ [symbolInnerMessage]: [
165
+ ...((toolCall as any)[symbolInnerMessage] ?? []),
166
+ output,
167
+ ],
168
+ },
169
+ result: output.result,
170
+ artifact: output.artifact,
171
+ isError: output.isError,
172
+ messages: output.messages,
173
+ };
174
+ }
175
+ } else {
176
+ const role = output.role;
177
+ const content = (
178
+ typeof output.content === "string"
179
+ ? [{ type: "text" as const, text: output.content }]
180
+ : output.content
181
+ ).map((c) => ({
182
+ ...c,
183
+ ...{ [symbolInnerMessage]: [output] },
184
+ }));
185
+ switch (role) {
186
+ case "system":
187
+ case "user":
188
+ return {
189
+ ...output,
190
+ content,
191
+ };
192
+ case "assistant":
193
+ if (assistantMessage.content.length === 0) {
194
+ assistantMessage.id = output.id;
195
+ assistantMessage.createdAt ??= output.createdAt;
196
+ assistantMessage.status ??= output.status;
197
+
198
+ if (output.attachments) {
199
+ assistantMessage.attachments = [
200
+ ...(assistantMessage.attachments ?? []),
201
+ ...output.attachments,
202
+ ];
203
+ }
204
+ }
205
+
206
+ if (output.metadata) {
207
+ assistantMessage.metadata ??= {};
208
+ if (output.metadata.unstable_state !== undefined) {
209
+ assistantMessage.metadata.unstable_state =
210
+ output.metadata.unstable_state;
211
+ }
212
+ if (output.metadata.unstable_annotations) {
213
+ assistantMessage.metadata.unstable_annotations = [
214
+ ...(assistantMessage.metadata.unstable_annotations ?? []),
215
+ ...output.metadata.unstable_annotations,
216
+ ];
217
+ }
218
+ if (output.metadata.unstable_data) {
219
+ assistantMessage.metadata.unstable_data = [
220
+ ...(assistantMessage.metadata.unstable_data ?? []),
221
+ ...output.metadata.unstable_data,
222
+ ];
223
+ }
224
+ if (output.metadata.steps) {
225
+ assistantMessage.metadata.steps = [
226
+ ...(assistantMessage.metadata.steps ?? []),
227
+ ...output.metadata.steps,
228
+ ];
229
+ }
230
+ if (output.metadata.custom) {
231
+ assistantMessage.metadata.custom = {
232
+ ...(assistantMessage.metadata.custom ?? {}),
233
+ ...output.metadata.custom,
234
+ };
235
+ }
236
+
237
+ if (output.metadata.timing) {
238
+ assistantMessage.metadata.timing = output.metadata.timing;
239
+ }
240
+
241
+ if (output.metadata.submittedFeedback) {
242
+ assistantMessage.metadata.submittedFeedback =
243
+ output.metadata.submittedFeedback;
244
+ }
245
+
246
+ if (output.metadata.isOptimistic) {
247
+ assistantMessage.metadata.isOptimistic = true;
248
+ }
249
+ // TODO keep this in sync with ThreadMessageLike["metadata"] / fromThreadMessageLike
250
+ }
251
+
252
+ // Add content parts, merging reasoning parts with same parentId
253
+ for (const part of content) {
254
+ if (part.type === "tool-call") {
255
+ const existingIdx = assistantMessage.content.findIndex(
256
+ (c) =>
257
+ c.type === "tool-call" && c.toolCallId === part.toolCallId,
258
+ );
259
+ if (existingIdx !== -1) {
260
+ const existing = assistantMessage.content[
261
+ existingIdx
262
+ ] as typeof part;
263
+ assistantMessage.content[existingIdx] = {
264
+ ...existing,
265
+ ...part,
266
+ ...mergeInnerMessages(existing, part),
267
+ };
268
+ continue;
269
+ }
270
+ }
271
+
272
+ if (
273
+ part.type === "reasoning" &&
274
+ "parentId" in part &&
275
+ part.parentId
276
+ ) {
277
+ const existingIdx = assistantMessage.content.findIndex(
278
+ (c) =>
279
+ c.type === "reasoning" &&
280
+ "parentId" in c &&
281
+ c.parentId === part.parentId,
282
+ );
283
+ if (existingIdx !== -1) {
284
+ const existing = assistantMessage.content[
285
+ existingIdx
286
+ ] as typeof part;
287
+ assistantMessage.content[existingIdx] = {
288
+ ...existing,
289
+ text: `${existing.text}\n\n${part.text}`,
290
+ ...mergeInnerMessages(existing, part),
291
+ };
292
+ continue;
293
+ }
294
+ }
295
+ assistantMessage.content.push(part);
296
+ }
297
+ break;
298
+ default: {
299
+ const unsupportedRole: never = role;
300
+ throw new Error(`Unknown message role: ${unsupportedRole}`);
301
+ }
302
+ }
303
+ }
304
+ }
305
+ return assistantMessage;
306
+ };
307
+
308
+ export const chunkExternalMessages = <T>(
309
+ callbackResults: ExternalMessageConverterCallbackResult<T>[],
310
+ joinStrategy?: JoinStrategy,
311
+ ) => {
312
+ const results: ExternalMessageConverterChunk<T>[] = [];
313
+ let isAssistant = false;
314
+ let pendingNone = false; // true if the previous assistant message had joinStrategy "none"
315
+ let inputs: T[] = [];
316
+ let outputs: ExternalMessageConverterMessage[] = [];
317
+
318
+ const flush = () => {
319
+ if (outputs.length) {
320
+ results.push({
321
+ inputs,
322
+ outputs,
323
+ });
324
+ }
325
+ inputs = [];
326
+ outputs = [];
327
+ isAssistant = false;
328
+ pendingNone = false;
329
+ };
330
+
331
+ for (const callbackResult of callbackResults) {
332
+ for (const output of callbackResult.outputs) {
333
+ if (
334
+ (pendingNone && output.role !== "tool") ||
335
+ !isAssistant ||
336
+ output.role === "user" ||
337
+ output.role === "system"
338
+ ) {
339
+ flush();
340
+ }
341
+ isAssistant = output.role === "assistant" || output.role === "tool";
342
+
343
+ if (inputs.at(-1) !== callbackResult.input) {
344
+ inputs.push(callbackResult.input);
345
+ }
346
+ outputs.push(output);
347
+
348
+ if (
349
+ output.role === "assistant" &&
350
+ (output.convertConfig?.joinStrategy === "none" ||
351
+ joinStrategy === "none")
352
+ ) {
353
+ pendingNone = true;
354
+ }
355
+ }
356
+ }
357
+ flush();
358
+ return results;
359
+ };
360
+
361
+ export const shallowArrayEqual = (a: unknown[], b: unknown[]) => {
362
+ if (a.length !== b.length) return false;
363
+ for (let i = 0; i < a.length; i++) {
364
+ if (a[i] !== b[i]) return false;
365
+ }
366
+ return true;
367
+ };
368
+
369
+ type ExternalMessageConversionCache = {
370
+ message: ThreadMessage | undefined;
371
+ generatedFallbackMessages: WeakSet<object>;
372
+ };
373
+
374
+ export const convertExternalMessageChunk = <T>(
375
+ message: ExternalMessageConverterChunk<T>,
376
+ idx: number,
377
+ chunkCount: number,
378
+ isRunning: boolean,
379
+ error: ReadonlyJSONValue | undefined,
380
+ cache?: ExternalMessageConversionCache,
381
+ ) => {
382
+ const isLast = idx === chunkCount - 1;
383
+ const joined = joinExternalMessages(message.outputs);
384
+ const hasInterruptedToolCalls =
385
+ typeof joined.content === "object" &&
386
+ joined.content.some(isInterruptedToolCall);
387
+ const hasPendingToolCalls =
388
+ typeof joined.content === "object" &&
389
+ joined.content.some(isPendingToolCall);
390
+ const autoStatus = getAutoStatus(
391
+ isLast,
392
+ isRunning,
393
+ hasInterruptedToolCalls,
394
+ hasPendingToolCalls,
395
+ isLast ? error : undefined,
396
+ );
397
+ const fallbackId = `${FALLBACK_ID_PREFIX}${idx}`;
398
+
399
+ const cachedMessage = cache?.message;
400
+ if (
401
+ cachedMessage &&
402
+ (cachedMessage.role !== "assistant" ||
403
+ !isAutoStatus(cachedMessage.status) ||
404
+ cachedMessage.status === autoStatus ||
405
+ (cachedMessage.status.type === "incomplete" &&
406
+ cachedMessage.status.reason === "error" &&
407
+ cachedMessage.status.error !== undefined &&
408
+ autoStatus.type === "incomplete" &&
409
+ autoStatus.reason === "error" &&
410
+ autoStatus.error !== undefined &&
411
+ (cachedMessage.status.error === autoStatus.error ||
412
+ isJSONValueEqual(cachedMessage.status.error, autoStatus.error))))
413
+ ) {
414
+ const inputs = getExternalStoreMessages<T>(cachedMessage);
415
+ if (shallowArrayEqual(inputs, message.inputs)) {
416
+ // A positional fallback id goes stale when messages are prepended
417
+ // or reordered; serving it unchanged makes two messages collide on
418
+ // one id and the dedup downstream drops one of them.
419
+ if (
420
+ cache.generatedFallbackMessages.has(cachedMessage) &&
421
+ cachedMessage.id !== fallbackId
422
+ ) {
423
+ const updated = { ...cachedMessage, id: fallbackId };
424
+ cache.generatedFallbackMessages.add(updated);
425
+ bindExternalStoreMessage(updated, message.inputs);
426
+ return updated;
427
+ }
428
+ return cachedMessage;
429
+ }
430
+ }
431
+
432
+ const newMessage = fromThreadMessageLike(joined, fallbackId, autoStatus);
433
+ if (cache && joined.id == null) {
434
+ cache.generatedFallbackMessages.add(newMessage);
435
+ }
436
+ bindExternalStoreMessage(newMessage, message.inputs);
437
+ return newMessage;
438
+ };
439
+
440
+ function createErrorAssistantMessage(
441
+ error: ReadonlyJSONValue,
442
+ ): ThreadAssistantMessage {
443
+ const msg: ThreadAssistantMessage = {
444
+ id: generateErrorMessageId(),
445
+ role: "assistant",
446
+ content: [],
447
+ status: { type: "incomplete", reason: "error", error },
448
+ createdAt: new Date(),
449
+ metadata: {
450
+ unstable_state: null,
451
+ unstable_annotations: [],
452
+ unstable_data: [],
453
+ custom: {},
454
+ steps: [],
455
+ },
456
+ };
457
+ bindExternalStoreMessage(msg, []);
458
+ return msg;
459
+ }
460
+
461
+ export const completeExternalMessageConversion = (
462
+ messages: ThreadMessage[],
463
+ error: ReadonlyJSONValue | undefined,
464
+ ) => {
465
+ if (error) {
466
+ const lastMessage = messages.at(-1);
467
+ if (!lastMessage || lastMessage.role !== "assistant") {
468
+ messages.push(createErrorAssistantMessage(error));
469
+ }
470
+ }
471
+ return messages;
472
+ };
473
+
474
+ export const convertExternalMessages = <T extends WeakKey>(
475
+ messages: T[],
476
+ callback: ExternalMessageConverterCallback<T>,
477
+ isRunning: boolean,
478
+ metadata: ExternalMessageConverterMetadata,
479
+ ) => {
480
+ const callbackResults = messages.map((message) =>
481
+ convertExternalMessageCallback(message, callback, metadata),
482
+ );
483
+ const chunks = chunkExternalMessages(callbackResults);
484
+ const result = chunks.map((message, idx) =>
485
+ convertExternalMessageChunk(
486
+ message,
487
+ idx,
488
+ chunks.length,
489
+ isRunning,
490
+ metadata.error,
491
+ ),
492
+ );
493
+ return completeExternalMessageConversion(result, metadata.error);
494
+ };
@@ -42,3 +42,5 @@ export const getExternalStoreMessages = <T>(
42
42
  container[symbolInnerMessages] = [value];
43
43
  return container[symbolInnerMessages];
44
44
  };
45
+
46
+ export const FALLBACK_ID_PREFIX = "__external_store_fallback_";
@@ -0,0 +1,195 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ThreadUserMessage } from "../../types/message";
3
+ import { createMessageRepositorySession } from "./message-repository-session";
4
+
5
+ const message = (id: string, text = id): ThreadUserMessage => ({
6
+ id,
7
+ role: "user",
8
+ createdAt: new Date(0),
9
+ content: [{ type: "text", text }],
10
+ attachments: [],
11
+ metadata: { custom: {} },
12
+ });
13
+
14
+ const withRootAndChild = () => {
15
+ const session = createMessageRepositorySession();
16
+ session.addOrUpdateMessage(null, message("root"));
17
+ session.addOrUpdateMessage("root", message("child"));
18
+ return session;
19
+ };
20
+
21
+ describe("createMessageRepositorySession", () => {
22
+ it("memoizes exports and invalidates them after every mutator", () => {
23
+ const cases = [
24
+ () => {
25
+ const session = createMessageRepositorySession();
26
+ return {
27
+ session,
28
+ mutate: () => session.addOrUpdateMessage(null, message("root")),
29
+ };
30
+ },
31
+ () => {
32
+ const session = createMessageRepositorySession();
33
+ session.addOrUpdateMessage(null, message("root"));
34
+ session.addOrUpdateMessage("root", message("child"));
35
+ return { session, mutate: () => session.deleteMessage("child") };
36
+ },
37
+ () => {
38
+ const session = createMessageRepositorySession();
39
+ session.addOrUpdateMessage(null, message("root"));
40
+ session.addOrUpdateMessage("root", message("child"));
41
+ return {
42
+ session,
43
+ mutate: () => expect(session.tryDeleteMessage("child")).toBe(true),
44
+ };
45
+ },
46
+ () => {
47
+ const session = createMessageRepositorySession();
48
+ session.addOrUpdateMessage(null, message("root"));
49
+ session.addOrUpdateMessage("root", message("first"));
50
+ session.addOrUpdateMessage("root", message("second"));
51
+ return { session, mutate: () => session.switchToBranch("first") };
52
+ },
53
+ () => {
54
+ const session = createMessageRepositorySession();
55
+ session.addOrUpdateMessage(null, message("root"));
56
+ session.addOrUpdateMessage("root", message("child"));
57
+ return { session, mutate: () => session.resetHead("root") };
58
+ },
59
+ () => {
60
+ const session = createMessageRepositorySession();
61
+ session.addOrUpdateMessage(null, message("root"));
62
+ return { session, mutate: () => session.clear() };
63
+ },
64
+ () => {
65
+ const session = createMessageRepositorySession();
66
+ session.addOrUpdateMessage(null, message("root"));
67
+ return {
68
+ session,
69
+ mutate: () =>
70
+ expect(
71
+ session.updateMessage("root", (current) => ({
72
+ ...current,
73
+ content: [{ type: "text", text: "updated" }],
74
+ })),
75
+ ).toBe(true),
76
+ };
77
+ },
78
+ () => {
79
+ const session = createMessageRepositorySession();
80
+ session.addOrUpdateMessage(null, message("previous"));
81
+ return {
82
+ session,
83
+ mutate: () =>
84
+ session.applyExternalMessageRepository({
85
+ headId: "root",
86
+ messages: [{ parentId: null, message: message("root") }],
87
+ }),
88
+ };
89
+ },
90
+ ];
91
+
92
+ for (const createCase of cases) {
93
+ const { session, mutate } = createCase();
94
+ const exported = session.export();
95
+ expect(session.export()).toBe(exported);
96
+
97
+ mutate();
98
+
99
+ expect(session.export()).not.toBe(exported);
100
+ }
101
+ });
102
+
103
+ it("decorates and memoizes exports", () => {
104
+ let decorations = 0;
105
+ const session = createMessageRepositorySession({
106
+ decorateExport: (exported) => {
107
+ decorations++;
108
+ return { ...exported, headId: "decorated" };
109
+ },
110
+ });
111
+ session.addOrUpdateMessage(null, message("root"));
112
+
113
+ const exported = session.export();
114
+
115
+ expect(exported.headId).toBe("decorated");
116
+ expect(session.export()).toBe(exported);
117
+ expect(decorations).toBe(1);
118
+
119
+ session.addOrUpdateMessage("root", message("child"));
120
+
121
+ expect(session.export()).not.toBe(exported);
122
+ expect(decorations).toBe(2);
123
+ });
124
+
125
+ it("preserves the export cache when an update returns the current message", () => {
126
+ const session = withRootAndChild();
127
+ const exported = session.export();
128
+
129
+ expect(session.updateMessage("child", (current) => current)).toBe(false);
130
+ expect(session.export()).toBe(exported);
131
+ });
132
+
133
+ it("imports out-of-order parent-child pairs topologically", () => {
134
+ const session = createMessageRepositorySession();
135
+
136
+ session.applyExternalMessageRepository({
137
+ headId: "child",
138
+ messages: [
139
+ { parentId: "root", message: message("child") },
140
+ { parentId: null, message: message("root") },
141
+ ],
142
+ });
143
+
144
+ expect(session.getMessages().map((item) => item.id)).toEqual([
145
+ "root",
146
+ "child",
147
+ ]);
148
+ });
149
+
150
+ it("uses the degenerate linear path for duplicate message ids", () => {
151
+ const session = createMessageRepositorySession();
152
+
153
+ session.applyExternalMessageRepository({
154
+ headId: "root",
155
+ messages: [
156
+ { parentId: null, message: message("root", "first") },
157
+ { parentId: "root", message: message("child") },
158
+ { parentId: "child", message: message("root", "replacement") },
159
+ ],
160
+ });
161
+
162
+ expect(session.tryGetMessage("root")?.message.content).toEqual([
163
+ { type: "text", text: "replacement" },
164
+ ]);
165
+ });
166
+
167
+ it("uses the degenerate linear path when the head is missing", () => {
168
+ const session = createMessageRepositorySession();
169
+
170
+ session.applyExternalMessageRepository({
171
+ headId: "missing",
172
+ messages: [
173
+ { parentId: null, message: message("root") },
174
+ { parentId: null, message: message("child") },
175
+ ],
176
+ });
177
+
178
+ expect(session.tryGetMessage("child")?.parentId).toBe("root");
179
+ });
180
+
181
+ it("preserves an already-imported message parent in the degenerate path", () => {
182
+ const session = createMessageRepositorySession();
183
+
184
+ session.applyExternalMessageRepository({
185
+ messages: [
186
+ { parentId: null, message: message("root") },
187
+ { parentId: "root", message: message("branch", "first") },
188
+ { parentId: "branch", message: message("child") },
189
+ { parentId: "child", message: message("branch", "replacement") },
190
+ ],
191
+ });
192
+
193
+ expect(session.tryGetMessage("branch")?.parentId).toBe("root");
194
+ });
195
+ });