@assistant-ui/core 0.3.15 → 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 (251) hide show
  1. package/dist/internal.d.ts +7 -2
  2. package/dist/internal.js +8 -3
  3. package/dist/model-context/frame/host.d.ts.map +1 -1
  4. package/dist/model-context/frame/host.js +3 -2
  5. package/dist/model-context/frame/host.js.map +1 -1
  6. package/dist/model-context/frame/provider.d.ts +2 -0
  7. package/dist/model-context/frame/provider.d.ts.map +1 -1
  8. package/dist/model-context/frame/provider.js +88 -25
  9. package/dist/model-context/frame/provider.js.map +1 -1
  10. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  11. package/dist/model-context/interactable-composer-metadata.js +1 -1
  12. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  13. package/dist/model-context/registry.d.ts +2 -2
  14. package/dist/model-context/registry.d.ts.map +1 -1
  15. package/dist/model-context/registry.js +9 -28
  16. package/dist/model-context/registry.js.map +1 -1
  17. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  18. package/dist/react/client/InMemoryThreadList.js +24 -20
  19. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts.map +1 -1
  21. package/dist/react/client/Interactables.js +59 -177
  22. package/dist/react/client/Interactables.js.map +1 -1
  23. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  24. package/dist/react/client/RemoteThreadList.js +23 -56
  25. package/dist/react/client/RemoteThreadList.js.map +1 -1
  26. package/dist/react/index.d.ts +2 -1
  27. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  28. package/dist/react/interactables-legacy/Interactables.js +56 -174
  29. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  30. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  31. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  32. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  33. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  34. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  35. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  36. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  37. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  38. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +1 -1
  39. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +1 -1
  40. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  41. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +23 -55
  42. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  43. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  44. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  45. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  46. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  47. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  48. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  49. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  50. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  51. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  52. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  53. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  54. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  55. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  56. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  57. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  58. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  59. package/dist/react/runtimes/external-message-converter.js +33 -233
  60. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  61. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  62. package/dist/runtime/api/composer-runtime.js.map +1 -1
  63. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  64. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  65. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  66. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  67. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  68. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  69. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  70. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  71. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  72. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  73. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  74. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  75. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  76. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  77. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  78. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  79. package/dist/runtime/internal.d.ts +2 -1
  80. package/dist/runtime/internal.js +2 -1
  81. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  82. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  83. package/dist/runtime/queue/message-queue.js +1 -1
  84. package/dist/runtime/queue/message-queue.js.map +1 -1
  85. package/dist/runtime/queue/queue-item.d.ts +12 -0
  86. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  87. package/dist/runtime/queue/queue-item.js +6 -0
  88. package/dist/runtime/queue/queue-item.js.map +1 -0
  89. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  90. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  91. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  92. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  93. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  94. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  95. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  96. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  97. package/dist/runtime/utils/auto-status.d.ts +6 -1
  98. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  99. package/dist/runtime/utils/auto-status.js +7 -1
  100. package/dist/runtime/utils/auto-status.js.map +1 -1
  101. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  102. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  103. package/dist/runtime/utils/external-message-conversion.js +221 -0
  104. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  105. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  106. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  107. package/dist/runtime/utils/external-store-message.js +2 -1
  108. package/dist/runtime/utils/external-store-message.js.map +1 -1
  109. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  110. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  111. package/dist/runtime/utils/message-repository-session.js +130 -0
  112. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  113. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  114. package/dist/runtime/utils/message-repository.js +3 -4
  115. package/dist/runtime/utils/message-repository.js.map +1 -1
  116. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  117. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  118. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  119. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  120. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  121. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  122. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  123. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  124. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  125. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  126. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  127. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  128. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  129. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +122 -20
  130. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  131. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  132. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  133. package/dist/runtimes/local/local-thread-runtime-core.js +20 -2
  134. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  135. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  136. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  137. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  138. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  139. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  140. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  141. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  142. package/dist/store/clients/external-thread.d.ts.map +1 -1
  143. package/dist/store/clients/external-thread.js +74 -99
  144. package/dist/store/clients/external-thread.js.map +1 -1
  145. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  146. package/dist/store/clients/single-thread-list.js +1 -1
  147. package/dist/store/clients/single-thread-list.js.map +1 -1
  148. package/dist/store/clients/suggestions.d.ts.map +1 -1
  149. package/dist/store/clients/suggestions.js +16 -7
  150. package/dist/store/clients/suggestions.js.map +1 -1
  151. package/dist/store/index.d.ts +4 -2
  152. package/dist/store/scope-registration.d.ts +1 -1
  153. package/dist/store/scopes/composer.d.ts +2 -1
  154. package/dist/store/scopes/composer.d.ts.map +1 -1
  155. package/dist/store/scopes/message.d.ts +1 -1
  156. package/dist/store/scopes/queue-item.d.ts +2 -9
  157. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  158. package/dist/store/scopes/queue-item.js +0 -6
  159. package/dist/types/message.d.ts +7 -3
  160. package/dist/types/message.d.ts.map +1 -1
  161. package/dist/types/message.js.map +1 -1
  162. package/dist/utils/data-url.d.ts +14 -1
  163. package/dist/utils/data-url.d.ts.map +1 -1
  164. package/dist/utils/data-url.js +13 -1
  165. package/dist/utils/data-url.js.map +1 -1
  166. package/dist/utils/invoke-user-callback.d.ts +5 -0
  167. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  168. package/dist/utils/invoke-user-callback.js +19 -0
  169. package/dist/utils/invoke-user-callback.js.map +1 -0
  170. package/dist/utils/normalizePartStatus.js.map +1 -1
  171. package/package.json +10 -10
  172. package/src/internal.ts +12 -0
  173. package/src/model-context/frame/host.test.ts +91 -7
  174. package/src/model-context/frame/host.ts +7 -2
  175. package/src/model-context/frame/provider.test.ts +267 -12
  176. package/src/model-context/frame/provider.ts +113 -31
  177. package/src/model-context/interactable-composer-metadata.ts +1 -1
  178. package/src/model-context/registry.test.ts +44 -0
  179. package/src/model-context/registry.ts +13 -35
  180. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  181. package/src/react/client/InMemoryThreadList.ts +7 -1
  182. package/src/react/client/Interactables.ts +19 -178
  183. package/src/react/client/RemoteThreadList.test.ts +96 -0
  184. package/src/react/client/RemoteThreadList.ts +27 -59
  185. package/src/react/interactables-legacy/Interactables.ts +20 -177
  186. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  187. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  188. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +26 -56
  189. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  190. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  191. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  192. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  193. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  194. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  195. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  196. package/src/react/runtimes/external-message-converter.ts +58 -481
  197. package/src/runtime/api/composer-runtime.ts +1 -1
  198. package/src/runtime/api/message-runtime.test.ts +28 -1
  199. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  200. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  201. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  202. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  203. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  204. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  205. package/src/runtime/internal.ts +5 -0
  206. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  207. package/src/runtime/queue/message-queue.ts +1 -4
  208. package/src/runtime/queue/queue-item.ts +10 -0
  209. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  210. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  211. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  212. package/src/runtime/utils/auto-status.ts +33 -0
  213. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  214. package/src/runtime/utils/external-message-conversion.ts +494 -0
  215. package/src/runtime/utils/external-store-message.ts +2 -0
  216. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  217. package/src/runtime/utils/message-repository-session.ts +176 -0
  218. package/src/runtime/utils/message-repository.ts +7 -4
  219. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  220. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  221. package/src/runtime/utils/thread-message-like.ts +2 -0
  222. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  223. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +175 -28
  224. package/src/runtimes/local/local-thread-runtime-core.test.ts +172 -1
  225. package/src/runtimes/local/local-thread-runtime-core.ts +48 -3
  226. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  227. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  228. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  229. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  230. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  231. package/src/store/clients/external-thread.ts +16 -61
  232. package/src/store/clients/single-thread-list.ts +4 -1
  233. package/src/store/clients/suggestions.test.ts +31 -0
  234. package/src/store/clients/suggestions.ts +18 -19
  235. package/src/store/scopes/message.ts +1 -1
  236. package/src/store/scopes/queue-item.ts +2 -9
  237. package/src/tests/MessageRepository.test.ts +77 -0
  238. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  239. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  240. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +164 -0
  241. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  242. package/src/tests/external-thread-attachments.test.tsx +173 -0
  243. package/src/tests/single-thread-list-item.test.tsx +47 -0
  244. package/src/tests/thread-message-like.test.ts +39 -1
  245. package/src/types/message.ts +8 -3
  246. package/src/utils/data-url.test.ts +52 -1
  247. package/src/utils/data-url.ts +21 -0
  248. package/src/utils/invoke-user-callback.test.ts +82 -0
  249. package/src/utils/invoke-user-callback.ts +31 -0
  250. package/src/utils/normalizePartStatus.ts +1 -1
  251. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -1155,3 +1155,645 @@ describe("ExternalStoreThreadRuntimeCore - message queue", () => {
1155
1155
  expect(queue.remove).toHaveBeenCalledWith("q1");
1156
1156
  });
1157
1157
  });
1158
+
1159
+ describe("ExternalStoreThreadRuntimeCore - deleteMessage via setMessages", () => {
1160
+ const message = (id: string, role: "user" | "assistant", text: string) =>
1161
+ ({
1162
+ id,
1163
+ role,
1164
+ content: [{ type: "text", text }],
1165
+ createdAt: new Date(0),
1166
+ metadata: { custom: {} },
1167
+ ...(role === "assistant"
1168
+ ? { status: { type: "complete", reason: "stop" } }
1169
+ : {}),
1170
+ }) as unknown as import("../types/message").ThreadMessage;
1171
+
1172
+ const setup = (initial: import("../types/message").ThreadMessage[]) => {
1173
+ let current = initial;
1174
+ const setMessages = vi.fn(
1175
+ (m: import("../types/message").ThreadMessage[]) => {
1176
+ current = m;
1177
+ },
1178
+ );
1179
+ const store = () => makeStore({ messages: current, setMessages });
1180
+ const runtime = new ExternalStoreThreadRuntimeCore(
1181
+ mockContextProvider,
1182
+ store(),
1183
+ );
1184
+ return {
1185
+ runtime,
1186
+ setMessages,
1187
+ setStoreMessages: (m: import("../types/message").ThreadMessage[]) => {
1188
+ current = m;
1189
+ },
1190
+ syncSnapshot: () => runtime.__internal_setAdapter(store()),
1191
+ };
1192
+ };
1193
+
1194
+ it("leaves no sibling branch behind", async () => {
1195
+ const { runtime, syncSnapshot } = setup([
1196
+ message("u1", "user", "hi"),
1197
+ message("a1", "assistant", "hello"),
1198
+ ]);
1199
+
1200
+ await runtime.deleteMessage("u1");
1201
+ syncSnapshot();
1202
+
1203
+ expect(runtime.messages.map((m) => m.id)).toEqual(["a1"]);
1204
+ expect(runtime.getBranches("a1")).toEqual(["a1"]);
1205
+ });
1206
+
1207
+ it("relinks children when deleting mid-thread", async () => {
1208
+ const { runtime, syncSnapshot } = setup([
1209
+ message("u1", "user", "one"),
1210
+ message("a1", "assistant", "two"),
1211
+ message("u2", "user", "three"),
1212
+ message("a2", "assistant", "four"),
1213
+ ]);
1214
+
1215
+ await runtime.deleteMessage("u2");
1216
+ syncSnapshot();
1217
+
1218
+ expect(runtime.messages.map((m) => m.id)).toEqual(["u1", "a1", "a2"]);
1219
+ expect(runtime.getBranches("a2")).toEqual(["a2"]);
1220
+ });
1221
+
1222
+ it("relinks every child branch when deleting a message with siblings", async () => {
1223
+ const { runtime, syncSnapshot, setStoreMessages } = setup([
1224
+ message("u1", "user", "one"),
1225
+ message("a1", "assistant", "two"),
1226
+ message("u2", "user", "three"),
1227
+ message("a2", "assistant", "four"),
1228
+ ]);
1229
+
1230
+ setStoreMessages([
1231
+ message("u1", "user", "one"),
1232
+ message("a1", "assistant", "two"),
1233
+ message("u2", "user", "three"),
1234
+ message("a3", "assistant", "five"),
1235
+ ]);
1236
+ syncSnapshot();
1237
+ expect(runtime.getBranches("a3")).toEqual(["a2", "a3"]);
1238
+
1239
+ await runtime.deleteMessage("u2");
1240
+ syncSnapshot();
1241
+
1242
+ expect(runtime.messages.map((m) => m.id)).toEqual(["u1", "a1", "a3"]);
1243
+ expect(runtime.getBranches("a3")).toEqual(["a2", "a3"]);
1244
+ });
1245
+
1246
+ it("does not resurrect deleted content through switchToBranch", async () => {
1247
+ const { runtime, setMessages, syncSnapshot } = setup([
1248
+ message("u1", "user", "hi"),
1249
+ message("a1", "assistant", "hello"),
1250
+ ]);
1251
+
1252
+ await runtime.deleteMessage("u1");
1253
+ syncSnapshot();
1254
+ setMessages.mockClear();
1255
+
1256
+ expect(() => runtime.switchToBranch("u1")).toThrow(
1257
+ "MessageRepository(switchToBranch): Branch not found",
1258
+ );
1259
+ expect(setMessages).not.toHaveBeenCalled();
1260
+ });
1261
+
1262
+ it("exposes a consistent messages/branch view at notify time", async () => {
1263
+ const { runtime } = setup([
1264
+ message("u1", "user", "hi"),
1265
+ message("a1", "assistant", "hello"),
1266
+ ]);
1267
+
1268
+ const observed: { ids: string[]; branches: readonly string[] }[] = [];
1269
+ runtime.subscribe(() => {
1270
+ observed.push({
1271
+ ids: runtime.messages.map((m) => m.id),
1272
+ branches: runtime.getBranches(runtime.messages.at(-1)!.id),
1273
+ });
1274
+ });
1275
+
1276
+ await runtime.deleteMessage("u1");
1277
+
1278
+ expect(observed).toContainEqual({ ids: ["a1"], branches: ["a1"] });
1279
+ for (const snapshot of observed) {
1280
+ expect(snapshot.ids).not.toContain("u1");
1281
+ expect(snapshot.branches).not.toContain("u1");
1282
+ }
1283
+ });
1284
+
1285
+ it("evicts the deleted message on the onDelete path too", async () => {
1286
+ let current = [
1287
+ message("u1", "user", "hi"),
1288
+ message("a1", "assistant", "hello"),
1289
+ ];
1290
+ const onDelete = vi.fn(async (id: string) => {
1291
+ current = current.filter((m) => m.id !== id);
1292
+ });
1293
+ const store = () =>
1294
+ makeStore({ messages: current, onDelete, setMessages: vi.fn() });
1295
+ const runtime = new ExternalStoreThreadRuntimeCore(
1296
+ mockContextProvider,
1297
+ store(),
1298
+ );
1299
+
1300
+ await runtime.deleteMessage("u1");
1301
+ runtime.__internal_setAdapter(store());
1302
+
1303
+ expect(runtime.messages.map((m) => m.id)).toEqual(["a1"]);
1304
+ expect(runtime.getBranches("a1")).toEqual(["a1"]);
1305
+ expect(() => runtime.switchToBranch("u1")).toThrow(
1306
+ "MessageRepository(switchToBranch): Branch not found",
1307
+ );
1308
+ });
1309
+
1310
+ it("keeps an off-branch sibling the host declined to delete", async () => {
1311
+ let current = [
1312
+ message("u1", "user", "hi"),
1313
+ message("a1", "assistant", "one"),
1314
+ ];
1315
+ const onDelete = vi.fn(async (id: string) => {
1316
+ current = current.filter((m) => m.id !== id);
1317
+ });
1318
+ const store = () => makeStore({ messages: current, onDelete });
1319
+ const runtime = new ExternalStoreThreadRuntimeCore(
1320
+ mockContextProvider,
1321
+ store(),
1322
+ );
1323
+
1324
+ current = [current[0]!, message("a2", "assistant", "two")];
1325
+ runtime.__internal_setAdapter(store());
1326
+ expect(runtime.getBranches("a2")).toEqual(["a1", "a2"]);
1327
+
1328
+ await runtime.deleteMessage("a1");
1329
+ current = [...current];
1330
+ runtime.__internal_setAdapter(store());
1331
+
1332
+ expect(onDelete).toHaveBeenCalledWith("a1");
1333
+ expect(runtime.getBranches("a2")).toEqual(["a1", "a2"]);
1334
+ });
1335
+
1336
+ it("keeps the eviction when a send races an in-flight delete", async () => {
1337
+ let current = [
1338
+ message("u1", "user", "hi"),
1339
+ message("a1", "assistant", "hello"),
1340
+ ];
1341
+ let resolveDelete!: () => void;
1342
+ const onDelete = vi.fn(
1343
+ (id: string) =>
1344
+ new Promise<void>((resolve) => {
1345
+ resolveDelete = () => {
1346
+ current = current.filter((m) => m.id !== id);
1347
+ resolve();
1348
+ };
1349
+ }),
1350
+ );
1351
+ const onNew = vi.fn();
1352
+ const store = () =>
1353
+ makeStore({ messages: current, onDelete, onNew, setMessages: vi.fn() });
1354
+ const runtime = new ExternalStoreThreadRuntimeCore(
1355
+ mockContextProvider,
1356
+ store(),
1357
+ );
1358
+
1359
+ const deletePromise = runtime.deleteMessage("u1");
1360
+ await runtime.append({
1361
+ role: "user",
1362
+ content: [{ type: "text", text: "while deleting" }],
1363
+ attachments: [],
1364
+ createdAt: new Date(0),
1365
+ parentId: "a1",
1366
+ sourceId: null,
1367
+ runConfig: {},
1368
+ metadata: { custom: {} },
1369
+ });
1370
+ resolveDelete();
1371
+ await deletePromise;
1372
+ runtime.__internal_setAdapter(store());
1373
+
1374
+ expect(onNew).toHaveBeenCalled();
1375
+ expect(runtime.getBranches("a1")).toEqual(["a1"]);
1376
+ expect(() => runtime.switchToBranch("u1")).toThrow(
1377
+ "MessageRepository(switchToBranch): Branch not found",
1378
+ );
1379
+ });
1380
+
1381
+ it("keeps a pending eviction across a branch switch swallowed mid-run", async () => {
1382
+ let current = [
1383
+ message("u1", "user", "hi"),
1384
+ message("a1", "assistant", "hello"),
1385
+ ];
1386
+ const onDelete = vi.fn(async (id: string) => {
1387
+ current = current.filter((m) => m.id !== id);
1388
+ });
1389
+ const store = (isRunning: boolean) =>
1390
+ makeStore({
1391
+ messages: current,
1392
+ onDelete,
1393
+ setMessages: vi.fn(),
1394
+ isRunning,
1395
+ });
1396
+ const runtime = new ExternalStoreThreadRuntimeCore(
1397
+ mockContextProvider,
1398
+ store(true),
1399
+ );
1400
+
1401
+ await runtime.deleteMessage("u1");
1402
+ runtime.switchToBranch("a1");
1403
+ runtime.__internal_setAdapter(store(true));
1404
+
1405
+ expect(runtime.getBranches("a1")).toEqual(["a1"]);
1406
+
1407
+ runtime.__internal_setAdapter(store(false));
1408
+ expect(() => runtime.switchToBranch("u1")).toThrow(
1409
+ "MessageRepository(switchToBranch): Branch not found",
1410
+ );
1411
+ });
1412
+
1413
+ it("keeps a visible message the host declined to delete", async () => {
1414
+ const current = [
1415
+ message("u1", "user", "hi"),
1416
+ message("a1", "assistant", "hello"),
1417
+ ];
1418
+ const onDelete = vi.fn(async () => {});
1419
+ const store = () =>
1420
+ makeStore({ messages: current, onDelete, setMessages: vi.fn() });
1421
+ const runtime = new ExternalStoreThreadRuntimeCore(
1422
+ mockContextProvider,
1423
+ store(),
1424
+ );
1425
+
1426
+ await runtime.deleteMessage("u1");
1427
+ runtime.__internal_setAdapter(store());
1428
+
1429
+ expect(onDelete).toHaveBeenCalledWith("u1");
1430
+ expect(runtime.messages.map((m) => m.id)).toEqual(["u1", "a1"]);
1431
+ expect(runtime.getBranches("a1")).toEqual(["a1"]);
1432
+
1433
+ runtime.__internal_setAdapter(
1434
+ makeStore({ messages: [...current], onDelete, setMessages: vi.fn() }),
1435
+ );
1436
+ expect(runtime.messages.map((m) => m.id)).toEqual(["u1", "a1"]);
1437
+ });
1438
+
1439
+ it("evicts when the host publishes the confirming snapshot before onDelete resolves", async () => {
1440
+ let current = [
1441
+ message("u1", "user", "hi"),
1442
+ message("a1", "assistant", "hello"),
1443
+ ];
1444
+ let syncSnapshot!: () => void;
1445
+ const onDelete = vi.fn(async (id: string) => {
1446
+ current = current.filter((m) => m.id !== id);
1447
+ syncSnapshot();
1448
+ await Promise.resolve();
1449
+ });
1450
+ const store = () =>
1451
+ makeStore({ messages: current, onDelete, setMessages: vi.fn() });
1452
+ const runtime = new ExternalStoreThreadRuntimeCore(
1453
+ mockContextProvider,
1454
+ store(),
1455
+ );
1456
+ syncSnapshot = () => runtime.__internal_setAdapter(store());
1457
+
1458
+ await runtime.deleteMessage("u1");
1459
+
1460
+ expect(runtime.getBranches("a1")).toEqual(["a1"]);
1461
+ expect(() => runtime.switchToBranch("u1")).toThrow(
1462
+ "MessageRepository(switchToBranch): Branch not found",
1463
+ );
1464
+ });
1465
+
1466
+ it("keeps the message when onDelete rejects", async () => {
1467
+ const current = [
1468
+ message("u1", "user", "hi"),
1469
+ message("a1", "assistant", "hello"),
1470
+ ];
1471
+ const onDelete = vi.fn(async () => {
1472
+ throw new Error("server down");
1473
+ });
1474
+ const store = () =>
1475
+ makeStore({ messages: current, onDelete, setMessages: vi.fn() });
1476
+ const runtime = new ExternalStoreThreadRuntimeCore(
1477
+ mockContextProvider,
1478
+ store(),
1479
+ );
1480
+
1481
+ await expect(runtime.deleteMessage("u1")).rejects.toThrow("server down");
1482
+
1483
+ runtime.__internal_setAdapter(
1484
+ makeStore({ messages: [...current], onDelete, setMessages: vi.fn() }),
1485
+ );
1486
+ expect(runtime.messages.map((m) => m.id)).toEqual(["u1", "a1"]);
1487
+ expect(runtime.getBranches("a1")).toEqual(["a1"]);
1488
+
1489
+ runtime.__internal_setAdapter(
1490
+ makeStore({
1491
+ messages: [
1492
+ message("u1b", "user", "other"),
1493
+ message("a1b", "assistant", "branch"),
1494
+ ],
1495
+ onDelete,
1496
+ setMessages: vi.fn(),
1497
+ }),
1498
+ );
1499
+ expect(runtime.getBranches("u1b")).toEqual(["u1", "u1b"]);
1500
+ });
1501
+
1502
+ it("does not evict a declined delete when a later mutation changes the branch", async () => {
1503
+ let current = [
1504
+ message("u1", "user", "hi"),
1505
+ message("a1", "assistant", "one"),
1506
+ ];
1507
+ const onDelete = vi.fn(async () => {});
1508
+ const setMessages = vi.fn((m: unknown[]) => {
1509
+ current = m as typeof current;
1510
+ });
1511
+ const store = () => makeStore({ messages: current, onDelete, setMessages });
1512
+ const runtime = new ExternalStoreThreadRuntimeCore(
1513
+ mockContextProvider,
1514
+ store(),
1515
+ );
1516
+
1517
+ current = [current[0]!, message("a2", "assistant", "two")];
1518
+ runtime.__internal_setAdapter(store());
1519
+ expect(runtime.getBranches("a2")).toEqual(["a1", "a2"]);
1520
+
1521
+ await runtime.deleteMessage("a2");
1522
+
1523
+ runtime.switchToBranch("a1");
1524
+ runtime.__internal_setAdapter(store());
1525
+
1526
+ expect(runtime.messages.map((m) => m.id)).toEqual(["u1", "a1"]);
1527
+ expect(runtime.getBranches("a1")).toEqual(["a1", "a2"]);
1528
+ });
1529
+
1530
+ it("leaves positional fallback ids for the snapshot remapping to prune", async () => {
1531
+ const convertMessage = (m: { text: string }): ThreadMessageLike => ({
1532
+ role: "user",
1533
+ content: [{ type: "text", text: m.text }],
1534
+ });
1535
+ let current = [{ text: "first" }, { text: "second" }];
1536
+ const setMessages = vi.fn((m: typeof current) => {
1537
+ current = m;
1538
+ });
1539
+ const store = () =>
1540
+ makeStore({ messages: current, convertMessage, setMessages });
1541
+ const runtime = new ExternalStoreThreadRuntimeCore(
1542
+ mockContextProvider,
1543
+ store(),
1544
+ );
1545
+ const firstId = runtime.messages[0]!.id;
1546
+
1547
+ await runtime.deleteMessage(firstId);
1548
+ runtime.__internal_setAdapter(store());
1549
+
1550
+ expect(runtime.messages).toHaveLength(1);
1551
+ expect(runtime.getBranches(runtime.messages[0]!.id)).toEqual([
1552
+ runtime.messages[0]!.id,
1553
+ ]);
1554
+ });
1555
+ });
1556
+
1557
+ describe("ExternalStoreThreadRuntimeCore - id-less converted messages", () => {
1558
+ const convertMessage = (m: {
1559
+ role?: "user" | "assistant";
1560
+ text: string;
1561
+ }): ThreadMessageLike => ({
1562
+ role: m.role ?? "user",
1563
+ content: [{ type: "text", text: m.text }],
1564
+ });
1565
+
1566
+ const storeWith = (
1567
+ messages: { role?: "user" | "assistant"; text: string }[],
1568
+ isRunning = false,
1569
+ ) => makeStore({ messages, convertMessage, isRunning });
1570
+
1571
+ const textOf = (message: { content: readonly { type: string }[] }) => {
1572
+ const part = message.content[0];
1573
+ return part && part.type === "text" && "text" in part
1574
+ ? part.text
1575
+ : undefined;
1576
+ };
1577
+
1578
+ it("keeps a prepended history message", () => {
1579
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
1580
+ try {
1581
+ const m0 = { text: "older-user" };
1582
+ const m1 = { text: "newer-user" };
1583
+ const m2 = { role: "assistant" as const, text: "newer-assistant" };
1584
+
1585
+ const runtime = new ExternalStoreThreadRuntimeCore(
1586
+ mockContextProvider,
1587
+ storeWith([m1, m2]),
1588
+ );
1589
+ expect(runtime.messages).toHaveLength(2);
1590
+
1591
+ runtime.__internal_setAdapter(storeWith([m0, m1, m2]));
1592
+
1593
+ expect(warn).not.toHaveBeenCalled();
1594
+ expect(runtime.messages).toHaveLength(3);
1595
+ expect(runtime.messages.map(textOf)).toEqual([
1596
+ "older-user",
1597
+ "newer-user",
1598
+ "newer-assistant",
1599
+ ]);
1600
+ } finally {
1601
+ warn.mockRestore();
1602
+ }
1603
+ });
1604
+
1605
+ it("keeps the last message id when its host object is replaced while running", () => {
1606
+ const user = { text: "hi" };
1607
+ const runtime = new ExternalStoreThreadRuntimeCore(
1608
+ mockContextProvider,
1609
+ storeWith([user, { role: "assistant", text: "he" }], true),
1610
+ );
1611
+ const assistantId = runtime.messages[1]!.id;
1612
+
1613
+ runtime.__internal_setAdapter(
1614
+ storeWith([user, { role: "assistant", text: "hel" }], true),
1615
+ );
1616
+ runtime.__internal_setAdapter(
1617
+ storeWith([user, { role: "assistant", text: "hello" }], true),
1618
+ );
1619
+
1620
+ expect(runtime.messages[1]!.id).toBe(assistantId);
1621
+ expect(runtime.getBranches(assistantId)).toEqual([assistantId]);
1622
+ expect(textOf(runtime.messages[1]!)).toBe("hello");
1623
+ });
1624
+
1625
+ it("does not rewrite an explicit host id when the list shifts", () => {
1626
+ const convertWithId = (m: {
1627
+ id?: string;
1628
+ text: string;
1629
+ }): ThreadMessageLike => ({
1630
+ ...(m.id !== undefined ? { id: m.id } : {}),
1631
+ role: "user",
1632
+ content: [{ type: "text", text: m.text }],
1633
+ });
1634
+
1635
+ const explicit = { id: "host-a", text: "kept" };
1636
+ const idLess = { text: "id-less" };
1637
+ const runtime = new ExternalStoreThreadRuntimeCore(
1638
+ mockContextProvider,
1639
+ makeStore({
1640
+ messages: [explicit],
1641
+ convertMessage: convertWithId,
1642
+ }),
1643
+ );
1644
+
1645
+ runtime.__internal_setAdapter(
1646
+ makeStore({
1647
+ messages: [idLess, explicit],
1648
+ convertMessage: convertWithId,
1649
+ }),
1650
+ );
1651
+
1652
+ expect(runtime.messages.map((m) => m.id)).toEqual([
1653
+ expect.stringMatching(/^__external_store_fallback_/),
1654
+ "host-a",
1655
+ ]);
1656
+ expect(runtime.messages.map(textOf)).toEqual(["id-less", "kept"]);
1657
+ });
1658
+ });
1659
+
1660
+ describe("ExternalStoreThreadRuntimeCore - convertMessage auto status", () => {
1661
+ const toolCall = (extra?: Record<string, unknown>) => ({
1662
+ type: "tool-call" as const,
1663
+ toolCallId: "t1",
1664
+ toolName: "search",
1665
+ args: {},
1666
+ argsText: "{}",
1667
+ ...extra,
1668
+ });
1669
+
1670
+ const makeRuntime = (assistant: ThreadMessageLike, isRunning = false) =>
1671
+ new ExternalStoreThreadRuntimeCore(
1672
+ mockContextProvider,
1673
+ makeStore({
1674
+ isRunning,
1675
+ messages: [{ role: "user", content: "run it" }, assistant],
1676
+ convertMessage: (m: ThreadMessageLike) => m,
1677
+ setMessages: vi.fn(),
1678
+ }),
1679
+ );
1680
+
1681
+ it("reports requires-action for a tool call without a result", () => {
1682
+ const runtime = makeRuntime({ role: "assistant", content: [toolCall()] });
1683
+ expect(runtime.messages[1]!.status).toMatchObject({
1684
+ type: "requires-action",
1685
+ reason: "tool-calls",
1686
+ });
1687
+ });
1688
+
1689
+ it("reports an interrupt for an unresolved approval", () => {
1690
+ const runtime = makeRuntime({
1691
+ role: "assistant",
1692
+ content: [toolCall({ approval: { id: "a1" } })],
1693
+ });
1694
+ expect(runtime.messages[1]!.status).toMatchObject({
1695
+ type: "requires-action",
1696
+ reason: "interrupt",
1697
+ });
1698
+ });
1699
+
1700
+ it("reports an interrupt for a human interrupt", () => {
1701
+ const runtime = makeRuntime({
1702
+ role: "assistant",
1703
+ content: [toolCall({ interrupt: { type: "human", payload: {} } })],
1704
+ });
1705
+ expect(runtime.messages[1]!.status).toMatchObject({
1706
+ type: "requires-action",
1707
+ reason: "interrupt",
1708
+ });
1709
+ });
1710
+
1711
+ it("reports complete once the tool call has a result", () => {
1712
+ const runtime = makeRuntime({
1713
+ role: "assistant",
1714
+ content: [toolCall({ result: "ok" })],
1715
+ });
1716
+ expect(runtime.messages[1]!.status).toMatchObject({ type: "complete" });
1717
+ });
1718
+
1719
+ it("keeps the last message running while a tool call is still pending", () => {
1720
+ const runtime = makeRuntime(
1721
+ { role: "assistant", content: [toolCall()] },
1722
+ true,
1723
+ );
1724
+ expect(runtime.messages[1]!.status).toMatchObject({ type: "running" });
1725
+ });
1726
+
1727
+ it("reuses the converted message until its auto status changes", () => {
1728
+ const pending: ThreadMessageLike = {
1729
+ role: "assistant",
1730
+ content: [toolCall()],
1731
+ };
1732
+ const user: ThreadMessageLike = { role: "user", content: "run it" };
1733
+ const convertMessage = vi.fn((m: ThreadMessageLike) => m);
1734
+ const runtime = new ExternalStoreThreadRuntimeCore(
1735
+ mockContextProvider,
1736
+ makeStore({
1737
+ isRunning: false,
1738
+ messages: [user, pending],
1739
+ convertMessage,
1740
+ setMessages: vi.fn(),
1741
+ }),
1742
+ );
1743
+ const first = runtime.messages[1]!;
1744
+ expect(convertMessage).toHaveBeenCalledTimes(2);
1745
+
1746
+ runtime.__internal_setAdapter(
1747
+ makeStore({
1748
+ isRunning: false,
1749
+ messages: [user, pending],
1750
+ convertMessage,
1751
+ setMessages: vi.fn(),
1752
+ }),
1753
+ );
1754
+ expect(runtime.messages[1]).toBe(first);
1755
+ expect(convertMessage).toHaveBeenCalledTimes(2);
1756
+
1757
+ runtime.__internal_setAdapter(
1758
+ makeStore({
1759
+ isRunning: false,
1760
+ messages: [user, { ...pending, content: [toolCall({ result: "ok" })] }],
1761
+ convertMessage,
1762
+ setMessages: vi.fn(),
1763
+ }),
1764
+ );
1765
+ expect(runtime.messages[1]!.status).toMatchObject({ type: "complete" });
1766
+ });
1767
+
1768
+ it("recomputes a cached message's status when the run ends", () => {
1769
+ const pending: ThreadMessageLike = {
1770
+ role: "assistant",
1771
+ content: [toolCall()],
1772
+ };
1773
+ const user: ThreadMessageLike = { role: "user", content: "run it" };
1774
+ const convertMessage = vi.fn((m: ThreadMessageLike) => m);
1775
+ const runtime = new ExternalStoreThreadRuntimeCore(
1776
+ mockContextProvider,
1777
+ makeStore({
1778
+ isRunning: true,
1779
+ messages: [user, pending],
1780
+ convertMessage,
1781
+ setMessages: vi.fn(),
1782
+ }),
1783
+ );
1784
+ expect(runtime.messages[1]!.status).toMatchObject({ type: "running" });
1785
+
1786
+ runtime.__internal_setAdapter(
1787
+ makeStore({
1788
+ isRunning: false,
1789
+ messages: [user, pending],
1790
+ convertMessage,
1791
+ setMessages: vi.fn(),
1792
+ }),
1793
+ );
1794
+ expect(runtime.messages[1]!.status).toMatchObject({
1795
+ type: "requires-action",
1796
+ reason: "tool-calls",
1797
+ });
1798
+ });
1799
+ });