vibefast-cli 0.2.2 → 0.2.4
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/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
- package/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
- package/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
- package/.wrangler/state/v3/r2/vibefast-recipes/blobs/177b5d7279681c1bec396cafe63779a2d89eaf538109e55733147727276e2b9f0000019a81f04ba2 +0 -0
- package/.wrangler/state/v3/r2/vibefast-recipes/blobs/4fe398bba6e2d5f13b569bc1be4244e696d86caa04c323db2d9fb0b9381c508f0000019a81f0503f +0 -0
- package/.wrangler/state/v3/r2/vibefast-recipes/blobs/f68f19a655380ac7fb575eb49c0623cde74046261ed89c498ba5107b8aacde9d0000019a81f05484 +0 -0
- package/DOCS-CLEANUP-SUMMARY.md +140 -0
- package/DOCS.md +141 -0
- package/IMPLEMENTATION-COMPLETE.md +6 -5
- package/MANUAL-STEPS-GUIDE.md +385 -0
- package/MANUAL-STEPS-USER-FLOW.md +231 -0
- package/PLAN-VS-IMPLEMENTATION.md +248 -0
- package/PUBLISHED-0.2.2.md +65 -0
- package/README.md +24 -2
- package/START-HERE.md +115 -0
- package/docs/next-steps.md +12 -0
- package/package.json +10 -1
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/index.ts +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +3 -0
- package/recipes/charts/apps/native/src/features/charts/types/index.ts +3 -0
- package/recipes/charts/recipe.json +7 -1
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +1 -0
- package/recipes/chatbot/packages/backend/convex/agents.ts +116 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/index.ts +30 -0
- package/recipes/chatbot/packages/backend/convex/chatbotAgent.ts +1085 -0
- package/recipes/chatbot/packages/backend/convex/chatbotHistory.ts +307 -0
- package/recipes/chatbot/packages/backend/convex/lib/rateLimit.ts +100 -0
- package/recipes/chatbot/packages/backend/convex/lib/telemetry.ts +29 -0
- package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +714 -0
- package/recipes/chatbot/packages/backend/convex/tools/index.ts +18 -0
- package/recipes/chatbot/packages/backend/convex/tools/knowledgeRetrieval.ts +92 -0
- package/recipes/chatbot/packages/backend/convex/tools/tavilySearch.ts +83 -0
- package/recipes/chatbot/packages/backend/convex/tools/userProfile.ts +72 -0
- package/recipes/chatbot/recipe.json +104 -1
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-generator/packages/backend/convex/imageGeneration/index.ts +12 -0
- package/recipes/image-generator/packages/backend/convex/imageGeneratorFunctions.ts +290 -0
- package/recipes/image-generator/recipe.json +41 -1
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/voice-bot/packages/backend/convex/router.ts +81 -0
- package/recipes/voice-bot/recipe.json +48 -1
- package/recipes/voice-bot@latest.zip +0 -0
- package/scripts/create-recipes.mjs +33 -1
- package/MONITORING-AND-ANNOUNCEMENT-GUIDE.md +0 -669
- package/PRE-PUBLISH-CHECKLIST.md +0 -558
- package/PUBLISHED-SUCCESS.md +0 -282
- package/READY-TO-PUBLISH.md +0 -419
- package/RECIPES-READY.md +0 -172
- package/cloudflare-worker/mini-native@latest.zip +0 -0
- package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +0 -15
- package/cloudflare-worker/test-recipe/recipe.json +0 -16
- package/text.md +0 -27
- /package/{AUTO-DETECT-DEPS.md → docs/archive/AUTO-DETECT-DEPS.md} +0 -0
- /package/{FINAL-PACKAGE-STRATEGY.md → docs/archive/FINAL-PACKAGE-STRATEGY.md} +0 -0
- /package/{FINAL-SIMPLE-PLAN.md → docs/archive/FINAL-SIMPLE-PLAN.md} +0 -0
- /package/{FINAL-STATUS.md → docs/archive/FINAL-STATUS.md} +0 -0
- /package/{FLOW-DIAGRAM.md → docs/archive/FLOW-DIAGRAM.md} +0 -0
- /package/{GOTCHAS-AND-RISKS.md → docs/archive/GOTCHAS-AND-RISKS.md} +0 -0
- /package/{IMPLEMENTATION-PLAN.md → docs/archive/IMPLEMENTATION-PLAN.md} +0 -0
- /package/{PLAN.md → docs/archive/PLAN.md} +0 -0
- /package/{PRODUCTION-READINESS.md → docs/archive/PRODUCTION-READINESS.md} +0 -0
- /package/{PRODUCTION-TEST-RESULTS.md → docs/archive/PRODUCTION-TEST-RESULTS.md} +0 -0
- /package/{SIMPLIFIED-PLAN.md → docs/archive/SIMPLIFIED-PLAN.md} +0 -0
- /package/{STATUS.md → docs/archive/STATUS.md} +0 -0
- /package/{SUCCESS.md → docs/archive/SUCCESS.md} +0 -0
- /package/{TEST-SUMMARY.md → docs/archive/TEST-SUMMARY.md} +0 -0
- /package/{TESTING-CHECKLIST.md → docs/archive/TESTING-CHECKLIST.md} +0 -0
- /package/{USER-MODIFICATIONS.md → docs/archive/USER-MODIFICATIONS.md} +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { getAuthUserId } from '@convex-dev/auth/server';
|
|
2
|
+
import { paginationOptsValidator } from 'convex/server';
|
|
3
|
+
import { v } from 'convex/values';
|
|
4
|
+
|
|
5
|
+
import { mutation, query } from './_generated/server';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a new conversation for a user
|
|
9
|
+
*/
|
|
10
|
+
export const createConversation = mutation({
|
|
11
|
+
args: {
|
|
12
|
+
name: v.optional(v.string()),
|
|
13
|
+
},
|
|
14
|
+
returns: v.id('conversations'),
|
|
15
|
+
handler: async (ctx, args) => {
|
|
16
|
+
const userId = await getAuthUserId(ctx);
|
|
17
|
+
if (!userId) {
|
|
18
|
+
throw new Error('Not authenticated');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const conversationId = await ctx.db.insert('conversations', {
|
|
22
|
+
userId,
|
|
23
|
+
name: args.name,
|
|
24
|
+
});
|
|
25
|
+
return conversationId;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get or create a default conversation for a user
|
|
31
|
+
* This ensures each user has exactly one default conversation
|
|
32
|
+
*/
|
|
33
|
+
export const getOrCreateDefaultConversation = mutation({
|
|
34
|
+
args: {},
|
|
35
|
+
returns: v.id('conversations'),
|
|
36
|
+
handler: async (ctx, _args) => {
|
|
37
|
+
const userId = await getAuthUserId(ctx);
|
|
38
|
+
if (!userId) {
|
|
39
|
+
throw new Error('Not authenticated');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// First, try to find an existing conversation for this user
|
|
43
|
+
const existingConversation = await ctx.db
|
|
44
|
+
.query('conversations')
|
|
45
|
+
.withIndex('by_userId', (q) => q.eq('userId', userId))
|
|
46
|
+
.first();
|
|
47
|
+
|
|
48
|
+
if (existingConversation) {
|
|
49
|
+
return existingConversation._id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If no conversation exists, create a new one
|
|
53
|
+
const conversationId = await ctx.db.insert('conversations', {
|
|
54
|
+
userId,
|
|
55
|
+
name: 'Default Chat',
|
|
56
|
+
});
|
|
57
|
+
return conversationId;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Store a message in a conversation (for user messages and finalized AI responses)
|
|
63
|
+
*/
|
|
64
|
+
export const storeMessage = mutation({
|
|
65
|
+
args: {
|
|
66
|
+
conversationId: v.id('conversations'),
|
|
67
|
+
authorType: v.union(v.literal('user'), v.literal('bot')),
|
|
68
|
+
/** Whether the message is finished streaming */
|
|
69
|
+
isComplete: v.optional(v.boolean()),
|
|
70
|
+
text: v.optional(v.string()),
|
|
71
|
+
attachments: v.optional(
|
|
72
|
+
v.array(
|
|
73
|
+
v.object({
|
|
74
|
+
type: v.union(v.literal('image')),
|
|
75
|
+
storageId: v.id('_storage'),
|
|
76
|
+
url: v.optional(v.string()),
|
|
77
|
+
fileName: v.optional(v.string()),
|
|
78
|
+
mimeType: v.optional(v.string()),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
aiProvider: v.optional(v.string()),
|
|
83
|
+
aiModel: v.optional(v.string()),
|
|
84
|
+
},
|
|
85
|
+
returns: v.id('messages'),
|
|
86
|
+
handler: async (ctx, args) => {
|
|
87
|
+
const userId = await getAuthUserId(ctx);
|
|
88
|
+
if (!userId) {
|
|
89
|
+
throw new Error('Not authenticated');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const textToStore = args.text?.trim() ? args.text.trim() : undefined;
|
|
93
|
+
// If there's no text and no attachments, maybe don't store? Or store as empty.
|
|
94
|
+
// Current logic: if text is undefined/empty string but attachments exist, text will be undefined.
|
|
95
|
+
// If text is spaces only and no attachments, text will be undefined.
|
|
96
|
+
if (
|
|
97
|
+
textToStore === undefined &&
|
|
98
|
+
(!args.attachments || args.attachments.length === 0)
|
|
99
|
+
) {
|
|
100
|
+
// Prevent storing messages that are truly empty (no text, no attachments).
|
|
101
|
+
// This was previously a comment, now it's enforced.
|
|
102
|
+
throw new Error(
|
|
103
|
+
'Cannot store a message with no text and no attachments.',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return await ctx.db.insert('messages', {
|
|
108
|
+
...args,
|
|
109
|
+
userId,
|
|
110
|
+
text: textToStore, // Use the potentially undefined trimmed text
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Store a finalized bot message after streaming completes
|
|
117
|
+
* Internal mutation called from the HTTP action
|
|
118
|
+
*/
|
|
119
|
+
export const storeFinalBotMessage = mutation({
|
|
120
|
+
args: {
|
|
121
|
+
conversationId: v.id('conversations'),
|
|
122
|
+
text: v.string(),
|
|
123
|
+
aiProvider: v.optional(v.string()),
|
|
124
|
+
aiModel: v.optional(v.string()),
|
|
125
|
+
},
|
|
126
|
+
returns: v.id('messages'),
|
|
127
|
+
handler: async (ctx, args) => {
|
|
128
|
+
const userId = await getAuthUserId(ctx);
|
|
129
|
+
if (!userId) {
|
|
130
|
+
throw new Error('Not authenticated');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return await ctx.db.insert('messages', {
|
|
134
|
+
conversationId: args.conversationId,
|
|
135
|
+
userId,
|
|
136
|
+
authorType: 'bot' as const,
|
|
137
|
+
text: args.text.trim() || undefined,
|
|
138
|
+
aiProvider: args.aiProvider,
|
|
139
|
+
aiModel: args.aiModel,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clear all messages in a conversation
|
|
146
|
+
* Uses batching to handle large conversations without timeout
|
|
147
|
+
*/
|
|
148
|
+
export const clearConversationMessages = mutation({
|
|
149
|
+
args: { conversationId: v.id('conversations') },
|
|
150
|
+
returns: v.null(),
|
|
151
|
+
handler: async (ctx, args) => {
|
|
152
|
+
const userId = await getAuthUserId(ctx);
|
|
153
|
+
if (!userId) {
|
|
154
|
+
throw new Error('Not authenticated');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const conversation = await ctx.db.get(args.conversationId);
|
|
158
|
+
if (!conversation || conversation.userId !== userId) {
|
|
159
|
+
throw new Error('Conversation not found or access denied');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Delete in batches to avoid timeout on large conversations
|
|
163
|
+
const BATCH_SIZE = 100;
|
|
164
|
+
let deletedCount = 0;
|
|
165
|
+
|
|
166
|
+
while (true) {
|
|
167
|
+
const messages = await ctx.db
|
|
168
|
+
.query('messages')
|
|
169
|
+
.withIndex('by_conversationId', (q) =>
|
|
170
|
+
q.eq('conversationId', args.conversationId),
|
|
171
|
+
)
|
|
172
|
+
.take(BATCH_SIZE);
|
|
173
|
+
|
|
174
|
+
if (messages.length === 0) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const message of messages) {
|
|
179
|
+
await ctx.db.delete(message._id);
|
|
180
|
+
deletedCount++;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// If we got fewer than BATCH_SIZE, we're done
|
|
184
|
+
if (messages.length < BATCH_SIZE) {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(
|
|
190
|
+
`[clearConversationMessages] Cleared ${deletedCount} messages from conversation ${args.conversationId}`,
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Delete a single user-authored message. Used to roll back optimistic UI when
|
|
197
|
+
* downstream agent execution fails.
|
|
198
|
+
*/
|
|
199
|
+
export const deleteUserMessage = mutation({
|
|
200
|
+
args: {
|
|
201
|
+
messageId: v.id('messages'),
|
|
202
|
+
},
|
|
203
|
+
returns: v.null(),
|
|
204
|
+
handler: async (ctx, args) => {
|
|
205
|
+
const userId = await getAuthUserId(ctx);
|
|
206
|
+
if (!userId) {
|
|
207
|
+
throw new Error('Not authenticated');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const message = await ctx.db.get(args.messageId);
|
|
211
|
+
if (!message) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (message.userId !== userId) {
|
|
216
|
+
throw new Error('Cannot delete message owned by another user.');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (message.authorType !== 'user') {
|
|
220
|
+
throw new Error('Only user messages can be deleted via this pathway.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await ctx.db.delete(args.messageId);
|
|
224
|
+
return null;
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* List messages in a conversation with pagination
|
|
230
|
+
*/
|
|
231
|
+
export const listMessages = query({
|
|
232
|
+
args: {
|
|
233
|
+
conversationId: v.id('conversations'),
|
|
234
|
+
paginationOpts: paginationOptsValidator,
|
|
235
|
+
},
|
|
236
|
+
returns: v.object({
|
|
237
|
+
page: v.array(
|
|
238
|
+
v.object({
|
|
239
|
+
_id: v.id('messages'),
|
|
240
|
+
_creationTime: v.number(),
|
|
241
|
+
conversationId: v.id('conversations'),
|
|
242
|
+
userId: v.id('users'),
|
|
243
|
+
authorType: v.union(v.literal('user'), v.literal('bot')),
|
|
244
|
+
text: v.optional(v.string()),
|
|
245
|
+
toolCalls: v.optional(
|
|
246
|
+
v.array(
|
|
247
|
+
v.object({
|
|
248
|
+
toolName: v.string(),
|
|
249
|
+
args: v.any(),
|
|
250
|
+
result: v.optional(v.any()),
|
|
251
|
+
}),
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
metadata: v.optional(
|
|
255
|
+
v.object({
|
|
256
|
+
streamId: v.optional(v.string()),
|
|
257
|
+
status: v.optional(v.string()),
|
|
258
|
+
usage: v.optional(
|
|
259
|
+
v.object({
|
|
260
|
+
promptTokens: v.optional(v.number()),
|
|
261
|
+
completionTokens: v.optional(v.number()),
|
|
262
|
+
totalTokens: v.optional(v.number()),
|
|
263
|
+
}),
|
|
264
|
+
),
|
|
265
|
+
}),
|
|
266
|
+
),
|
|
267
|
+
attachments: v.optional(
|
|
268
|
+
v.array(
|
|
269
|
+
v.object({
|
|
270
|
+
type: v.union(v.literal('image')),
|
|
271
|
+
storageId: v.id('_storage'),
|
|
272
|
+
url: v.optional(v.string()),
|
|
273
|
+
fileName: v.optional(v.string()),
|
|
274
|
+
mimeType: v.optional(v.string()),
|
|
275
|
+
}),
|
|
276
|
+
),
|
|
277
|
+
),
|
|
278
|
+
aiProvider: v.optional(v.string()),
|
|
279
|
+
aiModel: v.optional(v.string()),
|
|
280
|
+
}),
|
|
281
|
+
),
|
|
282
|
+
isDone: v.boolean(),
|
|
283
|
+
continueCursor: v.union(v.string(), v.null()),
|
|
284
|
+
pageStatus: v.optional(v.union(v.string(), v.null())),
|
|
285
|
+
splitCursor: v.optional(v.union(v.string(), v.null())),
|
|
286
|
+
}),
|
|
287
|
+
handler: async (ctx, args) => {
|
|
288
|
+
const userId = await getAuthUserId(ctx);
|
|
289
|
+
if (!userId) {
|
|
290
|
+
throw new Error('Not authenticated');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Verify the user owns this conversation
|
|
294
|
+
const conversation = await ctx.db.get(args.conversationId);
|
|
295
|
+
if (!conversation || conversation.userId !== userId) {
|
|
296
|
+
throw new Error('Conversation not found or access denied');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return await ctx.db
|
|
300
|
+
.query('messages')
|
|
301
|
+
.withIndex('by_conversationId', (q) =>
|
|
302
|
+
q.eq('conversationId', args.conversationId),
|
|
303
|
+
)
|
|
304
|
+
.order('desc')
|
|
305
|
+
.paginate(args.paginationOpts);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Id } from '../_generated/dataModel';
|
|
2
|
+
import type { MutationCtx } from '../_generated/server';
|
|
3
|
+
|
|
4
|
+
const AGENT_WINDOW_MS = 60 * 1000; // 1 minute
|
|
5
|
+
const AGENT_LIMIT = 12; // max agent calls per window
|
|
6
|
+
|
|
7
|
+
const TOOL_WINDOW_MS = 60 * 1000;
|
|
8
|
+
const TOOL_LIMIT = 20;
|
|
9
|
+
|
|
10
|
+
type RateLimitFields =
|
|
11
|
+
| [
|
|
12
|
+
'agentRateLimitWindowStart',
|
|
13
|
+
'agentRateLimitCount',
|
|
14
|
+
typeof AGENT_WINDOW_MS,
|
|
15
|
+
typeof AGENT_LIMIT,
|
|
16
|
+
]
|
|
17
|
+
| [
|
|
18
|
+
'toolRateLimitWindowStart',
|
|
19
|
+
'toolRateLimitCount',
|
|
20
|
+
typeof TOOL_WINDOW_MS,
|
|
21
|
+
typeof TOOL_LIMIT,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const getNow = () => Date.now();
|
|
25
|
+
|
|
26
|
+
async function enforceRateLimit(
|
|
27
|
+
db: MutationCtx['db'],
|
|
28
|
+
userId: Id<'users'>,
|
|
29
|
+
[windowField, countField, windowMs, limit]: RateLimitFields,
|
|
30
|
+
friendlyName: string,
|
|
31
|
+
) {
|
|
32
|
+
const userDoc = await db.get(userId);
|
|
33
|
+
if (!userDoc) {
|
|
34
|
+
throw new Error('User not found while enforcing rate limit.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nowMs = getNow();
|
|
38
|
+
const record = userDoc as unknown as Record<string, number | undefined>;
|
|
39
|
+
const windowStart = record[windowField] ?? 0;
|
|
40
|
+
const currentCount = record[countField] ?? 0;
|
|
41
|
+
|
|
42
|
+
const withinWindow = nowMs - windowStart < windowMs;
|
|
43
|
+
const nextCount = withinWindow ? currentCount + 1 : 1;
|
|
44
|
+
|
|
45
|
+
if (withinWindow && nextCount > limit) {
|
|
46
|
+
const retrySeconds = Math.ceil((windowMs - (nowMs - windowStart)) / 1000);
|
|
47
|
+
const message = `${friendlyName} limit reached. Please wait ${retrySeconds}s before trying again.`;
|
|
48
|
+
const error = new Error(message);
|
|
49
|
+
(error as Error & { code?: string }).code = 'RATE_LIMIT';
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await db.patch(userId, {
|
|
54
|
+
[windowField]: withinWindow ? windowStart : nowMs,
|
|
55
|
+
[countField]: nextCount,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function enforceAgentRateLimit(
|
|
60
|
+
db: MutationCtx['db'],
|
|
61
|
+
userId: Id<'users'>,
|
|
62
|
+
) {
|
|
63
|
+
return enforceRateLimit(
|
|
64
|
+
db,
|
|
65
|
+
userId,
|
|
66
|
+
[
|
|
67
|
+
'agentRateLimitWindowStart',
|
|
68
|
+
'agentRateLimitCount',
|
|
69
|
+
AGENT_WINDOW_MS,
|
|
70
|
+
AGENT_LIMIT,
|
|
71
|
+
],
|
|
72
|
+
'Chat agent',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function enforceToolRateLimit(
|
|
77
|
+
db: MutationCtx['db'],
|
|
78
|
+
userId: Id<'users'>,
|
|
79
|
+
) {
|
|
80
|
+
return enforceRateLimit(
|
|
81
|
+
db,
|
|
82
|
+
userId,
|
|
83
|
+
[
|
|
84
|
+
'toolRateLimitWindowStart',
|
|
85
|
+
'toolRateLimitCount',
|
|
86
|
+
TOOL_WINDOW_MS,
|
|
87
|
+
TOOL_LIMIT,
|
|
88
|
+
],
|
|
89
|
+
'Tool usage',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resetRateLimits(db: MutationCtx['db'], userId: Id<'users'>) {
|
|
94
|
+
return db.patch(userId, {
|
|
95
|
+
agentRateLimitWindowStart: undefined,
|
|
96
|
+
agentRateLimitCount: undefined,
|
|
97
|
+
toolRateLimitWindowStart: undefined,
|
|
98
|
+
toolRateLimitCount: undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type TelemetryMode = 'off' | 'sample' | 'debug';
|
|
2
|
+
|
|
3
|
+
function normalizeMode(raw: string | undefined): TelemetryMode {
|
|
4
|
+
if (raw === 'sample' || raw === 'debug') {
|
|
5
|
+
return raw;
|
|
6
|
+
}
|
|
7
|
+
return 'off';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getChatTelemetryMode(): TelemetryMode {
|
|
11
|
+
return normalizeMode(process.env.CHAT_LATENCY_TELEMETRY_MODE);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isChatTelemetryEnabled(): boolean {
|
|
15
|
+
return getChatTelemetryMode() !== 'off';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldSampleChatTelemetry(probability = 0.1): boolean {
|
|
19
|
+
const mode = getChatTelemetryMode();
|
|
20
|
+
if (mode === 'off') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (mode === 'debug') {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const clampedProbability = Math.min(Math.max(probability, 0), 1);
|
|
28
|
+
return Math.random() < clampedProbability;
|
|
29
|
+
}
|