@convex-dev/agent 0.6.3 → 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 (246) 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/apiKeys.js +5 -5
  11. package/dist/component/apiKeys.js.map +1 -1
  12. package/dist/component/files.d.ts.map +1 -1
  13. package/dist/component/files.js +13 -11
  14. package/dist/component/files.js.map +1 -1
  15. package/dist/component/messages.d.ts +1252 -201
  16. package/dist/component/messages.d.ts.map +1 -1
  17. package/dist/component/messages.js +83 -47
  18. package/dist/component/messages.js.map +1 -1
  19. package/dist/component/schema.d.ts +2593 -332
  20. package/dist/component/schema.d.ts.map +1 -1
  21. package/dist/component/streams.d.ts +7 -1
  22. package/dist/component/streams.d.ts.map +1 -1
  23. package/dist/component/streams.js +46 -47
  24. package/dist/component/streams.js.map +1 -1
  25. package/dist/component/threads.d.ts.map +1 -1
  26. package/dist/component/threads.js +8 -9
  27. package/dist/component/threads.js.map +1 -1
  28. package/dist/component/users.js +2 -2
  29. package/dist/component/users.js.map +1 -1
  30. package/dist/component/vector/index.d.ts.map +1 -1
  31. package/dist/component/vector/index.js +14 -8
  32. package/dist/component/vector/index.js.map +1 -1
  33. package/dist/react/index.d.ts +5 -5
  34. package/dist/react/index.d.ts.map +1 -1
  35. package/dist/react/index.js +5 -5
  36. package/dist/react/index.js.map +1 -1
  37. package/dist/shared.d.ts +19 -7
  38. package/dist/shared.d.ts.map +1 -1
  39. package/dist/shared.js +4 -4
  40. package/dist/shared.js.map +1 -1
  41. package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
  42. package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
  43. package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
  44. package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
  45. package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
  46. package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
  47. package/dist/streaming/persistedUIMessageChunks.js +475 -0
  48. package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
  49. package/dist/validators.d.ts +10026 -1055
  50. package/dist/validators.d.ts.map +1 -1
  51. package/dist/validators.js +71 -9
  52. package/dist/validators.js.map +1 -1
  53. package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
  54. package/dist/vercel/UIMessages.d.ts.map +1 -0
  55. package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
  56. package/dist/vercel/UIMessages.js.map +1 -0
  57. package/dist/{client → vercel/client}/createTool.d.ts +23 -16
  58. package/dist/vercel/client/createTool.d.ts.map +1 -0
  59. package/dist/{client → vercel/client}/createTool.js +2 -0
  60. package/dist/vercel/client/createTool.js.map +1 -0
  61. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +593 -134
  62. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
  63. package/dist/{client → vercel/client}/definePlaygroundAPI.js +13 -9
  64. package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
  65. package/dist/{client → vercel/client}/files.d.ts +2 -2
  66. package/dist/vercel/client/files.d.ts.map +1 -0
  67. package/dist/vercel/client/files.js.map +1 -0
  68. package/dist/vercel/client/messages.d.ts +542 -0
  69. package/dist/vercel/client/messages.d.ts.map +1 -0
  70. package/dist/vercel/client/messages.js +66 -0
  71. package/dist/vercel/client/messages.js.map +1 -0
  72. package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
  73. package/dist/vercel/client/mockModel.d.ts.map +1 -0
  74. package/dist/{client → vercel/client}/mockModel.js +12 -13
  75. package/dist/vercel/client/mockModel.js.map +1 -0
  76. package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
  77. package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
  78. package/dist/vercel/client/saveInputMessages.js.map +1 -0
  79. package/dist/{client → vercel/client}/search.d.ts +119 -22
  80. package/dist/vercel/client/search.d.ts.map +1 -0
  81. package/dist/{client → vercel/client}/search.js +2 -2
  82. package/dist/vercel/client/search.js.map +1 -0
  83. package/dist/{client → vercel/client}/start.d.ts +26 -11
  84. package/dist/vercel/client/start.d.ts.map +1 -0
  85. package/dist/{client → vercel/client}/start.js +64 -22
  86. package/dist/vercel/client/start.js.map +1 -0
  87. package/dist/{client → vercel/client}/streamText.d.ts +13 -12
  88. package/dist/vercel/client/streamText.d.ts.map +1 -0
  89. package/dist/{client → vercel/client}/streamText.js +43 -9
  90. package/dist/vercel/client/streamText.js.map +1 -0
  91. package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
  92. package/dist/vercel/client/streaming.d.ts.map +1 -0
  93. package/dist/{client → vercel/client}/streaming.js +120 -52
  94. package/dist/vercel/client/streaming.js.map +1 -0
  95. package/dist/vercel/client/types.d.ts +456 -0
  96. package/dist/vercel/client/types.d.ts.map +1 -0
  97. package/dist/vercel/client/types.js +3 -0
  98. package/dist/vercel/client/types.js.map +1 -0
  99. package/dist/{client → vercel/client}/utils.d.ts +3 -2
  100. package/dist/vercel/client/utils.d.ts.map +1 -0
  101. package/dist/{client → vercel/client}/utils.js +2 -2
  102. package/dist/vercel/client/utils.js.map +1 -0
  103. package/dist/vercel/deltas.d.ts +31 -0
  104. package/dist/vercel/deltas.d.ts.map +1 -0
  105. package/dist/vercel/deltas.js +525 -0
  106. package/dist/vercel/deltas.js.map +1 -0
  107. package/dist/{client → vercel}/index.d.ts +484 -97
  108. package/dist/vercel/index.d.ts.map +1 -0
  109. package/dist/{client → vercel}/index.js +65 -38
  110. package/dist/vercel/index.js.map +1 -0
  111. package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -13
  112. package/dist/vercel/mapping.d.ts.map +1 -0
  113. package/dist/{mapping.js → vercel/mapping.js} +411 -112
  114. package/dist/vercel/mapping.js.map +1 -0
  115. package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
  116. package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
  117. package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
  118. package/dist/{react → vercel/react}/types.d.ts +1 -1
  119. package/dist/vercel/react/types.d.ts.map +1 -0
  120. package/dist/{react → vercel/react}/types.js.map +1 -1
  121. package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
  122. package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
  123. package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
  124. package/dist/vercel/react/useDeltaStreams.js.map +1 -0
  125. package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
  126. package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
  127. package/dist/vercel/react/useStreamingUIMessages.js +112 -0
  128. package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
  129. package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
  130. package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
  131. package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
  132. package/dist/vercel/react/useThreadMessages.js.map +1 -0
  133. package/dist/{react → vercel/react}/useUIMessages.d.ts +2 -1
  134. package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
  135. package/dist/{react → vercel/react}/useUIMessages.js +8 -4
  136. package/dist/vercel/react/useUIMessages.js.map +1 -0
  137. package/package.json +30 -22
  138. package/src/client/messages.ts +5 -107
  139. package/src/client/types.ts +0 -621
  140. package/src/component/_generated/component.ts +1089 -48
  141. package/src/component/apiKeys.ts +5 -5
  142. package/src/component/files.test.ts +1 -1
  143. package/src/component/files.ts +14 -12
  144. package/src/component/messages.test.ts +273 -1
  145. package/src/component/messages.ts +99 -57
  146. package/src/component/streams.ts +80 -64
  147. package/src/component/threads.ts +12 -9
  148. package/src/component/users.ts +2 -2
  149. package/src/component/vector/index.ts +14 -7
  150. package/src/eslintConfig.test.ts +44 -0
  151. package/src/react/index.ts +8 -5
  152. package/src/shared.ts +26 -36
  153. package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
  154. package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
  155. package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
  156. package/src/streaming/persistedUIMessageChunks.ts +658 -0
  157. package/src/validators.test.ts +4 -1
  158. package/src/validators.ts +89 -8
  159. package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
  160. package/src/{UIMessages.ts → vercel/UIMessages.ts} +96 -5
  161. package/src/{client → vercel/client}/approval.test.ts +48 -19
  162. package/src/{client → vercel/client}/createTool.ts +63 -23
  163. package/src/{client → vercel/client}/definePlaygroundAPI.ts +44 -29
  164. package/src/{client → vercel/client}/files.ts +2 -2
  165. package/src/{client → vercel/client}/index.test.ts +162 -3
  166. package/src/vercel/client/messages.ts +182 -0
  167. package/src/{client → vercel/client}/mockModel.ts +38 -39
  168. package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
  169. package/src/{client → vercel/client}/saveInputMessages.ts +7 -4
  170. package/src/{client → vercel/client}/search.test.ts +3 -3
  171. package/src/{client → vercel/client}/search.ts +4 -4
  172. package/src/{client → vercel/client}/setup.test.ts +6 -3
  173. package/src/vercel/client/start.test.ts +14 -0
  174. package/src/{client → vercel/client}/start.ts +147 -36
  175. package/src/vercel/client/streamText.test.ts +223 -0
  176. package/src/{client → vercel/client}/streamText.ts +102 -31
  177. package/src/{client → vercel/client}/streaming.integration.test.ts +40 -115
  178. package/src/{client → vercel/client}/streaming.test.ts +178 -4
  179. package/src/{client → vercel/client}/streaming.ts +114 -52
  180. package/src/vercel/client/types.test.ts +13 -0
  181. package/src/vercel/client/types.ts +679 -0
  182. package/src/{client → vercel/client}/utils.ts +13 -7
  183. package/src/vercel/deltas.test.ts +898 -0
  184. package/src/vercel/deltas.ts +633 -0
  185. package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
  186. package/src/{client → vercel}/index.ts +169 -62
  187. package/src/vercel/mapping.test.ts +965 -0
  188. package/src/{mapping.ts → vercel/mapping.ts} +501 -131
  189. package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
  190. package/src/{react → vercel/react}/types.ts +1 -1
  191. package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
  192. package/src/{react → vercel/react}/useStreamingUIMessages.ts +67 -35
  193. package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
  194. package/src/{react → vercel/react}/useUIMessages.test.ts +80 -1
  195. package/src/{react → vercel/react}/useUIMessages.ts +13 -5
  196. package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
  197. package/dist/UIMessages.d.ts.map +0 -1
  198. package/dist/UIMessages.js.map +0 -1
  199. package/dist/client/createTool.d.ts.map +0 -1
  200. package/dist/client/createTool.js.map +0 -1
  201. package/dist/client/definePlaygroundAPI.js.map +0 -1
  202. package/dist/client/files.d.ts.map +0 -1
  203. package/dist/client/files.js.map +0 -1
  204. package/dist/client/index.d.ts.map +0 -1
  205. package/dist/client/index.js.map +0 -1
  206. package/dist/client/mockModel.d.ts.map +0 -1
  207. package/dist/client/mockModel.js.map +0 -1
  208. package/dist/client/saveInputMessages.d.ts.map +0 -1
  209. package/dist/client/saveInputMessages.js.map +0 -1
  210. package/dist/client/search.d.ts.map +0 -1
  211. package/dist/client/search.js.map +0 -1
  212. package/dist/client/start.d.ts.map +0 -1
  213. package/dist/client/start.js.map +0 -1
  214. package/dist/client/streamText.d.ts.map +0 -1
  215. package/dist/client/streamText.js.map +0 -1
  216. package/dist/client/streaming.d.ts.map +0 -1
  217. package/dist/client/streaming.js.map +0 -1
  218. package/dist/client/utils.d.ts.map +0 -1
  219. package/dist/client/utils.js.map +0 -1
  220. package/dist/deltas.d.ts +0 -43
  221. package/dist/deltas.d.ts.map +0 -1
  222. package/dist/deltas.js +0 -446
  223. package/dist/deltas.js.map +0 -1
  224. package/dist/mapping.d.ts.map +0 -1
  225. package/dist/mapping.js.map +0 -1
  226. package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
  227. package/dist/react/optimisticallySendMessage.js.map +0 -1
  228. package/dist/react/types.d.ts.map +0 -1
  229. package/dist/react/useDeltaStreams.d.ts.map +0 -1
  230. package/dist/react/useDeltaStreams.js.map +0 -1
  231. package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
  232. package/dist/react/useStreamingUIMessages.js +0 -92
  233. package/dist/react/useStreamingUIMessages.js.map +0 -1
  234. package/dist/react/useThreadMessages.d.ts.map +0 -1
  235. package/dist/react/useThreadMessages.js.map +0 -1
  236. package/dist/react/useUIMessages.d.ts.map +0 -1
  237. package/dist/react/useUIMessages.js.map +0 -1
  238. package/src/client/streamText.test.ts +0 -102
  239. package/src/deltas.test.ts +0 -626
  240. package/src/deltas.ts +0 -569
  241. package/src/mapping.test.ts +0 -402
  242. /package/dist/{client → vercel/client}/files.js +0 -0
  243. /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
  244. /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
  245. /package/dist/{react → vercel/react}/types.js +0 -0
  246. /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
@@ -13,7 +13,7 @@ export const issue = mutation({
13
13
  .first();
14
14
  if (existingApiKey) {
15
15
  console.warn(`API key ${args.name} already exists, deleting...`);
16
- await ctx.db.delete(existingApiKey._id);
16
+ await ctx.db.delete("apiKeys", existingApiKey._id);
17
17
  }
18
18
  }
19
19
  const apiKey = await ctx.db.insert("apiKeys", args);
@@ -27,7 +27,7 @@ export const validate = query({
27
27
  apiKey: v.id("apiKeys"),
28
28
  },
29
29
  handler: async (ctx, args) => {
30
- const apiKey = await ctx.db.get(args.apiKey);
30
+ const apiKey = await ctx.db.get("apiKeys", args.apiKey);
31
31
  if (!apiKey) {
32
32
  throw new Error("Invalid API key");
33
33
  }
@@ -43,14 +43,14 @@ export const destroy = mutation({
43
43
  }),
44
44
  handler: async (ctx, args) => {
45
45
  if (args.apiKey) {
46
- const apiKey = await ctx.db.get(args.apiKey);
46
+ const apiKey = await ctx.db.get("apiKeys", args.apiKey);
47
47
  if (!apiKey) {
48
48
  return "missing";
49
49
  }
50
50
  if (apiKey.name !== args.name) {
51
51
  return "name mismatch";
52
52
  }
53
- await ctx.db.delete(args.apiKey);
53
+ await ctx.db.delete("apiKeys", args.apiKey);
54
54
  } else if (args.name) {
55
55
  const apiKey = await ctx.db
56
56
  .query("apiKeys")
@@ -59,7 +59,7 @@ export const destroy = mutation({
59
59
  if (!apiKey) {
60
60
  return "missing";
61
61
  }
62
- await ctx.db.delete(apiKey._id);
62
+ await ctx.db.delete("apiKeys", apiKey._id);
63
63
  } else {
64
64
  return "must provide either apiKey or name";
65
65
  }
@@ -96,7 +96,7 @@ describe("files", () => {
96
96
  });
97
97
  // Manually set refcount to 0
98
98
  await t.run(async (ctx) => {
99
- await ctx.db.patch(fileId, { refcount: 0 });
99
+ await ctx.db.patch("files", fileId, { refcount: 0 });
100
100
  });
101
101
  files.push(fileId);
102
102
  }
@@ -29,15 +29,16 @@ export async function addFileHandler(
29
29
  ) {
30
30
  // Support both mediaType (preferred) and mimeType (deprecated)
31
31
  const mediaType = args.mediaType ?? args.mimeType;
32
-
32
+
33
33
  const existingFile = await ctx.db
34
34
  .query("files")
35
35
  .withIndex("hash", (q) => q.eq("hash", args.hash))
36
+ // eslint-disable-next-line @convex-dev/no-filter-in-query -- We do not expect many files with the same hash and different filenames
36
37
  .filter((q) => q.eq(q.field("filename"), args.filename))
37
38
  .first();
38
39
  if (existingFile) {
39
40
  // increment the refcount
40
- await ctx.db.patch(existingFile._id, {
41
+ await ctx.db.patch("files", existingFile._id, {
41
42
  refcount: existingFile.refcount + 1,
42
43
  lastTouchedAt: Date.now(),
43
44
  });
@@ -68,7 +69,7 @@ export const get = query({
68
69
  },
69
70
  returns: v.union(v.null(), v.doc("files")),
70
71
  handler: async (ctx, args) => {
71
- return ctx.db.get(args.fileId);
72
+ return ctx.db.get("files", args.fileId);
72
73
  },
73
74
  });
74
75
 
@@ -87,12 +88,13 @@ export const useExistingFile = mutation({
87
88
  const file = await ctx.db
88
89
  .query("files")
89
90
  .withIndex("hash", (q) => q.eq("hash", args.hash))
91
+ // eslint-disable-next-line @convex-dev/no-filter-in-query -- We do not expect many files with the same hash and different filenames
90
92
  .filter((q) => q.eq(q.field("filename"), args.filename))
91
93
  .first();
92
94
  if (!file) {
93
95
  return null;
94
96
  }
95
- await ctx.db.patch(file._id, {
97
+ await ctx.db.patch("files", file._id, {
96
98
  lastTouchedAt: Date.now(),
97
99
  });
98
100
  return { fileId: file._id, storageId: file.storageId };
@@ -115,9 +117,9 @@ export async function changeRefcount(
115
117
  const nextSet = new Set(next);
116
118
  for (const fileId of prevSet) {
117
119
  if (!nextSet.has(fileId)) {
118
- const file = await ctx.db.get(fileId);
120
+ const file = await ctx.db.get("files", fileId);
119
121
  if (file) {
120
- await ctx.db.patch(fileId, {
122
+ await ctx.db.patch("files", fileId, {
121
123
  refcount: file.refcount - 1,
122
124
  });
123
125
  } else {
@@ -127,9 +129,9 @@ export async function changeRefcount(
127
129
  }
128
130
  for (const fileId of nextSet) {
129
131
  if (!prevSet.has(fileId)) {
130
- const file = await ctx.db.get(fileId);
132
+ const file = await ctx.db.get("files", fileId);
131
133
  if (file) {
132
- await ctx.db.patch(fileId, {
134
+ await ctx.db.patch("files", fileId, {
133
135
  refcount: file.refcount + 1,
134
136
  });
135
137
  } else {
@@ -151,11 +153,11 @@ export async function copyFileHandler(
151
153
  ctx: MutationCtx,
152
154
  args: { fileId: Id<"files"> },
153
155
  ) {
154
- const file = await ctx.db.get(args.fileId);
156
+ const file = await ctx.db.get("files", args.fileId);
155
157
  if (!file) {
156
158
  throw new Error("File not found");
157
159
  }
158
- await ctx.db.patch(args.fileId, {
160
+ await ctx.db.patch("files", args.fileId, {
159
161
  refcount: file.refcount + 1,
160
162
  lastTouchedAt: Date.now(),
161
163
  });
@@ -195,7 +197,7 @@ export const deleteFiles = mutation({
195
197
  handler: async (ctx, args) => {
196
198
  const deletedFileIds = await Promise.all(
197
199
  args.fileIds.map(async (fileId) => {
198
- const file = await ctx.db.get(fileId);
200
+ const file = await ctx.db.get("files", fileId);
199
201
  if (!file) {
200
202
  console.error(`File ${fileId} not found when deleting, skipping...`);
201
203
  return null;
@@ -208,7 +210,7 @@ export const deleteFiles = mutation({
208
210
  return null;
209
211
  }
210
212
  }
211
- await ctx.db.delete(fileId);
213
+ await ctx.db.delete("files", fileId);
212
214
  return fileId;
213
215
  }),
214
216
  );
@@ -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
  });