@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
@@ -16,9 +16,13 @@ import type {
16
16
  import {
17
17
  getExternalStoreMessages,
18
18
  bindExternalStoreMessage,
19
+ FALLBACK_ID_PREFIX,
19
20
  } from "../../runtime/utils/external-store-message";
20
21
  import { ThreadMessageConverter } from "./thread-message-converter";
21
- import { getAutoStatus, isAutoStatus } from "../../runtime/utils/auto-status";
22
+ import {
23
+ getContentAutoStatus,
24
+ isAutoStatus,
25
+ } from "../../runtime/utils/auto-status";
22
26
  import {
23
27
  fromThreadMessageLike,
24
28
  type ThreadMessageLike,
@@ -39,8 +43,11 @@ import {
39
43
  MessageRepository,
40
44
  } from "../../runtime/utils/message-repository";
41
45
  import { generateId } from "../../utils/id";
42
- import { ToolInvocationTracker } from "../tool-invocations/ToolInvocationTracker";
43
- import { EMPTY_QUEUE_ITEMS } from "../../store/scopes/queue-item";
46
+ import {
47
+ ToolInvocationTracker,
48
+ type ToolExecutionStatus,
49
+ } from "../tool-invocations/ToolInvocationTracker";
50
+ import { EMPTY_QUEUE_ITEMS } from "../../runtime/queue/queue-item";
44
51
  import type { QuoteInfo } from "../../types/quote";
45
52
  import {
46
53
  captureThreadRuntimeGeneration,
@@ -110,6 +117,7 @@ export class ExternalStoreThreadRuntimeCore
110
117
  }
111
118
  // Unlike `isLoading`: pass `undefined` through to preserve the `getThreadState` fallback.
112
119
  public get isRunning(): boolean | undefined {
120
+ if (this._hasExecutingTools(this._store)) return true;
113
121
  return this._store.isRunning;
114
122
  }
115
123
 
@@ -136,6 +144,16 @@ export class ExternalStoreThreadRuntimeCore
136
144
 
137
145
  private _converter = new ThreadMessageConverter();
138
146
 
147
+ // Ids the host was asked to delete via onDelete. The snapshot pass evicts
148
+ // them from the repository once the host's array no longer carries them;
149
+ // an id the host kept is dropped from the set without eviction.
150
+ // Branch-changing mutations (edit, branch switch, reload) invalidate the
151
+ // set, because after them the incoming array omits off-branch ids for
152
+ // reasons unrelated to deletion. Plain tail sends do not clear: a tail
153
+ // append cannot make a visible id absent, so id-absence stays unambiguous
154
+ // and a delete whose confirmation races a send keeps its eviction.
155
+ private _pendingDeleteEvictions = new Set<string>();
156
+
139
157
  private _store!: ExternalStoreAdapter<any>;
140
158
 
141
159
  private _getInitializePromise?: () => Promise<unknown> | undefined;
@@ -153,6 +171,51 @@ export class ExternalStoreThreadRuntimeCore
153
171
  * snapshot — only when `adapter.unstable_enableToolInvocations === true`.
154
172
  */
155
173
  private _toolInvocations: ToolInvocationTracker | null = null;
174
+ private _toolStatuses: ReadonlyMap<string, ToolExecutionStatus> = new Map();
175
+ private _effectiveIsRunning = false;
176
+ private _inTrackerUpdate = false;
177
+ private _pendingRunningRefresh = false;
178
+
179
+ /**
180
+ * Tracker mutations initiated by this class (setState, reset) can publish
181
+ * status changes synchronously. Re-entering the snapshot pipeline from
182
+ * inside them would feed the tracker a stale snapshot and consume the
183
+ * restore arming a reset just installed, so the running refresh is
184
+ * deferred until the mutation returns and stays off the tracker.
185
+ */
186
+ private _runTrackerUpdate(fn: () => void): void {
187
+ this._inTrackerUpdate = true;
188
+ try {
189
+ fn();
190
+ } finally {
191
+ this._inTrackerUpdate = false;
192
+ }
193
+ if (this._pendingRunningRefresh) {
194
+ this._pendingRunningRefresh = false;
195
+ this._refreshEffectiveIsRunning();
196
+ }
197
+ }
198
+
199
+ private _refreshEffectiveIsRunning(): void {
200
+ const isRunning = this._getEffectiveIsRunning(this._store);
201
+ if (this._effectiveIsRunning === isRunning) return;
202
+ this._effectiveIsRunning = isRunning;
203
+ this._notifyEventSubscribers(isRunning ? "runStart" : "runEnd", {});
204
+ this._notifySubscribers();
205
+ }
206
+
207
+ private _hasExecutingTools(store: ExternalStoreAdapter<any>): boolean {
208
+ if (store.unstable_enableToolInvocations !== true) return false;
209
+ if (this._toolInvocations === null) return false;
210
+ for (const status of this._toolStatuses.values()) {
211
+ if (status.type === "executing") return true;
212
+ }
213
+ return false;
214
+ }
215
+
216
+ private _getEffectiveIsRunning(store: ExternalStoreAdapter<any>): boolean {
217
+ return (store.isRunning ?? false) || this._hasExecutingTools(store);
218
+ }
156
219
 
157
220
  public override beginEdit(messageId: string) {
158
221
  if (!this._store.onEdit)
@@ -172,12 +235,17 @@ export class ExternalStoreThreadRuntimeCore
172
235
  public __internal_setAdapter(store: ExternalStoreAdapter<any>) {
173
236
  if (this._store === store) return;
174
237
 
175
- const isRunning = store.isRunning ?? false;
238
+ this._updateStoreSnapshot(store);
239
+ }
240
+
241
+ private _updateStoreSnapshot(store: ExternalStoreAdapter<any>) {
242
+ const previousIsRunning = this._effectiveIsRunning;
176
243
  this.isDisabled = store.isDisabled ?? false;
177
244
  this.isSendDisabled = store.isSendDisabled ?? false;
178
245
 
179
246
  const oldStore = this._store as ExternalStoreAdapter<any> | undefined;
180
247
  this._store = store;
248
+ const isRunning = this._getEffectiveIsRunning(store);
181
249
  if (oldStore?.queue !== store.queue) {
182
250
  this._transformedQueue = undefined;
183
251
  store.queue?.__internal_setDispatchTransform?.((message) => {
@@ -229,7 +297,8 @@ export class ExternalStoreThreadRuntimeCore
229
297
  if (
230
298
  oldStore &&
231
299
  oldStore.isRunning === store.isRunning &&
232
- oldStore.messageRepository === store.messageRepository
300
+ oldStore.messageRepository === store.messageRepository &&
301
+ previousIsRunning === isRunning
233
302
  ) {
234
303
  this._notifySubscribers();
235
304
  return;
@@ -252,6 +321,7 @@ export class ExternalStoreThreadRuntimeCore
252
321
  this.repository.deleteMessage(message.id);
253
322
  }
254
323
  }
324
+ this._pendingDeleteEvictions.clear();
255
325
  this.repository.resetHead(headId);
256
326
  messages = this.repository.getMessages();
257
327
  }
@@ -264,7 +334,8 @@ export class ExternalStoreThreadRuntimeCore
264
334
  this._converter = new ThreadMessageConverter();
265
335
  } else if (
266
336
  oldStore.isRunning === store.isRunning &&
267
- oldStore.messages === store.messages
337
+ oldStore.messages === store.messages &&
338
+ previousIsRunning === isRunning
268
339
  ) {
269
340
  this._notifySubscribers();
270
341
  // no conversion update
@@ -278,27 +349,31 @@ export class ExternalStoreThreadRuntimeCore
278
349
  if (!store.convertMessage) return m;
279
350
 
280
351
  const isLast = idx === (store.messages?.length ?? 0) - 1;
281
- const autoStatus = getAutoStatus(
282
- isLast,
283
- isRunning,
284
- false,
285
- false,
286
- undefined,
287
- );
352
+ const fallbackId = `${FALLBACK_ID_PREFIX}${idx}`;
288
353
 
289
354
  if (
290
355
  cache &&
291
356
  (cache.role !== "assistant" ||
292
357
  !isAutoStatus(cache.status) ||
293
- cache.status === autoStatus)
294
- )
358
+ cache.status ===
359
+ getContentAutoStatus(cache.content, isLast, isRunning))
360
+ ) {
361
+ if (
362
+ cache.id.startsWith(FALLBACK_ID_PREFIX) &&
363
+ cache.id !== fallbackId
364
+ ) {
365
+ const updated = { ...cache, id: fallbackId };
366
+ bindExternalStoreMessage(updated, m);
367
+ return updated;
368
+ }
295
369
  return cache;
370
+ }
296
371
 
297
372
  const messageLike = store.convertMessage(m, idx);
298
373
  const newMessage = fromThreadMessageLike(
299
374
  messageLike,
300
- idx.toString(),
301
- autoStatus,
375
+ fallbackId,
376
+ getContentAutoStatus(messageLike.content, isLast, isRunning),
302
377
  );
303
378
  bindExternalStoreMessage(newMessage, m);
304
379
  return newMessage;
@@ -324,6 +399,20 @@ export class ExternalStoreThreadRuntimeCore
324
399
  const parent = messages[i - 1];
325
400
  this.repository.addOrUpdateMessage(parent?.id ?? null, message);
326
401
  }
402
+
403
+ if (this._pendingDeleteEvictions.size > 0) {
404
+ const incomingIds = new Set(messages.map((m) => m.id));
405
+ for (const id of this._pendingDeleteEvictions) {
406
+ this._pendingDeleteEvictions.delete(id);
407
+ if (incomingIds.has(id)) continue;
408
+ try {
409
+ this.repository.getMessage(id);
410
+ } catch {
411
+ continue;
412
+ }
413
+ this.repository.deleteMessage(id);
414
+ }
415
+ }
327
416
  } else {
328
417
  throw new Error(
329
418
  "ExternalStoreAdapter must provide either 'messages' or 'messageRepository'",
@@ -333,8 +422,9 @@ export class ExternalStoreThreadRuntimeCore
333
422
  // Common logic for both paths
334
423
  if (messages.length > 0) this.ensureInitialized();
335
424
 
336
- if ((oldStore?.isRunning ?? false) !== (store.isRunning ?? false)) {
337
- if (store.isRunning) {
425
+ this._effectiveIsRunning = isRunning;
426
+ if (previousIsRunning !== isRunning) {
427
+ if (isRunning) {
338
428
  this._notifyEventSubscribers("runStart", {});
339
429
  } else {
340
430
  this._notifyEventSubscribers("runEnd", {});
@@ -361,7 +451,7 @@ export class ExternalStoreThreadRuntimeCore
361
451
 
362
452
  this._messages = this.repository.getMessages();
363
453
 
364
- this._driveToolInvocations();
454
+ this._runTrackerUpdate(() => this._driveToolInvocations());
365
455
 
366
456
  this._notifySubscribers();
367
457
  }
@@ -380,6 +470,7 @@ export class ExternalStoreThreadRuntimeCore
380
470
  if (this._toolInvocations) {
381
471
  this._toolInvocations.reset();
382
472
  this._toolInvocations = null;
473
+ this._toolStatuses = new Map();
383
474
  this._store.setToolStatuses?.({});
384
475
  }
385
476
  return;
@@ -423,7 +514,19 @@ export class ExternalStoreThreadRuntimeCore
423
514
  }
424
515
  },
425
516
  onStatusesChange: (statuses) => {
426
- this._store.setToolStatuses?.(Object.fromEntries(statuses));
517
+ const hadExecutingTools = this._hasExecutingTools(this._store);
518
+ this._toolStatuses = statuses;
519
+ try {
520
+ this._store.setToolStatuses?.(Object.fromEntries(statuses));
521
+ } finally {
522
+ if (hadExecutingTools !== this._hasExecutingTools(this._store)) {
523
+ if (this._inTrackerUpdate) {
524
+ this._pendingRunningRefresh = true;
525
+ } else {
526
+ this._updateStoreSnapshot(this._store);
527
+ }
528
+ }
529
+ }
427
530
  },
428
531
  },
429
532
  );
@@ -431,7 +534,7 @@ export class ExternalStoreThreadRuntimeCore
431
534
 
432
535
  this._toolInvocations.setState({
433
536
  messages: this._messages,
434
- isRunning: this._store.isRunning ?? false,
537
+ isRunning: this._getEffectiveIsRunning(this._store),
435
538
  ...(this._store.isLoading !== undefined && {
436
539
  isLoading: this._store.isLoading,
437
540
  }),
@@ -475,7 +578,7 @@ export class ExternalStoreThreadRuntimeCore
475
578
  throw new Error("Runtime does not support switching branches.");
476
579
 
477
580
  // Silently ignore branch switches while running
478
- if (this._store.isRunning) {
581
+ if (this._getEffectiveIsRunning(this._store)) {
479
582
  return;
480
583
  }
481
584
 
@@ -485,6 +588,7 @@ export class ExternalStoreThreadRuntimeCore
485
588
  : null;
486
589
 
487
590
  this.repository.switchToBranch(branchId);
591
+ this._pendingDeleteEvictions.clear();
488
592
  this.updateMessages(this.repository.getMessages());
489
593
  if (onBranchChange) {
490
594
  this._notifyBranchChange(previousHeadId, onBranchChange);
@@ -547,7 +651,7 @@ export class ExternalStoreThreadRuntimeCore
547
651
  // Buffering does not start a run, so the tool-abort below must wait
548
652
  // until the queue flushes. By then the prior run (and its tools) has
549
653
  // settled.
550
- if (message.steer ?? this._store.isRunning ?? false)
654
+ if (message.steer ?? this._getEffectiveIsRunning(this._store))
551
655
  this._store.queue.steer(message);
552
656
  else this._store.queue.enqueue(message);
553
657
  return;
@@ -574,6 +678,7 @@ export class ExternalStoreThreadRuntimeCore
574
678
  if (isEdit) {
575
679
  if (!this._store.onEdit)
576
680
  throw new Error("Runtime does not support editing messages.");
681
+ this._pendingDeleteEvictions.clear();
577
682
  await this._store.onEdit(message);
578
683
  } else {
579
684
  await this._store.onNew(message);
@@ -582,14 +687,29 @@ export class ExternalStoreThreadRuntimeCore
582
687
 
583
688
  public async deleteMessage(messageId: string): Promise<void> {
584
689
  if (this._store.onDelete) {
585
- await this._store.onDelete(messageId);
690
+ // The host owns deletion here, and it may decline (fail a server call,
691
+ // cancel a confirm dialog, ignore an off-branch id). The eviction is
692
+ // therefore deferred to the snapshot pass, which evicts only once the
693
+ // host's own array no longer carries the id. Registered before the
694
+ // callback because an optimistic host publishes that snapshot while
695
+ // the callback is still awaited.
696
+ const wasVisible = this.repository
697
+ .getMessages()
698
+ .some((m) => m.id === messageId);
699
+ if (wasVisible) this._pendingDeleteEvictions.add(messageId);
700
+ try {
701
+ await this._store.onDelete(messageId);
702
+ } catch (error) {
703
+ this._pendingDeleteEvictions.delete(messageId);
704
+ throw error;
705
+ }
586
706
  return;
587
707
  }
588
708
 
589
709
  if (!this._store.setMessages)
590
710
  throw new Error("Runtime does not support deleting messages.");
591
711
 
592
- if (this._store.isRunning) {
712
+ if (this._getEffectiveIsRunning(this._store)) {
593
713
  await this._toolInvocations?.abort();
594
714
  }
595
715
 
@@ -597,7 +717,32 @@ export class ExternalStoreThreadRuntimeCore
597
717
  const messageIndex = messages.findIndex((m) => m.id === messageId);
598
718
  if (messageIndex === -1) throw new Error("Message not found.");
599
719
 
720
+ this._pendingDeleteEvictions.clear();
600
721
  this.updateMessages(messages.filter((message) => message.id !== messageId));
722
+ this._evictDeletedMessage(messageId);
723
+ }
724
+
725
+ // The snapshot pass only relinks incoming messages; it never evicts, so
726
+ // without this the deleted message survives as a sibling branch that the
727
+ // branch picker can resurrect into the host store. `_messages` is refreshed
728
+ // before notifying so `messages` and the branch graph agree at notify time,
729
+ // mirroring the end of the snapshot pass.
730
+ private _evictDeletedMessage(messageId: string) {
731
+ // Positional fallback ids are remapped in the snapshot pass; evicting
732
+ // them first leaves the pre-renumber node as a sibling of the new head.
733
+ if (messageId.startsWith(FALLBACK_ID_PREFIX)) return;
734
+ // A synchronous host update (e.g. a setMessages that re-entered the
735
+ // snapshot pass) may have evicted the message already; only that case is
736
+ // skipped, so genuine repository errors still propagate.
737
+ try {
738
+ this.repository.getMessage(messageId);
739
+ } catch {
740
+ return;
741
+ }
742
+
743
+ this.repository.deleteMessage(messageId);
744
+ this._messages = this.repository.getMessages();
745
+ this._notifySubscribers();
601
746
  }
602
747
 
603
748
  public getQueueItems() {
@@ -622,6 +767,8 @@ export class ExternalStoreThreadRuntimeCore
622
767
  if (!this._store.onReload)
623
768
  throw new Error("Runtime does not support reloading messages.");
624
769
 
770
+ this._pendingDeleteEvictions.clear();
771
+
625
772
  // Auto-abort in-flight client-side tool executions when a run reloads;
626
773
  // any results that land afterward would target a turn that no longer
627
774
  // exists. See `append` above for full rationale.
@@ -655,7 +802,7 @@ export class ExternalStoreThreadRuntimeCore
655
802
  // back here via __internal_setAdapter. The tracker publishes the
656
803
  // cleared status map itself, so adapter-side statuses reset only when
657
804
  // the tracker is the source of truth.
658
- this._toolInvocations?.reset();
805
+ this._runTrackerUpdate(() => this._toolInvocations?.reset());
659
806
 
660
807
  this._store.onLoadExternalState(state);
661
808
  }
@@ -666,7 +813,7 @@ export class ExternalStoreThreadRuntimeCore
666
813
  * without run-cancel semantics (`onCancel`, composer draft restoration).
667
814
  */
668
815
  public unstable_notifySessionReset(): void {
669
- this._toolInvocations?.reset();
816
+ this._runTrackerUpdate(() => this._toolInvocations?.reset());
670
817
  this._store.queue?.__internal_notifyCancelled?.();
671
818
  }
672
819
 
@@ -7,7 +7,11 @@ import type {
7
7
  } from "../../runtime/utils/chat-model-adapter";
8
8
  import type { AppendMessage } from "../../types/message";
9
9
  import type { LocalRuntimeOptionsBase } from "./local-runtime-options";
10
- import type { ExportedMessageRepositoryItem } from "../../runtime/utils/message-repository";
10
+ import {
11
+ ExportedMessageRepository,
12
+ type ExportedMessageRepositoryItem,
13
+ } from "../../runtime/utils/message-repository";
14
+ import type { ThreadMessageLike } from "../../runtime/utils/thread-message-like";
11
15
  import type { ThreadSuggestion } from "../../runtime/interfaces/thread-runtime-core";
12
16
  import { isMessageNotSentError } from "../../types/error";
13
17
 
@@ -2002,3 +2006,170 @@ describe("LocalRuntimeCore composer.canCancel", () => {
2002
2006
  thread.cancelRun();
2003
2007
  });
2004
2008
  });
2009
+
2010
+ describe("LocalThreadRuntimeCore imported approvals", () => {
2011
+ const pausedOnApproval = (
2012
+ approval: { id: string } | undefined,
2013
+ extra: Record<string, unknown> = {},
2014
+ ): ThreadMessageLike[] => [
2015
+ { role: "user", content: [{ type: "text", text: "send an email" }] },
2016
+ {
2017
+ role: "assistant",
2018
+ content: [
2019
+ {
2020
+ type: "tool-call",
2021
+ toolCallId: "call-send_email",
2022
+ toolName: "send_email",
2023
+ args: {},
2024
+ argsText: "{}",
2025
+ ...(approval !== undefined ? { approval } : {}),
2026
+ ...extra,
2027
+ },
2028
+ ],
2029
+ },
2030
+ ];
2031
+
2032
+ const createImportedThread = (
2033
+ initialMessages: ThreadMessageLike[],
2034
+ history?: LocalRuntimeOptionsBase["adapters"]["history"],
2035
+ ) => {
2036
+ const runs: ChatModelRunOptions[] = [];
2037
+ const core = new LocalRuntimeCore(
2038
+ {
2039
+ adapters: {
2040
+ chatModel: {
2041
+ async run(options) {
2042
+ runs.push(options);
2043
+ return { content: [{ type: "text", text: "done" }] };
2044
+ },
2045
+ },
2046
+ ...(history !== undefined && { history }),
2047
+ },
2048
+ unstable_humanToolNames: ["send_email"],
2049
+ },
2050
+ initialMessages,
2051
+ );
2052
+ return { thread: core.threads.getMainThreadRuntimeCore(), runs };
2053
+ };
2054
+
2055
+ it("resumes the run after approving an imported pending approval", async () => {
2056
+ const { thread, runs } = createImportedThread(
2057
+ pausedOnApproval({ id: "a1" }),
2058
+ );
2059
+
2060
+ expect(thread.messages.at(-1)?.status).toMatchObject({
2061
+ type: "requires-action",
2062
+ reason: "tool-calls",
2063
+ });
2064
+
2065
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
2066
+ await flush();
2067
+
2068
+ expect(runs).toHaveLength(1);
2069
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
2070
+ });
2071
+
2072
+ it("keeps the interrupt reason for an imported interrupted tool call", () => {
2073
+ const { thread } = createImportedThread(
2074
+ pausedOnApproval(undefined, {
2075
+ interrupt: { type: "human", payload: { question: "which one?" } },
2076
+ }),
2077
+ );
2078
+
2079
+ expect(thread.messages.at(-1)?.status).toMatchObject({
2080
+ type: "requires-action",
2081
+ reason: "interrupt",
2082
+ });
2083
+ });
2084
+
2085
+ it("resumes the run after approving an approval restored by the history adapter", async () => {
2086
+ const { thread, runs } = createImportedThread([], {
2087
+ async load() {
2088
+ return ExportedMessageRepository.fromArray(
2089
+ pausedOnApproval({ id: "a1" }),
2090
+ );
2091
+ },
2092
+ async append() {},
2093
+ async update() {},
2094
+ });
2095
+
2096
+ thread.__internal_load();
2097
+ await flush();
2098
+
2099
+ expect(thread.messages.at(-1)?.status).toMatchObject({
2100
+ type: "requires-action",
2101
+ reason: "tool-calls",
2102
+ });
2103
+
2104
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
2105
+ await flush();
2106
+
2107
+ expect(runs).toHaveLength(1);
2108
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
2109
+ });
2110
+
2111
+ it("normalizes a serialized approval pause that lost its auto-status marker", async () => {
2112
+ const { thread, runs } = createImportedThread([]);
2113
+ thread.import(
2114
+ JSON.parse(
2115
+ JSON.stringify(
2116
+ ExportedMessageRepository.fromArray(pausedOnApproval({ id: "a1" })),
2117
+ ),
2118
+ ),
2119
+ );
2120
+
2121
+ expect(thread.messages.at(-1)?.status).toMatchObject({
2122
+ type: "requires-action",
2123
+ reason: "tool-calls",
2124
+ });
2125
+
2126
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
2127
+ await flush();
2128
+
2129
+ expect(runs).toHaveLength(1);
2130
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
2131
+ });
2132
+
2133
+ it("completes an imported interrupt that has nothing left to act on", () => {
2134
+ const { thread } = createImportedThread([
2135
+ { role: "user", content: [{ type: "text", text: "send an email" }] },
2136
+ {
2137
+ role: "assistant",
2138
+ status: { type: "requires-action", reason: "interrupt" },
2139
+ content: [
2140
+ {
2141
+ type: "tool-call",
2142
+ toolCallId: "call-send_email",
2143
+ toolName: "send_email",
2144
+ args: {},
2145
+ argsText: "{}",
2146
+ result: { sent: true },
2147
+ },
2148
+ ],
2149
+ } as ThreadMessageLike,
2150
+ ]);
2151
+
2152
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
2153
+ });
2154
+
2155
+ it("resumes the run after adding a result to an imported resultless tool call", async () => {
2156
+ const { thread, runs } = createImportedThread(pausedOnApproval(undefined));
2157
+
2158
+ expect(thread.messages.at(-1)?.status).toMatchObject({
2159
+ type: "requires-action",
2160
+ reason: "tool-calls",
2161
+ });
2162
+
2163
+ thread.addToolResult({
2164
+ messageId: thread.messages.at(-1)!.id,
2165
+ toolCallId: "call-send_email",
2166
+ toolName: "send_email",
2167
+ result: { sent: true },
2168
+ isError: false,
2169
+ });
2170
+ await flush();
2171
+
2172
+ expect(runs).toHaveLength(1);
2173
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
2174
+ });
2175
+ });
@@ -5,6 +5,8 @@ import type {
5
5
  ChatModelRunResult,
6
6
  } from "../../runtime/utils/chat-model-adapter";
7
7
  import { shouldContinue } from "./should-continue";
8
+ import { getAutoStatus } from "../../runtime/utils/auto-status";
9
+ import type { ExportedMessageRepository } from "../../runtime/utils/message-repository";
8
10
  import type { LocalRuntimeOptionsBase } from "./local-runtime-options";
9
11
  import { consumeSuggestionResult } from "../../adapters/suggestion";
10
12
  import type {
@@ -21,7 +23,7 @@ import type {
21
23
  AppendMessage,
22
24
  ThreadAssistantMessage,
23
25
  } from "../../types/message";
24
- import type { RunConfig } from "../../types/message";
26
+ import type { RunConfig, ThreadMessage } from "../../types/message";
25
27
  import { MessageNotSentError, toAssistantError } from "../../types/error";
26
28
  import type { ModelContextProvider } from "../../model-context/types";
27
29
  import {
@@ -32,7 +34,7 @@ import type { QueuePlacement } from "../../runtime/queue/external-thread-queue-a
32
34
  import {
33
35
  EMPTY_QUEUE_ITEMS,
34
36
  type QueueItemState,
35
- } from "../../store/scopes/queue-item";
37
+ } from "../../runtime/queue/queue-item";
36
38
  import {
37
39
  captureThreadRuntimeGeneration,
38
40
  invalidateThreadRuntime,
@@ -49,6 +51,45 @@ class AbortError extends Error {
49
51
  }
50
52
  }
51
53
 
54
+ // `shouldContinue` resumes only on `tool-calls` and `resumeToolCall` throws, so
55
+ // an imported `interrupt` with no interrupt payload would be stranded here.
56
+ // Provenance cannot gate it: a repository that went through JSON has no marker.
57
+ // The replacement stays content-derived, so a message with nothing resultless
58
+ // left to act on lands on `complete` rather than an unactionable pause.
59
+ const withLocalPauseReason = (message: ThreadMessage): ThreadMessage => {
60
+ if (
61
+ message.role !== "assistant" ||
62
+ message.status.type !== "requires-action" ||
63
+ message.status.reason !== "interrupt" ||
64
+ message.content.some(
65
+ (c) =>
66
+ c.type === "tool-call" && c.result === undefined && c.interrupt != null,
67
+ )
68
+ )
69
+ return message;
70
+ return {
71
+ ...message,
72
+ status: getAutoStatus(
73
+ false,
74
+ false,
75
+ false,
76
+ message.content.some(
77
+ (c) => c.type === "tool-call" && c.result === undefined,
78
+ ),
79
+ ),
80
+ };
81
+ };
82
+
83
+ const withLocalPauseReasons = (
84
+ data: ExportedMessageRepository,
85
+ ): ExportedMessageRepository => ({
86
+ ...data,
87
+ messages: data.messages.map((item) => ({
88
+ ...item,
89
+ message: withLocalPauseReason(item.message),
90
+ })),
91
+ });
92
+
52
93
  export class LocalThreadRuntimeCore
53
94
  extends BaseThreadRuntimeCore
54
95
  implements ThreadRuntimeCore
@@ -272,7 +313,7 @@ export class LocalThreadRuntimeCore
272
313
  this._loadPromise = promise
273
314
  .then((repo) => {
274
315
  if (!repo) return;
275
- this.repository.import(repo);
316
+ this.repository.import(withLocalPauseReasons(repo));
276
317
  if (repo.messages.length > 0) {
277
318
  this.ensureInitialized();
278
319
  }
@@ -434,6 +475,10 @@ export class LocalThreadRuntimeCore
434
475
  throw new Error("Runtime does not support exporting external states.");
435
476
  }
436
477
 
478
+ public override import(data: ExportedMessageRepository) {
479
+ super.import(withLocalPauseReasons(data));
480
+ }
481
+
437
482
  public importExternalState(): void {
438
483
  throw new Error("Runtime does not support importing external states.");
439
484
  }