@okrlinkhub/agent-factory 0.2.14 → 0.2.15

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 (35) hide show
  1. package/README.md +1 -4
  2. package/dist/client/index.d.ts +1 -1
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +0 -3
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/component.d.ts +0 -34
  7. package/dist/component/_generated/component.d.ts.map +1 -1
  8. package/dist/component/lib.d.ts +1 -1
  9. package/dist/component/lib.d.ts.map +1 -1
  10. package/dist/component/lib.js +1 -1
  11. package/dist/component/lib.js.map +1 -1
  12. package/dist/component/providers/fly.d.ts +14 -0
  13. package/dist/component/providers/fly.d.ts.map +1 -1
  14. package/dist/component/providers/fly.js +35 -5
  15. package/dist/component/providers/fly.js.map +1 -1
  16. package/dist/component/queue.d.ts +5 -20
  17. package/dist/component/queue.d.ts.map +1 -1
  18. package/dist/component/queue.js +41 -107
  19. package/dist/component/queue.js.map +1 -1
  20. package/dist/component/scheduler.d.ts.map +1 -1
  21. package/dist/component/scheduler.js +127 -81
  22. package/dist/component/scheduler.js.map +1 -1
  23. package/dist/component/schema.d.ts +5 -13
  24. package/dist/component/schema.d.ts.map +1 -1
  25. package/dist/component/schema.js +0 -4
  26. package/dist/component/schema.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/client/index.ts +0 -3
  29. package/src/component/_generated/component.ts +0 -42
  30. package/src/component/lib.test.ts +348 -88
  31. package/src/component/lib.ts +0 -1
  32. package/src/component/providers/fly.ts +50 -5
  33. package/src/component/queue.ts +52 -135
  34. package/src/component/scheduler.ts +211 -96
  35. package/src/component/schema.ts +0 -4
@@ -15,21 +15,54 @@ const TEST_PROVIDER_CONFIG = {
15
15
  volumeSizeGb: 10,
16
16
  };
17
17
 
18
+ function stableHashBase36(input: string): string {
19
+ let hash = 2166136261;
20
+ for (let index = 0; index < input.length; index += 1) {
21
+ hash ^= input.charCodeAt(index);
22
+ hash = Math.imul(hash, 16777619);
23
+ }
24
+ return (hash >>> 0).toString(36);
25
+ }
26
+
27
+ function buildDedicatedVolumeName(prefix: string, workerId: string) {
28
+ const sanitize = (value: string) =>
29
+ value
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9_]/g, "_")
32
+ .replace(/_+/g, "_")
33
+ .replace(/^_+|_+$/g, "");
34
+ const normalizedPrefix = sanitize(prefix) || "openclaw";
35
+ const normalizedWorker = sanitize(workerId) || "worker";
36
+ const workerHash = stableHashBase36(normalizedWorker).slice(0, 8);
37
+ const maxPrefixLen = 30 - 1 - workerHash.length;
38
+ return `${normalizedPrefix.slice(0, Math.max(1, maxPrefixLen))}_${workerHash}`;
39
+ }
40
+
41
+ function jsonResponse(body: unknown, status = 200) {
42
+ return new Response(JSON.stringify(body), {
43
+ status,
44
+ headers: { "Content-Type": "application/json" },
45
+ });
46
+ }
47
+
48
+ function emptyResponse(status = 204) {
49
+ return new Response(null, { status });
50
+ }
51
+
18
52
  describe("component lib", () => {
19
53
  beforeEach(async () => {
20
54
  vi.useFakeTimers();
21
55
  });
22
56
  afterEach(() => {
23
57
  vi.useRealTimers();
58
+ vi.restoreAllMocks();
59
+ vi.unstubAllGlobals();
24
60
  });
25
61
  test("enqueue and claim should respect queue flow", async () => {
26
62
  const t = initConvexTest();
27
63
  await t.mutation(api.queue.upsertAgentProfile, {
28
64
  agentKey: "support-agent",
29
65
  version: "1.0.0",
30
- soulMd: "# Soul",
31
- clientMd: "# Client",
32
- skills: ["agent-bridge"],
33
66
  secretsRef: ["telegram.botToken"],
34
67
  enabled: true,
35
68
  });
@@ -158,12 +191,11 @@ describe("component lib", () => {
158
191
  expect(claim?.payload.messageText).toBe("hello");
159
192
  });
160
193
 
161
- test("enqueue should fail when providerUserId is blank in both profile and payload", async () => {
194
+ test("enqueue should fail when providerUserId is blank in payload", async () => {
162
195
  const t = initConvexTest();
163
196
  await t.mutation(api.queue.upsertAgentProfile, {
164
197
  agentKey: "missing-provider-user-agent",
165
198
  version: "1.0.0",
166
- providerUserId: " ",
167
199
  secretsRef: [],
168
200
  enabled: true,
169
201
  });
@@ -178,53 +210,7 @@ describe("component lib", () => {
178
210
  messageText: "hello",
179
211
  },
180
212
  }),
181
- ).rejects.toThrow("providerUserId is required but missing");
182
- });
183
-
184
- test("clearDeprecatedAgentProfileFields should remove deprecated profile fields", async () => {
185
- const t = initConvexTest();
186
- await t.mutation(api.queue.upsertAgentProfile, {
187
- agentKey: "cleanup-agent",
188
- version: "1.0.0",
189
- providerUserId: "legacy-user-1",
190
- soulMd: "# Legacy Soul",
191
- clientMd: "# Legacy Client",
192
- skills: ["agent-bridge"],
193
- secretsRef: [],
194
- enabled: true,
195
- });
196
- await t.mutation(api.queue.upsertAgentProfile, {
197
- agentKey: "already-clean-agent",
198
- version: "1.0.0",
199
- secretsRef: [],
200
- enabled: true,
201
- });
202
-
203
- const dryRun = await t.mutation((api.lib as any).clearDeprecatedAgentProfileFields, {
204
- dryRun: true,
205
- });
206
- expect(dryRun.dryRun).toBe(true);
207
- expect(dryRun.scanned).toBe(2);
208
- expect(dryRun.updated).toBe(1);
209
- expect(dryRun.unchanged).toBe(1);
210
- expect(dryRun.clearedProviderUserId).toBe(1);
211
- expect(dryRun.clearedSoulMd).toBe(1);
212
- expect(dryRun.clearedClientMd).toBe(1);
213
- expect(dryRun.clearedSkills).toBe(1);
214
- expect(dryRun.updatedAgentKeys).toEqual(["cleanup-agent"]);
215
-
216
- const cleanup = await t.mutation((api.lib as any).clearDeprecatedAgentProfileFields, {});
217
- expect(cleanup.dryRun).toBe(false);
218
- expect(cleanup.updated).toBe(1);
219
- expect(cleanup.updatedAgentKeys).toEqual(["cleanup-agent"]);
220
-
221
- const secondPass = await t.mutation((api.lib as any).clearDeprecatedAgentProfileFields, {});
222
- expect(secondPass.updated).toBe(0);
223
- expect(secondPass.unchanged).toBe(2);
224
- expect(secondPass.clearedProviderUserId).toBe(0);
225
- expect(secondPass.clearedSoulMd).toBe(0);
226
- expect(secondPass.clearedClientMd).toBe(0);
227
- expect(secondPass.clearedSkills).toBe(0);
213
+ ).rejects.toThrow("providerUserId is required but missing in payload");
228
214
  });
229
215
 
230
216
  test("identity binding should resolve, rebind and revoke", async () => {
@@ -232,18 +218,12 @@ describe("component lib", () => {
232
218
  await t.mutation(api.queue.upsertAgentProfile, {
233
219
  agentKey: "agent-a",
234
220
  version: "1.0.0",
235
- soulMd: "# Soul",
236
- clientMd: "# Client",
237
- skills: ["agent-bridge"],
238
221
  secretsRef: [],
239
222
  enabled: true,
240
223
  });
241
224
  await t.mutation(api.queue.upsertAgentProfile, {
242
225
  agentKey: "agent-b",
243
226
  version: "1.0.0",
244
- soulMd: "# Soul",
245
- clientMd: "# Client",
246
- skills: ["agent-bridge"],
247
227
  secretsRef: [],
248
228
  enabled: true,
249
229
  });
@@ -298,9 +278,6 @@ describe("component lib", () => {
298
278
  await t.mutation(api.queue.upsertAgentProfile, {
299
279
  agentKey: "support-agent",
300
280
  version: "1.0.0",
301
- soulMd: "# Soul",
302
- clientMd: "# Client",
303
- skills: ["agent-bridge"],
304
281
  secretsRef: [],
305
282
  enabled: true,
306
283
  });
@@ -332,14 +309,93 @@ describe("component lib", () => {
332
309
  expect(worker?.scheduledShutdownAt).toBe(now + 300_000);
333
310
  });
334
311
 
312
+ test("idle shutdown should force worker to stopped and prevent reactivation", async () => {
313
+ const t = initConvexTest();
314
+ const claimTime = Date.UTC(2026, 0, 1, 12, 0, 0);
315
+ vi.stubGlobal(
316
+ "fetch",
317
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
318
+ const url = String(input);
319
+ const method = init?.method ?? "GET";
320
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "GET") {
321
+ return jsonResponse([]);
322
+ }
323
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "GET") {
324
+ return jsonResponse([]);
325
+ }
326
+ throw new Error(`Unexpected fetch ${method} ${url}`);
327
+ }),
328
+ );
329
+ vi.setSystemTime(claimTime);
330
+ await t.mutation(api.queue.upsertAgentProfile, {
331
+ agentKey: "support-agent",
332
+ version: "1.0.0",
333
+ secretsRef: [],
334
+ enabled: true,
335
+ });
336
+
337
+ const messageId = await t.mutation(api.lib.enqueue, {
338
+ conversationId: "telegram:chat:forced-stop",
339
+ agentKey: "support-agent",
340
+ payload: {
341
+ provider: "telegram",
342
+ providerUserId: "u-stop",
343
+ messageText: "stop me",
344
+ },
345
+ });
346
+ const claim = await t.mutation(api.lib.claim, { workerId: "worker-stop-force-1" });
347
+ expect(claim?.messageId).toBe(messageId);
348
+
349
+ const completionTime = claimTime + 60_000;
350
+ vi.setSystemTime(completionTime);
351
+ await t.mutation(api.lib.complete, {
352
+ workerId: "worker-stop-force-1",
353
+ messageId,
354
+ leaseId: claim?.leaseId ?? "",
355
+ providerConfig: TEST_PROVIDER_CONFIG,
356
+ } as any);
357
+
358
+ const dueTime = claimTime + 300_001;
359
+ vi.setSystemTime(dueTime);
360
+ const shutdown = await t.action(api.scheduler.checkIdleShutdowns, {
361
+ nowMs: dueTime,
362
+ flyApiToken: "fly-token",
363
+ providerConfig: TEST_PROVIDER_CONFIG,
364
+ });
365
+ expect(shutdown.stopped).toBe(1);
366
+
367
+ const workers = await t.query((internal.queue as any).listWorkersForScheduler, {});
368
+ const worker = workers.find((row: { workerId: string }) => row.workerId === "worker-stop-force-1");
369
+ expect(worker?.status).toBe("stopped");
370
+ expect(worker?.stoppedAt).toBe(dueTime);
371
+
372
+ const control = await t.query(api.queue.getWorkerControlState as any, {
373
+ workerId: "worker-stop-force-1",
374
+ });
375
+ expect(control.shouldStop).toBe(true);
376
+
377
+ const newMessageId = await t.mutation(api.lib.enqueue, {
378
+ conversationId: "telegram:chat:forced-stop:2",
379
+ agentKey: "support-agent",
380
+ payload: {
381
+ provider: "telegram",
382
+ providerUserId: "u-stop",
383
+ messageText: "new message",
384
+ },
385
+ });
386
+ expect(newMessageId).toBeDefined();
387
+
388
+ const reactivatedClaim = await t.mutation(api.lib.claim, {
389
+ workerId: "worker-stop-force-1",
390
+ });
391
+ expect(reactivatedClaim).toBeNull();
392
+ });
393
+
335
394
  test("hydration bundle should include resolved agent-bridge runtime config", async () => {
336
395
  const t = initConvexTest();
337
396
  await t.mutation(api.queue.upsertAgentProfile, {
338
397
  agentKey: "bridge-agent",
339
398
  version: "1.0.0",
340
- soulMd: "# Soul",
341
- clientMd: "# Client",
342
- skills: ["agent-bridge"],
343
399
  secretsRef: [],
344
400
  bridgeConfig: {
345
401
  enabled: true,
@@ -406,14 +462,239 @@ describe("component lib", () => {
406
462
  expect(controlUnknown.shouldStop).toBe(true);
407
463
  });
408
464
 
465
+ test("worker control state should stop active workers past scheduled shutdown", async () => {
466
+ const t = initConvexTest();
467
+ const nowMs = Date.UTC(2026, 0, 1, 14, 0, 0);
468
+ vi.setSystemTime(nowMs);
469
+ await t.mutation(internal.queue.upsertWorkerState, {
470
+ workerId: "worker-overdue-1",
471
+ provider: "fly",
472
+ status: "active",
473
+ load: 0,
474
+ nowMs: nowMs - 60_000,
475
+ scheduledShutdownAt: nowMs - 1,
476
+ });
477
+ const control = await t.query(api.queue.getWorkerControlState as any, {
478
+ workerId: "worker-overdue-1",
479
+ });
480
+ expect(control.shouldStop).toBe(true);
481
+ });
482
+
483
+ test("shutdown teardown should wait for final snapshot before deleting worker volume", async () => {
484
+ const t = initConvexTest();
485
+ const workerId = "worker-cleanup-1";
486
+ const machineId = "machine-cleanup-1";
487
+ const volumeId = "vol-cleanup-1";
488
+ const volumeName = buildDedicatedVolumeName(TEST_PROVIDER_CONFIG.volumeName, workerId);
489
+ const claimTime = Date.UTC(2026, 0, 1, 15, 0, 0);
490
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
491
+ const url = String(input);
492
+ const method = init?.method ?? "GET";
493
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "GET") {
494
+ return jsonResponse([
495
+ {
496
+ id: machineId,
497
+ name: workerId,
498
+ region: TEST_PROVIDER_CONFIG.region,
499
+ state: "started",
500
+ config: { image: TEST_PROVIDER_CONFIG.image },
501
+ },
502
+ ]);
503
+ }
504
+ if (url.endsWith(`/machines/${machineId}/cordon`) && method === "POST") {
505
+ return emptyResponse();
506
+ }
507
+ if (url.endsWith(`/machines/${machineId}/stop`) && method === "POST") {
508
+ return emptyResponse();
509
+ }
510
+ if (url.endsWith(`/machines/${machineId}`) && method === "DELETE") {
511
+ return emptyResponse();
512
+ }
513
+ if (url.endsWith(`/machines/${machineId}`) && method === "GET") {
514
+ return jsonResponse({
515
+ id: machineId,
516
+ config: {
517
+ mounts: [{ volume: volumeId, path: TEST_PROVIDER_CONFIG.volumePath }],
518
+ },
519
+ });
520
+ }
521
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "GET") {
522
+ return jsonResponse([
523
+ {
524
+ id: volumeId,
525
+ name: volumeName,
526
+ region: TEST_PROVIDER_CONFIG.region,
527
+ },
528
+ ]);
529
+ }
530
+ if (url.endsWith(`/volumes/${volumeId}`) && method === "DELETE") {
531
+ return emptyResponse();
532
+ }
533
+ throw new Error(`Unexpected fetch ${method} ${url}`);
534
+ });
535
+ vi.stubGlobal("fetch", fetchMock);
536
+ vi.setSystemTime(claimTime);
537
+
538
+ await t.mutation(api.queue.upsertAgentProfile, {
539
+ agentKey: "support-agent",
540
+ version: "1.0.0",
541
+ secretsRef: [],
542
+ enabled: true,
543
+ });
544
+ const messageId = await t.mutation(api.lib.enqueue, {
545
+ conversationId: "telegram:chat:cleanup",
546
+ agentKey: "support-agent",
547
+ payload: {
548
+ provider: "telegram",
549
+ providerUserId: "u-clean",
550
+ messageText: "cleanup",
551
+ },
552
+ });
553
+ const claim = await t.mutation(api.lib.claim, { workerId });
554
+ expect(claim?.messageId).toBe(messageId);
555
+
556
+ await t.mutation(internal.queue.upsertWorkerState, {
557
+ workerId,
558
+ provider: "fly",
559
+ status: "active",
560
+ load: 1,
561
+ nowMs: claimTime,
562
+ machineId,
563
+ appName: TEST_PROVIDER_CONFIG.appName,
564
+ region: TEST_PROVIDER_CONFIG.region,
565
+ });
566
+
567
+ const completionTime = claimTime + 60_000;
568
+ vi.setSystemTime(completionTime);
569
+ await t.mutation(api.queue.completeJob as any, {
570
+ workerId,
571
+ messageId,
572
+ leaseId: claim?.leaseId ?? "",
573
+ nowMs: completionTime,
574
+ providerConfig: TEST_PROVIDER_CONFIG,
575
+ });
576
+
577
+ const dueTime = claimTime + 300_001;
578
+ vi.setSystemTime(dueTime);
579
+ const firstPass = await t.action(api.scheduler.checkIdleShutdowns, {
580
+ nowMs: dueTime,
581
+ flyApiToken: "fly-token",
582
+ providerConfig: TEST_PROVIDER_CONFIG,
583
+ });
584
+ expect(firstPass.stopped).toBe(1);
585
+ expect(firstPass.pending).toBe(1);
586
+
587
+ const prematureDeleteCalls = fetchMock.mock.calls.filter((call) =>
588
+ String(call[0]).includes(`/volumes/${volumeId}`),
589
+ );
590
+ expect(prematureDeleteCalls).toHaveLength(0);
591
+
592
+ const snapshot = await t.mutation(api.queue.prepareDataSnapshotUpload as any, {
593
+ workerId,
594
+ workspaceId: "default",
595
+ agentKey: "support-agent",
596
+ conversationId: "telegram:chat:cleanup",
597
+ reason: "drain",
598
+ nowMs: dueTime + 1,
599
+ });
600
+ const storageId = await t.run(async (ctx) => {
601
+ return await ctx.storage.store(new Blob(["snapshot-ready"]));
602
+ });
603
+ const finalized = await t.mutation(api.queue.finalizeDataSnapshotUpload as any, {
604
+ workerId,
605
+ snapshotId: snapshot.snapshotId,
606
+ storageId,
607
+ sha256: "deadbeef",
608
+ sizeBytes: 14,
609
+ nowMs: dueTime + 2,
610
+ });
611
+ expect(finalized).toBe(true);
612
+
613
+ const secondPass = await t.action(api.scheduler.checkIdleShutdowns, {
614
+ nowMs: dueTime + 10_000,
615
+ flyApiToken: "fly-token",
616
+ providerConfig: TEST_PROVIDER_CONFIG,
617
+ });
618
+ expect(secondPass.pending).toBe(0);
619
+
620
+ const deleteMachineCalls = fetchMock.mock.calls.filter(
621
+ (call) =>
622
+ String(call[0]).endsWith(`/machines/${machineId}`) &&
623
+ ((call[1] as RequestInit | undefined)?.method ?? "GET") === "DELETE",
624
+ );
625
+ const deleteVolumeCalls = fetchMock.mock.calls.filter(
626
+ (call) =>
627
+ String(call[0]).endsWith(`/volumes/${volumeId}`) &&
628
+ ((call[1] as RequestInit | undefined)?.method ?? "GET") === "DELETE",
629
+ );
630
+ expect(deleteMachineCalls).toHaveLength(1);
631
+ expect(deleteVolumeCalls).toHaveLength(1);
632
+ });
633
+
634
+ test("cleanup should remove orphan worker volumes when the machine is already gone", async () => {
635
+ const t = initConvexTest();
636
+ const workerId = "worker-orphan-1";
637
+ const volumeId = "vol-orphan-1";
638
+ const volumeName = buildDedicatedVolumeName(TEST_PROVIDER_CONFIG.volumeName, workerId);
639
+ const nowMs = Date.UTC(2026, 0, 1, 16, 0, 0);
640
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
641
+ const url = String(input);
642
+ const method = init?.method ?? "GET";
643
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "GET") {
644
+ return jsonResponse([]);
645
+ }
646
+ if (url.endsWith(`/machines/machine-orphan-1`) && method === "GET") {
647
+ return new Response("not found", { status: 404 });
648
+ }
649
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "GET") {
650
+ return jsonResponse([
651
+ {
652
+ id: volumeId,
653
+ name: volumeName,
654
+ region: TEST_PROVIDER_CONFIG.region,
655
+ },
656
+ ]);
657
+ }
658
+ if (url.endsWith(`/volumes/${volumeId}`) && method === "DELETE") {
659
+ return emptyResponse();
660
+ }
661
+ throw new Error(`Unexpected fetch ${method} ${url}`);
662
+ });
663
+ vi.stubGlobal("fetch", fetchMock);
664
+
665
+ await t.mutation(internal.queue.upsertWorkerState, {
666
+ workerId,
667
+ provider: "fly",
668
+ status: "active",
669
+ load: 0,
670
+ nowMs,
671
+ scheduledShutdownAt: nowMs - 1,
672
+ machineId: "machine-orphan-1",
673
+ appName: TEST_PROVIDER_CONFIG.appName,
674
+ region: TEST_PROVIDER_CONFIG.region,
675
+ });
676
+
677
+ const result = await t.action(api.scheduler.checkIdleShutdowns, {
678
+ nowMs,
679
+ flyApiToken: "fly-token",
680
+ providerConfig: TEST_PROVIDER_CONFIG,
681
+ });
682
+ expect(result.stopped).toBe(1);
683
+ expect(result.pending).toBe(0);
684
+
685
+ const deleteVolumeCalls = fetchMock.mock.calls.filter(
686
+ (call) =>
687
+ String(call[0]).endsWith(`/volumes/${volumeId}`) &&
688
+ ((call[1] as RequestInit | undefined)?.method ?? "GET") === "DELETE",
689
+ );
690
+ expect(deleteVolumeCalls).toHaveLength(1);
691
+ });
692
+
409
693
  test("scheduler count includes queued and in-progress conversations", async () => {
410
694
  const t = initConvexTest();
411
695
  await t.mutation(api.queue.upsertAgentProfile, {
412
696
  agentKey: "support-agent",
413
697
  version: "1.0.0",
414
- soulMd: "# Soul",
415
- clientMd: "# Client",
416
- skills: ["agent-bridge"],
417
698
  secretsRef: [],
418
699
  enabled: true,
419
700
  });
@@ -491,9 +772,6 @@ describe("component lib", () => {
491
772
  await t.mutation(api.queue.upsertAgentProfile, {
492
773
  agentKey: "support-agent",
493
774
  version: "1.0.0",
494
- soulMd: "# Soul",
495
- clientMd: "# Client",
496
- skills: ["agent-bridge"],
497
775
  secretsRef: [],
498
776
  enabled: true,
499
777
  });
@@ -564,9 +842,6 @@ describe("component lib", () => {
564
842
  await t.mutation(api.queue.upsertAgentProfile, {
565
843
  agentKey: "support-agent",
566
844
  version: "1.0.0",
567
- soulMd: "# Soul",
568
- clientMd: "# Client",
569
- skills: ["agent-bridge"],
570
845
  secretsRef: [],
571
846
  enabled: true,
572
847
  });
@@ -637,9 +912,6 @@ describe("component lib", () => {
637
912
  await t.mutation(api.queue.upsertAgentProfile, {
638
913
  agentKey: "push-agent",
639
914
  version: "1.0.0",
640
- soulMd: "# Soul",
641
- clientMd: "# Client",
642
- skills: [],
643
915
  secretsRef: [],
644
916
  enabled: true,
645
917
  });
@@ -692,9 +964,6 @@ describe("component lib", () => {
692
964
  await t.mutation(api.queue.upsertAgentProfile, {
693
965
  agentKey: "push-telegram-manual-agent",
694
966
  version: "1.0.0",
695
- soulMd: "# Soul",
696
- clientMd: "# Client",
697
- skills: [],
698
967
  secretsRef: [],
699
968
  enabled: true,
700
969
  });
@@ -738,9 +1007,6 @@ describe("component lib", () => {
738
1007
  await t.mutation(api.queue.upsertAgentProfile, {
739
1008
  agentKey: "push-telegram-scheduled-agent",
740
1009
  version: "1.0.0",
741
- soulMd: "# Soul",
742
- clientMd: "# Client",
743
- skills: [],
744
1010
  secretsRef: [],
745
1011
  enabled: true,
746
1012
  });
@@ -787,18 +1053,12 @@ describe("component lib", () => {
787
1053
  await t.mutation(api.queue.upsertAgentProfile, {
788
1054
  agentKey: "broadcast-agent-a",
789
1055
  version: "1.0.0",
790
- soulMd: "# Soul",
791
- clientMd: "# Client",
792
- skills: [],
793
1056
  secretsRef: [],
794
1057
  enabled: true,
795
1058
  });
796
1059
  await t.mutation(api.queue.upsertAgentProfile, {
797
1060
  agentKey: "broadcast-agent-b",
798
1061
  version: "1.0.0",
799
- soulMd: "# Soul",
800
- clientMd: "# Client",
801
- skills: [],
802
1062
  secretsRef: [],
803
1063
  enabled: true,
804
1064
  });
@@ -1,6 +1,5 @@
1
1
  export {
2
2
  upsertAgentProfile as configureAgent,
3
- clearDeprecatedAgentProfileFields,
4
3
  importPlaintextSecret as importSecret,
5
4
  getSecretsStatus as secretStatus,
6
5
  providerRuntimeConfig,
@@ -33,6 +33,13 @@ export interface WorkerProvider {
33
33
  terminateWorker(appName: string, machineId: string): Promise<void>;
34
34
  cordonWorker(appName: string, machineId: string): Promise<void>;
35
35
  stopWorker(appName: string, machineId: string): Promise<void>;
36
+ cleanupWorkerStorage(input: {
37
+ appName: string;
38
+ workerId: string;
39
+ machineId?: string | null;
40
+ region?: string;
41
+ volumeName: string;
42
+ }): Promise<void>;
36
43
  }
37
44
 
38
45
  type FlyMachine = {
@@ -153,15 +160,53 @@ export class FlyMachinesProvider implements WorkerProvider {
153
160
  }
154
161
 
155
162
  async terminateWorker(appName: string, machineId: string): Promise<void> {
156
- const volumeIds = await this.getMachineVolumeIds(appName, machineId);
157
- await this.request<void>({
158
- path: `/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}`,
159
- method: "DELETE",
163
+ try {
164
+ await this.request<void>({
165
+ path: `/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}`,
166
+ method: "DELETE",
167
+ });
168
+ } catch (error) {
169
+ if (isFlyNotFoundError(error)) {
170
+ return;
171
+ }
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ async cleanupWorkerStorage(input: {
177
+ appName: string;
178
+ workerId: string;
179
+ machineId?: string | null;
180
+ region?: string;
181
+ volumeName: string;
182
+ }): Promise<void> {
183
+ const volumeIds = new Set<string>();
184
+ if (input.machineId) {
185
+ const machineVolumeIds = await this.getMachineVolumeIds(input.appName, input.machineId);
186
+ for (const volumeId of machineVolumeIds) {
187
+ volumeIds.add(volumeId);
188
+ }
189
+ }
190
+
191
+ const expectedVolumeName = buildDedicatedVolumeName(input.volumeName, input.workerId);
192
+ const volumes = await this.request<Array<FlyVolume>>({
193
+ path: `/apps/${encodeURIComponent(input.appName)}/volumes`,
194
+ method: "GET",
160
195
  });
196
+ for (const volume of volumes) {
197
+ if (volume.name !== expectedVolumeName) {
198
+ continue;
199
+ }
200
+ if (input.region && volume.region && volume.region !== input.region) {
201
+ continue;
202
+ }
203
+ volumeIds.add(volume.id);
204
+ }
205
+
161
206
  for (const volumeId of volumeIds) {
162
207
  try {
163
208
  await this.request<void>({
164
- path: `/apps/${encodeURIComponent(appName)}/volumes/${encodeURIComponent(volumeId)}`,
209
+ path: `/apps/${encodeURIComponent(input.appName)}/volumes/${encodeURIComponent(volumeId)}`,
165
210
  method: "DELETE",
166
211
  });
167
212
  } catch (error) {