@assistant-ui/core 0.2.21 → 0.2.23

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 (345) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/speech.d.ts.map +1 -1
  3. package/dist/adapters/speech.js +9 -2
  4. package/dist/adapters/speech.js.map +1 -1
  5. package/dist/adapters/thread-history.d.ts +8 -0
  6. package/dist/adapters/thread-history.d.ts.map +1 -1
  7. package/dist/adapters/voice.d.ts.map +1 -1
  8. package/dist/adapters/voice.js +13 -5
  9. package/dist/adapters/voice.js.map +1 -1
  10. package/dist/internal.d.ts +5 -2
  11. package/dist/internal.js +5 -2
  12. package/dist/model-context/frame/host.d.ts +1 -0
  13. package/dist/model-context/frame/host.d.ts.map +1 -1
  14. package/dist/model-context/frame/host.js +41 -21
  15. package/dist/model-context/frame/host.js.map +1 -1
  16. package/dist/model-context/frame/provider.d.ts.map +1 -1
  17. package/dist/model-context/frame/provider.js +1 -0
  18. package/dist/model-context/frame/provider.js.map +1 -1
  19. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  20. package/dist/model-context/types.d.ts.map +1 -1
  21. package/dist/model-context/types.js +13 -4
  22. package/dist/model-context/types.js.map +1 -1
  23. package/dist/react/AssistantProvider.js.map +1 -1
  24. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  25. package/dist/react/adapters/LocalStorageThreadListAdapter.js +78 -50
  26. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  27. package/dist/react/client/Interactables.js.map +1 -1
  28. package/dist/react/client/Tools.d.ts.map +1 -1
  29. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  30. package/dist/react/client/interactable-model-context.js +1 -0
  31. package/dist/react/client/interactable-model-context.js.map +1 -1
  32. package/dist/react/interactables-legacy/useInteractableState.js +1 -0
  33. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -1
  34. package/dist/react/model-context/useInteractable.js.map +1 -1
  35. package/dist/react/model-context/useInteractableState.js +1 -0
  36. package/dist/react/model-context/useInteractableState.js.map +1 -1
  37. package/dist/react/model-context/useInteractableVersions.js +1 -0
  38. package/dist/react/model-context/useInteractableVersions.js.map +1 -1
  39. package/dist/react/model-context/useToolArgsStatus.js +1 -0
  40. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  41. package/dist/react/primitive-hooks/useActionBarCopy.js +14 -13
  42. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  43. package/dist/react/primitive-hooks/useActionBarEdit.js +1 -0
  44. package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -1
  45. package/dist/react/primitive-hooks/useActionBarFeedback.js +1 -0
  46. package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -1
  47. package/dist/react/primitive-hooks/useActionBarReload.js +1 -0
  48. package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -1
  49. package/dist/react/primitive-hooks/useActionBarSpeak.js +1 -0
  50. package/dist/react/primitive-hooks/useActionBarSpeak.js.map +1 -1
  51. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +1 -0
  52. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js.map +1 -1
  53. package/dist/react/primitive-hooks/useBranchPickerNext.js +1 -0
  54. package/dist/react/primitive-hooks/useBranchPickerNext.js.map +1 -1
  55. package/dist/react/primitive-hooks/useBranchPickerPrevious.js +1 -0
  56. package/dist/react/primitive-hooks/useBranchPickerPrevious.js.map +1 -1
  57. package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts.map +1 -1
  58. package/dist/react/primitive-hooks/useComposerAddAttachment.js +1 -0
  59. package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -1
  60. package/dist/react/primitive-hooks/useComposerCancel.js +1 -0
  61. package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -1
  62. package/dist/react/primitive-hooks/useComposerDictate.js +1 -0
  63. package/dist/react/primitive-hooks/useComposerDictate.js.map +1 -1
  64. package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
  65. package/dist/react/primitive-hooks/useComposerSend.js +1 -0
  66. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
  67. package/dist/react/primitive-hooks/useEditComposerCancel.js +1 -0
  68. package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -1
  69. package/dist/react/primitive-hooks/useEditComposerSend.js +1 -0
  70. package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -1
  71. package/dist/react/primitive-hooks/useMessageBranching.js +1 -0
  72. package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -1
  73. package/dist/react/primitive-hooks/useMessageReload.js +1 -0
  74. package/dist/react/primitive-hooks/useMessageReload.js.map +1 -1
  75. package/dist/react/primitive-hooks/useSuggestionTrigger.js +1 -0
  76. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  77. package/dist/react/primitive-hooks/useThreadListItemArchive.js +1 -0
  78. package/dist/react/primitive-hooks/useThreadListItemArchive.js.map +1 -1
  79. package/dist/react/primitive-hooks/useThreadListItemDelete.js +1 -0
  80. package/dist/react/primitive-hooks/useThreadListItemDelete.js.map +1 -1
  81. package/dist/react/primitive-hooks/useThreadListItemTrigger.js +1 -0
  82. package/dist/react/primitive-hooks/useThreadListItemTrigger.js.map +1 -1
  83. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +1 -0
  84. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js.map +1 -1
  85. package/dist/react/primitive-hooks/useThreadListLoadMore.js +1 -0
  86. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  87. package/dist/react/primitive-hooks/useThreadListNew.js +1 -0
  88. package/dist/react/primitive-hooks/useThreadListNew.js.map +1 -1
  89. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.d.ts.map +1 -1
  90. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  91. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
  92. package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
  93. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  94. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
  95. package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
  96. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  97. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  98. package/dist/react/primitives/message/MessageParts.d.ts +2 -1
  99. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  100. package/dist/react/primitives/message/MessageParts.js +46 -36
  101. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  102. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  103. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  104. package/dist/react/primitives/thread/ThreadSuggestions.js.map +1 -1
  105. package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
  106. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
  107. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +18 -30
  108. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
  109. package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
  110. package/dist/react/providers/PartByIndexProvider.js +18 -30
  111. package/dist/react/providers/PartByIndexProvider.js.map +1 -1
  112. package/dist/react/providers/TextMessagePartProvider.js +1 -0
  113. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  114. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  115. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -0
  116. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  117. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +9 -2
  118. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  119. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +55 -12
  120. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  121. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +31 -1
  122. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  123. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +12 -2
  124. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  125. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -1
  126. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  127. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +3 -2
  128. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  129. package/dist/react/runtimes/cloud/auiV0.d.ts +12 -1
  130. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  131. package/dist/react/runtimes/cloud/auiV0.js +2 -1
  132. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  133. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +75 -73
  134. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  135. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  136. package/dist/react/runtimes/external-message-converter.js +2 -2
  137. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  138. package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -0
  139. package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -1
  140. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  141. package/dist/react/runtimes/useLocalRuntime.js +9 -1
  142. package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
  143. package/dist/runtime/api/assistant-runtime.d.ts +1 -1
  144. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  145. package/dist/runtime/api/assistant-runtime.js +1 -1
  146. package/dist/runtime/api/assistant-runtime.js.map +1 -1
  147. package/dist/runtime/api/attachment-runtime.d.ts +1 -1
  148. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  149. package/dist/runtime/api/attachment-runtime.js +1 -1
  150. package/dist/runtime/api/attachment-runtime.js.map +1 -1
  151. package/dist/runtime/api/composer-runtime.d.ts +2 -2
  152. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  153. package/dist/runtime/api/composer-runtime.js +2 -2
  154. package/dist/runtime/api/composer-runtime.js.map +1 -1
  155. package/dist/runtime/api/message-part-runtime.d.ts +4 -4
  156. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  157. package/dist/runtime/api/message-part-runtime.js +3 -3
  158. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  159. package/dist/runtime/api/message-runtime.d.ts +1 -1
  160. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  161. package/dist/runtime/api/message-runtime.js +6 -6
  162. package/dist/runtime/api/message-runtime.js.map +1 -1
  163. package/dist/runtime/api/thread-list-item-runtime.d.ts +1 -1
  164. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  165. package/dist/runtime/api/thread-list-item-runtime.js +4 -3
  166. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  167. package/dist/runtime/api/thread-list-runtime.d.ts +1 -1
  168. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  169. package/dist/runtime/api/thread-list-runtime.js +1 -1
  170. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  171. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  172. package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -1
  173. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  174. package/dist/runtime/base/base-composer-runtime-core.js +41 -14
  175. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  176. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -1
  177. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  178. package/dist/runtime/base/base-thread-runtime-core.js +3 -2
  179. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  180. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +2 -2
  181. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  182. package/dist/runtime/base/default-edit-composer-runtime-core.js +6 -4
  183. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  184. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  185. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  186. package/dist/runtime/base/default-thread-composer-runtime-core.js +3 -3
  187. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  188. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  189. package/dist/runtime/utils/message-repository.js +4 -2
  190. package/dist/runtime/utils/message-repository.js.map +1 -1
  191. package/dist/runtime/utils/thread-message-like.js +4 -1
  192. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  193. package/dist/runtimes/external-store/external-store-runtime-core.d.ts.map +1 -1
  194. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
  195. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  196. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +1 -1
  197. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  198. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  199. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +12 -0
  200. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  201. package/dist/runtimes/internal.d.ts +2 -1
  202. package/dist/runtimes/internal.js +2 -1
  203. package/dist/runtimes/local/local-runtime-core.d.ts.map +1 -1
  204. package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
  205. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -0
  206. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  207. package/dist/runtimes/local/local-thread-runtime-core.js +49 -15
  208. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  209. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  210. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +4 -4
  211. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  212. package/dist/runtimes/remote-thread-list/optimistic-state.js +22 -9
  213. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  214. package/dist/runtimes/remote-thread-list/types.d.ts +1 -1
  215. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +32 -27
  216. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  217. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +3 -2
  218. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  219. package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
  220. package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
  221. package/dist/store/clients/no-op-composer-client.js +1 -0
  222. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  223. package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
  224. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  225. package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
  226. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  227. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  228. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  229. package/dist/store/runtime-clients/thread-list-runtime-client.js +1 -0
  230. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  231. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  232. package/dist/store/runtime-clients/thread-runtime-client.js +17 -15
  233. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  234. package/dist/store/scopes/thread.d.ts +1 -0
  235. package/dist/store/scopes/thread.d.ts.map +1 -1
  236. package/dist/subscribable/subscribable.d.ts +4 -4
  237. package/dist/subscribable/subscribable.d.ts.map +1 -1
  238. package/dist/subscribable/subscribable.js +14 -6
  239. package/dist/subscribable/subscribable.js.map +1 -1
  240. package/dist/tests/remote-thread-list-test-helpers.d.ts +2 -1
  241. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  242. package/dist/tests/remote-thread-list-test-helpers.js +5 -2
  243. package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
  244. package/dist/types/attachment.d.ts +2 -1
  245. package/dist/types/attachment.d.ts.map +1 -1
  246. package/dist/types/attachment.js +6 -0
  247. package/dist/types/attachment.js.map +1 -0
  248. package/dist/types/message.d.ts +1 -0
  249. package/dist/types/message.d.ts.map +1 -1
  250. package/dist/types/message.js.map +1 -1
  251. package/dist/utils/data-url.d.ts +9 -0
  252. package/dist/utils/data-url.d.ts.map +1 -0
  253. package/dist/utils/data-url.js +14 -0
  254. package/dist/utils/data-url.js.map +1 -0
  255. package/dist/utils/notify-event-listeners.d.ts +6 -0
  256. package/dist/utils/notify-event-listeners.d.ts.map +1 -0
  257. package/dist/utils/notify-event-listeners.js +16 -0
  258. package/dist/utils/notify-event-listeners.js.map +1 -0
  259. package/package.json +10 -9
  260. package/src/adapters/speech.test.ts +88 -0
  261. package/src/adapters/speech.ts +17 -2
  262. package/src/adapters/thread-history.ts +8 -0
  263. package/src/adapters/voice.test.ts +130 -0
  264. package/src/adapters/voice.ts +26 -6
  265. package/src/internal.ts +6 -1
  266. package/src/model-context/frame/host.test.ts +147 -0
  267. package/src/model-context/frame/host.ts +56 -19
  268. package/src/model-context/frame/provider.test.ts +83 -0
  269. package/src/model-context/frame/provider.ts +1 -0
  270. package/src/model-context/types.ts +16 -6
  271. package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +82 -0
  272. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +108 -58
  273. package/src/react/primitive-hooks/useActionBarCopy.test.ts +79 -0
  274. package/src/react/primitive-hooks/useActionBarCopy.ts +4 -3
  275. package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -3
  276. package/src/react/primitives/message/MessageParts.tsx +48 -35
  277. package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +19 -2
  278. package/src/react/providers/PartByIndexProvider.tsx +16 -2
  279. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +18 -0
  280. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +1 -0
  281. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +68 -13
  282. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +34 -0
  283. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +69 -0
  284. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +27 -6
  285. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +16 -0
  286. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +7 -2
  287. package/src/react/runtimes/cloud/auiV0.ts +14 -0
  288. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +90 -88
  289. package/src/react/runtimes/external-message-converter.test.ts +346 -0
  290. package/src/react/runtimes/external-message-converter.test.tsx +21 -0
  291. package/src/react/runtimes/external-message-converter.ts +2 -2
  292. package/src/react/runtimes/useLocalRuntime.test.tsx +99 -0
  293. package/src/react/runtimes/useLocalRuntime.ts +13 -1
  294. package/src/runtime/api/assistant-runtime.ts +4 -1
  295. package/src/runtime/api/attachment-runtime.ts +8 -2
  296. package/src/runtime/api/composer-runtime.ts +9 -5
  297. package/src/runtime/api/message-part-runtime.ts +10 -3
  298. package/src/runtime/api/message-runtime.test.ts +81 -0
  299. package/src/runtime/api/message-runtime.ts +11 -8
  300. package/src/runtime/api/thread-list-item-runtime.ts +13 -3
  301. package/src/runtime/api/thread-list-runtime.ts +10 -2
  302. package/src/runtime/base/base-composer-runtime-core.ts +77 -31
  303. package/src/runtime/base/base-thread-runtime-core.ts +7 -2
  304. package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -4
  305. package/src/runtime/base/default-thread-composer-runtime-core.ts +13 -3
  306. package/src/runtime/utils/message-repository.ts +19 -13
  307. package/src/runtime/utils/thread-message-like.ts +1 -0
  308. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +4 -1
  309. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +15 -0
  310. package/src/runtimes/internal.ts +3 -0
  311. package/src/runtimes/local/local-thread-runtime-core.test.ts +746 -1
  312. package/src/runtimes/local/local-thread-runtime-core.ts +89 -23
  313. package/src/runtimes/local/should-continue.test.ts +122 -0
  314. package/src/runtimes/remote-thread-list/optimistic-state.test.ts +63 -0
  315. package/src/runtimes/remote-thread-list/optimistic-state.ts +49 -17
  316. package/src/runtimes/remote-thread-list/types.ts +1 -1
  317. package/src/runtimes/tool-invocations/EDGE_CASES.md +13 -19
  318. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +128 -8
  319. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +42 -34
  320. package/src/store/clients/model-context-client.test.ts +21 -1
  321. package/src/store/runtime-clients/thread-runtime-client.ts +2 -1
  322. package/src/store/scopes/thread.ts +1 -0
  323. package/src/subscribable/subscribable.test.ts +58 -0
  324. package/src/subscribable/subscribable.ts +29 -11
  325. package/src/tests/MessageRepository.test.ts +12 -0
  326. package/src/tests/OptimisticState-list-race.test.ts +33 -0
  327. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-order.test.ts +223 -0
  328. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-position.test.ts +121 -0
  329. package/src/tests/auiV0Encode.test.ts +74 -0
  330. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +44 -0
  331. package/src/tests/base-composer-runtime-core-send.test.ts +433 -5
  332. package/src/tests/base-composer-runtime-core.test.ts +422 -0
  333. package/src/tests/default-edit-composer-runtime-core.test.ts +43 -5
  334. package/src/tests/event-subscription-listener-errors.test.ts +205 -0
  335. package/src/tests/external-store-thread-list-runtime-core.test.ts +6 -6
  336. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +575 -0
  337. package/src/tests/external-store-thread-runtime-core.test.ts +55 -1
  338. package/src/tests/remote-thread-list-test-helpers.ts +14 -7
  339. package/src/tests/thread-message-like.test.ts +57 -0
  340. package/src/types/attachment.ts +5 -0
  341. package/src/types/message.ts +1 -0
  342. package/src/utils/data-url.test.ts +50 -0
  343. package/src/utils/data-url.ts +9 -0
  344. package/src/utils/notify-event-listeners.ts +30 -0
  345. package/src/tests/OptimisticState-delete-crash.test.ts +0 -208
@@ -3,7 +3,6 @@ import { describe, expect, it, vi } from "vitest";
3
3
  import {
4
4
  ToolInvocationTracker,
5
5
  type ToolExecutionStatus,
6
- type ToolInvocationTrackerSnapshot,
7
6
  } from "./ToolInvocationTracker";
8
7
  import type {
9
8
  ThreadAssistantMessage,
@@ -34,7 +33,7 @@ async function waitFor(
34
33
  const createState = (
35
34
  messages: ThreadAssistantMessage[],
36
35
  isRunning: boolean = true,
37
- ): ToolInvocationTrackerSnapshot => ({
36
+ ): ToolInvocationTracker.Snapshot => ({
38
37
  messages: messages as readonly ThreadMessage[],
39
38
  isRunning,
40
39
  });
@@ -152,6 +151,131 @@ describe("ToolInvocationTracker", () => {
152
151
  }
153
152
  });
154
153
 
154
+ it("does not auto-submit a parse-error result when divergent argsText closes without a backend result", async () => {
155
+ // A human-in-the-loop tool whose argsText diverges mid-stream and never
156
+ // re-converges, with no backend result at close time. The args stream must
157
+ // not close on the divergent snapshot (which holds a stale prefix the
158
+ // execution path would parse), so no bogus parse-error result is
159
+ // auto-submitted to resume the host graph and abandon the pending
160
+ // interrupt.
161
+ const execute = vi.fn(async () => ({ forecast: "ok" }));
162
+ const streamCall = vi.fn((_reader, { human }) => {
163
+ // Request human input immediately — sets up the pending interrupt.
164
+ void human({ request: "approve" });
165
+ });
166
+ const getTools = () => ({
167
+ weatherSearch: {
168
+ parameters: { type: "object", properties: {} },
169
+ execute,
170
+ streamCall,
171
+ } satisfies Tool,
172
+ });
173
+ const onResult = vi.fn();
174
+ let statuses: Record<string, ToolExecutionStatus> = {};
175
+ const onStatusesChange = (s: ReadonlyMap<string, ToolExecutionStatus>) => {
176
+ statuses = Object.fromEntries(s);
177
+ };
178
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
179
+
180
+ try {
181
+ const tracker = new ToolInvocationTracker(getTools, {
182
+ onResult,
183
+ onStatusesChange,
184
+ });
185
+ tracker.setState(createState([]));
186
+
187
+ tracker.setState(
188
+ createState([
189
+ createAssistantMessage('{"query":"London","longitude":0', {
190
+ query: "London",
191
+ longitude: 0,
192
+ }),
193
+ ]),
194
+ );
195
+
196
+ // The pending interrupt is set up via streamCall → human().
197
+ await waitFor(() => {
198
+ expect(statuses["tool-1"]?.type).toBe("interrupt");
199
+ });
200
+
201
+ // Divergent regression (not a prefix of the streamed text).
202
+ tracker.setState(
203
+ createState([
204
+ createAssistantMessage('{"query":"London","longitude":-0.125', {
205
+ query: "London",
206
+ longitude: -0.125,
207
+ }),
208
+ ]),
209
+ );
210
+
211
+ // Complete valid JSON, divergent from the streamed prefix, no backend
212
+ // result. Previously this closed the args stream on the snapshot's
213
+ // completeness, parsed the stale prefix, and auto-submitted an error.
214
+ tracker.setState(
215
+ createState([
216
+ createAssistantMessage(
217
+ '{"query":"London","longitude":-0.125,"latitude":51.5072}',
218
+ { query: "London", longitude: -0.125, latitude: 51.5072 },
219
+ ),
220
+ ]),
221
+ );
222
+
223
+ for (let i = 0; i < 5; i++) {
224
+ await new Promise((r) => setTimeout(r, 0));
225
+ }
226
+
227
+ // No bogus parse-error result is auto-submitted, so the host graph is
228
+ // not resumed with a fake tool failure and the pending interrupt is
229
+ // preserved.
230
+ expect(onResult).not.toHaveBeenCalled();
231
+ // The frontend execute never ran: the stale prefix was never parsed.
232
+ expect(execute).not.toHaveBeenCalled();
233
+ } finally {
234
+ warnSpy.mockRestore();
235
+ }
236
+ });
237
+
238
+ it("does not auto-submit a parse-error result for a non-executable tool whose divergent argsText closes", async () => {
239
+ // Same close-gating mismatch as the executable case, but for a tool with
240
+ // no frontend execute. Closing on the divergent complete snapshot would
241
+ // still parse the incomplete stale prefix and fabricate a parse-error
242
+ // result, so the close must gate on the controller's streamed content.
243
+ const getTools = () => ({
244
+ weatherSearch: {
245
+ parameters: { type: "object", properties: {} },
246
+ } satisfies Tool,
247
+ });
248
+ const onResult = vi.fn();
249
+ const onStatusesChange = () => {};
250
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
251
+
252
+ try {
253
+ const tracker = new ToolInvocationTracker(getTools, {
254
+ onResult,
255
+ onStatusesChange,
256
+ });
257
+ tracker.setState(createState([]));
258
+
259
+ // Incomplete prefix while the run is still streaming.
260
+ tracker.setState(
261
+ createState([createAssistantMessage('{"a":1', { a: 1 })], true),
262
+ );
263
+
264
+ // Run settles and the snapshot regresses to a complete divergent text.
265
+ tracker.setState(
266
+ createState([createAssistantMessage('{"a":2}', { a: 2 })], false),
267
+ );
268
+
269
+ for (let i = 0; i < 5; i++) {
270
+ await new Promise((r) => setTimeout(r, 0));
271
+ }
272
+
273
+ expect(onResult).not.toHaveBeenCalled();
274
+ } finally {
275
+ warnSpy.mockRestore();
276
+ }
277
+ });
278
+
155
279
  it("clears executing status under the logical toolCallId when reset() lands while execute is pending", async () => {
156
280
  // Tests the F.1 lifecycle: reset() aborts in-flight execute() invocations
157
281
  // and clears their executing status. The status key is the logical
@@ -948,12 +1072,8 @@ describe("ToolInvocationTracker", () => {
948
1072
  // result replacement) and verifying streamCall fires exactly once.
949
1073
  //
950
1074
  // The pathological mid-stream regression case (A.2) is covered by
951
- // the dedicated regression test above. Mixing A.2 with a backend
952
- // result in the same snapshot exposes a separate issue inside
953
- // assistant-stream's `ToolCallStreamController.setResponse` ordering
954
- // (parse-failure result reaches the reader before the backend
955
- // result); that's tracked separately and out of scope for the
956
- // tracker-level contract.
1075
+ // the assistant-stream ordering regression test in
1076
+ // packages/assistant-stream/src/core/modules/tool-call.test.ts.
957
1077
  const streamCall = vi.fn();
958
1078
  const execute = vi.fn(async () => ({ forecast: "ok" }));
959
1079
  const getTools = () => ({
@@ -35,34 +35,6 @@ export type AddToolResultCommand = {
35
35
  readonly modelContent?: readonly ToolModelContentPart[];
36
36
  };
37
37
 
38
- export type ToolInvocationTrackerSnapshot = {
39
- readonly messages: readonly ThreadMessage[];
40
- /** Whether the producing runtime is currently streaming new output. */
41
- readonly isRunning: boolean;
42
- /**
43
- * Whether the producing runtime is still loading historical state.
44
- * When `true`, every snapshot is treated as historical (no `streamCall` /
45
- * `execute` fires). When `false`, processing resumes as live.
46
- */
47
- readonly isLoading?: boolean;
48
- };
49
-
50
- export type ToolInvocationTrackerCallbacks = {
51
- /**
52
- * Invoked when a client-side `execute()` returns a result and the runtime
53
- * needs to feed it back into the conversation.
54
- */
55
- onResult: (command: AddToolResultCommand) => void;
56
- /**
57
- * Invoked whenever the per-tool-call status map changes (executing /
58
- * interrupt / cleared). The callback receives a fresh map; mutating the
59
- * argument is not supported.
60
- */
61
- onStatusesChange: (
62
- statuses: ReadonlyMap<string, ToolExecutionStatus>,
63
- ) => void;
64
- };
65
-
66
38
  type ToolCallEntry = {
67
39
  toolName: string;
68
40
  argsText: string;
@@ -123,10 +95,12 @@ const isEquivalentCompleteArgsText = (previous: string, next: string) => {
123
95
  * the hot message-processing path, so a malformed snapshot must never crash
124
96
  * the host runtime. See ./EDGE_CASES.md for the known non-trivial state
125
97
  * transitions and what each does today.
98
+ *
99
+ * @deprecated Internal — for framework bindings; not a public API. May change without notice.
126
100
  */
127
101
  export class ToolInvocationTracker {
128
102
  private readonly _getTools: () => Record<string, Tool> | undefined;
129
- private readonly _callbacks: ToolInvocationTrackerCallbacks;
103
+ private readonly _callbacks: ToolInvocationTracker.Callbacks;
130
104
 
131
105
  private readonly _entries = new Map<string, ToolCallEntry>();
132
106
  /**
@@ -157,7 +131,7 @@ export class ToolInvocationTracker {
157
131
  private _pendingRestore = true;
158
132
 
159
133
  /** Cached last snapshot, used to skip processing on identical re-renders. */
160
- private _lastSnapshot: ToolInvocationTrackerSnapshot | null = null;
134
+ private _lastSnapshot: ToolInvocationTracker.Snapshot | null = null;
161
135
  private _isRunning = false;
162
136
 
163
137
  private _controller!: ReturnType<typeof createAssistantStreamController>[1];
@@ -175,7 +149,7 @@ export class ToolInvocationTracker {
175
149
 
176
150
  constructor(
177
151
  getTools: () => Record<string, Tool> | undefined,
178
- callbacks: ToolInvocationTrackerCallbacks,
152
+ callbacks: ToolInvocationTracker.Callbacks,
179
153
  ) {
180
154
  this._getTools = getTools;
181
155
  this._callbacks = callbacks;
@@ -235,7 +209,7 @@ export class ToolInvocationTracker {
235
209
  * Feed the next observed snapshot into the tracker. Called from the host
236
210
  * runtime whenever its message list / running state changes.
237
211
  */
238
- public setState(snapshot: ToolInvocationTrackerSnapshot): void {
212
+ public setState(snapshot: ToolInvocationTracker.Snapshot): void {
239
213
  try {
240
214
  // Recover from a dead pipeline before processing anything. We demote
241
215
  // all active entries to "restored" so the rebuilt pipeline does not
@@ -687,14 +661,16 @@ export class ToolInvocationTracker {
687
661
  }
688
662
 
689
663
  if (!entry.argsComplete && entry.controller) {
664
+ // ToolExecutionStream parses the streamed prefix on close, so the close
665
+ // gates on the streamed content; a divergent snapshot (A.2) can be
666
+ // complete while the controller still holds an incomplete stale prefix.
690
667
  const shouldClose = this._shouldCloseArgsStream({
691
668
  toolName: content.toolName,
692
- argsText: content.argsText,
669
+ argsText: entry.argsText,
693
670
  hasResult,
694
671
  });
695
672
  if (shouldClose) {
696
673
  entry.controller.argsText.close();
697
- entry.argsText = content.argsText;
698
674
  entry.argsComplete = true;
699
675
  }
700
676
  }
@@ -780,3 +756,35 @@ export class ToolInvocationTracker {
780
756
  }
781
757
  }
782
758
  }
759
+
760
+ export namespace ToolInvocationTracker {
761
+ export type ExecutionStatus = ToolExecutionStatus;
762
+
763
+ export type Snapshot = {
764
+ readonly messages: readonly ThreadMessage[];
765
+ /** Whether the producing runtime is currently streaming new output. */
766
+ readonly isRunning: boolean;
767
+ /**
768
+ * Whether the producing runtime is still loading historical state.
769
+ * When `true`, every snapshot is treated as historical (no `streamCall` /
770
+ * `execute` fires). When `false`, processing resumes as live.
771
+ */
772
+ readonly isLoading?: boolean;
773
+ };
774
+
775
+ export type Callbacks = {
776
+ /**
777
+ * Invoked when a client-side `execute()` returns a result and the runtime
778
+ * needs to feed it back into the conversation.
779
+ */
780
+ onResult: (command: AddToolResultCommand) => void;
781
+ /**
782
+ * Invoked whenever the per-tool-call status map changes (executing /
783
+ * interrupt / cleared). The callback receives a fresh map; mutating the
784
+ * argument is not supported.
785
+ */
786
+ onStatusesChange: (
787
+ statuses: ReadonlyMap<string, ToolExecutionStatus>,
788
+ ) => void;
789
+ };
790
+ }
@@ -149,7 +149,7 @@ describe("mergeModelContexts", () => {
149
149
  });
150
150
  });
151
151
 
152
- it("still rejects duplicate tools at the same priority", () => {
152
+ it("rejects duplicate tools at the same priority", () => {
153
153
  expect(() =>
154
154
  mergeModelContexts(
155
155
  new Set([
@@ -160,6 +160,26 @@ describe("mergeModelContexts", () => {
160
160
  ).toThrow(/already exists/);
161
161
  });
162
162
 
163
+ it("silently replaces a same-priority tool when the later registration sets overwrite", () => {
164
+ const merged = mergeModelContexts(
165
+ new Set([
166
+ provider({ tools: { duplicate: toolFixture() } }),
167
+ provider({
168
+ tools: {
169
+ duplicate: {
170
+ ...toolFixture(),
171
+ description: "latest",
172
+ overwrite: true,
173
+ } as Tool<any, any>,
174
+ },
175
+ }),
176
+ ]),
177
+ );
178
+
179
+ expect(merged.tools?.duplicate?.description).toBe("latest");
180
+ expect(merged.tools?.duplicate?.overwrite).toBeUndefined();
181
+ });
182
+
163
183
  it("preserves the highest priority when a lower-priority provider reuses the same tool object", () => {
164
184
  const shared = {
165
185
  ...toolFixture(),
@@ -1,7 +1,7 @@
1
1
  import type { Unsubscribe } from "../../types/unsubscribe";
2
2
  import type { ThreadRuntimeEventType } from "../../runtime/interfaces/thread-runtime-core";
3
3
  import type { ThreadRuntime } from "../../runtime/api/thread-runtime";
4
- import { useMemo, useEffect, RefObject } from "react";
4
+ import { useMemo, useEffect, type RefObject } from "react";
5
5
  import { useResource, resource, withKey } from "@assistant-ui/tap";
6
6
  import { liveRef } from "./liveRef";
7
7
  import {
@@ -113,6 +113,7 @@ const useThreadClient = ({
113
113
  deleteMessage: runtime.deleteMessage,
114
114
  startRun: runtime.startRun,
115
115
  resumeRun: runtime.resumeRun,
116
+ importExternalState: runtime.importExternalState,
116
117
  cancelRun: runtime.cancelRun,
117
118
  getModelContext: runtime.getModelContext,
118
119
  export: runtime.export,
@@ -108,6 +108,7 @@ export type ThreadMethods = {
108
108
  * @param initialMessages - Optional array of initial messages to populate the thread
109
109
  */
110
110
  reset(initialMessages?: readonly ThreadMessageLike[]): void;
111
+ importExternalState?(state: unknown): void;
111
112
  message(selector: { id: string } | { index: number }): MessageMethods;
112
113
  /** @deprecated This API is still under active development and might change without notice. */
113
114
  stopSpeaking(): void;
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { SubscribableWithState } from "./subscribable";
3
+ import { ShallowMemoizeSubject } from "./subscribable";
4
+
5
+ type TestState = {
6
+ status: string;
7
+ error?: string;
8
+ };
9
+
10
+ const createBinding = (initialState: TestState) => {
11
+ let state = initialState;
12
+ const subscribers = new Set<() => void>();
13
+
14
+ const binding: SubscribableWithState<TestState, null> = {
15
+ path: null,
16
+ getState: () => state,
17
+ subscribe: (callback) => {
18
+ subscribers.add(callback);
19
+ return () => subscribers.delete(callback);
20
+ },
21
+ };
22
+
23
+ return {
24
+ binding,
25
+ update(nextState: TestState) {
26
+ state = nextState;
27
+ for (const callback of subscribers) callback();
28
+ },
29
+ };
30
+ };
31
+
32
+ describe("ShallowMemoizeSubject", () => {
33
+ it("notifies subscribers when a state key is removed", () => {
34
+ const source = createBinding({
35
+ status: "running",
36
+ error: "Connection failed",
37
+ });
38
+ const subject = new ShallowMemoizeSubject(source.binding);
39
+ const subscriber = vi.fn();
40
+ subject.subscribe(subscriber);
41
+
42
+ source.update({ status: "running" });
43
+
44
+ expect(subscriber).toHaveBeenCalledOnce();
45
+ expect(subject.getState()).toEqual({ status: "running" });
46
+ });
47
+
48
+ it("does not notify subscribers for a shallow-equal state", () => {
49
+ const source = createBinding({ status: "running" });
50
+ const subject = new ShallowMemoizeSubject(source.binding);
51
+ const subscriber = vi.fn();
52
+ subject.subscribe(subscriber);
53
+
54
+ source.update({ status: "running" });
55
+
56
+ expect(subscriber).not.toHaveBeenCalled();
57
+ });
58
+ });
@@ -1,4 +1,5 @@
1
1
  import type { Unsubscribe } from "../types/unsubscribe";
2
+ import { notifyEventListeners } from "../utils/notify-event-listeners";
2
3
 
3
4
  export const SKIP_UPDATE = Symbol("skip-update");
4
5
  export type SKIP_UPDATE = typeof SKIP_UPDATE;
@@ -39,7 +40,10 @@ function shallowEqual<T extends object>(
39
40
  if (objA === undefined) return false;
40
41
  if (objB === undefined) return false;
41
42
 
42
- for (const key of Object.keys(objA)) {
43
+ const keysA = Object.keys(objA);
44
+ if (keysA.length !== Object.keys(objB).length) return false;
45
+
46
+ for (const key of keysA) {
43
47
  const valueA = objA[key as keyof T];
44
48
  const valueB = objB[key as keyof T];
45
49
  if (!Object.is(valueA, valueB)) return false;
@@ -99,7 +103,12 @@ export abstract class BaseSubject {
99
103
 
100
104
  protected abstract _connect(): Unsubscribe;
101
105
 
102
- protected notifySubscribers(payload?: unknown) {
106
+ protected notifySubscribers(payload?: unknown, errorContext?: string) {
107
+ if (errorContext) {
108
+ notifyEventListeners(this._subscriptions, payload, errorContext);
109
+ return;
110
+ }
111
+
103
112
  for (const callback of this._subscriptions) callback(payload);
104
113
  }
105
114
 
@@ -132,10 +141,11 @@ export class ShallowMemoizeSubject<TState extends object, TPath>
132
141
  return this.binding.path;
133
142
  }
134
143
 
135
- constructor(
136
- private binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>,
137
- ) {
144
+ private binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>;
145
+
146
+ constructor(binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>) {
138
147
  super();
148
+ this.binding = binding;
139
149
  const state = binding.getState();
140
150
  if (state === SKIP_UPDATE)
141
151
  throw new Error("Entry not available in the store");
@@ -175,10 +185,11 @@ export class LazyMemoizeSubject<TState extends object, TPath>
175
185
  return this.binding.path;
176
186
  }
177
187
 
178
- constructor(
179
- private binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>,
180
- ) {
188
+ private binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>;
189
+
190
+ constructor(binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>) {
181
191
  super();
192
+ this.binding = binding;
182
193
  }
183
194
 
184
195
  private _previousStateDirty = true;
@@ -219,8 +230,11 @@ export class NestedSubscriptionSubject<
219
230
  return this.binding.path;
220
231
  }
221
232
 
222
- constructor(private binding: NestedSubscribable<TState, TPath>) {
233
+ private binding: NestedSubscribable<TState, TPath>;
234
+
235
+ constructor(binding: NestedSubscribable<TState, TPath>) {
223
236
  super();
237
+ this.binding = binding;
224
238
  }
225
239
 
226
240
  public getState() {
@@ -260,8 +274,11 @@ export class NestedSubscriptionSubject<
260
274
  export class EventSubscriptionSubject<
261
275
  TEvent extends string,
262
276
  > extends BaseSubject {
263
- constructor(private config: EventSubscribable<TEvent>) {
277
+ private config: EventSubscribable<TEvent>;
278
+
279
+ constructor(config: EventSubscribable<TEvent>) {
264
280
  super();
281
+ this.config = config;
265
282
  }
266
283
 
267
284
  public getState() {
@@ -273,8 +290,9 @@ export class EventSubscriptionSubject<
273
290
  }
274
291
 
275
292
  protected _connect(): Unsubscribe {
293
+ const errorContext = `Runtime event "${this.config.event}"`;
276
294
  const callback = (payload?: unknown) => {
277
- this.notifySubscribers(payload);
295
+ this.notifySubscribers(payload, errorContext);
278
296
  };
279
297
 
280
298
  let lastState = this.config.binding.getState();
@@ -110,6 +110,18 @@ describe("MessageRepository", () => {
110
110
  }).toThrow(/Parent message not found/);
111
111
  });
112
112
 
113
+ it("should throw when a message would be linked under an ancestor with the same id", () => {
114
+ const message = createTestMessage({ id: "message-id" });
115
+ repository.addOrUpdateMessage(null, message);
116
+
117
+ expect(() => {
118
+ repository.addOrUpdateMessage(
119
+ "message-id",
120
+ createTestMessage({ id: "message-id" }),
121
+ );
122
+ }).toThrow(/same id already exists in the parent tree/);
123
+ });
124
+
113
125
  it("should retrieve all messages in the current branch", () => {
114
126
  const parent = createTestMessage({ id: "parent-id" });
115
127
  const child = createTestMessage({ id: "child-id" });
@@ -79,6 +79,39 @@ const applyListResult = (
79
79
  };
80
80
 
81
81
  describe("list + delete race condition", () => {
82
+ it("stale list() does not re-add a thread deleted before list started", async () => {
83
+ const state = new OptimisticState<RemoteThreadState>(
84
+ applyListResult(EMPTY_STATE, {
85
+ threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
86
+ }),
87
+ );
88
+ const deleteDeferred = deferred<void>();
89
+ const listDeferred = deferred<ListResult>();
90
+
91
+ const deletePromise = state.optimisticUpdate({
92
+ execute: () => deleteDeferred.promise,
93
+ optimistic: (s) => updateStatusReducer(s, "thread-A", "deleted"),
94
+ });
95
+ const listPromise = state.optimisticUpdate({
96
+ execute: () => listDeferred.promise,
97
+ loading: (s) => ({ ...s, isLoading: true }),
98
+ then: applyListResult,
99
+ });
100
+
101
+ deleteDeferred.resolve();
102
+ await deletePromise;
103
+
104
+ listDeferred.resolve({
105
+ threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
106
+ });
107
+ await listPromise;
108
+
109
+ expect(state.value.threadIds).not.toContain("thread-A");
110
+ expect(
111
+ state.value.threadData[createThreadMappingId("thread-A")],
112
+ ).toBeUndefined();
113
+ });
114
+
82
115
  it("stale list() does not re-add a thread deleted while list was in flight", async () => {
83
116
  const state = new OptimisticState<RemoteThreadState>(EMPTY_STATE);
84
117