@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.
- package/MIGRATION.md +60 -116
- package/dist/client/messages.d.ts +1 -398
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +3 -47
- package/dist/client/messages.js.map +1 -1
- package/dist/client/types.d.ts +1 -455
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +805 -45
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/apiKeys.js +5 -5
- package/dist/component/apiKeys.js.map +1 -1
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +13 -11
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +1252 -201
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +83 -47
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +2593 -332
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/streams.d.ts +7 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +46 -47
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts.map +1 -1
- package/dist/component/threads.js +8 -9
- package/dist/component/threads.js.map +1 -1
- package/dist/component/users.js +2 -2
- package/dist/component/users.js.map +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +14 -8
- package/dist/component/vector/index.js.map +1 -1
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -5
- package/dist/react/index.js.map +1 -1
- package/dist/shared.d.ts +19 -7
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +4 -4
- package/dist/shared.js.map +1 -1
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.js +475 -0
- package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
- package/dist/validators.d.ts +10026 -1055
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +71 -9
- package/dist/validators.js.map +1 -1
- package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
- package/dist/vercel/UIMessages.d.ts.map +1 -0
- package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
- package/dist/vercel/UIMessages.js.map +1 -0
- package/dist/{client → vercel/client}/createTool.d.ts +23 -16
- package/dist/vercel/client/createTool.d.ts.map +1 -0
- package/dist/{client → vercel/client}/createTool.js +2 -0
- package/dist/vercel/client/createTool.js.map +1 -0
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +593 -134
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/{client → vercel/client}/definePlaygroundAPI.js +13 -9
- package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
- package/dist/{client → vercel/client}/files.d.ts +2 -2
- package/dist/vercel/client/files.d.ts.map +1 -0
- package/dist/vercel/client/files.js.map +1 -0
- package/dist/vercel/client/messages.d.ts +542 -0
- package/dist/vercel/client/messages.d.ts.map +1 -0
- package/dist/vercel/client/messages.js +66 -0
- package/dist/vercel/client/messages.js.map +1 -0
- package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
- package/dist/vercel/client/mockModel.d.ts.map +1 -0
- package/dist/{client → vercel/client}/mockModel.js +12 -13
- package/dist/vercel/client/mockModel.js.map +1 -0
- package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
- package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
- package/dist/vercel/client/saveInputMessages.js.map +1 -0
- package/dist/{client → vercel/client}/search.d.ts +119 -22
- package/dist/vercel/client/search.d.ts.map +1 -0
- package/dist/{client → vercel/client}/search.js +2 -2
- package/dist/vercel/client/search.js.map +1 -0
- package/dist/{client → vercel/client}/start.d.ts +26 -11
- package/dist/vercel/client/start.d.ts.map +1 -0
- package/dist/{client → vercel/client}/start.js +64 -22
- package/dist/vercel/client/start.js.map +1 -0
- package/dist/{client → vercel/client}/streamText.d.ts +13 -12
- package/dist/vercel/client/streamText.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streamText.js +43 -9
- package/dist/vercel/client/streamText.js.map +1 -0
- package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
- package/dist/vercel/client/streaming.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streaming.js +120 -52
- package/dist/vercel/client/streaming.js.map +1 -0
- package/dist/vercel/client/types.d.ts +456 -0
- package/dist/vercel/client/types.d.ts.map +1 -0
- package/dist/vercel/client/types.js +3 -0
- package/dist/vercel/client/types.js.map +1 -0
- package/dist/{client → vercel/client}/utils.d.ts +3 -2
- package/dist/vercel/client/utils.d.ts.map +1 -0
- package/dist/{client → vercel/client}/utils.js +2 -2
- package/dist/vercel/client/utils.js.map +1 -0
- package/dist/vercel/deltas.d.ts +31 -0
- package/dist/vercel/deltas.d.ts.map +1 -0
- package/dist/vercel/deltas.js +525 -0
- package/dist/vercel/deltas.js.map +1 -0
- package/dist/{client → vercel}/index.d.ts +484 -97
- package/dist/vercel/index.d.ts.map +1 -0
- package/dist/{client → vercel}/index.js +65 -38
- package/dist/vercel/index.js.map +1 -0
- package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -13
- package/dist/vercel/mapping.d.ts.map +1 -0
- package/dist/{mapping.js → vercel/mapping.js} +411 -112
- package/dist/vercel/mapping.js.map +1 -0
- package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
- package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
- package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
- package/dist/{react → vercel/react}/types.d.ts +1 -1
- package/dist/vercel/react/types.d.ts.map +1 -0
- package/dist/{react → vercel/react}/types.js.map +1 -1
- package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
- package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
- package/dist/vercel/react/useDeltaStreams.js.map +1 -0
- package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
- package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/vercel/react/useStreamingUIMessages.js +112 -0
- package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
- package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
- package/dist/vercel/react/useThreadMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.d.ts +2 -1
- package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.js +8 -4
- package/dist/vercel/react/useUIMessages.js.map +1 -0
- package/package.json +30 -22
- package/src/client/messages.ts +5 -107
- package/src/client/types.ts +0 -621
- package/src/component/_generated/component.ts +1089 -48
- package/src/component/apiKeys.ts +5 -5
- package/src/component/files.test.ts +1 -1
- package/src/component/files.ts +14 -12
- package/src/component/messages.test.ts +273 -1
- package/src/component/messages.ts +99 -57
- package/src/component/streams.ts +80 -64
- package/src/component/threads.ts +12 -9
- package/src/component/users.ts +2 -2
- package/src/component/vector/index.ts +14 -7
- package/src/eslintConfig.test.ts +44 -0
- package/src/react/index.ts +8 -5
- package/src/shared.ts +26 -36
- package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
- package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
- package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
- package/src/streaming/persistedUIMessageChunks.ts +658 -0
- package/src/validators.test.ts +4 -1
- package/src/validators.ts +89 -8
- package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
- package/src/{UIMessages.ts → vercel/UIMessages.ts} +96 -5
- package/src/{client → vercel/client}/approval.test.ts +48 -19
- package/src/{client → vercel/client}/createTool.ts +63 -23
- package/src/{client → vercel/client}/definePlaygroundAPI.ts +44 -29
- package/src/{client → vercel/client}/files.ts +2 -2
- package/src/{client → vercel/client}/index.test.ts +162 -3
- package/src/vercel/client/messages.ts +182 -0
- package/src/{client → vercel/client}/mockModel.ts +38 -39
- package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
- package/src/{client → vercel/client}/saveInputMessages.ts +7 -4
- package/src/{client → vercel/client}/search.test.ts +3 -3
- package/src/{client → vercel/client}/search.ts +4 -4
- package/src/{client → vercel/client}/setup.test.ts +6 -3
- package/src/vercel/client/start.test.ts +14 -0
- package/src/{client → vercel/client}/start.ts +147 -36
- package/src/vercel/client/streamText.test.ts +223 -0
- package/src/{client → vercel/client}/streamText.ts +102 -31
- package/src/{client → vercel/client}/streaming.integration.test.ts +40 -115
- package/src/{client → vercel/client}/streaming.test.ts +178 -4
- package/src/{client → vercel/client}/streaming.ts +114 -52
- package/src/vercel/client/types.test.ts +13 -0
- package/src/vercel/client/types.ts +679 -0
- package/src/{client → vercel/client}/utils.ts +13 -7
- package/src/vercel/deltas.test.ts +898 -0
- package/src/vercel/deltas.ts +633 -0
- package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
- package/src/{client → vercel}/index.ts +169 -62
- package/src/vercel/mapping.test.ts +965 -0
- package/src/{mapping.ts → vercel/mapping.ts} +501 -131
- package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
- package/src/{react → vercel/react}/types.ts +1 -1
- package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
- package/src/{react → vercel/react}/useStreamingUIMessages.ts +67 -35
- package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
- package/src/{react → vercel/react}/useUIMessages.test.ts +80 -1
- package/src/{react → vercel/react}/useUIMessages.ts +13 -5
- package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
- package/dist/UIMessages.d.ts.map +0 -1
- package/dist/UIMessages.js.map +0 -1
- package/dist/client/createTool.d.ts.map +0 -1
- package/dist/client/createTool.js.map +0 -1
- package/dist/client/definePlaygroundAPI.js.map +0 -1
- package/dist/client/files.d.ts.map +0 -1
- package/dist/client/files.js.map +0 -1
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/client/mockModel.d.ts.map +0 -1
- package/dist/client/mockModel.js.map +0 -1
- package/dist/client/saveInputMessages.d.ts.map +0 -1
- package/dist/client/saveInputMessages.js.map +0 -1
- package/dist/client/search.d.ts.map +0 -1
- package/dist/client/search.js.map +0 -1
- package/dist/client/start.d.ts.map +0 -1
- package/dist/client/start.js.map +0 -1
- package/dist/client/streamText.d.ts.map +0 -1
- package/dist/client/streamText.js.map +0 -1
- package/dist/client/streaming.d.ts.map +0 -1
- package/dist/client/streaming.js.map +0 -1
- package/dist/client/utils.d.ts.map +0 -1
- package/dist/client/utils.js.map +0 -1
- package/dist/deltas.d.ts +0 -43
- package/dist/deltas.d.ts.map +0 -1
- package/dist/deltas.js +0 -446
- package/dist/deltas.js.map +0 -1
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js.map +0 -1
- package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
- package/dist/react/optimisticallySendMessage.js.map +0 -1
- package/dist/react/types.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.js.map +0 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
- package/dist/react/useStreamingUIMessages.js +0 -92
- package/dist/react/useStreamingUIMessages.js.map +0 -1
- package/dist/react/useThreadMessages.d.ts.map +0 -1
- package/dist/react/useThreadMessages.js.map +0 -1
- package/dist/react/useUIMessages.d.ts.map +0 -1
- package/dist/react/useUIMessages.js.map +0 -1
- package/src/client/streamText.test.ts +0 -102
- package/src/deltas.test.ts +0 -626
- package/src/deltas.ts +0 -569
- package/src/mapping.test.ts +0 -402
- /package/dist/{client → vercel/client}/files.js +0 -0
- /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
- /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
- /package/dist/{react → vercel/react}/types.js +0 -0
- /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
package/src/component/apiKeys.ts
CHANGED
|
@@ -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
|
}
|
package/src/component/files.ts
CHANGED
|
@@ -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
|
});
|