@okrlinkhub/agent-factory 0.2.4 → 0.2.6

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 (40) hide show
  1. package/README.md +32 -0
  2. package/dist/client/index.d.ts +145 -3
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +201 -0
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -0
  7. package/dist/component/_generated/api.d.ts.map +1 -1
  8. package/dist/component/_generated/api.js.map +1 -1
  9. package/dist/component/_generated/component.d.ts +516 -4
  10. package/dist/component/_generated/component.d.ts.map +1 -1
  11. package/dist/component/lib.d.ts +1 -0
  12. package/dist/component/lib.d.ts.map +1 -1
  13. package/dist/component/lib.js +1 -0
  14. package/dist/component/lib.js.map +1 -1
  15. package/dist/component/pushing.d.ts +256 -0
  16. package/dist/component/pushing.d.ts.map +1 -0
  17. package/dist/component/pushing.js +962 -0
  18. package/dist/component/pushing.js.map +1 -0
  19. package/dist/component/queue.d.ts +8 -7
  20. package/dist/component/queue.d.ts.map +1 -1
  21. package/dist/component/queue.js +54 -1
  22. package/dist/component/queue.js.map +1 -1
  23. package/dist/component/scheduler.d.ts +5 -5
  24. package/dist/component/scheduler.js +13 -1
  25. package/dist/component/scheduler.js.map +1 -1
  26. package/dist/component/schema.d.ts +258 -8
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +103 -0
  29. package/dist/component/schema.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/client/index.test.ts +29 -19
  32. package/src/client/index.ts +218 -0
  33. package/src/component/_generated/api.ts +2 -0
  34. package/src/component/_generated/component.ts +520 -4
  35. package/src/component/lib.test.ts +98 -0
  36. package/src/component/lib.ts +17 -0
  37. package/src/component/pushing.ts +1121 -0
  38. package/src/component/queue.ts +65 -1
  39. package/src/component/scheduler.ts +17 -1
  40. package/src/component/schema.ts +137 -0
@@ -94,6 +94,31 @@ export const enqueueMessage = mutation({
94
94
  if (!profile || !profile.enabled) {
95
95
  throw new Error(`Agent profile '${args.agentKey}' not found or disabled`);
96
96
  }
97
+ const resolvedProviderUserId =
98
+ profile.providerUserId && profile.providerUserId.trim().length > 0
99
+ ? profile.providerUserId.trim()
100
+ : args.payload.providerUserId;
101
+
102
+ const providerUserIdStr =
103
+ typeof resolvedProviderUserId === "string" &&
104
+ resolvedProviderUserId.trim().length > 0
105
+ ? resolvedProviderUserId.trim()
106
+ : null;
107
+
108
+ if (providerUserIdStr === null) {
109
+ throw new Error(
110
+ `providerUserId is required but missing: profile.providerUserId=${JSON.stringify(profile.providerUserId)}, payload.providerUserId=${JSON.stringify(args.payload.providerUserId)}`,
111
+ );
112
+ }
113
+
114
+ const payload = {
115
+ ...args.payload,
116
+ providerUserId: providerUserIdStr,
117
+ metadata: {
118
+ ...(args.payload.metadata ?? {}),
119
+ providerUserId: providerUserIdStr,
120
+ },
121
+ };
97
122
 
98
123
  const existingConversation = await ctx.db
99
124
  .query("conversations")
@@ -119,7 +144,7 @@ export const enqueueMessage = mutation({
119
144
  const messageId = await ctx.db.insert("messageQueue", {
120
145
  conversationId: args.conversationId,
121
146
  agentKey: args.agentKey,
122
- payload: args.payload,
147
+ payload,
123
148
  status: "queued",
124
149
  priority,
125
150
  scheduledFor: args.scheduledFor ?? nowMs,
@@ -212,6 +237,7 @@ export const appendConversationMessages = mutation({
212
237
  export const upsertAgentProfile = mutation({
213
238
  args: {
214
239
  agentKey: v.string(),
240
+ providerUserId: v.optional(v.string()),
215
241
  version: v.string(),
216
242
  soulMd: v.string(),
217
243
  clientMd: v.optional(v.string()),
@@ -745,6 +771,7 @@ export const releaseExpiredLeases = internalMutation({
745
771
  let requeued = 0;
746
772
  let unlocked = 0;
747
773
  for (const message of stuck) {
774
+ const claimedWorkerId = message.claimedBy;
748
775
  await ctx.db.patch(message._id, {
749
776
  status: "queued",
750
777
  scheduledFor: nowMs,
@@ -768,6 +795,24 @@ export const releaseExpiredLeases = internalMutation({
768
795
  await ctx.db.patch(conversation._id, { processingLock: undefined });
769
796
  unlocked += 1;
770
797
  }
798
+
799
+ if (claimedWorkerId) {
800
+ const worker = await ctx.db
801
+ .query("workers")
802
+ .withIndex("by_workerId", (q) => q.eq("workerId", claimedWorkerId))
803
+ .unique();
804
+ if (worker && worker.status === "active") {
805
+ const nextLoad = Math.max(0, worker.load - 1);
806
+ const nextScheduledShutdownAt =
807
+ 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
+ }
771
816
  }
772
817
 
773
818
  return { requeued, unlocked };
@@ -797,6 +842,7 @@ export const releaseStuckJobs = mutation({
797
842
  let requeued = 0;
798
843
  let unlocked = 0;
799
844
  for (const message of stuck) {
845
+ const claimedWorkerId = message.claimedBy;
800
846
  await ctx.db.patch(message._id, {
801
847
  status: "queued",
802
848
  scheduledFor: nowMs,
@@ -820,6 +866,24 @@ export const releaseStuckJobs = mutation({
820
866
  await ctx.db.patch(conversation._id, { processingLock: undefined });
821
867
  unlocked += 1;
822
868
  }
869
+
870
+ if (claimedWorkerId) {
871
+ const worker = await ctx.db
872
+ .query("workers")
873
+ .withIndex("by_workerId", (q) => q.eq("workerId", claimedWorkerId))
874
+ .unique();
875
+ if (worker && worker.status === "active") {
876
+ const nextLoad = Math.max(0, worker.load - 1);
877
+ const nextScheduledShutdownAt =
878
+ nextLoad === 0 ? nowMs + DEFAULT_CONFIG.scaling.idleTimeoutMs : undefined;
879
+ await ctx.db.patch(worker._id, {
880
+ load: nextLoad,
881
+ heartbeatAt: nowMs,
882
+ scheduledShutdownAt: nextScheduledShutdownAt,
883
+ stoppedAt: undefined,
884
+ });
885
+ }
886
+ }
823
887
  }
824
888
 
825
889
  return { requeued, unlocked };
@@ -123,6 +123,21 @@ async function runReconcileWorkerPool(
123
123
  const workspaceId = args.workspaceId ?? "default";
124
124
  const provider = resolveProvider(providerConfig.kind, flyApiToken);
125
125
 
126
+ // Always recover expired leases before scaling decisions.
127
+ // This prevents stale "processing" jobs from blocking queue drain forever.
128
+ try {
129
+ await ctx.runMutation((internal.queue as any).releaseExpiredLeases, {
130
+ nowMs,
131
+ limit: 500,
132
+ });
133
+ } catch (error) {
134
+ console.warn(
135
+ `[scheduler] releaseExpiredLeases failed: ${
136
+ error instanceof Error ? error.message : String(error)
137
+ }`,
138
+ );
139
+ }
140
+
126
141
  const activeConversationCount: number = await ctx.runQuery(
127
142
  (internal.queue as any).getActiveConversationCountForScheduler,
128
143
  { nowMs, limit: 1000 },
@@ -204,8 +219,9 @@ async function runReconcileWorkerPool(
204
219
  `[scheduler] dedicated volume mode enabled for ${providerConfig.volumeName}; clamping desired workers to 1`,
205
220
  );
206
221
  }
222
+ const staleHeartbeatCutoff = nowMs - DEFAULT_CONFIG.lease.staleAfterMs;
207
223
  const activeWorkers = workerRows.filter(
208
- (worker) => worker.status === "active",
224
+ (worker) => worker.status === "active" && worker.heartbeatAt > staleHeartbeatCutoff,
209
225
  ).length;
210
226
 
211
227
  if (targetActiveWorkers > activeWorkers) {
@@ -4,6 +4,7 @@ import { v } from "convex/values";
4
4
  export default defineSchema({
5
5
  agentProfiles: defineTable({
6
6
  agentKey: v.string(),
7
+ providerUserId: v.optional(v.string()),
7
8
  version: v.string(),
8
9
  soulMd: v.string(),
9
10
  clientMd: v.optional(v.string()),
@@ -228,4 +229,140 @@ export default defineSchema({
228
229
  })
229
230
  .index("by_conversationId", ["conversationId"])
230
231
  .index("by_agentKey_and_lastHydratedAt", ["agentKey", "lastHydratedAt"]),
232
+
233
+ messagePushTemplates: defineTable({
234
+ companyId: v.string(),
235
+ templateKey: v.string(),
236
+ title: v.string(),
237
+ text: v.string(),
238
+ periodicity: v.union(
239
+ v.literal("manual"),
240
+ v.literal("daily"),
241
+ v.literal("weekly"),
242
+ v.literal("monthly"),
243
+ ),
244
+ suggestedTimes: v.array(
245
+ v.union(
246
+ v.object({
247
+ kind: v.literal("daily"),
248
+ time: v.string(),
249
+ }),
250
+ v.object({
251
+ kind: v.literal("weekly"),
252
+ weekday: v.number(),
253
+ time: v.string(),
254
+ }),
255
+ v.object({
256
+ kind: v.literal("monthly"),
257
+ dayOfMonth: v.union(v.number(), v.literal("last")),
258
+ time: v.string(),
259
+ }),
260
+ ),
261
+ ),
262
+ enabled: v.boolean(),
263
+ createdBy: v.string(),
264
+ updatedBy: v.string(),
265
+ createdAt: v.number(),
266
+ updatedAt: v.number(),
267
+ })
268
+ .index("by_companyId", ["companyId"])
269
+ .index("by_companyId_and_templateKey", ["companyId", "templateKey"])
270
+ .index("by_companyId_and_enabled", ["companyId", "enabled"]),
271
+
272
+ messagePushJobs: defineTable({
273
+ companyId: v.string(),
274
+ consumerUserId: v.string(),
275
+ agentKey: v.optional(v.string()),
276
+ sourceTemplateId: v.optional(v.id("messagePushTemplates")),
277
+ title: v.string(),
278
+ text: v.string(),
279
+ periodicity: v.union(
280
+ v.literal("manual"),
281
+ v.literal("daily"),
282
+ v.literal("weekly"),
283
+ v.literal("monthly"),
284
+ ),
285
+ timezone: v.string(),
286
+ schedule: v.union(
287
+ v.object({
288
+ kind: v.literal("manual"),
289
+ }),
290
+ v.object({
291
+ kind: v.literal("daily"),
292
+ time: v.string(),
293
+ }),
294
+ v.object({
295
+ kind: v.literal("weekly"),
296
+ weekday: v.number(),
297
+ time: v.string(),
298
+ }),
299
+ v.object({
300
+ kind: v.literal("monthly"),
301
+ dayOfMonth: v.union(v.number(), v.literal("last")),
302
+ time: v.string(),
303
+ }),
304
+ ),
305
+ enabled: v.boolean(),
306
+ nextRunAt: v.optional(v.number()),
307
+ lastRunAt: v.optional(v.number()),
308
+ lastRunKey: v.optional(v.string()),
309
+ createdAt: v.number(),
310
+ updatedAt: v.number(),
311
+ })
312
+ .index("by_enabled_and_nextRunAt", ["enabled", "nextRunAt"])
313
+ .index("by_consumerUserId", ["consumerUserId"])
314
+ .index("by_consumerUserId_and_enabled", ["consumerUserId", "enabled"])
315
+ .index("by_companyId", ["companyId"])
316
+ .index("by_companyId_and_enabled", ["companyId", "enabled"])
317
+ .index("by_sourceTemplateId", ["sourceTemplateId"]),
318
+
319
+ messagePushDispatches: defineTable({
320
+ jobId: v.id("messagePushJobs"),
321
+ consumerUserId: v.string(),
322
+ runKey: v.string(),
323
+ scheduledFor: v.number(),
324
+ enqueuedMessageId: v.optional(v.id("messageQueue")),
325
+ status: v.union(
326
+ v.literal("enqueued"),
327
+ v.literal("skipped"),
328
+ v.literal("failed"),
329
+ ),
330
+ error: v.optional(v.string()),
331
+ createdAt: v.number(),
332
+ })
333
+ .index("by_jobId_and_runKey", ["jobId", "runKey"])
334
+ .index("by_consumerUserId_and_createdAt", ["consumerUserId", "createdAt"]),
335
+
336
+ messagePushBroadcasts: defineTable({
337
+ companyId: v.string(),
338
+ title: v.string(),
339
+ text: v.string(),
340
+ target: v.literal("all_active_agents"),
341
+ requestedBy: v.string(),
342
+ requestedAt: v.number(),
343
+ status: v.union(v.literal("running"), v.literal("done"), v.literal("failed")),
344
+ totalTargets: v.number(),
345
+ enqueuedCount: v.number(),
346
+ failedCount: v.number(),
347
+ completedAt: v.optional(v.number()),
348
+ })
349
+ .index("by_companyId_and_requestedAt", ["companyId", "requestedAt"])
350
+ .index("by_status", ["status"]),
351
+
352
+ messagePushBroadcastDispatches: defineTable({
353
+ broadcastId: v.id("messagePushBroadcasts"),
354
+ consumerUserId: v.string(),
355
+ agentKey: v.string(),
356
+ runKey: v.string(),
357
+ enqueuedMessageId: v.optional(v.id("messageQueue")),
358
+ status: v.union(
359
+ v.literal("enqueued"),
360
+ v.literal("skipped"),
361
+ v.literal("failed"),
362
+ ),
363
+ error: v.optional(v.string()),
364
+ createdAt: v.number(),
365
+ })
366
+ .index("by_broadcastId_and_consumerUserId", ["broadcastId", "consumerUserId"])
367
+ .index("by_broadcastId_and_createdAt", ["broadcastId", "createdAt"]),
231
368
  });