@okrlinkhub/agent-factory 0.2.11 → 0.2.12

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 (69) 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 +516 -0
  10. package/dist/client/index.d.ts.map +1 -0
  11. package/dist/client/index.js +795 -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 +1396 -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 +152 -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 +361 -0
  54. package/dist/component/queue.d.ts.map +1 -0
  55. package/dist/component/queue.js +1407 -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 +648 -0
  62. package/dist/component/schema.d.ts.map +1 -0
  63. package/dist/component/schema.js +295 -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
@@ -0,0 +1,1407 @@
1
+ import { v } from "convex/values";
2
+ import { internal } from "./_generated/api.js";
3
+ import { internalMutation, internalQuery, mutation, query, } from "./_generated/server.js";
4
+ import { computeRetryDelayMs, DEFAULT_CONFIG, providerConfigValidator } from "./config.js";
5
+ const queueStatusValidator = v.union(v.literal("queued"), v.literal("processing"), v.literal("done"), v.literal("failed"), v.literal("dead_letter"));
6
+ const queuePayloadValidator = v.object({
7
+ provider: v.string(),
8
+ providerUserId: v.string(),
9
+ messageText: v.string(),
10
+ externalMessageId: v.optional(v.string()),
11
+ rawUpdateJson: v.optional(v.string()),
12
+ metadata: v.optional(v.record(v.string(), v.string())),
13
+ });
14
+ const snapshotReasonValidator = v.union(v.literal("drain"), v.literal("signal"), v.literal("manual"));
15
+ const DATA_SNAPSHOT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
16
+ const claimedJobValidator = v.object({
17
+ messageId: v.id("messageQueue"),
18
+ conversationId: v.string(),
19
+ agentKey: v.string(),
20
+ leaseId: v.string(),
21
+ leaseExpiresAt: v.number(),
22
+ payload: queuePayloadValidator,
23
+ });
24
+ const secretStatusValidator = v.object({
25
+ secretRef: v.string(),
26
+ hasActive: v.boolean(),
27
+ version: v.union(v.null(), v.number()),
28
+ });
29
+ const bridgeProfileConfigValidator = v.object({
30
+ enabled: v.boolean(),
31
+ baseUrl: v.optional(v.string()),
32
+ serviceId: v.optional(v.string()),
33
+ appKey: v.optional(v.string()),
34
+ serviceKeySecretRef: v.optional(v.string()),
35
+ appBaseUrlMapJsonSecretRef: v.optional(v.string()),
36
+ });
37
+ const bridgeRuntimeConfigValidator = v.object({
38
+ baseUrl: v.union(v.null(), v.string()),
39
+ appBaseUrlMapJson: v.union(v.null(), v.string()),
40
+ serviceId: v.union(v.null(), v.string()),
41
+ appKey: v.union(v.null(), v.string()),
42
+ serviceKey: v.union(v.null(), v.string()),
43
+ serviceKeySecretRef: v.union(v.null(), v.string()),
44
+ });
45
+ const BRIDGE_SECRET_REFS = {
46
+ serviceKey: "agent-bridge.serviceKey",
47
+ baseUrl: "agent-bridge.baseUrl",
48
+ baseUrlMapJson: "agent-bridge.baseUrlMapJson",
49
+ serviceId: "agent-bridge.serviceId",
50
+ appKey: "agent-bridge.appKey",
51
+ };
52
+ export const enqueueMessage = mutation({
53
+ args: {
54
+ conversationId: v.string(),
55
+ agentKey: v.string(),
56
+ payload: queuePayloadValidator,
57
+ priority: v.optional(v.number()),
58
+ scheduledFor: v.optional(v.number()),
59
+ maxAttempts: v.optional(v.number()),
60
+ nowMs: v.optional(v.number()),
61
+ providerConfig: v.optional(providerConfigValidator),
62
+ },
63
+ returns: v.id("messageQueue"),
64
+ handler: async (ctx, args) => {
65
+ const nowMs = args.nowMs ?? Date.now();
66
+ const profile = await ctx.db
67
+ .query("agentProfiles")
68
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
69
+ .unique();
70
+ if (!profile || !profile.enabled) {
71
+ throw new Error(`Agent profile '${args.agentKey}' not found or disabled`);
72
+ }
73
+ const resolvedProviderUserId = profile.providerUserId && profile.providerUserId.trim().length > 0
74
+ ? profile.providerUserId.trim()
75
+ : args.payload.providerUserId;
76
+ const providerUserIdStr = typeof resolvedProviderUserId === "string" &&
77
+ resolvedProviderUserId.trim().length > 0
78
+ ? resolvedProviderUserId.trim()
79
+ : null;
80
+ if (providerUserIdStr === null) {
81
+ throw new Error(`providerUserId is required but missing: profile.providerUserId=${JSON.stringify(profile.providerUserId)}, payload.providerUserId=${JSON.stringify(args.payload.providerUserId)}`);
82
+ }
83
+ const payload = {
84
+ ...args.payload,
85
+ providerUserId: providerUserIdStr,
86
+ metadata: {
87
+ ...(args.payload.metadata ?? {}),
88
+ providerUserId: providerUserIdStr,
89
+ },
90
+ };
91
+ const existingConversation = await ctx.db
92
+ .query("conversations")
93
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", args.conversationId))
94
+ .unique();
95
+ if (!existingConversation) {
96
+ await ctx.db.insert("conversations", {
97
+ conversationId: args.conversationId,
98
+ agentKey: args.agentKey,
99
+ contextHistory: [],
100
+ pendingToolCalls: [],
101
+ });
102
+ }
103
+ const priority = Math.min(DEFAULT_CONFIG.queue.maxPriority, Math.max(0, args.priority ?? DEFAULT_CONFIG.queue.defaultPriority));
104
+ const messageId = await ctx.db.insert("messageQueue", {
105
+ conversationId: args.conversationId,
106
+ agentKey: args.agentKey,
107
+ payload,
108
+ status: "queued",
109
+ priority,
110
+ scheduledFor: args.scheduledFor ?? nowMs,
111
+ attempts: 0,
112
+ maxAttempts: args.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
113
+ });
114
+ try {
115
+ await ctx.scheduler.runAfter(0, internal.scheduler.reconcileWorkerPoolFromEnqueue, {
116
+ workspaceId: "default",
117
+ providerConfig: args.providerConfig,
118
+ });
119
+ }
120
+ catch (error) {
121
+ console.warn(`[queue] failed to schedule reconcile after enqueue: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ return messageId;
124
+ },
125
+ });
126
+ export const appendConversationMessages = mutation({
127
+ args: {
128
+ conversationId: v.string(),
129
+ workspaceId: v.optional(v.string()),
130
+ messages: v.array(v.object({
131
+ role: v.union(v.literal("system"), v.literal("user"), v.literal("assistant"), v.literal("tool")),
132
+ content: v.string(),
133
+ at: v.optional(v.number()),
134
+ })),
135
+ nowMs: v.optional(v.number()),
136
+ },
137
+ returns: v.object({
138
+ updated: v.boolean(),
139
+ messageCount: v.number(),
140
+ }),
141
+ handler: async (ctx, args) => {
142
+ const conversation = await ctx.db
143
+ .query("conversations")
144
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", args.conversationId))
145
+ .unique();
146
+ if (!conversation) {
147
+ return { updated: false, messageCount: 0 };
148
+ }
149
+ const nowMs = args.nowMs ?? Date.now();
150
+ const messages = args.messages.map((message, index) => ({
151
+ role: message.role,
152
+ content: message.content,
153
+ at: message.at ?? nowMs + index,
154
+ }));
155
+ const nextContextHistory = [...conversation.contextHistory, ...messages];
156
+ await ctx.db.patch(conversation._id, { contextHistory: nextContextHistory });
157
+ const snapshotKey = `${args.workspaceId ?? "default"}:${conversation.agentKey}`;
158
+ const cache = await ctx.db
159
+ .query("conversationHydrationCache")
160
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", args.conversationId))
161
+ .first();
162
+ const deltaContext = nextContextHistory.slice(-64);
163
+ const deltaFingerprint = fingerprintConversationDelta(deltaContext);
164
+ if (!cache) {
165
+ await ctx.db.insert("conversationHydrationCache", {
166
+ conversationId: args.conversationId,
167
+ agentKey: conversation.agentKey,
168
+ snapshotKey,
169
+ lastHydratedAt: nowMs,
170
+ deltaContext,
171
+ deltaFingerprint,
172
+ });
173
+ }
174
+ else {
175
+ await ctx.db.patch(cache._id, {
176
+ agentKey: conversation.agentKey,
177
+ snapshotKey,
178
+ lastHydratedAt: nowMs,
179
+ deltaContext,
180
+ deltaFingerprint,
181
+ });
182
+ }
183
+ return { updated: true, messageCount: messages.length };
184
+ },
185
+ });
186
+ export const upsertAgentProfile = mutation({
187
+ args: {
188
+ agentKey: v.string(),
189
+ providerUserId: v.optional(v.string()),
190
+ version: v.string(),
191
+ soulMd: v.string(),
192
+ clientMd: v.optional(v.string()),
193
+ skills: v.array(v.string()),
194
+ secretsRef: v.array(v.string()),
195
+ bridgeConfig: v.optional(bridgeProfileConfigValidator),
196
+ enabled: v.boolean(),
197
+ },
198
+ returns: v.id("agentProfiles"),
199
+ handler: async (ctx, args) => {
200
+ const defaultSecretsRef = ["convex.url", "fly.apiToken"];
201
+ const bridgeSecretsRef = getBridgeSecretRefsForProfile(args.agentKey, args.bridgeConfig);
202
+ const secretsRef = Array.from(new Set([...args.secretsRef, ...defaultSecretsRef, ...bridgeSecretsRef]));
203
+ const existing = await ctx.db
204
+ .query("agentProfiles")
205
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
206
+ .unique();
207
+ if (!existing) {
208
+ return await ctx.db.insert("agentProfiles", { ...args, secretsRef });
209
+ }
210
+ await ctx.db.patch(existing._id, { ...args, secretsRef });
211
+ return existing._id;
212
+ },
213
+ });
214
+ export const importPlaintextSecret = mutation({
215
+ args: {
216
+ secretRef: v.string(),
217
+ plaintextValue: v.string(),
218
+ metadata: v.optional(v.record(v.string(), v.string())),
219
+ },
220
+ returns: v.object({
221
+ secretId: v.id("secrets"),
222
+ secretRef: v.string(),
223
+ version: v.number(),
224
+ }),
225
+ handler: async (ctx, args) => {
226
+ const history = await ctx.db
227
+ .query("secrets")
228
+ .withIndex("by_secretRef", (q) => q.eq("secretRef", args.secretRef))
229
+ .collect();
230
+ const nextVersion = history.reduce((maxVersion, row) => Math.max(maxVersion, row.version), 0) + 1;
231
+ const previousActive = history.find((row) => row.active);
232
+ const encoded = encryptSecretValue(args.plaintextValue);
233
+ for (const row of history) {
234
+ if (row.active) {
235
+ await ctx.db.patch(row._id, { active: false });
236
+ }
237
+ }
238
+ const secretId = await ctx.db.insert("secrets", {
239
+ secretRef: args.secretRef,
240
+ version: nextVersion,
241
+ encryptedValue: encoded,
242
+ keyId: "component-local",
243
+ algorithm: "xor-hex-v1",
244
+ active: true,
245
+ rotatedFrom: previousActive?.version,
246
+ metadata: args.metadata,
247
+ });
248
+ return {
249
+ secretId,
250
+ secretRef: args.secretRef,
251
+ version: nextVersion,
252
+ };
253
+ },
254
+ });
255
+ export const getSecretsStatus = query({
256
+ args: {
257
+ secretRefs: v.array(v.string()),
258
+ },
259
+ returns: v.array(secretStatusValidator),
260
+ handler: async (ctx, args) => {
261
+ const statuses = [];
262
+ for (const ref of args.secretRefs) {
263
+ const active = await ctx.db
264
+ .query("secrets")
265
+ .withIndex("by_secretRef_and_active", (q) => q.eq("secretRef", ref).eq("active", true))
266
+ .unique();
267
+ statuses.push({
268
+ secretRef: ref,
269
+ hasActive: active !== null,
270
+ version: active?.version ?? null,
271
+ });
272
+ }
273
+ return statuses;
274
+ },
275
+ });
276
+ export const getActiveSecretPlaintext = internalQuery({
277
+ args: {
278
+ secretRef: v.string(),
279
+ },
280
+ returns: v.union(v.null(), v.string()),
281
+ handler: async (ctx, args) => {
282
+ const active = await ctx.db
283
+ .query("secrets")
284
+ .withIndex("by_secretRef_and_active", (q) => q.eq("secretRef", args.secretRef).eq("active", true))
285
+ .unique();
286
+ if (!active) {
287
+ return null;
288
+ }
289
+ return decryptSecretValue(active.encryptedValue, active.algorithm);
290
+ },
291
+ });
292
+ export const getProviderRuntimeConfig = internalQuery({
293
+ args: {},
294
+ returns: v.union(v.null(), providerConfigValidator),
295
+ handler: async (ctx) => {
296
+ const row = await ctx.db
297
+ .query("runtimeConfig")
298
+ .withIndex("by_key", (q) => q.eq("key", "provider"))
299
+ .unique();
300
+ if (!row) {
301
+ return null;
302
+ }
303
+ return row.providerConfig;
304
+ },
305
+ });
306
+ export const upsertProviderRuntimeConfig = internalMutation({
307
+ args: {
308
+ providerConfig: providerConfigValidator,
309
+ nowMs: v.optional(v.number()),
310
+ },
311
+ returns: v.null(),
312
+ handler: async (ctx, args) => {
313
+ const nowMs = args.nowMs ?? Date.now();
314
+ const existing = await ctx.db
315
+ .query("runtimeConfig")
316
+ .withIndex("by_key", (q) => q.eq("key", "provider"))
317
+ .unique();
318
+ if (!existing) {
319
+ await ctx.db.insert("runtimeConfig", {
320
+ key: "provider",
321
+ providerConfig: args.providerConfig,
322
+ updatedAt: nowMs,
323
+ });
324
+ return null;
325
+ }
326
+ await ctx.db.patch(existing._id, {
327
+ providerConfig: args.providerConfig,
328
+ updatedAt: nowMs,
329
+ });
330
+ return null;
331
+ },
332
+ });
333
+ export const providerRuntimeConfig = query({
334
+ args: {},
335
+ returns: v.union(v.null(), providerConfigValidator),
336
+ handler: async (ctx) => {
337
+ const row = await ctx.db
338
+ .query("runtimeConfig")
339
+ .withIndex("by_key", (q) => q.eq("key", "provider"))
340
+ .unique();
341
+ if (!row) {
342
+ return null;
343
+ }
344
+ return row.providerConfig;
345
+ },
346
+ });
347
+ export const setProviderRuntimeConfig = mutation({
348
+ args: {
349
+ providerConfig: providerConfigValidator,
350
+ nowMs: v.optional(v.number()),
351
+ },
352
+ returns: v.null(),
353
+ handler: async (ctx, args) => {
354
+ const nowMs = args.nowMs ?? Date.now();
355
+ const existing = await ctx.db
356
+ .query("runtimeConfig")
357
+ .withIndex("by_key", (q) => q.eq("key", "provider"))
358
+ .unique();
359
+ if (!existing) {
360
+ await ctx.db.insert("runtimeConfig", {
361
+ key: "provider",
362
+ providerConfig: args.providerConfig,
363
+ updatedAt: nowMs,
364
+ });
365
+ return null;
366
+ }
367
+ await ctx.db.patch(existing._id, {
368
+ providerConfig: args.providerConfig,
369
+ updatedAt: nowMs,
370
+ });
371
+ return null;
372
+ },
373
+ });
374
+ export const generateMediaUploadUrl = mutation({
375
+ args: {},
376
+ returns: v.object({
377
+ uploadUrl: v.string(),
378
+ }),
379
+ handler: async (ctx) => {
380
+ const uploadUrl = await ctx.storage.generateUploadUrl();
381
+ return { uploadUrl };
382
+ },
383
+ });
384
+ export const getStorageFileUrl = query({
385
+ args: {
386
+ storageId: v.id("_storage"),
387
+ },
388
+ returns: v.union(v.null(), v.string()),
389
+ handler: async (ctx, args) => {
390
+ return await ctx.storage.getUrl(args.storageId);
391
+ },
392
+ });
393
+ export const attachMessageMetadata = mutation({
394
+ args: {
395
+ messageId: v.id("messageQueue"),
396
+ metadata: v.record(v.string(), v.string()),
397
+ },
398
+ returns: v.boolean(),
399
+ handler: async (ctx, args) => {
400
+ const message = await ctx.db.get(args.messageId);
401
+ if (!message)
402
+ return false;
403
+ await ctx.db.patch(message._id, {
404
+ payload: {
405
+ ...message.payload,
406
+ metadata: {
407
+ ...(message.payload.metadata ?? {}),
408
+ ...args.metadata,
409
+ },
410
+ },
411
+ });
412
+ return true;
413
+ },
414
+ });
415
+ export const claimNextJob = mutation({
416
+ args: {
417
+ workerId: v.string(),
418
+ nowMs: v.optional(v.number()),
419
+ },
420
+ returns: v.union(v.null(), claimedJobValidator),
421
+ handler: async (ctx, args) => {
422
+ const nowMs = args.nowMs ?? Date.now();
423
+ const candidates = await ctx.db
424
+ .query("messageQueue")
425
+ .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", "queued").lte("scheduledFor", nowMs))
426
+ .take(DEFAULT_CONFIG.queue.claimBatchSize);
427
+ candidates.sort((a, b) => {
428
+ if (a.priority !== b.priority)
429
+ return b.priority - a.priority;
430
+ if (a.scheduledFor !== b.scheduledFor)
431
+ return a.scheduledFor - b.scheduledFor;
432
+ return a._creationTime - b._creationTime;
433
+ });
434
+ for (const candidate of candidates) {
435
+ const conversation = await ctx.db
436
+ .query("conversations")
437
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", candidate.conversationId))
438
+ .unique();
439
+ if (!conversation)
440
+ continue;
441
+ const lock = conversation.processingLock;
442
+ if (lock && lock.leaseExpiresAt > nowMs)
443
+ continue;
444
+ const leaseId = `${nowMs}-${Math.random().toString(36).slice(2, 10)}`;
445
+ const leaseExpiresAt = nowMs + DEFAULT_CONFIG.lease.leaseMs;
446
+ await ctx.db.patch(candidate._id, {
447
+ status: "processing",
448
+ claimedBy: args.workerId,
449
+ leaseId,
450
+ leaseExpiresAt,
451
+ });
452
+ await ctx.db.patch(conversation._id, {
453
+ processingLock: {
454
+ leaseId,
455
+ workerId: args.workerId,
456
+ leaseExpiresAt,
457
+ heartbeatAt: nowMs,
458
+ claimedMessageId: candidate._id,
459
+ },
460
+ });
461
+ const worker = await ctx.db
462
+ .query("workers")
463
+ .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
464
+ .unique();
465
+ if (!worker) {
466
+ await ctx.db.insert("workers", {
467
+ workerId: args.workerId,
468
+ provider: "fly",
469
+ status: "active",
470
+ load: 1,
471
+ heartbeatAt: nowMs,
472
+ lastClaimAt: nowMs,
473
+ scheduledShutdownAt: undefined,
474
+ stoppedAt: undefined,
475
+ capabilities: [],
476
+ });
477
+ }
478
+ else {
479
+ await ctx.db.patch(worker._id, {
480
+ status: "active",
481
+ load: worker.load + 1,
482
+ heartbeatAt: nowMs,
483
+ lastClaimAt: nowMs,
484
+ scheduledShutdownAt: undefined,
485
+ stoppedAt: undefined,
486
+ });
487
+ }
488
+ return {
489
+ messageId: candidate._id,
490
+ conversationId: candidate.conversationId,
491
+ agentKey: candidate.agentKey,
492
+ leaseId,
493
+ leaseExpiresAt,
494
+ payload: candidate.payload,
495
+ };
496
+ }
497
+ return null;
498
+ },
499
+ });
500
+ export const heartbeatJob = mutation({
501
+ args: {
502
+ workerId: v.string(),
503
+ messageId: v.id("messageQueue"),
504
+ leaseId: v.string(),
505
+ nowMs: v.optional(v.number()),
506
+ },
507
+ returns: v.boolean(),
508
+ handler: async (ctx, args) => {
509
+ const nowMs = args.nowMs ?? Date.now();
510
+ const message = await ctx.db.get(args.messageId);
511
+ if (!message ||
512
+ message.status !== "processing" ||
513
+ message.leaseId !== args.leaseId ||
514
+ message.claimedBy !== args.workerId) {
515
+ return false;
516
+ }
517
+ const leaseExpiresAt = nowMs + DEFAULT_CONFIG.lease.leaseMs;
518
+ await ctx.db.patch(message._id, {
519
+ leaseExpiresAt,
520
+ });
521
+ const conversation = await ctx.db
522
+ .query("conversations")
523
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
524
+ .unique();
525
+ if (conversation?.processingLock &&
526
+ conversation.processingLock.leaseId === args.leaseId) {
527
+ await ctx.db.patch(conversation._id, {
528
+ processingLock: {
529
+ ...conversation.processingLock,
530
+ leaseExpiresAt,
531
+ heartbeatAt: nowMs,
532
+ },
533
+ });
534
+ }
535
+ const worker = await ctx.db
536
+ .query("workers")
537
+ .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
538
+ .unique();
539
+ if (worker?.status === "active") {
540
+ await ctx.db.patch(worker._id, { heartbeatAt: nowMs });
541
+ }
542
+ return true;
543
+ },
544
+ });
545
+ export const completeJob = mutation({
546
+ args: {
547
+ workerId: v.string(),
548
+ messageId: v.id("messageQueue"),
549
+ leaseId: v.string(),
550
+ nowMs: v.optional(v.number()),
551
+ providerConfig: v.optional(providerConfigValidator),
552
+ },
553
+ returns: v.boolean(),
554
+ handler: async (ctx, args) => {
555
+ const nowMs = args.nowMs ?? Date.now();
556
+ const message = await ctx.db.get(args.messageId);
557
+ if (!message ||
558
+ message.status !== "processing" ||
559
+ message.leaseId !== args.leaseId ||
560
+ message.claimedBy !== args.workerId) {
561
+ return false;
562
+ }
563
+ await ctx.db.patch(message._id, {
564
+ status: "done",
565
+ claimedBy: undefined,
566
+ leaseId: undefined,
567
+ leaseExpiresAt: undefined,
568
+ lastError: undefined,
569
+ nextRetryAt: undefined,
570
+ });
571
+ const conversation = await ctx.db
572
+ .query("conversations")
573
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
574
+ .unique();
575
+ if (conversation?.processingLock &&
576
+ conversation.processingLock.claimedMessageId === message._id) {
577
+ await ctx.db.patch(conversation._id, { processingLock: undefined });
578
+ }
579
+ const worker = await ctx.db
580
+ .query("workers")
581
+ .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
582
+ .unique();
583
+ if (worker) {
584
+ const nextLoad = Math.max(0, worker.load - 1);
585
+ const shutdownBaseMs = worker.lastClaimAt ?? nowMs;
586
+ const nextScheduledShutdownAt = nextLoad === 0
587
+ ? shutdownBaseMs + DEFAULT_CONFIG.scaling.idleTimeoutMs
588
+ : undefined;
589
+ await ctx.db.patch(worker._id, {
590
+ load: nextLoad,
591
+ heartbeatAt: nowMs,
592
+ scheduledShutdownAt: nextScheduledShutdownAt,
593
+ stoppedAt: undefined,
594
+ });
595
+ if (nextScheduledShutdownAt !== undefined) {
596
+ const delayMs = Math.max(0, nextScheduledShutdownAt - nowMs) + 1_000;
597
+ try {
598
+ await ctx.scheduler.runAfter(delayMs, internal.scheduler.enforceIdleShutdowns, {
599
+ providerConfig: args.providerConfig,
600
+ });
601
+ }
602
+ catch (error) {
603
+ console.warn(`[queue] failed to schedule idle-shutdown watchdog: ${error instanceof Error ? error.message : String(error)}`);
604
+ }
605
+ }
606
+ }
607
+ return true;
608
+ },
609
+ });
610
+ export const failJob = mutation({
611
+ args: {
612
+ workerId: v.string(),
613
+ messageId: v.id("messageQueue"),
614
+ leaseId: v.string(),
615
+ errorMessage: v.string(),
616
+ nowMs: v.optional(v.number()),
617
+ providerConfig: v.optional(providerConfigValidator),
618
+ },
619
+ returns: v.object({
620
+ requeued: v.boolean(),
621
+ deadLettered: v.boolean(),
622
+ nextScheduledFor: v.union(v.null(), v.number()),
623
+ }),
624
+ handler: async (ctx, args) => {
625
+ const nowMs = args.nowMs ?? Date.now();
626
+ const message = await ctx.db.get(args.messageId);
627
+ if (!message ||
628
+ message.status !== "processing" ||
629
+ message.leaseId !== args.leaseId ||
630
+ message.claimedBy !== args.workerId) {
631
+ return { requeued: false, deadLettered: false, nextScheduledFor: null };
632
+ }
633
+ const attempts = message.attempts + 1;
634
+ const reachedMaxAttempts = attempts >= message.maxAttempts;
635
+ let nextScheduledFor = null;
636
+ if (reachedMaxAttempts) {
637
+ await ctx.db.patch(message._id, {
638
+ status: "dead_letter",
639
+ attempts,
640
+ deadLetteredAt: nowMs,
641
+ lastError: args.errorMessage,
642
+ claimedBy: undefined,
643
+ leaseId: undefined,
644
+ leaseExpiresAt: undefined,
645
+ });
646
+ }
647
+ else {
648
+ const retryDelayMs = computeRetryDelayMs(attempts, DEFAULT_CONFIG.retry, nowMs);
649
+ nextScheduledFor = nowMs + retryDelayMs;
650
+ await ctx.db.patch(message._id, {
651
+ status: "queued",
652
+ attempts,
653
+ scheduledFor: nextScheduledFor,
654
+ nextRetryAt: nextScheduledFor,
655
+ lastError: args.errorMessage,
656
+ claimedBy: undefined,
657
+ leaseId: undefined,
658
+ leaseExpiresAt: undefined,
659
+ });
660
+ }
661
+ const conversation = await ctx.db
662
+ .query("conversations")
663
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
664
+ .unique();
665
+ if (conversation?.processingLock &&
666
+ conversation.processingLock.claimedMessageId === message._id) {
667
+ await ctx.db.patch(conversation._id, { processingLock: undefined });
668
+ }
669
+ const worker = await ctx.db
670
+ .query("workers")
671
+ .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
672
+ .unique();
673
+ if (worker) {
674
+ const nextLoad = Math.max(0, worker.load - 1);
675
+ const shutdownBaseMs = worker.lastClaimAt ?? nowMs;
676
+ const nextScheduledShutdownAt = nextLoad === 0
677
+ ? shutdownBaseMs + DEFAULT_CONFIG.scaling.idleTimeoutMs
678
+ : undefined;
679
+ await ctx.db.patch(worker._id, {
680
+ load: nextLoad,
681
+ heartbeatAt: nowMs,
682
+ scheduledShutdownAt: nextScheduledShutdownAt,
683
+ stoppedAt: undefined,
684
+ });
685
+ if (nextScheduledShutdownAt !== undefined) {
686
+ const delayMs = Math.max(0, nextScheduledShutdownAt - nowMs) + 1_000;
687
+ try {
688
+ await ctx.scheduler.runAfter(delayMs, internal.scheduler.enforceIdleShutdowns, {
689
+ providerConfig: args.providerConfig,
690
+ });
691
+ }
692
+ catch (error) {
693
+ console.warn(`[queue] failed to schedule idle-shutdown watchdog: ${error instanceof Error ? error.message : String(error)}`);
694
+ }
695
+ }
696
+ }
697
+ return {
698
+ requeued: !reachedMaxAttempts,
699
+ deadLettered: reachedMaxAttempts,
700
+ nextScheduledFor,
701
+ };
702
+ },
703
+ });
704
+ export const releaseExpiredLeases = internalMutation({
705
+ args: {
706
+ nowMs: v.optional(v.number()),
707
+ limit: v.optional(v.number()),
708
+ },
709
+ returns: v.object({
710
+ requeued: v.number(),
711
+ unlocked: v.number(),
712
+ }),
713
+ handler: async (ctx, args) => {
714
+ const nowMs = args.nowMs ?? Date.now();
715
+ const limit = args.limit ?? 100;
716
+ const stuck = await ctx.db
717
+ .query("messageQueue")
718
+ .withIndex("by_status_and_leaseExpiresAt", (q) => q.eq("status", "processing").lte("leaseExpiresAt", nowMs))
719
+ .take(limit);
720
+ let requeued = 0;
721
+ let unlocked = 0;
722
+ for (const message of stuck) {
723
+ const claimedWorkerId = message.claimedBy;
724
+ await ctx.db.patch(message._id, {
725
+ status: "queued",
726
+ scheduledFor: nowMs,
727
+ claimedBy: undefined,
728
+ leaseId: undefined,
729
+ leaseExpiresAt: undefined,
730
+ lastError: "Lease expired while processing",
731
+ });
732
+ requeued += 1;
733
+ const conversation = await ctx.db
734
+ .query("conversations")
735
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
736
+ .unique();
737
+ if (conversation?.processingLock &&
738
+ conversation.processingLock.claimedMessageId === message._id) {
739
+ await ctx.db.patch(conversation._id, { processingLock: undefined });
740
+ unlocked += 1;
741
+ }
742
+ if (claimedWorkerId) {
743
+ const worker = await ctx.db
744
+ .query("workers")
745
+ .withIndex("by_workerId", (q) => q.eq("workerId", claimedWorkerId))
746
+ .unique();
747
+ if (worker && worker.status === "active") {
748
+ const nextLoad = Math.max(0, worker.load - 1);
749
+ const nextScheduledShutdownAt = nextLoad === 0 ? nowMs + DEFAULT_CONFIG.scaling.idleTimeoutMs : undefined;
750
+ await ctx.db.patch(worker._id, {
751
+ load: nextLoad,
752
+ heartbeatAt: nowMs,
753
+ scheduledShutdownAt: nextScheduledShutdownAt,
754
+ stoppedAt: undefined,
755
+ });
756
+ }
757
+ }
758
+ }
759
+ return { requeued, unlocked };
760
+ },
761
+ });
762
+ export const releaseStuckJobs = mutation({
763
+ args: {
764
+ nowMs: v.optional(v.number()),
765
+ limit: v.optional(v.number()),
766
+ },
767
+ returns: v.object({
768
+ requeued: v.number(),
769
+ unlocked: v.number(),
770
+ }),
771
+ handler: async (ctx, args) => {
772
+ const nowMs = args.nowMs ?? Date.now();
773
+ const limit = args.limit ?? 100;
774
+ const stuck = await ctx.db
775
+ .query("messageQueue")
776
+ .withIndex("by_status_and_leaseExpiresAt", (q) => q.eq("status", "processing").lte("leaseExpiresAt", nowMs))
777
+ .take(limit);
778
+ let requeued = 0;
779
+ let unlocked = 0;
780
+ for (const message of stuck) {
781
+ const claimedWorkerId = message.claimedBy;
782
+ await ctx.db.patch(message._id, {
783
+ status: "queued",
784
+ scheduledFor: nowMs,
785
+ claimedBy: undefined,
786
+ leaseId: undefined,
787
+ leaseExpiresAt: undefined,
788
+ lastError: "Lease expired while processing",
789
+ });
790
+ requeued += 1;
791
+ const conversation = await ctx.db
792
+ .query("conversations")
793
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
794
+ .unique();
795
+ if (conversation?.processingLock &&
796
+ conversation.processingLock.claimedMessageId === message._id) {
797
+ await ctx.db.patch(conversation._id, { processingLock: undefined });
798
+ unlocked += 1;
799
+ }
800
+ if (claimedWorkerId) {
801
+ const worker = await ctx.db
802
+ .query("workers")
803
+ .withIndex("by_workerId", (q) => q.eq("workerId", claimedWorkerId))
804
+ .unique();
805
+ if (worker && worker.status === "active") {
806
+ const nextLoad = Math.max(0, worker.load - 1);
807
+ const nextScheduledShutdownAt = nextLoad === 0 ? nowMs + DEFAULT_CONFIG.scaling.idleTimeoutMs : undefined;
808
+ await ctx.db.patch(worker._id, {
809
+ load: nextLoad,
810
+ heartbeatAt: nowMs,
811
+ scheduledShutdownAt: nextScheduledShutdownAt,
812
+ stoppedAt: undefined,
813
+ });
814
+ }
815
+ }
816
+ }
817
+ return { requeued, unlocked };
818
+ },
819
+ });
820
+ export const getHydrationBundleForClaimedJob = query({
821
+ args: {
822
+ messageId: v.id("messageQueue"),
823
+ workspaceId: v.string(),
824
+ },
825
+ returns: v.union(v.null(), v.object({
826
+ messageId: v.id("messageQueue"),
827
+ conversationId: v.string(),
828
+ agentKey: v.string(),
829
+ payload: queuePayloadValidator,
830
+ conversationState: v.object({
831
+ contextHistory: v.array(v.object({
832
+ role: v.union(v.literal("system"), v.literal("user"), v.literal("assistant"), v.literal("tool")),
833
+ content: v.string(),
834
+ at: v.number(),
835
+ })),
836
+ pendingToolCalls: v.array(v.object({
837
+ toolName: v.string(),
838
+ callId: v.string(),
839
+ status: v.union(v.literal("pending"), v.literal("running"), v.literal("done"), v.literal("failed")),
840
+ })),
841
+ }),
842
+ telegramBotToken: v.union(v.null(), v.string()),
843
+ bridgeRuntimeConfig: v.union(v.null(), bridgeRuntimeConfigValidator),
844
+ })),
845
+ handler: async (ctx, args) => {
846
+ const message = await ctx.db.get(args.messageId);
847
+ if (!message || message.status !== "processing")
848
+ return null;
849
+ const conversation = await ctx.db
850
+ .query("conversations")
851
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
852
+ .unique();
853
+ const profile = await ctx.db
854
+ .query("agentProfiles")
855
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", message.agentKey))
856
+ .unique();
857
+ if (!conversation || !profile)
858
+ return null;
859
+ const snapshotKey = `${args.workspaceId}:${message.agentKey}`;
860
+ const conversationCache = await ctx.db
861
+ .query("conversationHydrationCache")
862
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
863
+ .first();
864
+ let telegramBotToken = null;
865
+ const telegramSecretRefs = profile.secretsRef.filter((ref) => ref === "telegram.botToken" || ref.startsWith("telegram.botToken."));
866
+ for (const telegramSecretRef of telegramSecretRefs) {
867
+ const activeSecret = await ctx.db
868
+ .query("secrets")
869
+ .withIndex("by_secretRef_and_active", (q) => q.eq("secretRef", telegramSecretRef).eq("active", true))
870
+ .unique();
871
+ if (activeSecret) {
872
+ telegramBotToken = decryptSecretValue(activeSecret.encryptedValue, activeSecret.algorithm);
873
+ break;
874
+ }
875
+ }
876
+ const contextHistory = conversationCache && conversationCache.snapshotKey === snapshotKey
877
+ ? conversationCache.deltaContext
878
+ : conversation.contextHistory;
879
+ const bridgeRuntimeConfig = await resolveBridgeRuntimeConfig(ctx, profile);
880
+ return {
881
+ messageId: message._id,
882
+ conversationId: message.conversationId,
883
+ agentKey: message.agentKey,
884
+ payload: message.payload,
885
+ conversationState: {
886
+ contextHistory,
887
+ pendingToolCalls: conversation.pendingToolCalls,
888
+ },
889
+ telegramBotToken,
890
+ bridgeRuntimeConfig,
891
+ };
892
+ },
893
+ });
894
+ export const getQueueStats = query({
895
+ args: {
896
+ nowMs: v.optional(v.number()),
897
+ },
898
+ returns: v.object({
899
+ queuedReady: v.number(),
900
+ processing: v.number(),
901
+ deadLetter: v.number(),
902
+ }),
903
+ handler: async (ctx, args) => {
904
+ const nowMs = args.nowMs ?? Date.now();
905
+ const queued = await ctx.db
906
+ .query("messageQueue")
907
+ .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", "queued"))
908
+ .collect();
909
+ const processing = await ctx.db
910
+ .query("messageQueue")
911
+ .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", "processing"))
912
+ .collect();
913
+ const deadLetter = await ctx.db
914
+ .query("messageQueue")
915
+ .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", "dead_letter"))
916
+ .collect();
917
+ return {
918
+ queuedReady: queued.filter((job) => job.scheduledFor <= nowMs).length,
919
+ processing: processing.length,
920
+ deadLetter: deadLetter.length,
921
+ };
922
+ },
923
+ });
924
+ export const hasQueuedJobsForConversation = query({
925
+ args: {
926
+ conversationId: v.string(),
927
+ },
928
+ returns: v.boolean(),
929
+ handler: async (ctx, args) => {
930
+ const queuedJob = await ctx.db
931
+ .query("messageQueue")
932
+ .withIndex("by_conversationId_and_status", (q) => q.eq("conversationId", args.conversationId).eq("status", "queued"))
933
+ .first();
934
+ return queuedJob !== null;
935
+ },
936
+ });
937
+ export const getReadyConversationCountForScheduler = internalQuery({
938
+ args: {
939
+ nowMs: v.optional(v.number()),
940
+ limit: v.optional(v.number()),
941
+ },
942
+ returns: v.number(),
943
+ handler: async (ctx, args) => {
944
+ const nowMs = args.nowMs ?? Date.now();
945
+ const limit = Math.max(1, args.limit ?? 1000);
946
+ const queuedJobs = await ctx.db
947
+ .query("messageQueue")
948
+ .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", "queued").lte("scheduledFor", nowMs))
949
+ .take(limit);
950
+ const conversationIds = [...new Set(queuedJobs.map((job) => job.conversationId))];
951
+ let readyConversations = 0;
952
+ for (const conversationId of conversationIds) {
953
+ const conversation = await ctx.db
954
+ .query("conversations")
955
+ .withIndex("by_conversationId", (q) => q.eq("conversationId", conversationId))
956
+ .unique();
957
+ const lock = conversation?.processingLock;
958
+ if (!lock || lock.leaseExpiresAt <= nowMs) {
959
+ readyConversations += 1;
960
+ }
961
+ }
962
+ return readyConversations;
963
+ },
964
+ });
965
+ export const getActiveConversationCountForScheduler = internalQuery({
966
+ args: {
967
+ nowMs: v.optional(v.number()),
968
+ limit: v.optional(v.number()),
969
+ },
970
+ returns: v.number(),
971
+ handler: async (ctx, args) => {
972
+ const nowMs = args.nowMs ?? Date.now();
973
+ const limit = Math.max(1, args.limit ?? 1000);
974
+ const queuedJobs = await ctx.db
975
+ .query("messageQueue")
976
+ .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", "queued").lte("scheduledFor", nowMs))
977
+ .take(limit);
978
+ const processingJobs = await ctx.db
979
+ .query("messageQueue")
980
+ .withIndex("by_status_and_leaseExpiresAt", (q) => q.eq("status", "processing").gt("leaseExpiresAt", nowMs))
981
+ .take(limit);
982
+ const conversationIds = new Set();
983
+ for (const job of queuedJobs) {
984
+ conversationIds.add(job.conversationId);
985
+ }
986
+ for (const job of processingJobs) {
987
+ conversationIds.add(job.conversationId);
988
+ }
989
+ return conversationIds.size;
990
+ },
991
+ });
992
+ export const listJobsByStatus = query({
993
+ args: {
994
+ status: queueStatusValidator,
995
+ limit: v.optional(v.number()),
996
+ },
997
+ returns: v.array(v.object({
998
+ _id: v.id("messageQueue"),
999
+ _creationTime: v.number(),
1000
+ conversationId: v.string(),
1001
+ agentKey: v.string(),
1002
+ status: queueStatusValidator,
1003
+ priority: v.number(),
1004
+ scheduledFor: v.number(),
1005
+ attempts: v.number(),
1006
+ maxAttempts: v.number(),
1007
+ lastError: v.optional(v.string()),
1008
+ })),
1009
+ handler: async (ctx, args) => {
1010
+ const jobs = await ctx.db
1011
+ .query("messageQueue")
1012
+ .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", args.status))
1013
+ .order("asc")
1014
+ .take(args.limit ?? 100);
1015
+ return jobs.map((job) => ({
1016
+ _id: job._id,
1017
+ _creationTime: job._creationTime,
1018
+ conversationId: job.conversationId,
1019
+ agentKey: job.agentKey,
1020
+ status: job.status,
1021
+ priority: job.priority,
1022
+ scheduledFor: job.scheduledFor,
1023
+ attempts: job.attempts,
1024
+ maxAttempts: job.maxAttempts,
1025
+ lastError: job.lastError,
1026
+ }));
1027
+ },
1028
+ });
1029
+ export const upsertWorkerState = internalMutation({
1030
+ args: {
1031
+ workerId: v.string(),
1032
+ provider: v.string(),
1033
+ status: v.union(v.literal("active"), v.literal("stopped")),
1034
+ load: v.number(),
1035
+ nowMs: v.optional(v.number()),
1036
+ scheduledShutdownAt: v.optional(v.number()),
1037
+ stoppedAt: v.optional(v.number()),
1038
+ machineId: v.optional(v.string()),
1039
+ appName: v.optional(v.string()),
1040
+ region: v.optional(v.string()),
1041
+ },
1042
+ returns: v.null(),
1043
+ handler: async (ctx, args) => {
1044
+ const nowMs = args.nowMs ?? Date.now();
1045
+ const worker = await ctx.db
1046
+ .query("workers")
1047
+ .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
1048
+ .unique();
1049
+ if (!worker) {
1050
+ await ctx.db.insert("workers", {
1051
+ workerId: args.workerId,
1052
+ provider: args.provider,
1053
+ status: args.status,
1054
+ load: args.load,
1055
+ heartbeatAt: nowMs,
1056
+ scheduledShutdownAt: args.scheduledShutdownAt,
1057
+ stoppedAt: args.status === "stopped" ? (args.stoppedAt ?? nowMs) : undefined,
1058
+ machineRef: args.machineId && args.appName
1059
+ ? {
1060
+ appName: args.appName,
1061
+ machineId: args.machineId,
1062
+ region: args.region,
1063
+ }
1064
+ : undefined,
1065
+ capabilities: [],
1066
+ });
1067
+ return null;
1068
+ }
1069
+ await ctx.db.patch(worker._id, {
1070
+ status: args.status,
1071
+ load: args.load,
1072
+ heartbeatAt: args.status === "active" ? nowMs : worker.heartbeatAt,
1073
+ scheduledShutdownAt: args.scheduledShutdownAt ?? worker.scheduledShutdownAt,
1074
+ stoppedAt: args.status === "active"
1075
+ ? undefined
1076
+ : (args.stoppedAt ?? worker.stoppedAt ?? nowMs),
1077
+ machineRef: args.machineId && args.appName
1078
+ ? {
1079
+ appName: args.appName,
1080
+ machineId: args.machineId,
1081
+ region: args.region,
1082
+ }
1083
+ : worker.machineRef,
1084
+ });
1085
+ return null;
1086
+ },
1087
+ });
1088
+ export const getWorkerControlState = query({
1089
+ args: {
1090
+ workerId: v.string(),
1091
+ },
1092
+ returns: v.object({
1093
+ shouldStop: v.boolean(),
1094
+ }),
1095
+ handler: async (ctx, args) => {
1096
+ const worker = await ctx.db
1097
+ .query("workers")
1098
+ .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
1099
+ .unique();
1100
+ return {
1101
+ shouldStop: !worker || worker.status === "stopped",
1102
+ };
1103
+ },
1104
+ });
1105
+ export const prepareDataSnapshotUpload = mutation({
1106
+ args: {
1107
+ workerId: v.string(),
1108
+ workspaceId: v.string(),
1109
+ agentKey: v.string(),
1110
+ conversationId: v.optional(v.string()),
1111
+ reason: snapshotReasonValidator,
1112
+ nowMs: v.optional(v.number()),
1113
+ },
1114
+ returns: v.object({
1115
+ snapshotId: v.id("dataSnapshots"),
1116
+ uploadUrl: v.string(),
1117
+ expiresAt: v.number(),
1118
+ }),
1119
+ handler: async (ctx, args) => {
1120
+ const nowMs = args.nowMs ?? Date.now();
1121
+ const expiresAt = nowMs + DATA_SNAPSHOT_RETENTION_MS;
1122
+ const snapshotId = await ctx.db.insert("dataSnapshots", {
1123
+ workspaceId: args.workspaceId,
1124
+ agentKey: args.agentKey,
1125
+ workerId: args.workerId,
1126
+ conversationId: args.conversationId,
1127
+ reason: args.reason,
1128
+ formatVersion: 1,
1129
+ status: "uploading",
1130
+ createdAt: nowMs,
1131
+ expiresAt,
1132
+ });
1133
+ const uploadUrl = await ctx.storage.generateUploadUrl();
1134
+ return { snapshotId, uploadUrl, expiresAt };
1135
+ },
1136
+ });
1137
+ export const finalizeDataSnapshotUpload = mutation({
1138
+ args: {
1139
+ workerId: v.string(),
1140
+ snapshotId: v.id("dataSnapshots"),
1141
+ storageId: v.id("_storage"),
1142
+ sha256: v.string(),
1143
+ sizeBytes: v.number(),
1144
+ nowMs: v.optional(v.number()),
1145
+ },
1146
+ returns: v.boolean(),
1147
+ handler: async (ctx, args) => {
1148
+ const nowMs = args.nowMs ?? Date.now();
1149
+ const snapshot = await ctx.db.get(args.snapshotId);
1150
+ if (!snapshot || snapshot.workerId !== args.workerId)
1151
+ return false;
1152
+ await ctx.db.patch(snapshot._id, {
1153
+ archiveFileId: args.storageId,
1154
+ sha256: args.sha256,
1155
+ sizeBytes: args.sizeBytes,
1156
+ status: "ready",
1157
+ completedAt: nowMs,
1158
+ });
1159
+ const worker = await ctx.db
1160
+ .query("workers")
1161
+ .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
1162
+ .unique();
1163
+ if (worker) {
1164
+ await ctx.db.patch(worker._id, {
1165
+ lastSnapshotId: snapshot._id,
1166
+ });
1167
+ }
1168
+ return true;
1169
+ },
1170
+ });
1171
+ export const failDataSnapshotUpload = mutation({
1172
+ args: {
1173
+ workerId: v.string(),
1174
+ snapshotId: v.id("dataSnapshots"),
1175
+ error: v.string(),
1176
+ nowMs: v.optional(v.number()),
1177
+ },
1178
+ returns: v.boolean(),
1179
+ handler: async (ctx, args) => {
1180
+ const nowMs = args.nowMs ?? Date.now();
1181
+ const snapshot = await ctx.db.get(args.snapshotId);
1182
+ if (!snapshot || snapshot.workerId !== args.workerId)
1183
+ return false;
1184
+ await ctx.db.patch(snapshot._id, {
1185
+ status: "failed",
1186
+ error: args.error,
1187
+ completedAt: nowMs,
1188
+ });
1189
+ return true;
1190
+ },
1191
+ });
1192
+ export const getLatestDataSnapshotForRestore = query({
1193
+ args: {
1194
+ workspaceId: v.string(),
1195
+ agentKey: v.string(),
1196
+ conversationId: v.optional(v.string()),
1197
+ nowMs: v.optional(v.number()),
1198
+ },
1199
+ returns: v.union(v.null(), v.object({
1200
+ snapshotId: v.id("dataSnapshots"),
1201
+ downloadUrl: v.string(),
1202
+ sha256: v.union(v.null(), v.string()),
1203
+ sizeBytes: v.union(v.null(), v.number()),
1204
+ createdAt: v.number(),
1205
+ })),
1206
+ handler: async (ctx, args) => {
1207
+ const nowMs = args.nowMs ?? Date.now();
1208
+ const candidates = await ctx.db
1209
+ .query("dataSnapshots")
1210
+ .withIndex("by_workspaceId_and_agentKey_and_createdAt", (q) => q.eq("workspaceId", args.workspaceId).eq("agentKey", args.agentKey))
1211
+ .order("desc")
1212
+ .take(50);
1213
+ const ready = candidates.filter((snapshot) => snapshot.status === "ready" &&
1214
+ snapshot.archiveFileId !== undefined &&
1215
+ snapshot.expiresAt > nowMs);
1216
+ const preferred = (args.conversationId
1217
+ ? ready.find((snapshot) => snapshot.conversationId === args.conversationId)
1218
+ : undefined) ?? ready[0];
1219
+ if (!preferred || !preferred.archiveFileId)
1220
+ return null;
1221
+ const downloadUrl = await ctx.storage.getUrl(preferred.archiveFileId);
1222
+ if (!downloadUrl)
1223
+ return null;
1224
+ return {
1225
+ snapshotId: preferred._id,
1226
+ downloadUrl,
1227
+ sha256: preferred.sha256 ?? null,
1228
+ sizeBytes: preferred.sizeBytes ?? null,
1229
+ createdAt: preferred.createdAt,
1230
+ };
1231
+ },
1232
+ });
1233
+ export const listWorkersForScheduler = internalQuery({
1234
+ args: {},
1235
+ returns: v.array(v.object({
1236
+ workerId: v.string(),
1237
+ status: v.union(v.literal("active"), v.literal("stopped")),
1238
+ load: v.number(),
1239
+ heartbeatAt: v.number(),
1240
+ lastClaimAt: v.union(v.null(), v.number()),
1241
+ scheduledShutdownAt: v.union(v.null(), v.number()),
1242
+ stoppedAt: v.union(v.null(), v.number()),
1243
+ machineId: v.union(v.null(), v.string()),
1244
+ appName: v.union(v.null(), v.string()),
1245
+ region: v.union(v.null(), v.string()),
1246
+ })),
1247
+ handler: async (ctx) => {
1248
+ const rows = await ctx.db.query("workers").collect();
1249
+ return rows.map((worker) => ({
1250
+ workerId: worker.workerId,
1251
+ status: worker.status,
1252
+ load: worker.load,
1253
+ heartbeatAt: worker.heartbeatAt,
1254
+ lastClaimAt: worker.lastClaimAt ?? null,
1255
+ scheduledShutdownAt: worker.scheduledShutdownAt ?? null,
1256
+ stoppedAt: worker.stoppedAt ?? null,
1257
+ machineId: worker.machineRef?.machineId ?? null,
1258
+ appName: worker.machineRef?.appName ?? null,
1259
+ region: worker.machineRef?.region ?? null,
1260
+ }));
1261
+ },
1262
+ });
1263
+ export const expireOldDataSnapshots = internalMutation({
1264
+ args: {
1265
+ nowMs: v.optional(v.number()),
1266
+ limit: v.optional(v.number()),
1267
+ },
1268
+ returns: v.number(),
1269
+ handler: async (ctx, args) => {
1270
+ const nowMs = args.nowMs ?? Date.now();
1271
+ const limit = args.limit ?? 100;
1272
+ const rows = await ctx.db
1273
+ .query("dataSnapshots")
1274
+ .withIndex("by_status_and_expiresAt", (q) => q.eq("status", "ready").lte("expiresAt", nowMs))
1275
+ .take(limit);
1276
+ for (const row of rows) {
1277
+ await ctx.db.patch(row._id, { status: "expired" });
1278
+ }
1279
+ return rows.length;
1280
+ },
1281
+ });
1282
+ export const getWorkerStats = query({
1283
+ args: {},
1284
+ returns: v.object({
1285
+ activeCount: v.number(),
1286
+ idleCount: v.number(),
1287
+ workers: v.array(v.object({
1288
+ workerId: v.string(),
1289
+ status: v.union(v.literal("active"), v.literal("stopped")),
1290
+ load: v.number(),
1291
+ heartbeatAt: v.number(),
1292
+ machineId: v.union(v.null(), v.string()),
1293
+ appName: v.union(v.null(), v.string()),
1294
+ })),
1295
+ }),
1296
+ handler: async (ctx) => {
1297
+ const activeWorkers = await ctx.db
1298
+ .query("workers")
1299
+ .withIndex("by_status", (q) => q.eq("status", "active"))
1300
+ .collect();
1301
+ const withLoad = activeWorkers.filter((w) => w.load > 0);
1302
+ const idle = activeWorkers.filter((w) => w.load === 0);
1303
+ return {
1304
+ activeCount: withLoad.length,
1305
+ idleCount: idle.length,
1306
+ workers: activeWorkers.map((worker) => ({
1307
+ workerId: worker.workerId,
1308
+ status: worker.status,
1309
+ load: worker.load,
1310
+ heartbeatAt: worker.heartbeatAt,
1311
+ machineId: worker.machineRef?.machineId ?? null,
1312
+ appName: worker.machineRef?.appName ?? null,
1313
+ })),
1314
+ };
1315
+ },
1316
+ });
1317
+ async function resolveBridgeRuntimeConfig(ctx, profile) {
1318
+ if (!profile.bridgeConfig?.enabled) {
1319
+ return null;
1320
+ }
1321
+ const configuredServiceKeySecretRef = profile.bridgeConfig.serviceKeySecretRef ?? null;
1322
+ const configuredBaseUrlMapSecretRef = profile.bridgeConfig.appBaseUrlMapJsonSecretRef ?? null;
1323
+ const [serviceKeySecretRef, serviceKey] = await resolveFirstActiveSecretValue(ctx, getScopedSecretRefCandidates(profile.agentKey, BRIDGE_SECRET_REFS.serviceKey, configuredServiceKeySecretRef));
1324
+ const [, baseUrlFromSecret] = await resolveFirstActiveSecretValue(ctx, getScopedSecretRefCandidates(profile.agentKey, BRIDGE_SECRET_REFS.baseUrl));
1325
+ const [, appBaseUrlMapJsonFromSecret] = await resolveFirstActiveSecretValue(ctx, getScopedSecretRefCandidates(profile.agentKey, BRIDGE_SECRET_REFS.baseUrlMapJson, configuredBaseUrlMapSecretRef));
1326
+ const [, serviceIdFromSecret] = await resolveFirstActiveSecretValue(ctx, getScopedSecretRefCandidates(profile.agentKey, BRIDGE_SECRET_REFS.serviceId));
1327
+ const [, appKeyFromSecret] = await resolveFirstActiveSecretValue(ctx, getScopedSecretRefCandidates(profile.agentKey, BRIDGE_SECRET_REFS.appKey));
1328
+ return {
1329
+ baseUrl: profile.bridgeConfig.baseUrl ?? baseUrlFromSecret,
1330
+ appBaseUrlMapJson: appBaseUrlMapJsonFromSecret,
1331
+ serviceId: profile.bridgeConfig.serviceId ?? serviceIdFromSecret,
1332
+ appKey: profile.bridgeConfig.appKey ?? appKeyFromSecret,
1333
+ serviceKey,
1334
+ serviceKeySecretRef,
1335
+ };
1336
+ }
1337
+ function getBridgeSecretRefsForProfile(agentKey, bridgeConfig) {
1338
+ if (!bridgeConfig?.enabled) {
1339
+ return [];
1340
+ }
1341
+ const refs = [
1342
+ bridgeConfig.serviceKeySecretRef ?? `${BRIDGE_SECRET_REFS.serviceKey}.${agentKey}`,
1343
+ bridgeConfig.appBaseUrlMapJsonSecretRef ??
1344
+ `${BRIDGE_SECRET_REFS.baseUrlMapJson}.${agentKey}`,
1345
+ ];
1346
+ return refs;
1347
+ }
1348
+ function getScopedSecretRefCandidates(agentKey, globalPrefix, preferredRef) {
1349
+ const refs = [];
1350
+ if (preferredRef && preferredRef.trim().length > 0) {
1351
+ refs.push(preferredRef.trim());
1352
+ }
1353
+ refs.push(`${globalPrefix}.${agentKey}`);
1354
+ refs.push(globalPrefix);
1355
+ return Array.from(new Set(refs));
1356
+ }
1357
+ async function resolveFirstActiveSecretValue(ctx, secretRefs) {
1358
+ for (const secretRef of secretRefs) {
1359
+ const active = await ctx.db
1360
+ .query("secrets")
1361
+ .withIndex("by_secretRef_and_active", (q) => q.eq("secretRef", secretRef).eq("active", true))
1362
+ .unique();
1363
+ if (active) {
1364
+ return [secretRef, decryptSecretValue(active.encryptedValue, active.algorithm)];
1365
+ }
1366
+ }
1367
+ return [null, null];
1368
+ }
1369
+ function fingerprintConversationDelta(deltaContext) {
1370
+ const payload = deltaContext.map((entry) => `${entry.role}:${entry.at}:${entry.content}`).join("|");
1371
+ let hash = 2166136261;
1372
+ for (let index = 0; index < payload.length; index += 1) {
1373
+ hash ^= payload.charCodeAt(index);
1374
+ hash = Math.imul(hash, 16777619);
1375
+ }
1376
+ return `f${(hash >>> 0).toString(16)}`;
1377
+ }
1378
+ function encryptSecretValue(plaintext) {
1379
+ const units = Array.from(plaintext);
1380
+ return units
1381
+ .map((char, index) => {
1382
+ const code = char.charCodeAt(0);
1383
+ const mask = 11 + (index % 7);
1384
+ return (code ^ mask).toString(16).padStart(4, "0");
1385
+ })
1386
+ .join("");
1387
+ }
1388
+ function decryptSecretValue(encryptedValue, algorithm) {
1389
+ if (algorithm !== "xor-hex-v1") {
1390
+ throw new Error(`Unsupported secret algorithm '${algorithm}'`);
1391
+ }
1392
+ if (encryptedValue.length % 4 !== 0) {
1393
+ throw new Error("Invalid secret payload");
1394
+ }
1395
+ let out = "";
1396
+ for (let i = 0; i < encryptedValue.length; i += 4) {
1397
+ const chunk = encryptedValue.slice(i, i + 4);
1398
+ const value = Number.parseInt(chunk, 16);
1399
+ if (Number.isNaN(value)) {
1400
+ throw new Error("Invalid secret payload");
1401
+ }
1402
+ const mask = 11 + ((i / 4) % 7);
1403
+ out += String.fromCharCode(value ^ mask);
1404
+ }
1405
+ return out;
1406
+ }
1407
+ //# sourceMappingURL=queue.js.map