@okrlinkhub/agent-factory 3.0.1 → 3.0.3

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 (46) hide show
  1. package/README.md +185 -31
  2. package/dist/client/index.d.ts +11 -6
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +16 -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 +60 -0
  10. package/dist/component/_generated/component.d.ts.map +1 -1
  11. package/dist/component/flyCleanup.d.ts +32 -0
  12. package/dist/component/flyCleanup.d.ts.map +1 -0
  13. package/dist/component/flyCleanup.js +272 -0
  14. package/dist/component/flyCleanup.js.map +1 -0
  15. package/dist/component/lib.d.ts +1 -0
  16. package/dist/component/lib.d.ts.map +1 -1
  17. package/dist/component/lib.js +1 -0
  18. package/dist/component/lib.js.map +1 -1
  19. package/dist/component/providers/fly.d.ts +23 -2
  20. package/dist/component/providers/fly.d.ts.map +1 -1
  21. package/dist/component/providers/fly.js +15 -3
  22. package/dist/component/providers/fly.js.map +1 -1
  23. package/dist/component/pushing.d.ts +4 -4
  24. package/dist/component/queue.d.ts +32 -16
  25. package/dist/component/queue.d.ts.map +1 -1
  26. package/dist/component/queue.js +44 -2
  27. package/dist/component/queue.js.map +1 -1
  28. package/dist/component/scheduler.d.ts +8 -8
  29. package/dist/component/scheduler.d.ts.map +1 -1
  30. package/dist/component/scheduler.js +59 -12
  31. package/dist/component/scheduler.js.map +1 -1
  32. package/dist/component/schema.d.ts +30 -28
  33. package/dist/component/schema.d.ts.map +1 -1
  34. package/dist/component/schema.js +1 -0
  35. package/dist/component/schema.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/client/index.ts +16 -0
  38. package/src/component/_generated/api.ts +2 -0
  39. package/src/component/_generated/component.ts +72 -0
  40. package/src/component/flyCleanup.ts +386 -0
  41. package/src/component/lib.test.ts +280 -4
  42. package/src/component/lib.ts +1 -0
  43. package/src/component/providers/fly.ts +39 -5
  44. package/src/component/queue.ts +55 -2
  45. package/src/component/scheduler.ts +100 -16
  46. package/src/component/schema.ts +1 -0
@@ -73,6 +73,12 @@ type SchedulerWorkerRow = {
73
73
  machineId: string | null;
74
74
  appName: string | null;
75
75
  region: string | null;
76
+ volumeId: string | null;
77
+ };
78
+
79
+ type SchedulerConversationTarget = {
80
+ conversationId: string;
81
+ agentKey: string;
76
82
  };
77
83
 
78
84
  const PROVIDER_RECONCILE_GRACE_MS = 90_000;
@@ -182,11 +188,11 @@ async function runReconcileWorkerPool(
182
188
  }
183
189
  const workspaceId = args.workspaceId ?? "default";
184
190
  const provider = resolveProvider(providerConfig.kind, flyApiToken);
185
- const activeConversationIds: Array<string> = await ctx.runQuery(
186
- (internal.queue as any).getActiveConversationIdsForScheduler,
191
+ const activeConversations: Array<SchedulerConversationTarget> = await ctx.runQuery(
192
+ (internal.queue as any).getActiveConversationsForScheduler,
187
193
  { nowMs, limit: 1000 },
188
194
  );
189
- const activeConversationCount = activeConversationIds.length;
195
+ const activeConversationCount = activeConversations.length;
190
196
  const cycle = await runWorkerLifecycleCycle(ctx, {
191
197
  nowMs,
192
198
  provider,
@@ -195,7 +201,7 @@ async function runReconcileWorkerPool(
195
201
  allowSpawn: true,
196
202
  convexUrl,
197
203
  workspaceId,
198
- activeConversationIds,
204
+ activeConversations,
199
205
  desiredActiveWorkers: clamp(activeConversationCount, 0, scaling.maxWorkers),
200
206
  });
201
207
  if (activeConversationCount > 0 || cycle.pending > 0) {
@@ -258,7 +264,7 @@ async function runEnforceIdleShutdowns(
258
264
  scaling: DEFAULT_CONFIG.scaling,
259
265
  allowSpawn: false,
260
266
  desiredActiveWorkers: 0,
261
- activeConversationIds: [],
267
+ activeConversations: [],
262
268
  });
263
269
 
264
270
  if (cycle.pending > 0) {
@@ -282,7 +288,7 @@ async function runWorkerLifecycleCycle(
282
288
  scaling: typeof DEFAULT_CONFIG.scaling;
283
289
  allowSpawn: boolean;
284
290
  desiredActiveWorkers: number;
285
- activeConversationIds: Array<string>;
291
+ activeConversations: Array<SchedulerConversationTarget>;
286
292
  convexUrl?: string;
287
293
  workspaceId?: string;
288
294
  },
@@ -338,7 +344,7 @@ async function runWorkerLifecycleCycle(
338
344
  if (input.allowSpawn && input.desiredActiveWorkers > 0) {
339
345
  const claimableWorkers = countWorkersAvailableForActiveConversations(
340
346
  filterScopedWorkers(workerRows, input.providerConfig.appName),
341
- input.activeConversationIds,
347
+ input.activeConversations,
342
348
  staleHeartbeatCutoff,
343
349
  );
344
350
  if (input.desiredActiveWorkers > claimableWorkers) {
@@ -350,8 +356,30 @@ async function runWorkerLifecycleCycle(
350
356
  input.scaling.spawnStep,
351
357
  input.desiredActiveWorkers - claimableWorkers,
352
358
  );
359
+ const spawnTargets = selectSpawnTargetsForActiveConversations(
360
+ filterScopedWorkers(workerRows, input.providerConfig.appName),
361
+ input.activeConversations,
362
+ staleHeartbeatCutoff,
363
+ toSpawn,
364
+ );
353
365
  for (let index = 0; index < toSpawn; index += 1) {
354
366
  const workerId = `afw-${input.nowMs}-${index}`;
367
+ const target = spawnTargets[index];
368
+ const assignment = target
369
+ ? {
370
+ conversationId: target.conversationId,
371
+ agentKey: target.agentKey,
372
+ leaseId: `spawn:${workerId}`,
373
+ assignedAt: input.nowMs,
374
+ }
375
+ : undefined;
376
+ const workerVolume = await input.provider.ensureWorkerVolume({
377
+ appName: input.providerConfig.appName,
378
+ workerId,
379
+ region: input.providerConfig.region,
380
+ volumeName: input.providerConfig.volumeName,
381
+ volumeSizeGb: input.providerConfig.volumeSizeGb,
382
+ });
355
383
  await ctx.runMutation(internal.queue.upsertWorkerState, {
356
384
  workerId,
357
385
  provider: input.providerConfig.kind,
@@ -359,6 +387,8 @@ async function runWorkerLifecycleCycle(
359
387
  load: 0,
360
388
  nowMs: input.nowMs,
361
389
  scheduledShutdownAt: input.nowMs + input.scaling.idleTimeoutMs,
390
+ volumeId: workerVolume.volumeId,
391
+ assignment,
362
392
  });
363
393
  let created;
364
394
  try {
@@ -367,9 +397,8 @@ async function runWorkerLifecycleCycle(
367
397
  appName: input.providerConfig.appName,
368
398
  image: input.providerConfig.image,
369
399
  region: input.providerConfig.region,
370
- volumeName: input.providerConfig.volumeName,
400
+ volumeId: workerVolume.volumeId,
371
401
  volumePath: input.providerConfig.volumePath,
372
- volumeSizeGb: input.providerConfig.volumeSizeGb,
373
402
  env: compactEnv({
374
403
  ...DEFAULT_WORKER_RUNTIME_ENV,
375
404
  ...forwardedOpenClawEnv,
@@ -377,6 +406,8 @@ async function runWorkerLifecycleCycle(
377
406
  WORKSPACE_ID: input.workspaceId ?? "default",
378
407
  WORKER_ID: workerId,
379
408
  WORKER_IDLE_TIMEOUT_MS: String(input.scaling.idleTimeoutMs),
409
+ OPENCLAW_AGENT_KEY: target?.agentKey,
410
+ OPENCLAW_CONVERSATION_ID: target?.conversationId,
380
411
  }),
381
412
  });
382
413
  } catch (error) {
@@ -385,6 +416,13 @@ async function runWorkerLifecycleCycle(
385
416
  error instanceof Error ? error.message : String(error)
386
417
  }`,
387
418
  );
419
+ await input.provider.cleanupWorkerStorage({
420
+ appName: input.providerConfig.appName,
421
+ workerId,
422
+ region: input.providerConfig.region,
423
+ volumeName: input.providerConfig.volumeName,
424
+ volumeId: workerVolume.volumeId,
425
+ });
388
426
  await transitionWorkerToDraining(
389
427
  ctx,
390
428
  {
@@ -400,6 +438,7 @@ async function runWorkerLifecycleCycle(
400
438
  machineId: null,
401
439
  appName: input.providerConfig.appName,
402
440
  region: input.providerConfig.region,
441
+ volumeId: workerVolume.volumeId,
403
442
  },
404
443
  input.providerConfig,
405
444
  input.nowMs,
@@ -420,6 +459,7 @@ async function runWorkerLifecycleCycle(
420
459
  machineId: null,
421
460
  appName: input.providerConfig.appName,
422
461
  region: input.providerConfig.region,
462
+ volumeId: workerVolume.volumeId,
423
463
  },
424
464
  input.providerConfig,
425
465
  input.nowMs,
@@ -440,6 +480,7 @@ async function runWorkerLifecycleCycle(
440
480
  machineId: null,
441
481
  appName: input.providerConfig.appName,
442
482
  region: input.providerConfig.region,
483
+ volumeId: workerVolume.volumeId,
443
484
  },
444
485
  input.providerConfig,
445
486
  input.nowMs,
@@ -456,6 +497,8 @@ async function runWorkerLifecycleCycle(
456
497
  machineId: created.machineId,
457
498
  appName: input.providerConfig.appName,
458
499
  region: created.region,
500
+ volumeId: created.volumeId ?? workerVolume.volumeId,
501
+ assignment,
459
502
  });
460
503
  await scheduleIdleShutdownWatch(
461
504
  ctx,
@@ -655,6 +698,7 @@ async function transitionWorkerToStopped(
655
698
  scheduledShutdownAt: worker.scheduledShutdownAt ?? nowMs,
656
699
  stoppedAt: worker.stoppedAt ?? nowMs,
657
700
  clearMachineRef: true,
701
+ clearVolumeId: true,
658
702
  });
659
703
  }
660
704
 
@@ -682,6 +726,7 @@ async function finalizeWorkerTeardown(input: {
682
726
  machineId,
683
727
  region: input.worker.region ?? input.providerConfig.region,
684
728
  volumeName: input.providerConfig.volumeName,
729
+ volumeId: input.worker.volumeId,
685
730
  });
686
731
  return true;
687
732
  }
@@ -862,10 +907,46 @@ function filterScopedWorkers(workerRows: Array<SchedulerWorkerRow>, appName: str
862
907
 
863
908
  function countWorkersAvailableForActiveConversations(
864
909
  workerRows: Array<SchedulerWorkerRow>,
865
- activeConversationIds: Array<string>,
910
+ activeConversations: Array<SchedulerConversationTarget>,
911
+ staleHeartbeatCutoff: number,
912
+ ) {
913
+ const coverage = summarizeWorkerConversationCoverage(
914
+ workerRows,
915
+ activeConversations,
916
+ staleHeartbeatCutoff,
917
+ );
918
+ return coverage.unassignedWorkers + coverage.assignedConversationKeys.size;
919
+ }
920
+
921
+ function selectSpawnTargetsForActiveConversations(
922
+ workerRows: Array<SchedulerWorkerRow>,
923
+ activeConversations: Array<SchedulerConversationTarget>,
866
924
  staleHeartbeatCutoff: number,
925
+ limit: number,
867
926
  ) {
868
- const activeConversationSet = new Set(activeConversationIds);
927
+ const coverage = summarizeWorkerConversationCoverage(
928
+ workerRows,
929
+ activeConversations,
930
+ staleHeartbeatCutoff,
931
+ );
932
+ const uncoveredConversations = activeConversations.filter(
933
+ (conversation) =>
934
+ !coverage.assignedConversationKeys.has(getConversationTargetKey(conversation)),
935
+ );
936
+ return uncoveredConversations.slice(
937
+ coverage.unassignedWorkers,
938
+ coverage.unassignedWorkers + Math.max(0, limit),
939
+ );
940
+ }
941
+
942
+ function summarizeWorkerConversationCoverage(
943
+ workerRows: Array<SchedulerWorkerRow>,
944
+ activeConversations: Array<SchedulerConversationTarget>,
945
+ staleHeartbeatCutoff: number,
946
+ ) {
947
+ const activeConversationSet = new Set(
948
+ activeConversations.map((conversation) => getConversationTargetKey(conversation)),
949
+ );
869
950
  const assignedConversationKeys = new Set<string>();
870
951
  let unassignedWorkers = 0;
871
952
  for (const worker of workerRows) {
@@ -876,13 +957,16 @@ function countWorkersAvailableForActiveConversations(
876
957
  unassignedWorkers += 1;
877
958
  continue;
878
959
  }
879
- if (activeConversationSet.has(worker.assignment.conversationId)) {
880
- assignedConversationKeys.add(
881
- `${worker.assignment.agentKey}::${worker.assignment.conversationId}`,
882
- );
960
+ const assignmentKey = getConversationTargetKey(worker.assignment);
961
+ if (activeConversationSet.has(assignmentKey)) {
962
+ assignedConversationKeys.add(assignmentKey);
883
963
  }
884
964
  }
885
- return unassignedWorkers + assignedConversationKeys.size;
965
+ return { assignedConversationKeys, unassignedWorkers };
966
+ }
967
+
968
+ function getConversationTargetKey(input: { conversationId: string; agentKey: string }) {
969
+ return `${input.agentKey}::${input.conversationId}`;
886
970
  }
887
971
 
888
972
  function deriveScheduledShutdownAt(
@@ -127,6 +127,7 @@ export default defineSchema({
127
127
  workers: defineTable({
128
128
  workerId: v.string(),
129
129
  provider: v.string(),
130
+ volumeId: v.optional(v.string()),
130
131
  machineRef: v.optional(
131
132
  v.object({
132
133
  appName: v.string(),