@okrlinkhub/agent-factory 1.0.0 → 1.0.2

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.
@@ -2,10 +2,10 @@ export declare const enqueueMessage: import("convex/server").RegisteredMutation<
2
2
  nowMs?: number | undefined;
3
3
  providerConfig?: {
4
4
  appName: string;
5
- region: string;
6
5
  kind: "fly" | "runpod" | "ecs";
7
6
  organizationSlug: string;
8
7
  image: string;
8
+ region: string;
9
9
  volumeName: string;
10
10
  volumePath: string;
11
11
  volumeSizeGb: number;
@@ -72,10 +72,10 @@ export declare const getActiveSecretPlaintext: import("convex/server").Registere
72
72
  }, Promise<string | null>>;
73
73
  export declare const getProviderRuntimeConfig: import("convex/server").RegisteredQuery<"internal", {}, Promise<{
74
74
  appName: string;
75
- region: string;
76
75
  kind: "fly" | "runpod" | "ecs";
77
76
  organizationSlug: string;
78
77
  image: string;
78
+ region: string;
79
79
  volumeName: string;
80
80
  volumePath: string;
81
81
  volumeSizeGb: number;
@@ -84,10 +84,10 @@ export declare const upsertProviderRuntimeConfig: import("convex/server").Regist
84
84
  nowMs?: number | undefined;
85
85
  providerConfig: {
86
86
  appName: string;
87
- region: string;
88
87
  kind: "fly" | "runpod" | "ecs";
89
88
  organizationSlug: string;
90
89
  image: string;
90
+ region: string;
91
91
  volumeName: string;
92
92
  volumePath: string;
93
93
  volumeSizeGb: number;
@@ -95,10 +95,10 @@ export declare const upsertProviderRuntimeConfig: import("convex/server").Regist
95
95
  }, Promise<null>>;
96
96
  export declare const providerRuntimeConfig: import("convex/server").RegisteredQuery<"public", {}, Promise<{
97
97
  appName: string;
98
- region: string;
99
98
  kind: "fly" | "runpod" | "ecs";
100
99
  organizationSlug: string;
101
100
  image: string;
101
+ region: string;
102
102
  volumeName: string;
103
103
  volumePath: string;
104
104
  volumeSizeGb: number;
@@ -107,10 +107,10 @@ export declare const setProviderRuntimeConfig: import("convex/server").Registere
107
107
  nowMs?: number | undefined;
108
108
  providerConfig: {
109
109
  appName: string;
110
- region: string;
111
110
  kind: "fly" | "runpod" | "ecs";
112
111
  organizationSlug: string;
113
112
  image: string;
113
+ region: string;
114
114
  volumeName: string;
115
115
  volumePath: string;
116
116
  volumeSizeGb: number;
@@ -155,9 +155,9 @@ export declare const deployGlobalSkill: import("convex/server").RegisteredMutati
155
155
  releaseChannel: "stable" | "canary";
156
156
  }>>;
157
157
  export declare const listGlobalSkills: import("convex/server").RegisteredQuery<"public", {
158
+ status?: "active" | "disabled" | undefined;
158
159
  limit?: number | undefined;
159
160
  releaseChannel?: "stable" | "canary" | undefined;
160
- status?: "active" | "disabled" | undefined;
161
161
  }, Promise<{
162
162
  skillId: any;
163
163
  slug: string;
@@ -197,8 +197,8 @@ export declare const getWorkerGlobalSkillsManifest: import("convex/server").Regi
197
197
  export declare const setGlobalSkillStatus: import("convex/server").RegisteredMutation<"public", {
198
198
  nowMs?: number | undefined;
199
199
  actor?: string | undefined;
200
- slug: string;
201
200
  status: "active" | "disabled";
201
+ slug: string;
202
202
  }, Promise<{
203
203
  updated: boolean;
204
204
  slug: string;
@@ -224,6 +224,7 @@ export declare const attachMessageMetadata: import("convex/server").RegisteredMu
224
224
  }, Promise<boolean>>;
225
225
  export declare const claimNextJob: import("convex/server").RegisteredMutation<"public", {
226
226
  nowMs?: number | undefined;
227
+ conversationId?: string | undefined;
227
228
  workerId: string;
228
229
  }, Promise<{
229
230
  messageId: import("convex/values").GenericId<"messageQueue">;
@@ -250,10 +251,10 @@ export declare const completeJob: import("convex/server").RegisteredMutation<"pu
250
251
  nowMs?: number | undefined;
251
252
  providerConfig?: {
252
253
  appName: string;
253
- region: string;
254
254
  kind: "fly" | "runpod" | "ecs";
255
255
  organizationSlug: string;
256
256
  image: string;
257
+ region: string;
257
258
  volumeName: string;
258
259
  volumePath: string;
259
260
  volumeSizeGb: number;
@@ -266,10 +267,10 @@ export declare const failJob: import("convex/server").RegisteredMutation<"public
266
267
  nowMs?: number | undefined;
267
268
  providerConfig?: {
268
269
  appName: string;
269
- region: string;
270
270
  kind: "fly" | "runpod" | "ecs";
271
271
  organizationSlug: string;
272
272
  image: string;
273
+ region: string;
273
274
  volumeName: string;
274
275
  volumePath: string;
275
276
  volumeSizeGb: number;
@@ -352,6 +353,10 @@ export declare const getActiveConversationCountForScheduler: import("convex/serv
352
353
  nowMs?: number | undefined;
353
354
  limit?: number | undefined;
354
355
  }, Promise<number>>;
356
+ export declare const getActiveConversationIdsForScheduler: import("convex/server").RegisteredQuery<"internal", {
357
+ nowMs?: number | undefined;
358
+ limit?: number | undefined;
359
+ }, Promise<string[]>>;
355
360
  export declare const listJobsByStatus: import("convex/server").RegisteredQuery<"public", {
356
361
  limit?: number | undefined;
357
362
  status: "done" | "failed" | "queued" | "processing" | "dead_letter";
@@ -370,14 +375,14 @@ export declare const listJobsByStatus: import("convex/server").RegisteredQuery<"
370
375
  export declare const upsertWorkerState: import("convex/server").RegisteredMutation<"internal", {
371
376
  nowMs?: number | undefined;
372
377
  appName?: string | undefined;
373
- machineId?: string | undefined;
374
378
  region?: string | undefined;
379
+ machineId?: string | undefined;
375
380
  scheduledShutdownAt?: number | undefined;
376
381
  stoppedAt?: number | undefined;
377
382
  clearLastSnapshotId?: boolean | undefined;
378
383
  clearMachineRef?: boolean | undefined;
379
- workerId: string;
380
384
  status: "active" | "draining" | "stopping" | "stopped";
385
+ workerId: string;
381
386
  provider: string;
382
387
  load: number;
383
388
  }, Promise<null>>;
@@ -408,8 +413,8 @@ export declare const finalizeDataSnapshotUpload: import("convex/server").Registe
408
413
  }, Promise<boolean>>;
409
414
  export declare const failDataSnapshotUpload: import("convex/server").RegisteredMutation<"public", {
410
415
  nowMs?: number | undefined;
411
- workerId: string;
412
416
  error: string;
417
+ workerId: string;
413
418
  snapshotId: import("convex/values").GenericId<"dataSnapshots">;
414
419
  }, Promise<boolean>>;
415
420
  export declare const getLatestDataSnapshotForRestore: import("convex/server").RegisteredQuery<"public", {
@@ -433,6 +438,12 @@ export declare const listWorkersForScheduler: import("convex/server").Registered
433
438
  scheduledShutdownAt: number | null;
434
439
  stoppedAt: number | null;
435
440
  lastSnapshotId: import("convex/values").GenericId<"dataSnapshots"> | null;
441
+ assignment: {
442
+ agentKey: string;
443
+ conversationId: string;
444
+ leaseId: string;
445
+ assignedAt: number;
446
+ } | null;
436
447
  machineId: string | null;
437
448
  appName: string | null;
438
449
  region: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/component/queue.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/component/queue.ts"],"names":[],"mappings":"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"}
@@ -22,6 +22,12 @@ const claimedJobValidator = v.object({
22
22
  leaseExpiresAt: v.number(),
23
23
  payload: queuePayloadValidator,
24
24
  });
25
+ const workerAssignmentValidator = v.object({
26
+ conversationId: v.string(),
27
+ agentKey: v.string(),
28
+ leaseId: v.string(),
29
+ assignedAt: v.number(),
30
+ });
25
31
  const secretStatusValidator = v.object({
26
32
  secretRef: v.string(),
27
33
  hasActive: v.boolean(),
@@ -121,6 +127,9 @@ export const enqueueMessage = mutation({
121
127
  pendingToolCalls: [],
122
128
  });
123
129
  }
130
+ else if (existingConversation.agentKey !== args.agentKey) {
131
+ throw new Error(`Conversation '${args.conversationId}' is already bound to agent '${existingConversation.agentKey}', cannot enqueue for '${args.agentKey}'.`);
132
+ }
124
133
  const priority = Math.min(DEFAULT_CONFIG.queue.maxPriority, Math.max(0, args.priority ?? DEFAULT_CONFIG.queue.defaultPriority));
125
134
  const messageId = await ctx.db.insert("messageQueue", {
126
135
  conversationId: args.conversationId,
@@ -864,11 +873,13 @@ export const attachMessageMetadata = mutation({
864
873
  export const claimNextJob = mutation({
865
874
  args: {
866
875
  workerId: v.string(),
876
+ conversationId: v.optional(v.string()),
867
877
  nowMs: v.optional(v.number()),
868
878
  },
869
879
  returns: v.union(v.null(), claimedJobValidator),
870
880
  handler: async (ctx, args) => {
871
881
  const nowMs = args.nowMs ?? Date.now();
882
+ const staleHeartbeatCutoff = nowMs - DEFAULT_CONFIG.lease.staleAfterMs;
872
883
  const worker = await ctx.db
873
884
  .query("workers")
874
885
  .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
@@ -876,6 +887,12 @@ export const claimNextJob = mutation({
876
887
  if (worker && !isWorkerClaimable(worker.status)) {
877
888
  return null;
878
889
  }
890
+ if (worker?.assignment &&
891
+ args.conversationId &&
892
+ worker.assignment.conversationId !== args.conversationId) {
893
+ return null;
894
+ }
895
+ const workers = await ctx.db.query("workers").collect();
879
896
  const candidates = await ctx.db
880
897
  .query("messageQueue")
881
898
  .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", "queued").lte("scheduledFor", nowMs))
@@ -888,17 +905,45 @@ export const claimNextJob = mutation({
888
905
  return a._creationTime - b._creationTime;
889
906
  });
890
907
  for (const candidate of candidates) {
908
+ if (args.conversationId && candidate.conversationId !== args.conversationId) {
909
+ continue;
910
+ }
911
+ if (worker?.assignment &&
912
+ candidate.conversationId !== worker.assignment.conversationId) {
913
+ continue;
914
+ }
891
915
  const conversation = await ctx.db
892
916
  .query("conversations")
893
917
  .withIndex("by_conversationId", (q) => q.eq("conversationId", candidate.conversationId))
894
918
  .unique();
895
919
  if (!conversation)
896
920
  continue;
921
+ if (conversation.agentKey !== candidate.agentKey)
922
+ continue;
923
+ if (worker?.assignment &&
924
+ conversation.agentKey !== worker.assignment.agentKey) {
925
+ continue;
926
+ }
927
+ const existingOwner = findActiveAssignmentOwner(workers, {
928
+ conversationId: candidate.conversationId,
929
+ agentKey: candidate.agentKey,
930
+ excludeWorkerId: args.workerId,
931
+ staleHeartbeatCutoff,
932
+ });
933
+ if (existingOwner) {
934
+ continue;
935
+ }
897
936
  const lock = conversation.processingLock;
898
937
  if (lock && lock.leaseExpiresAt > nowMs)
899
938
  continue;
900
939
  const leaseId = `${nowMs}-${Math.random().toString(36).slice(2, 10)}`;
901
940
  const leaseExpiresAt = nowMs + DEFAULT_CONFIG.lease.leaseMs;
941
+ const nextAssignment = {
942
+ conversationId: candidate.conversationId,
943
+ agentKey: candidate.agentKey,
944
+ leaseId,
945
+ assignedAt: worker?.assignment?.assignedAt ?? nowMs,
946
+ };
902
947
  await ctx.db.patch(candidate._id, {
903
948
  status: "processing",
904
949
  claimedBy: args.workerId,
@@ -924,6 +969,7 @@ export const claimNextJob = mutation({
924
969
  lastClaimAt: nowMs,
925
970
  scheduledShutdownAt: undefined,
926
971
  stoppedAt: undefined,
972
+ assignment: nextAssignment,
927
973
  capabilities: [],
928
974
  });
929
975
  }
@@ -935,6 +981,7 @@ export const claimNextJob = mutation({
935
981
  lastClaimAt: nowMs,
936
982
  scheduledShutdownAt: undefined,
937
983
  stoppedAt: undefined,
984
+ assignment: nextAssignment,
938
985
  });
939
986
  }
940
987
  await scheduleLeaseRecoveryWatchdog(ctx, nowMs);
@@ -990,7 +1037,19 @@ export const heartbeatJob = mutation({
990
1037
  .withIndex("by_workerId", (q) => q.eq("workerId", args.workerId))
991
1038
  .unique();
992
1039
  if (worker && isWorkerRunning(worker.status)) {
993
- await ctx.db.patch(worker._id, { heartbeatAt: nowMs });
1040
+ const nextPatch = { heartbeatAt: nowMs };
1041
+ if (!worker.assignment ||
1042
+ worker.assignment.conversationId !== message.conversationId ||
1043
+ worker.assignment.agentKey !== message.agentKey ||
1044
+ worker.assignment.leaseId !== args.leaseId) {
1045
+ nextPatch.assignment = {
1046
+ conversationId: message.conversationId,
1047
+ agentKey: message.agentKey,
1048
+ leaseId: args.leaseId,
1049
+ assignedAt: worker.assignment?.assignedAt ?? nowMs,
1050
+ };
1051
+ }
1052
+ await ctx.db.patch(worker._id, nextPatch);
994
1053
  }
995
1054
  return true;
996
1055
  },
@@ -1040,6 +1099,7 @@ export const completeJob = mutation({
1040
1099
  load: nextLoad,
1041
1100
  heartbeatAt: nowMs,
1042
1101
  scheduledShutdownAt: nextScheduledShutdownAt,
1102
+ assignment: getAssignmentForCompletedConversation(worker, message),
1043
1103
  });
1044
1104
  if (nextScheduledShutdownAt !== undefined) {
1045
1105
  await scheduleIdleShutdownWatchdog(ctx, nextScheduledShutdownAt, nowMs, args.providerConfig);
@@ -1118,6 +1178,7 @@ export const failJob = mutation({
1118
1178
  load: nextLoad,
1119
1179
  heartbeatAt: nowMs,
1120
1180
  scheduledShutdownAt: nextScheduledShutdownAt,
1181
+ assignment: getAssignmentForCompletedConversation(worker, message),
1121
1182
  });
1122
1183
  if (nextScheduledShutdownAt !== undefined) {
1123
1184
  await scheduleIdleShutdownWatchdog(ctx, nextScheduledShutdownAt, nowMs, args.providerConfig);
@@ -1187,6 +1248,7 @@ export const releaseExpiredLeases = internalMutation({
1187
1248
  load: nextLoad,
1188
1249
  heartbeatAt: nowMs,
1189
1250
  scheduledShutdownAt: nextScheduledShutdownAt,
1251
+ assignment: clearAssignmentForMessage(worker, message, nextLoad),
1190
1252
  });
1191
1253
  if (nextScheduledShutdownAt !== undefined) {
1192
1254
  await scheduleIdleShutdownWatchdog(ctx, nextScheduledShutdownAt, nowMs);
@@ -1254,6 +1316,7 @@ export const releaseStuckJobs = mutation({
1254
1316
  load: nextLoad,
1255
1317
  heartbeatAt: nowMs,
1256
1318
  scheduledShutdownAt: nextScheduledShutdownAt,
1319
+ assignment: clearAssignmentForMessage(worker, message, nextLoad),
1257
1320
  });
1258
1321
  if (nextScheduledShutdownAt !== undefined) {
1259
1322
  await scheduleIdleShutdownWatchdog(ctx, nextScheduledShutdownAt, nowMs);
@@ -1436,6 +1499,26 @@ export const getActiveConversationCountForScheduler = internalQuery({
1436
1499
  return conversationIds.size;
1437
1500
  },
1438
1501
  });
1502
+ export const getActiveConversationIdsForScheduler = internalQuery({
1503
+ args: {
1504
+ nowMs: v.optional(v.number()),
1505
+ limit: v.optional(v.number()),
1506
+ },
1507
+ returns: v.array(v.string()),
1508
+ handler: async (ctx, args) => {
1509
+ const nowMs = args.nowMs ?? Date.now();
1510
+ const limit = Math.max(1, args.limit ?? 1000);
1511
+ const queuedJobs = await ctx.db
1512
+ .query("messageQueue")
1513
+ .withIndex("by_status_and_scheduledFor", (q) => q.eq("status", "queued").lte("scheduledFor", nowMs))
1514
+ .take(limit);
1515
+ const processingJobs = await ctx.db
1516
+ .query("messageQueue")
1517
+ .withIndex("by_status_and_leaseExpiresAt", (q) => q.eq("status", "processing").gt("leaseExpiresAt", nowMs))
1518
+ .take(limit);
1519
+ return Array.from(new Set([...queuedJobs, ...processingJobs].map((job) => job.conversationId))).sort();
1520
+ },
1521
+ });
1439
1522
  export const listJobsByStatus = query({
1440
1523
  args: {
1441
1524
  status: queueStatusValidator,
@@ -1506,6 +1589,7 @@ export const upsertWorkerState = internalMutation({
1506
1589
  stoppedAt: args.status === "stopped" || args.status === "stopping"
1507
1590
  ? (args.stoppedAt ?? nowMs)
1508
1591
  : undefined,
1592
+ assignment: undefined,
1509
1593
  machineRef: args.machineId && args.appName
1510
1594
  ? {
1511
1595
  appName: args.appName,
@@ -1529,6 +1613,7 @@ export const upsertWorkerState = internalMutation({
1529
1613
  ? (args.stoppedAt ?? worker.stoppedAt ?? nowMs)
1530
1614
  : undefined,
1531
1615
  lastSnapshotId: args.clearLastSnapshotId ? undefined : worker.lastSnapshotId,
1616
+ assignment: worker.assignment,
1532
1617
  machineRef: args.clearMachineRef
1533
1618
  ? undefined
1534
1619
  : args.machineId && args.appName
@@ -1677,9 +1762,9 @@ export const getLatestDataSnapshotForRestore = query({
1677
1762
  const ready = candidates.filter((snapshot) => snapshot.status === "ready" &&
1678
1763
  snapshot.archiveFileId !== undefined &&
1679
1764
  snapshot.expiresAt > nowMs);
1680
- const preferred = (args.conversationId
1765
+ const preferred = args.conversationId
1681
1766
  ? ready.find((snapshot) => snapshot.conversationId === args.conversationId)
1682
- : undefined) ?? ready[0];
1767
+ : ready[0];
1683
1768
  if (!preferred || !preferred.archiveFileId)
1684
1769
  return null;
1685
1770
  const downloadUrl = await ctx.storage.getUrl(preferred.archiveFileId);
@@ -1705,6 +1790,7 @@ export const listWorkersForScheduler = internalQuery({
1705
1790
  scheduledShutdownAt: v.union(v.null(), v.number()),
1706
1791
  stoppedAt: v.union(v.null(), v.number()),
1707
1792
  lastSnapshotId: v.union(v.null(), v.id("dataSnapshots")),
1793
+ assignment: v.union(v.null(), workerAssignmentValidator),
1708
1794
  machineId: v.union(v.null(), v.string()),
1709
1795
  appName: v.union(v.null(), v.string()),
1710
1796
  region: v.union(v.null(), v.string()),
@@ -1720,6 +1806,7 @@ export const listWorkersForScheduler = internalQuery({
1720
1806
  scheduledShutdownAt: worker.scheduledShutdownAt ?? null,
1721
1807
  stoppedAt: worker.stoppedAt ?? null,
1722
1808
  lastSnapshotId: worker.lastSnapshotId ?? null,
1809
+ assignment: worker.assignment ?? null,
1723
1810
  machineId: worker.machineRef?.machineId ?? null,
1724
1811
  appName: worker.machineRef?.appName ?? null,
1725
1812
  region: worker.machineRef?.region ?? null,
@@ -1822,7 +1909,7 @@ async function scheduleIdleShutdownWatchdog(ctx, scheduledShutdownAt, nowMs, pro
1822
1909
  console.warn(`[queue] failed to schedule idle-shutdown watchdog: ${error instanceof Error ? error.message : String(error)}`);
1823
1910
  }
1824
1911
  }
1825
- async function scheduleLeaseRecoveryWatchdog(ctx, nowMs) {
1912
+ async function scheduleLeaseRecoveryWatchdog(ctx, _nowMs) {
1826
1913
  const delayMs = DEFAULT_CONFIG.lease.leaseMs + 1_000;
1827
1914
  try {
1828
1915
  await ctx.scheduler.runAfter(delayMs, internal.scheduler.reconcileWorkerPoolInternal, {
@@ -1840,6 +1927,33 @@ function computeNextScheduledShutdownAt(worker, nextLoad, nowMs) {
1840
1927
  const shutdownBaseMs = worker.lastClaimAt ?? nowMs;
1841
1928
  return worker.scheduledShutdownAt ?? shutdownBaseMs + DEFAULT_CONFIG.scaling.idleTimeoutMs;
1842
1929
  }
1930
+ function getAssignmentForCompletedConversation(worker, message) {
1931
+ if (worker.assignment &&
1932
+ worker.assignment.conversationId === message.conversationId &&
1933
+ worker.assignment.agentKey === message.agentKey) {
1934
+ return {
1935
+ ...worker.assignment,
1936
+ leaseId: message.leaseId ?? worker.assignment.leaseId,
1937
+ };
1938
+ }
1939
+ return worker.assignment;
1940
+ }
1941
+ function clearAssignmentForMessage(worker, message, nextLoad) {
1942
+ if (nextLoad === 0 &&
1943
+ worker.assignment &&
1944
+ worker.assignment.conversationId === message.conversationId &&
1945
+ worker.assignment.agentKey === message.agentKey) {
1946
+ return undefined;
1947
+ }
1948
+ return worker.assignment;
1949
+ }
1950
+ function findActiveAssignmentOwner(workers, args) {
1951
+ return workers.find((candidate) => candidate.workerId !== args.excludeWorkerId &&
1952
+ isWorkerClaimable(candidate.status) &&
1953
+ candidate.heartbeatAt > args.staleHeartbeatCutoff &&
1954
+ candidate.assignment?.conversationId === args.conversationId &&
1955
+ candidate.assignment.agentKey === args.agentKey);
1956
+ }
1843
1957
  function dedupeMessagesById(messages) {
1844
1958
  const seen = new Set();
1845
1959
  const deduped = [];