@okrlinkhub/agent-factory 0.2.11 → 0.2.13

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 (74) hide show
  1. package/dist/client/_generated/_ignore.d.ts +1 -0
  2. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  3. package/dist/client/_generated/_ignore.js +3 -0
  4. package/dist/client/_generated/_ignore.js.map +1 -0
  5. package/dist/client/bridge.d.ts +65 -0
  6. package/dist/client/bridge.d.ts.map +1 -0
  7. package/dist/client/bridge.js +192 -0
  8. package/dist/client/bridge.js.map +1 -0
  9. package/dist/client/index.d.ts +545 -0
  10. package/dist/client/index.d.ts.map +1 -0
  11. package/dist/client/index.js +854 -0
  12. package/dist/client/index.js.map +1 -0
  13. package/dist/component/_generated/api.d.ts +46 -0
  14. package/dist/component/_generated/api.d.ts.map +1 -0
  15. package/dist/component/_generated/api.js +31 -0
  16. package/dist/component/_generated/api.js.map +1 -0
  17. package/dist/component/_generated/component.d.ts +1474 -0
  18. package/dist/component/_generated/component.d.ts.map +1 -0
  19. package/dist/component/_generated/component.js +11 -0
  20. package/dist/component/_generated/component.js.map +1 -0
  21. package/dist/component/_generated/dataModel.d.ts +46 -0
  22. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  23. package/dist/component/_generated/dataModel.js +11 -0
  24. package/dist/component/_generated/dataModel.js.map +1 -0
  25. package/dist/component/_generated/server.d.ts +121 -0
  26. package/dist/component/_generated/server.d.ts.map +1 -0
  27. package/dist/component/_generated/server.js +78 -0
  28. package/dist/component/_generated/server.js.map +1 -0
  29. package/dist/component/config.d.ts +254 -0
  30. package/dist/component/config.d.ts.map +1 -0
  31. package/dist/component/config.js +157 -0
  32. package/dist/component/config.js.map +1 -0
  33. package/dist/component/convex.config.d.ts +3 -0
  34. package/dist/component/convex.config.d.ts.map +1 -0
  35. package/dist/component/convex.config.js +3 -0
  36. package/dist/component/convex.config.js.map +1 -0
  37. package/dist/component/identity.d.ts +111 -0
  38. package/dist/component/identity.d.ts.map +1 -0
  39. package/dist/component/identity.js +455 -0
  40. package/dist/component/identity.js.map +1 -0
  41. package/dist/component/lib.d.ts +6 -0
  42. package/dist/component/lib.d.ts.map +1 -0
  43. package/dist/component/lib.js +6 -0
  44. package/dist/component/lib.js.map +1 -0
  45. package/dist/component/providers/fly.d.ts +51 -0
  46. package/dist/component/providers/fly.d.ts.map +1 -0
  47. package/dist/component/providers/fly.js +234 -0
  48. package/dist/component/providers/fly.js.map +1 -0
  49. package/dist/component/pushing.d.ts +256 -0
  50. package/dist/component/pushing.d.ts.map +1 -0
  51. package/dist/component/pushing.js +1009 -0
  52. package/dist/component/pushing.js.map +1 -0
  53. package/dist/component/queue.d.ts +439 -0
  54. package/dist/component/queue.d.ts.map +1 -0
  55. package/dist/component/queue.js +1761 -0
  56. package/dist/component/queue.js.map +1 -0
  57. package/dist/component/scheduler.d.ts +116 -0
  58. package/dist/component/scheduler.d.ts.map +1 -0
  59. package/dist/component/scheduler.js +410 -0
  60. package/dist/component/scheduler.js.map +1 -0
  61. package/dist/component/schema.d.ts +710 -0
  62. package/dist/component/schema.d.ts.map +1 -0
  63. package/dist/component/schema.js +337 -0
  64. package/dist/component/schema.js.map +1 -0
  65. package/dist/react/index.d.ts +2 -0
  66. package/dist/react/index.d.ts.map +1 -0
  67. package/dist/react/index.js +6 -0
  68. package/dist/react/index.js.map +1 -0
  69. package/package.json +1 -1
  70. package/src/client/index.ts +61 -2
  71. package/src/component/_generated/component.ts +102 -0
  72. package/src/component/config.ts +5 -0
  73. package/src/component/queue.ts +426 -0
  74. package/src/component/schema.ts +45 -0
@@ -0,0 +1,1009 @@
1
+ import { v } from "convex/values";
2
+ import { internal } from "./_generated/api.js";
3
+ import { mutation, query } from "./_generated/server.js";
4
+ import { providerConfigValidator } from "./config.js";
5
+ const periodicityValidator = v.union(v.literal("manual"), v.literal("daily"), v.literal("weekly"), v.literal("monthly"));
6
+ const suggestedTimeValidator = v.union(v.object({
7
+ kind: v.literal("daily"),
8
+ time: v.string(),
9
+ }), v.object({
10
+ kind: v.literal("weekly"),
11
+ weekday: v.number(),
12
+ time: v.string(),
13
+ }), v.object({
14
+ kind: v.literal("monthly"),
15
+ dayOfMonth: v.union(v.number(), v.literal("last")),
16
+ time: v.string(),
17
+ }));
18
+ const scheduleValidator = v.union(v.object({
19
+ kind: v.literal("manual"),
20
+ }), v.object({
21
+ kind: v.literal("daily"),
22
+ time: v.string(),
23
+ }), v.object({
24
+ kind: v.literal("weekly"),
25
+ weekday: v.number(),
26
+ time: v.string(),
27
+ }), v.object({
28
+ kind: v.literal("monthly"),
29
+ dayOfMonth: v.union(v.number(), v.literal("last")),
30
+ time: v.string(),
31
+ }));
32
+ const templateViewValidator = v.object({
33
+ _id: v.id("messagePushTemplates"),
34
+ companyId: v.string(),
35
+ templateKey: v.string(),
36
+ title: v.string(),
37
+ text: v.string(),
38
+ periodicity: periodicityValidator,
39
+ suggestedTimes: v.array(suggestedTimeValidator),
40
+ enabled: v.boolean(),
41
+ createdBy: v.string(),
42
+ updatedBy: v.string(),
43
+ createdAt: v.number(),
44
+ updatedAt: v.number(),
45
+ });
46
+ const jobViewValidator = v.object({
47
+ _id: v.id("messagePushJobs"),
48
+ companyId: v.string(),
49
+ consumerUserId: v.string(),
50
+ agentKey: v.union(v.null(), v.string()),
51
+ sourceTemplateId: v.union(v.null(), v.id("messagePushTemplates")),
52
+ title: v.string(),
53
+ text: v.string(),
54
+ periodicity: periodicityValidator,
55
+ timezone: v.string(),
56
+ schedule: scheduleValidator,
57
+ enabled: v.boolean(),
58
+ nextRunAt: v.union(v.null(), v.number()),
59
+ lastRunAt: v.union(v.null(), v.number()),
60
+ lastRunKey: v.union(v.null(), v.string()),
61
+ createdAt: v.number(),
62
+ updatedAt: v.number(),
63
+ });
64
+ const dispatchStatusValidator = v.union(v.literal("enqueued"), v.literal("skipped"), v.literal("failed"));
65
+ export const createPushTemplate = mutation({
66
+ args: {
67
+ companyId: v.string(),
68
+ templateKey: v.string(),
69
+ title: v.string(),
70
+ text: v.string(),
71
+ periodicity: periodicityValidator,
72
+ suggestedTimes: v.array(suggestedTimeValidator),
73
+ enabled: v.optional(v.boolean()),
74
+ actorUserId: v.string(),
75
+ nowMs: v.optional(v.number()),
76
+ },
77
+ returns: v.id("messagePushTemplates"),
78
+ handler: async (ctx, args) => {
79
+ validateTemplateTimes(args.periodicity, args.suggestedTimes);
80
+ const existing = await ctx.db
81
+ .query("messagePushTemplates")
82
+ .withIndex("by_companyId_and_templateKey", (q) => q.eq("companyId", args.companyId).eq("templateKey", args.templateKey))
83
+ .unique();
84
+ if (existing) {
85
+ throw new Error(`Template key '${args.templateKey}' already exists for company`);
86
+ }
87
+ const nowMs = args.nowMs ?? Date.now();
88
+ return await ctx.db.insert("messagePushTemplates", {
89
+ companyId: args.companyId,
90
+ templateKey: args.templateKey,
91
+ title: args.title,
92
+ text: args.text,
93
+ periodicity: args.periodicity,
94
+ suggestedTimes: args.suggestedTimes,
95
+ enabled: args.enabled ?? true,
96
+ createdBy: args.actorUserId,
97
+ updatedBy: args.actorUserId,
98
+ createdAt: nowMs,
99
+ updatedAt: nowMs,
100
+ });
101
+ },
102
+ });
103
+ export const updatePushTemplate = mutation({
104
+ args: {
105
+ templateId: v.id("messagePushTemplates"),
106
+ title: v.optional(v.string()),
107
+ text: v.optional(v.string()),
108
+ periodicity: v.optional(periodicityValidator),
109
+ suggestedTimes: v.optional(v.array(suggestedTimeValidator)),
110
+ enabled: v.optional(v.boolean()),
111
+ actorUserId: v.string(),
112
+ nowMs: v.optional(v.number()),
113
+ },
114
+ returns: v.boolean(),
115
+ handler: async (ctx, args) => {
116
+ const template = await ctx.db.get(args.templateId);
117
+ if (!template)
118
+ return false;
119
+ const nextPeriodicity = args.periodicity ?? template.periodicity;
120
+ const nextSuggestedTimes = args.suggestedTimes ?? template.suggestedTimes;
121
+ validateTemplateTimes(nextPeriodicity, nextSuggestedTimes);
122
+ const nowMs = args.nowMs ?? Date.now();
123
+ await ctx.db.patch(template._id, {
124
+ title: args.title ?? template.title,
125
+ text: args.text ?? template.text,
126
+ periodicity: nextPeriodicity,
127
+ suggestedTimes: nextSuggestedTimes,
128
+ enabled: args.enabled ?? template.enabled,
129
+ updatedBy: args.actorUserId,
130
+ updatedAt: nowMs,
131
+ });
132
+ return true;
133
+ },
134
+ });
135
+ export const deletePushTemplate = mutation({
136
+ args: {
137
+ templateId: v.id("messagePushTemplates"),
138
+ },
139
+ returns: v.boolean(),
140
+ handler: async (ctx, args) => {
141
+ const template = await ctx.db.get(args.templateId);
142
+ if (!template)
143
+ return false;
144
+ await ctx.db.delete(template._id);
145
+ return true;
146
+ },
147
+ });
148
+ export const listPushTemplatesByCompany = query({
149
+ args: {
150
+ companyId: v.string(),
151
+ includeDisabled: v.optional(v.boolean()),
152
+ },
153
+ returns: v.array(templateViewValidator),
154
+ handler: async (ctx, args) => {
155
+ const includeDisabled = args.includeDisabled ?? false;
156
+ const rows = includeDisabled
157
+ ? await ctx.db
158
+ .query("messagePushTemplates")
159
+ .withIndex("by_companyId", (q) => q.eq("companyId", args.companyId))
160
+ .collect()
161
+ : await ctx.db
162
+ .query("messagePushTemplates")
163
+ .withIndex("by_companyId_and_enabled", (q) => q.eq("companyId", args.companyId).eq("enabled", true))
164
+ .collect();
165
+ rows.sort((a, b) => b.updatedAt - a.updatedAt);
166
+ return rows.map((row) => ({
167
+ _id: row._id,
168
+ companyId: row.companyId,
169
+ templateKey: row.templateKey,
170
+ title: row.title,
171
+ text: row.text,
172
+ periodicity: row.periodicity,
173
+ suggestedTimes: row.suggestedTimes,
174
+ enabled: row.enabled,
175
+ createdBy: row.createdBy,
176
+ updatedBy: row.updatedBy,
177
+ createdAt: row.createdAt,
178
+ updatedAt: row.updatedAt,
179
+ }));
180
+ },
181
+ });
182
+ export const createPushJobFromTemplate = mutation({
183
+ args: {
184
+ companyId: v.string(),
185
+ consumerUserId: v.string(),
186
+ templateId: v.id("messagePushTemplates"),
187
+ timezone: v.string(),
188
+ schedule: v.optional(scheduleValidator),
189
+ enabled: v.optional(v.boolean()),
190
+ nowMs: v.optional(v.number()),
191
+ },
192
+ returns: v.id("messagePushJobs"),
193
+ handler: async (ctx, args) => {
194
+ assertValidTimezone(args.timezone);
195
+ const template = await ctx.db.get(args.templateId);
196
+ if (!template) {
197
+ throw new Error("Template not found");
198
+ }
199
+ if (template.companyId !== args.companyId) {
200
+ throw new Error("Template company mismatch");
201
+ }
202
+ const schedule = resolveScheduleForTemplate(template.periodicity, template.suggestedTimes, args.schedule);
203
+ validateSchedule(template.periodicity, schedule);
204
+ const nowMs = args.nowMs ?? Date.now();
205
+ const enabled = args.enabled ?? true;
206
+ const recurringSchedule = toRecurringSchedule(template.periodicity, schedule);
207
+ const nextRunAt = enabled && recurringSchedule
208
+ ? computeNextRunAt({
209
+ periodicity: recurringSchedule.periodicity,
210
+ schedule: recurringSchedule.schedule,
211
+ timezone: args.timezone,
212
+ fromMs: nowMs,
213
+ })
214
+ : undefined;
215
+ const agentKey = await resolveActiveAgentKeyForUser(ctx, args.consumerUserId);
216
+ return await ctx.db.insert("messagePushJobs", {
217
+ companyId: args.companyId,
218
+ consumerUserId: args.consumerUserId,
219
+ agentKey: agentKey ?? undefined,
220
+ sourceTemplateId: template._id,
221
+ title: template.title,
222
+ text: template.text,
223
+ periodicity: template.periodicity,
224
+ timezone: args.timezone,
225
+ schedule,
226
+ enabled,
227
+ nextRunAt,
228
+ createdAt: nowMs,
229
+ updatedAt: nowMs,
230
+ });
231
+ },
232
+ });
233
+ export const createPushJobCustom = mutation({
234
+ args: {
235
+ companyId: v.string(),
236
+ consumerUserId: v.string(),
237
+ title: v.string(),
238
+ text: v.string(),
239
+ periodicity: periodicityValidator,
240
+ timezone: v.string(),
241
+ schedule: scheduleValidator,
242
+ enabled: v.optional(v.boolean()),
243
+ nowMs: v.optional(v.number()),
244
+ },
245
+ returns: v.id("messagePushJobs"),
246
+ handler: async (ctx, args) => {
247
+ assertValidTimezone(args.timezone);
248
+ validateSchedule(args.periodicity, args.schedule);
249
+ const nowMs = args.nowMs ?? Date.now();
250
+ const enabled = args.enabled ?? true;
251
+ const recurringSchedule = toRecurringSchedule(args.periodicity, args.schedule);
252
+ const nextRunAt = enabled && recurringSchedule
253
+ ? computeNextRunAt({
254
+ periodicity: recurringSchedule.periodicity,
255
+ schedule: recurringSchedule.schedule,
256
+ timezone: args.timezone,
257
+ fromMs: nowMs,
258
+ })
259
+ : undefined;
260
+ const agentKey = await resolveActiveAgentKeyForUser(ctx, args.consumerUserId);
261
+ return await ctx.db.insert("messagePushJobs", {
262
+ companyId: args.companyId,
263
+ consumerUserId: args.consumerUserId,
264
+ agentKey: agentKey ?? undefined,
265
+ sourceTemplateId: undefined,
266
+ title: args.title,
267
+ text: args.text,
268
+ periodicity: args.periodicity,
269
+ timezone: args.timezone,
270
+ schedule: args.schedule,
271
+ enabled,
272
+ nextRunAt,
273
+ createdAt: nowMs,
274
+ updatedAt: nowMs,
275
+ });
276
+ },
277
+ });
278
+ export const updatePushJob = mutation({
279
+ args: {
280
+ jobId: v.id("messagePushJobs"),
281
+ title: v.optional(v.string()),
282
+ text: v.optional(v.string()),
283
+ periodicity: v.optional(periodicityValidator),
284
+ timezone: v.optional(v.string()),
285
+ schedule: v.optional(scheduleValidator),
286
+ enabled: v.optional(v.boolean()),
287
+ nowMs: v.optional(v.number()),
288
+ },
289
+ returns: v.boolean(),
290
+ handler: async (ctx, args) => {
291
+ const job = await ctx.db.get(args.jobId);
292
+ if (!job)
293
+ return false;
294
+ const nowMs = args.nowMs ?? Date.now();
295
+ const periodicity = args.periodicity ?? job.periodicity;
296
+ const timezone = args.timezone ?? job.timezone;
297
+ const schedule = args.schedule ?? job.schedule;
298
+ const enabled = args.enabled ?? job.enabled;
299
+ assertValidTimezone(timezone);
300
+ validateSchedule(periodicity, schedule);
301
+ const recurringSchedule = toRecurringSchedule(periodicity, schedule);
302
+ const nextRunAt = enabled && recurringSchedule
303
+ ? computeNextRunAt({
304
+ periodicity: recurringSchedule.periodicity,
305
+ schedule: recurringSchedule.schedule,
306
+ timezone,
307
+ fromMs: nowMs,
308
+ })
309
+ : undefined;
310
+ await ctx.db.patch(job._id, {
311
+ title: args.title ?? job.title,
312
+ text: args.text ?? job.text,
313
+ periodicity,
314
+ timezone,
315
+ schedule,
316
+ enabled,
317
+ nextRunAt,
318
+ updatedAt: nowMs,
319
+ });
320
+ return true;
321
+ },
322
+ });
323
+ export const deletePushJob = mutation({
324
+ args: {
325
+ jobId: v.id("messagePushJobs"),
326
+ },
327
+ returns: v.boolean(),
328
+ handler: async (ctx, args) => {
329
+ const job = await ctx.db.get(args.jobId);
330
+ if (!job)
331
+ return false;
332
+ await ctx.db.delete(job._id);
333
+ return true;
334
+ },
335
+ });
336
+ export const setPushJobEnabled = mutation({
337
+ args: {
338
+ jobId: v.id("messagePushJobs"),
339
+ enabled: v.boolean(),
340
+ nowMs: v.optional(v.number()),
341
+ },
342
+ returns: v.boolean(),
343
+ handler: async (ctx, args) => {
344
+ const job = await ctx.db.get(args.jobId);
345
+ if (!job)
346
+ return false;
347
+ const nowMs = args.nowMs ?? Date.now();
348
+ const recurringSchedule = toRecurringSchedule(job.periodicity, job.schedule);
349
+ const nextRunAt = args.enabled && recurringSchedule
350
+ ? computeNextRunAt({
351
+ periodicity: recurringSchedule.periodicity,
352
+ schedule: recurringSchedule.schedule,
353
+ timezone: job.timezone,
354
+ fromMs: nowMs,
355
+ })
356
+ : undefined;
357
+ await ctx.db.patch(job._id, {
358
+ enabled: args.enabled,
359
+ nextRunAt,
360
+ updatedAt: nowMs,
361
+ });
362
+ return true;
363
+ },
364
+ });
365
+ export const listPushJobsForUser = query({
366
+ args: {
367
+ consumerUserId: v.string(),
368
+ includeDisabled: v.optional(v.boolean()),
369
+ },
370
+ returns: v.array(jobViewValidator),
371
+ handler: async (ctx, args) => {
372
+ const includeDisabled = args.includeDisabled ?? true;
373
+ const rows = includeDisabled
374
+ ? await ctx.db
375
+ .query("messagePushJobs")
376
+ .withIndex("by_consumerUserId", (q) => q.eq("consumerUserId", args.consumerUserId))
377
+ .collect()
378
+ : await ctx.db
379
+ .query("messagePushJobs")
380
+ .withIndex("by_consumerUserId_and_enabled", (q) => q.eq("consumerUserId", args.consumerUserId).eq("enabled", true))
381
+ .collect();
382
+ rows.sort((a, b) => b.updatedAt - a.updatedAt);
383
+ return rows.map((row) => ({
384
+ _id: row._id,
385
+ companyId: row.companyId,
386
+ consumerUserId: row.consumerUserId,
387
+ agentKey: row.agentKey ?? null,
388
+ sourceTemplateId: row.sourceTemplateId ?? null,
389
+ title: row.title,
390
+ text: row.text,
391
+ periodicity: row.periodicity,
392
+ timezone: row.timezone,
393
+ schedule: row.schedule,
394
+ enabled: row.enabled,
395
+ nextRunAt: row.nextRunAt ?? null,
396
+ lastRunAt: row.lastRunAt ?? null,
397
+ lastRunKey: row.lastRunKey ?? null,
398
+ createdAt: row.createdAt,
399
+ updatedAt: row.updatedAt,
400
+ }));
401
+ },
402
+ });
403
+ export const triggerPushJobNow = mutation({
404
+ args: {
405
+ jobId: v.id("messagePushJobs"),
406
+ nowMs: v.optional(v.number()),
407
+ providerConfig: v.optional(providerConfigValidator),
408
+ },
409
+ returns: v.object({
410
+ enqueuedMessageId: v.id("messageQueue"),
411
+ runKey: v.string(),
412
+ }),
413
+ handler: async (ctx, args) => {
414
+ const job = await ctx.db.get(args.jobId);
415
+ if (!job) {
416
+ throw new Error("Push job not found");
417
+ }
418
+ const nowMs = args.nowMs ?? Date.now();
419
+ const agentKey = (await resolveActiveAgentKeyForUser(ctx, job.consumerUserId)) ?? job.agentKey;
420
+ if (!agentKey) {
421
+ throw new Error("No active agent binding for user");
422
+ }
423
+ const conversationTarget = await resolveConversationTargetForUser(ctx, job.consumerUserId);
424
+ const runKey = `manual:${job._id}:${nowMs}`;
425
+ const targetProviderUserId = conversationTarget.source === "telegram_chat"
426
+ ? (conversationTarget.telegramUserId ?? conversationTarget.telegramChatId ?? job.consumerUserId)
427
+ : job.consumerUserId;
428
+ const messageId = await enqueuePushMessage(ctx, {
429
+ conversationId: conversationTarget.conversationId,
430
+ agentKey,
431
+ consumerUserId: job.consumerUserId,
432
+ text: job.text,
433
+ metadata: {
434
+ pushJobId: String(job._id),
435
+ runKey,
436
+ pushMode: "manual",
437
+ conversationTargetSource: conversationTarget.source,
438
+ ...(conversationTarget.telegramChatId
439
+ ? { telegramChatId: conversationTarget.telegramChatId }
440
+ : {}),
441
+ ...(conversationTarget.telegramUserId
442
+ ? { telegramUserId: conversationTarget.telegramUserId }
443
+ : {}),
444
+ },
445
+ scheduledFor: nowMs,
446
+ provider: conversationTarget.source === "telegram_chat" ? "telegram" : "system_push",
447
+ providerUserId: targetProviderUserId,
448
+ providerConfig: args.providerConfig,
449
+ });
450
+ await ctx.db.insert("messagePushDispatches", {
451
+ jobId: job._id,
452
+ consumerUserId: job.consumerUserId,
453
+ runKey,
454
+ scheduledFor: nowMs,
455
+ enqueuedMessageId: messageId,
456
+ status: "enqueued",
457
+ createdAt: nowMs,
458
+ });
459
+ const recurringSchedule = toRecurringSchedule(job.periodicity, job.schedule);
460
+ const nextRunAt = job.enabled && recurringSchedule
461
+ ? computeNextRunAt({
462
+ periodicity: recurringSchedule.periodicity,
463
+ schedule: recurringSchedule.schedule,
464
+ timezone: job.timezone,
465
+ fromMs: nowMs,
466
+ })
467
+ : undefined;
468
+ await ctx.db.patch(job._id, {
469
+ agentKey,
470
+ lastRunAt: nowMs,
471
+ lastRunKey: runKey,
472
+ nextRunAt,
473
+ updatedAt: nowMs,
474
+ });
475
+ return {
476
+ enqueuedMessageId: messageId,
477
+ runKey,
478
+ };
479
+ },
480
+ });
481
+ export const dispatchDuePushJobs = mutation({
482
+ args: {
483
+ nowMs: v.optional(v.number()),
484
+ limit: v.optional(v.number()),
485
+ providerConfig: v.optional(providerConfigValidator),
486
+ },
487
+ returns: v.object({
488
+ scanned: v.number(),
489
+ enqueued: v.number(),
490
+ skipped: v.number(),
491
+ failed: v.number(),
492
+ }),
493
+ handler: async (ctx, args) => {
494
+ const nowMs = args.nowMs ?? Date.now();
495
+ const limit = Math.max(1, Math.min(args.limit ?? 200, 1000));
496
+ const dueJobs = await ctx.db
497
+ .query("messagePushJobs")
498
+ .withIndex("by_enabled_and_nextRunAt", (q) => q.eq("enabled", true).lte("nextRunAt", nowMs))
499
+ .take(limit);
500
+ let enqueued = 0;
501
+ let skipped = 0;
502
+ let failed = 0;
503
+ for (const job of dueJobs) {
504
+ if (job.nextRunAt === undefined) {
505
+ continue;
506
+ }
507
+ const runKey = buildRunKey(job._id, job.nextRunAt);
508
+ const existingDispatch = await ctx.db
509
+ .query("messagePushDispatches")
510
+ .withIndex("by_jobId_and_runKey", (q) => q.eq("jobId", job._id).eq("runKey", runKey))
511
+ .unique();
512
+ if (existingDispatch) {
513
+ skipped += 1;
514
+ await advanceJobNextRun(ctx, job, nowMs, runKey);
515
+ continue;
516
+ }
517
+ const agentKey = (await resolveActiveAgentKeyForUser(ctx, job.consumerUserId)) ?? job.agentKey;
518
+ if (!agentKey) {
519
+ await ctx.db.insert("messagePushDispatches", {
520
+ jobId: job._id,
521
+ consumerUserId: job.consumerUserId,
522
+ runKey,
523
+ scheduledFor: job.nextRunAt,
524
+ status: "failed",
525
+ error: "No active agent binding for user",
526
+ createdAt: nowMs,
527
+ });
528
+ await advanceJobNextRun(ctx, job, nowMs, runKey);
529
+ failed += 1;
530
+ continue;
531
+ }
532
+ const conversationTarget = await resolveConversationTargetForUser(ctx, job.consumerUserId);
533
+ try {
534
+ const targetProviderUserId = conversationTarget.source === "telegram_chat"
535
+ ? (conversationTarget.telegramUserId ?? conversationTarget.telegramChatId ?? job.consumerUserId)
536
+ : job.consumerUserId;
537
+ const messageId = await enqueuePushMessage(ctx, {
538
+ conversationId: conversationTarget.conversationId,
539
+ agentKey,
540
+ consumerUserId: job.consumerUserId,
541
+ text: job.text,
542
+ metadata: {
543
+ pushJobId: String(job._id),
544
+ runKey,
545
+ pushMode: "scheduled",
546
+ conversationTargetSource: conversationTarget.source,
547
+ ...(conversationTarget.telegramChatId
548
+ ? { telegramChatId: conversationTarget.telegramChatId }
549
+ : {}),
550
+ ...(conversationTarget.telegramUserId
551
+ ? { telegramUserId: conversationTarget.telegramUserId }
552
+ : {}),
553
+ },
554
+ scheduledFor: nowMs,
555
+ provider: conversationTarget.source === "telegram_chat" ? "telegram" : "system_push",
556
+ providerUserId: targetProviderUserId,
557
+ providerConfig: args.providerConfig,
558
+ });
559
+ await ctx.db.insert("messagePushDispatches", {
560
+ jobId: job._id,
561
+ consumerUserId: job.consumerUserId,
562
+ runKey,
563
+ scheduledFor: job.nextRunAt,
564
+ enqueuedMessageId: messageId,
565
+ status: "enqueued",
566
+ createdAt: nowMs,
567
+ });
568
+ await advanceJobNextRun(ctx, { ...job, agentKey }, nowMs, runKey);
569
+ enqueued += 1;
570
+ }
571
+ catch (error) {
572
+ const errorMessage = (error && typeof error === "object" && "message" in error)
573
+ ? error.message
574
+ : "Unknown enqueue error";
575
+ await ctx.db.insert("messagePushDispatches", {
576
+ jobId: job._id,
577
+ consumerUserId: job.consumerUserId,
578
+ runKey,
579
+ scheduledFor: job.nextRunAt,
580
+ status: "failed",
581
+ error: errorMessage,
582
+ createdAt: nowMs,
583
+ });
584
+ await advanceJobNextRun(ctx, job, nowMs, runKey);
585
+ failed += 1;
586
+ }
587
+ }
588
+ return {
589
+ scanned: dueJobs.length,
590
+ enqueued,
591
+ skipped,
592
+ failed,
593
+ };
594
+ },
595
+ });
596
+ export const sendBroadcastToAllActiveAgents = mutation({
597
+ args: {
598
+ companyId: v.string(),
599
+ title: v.string(),
600
+ text: v.string(),
601
+ requestedBy: v.string(),
602
+ nowMs: v.optional(v.number()),
603
+ providerConfig: v.optional(providerConfigValidator),
604
+ },
605
+ returns: v.object({
606
+ broadcastId: v.id("messagePushBroadcasts"),
607
+ totalTargets: v.number(),
608
+ enqueued: v.number(),
609
+ failed: v.number(),
610
+ }),
611
+ handler: async (ctx, args) => {
612
+ const nowMs = args.nowMs ?? Date.now();
613
+ const targets = await getBroadcastTargets(ctx, args.companyId);
614
+ const broadcastId = await ctx.db.insert("messagePushBroadcasts", {
615
+ companyId: args.companyId,
616
+ title: args.title,
617
+ text: args.text,
618
+ target: "all_active_agents",
619
+ requestedBy: args.requestedBy,
620
+ requestedAt: nowMs,
621
+ status: "running",
622
+ totalTargets: targets.length,
623
+ enqueuedCount: 0,
624
+ failedCount: 0,
625
+ });
626
+ let enqueued = 0;
627
+ let failed = 0;
628
+ for (const target of targets) {
629
+ const targetConsumerUserId = `agent:${target.agentKey}`;
630
+ const existing = await ctx.db
631
+ .query("messagePushBroadcastDispatches")
632
+ .withIndex("by_broadcastId_and_consumerUserId", (q) => q.eq("broadcastId", broadcastId).eq("consumerUserId", targetConsumerUserId))
633
+ .first();
634
+ if (existing) {
635
+ continue;
636
+ }
637
+ const runKey = `broadcast:${broadcastId}:${target.agentKey}`;
638
+ try {
639
+ const messageId = await enqueuePushMessage(ctx, {
640
+ conversationId: `broadcast:agent:${target.agentKey}`,
641
+ agentKey: target.agentKey,
642
+ consumerUserId: targetConsumerUserId,
643
+ text: `${args.title}\n\n${args.text}`.trim(),
644
+ metadata: {
645
+ broadcastId: String(broadcastId),
646
+ runKey,
647
+ adminInitiated: "true",
648
+ companyId: args.companyId,
649
+ },
650
+ scheduledFor: nowMs,
651
+ provider: "system_push",
652
+ providerUserId: targetConsumerUserId,
653
+ providerConfig: args.providerConfig,
654
+ });
655
+ await ctx.db.insert("messagePushBroadcastDispatches", {
656
+ broadcastId,
657
+ consumerUserId: targetConsumerUserId,
658
+ agentKey: target.agentKey,
659
+ runKey,
660
+ enqueuedMessageId: messageId,
661
+ status: "enqueued",
662
+ createdAt: nowMs,
663
+ });
664
+ enqueued += 1;
665
+ }
666
+ catch (error) {
667
+ const errorMessage = (error && typeof error === "object" && "message" in error)
668
+ ? error.message
669
+ : "Unknown enqueue error";
670
+ await ctx.db.insert("messagePushBroadcastDispatches", {
671
+ broadcastId,
672
+ consumerUserId: targetConsumerUserId,
673
+ agentKey: target.agentKey,
674
+ runKey,
675
+ status: "failed",
676
+ error: errorMessage,
677
+ createdAt: nowMs,
678
+ });
679
+ failed += 1;
680
+ }
681
+ }
682
+ await ctx.db.patch(broadcastId, {
683
+ status: failed > 0 ? "failed" : "done",
684
+ enqueuedCount: enqueued,
685
+ failedCount: failed,
686
+ completedAt: nowMs,
687
+ });
688
+ return {
689
+ broadcastId,
690
+ totalTargets: targets.length,
691
+ enqueued,
692
+ failed,
693
+ };
694
+ },
695
+ });
696
+ export const listPushDispatchesByJob = query({
697
+ args: {
698
+ jobId: v.id("messagePushJobs"),
699
+ limit: v.optional(v.number()),
700
+ },
701
+ returns: v.array(v.object({
702
+ _id: v.id("messagePushDispatches"),
703
+ runKey: v.string(),
704
+ status: dispatchStatusValidator,
705
+ scheduledFor: v.number(),
706
+ createdAt: v.number(),
707
+ error: v.union(v.null(), v.string()),
708
+ })),
709
+ handler: async (ctx, args) => {
710
+ const limit = Math.max(1, Math.min(args.limit ?? 50, 200));
711
+ const rows = await ctx.db
712
+ .query("messagePushDispatches")
713
+ .withIndex("by_jobId_and_runKey", (q) => q.eq("jobId", args.jobId))
714
+ .take(limit);
715
+ rows.sort((a, b) => b.createdAt - a.createdAt);
716
+ return rows.map((row) => ({
717
+ _id: row._id,
718
+ runKey: row.runKey,
719
+ status: row.status,
720
+ scheduledFor: row.scheduledFor,
721
+ createdAt: row.createdAt,
722
+ error: row.error ?? null,
723
+ }));
724
+ },
725
+ });
726
+ async function enqueuePushMessage(ctx, input) {
727
+ return await ctx.runMutation(internal.queue.enqueueMessage, {
728
+ conversationId: input.conversationId,
729
+ agentKey: input.agentKey,
730
+ payload: {
731
+ provider: input.provider,
732
+ providerUserId: input.providerUserId,
733
+ messageText: input.text,
734
+ metadata: input.metadata,
735
+ },
736
+ scheduledFor: input.scheduledFor,
737
+ providerConfig: input.providerConfig,
738
+ });
739
+ }
740
+ async function resolveActiveAgentKeyForUser(ctx, consumerUserId) {
741
+ const binding = await ctx.db
742
+ .query("identityBindings")
743
+ .withIndex("by_consumerUserId_and_status", (q) => q.eq("consumerUserId", consumerUserId).eq("status", "active"))
744
+ .first();
745
+ if (!binding) {
746
+ return null;
747
+ }
748
+ const profile = await ctx.db
749
+ .query("agentProfiles")
750
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", binding.agentKey))
751
+ .unique();
752
+ if (!profile || !profile.enabled) {
753
+ return null;
754
+ }
755
+ return binding.agentKey;
756
+ }
757
+ async function resolveConversationTargetForUser(ctx, consumerUserId) {
758
+ const binding = await ctx.db
759
+ .query("identityBindings")
760
+ .withIndex("by_consumerUserId_and_status", (q) => q.eq("consumerUserId", consumerUserId).eq("status", "active"))
761
+ .first();
762
+ const telegramChatId = binding?.telegramChatId?.trim();
763
+ if (telegramChatId && telegramChatId.length > 0) {
764
+ return {
765
+ conversationId: `telegram:${telegramChatId}`,
766
+ source: "telegram_chat",
767
+ telegramChatId,
768
+ telegramUserId: binding?.telegramUserId?.trim() || undefined,
769
+ };
770
+ }
771
+ return {
772
+ conversationId: `user:${consumerUserId}`,
773
+ source: "legacy_user",
774
+ };
775
+ }
776
+ async function advanceJobNextRun(ctx, job, nowMs, runKey) {
777
+ const recurringSchedule = toRecurringSchedule(job.periodicity, job.schedule);
778
+ const nextRunAt = job.enabled && recurringSchedule
779
+ ? computeNextRunAt({
780
+ periodicity: recurringSchedule.periodicity,
781
+ schedule: recurringSchedule.schedule,
782
+ timezone: job.timezone,
783
+ fromMs: nowMs,
784
+ })
785
+ : undefined;
786
+ await ctx.db.patch(job._id, {
787
+ agentKey: job.agentKey,
788
+ lastRunAt: nowMs,
789
+ lastRunKey: runKey,
790
+ nextRunAt,
791
+ updatedAt: nowMs,
792
+ });
793
+ }
794
+ async function getBroadcastTargets(ctx, _companyId) {
795
+ const enabledProfiles = await ctx.db
796
+ .query("agentProfiles")
797
+ .withIndex("by_enabled", (q) => q.eq("enabled", true))
798
+ .collect();
799
+ return enabledProfiles.map((profile) => ({
800
+ agentKey: profile.agentKey,
801
+ }));
802
+ }
803
+ function resolveScheduleForTemplate(periodicity, suggestedTimes, providedSchedule) {
804
+ if (periodicity === "manual") {
805
+ return { kind: "manual" };
806
+ }
807
+ if (providedSchedule) {
808
+ return providedSchedule;
809
+ }
810
+ const fallback = suggestedTimes.find((time) => time.kind === periodicity);
811
+ if (!fallback) {
812
+ throw new Error("Schedule is required for non-manual template");
813
+ }
814
+ return fallback;
815
+ }
816
+ function validateTemplateTimes(periodicity, suggestedTimes) {
817
+ if (periodicity === "manual") {
818
+ if (suggestedTimes.length > 0) {
819
+ throw new Error("Manual template does not accept suggested times");
820
+ }
821
+ return;
822
+ }
823
+ for (const time of suggestedTimes) {
824
+ if (time.kind !== periodicity) {
825
+ throw new Error("All suggested times must match template periodicity");
826
+ }
827
+ assertScheduleSlotValidity(time);
828
+ }
829
+ }
830
+ function validateSchedule(periodicity, schedule) {
831
+ if (schedule.kind !== periodicity) {
832
+ throw new Error("Schedule kind must match periodicity");
833
+ }
834
+ assertScheduleSlotValidity(schedule);
835
+ }
836
+ function assertScheduleSlotValidity(schedule) {
837
+ if (schedule.kind === "manual") {
838
+ return;
839
+ }
840
+ parseTimeString(schedule.time);
841
+ if (schedule.kind === "weekly") {
842
+ if (!Number.isInteger(schedule.weekday) || schedule.weekday < 1 || schedule.weekday > 7) {
843
+ throw new Error("Weekly schedule weekday must be in range 1..7");
844
+ }
845
+ }
846
+ if (schedule.kind === "monthly") {
847
+ if (schedule.dayOfMonth !== "last" &&
848
+ (!Number.isInteger(schedule.dayOfMonth) || schedule.dayOfMonth < 1 || schedule.dayOfMonth > 31)) {
849
+ throw new Error("Monthly schedule dayOfMonth must be 1..31 or 'last'");
850
+ }
851
+ }
852
+ }
853
+ function assertValidTimezone(timezone) {
854
+ if (!timezone || !timezone.trim()) {
855
+ throw new Error("Timezone is required");
856
+ }
857
+ try {
858
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format(new Date());
859
+ }
860
+ catch {
861
+ throw new Error(`Invalid timezone '${timezone}'`);
862
+ }
863
+ }
864
+ function buildRunKey(jobId, scheduledFor) {
865
+ return `scheduled:${jobId}:${scheduledFor}`;
866
+ }
867
+ function computeNextRunAt(input) {
868
+ const fromMs = input.fromMs + 1_000;
869
+ if (input.schedule.kind === "daily") {
870
+ const { hour, minute } = parseTimeString(input.schedule.time);
871
+ return findNextDailyUtcMs(fromMs, input.timezone, hour, minute);
872
+ }
873
+ if (input.schedule.kind === "weekly") {
874
+ const { hour, minute } = parseTimeString(input.schedule.time);
875
+ return findNextWeeklyUtcMs(fromMs, input.timezone, input.schedule.weekday, hour, minute);
876
+ }
877
+ const { hour, minute } = parseTimeString(input.schedule.time);
878
+ return findNextMonthlyUtcMs(fromMs, input.timezone, input.schedule.dayOfMonth, hour, minute);
879
+ }
880
+ function toRecurringSchedule(periodicity, schedule) {
881
+ if (periodicity === "manual")
882
+ return null;
883
+ if (schedule.kind === "manual") {
884
+ throw new Error("Recurring periodicity requires a non-manual schedule");
885
+ }
886
+ return {
887
+ periodicity,
888
+ schedule,
889
+ };
890
+ }
891
+ function parseTimeString(time) {
892
+ const trimmed = time.trim();
893
+ const match = trimmed.match(/^([01]\d|2[0-3]):([0-5]\d)$/);
894
+ if (!match) {
895
+ throw new Error(`Invalid time '${time}', expected HH:mm`);
896
+ }
897
+ return {
898
+ hour: Number.parseInt(match[1], 10),
899
+ minute: Number.parseInt(match[2], 10),
900
+ };
901
+ }
902
+ function getZonedDateParts(timestampMs, timezone) {
903
+ const formatter = new Intl.DateTimeFormat("en-US", {
904
+ timeZone: timezone,
905
+ year: "numeric",
906
+ month: "2-digit",
907
+ day: "2-digit",
908
+ weekday: "short",
909
+ hour: "2-digit",
910
+ minute: "2-digit",
911
+ second: "2-digit",
912
+ hour12: false,
913
+ });
914
+ const parts = formatter.formatToParts(new Date(timestampMs));
915
+ const map = {};
916
+ for (const part of parts) {
917
+ if (part.type !== "literal") {
918
+ map[part.type] = part.value;
919
+ }
920
+ }
921
+ return {
922
+ year: Number.parseInt(map.year, 10),
923
+ month: Number.parseInt(map.month, 10),
924
+ day: Number.parseInt(map.day, 10),
925
+ hour: Number.parseInt(map.hour, 10),
926
+ minute: Number.parseInt(map.minute, 10),
927
+ second: Number.parseInt(map.second, 10),
928
+ weekday: parseWeekday(map.weekday),
929
+ };
930
+ }
931
+ function parseWeekday(weekday) {
932
+ switch (weekday.toLowerCase()) {
933
+ case "mon":
934
+ return 1;
935
+ case "tue":
936
+ return 2;
937
+ case "wed":
938
+ return 3;
939
+ case "thu":
940
+ return 4;
941
+ case "fri":
942
+ return 5;
943
+ case "sat":
944
+ return 6;
945
+ case "sun":
946
+ return 7;
947
+ default:
948
+ throw new Error(`Unsupported weekday '${weekday}'`);
949
+ }
950
+ }
951
+ function zonedLocalToUtcMs(timezone, year, month, day, hour, minute) {
952
+ let guess = Date.UTC(year, month - 1, day, hour, minute, 0, 0);
953
+ for (let i = 0; i < 3; i += 1) {
954
+ const offsetMs = getTimeZoneOffsetMs(guess, timezone);
955
+ const nextGuess = Date.UTC(year, month - 1, day, hour, minute, 0, 0) - offsetMs;
956
+ if (nextGuess === guess)
957
+ break;
958
+ guess = nextGuess;
959
+ }
960
+ return guess;
961
+ }
962
+ function getTimeZoneOffsetMs(timestampMs, timezone) {
963
+ const parts = getZonedDateParts(timestampMs, timezone);
964
+ const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second, 0);
965
+ return asUtc - timestampMs;
966
+ }
967
+ function daysInMonth(year, month) {
968
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
969
+ }
970
+ function findNextDailyUtcMs(fromMs, timezone, hour, minute) {
971
+ const base = getZonedDateParts(fromMs, timezone);
972
+ for (let addDays = 0; addDays < 370; addDays += 1) {
973
+ const dayUtc = Date.UTC(base.year, base.month - 1, base.day + addDays, 0, 0, 0, 0);
974
+ const local = getZonedDateParts(dayUtc, timezone);
975
+ const candidate = zonedLocalToUtcMs(timezone, local.year, local.month, local.day, hour, minute);
976
+ if (candidate > fromMs)
977
+ return candidate;
978
+ }
979
+ throw new Error("Unable to compute next daily run");
980
+ }
981
+ function findNextWeeklyUtcMs(fromMs, timezone, weekday, hour, minute) {
982
+ const base = getZonedDateParts(fromMs, timezone);
983
+ for (let addDays = 0; addDays < 380; addDays += 1) {
984
+ const dayUtc = Date.UTC(base.year, base.month - 1, base.day + addDays, 0, 0, 0, 0);
985
+ const local = getZonedDateParts(dayUtc, timezone);
986
+ if (local.weekday !== weekday)
987
+ continue;
988
+ const candidate = zonedLocalToUtcMs(timezone, local.year, local.month, local.day, hour, minute);
989
+ if (candidate > fromMs)
990
+ return candidate;
991
+ }
992
+ throw new Error("Unable to compute next weekly run");
993
+ }
994
+ function findNextMonthlyUtcMs(fromMs, timezone, dayOfMonth, hour, minute) {
995
+ const base = getZonedDateParts(fromMs, timezone);
996
+ for (let addMonths = 0; addMonths < 36; addMonths += 1) {
997
+ const monthBaseUtc = Date.UTC(base.year, base.month - 1 + addMonths, 1, 0, 0, 0, 0);
998
+ const local = getZonedDateParts(monthBaseUtc, timezone);
999
+ const monthMaxDay = daysInMonth(local.year, local.month);
1000
+ const day = dayOfMonth === "last"
1001
+ ? monthMaxDay
1002
+ : Math.min(dayOfMonth, monthMaxDay);
1003
+ const candidate = zonedLocalToUtcMs(timezone, local.year, local.month, day, hour, minute);
1004
+ if (candidate > fromMs)
1005
+ return candidate;
1006
+ }
1007
+ throw new Error("Unable to compute next monthly run");
1008
+ }
1009
+ //# sourceMappingURL=pushing.js.map