@convex-dev/agent 0.6.4 → 0.7.0-alpha.0

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 (225) hide show
  1. package/MIGRATION.md +60 -116
  2. package/dist/client/messages.d.ts +1 -398
  3. package/dist/client/messages.d.ts.map +1 -1
  4. package/dist/client/messages.js +3 -47
  5. package/dist/client/messages.js.map +1 -1
  6. package/dist/client/types.d.ts +1 -455
  7. package/dist/client/types.d.ts.map +1 -1
  8. package/dist/component/_generated/component.d.ts +805 -45
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/messages.d.ts +1252 -201
  11. package/dist/component/messages.d.ts.map +1 -1
  12. package/dist/component/messages.js +49 -23
  13. package/dist/component/messages.js.map +1 -1
  14. package/dist/component/schema.d.ts +2593 -332
  15. package/dist/component/schema.d.ts.map +1 -1
  16. package/dist/component/streams.d.ts +7 -1
  17. package/dist/component/streams.d.ts.map +1 -1
  18. package/dist/component/streams.js +24 -30
  19. package/dist/component/streams.js.map +1 -1
  20. package/dist/component/threads.d.ts.map +1 -1
  21. package/dist/component/threads.js +1 -2
  22. package/dist/component/threads.js.map +1 -1
  23. package/dist/react/index.d.ts +5 -5
  24. package/dist/react/index.d.ts.map +1 -1
  25. package/dist/react/index.js +5 -5
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/shared.d.ts +19 -7
  28. package/dist/shared.d.ts.map +1 -1
  29. package/dist/shared.js +4 -4
  30. package/dist/shared.js.map +1 -1
  31. package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
  32. package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
  33. package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
  34. package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
  35. package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
  36. package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
  37. package/dist/streaming/persistedUIMessageChunks.js +475 -0
  38. package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
  39. package/dist/validators.d.ts +10026 -1055
  40. package/dist/validators.d.ts.map +1 -1
  41. package/dist/validators.js +71 -9
  42. package/dist/validators.js.map +1 -1
  43. package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
  44. package/dist/vercel/UIMessages.d.ts.map +1 -0
  45. package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
  46. package/dist/vercel/UIMessages.js.map +1 -0
  47. package/dist/{client → vercel/client}/createTool.d.ts +23 -16
  48. package/dist/vercel/client/createTool.d.ts.map +1 -0
  49. package/dist/{client → vercel/client}/createTool.js +2 -0
  50. package/dist/vercel/client/createTool.js.map +1 -0
  51. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +533 -79
  52. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
  53. package/dist/{client → vercel/client}/definePlaygroundAPI.js +8 -7
  54. package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
  55. package/dist/{client → vercel/client}/files.d.ts +2 -2
  56. package/dist/vercel/client/files.d.ts.map +1 -0
  57. package/dist/vercel/client/files.js.map +1 -0
  58. package/dist/vercel/client/messages.d.ts +542 -0
  59. package/dist/vercel/client/messages.d.ts.map +1 -0
  60. package/dist/vercel/client/messages.js +66 -0
  61. package/dist/vercel/client/messages.js.map +1 -0
  62. package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
  63. package/dist/vercel/client/mockModel.d.ts.map +1 -0
  64. package/dist/{client → vercel/client}/mockModel.js +12 -13
  65. package/dist/vercel/client/mockModel.js.map +1 -0
  66. package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
  67. package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
  68. package/dist/vercel/client/saveInputMessages.js.map +1 -0
  69. package/dist/{client → vercel/client}/search.d.ts +119 -22
  70. package/dist/vercel/client/search.d.ts.map +1 -0
  71. package/dist/{client → vercel/client}/search.js +2 -2
  72. package/dist/vercel/client/search.js.map +1 -0
  73. package/dist/{client → vercel/client}/start.d.ts +26 -11
  74. package/dist/vercel/client/start.d.ts.map +1 -0
  75. package/dist/{client → vercel/client}/start.js +64 -22
  76. package/dist/vercel/client/start.js.map +1 -0
  77. package/dist/{client → vercel/client}/streamText.d.ts +13 -12
  78. package/dist/vercel/client/streamText.d.ts.map +1 -0
  79. package/dist/{client → vercel/client}/streamText.js +43 -9
  80. package/dist/vercel/client/streamText.js.map +1 -0
  81. package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
  82. package/dist/vercel/client/streaming.d.ts.map +1 -0
  83. package/dist/{client → vercel/client}/streaming.js +120 -52
  84. package/dist/vercel/client/streaming.js.map +1 -0
  85. package/dist/vercel/client/types.d.ts +456 -0
  86. package/dist/vercel/client/types.d.ts.map +1 -0
  87. package/dist/vercel/client/types.js +3 -0
  88. package/dist/vercel/client/types.js.map +1 -0
  89. package/dist/{client → vercel/client}/utils.d.ts +3 -2
  90. package/dist/vercel/client/utils.d.ts.map +1 -0
  91. package/dist/{client → vercel/client}/utils.js +2 -2
  92. package/dist/vercel/client/utils.js.map +1 -0
  93. package/dist/{deltas.d.ts → vercel/deltas.d.ts} +3 -4
  94. package/dist/vercel/deltas.d.ts.map +1 -0
  95. package/dist/{deltas.js → vercel/deltas.js} +195 -99
  96. package/dist/vercel/deltas.js.map +1 -0
  97. package/dist/{client → vercel}/index.d.ts +482 -106
  98. package/dist/vercel/index.d.ts.map +1 -0
  99. package/dist/{client → vercel}/index.js +64 -46
  100. package/dist/vercel/index.js.map +1 -0
  101. package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -19
  102. package/dist/vercel/mapping.d.ts.map +1 -0
  103. package/dist/{mapping.js → vercel/mapping.js} +411 -114
  104. package/dist/vercel/mapping.js.map +1 -0
  105. package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
  106. package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
  107. package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
  108. package/dist/{react → vercel/react}/types.d.ts +1 -1
  109. package/dist/vercel/react/types.d.ts.map +1 -0
  110. package/dist/{react → vercel/react}/types.js.map +1 -1
  111. package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
  112. package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
  113. package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
  114. package/dist/vercel/react/useDeltaStreams.js.map +1 -0
  115. package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
  116. package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
  117. package/dist/{react → vercel/react}/useStreamingUIMessages.js +7 -3
  118. package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
  119. package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
  120. package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
  121. package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
  122. package/dist/vercel/react/useThreadMessages.js.map +1 -0
  123. package/dist/{react → vercel/react}/useUIMessages.d.ts +1 -1
  124. package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
  125. package/dist/{react → vercel/react}/useUIMessages.js +1 -1
  126. package/dist/vercel/react/useUIMessages.js.map +1 -0
  127. package/package.json +29 -22
  128. package/src/client/messages.ts +5 -107
  129. package/src/client/types.ts +0 -608
  130. package/src/component/_generated/component.ts +1089 -48
  131. package/src/component/messages.test.ts +273 -1
  132. package/src/component/messages.ts +62 -32
  133. package/src/component/streams.ts +47 -47
  134. package/src/component/threads.ts +5 -2
  135. package/src/eslintConfig.test.ts +44 -0
  136. package/src/react/index.ts +8 -5
  137. package/src/shared.ts +26 -36
  138. package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
  139. package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
  140. package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
  141. package/src/streaming/persistedUIMessageChunks.ts +658 -0
  142. package/src/validators.test.ts +4 -1
  143. package/src/validators.ts +89 -8
  144. package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
  145. package/src/{UIMessages.ts → vercel/UIMessages.ts} +95 -3
  146. package/src/{client → vercel/client}/approval.test.ts +23 -13
  147. package/src/{client → vercel/client}/createTool.ts +62 -22
  148. package/src/{client → vercel/client}/definePlaygroundAPI.ts +15 -16
  149. package/src/{client → vercel/client}/files.ts +2 -2
  150. package/src/{client → vercel/client}/index.test.ts +99 -31
  151. package/src/vercel/client/messages.ts +182 -0
  152. package/src/{client → vercel/client}/mockModel.ts +38 -39
  153. package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
  154. package/src/{client → vercel/client}/saveInputMessages.ts +3 -3
  155. package/src/{client → vercel/client}/search.test.ts +3 -3
  156. package/src/{client → vercel/client}/search.ts +4 -4
  157. package/src/{client → vercel/client}/setup.test.ts +6 -3
  158. package/src/vercel/client/start.test.ts +14 -0
  159. package/src/{client → vercel/client}/start.ts +147 -36
  160. package/src/vercel/client/streamText.test.ts +223 -0
  161. package/src/{client → vercel/client}/streamText.ts +102 -31
  162. package/src/{client → vercel/client}/streaming.integration.test.ts +6 -3
  163. package/src/{client → vercel/client}/streaming.test.ts +178 -4
  164. package/src/{client → vercel/client}/streaming.ts +114 -52
  165. package/src/vercel/client/types.test.ts +13 -0
  166. package/src/vercel/client/types.ts +679 -0
  167. package/src/{client → vercel/client}/utils.ts +13 -7
  168. package/src/{deltas.test.ts → vercel/deltas.test.ts} +384 -93
  169. package/src/{deltas.ts → vercel/deltas.ts} +218 -115
  170. package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
  171. package/src/{client → vercel}/index.ts +167 -84
  172. package/src/{mapping.test.ts → vercel/mapping.test.ts} +377 -92
  173. package/src/{mapping.ts → vercel/mapping.ts} +501 -137
  174. package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
  175. package/src/{react → vercel/react}/types.ts +1 -1
  176. package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
  177. package/src/{react → vercel/react}/useStreamingUIMessages.ts +7 -3
  178. package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
  179. package/src/{react → vercel/react}/useUIMessages.ts +2 -2
  180. package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
  181. package/dist/UIMessages.d.ts.map +0 -1
  182. package/dist/UIMessages.js.map +0 -1
  183. package/dist/client/createTool.d.ts.map +0 -1
  184. package/dist/client/createTool.js.map +0 -1
  185. package/dist/client/definePlaygroundAPI.js.map +0 -1
  186. package/dist/client/files.d.ts.map +0 -1
  187. package/dist/client/files.js.map +0 -1
  188. package/dist/client/index.d.ts.map +0 -1
  189. package/dist/client/index.js.map +0 -1
  190. package/dist/client/mockModel.d.ts.map +0 -1
  191. package/dist/client/mockModel.js.map +0 -1
  192. package/dist/client/saveInputMessages.d.ts.map +0 -1
  193. package/dist/client/saveInputMessages.js.map +0 -1
  194. package/dist/client/search.d.ts.map +0 -1
  195. package/dist/client/search.js.map +0 -1
  196. package/dist/client/start.d.ts.map +0 -1
  197. package/dist/client/start.js.map +0 -1
  198. package/dist/client/streamText.d.ts.map +0 -1
  199. package/dist/client/streamText.js.map +0 -1
  200. package/dist/client/streaming.d.ts.map +0 -1
  201. package/dist/client/streaming.js.map +0 -1
  202. package/dist/client/utils.d.ts.map +0 -1
  203. package/dist/client/utils.js.map +0 -1
  204. package/dist/deltas.d.ts.map +0 -1
  205. package/dist/deltas.js.map +0 -1
  206. package/dist/mapping.d.ts.map +0 -1
  207. package/dist/mapping.js.map +0 -1
  208. package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
  209. package/dist/react/optimisticallySendMessage.js.map +0 -1
  210. package/dist/react/types.d.ts.map +0 -1
  211. package/dist/react/useDeltaStreams.d.ts.map +0 -1
  212. package/dist/react/useDeltaStreams.js.map +0 -1
  213. package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
  214. package/dist/react/useStreamingUIMessages.js.map +0 -1
  215. package/dist/react/useThreadMessages.d.ts.map +0 -1
  216. package/dist/react/useThreadMessages.js.map +0 -1
  217. package/dist/react/useUIMessages.d.ts.map +0 -1
  218. package/dist/react/useUIMessages.js.map +0 -1
  219. package/src/client/streamText.test.ts +0 -102
  220. /package/dist/{client → vercel/client}/files.js +0 -0
  221. /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
  222. /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
  223. /package/dist/{react → vercel/react}/types.js +0 -0
  224. /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
  225. /package/src/{react → vercel/react}/useUIMessages.test.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
3
  import { convexTest } from "convex-test";
4
- import { describe, expect, test } from "vitest";
4
+ import { describe, expect, test, vi } from "vitest";
5
5
  import { api } from "./_generated/api.js";
6
6
  import type { Id } from "./_generated/dataModel.js";
7
7
  import { getMaxMessage } from "./messages.js";
@@ -652,4 +652,276 @@ describe("agent", () => {
652
652
  );
653
653
  expect(remainingMessages.page).toHaveLength(1);
654
654
  });
655
+
656
+ // Regression test for #256: when searching across threads with
657
+ // searchAllMessagesForUserId, the targetMessage's order should not filter
658
+ // out messages from other threads (their order sequences are independent).
659
+ test("textSearch returns cross-thread matches even when target order is lower", async () => {
660
+ const t = convexTest(schema, modules);
661
+ const userId = "user-256";
662
+
663
+ // Old thread: build up several messages so the matching one has a high order.
664
+ const oldThread = await t.mutation(api.threads.createThread, { userId });
665
+ for (let i = 0; i < 5; i++) {
666
+ await t.mutation(api.messages.addMessages, {
667
+ threadId: oldThread._id as Id<"threads">,
668
+ userId,
669
+ messages: [
670
+ { message: { role: "user", content: `filler message ${i}` } },
671
+ ],
672
+ });
673
+ }
674
+ await t.mutation(api.messages.addMessages, {
675
+ threadId: oldThread._id as Id<"threads">,
676
+ userId,
677
+ messages: [
678
+ {
679
+ message: {
680
+ role: "user",
681
+ content:
682
+ "tom and jerry are both amazing high-ticket coaches and educators",
683
+ },
684
+ },
685
+ ],
686
+ });
687
+
688
+ // New thread: only one message — its order will be 0, lower than the
689
+ // matching message in the old thread.
690
+ const newThread = await t.mutation(api.threads.createThread, { userId });
691
+ const { messages: newMessages } = await t.mutation(
692
+ api.messages.addMessages,
693
+ {
694
+ threadId: newThread._id as Id<"threads">,
695
+ userId,
696
+ messages: [
697
+ {
698
+ message: {
699
+ role: "user",
700
+ content: "what do you remember about high-ticket coaches",
701
+ },
702
+ },
703
+ ],
704
+ },
705
+ );
706
+ const targetMessageId = newMessages[0]._id as Id<"messages">;
707
+
708
+ const results = await t.query(api.messages.textSearch, {
709
+ searchAllMessagesForUserId: userId,
710
+ targetMessageId,
711
+ text: "high-ticket coaches",
712
+ limit: 10,
713
+ });
714
+
715
+ // The cross-thread match should NOT be filtered out by the target's order.
716
+ expect(
717
+ results.some((m) =>
718
+ m.text?.includes("tom and jerry are both amazing high-ticket coaches"),
719
+ ),
720
+ ).toBe(true);
721
+ // The target message itself must still be excluded.
722
+ expect(results.some((m) => m._id === targetMessageId)).toBe(false);
723
+ });
724
+
725
+ // Regression test for #256: same-thread order filter must still work even
726
+ // when searching across threads.
727
+ test("textSearch still filters same-thread results past the target order", async () => {
728
+ const t = convexTest(schema, modules);
729
+ const userId = "user-256-same";
730
+
731
+ const thread = await t.mutation(api.threads.createThread, { userId });
732
+ // earlier match
733
+ await t.mutation(api.messages.addMessages, {
734
+ threadId: thread._id as Id<"threads">,
735
+ userId,
736
+ messages: [
737
+ { message: { role: "user", content: "earlier high-ticket match" } },
738
+ ],
739
+ });
740
+ // target
741
+ const { messages: targetMessages } = await t.mutation(
742
+ api.messages.addMessages,
743
+ {
744
+ threadId: thread._id as Id<"threads">,
745
+ userId,
746
+ messages: [
747
+ { message: { role: "user", content: "target high-ticket message" } },
748
+ ],
749
+ },
750
+ );
751
+ // later match in same thread — should be filtered out
752
+ await t.mutation(api.messages.addMessages, {
753
+ threadId: thread._id as Id<"threads">,
754
+ userId,
755
+ messages: [
756
+ { message: { role: "user", content: "later high-ticket match" } },
757
+ ],
758
+ });
759
+
760
+ const results = await t.query(api.messages.textSearch, {
761
+ searchAllMessagesForUserId: userId,
762
+ targetMessageId: targetMessages[0]._id as Id<"messages">,
763
+ text: "high-ticket",
764
+ limit: 10,
765
+ });
766
+
767
+ expect(results.some((m) => m.text === "earlier high-ticket match")).toBe(
768
+ true,
769
+ );
770
+ expect(results.some((m) => m.text === "later high-ticket match")).toBe(
771
+ false,
772
+ );
773
+ expect(results.some((m) => m._id === targetMessages[0]._id)).toBe(false);
774
+ });
775
+
776
+ test("finalizeMessage commits a failed pending message when stream recovery is malformed", async () => {
777
+ const t = initConvexTest();
778
+ const thread = await t.mutation(api.threads.createThread, {
779
+ userId: "recovery-user",
780
+ });
781
+ const { messages } = await t.mutation(api.messages.addMessages, {
782
+ threadId: thread._id as Id<"threads">,
783
+ messages: [
784
+ {
785
+ message: { role: "assistant", content: [] },
786
+ status: "pending",
787
+ },
788
+ ],
789
+ });
790
+ const pending = messages[0];
791
+ const streamId = await t.run(async (ctx) =>
792
+ ctx.db.insert("streamingMessages", {
793
+ threadId: thread._id as Id<"threads">,
794
+ order: pending.order,
795
+ stepOrder: pending.stepOrder,
796
+ format: "UIMessageChunk",
797
+ state: { kind: "aborted", reason: "interrupted" },
798
+ }),
799
+ );
800
+ await t.run(async (ctx) =>
801
+ ctx.db.insert("streamDeltas", {
802
+ streamId,
803
+ start: 0,
804
+ end: 1,
805
+ parts: [{ type: "text-delta", id: "missing", delta: "partial" }],
806
+ }),
807
+ );
808
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
809
+
810
+ try {
811
+ await expect(
812
+ t.mutation(api.messages.finalizeMessage, {
813
+ messageId: pending._id as Id<"messages">,
814
+ result: { status: "success" },
815
+ }),
816
+ ).resolves.toBeNull();
817
+ } finally {
818
+ errorSpy.mockRestore();
819
+ }
820
+
821
+ const finalized = await t.run((ctx) =>
822
+ ctx.db.get("messages", pending._id as Id<"messages">),
823
+ );
824
+ expect(finalized).toMatchObject({
825
+ status: "failed",
826
+ error: "Failed to recover persisted assistant stream output",
827
+ message: { role: "assistant", content: [] },
828
+ });
829
+ });
830
+
831
+ test("finalizeMessage discards all recovered output when any stream is malformed", async () => {
832
+ const t = initConvexTest();
833
+ const thread = await t.mutation(api.threads.createThread, {
834
+ userId: "mixed-recovery-user",
835
+ });
836
+ const { messages } = await t.mutation(api.messages.addMessages, {
837
+ threadId: thread._id as Id<"threads">,
838
+ messages: [
839
+ {
840
+ message: { role: "assistant", content: [] },
841
+ status: "pending",
842
+ },
843
+ ],
844
+ });
845
+ const pending = messages[0];
846
+ const embeddingId = await t.run(async (ctx) =>
847
+ ctx.db.insert("embeddings_128", {
848
+ model: "embedding-model",
849
+ table: "messages",
850
+ threadId: thread._id,
851
+ vector: Array.from({ length: 128 }, () => 0),
852
+ }),
853
+ );
854
+ await t.run((ctx) =>
855
+ ctx.db.patch("messages", pending._id as Id<"messages">, { embeddingId }),
856
+ );
857
+
858
+ const validStreamId = await t.run(async (ctx) =>
859
+ ctx.db.insert("streamingMessages", {
860
+ threadId: thread._id as Id<"threads">,
861
+ order: pending.order,
862
+ stepOrder: pending.stepOrder,
863
+ format: "UIMessageChunk",
864
+ state: { kind: "finished", endedAt: Date.now() },
865
+ }),
866
+ );
867
+ await t.run(async (ctx) =>
868
+ ctx.db.insert("streamDeltas", {
869
+ streamId: validStreamId,
870
+ start: 0,
871
+ end: 3,
872
+ parts: [
873
+ { type: "text-start", id: "valid" },
874
+ { type: "text-delta", id: "valid", delta: "must not be inserted" },
875
+ { type: "text-end", id: "valid" },
876
+ ],
877
+ }),
878
+ );
879
+ const malformedStreamId = await t.run(async (ctx) =>
880
+ ctx.db.insert("streamingMessages", {
881
+ threadId: thread._id as Id<"threads">,
882
+ order: pending.order,
883
+ stepOrder: pending.stepOrder,
884
+ format: "UIMessageChunk",
885
+ state: { kind: "aborted", reason: "interrupted" },
886
+ }),
887
+ );
888
+ await t.run(async (ctx) =>
889
+ ctx.db.insert("streamDeltas", {
890
+ streamId: malformedStreamId,
891
+ start: 0,
892
+ end: 1,
893
+ parts: [
894
+ {
895
+ type: "tool-input-delta",
896
+ toolCallId: "missing",
897
+ inputTextDelta: "{}",
898
+ },
899
+ ],
900
+ }),
901
+ );
902
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
903
+
904
+ try {
905
+ await t.mutation(api.messages.finalizeMessage, {
906
+ messageId: pending._id as Id<"messages">,
907
+ result: { status: "failed", error: "provider failed" },
908
+ });
909
+ } finally {
910
+ errorSpy.mockRestore();
911
+ }
912
+
913
+ const state = await t.run(async (ctx) => ({
914
+ messages: await ctx.db.query("messages").collect(),
915
+ embedding: await ctx.db.get("embeddings_128", embeddingId),
916
+ }));
917
+ expect(state.messages).toHaveLength(1);
918
+ expect(state.messages[0]).toMatchObject({
919
+ _id: pending._id,
920
+ status: "failed",
921
+ error: "provider failed",
922
+ message: { role: "assistant", content: [] },
923
+ });
924
+ expect(state.messages[0].embeddingId).toBeUndefined();
925
+ expect(state.embedding).toBeNull();
926
+ });
655
927
  });
@@ -83,6 +83,25 @@ export const messageStatuses = vMessageDoc.fields.status.members.map(
83
83
  (m) => m.value,
84
84
  );
85
85
 
86
+ const STREAM_RECOVERY_FAILURE =
87
+ "Failed to recover persisted assistant stream output";
88
+
89
+ async function markPendingMessageFailed(
90
+ ctx: MutationCtx,
91
+ message: Doc<"messages">,
92
+ error: string,
93
+ ) {
94
+ if (message.embeddingId) {
95
+ const { tableName } = getVectorIdInfo(ctx, message.embeddingId);
96
+ await ctx.db.delete(tableName, message.embeddingId);
97
+ }
98
+ await ctx.db.patch("messages", message._id, {
99
+ status: "failed",
100
+ error,
101
+ embeddingId: undefined,
102
+ });
103
+ }
104
+
86
105
  export const deleteByOrder = mutation({
87
106
  args: {
88
107
  threadId: v.id("threads"),
@@ -390,11 +409,20 @@ export const finalizeMessage = mutation({
390
409
  }
391
410
  // See if we can add any in-progress data
392
411
  if (!message.message?.content.length) {
393
- const messages = await getStreamingMessagesWithMetadata(
394
- ctx,
395
- message,
396
- result,
397
- );
412
+ const { messages, materializationFailures } =
413
+ await getStreamingMessagesWithMetadata(ctx, message, result);
414
+ if (materializationFailures.length > 0) {
415
+ console.error(
416
+ "Failed to materialize persisted assistant streams",
417
+ materializationFailures,
418
+ );
419
+ await markPendingMessageFailed(
420
+ ctx,
421
+ message,
422
+ result.status === "failed" ? result.error : STREAM_RECOVERY_FAILURE,
423
+ );
424
+ return;
425
+ }
398
426
  if (messages.length > 0) {
399
427
  await addMessagesHandler(ctx, {
400
428
  messages,
@@ -409,15 +437,7 @@ export const finalizeMessage = mutation({
409
437
  }
410
438
  }
411
439
  if (result.status === "failed") {
412
- if (message.embeddingId) {
413
- const { tableName } = getVectorIdInfo(ctx, message.embeddingId);
414
- await ctx.db.delete(tableName, message.embeddingId);
415
- }
416
- await ctx.db.patch("messages", messageId, {
417
- status: "failed",
418
- error: result.error,
419
- embeddingId: undefined,
420
- });
440
+ await markPendingMessageFailed(ctx, message, result.error);
421
441
  } else {
422
442
  await ctx.db.patch("messages", messageId, { status: "success" });
423
443
  }
@@ -830,16 +850,18 @@ export const _fetchSearchMessages = internalQuery({
830
850
  ),
831
851
  )
832
852
  )
833
- .filter(
834
- (m): m is Doc<"messages"> =>
835
- m !== undefined &&
836
- m !== null &&
837
- !m.tool &&
838
- (!beforeMessage ||
839
- m.order < beforeMessage.order ||
840
- (m.order === beforeMessage.order &&
841
- m.stepOrder < beforeMessage.stepOrder)),
842
- )
853
+ .filter((m): m is Doc<"messages"> => {
854
+ if (m === undefined || m === null || m.tool) return false;
855
+ if (!beforeMessage) return true;
856
+ // The order filter is only meaningful within the same thread.
857
+ // Messages from other threads have independent order sequences.
858
+ if (m.threadId !== beforeMessage.threadId) return true;
859
+ return (
860
+ m.order < beforeMessage.order ||
861
+ (m.order === beforeMessage.order &&
862
+ m.stepOrder < beforeMessage.stepOrder)
863
+ );
864
+ })
843
865
  .map(publicMessage);
844
866
  messages.push(...(args.textSearchMessages ?? []));
845
867
  // TODO: prioritize more recent messages
@@ -922,12 +944,16 @@ export const textSearch = query({
922
944
  const targetMessage =
923
945
  args.targetMessageId &&
924
946
  (await ctx.db.get("messages", args.targetMessageId));
925
- const order = targetMessage?.order;
926
947
  const text = args.text || targetMessage?.text;
927
948
  if (!text) {
928
949
  console.warn("No text to search", targetMessage, args.text);
929
950
  return [];
930
951
  }
952
+ // When searching across threads (searchAllMessagesForUserId), the
953
+ // targetMessage's order is only meaningful within its own thread, so we
954
+ // can't apply it as a database-level filter. We still apply it post-fetch
955
+ // for same-thread results below.
956
+ const restrictOrderInDb = !args.searchAllMessagesForUserId && targetMessage;
931
957
  const messages = await ctx.db
932
958
  .query("messages")
933
959
  .withSearchIndex("text_search", (q) =>
@@ -939,20 +965,24 @@ export const textSearch = query({
939
965
  // eslint-disable-next-line @convex-dev/no-filter-in-query -- we can't do this in the search index, but text search ideally isn't for really old orders
940
966
  .filter((q) => {
941
967
  const qq = q.eq(q.field("tool"), false);
942
- if (order) {
943
- return q.and(qq, q.lte(q.field("order"), order));
968
+ if (restrictOrderInDb) {
969
+ return q.and(qq, q.lte(q.field("order"), targetMessage.order));
944
970
  }
945
971
  return qq;
946
972
  })
947
973
  .take(args.limit);
948
974
  return messages
949
- .filter(
950
- (m) =>
951
- !targetMessage ||
975
+ .filter((m) => {
976
+ if (!targetMessage) return true;
977
+ // Order is only meaningful within the same thread; cross-thread
978
+ // results have independent order sequences and should pass through.
979
+ if (m.threadId !== targetMessage.threadId) return true;
980
+ return (
952
981
  m.order < targetMessage.order ||
953
982
  (m.order === targetMessage.order &&
954
- m.stepOrder < targetMessage.stepOrder),
955
- )
983
+ m.stepOrder < targetMessage.stepOrder)
984
+ );
985
+ })
956
986
  .map(publicMessage);
957
987
  },
958
988
  returns: v.array(vMessageDoc),
@@ -21,8 +21,10 @@ import { stream } from "convex-helpers/server/stream";
21
21
  import { mergedStream } from "convex-helpers/server/stream";
22
22
  import { paginator } from "convex-helpers/server/pagination";
23
23
  import type { WithoutSystemFields } from "convex/server";
24
- import { deriveUIMessagesFromDeltas } from "../deltas.js";
25
- import { fromUIMessages } from "../UIMessages.js";
24
+ import {
25
+ getPersistedUIMessageChunkParts,
26
+ projectPersistedUIMessageChunks,
27
+ } from "../streaming/materializePersistedUIMessageChunks.js";
26
28
 
27
29
  const SECOND = 1000;
28
30
  const MINUTE = 60 * SECOND;
@@ -536,7 +538,13 @@ export async function getStreamingMessagesWithMetadata(
536
538
  stepOrder,
537
539
  }: { threadId: Id<"threads">; order: number; stepOrder: number },
538
540
  metadata: { status: "success" | "failed"; error?: string },
539
- ): Promise<MessageWithMetadataInternal[]> {
541
+ ): Promise<{
542
+ messages: MessageWithMetadataInternal[];
543
+ materializationFailures: Array<{
544
+ streamId: Id<"streamingMessages">;
545
+ reason: string;
546
+ }>;
547
+ }> {
540
548
  // See if there are any streaming messages for this order
541
549
  const streamingMessages = await getStreamingMessages(
542
550
  ctx,
@@ -544,50 +552,42 @@ export async function getStreamingMessagesWithMetadata(
544
552
  order,
545
553
  stepOrder,
546
554
  );
547
- const messages = (
548
- await Promise.all(
549
- streamingMessages.map(async (streamingMessage) => {
550
- const deltas = await ctx.db
551
- .query("streamDeltas")
552
- .withIndex("streamId_start_end", (q) =>
553
- q.eq("streamId", streamingMessage._id),
554
- )
555
- .take(1000);
556
- const uiMessages = await deriveUIMessagesFromDeltas(
557
- threadId,
558
- [publicStreamMessage(streamingMessage)],
559
- deltas,
560
- );
555
+ const materializedStreams = await Promise.all(
556
+ streamingMessages.map(async (streamingMessage) => {
557
+ const deltas = await ctx.db
558
+ .query("streamDeltas")
559
+ .withIndex("streamId_start_end", (q) =>
560
+ q.eq("streamId", streamingMessage._id),
561
+ )
562
+ .take(1000);
563
+ try {
564
+ const streamMessage = publicStreamMessage(streamingMessage);
565
+ const { parts } = getPersistedUIMessageChunkParts(deltas);
561
566
  // We don't save messages that have already been saved
562
567
  const numToSkip = stepOrder - streamingMessage.stepOrder;
563
- const messages = await Promise.all(
564
- (await fromUIMessages(uiMessages, streamingMessage))
565
- .slice(numToSkip)
566
- .filter((m) => m.message !== undefined)
567
- .map(async (msg) => {
568
- return {
569
- ...pick(msg, [
570
- "message",
571
- "fileIds",
572
- "status",
573
- "finishReason",
574
- "model",
575
- "provider",
576
- "providerMetadata",
577
- "sources",
578
- "reasoning",
579
- "reasoningDetails",
580
- "usage",
581
- "warnings",
582
- "error",
583
- ]),
584
- ...metadata,
585
- } as MessageWithMetadataInternal;
586
- }),
587
- );
588
- return messages;
589
- }),
590
- )
591
- ).flat();
592
- return messages;
568
+ return {
569
+ messages: projectPersistedUIMessageChunks(
570
+ streamMessage,
571
+ parts,
572
+ metadata,
573
+ ).slice(numToSkip),
574
+ failure: undefined,
575
+ };
576
+ } catch (error) {
577
+ return {
578
+ messages: [],
579
+ failure: {
580
+ streamId: streamingMessage._id,
581
+ reason: error instanceof Error ? error.message : String(error),
582
+ },
583
+ };
584
+ }
585
+ }),
586
+ );
587
+ return {
588
+ messages: materializedStreams.flatMap(({ messages }) => messages),
589
+ materializationFailures: materializedStreams.flatMap(({ failure }) =>
590
+ failure ? [failure] : [],
591
+ ),
592
+ };
593
593
  }
@@ -3,8 +3,11 @@ import { paginator } from "convex-helpers/server/pagination";
3
3
  import { partial } from "convex-helpers/validators";
4
4
  import { paginationOptsValidator } from "convex/server";
5
5
  import type { ObjectType } from "convex/values";
6
- import { type ThreadDoc, vThreadDoc } from "../client/index.js";
7
- import { vPaginationResult } from "../validators.js";
6
+ import {
7
+ type ThreadDoc,
8
+ vPaginationResult,
9
+ vThreadDoc,
10
+ } from "../validators.js";
8
11
  import { api, internal } from "./_generated/api.js";
9
12
  import type { Doc } from "./_generated/dataModel.js";
10
13
  import {
@@ -0,0 +1,44 @@
1
+ import { Linter } from "eslint";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ describe("provider adapter boundary lint selectors", () => {
5
+ it("parse on Node 20 and reject every restricted dynamic import shape", async () => {
6
+ const configUrl = new URL("../eslint.config.js", import.meta.url).href;
7
+ const { default: eslintConfig } = (await import(configUrl)) as {
8
+ default: Array<{
9
+ files?: string[];
10
+ rules?: Record<string, unknown>;
11
+ }>;
12
+ };
13
+ const boundaryConfig = eslintConfig.find(
14
+ (config) =>
15
+ "files" in config &&
16
+ config.files?.some((pattern) => pattern.includes("src/streaming/")) &&
17
+ config.rules?.["no-restricted-syntax"],
18
+ );
19
+ const restrictedSyntax = boundaryConfig?.rules?.["no-restricted-syntax"];
20
+ expect(restrictedSyntax).toBeDefined();
21
+
22
+ const messages = new Linter({ configType: "flat" }).verify(
23
+ [
24
+ 'import("ai");',
25
+ 'import("ai/test");',
26
+ 'import("@ai-sdk/provider");',
27
+ 'import("vercel");',
28
+ 'import("vercel/test");',
29
+ 'import("../vercel");',
30
+ 'import("../vercel/test");',
31
+ ].join("\n"),
32
+ {
33
+ languageOptions: { ecmaVersion: 2020, sourceType: "module" },
34
+ rules: {
35
+ "no-restricted-syntax": restrictedSyntax as Linter.RuleEntry,
36
+ },
37
+ },
38
+ );
39
+
40
+ expect(messages).toHaveLength(7);
41
+ expect(messages.every((message) => message.ruleId === "no-restricted-syntax"))
42
+ .toBe(true);
43
+ });
44
+ });
@@ -1,17 +1,20 @@
1
1
  "use client";
2
2
  import { useMemo, useState } from "react";
3
- export { toUIMessages, type UIMessage } from "../UIMessages.js";
3
+ export { toUIMessages, type UIMessage } from "../vercel/UIMessages.js";
4
4
 
5
- export { optimisticallySendMessage } from "./optimisticallySendMessage.js";
5
+ export { optimisticallySendMessage } from "../vercel/react/optimisticallySendMessage.js";
6
6
  export { useSmoothText } from "./useSmoothText.js";
7
7
  export { SmoothText } from "./SmoothText.js";
8
8
  export {
9
9
  type ThreadMessagesQuery,
10
10
  useThreadMessages,
11
11
  useStreamingThreadMessages,
12
- } from "./useThreadMessages.js";
13
- export { type UIMessagesQuery, useUIMessages } from "./useUIMessages.js";
14
- export { useStreamingUIMessages } from "./useStreamingUIMessages.js";
12
+ } from "../vercel/react/useThreadMessages.js";
13
+ export {
14
+ type UIMessagesQuery,
15
+ useUIMessages,
16
+ } from "../vercel/react/useUIMessages.js";
17
+ export { useStreamingUIMessages } from "../vercel/react/useStreamingUIMessages.js";
15
18
 
16
19
  /**
17
20
  * @deprecated use useThreadMessages or useStreamingThreadMessages instead