@okrlinkhub/agent-factory 3.0.2 → 3.1.0

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 (55) hide show
  1. package/README.md +235 -31
  2. package/dist/client/bridge.d.ts +1 -0
  3. package/dist/client/bridge.d.ts.map +1 -1
  4. package/dist/client/bridge.js.map +1 -1
  5. package/dist/client/index.d.ts +29 -3
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/index.js +59 -3
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/component/_generated/api.d.ts +2 -0
  10. package/dist/component/_generated/api.d.ts.map +1 -1
  11. package/dist/component/_generated/api.js.map +1 -1
  12. package/dist/component/_generated/component.d.ts +140 -2
  13. package/dist/component/_generated/component.d.ts.map +1 -1
  14. package/dist/component/flyCleanup.d.ts +32 -0
  15. package/dist/component/flyCleanup.d.ts.map +1 -0
  16. package/dist/component/flyCleanup.js +272 -0
  17. package/dist/component/flyCleanup.js.map +1 -0
  18. package/dist/component/identity.d.ts +60 -2
  19. package/dist/component/identity.d.ts.map +1 -1
  20. package/dist/component/identity.js +372 -32
  21. package/dist/component/identity.js.map +1 -1
  22. package/dist/component/lib.d.ts +2 -1
  23. package/dist/component/lib.d.ts.map +1 -1
  24. package/dist/component/lib.js +2 -1
  25. package/dist/component/lib.js.map +1 -1
  26. package/dist/component/providers/fly.d.ts +23 -2
  27. package/dist/component/providers/fly.d.ts.map +1 -1
  28. package/dist/component/providers/fly.js +15 -3
  29. package/dist/component/providers/fly.js.map +1 -1
  30. package/dist/component/pushing.d.ts +4 -4
  31. package/dist/component/queue.d.ts +12 -7
  32. package/dist/component/queue.d.ts.map +1 -1
  33. package/dist/component/queue.js +9 -0
  34. package/dist/component/queue.js.map +1 -1
  35. package/dist/component/scheduler.d.ts +8 -8
  36. package/dist/component/scheduler.d.ts.map +1 -1
  37. package/dist/component/scheduler.js +22 -2
  38. package/dist/component/scheduler.js.map +1 -1
  39. package/dist/component/schema.d.ts +16 -4
  40. package/dist/component/schema.d.ts.map +1 -1
  41. package/dist/component/schema.js +16 -0
  42. package/dist/component/schema.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/client/bridge.ts +1 -0
  45. package/src/client/index.ts +68 -3
  46. package/src/component/_generated/api.ts +2 -0
  47. package/src/component/_generated/component.ts +188 -8
  48. package/src/component/flyCleanup.ts +386 -0
  49. package/src/component/identity.ts +425 -31
  50. package/src/component/lib.test.ts +197 -3
  51. package/src/component/lib.ts +3 -0
  52. package/src/component/providers/fly.ts +39 -5
  53. package/src/component/queue.ts +11 -0
  54. package/src/component/scheduler.ts +23 -2
  55. package/src/component/schema.ts +16 -0
@@ -487,18 +487,21 @@ describe("component lib", () => {
487
487
  agentKey: "agent-a",
488
488
  version: "1.0.0",
489
489
  secretsRef: [],
490
+ botIdentity: "bot-agent-a",
490
491
  enabled: true,
491
492
  });
492
493
  await t.mutation(api.queue.upsertAgentProfile, {
493
494
  agentKey: "agent-b",
494
495
  version: "1.0.0",
495
496
  secretsRef: [],
497
+ botIdentity: "bot-agent-b",
496
498
  enabled: true,
497
499
  });
498
500
 
499
501
  const first = await t.mutation(api.lib.bindUserAgent, {
500
502
  consumerUserId: "u-1",
501
503
  agentKey: "agent-a",
504
+ botIdentity: "bot-agent-a",
502
505
  source: "telegram_pairing",
503
506
  telegramUserId: "tg-user-1",
504
507
  telegramChatId: "tg-chat-1",
@@ -711,6 +714,7 @@ describe("component lib", () => {
711
714
  appKey: "crm",
712
715
  serviceKey: "abs_live_bridge_key",
713
716
  serviceKeySecretRef: "agent-bridge.serviceKey.bridge-agent",
717
+ botIdentity: null,
714
718
  });
715
719
  });
716
720
 
@@ -833,6 +837,7 @@ describe("component lib", () => {
833
837
  machineId,
834
838
  appName: TEST_PROVIDER_CONFIG.appName,
835
839
  region: TEST_PROVIDER_CONFIG.region,
840
+ volumeId,
836
841
  });
837
842
 
838
843
  const completionTime = claimTime + 60_000;
@@ -926,9 +931,6 @@ describe("component lib", () => {
926
931
  if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "GET") {
927
932
  return jsonResponse([]);
928
933
  }
929
- if (url.endsWith(`/machines/machine-orphan-1`) && method === "GET") {
930
- return new Response("not found", { status: 404 });
931
- }
932
934
  if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "GET") {
933
935
  return jsonResponse([
934
936
  {
@@ -955,6 +957,7 @@ describe("component lib", () => {
955
957
  machineId: "machine-orphan-1",
956
958
  appName: TEST_PROVIDER_CONFIG.appName,
957
959
  region: TEST_PROVIDER_CONFIG.region,
960
+ volumeId,
958
961
  });
959
962
 
960
963
  const result = await t.action(api.scheduler.checkIdleShutdowns, {
@@ -971,12 +974,195 @@ describe("component lib", () => {
971
974
  ((call[1] as RequestInit | undefined)?.method ?? "GET") === "DELETE",
972
975
  );
973
976
  expect(deleteVolumeCalls).toHaveLength(1);
977
+ const machineDetailCalls = fetchMock.mock.calls.filter(
978
+ (call) =>
979
+ String(call[0]).endsWith(`/machines/machine-orphan-1`) &&
980
+ ((call[1] as RequestInit | undefined)?.method ?? "GET") === "GET",
981
+ );
982
+ expect(machineDetailCalls).toHaveLength(0);
974
983
 
975
984
  const workers = await t.query((internal.queue as any).listWorkersForScheduler, {});
976
985
  const worker = workers.find((row: { workerId: string }) => row.workerId === workerId);
977
986
  expect(worker?.status).toBe("stopped");
978
987
  });
979
988
 
989
+ test("reconcile should persist volumeId on the worker row after spawn", async () => {
990
+ const t = initConvexTest();
991
+ const nowMs = Date.UTC(2026, 0, 2, 9, 0, 0);
992
+ const volumeId = "vol-persist-1";
993
+ const machineId = "machine-persist-1";
994
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
995
+ const url = String(input);
996
+ const method = init?.method ?? "GET";
997
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "GET") {
998
+ return jsonResponse(
999
+ fetchMock.mock.calls.some(
1000
+ (call) =>
1001
+ String(call[0]).endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) &&
1002
+ ((call[1] as RequestInit | undefined)?.method ?? "GET") === "POST",
1003
+ )
1004
+ ? [
1005
+ {
1006
+ id: machineId,
1007
+ name: `afw-${nowMs}-0`,
1008
+ region: TEST_PROVIDER_CONFIG.region,
1009
+ state: "started",
1010
+ config: { image: TEST_PROVIDER_CONFIG.image },
1011
+ },
1012
+ ]
1013
+ : [],
1014
+ );
1015
+ }
1016
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "GET") {
1017
+ return jsonResponse([]);
1018
+ }
1019
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "POST") {
1020
+ return jsonResponse({
1021
+ id: volumeId,
1022
+ name: buildDedicatedVolumeName(TEST_PROVIDER_CONFIG.volumeName, `afw-${nowMs}-0`),
1023
+ region: TEST_PROVIDER_CONFIG.region,
1024
+ });
1025
+ }
1026
+ if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "POST") {
1027
+ return jsonResponse({
1028
+ id: machineId,
1029
+ region: TEST_PROVIDER_CONFIG.region,
1030
+ state: "started",
1031
+ config: { image: TEST_PROVIDER_CONFIG.image },
1032
+ });
1033
+ }
1034
+ throw new Error(`Unexpected fetch ${method} ${url}`);
1035
+ });
1036
+ vi.stubGlobal("fetch", fetchMock);
1037
+
1038
+ await t.mutation(api.queue.upsertAgentProfile, {
1039
+ agentKey: "support-agent",
1040
+ version: "1.0.0",
1041
+ secretsRef: [],
1042
+ enabled: true,
1043
+ });
1044
+ await t.mutation(api.queue.importPlaintextSecret, {
1045
+ secretRef: "fly.apiToken",
1046
+ plaintextValue: "fly-token",
1047
+ });
1048
+ await t.mutation(api.queue.importPlaintextSecret, {
1049
+ secretRef: "convex.url",
1050
+ plaintextValue: "https://example.convex.cloud",
1051
+ });
1052
+ await t.mutation(api.queue.enqueueMessage, {
1053
+ conversationId: "telegram:chat:persist-volume",
1054
+ agentKey: "support-agent",
1055
+ payload: {
1056
+ provider: "telegram",
1057
+ providerUserId: "u-persist-volume",
1058
+ messageText: "hello",
1059
+ },
1060
+ nowMs,
1061
+ });
1062
+
1063
+ await t.action(api.scheduler.reconcileWorkerPool, {
1064
+ nowMs,
1065
+ flyApiToken: "fly-token",
1066
+ convexUrl: "https://example.convex.cloud",
1067
+ scalingPolicy: {
1068
+ maxWorkers: 5,
1069
+ queuePerWorkerTarget: 1,
1070
+ spawnStep: 1,
1071
+ idleTimeoutMs: 300_000,
1072
+ reconcileIntervalMs: 15_000,
1073
+ },
1074
+ providerConfig: TEST_PROVIDER_CONFIG,
1075
+ });
1076
+
1077
+ const workers = await t.query((internal.queue as any).listWorkersForScheduler, {});
1078
+ const worker = workers.find((row: { workerId: string }) => row.workerId === `afw-${nowMs}-0`);
1079
+ expect(worker?.machineId).toBe(machineId);
1080
+ expect(worker?.volumeId).toBe(volumeId);
1081
+ });
1082
+
1083
+ test("runFlyCleanup should destroy machines and volumes with final verification", async () => {
1084
+ const t = initConvexTest();
1085
+ await t.mutation(api.queue.importPlaintextSecret, {
1086
+ secretRef: "fly.apiToken",
1087
+ plaintextValue: "fly-token",
1088
+ });
1089
+ await t.mutation(api.queue.setProviderRuntimeConfig, {
1090
+ providerConfig: TEST_PROVIDER_CONFIG,
1091
+ });
1092
+
1093
+ let machineListCalls = 0;
1094
+ let volumeListCalls = 0;
1095
+ const deletedMachines: Array<string> = [];
1096
+ const deletedVolumes: Array<string> = [];
1097
+
1098
+ vi.stubGlobal(
1099
+ "fetch",
1100
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
1101
+ const url = String(input);
1102
+ const method = init?.method ?? "GET";
1103
+ const appBase = "https://api.machines.dev/v1/apps/agent-factory-workers-test";
1104
+
1105
+ if (url === `${appBase}` && method === "GET") {
1106
+ return jsonResponse({ name: "agent-factory-workers-test" });
1107
+ }
1108
+ if (url === `${appBase}/machines` && method === "GET") {
1109
+ machineListCalls += 1;
1110
+ return jsonResponse(machineListCalls === 1 ? [{ id: "machine-1" }, { id: "machine-2" }] : []);
1111
+ }
1112
+ if (url === `${appBase}/volumes` && method === "GET") {
1113
+ volumeListCalls += 1;
1114
+ return jsonResponse(
1115
+ volumeListCalls === 1
1116
+ ? [{ id: "volume-1" }, { id: "volume-2" }, { id: "volume-3" }]
1117
+ : [],
1118
+ );
1119
+ }
1120
+ if (url.includes("/machines/") && url.endsWith("/cordon") && method === "POST") {
1121
+ return jsonResponse({});
1122
+ }
1123
+ if (url.includes("/machines/") && url.endsWith("/stop") && method === "POST") {
1124
+ return jsonResponse({});
1125
+ }
1126
+ if (url.includes("/machines/") && method === "DELETE") {
1127
+ const segments = url.split("/");
1128
+ deletedMachines.push(segments[segments.length - 1]!);
1129
+ return new Response(null, { status: 204 });
1130
+ }
1131
+ if (url.includes("/volumes/") && method === "DELETE") {
1132
+ const segments = url.split("/");
1133
+ deletedVolumes.push(segments[segments.length - 1]!);
1134
+ return new Response(null, { status: 204 });
1135
+ }
1136
+ throw new Error(`Unexpected fetch ${method} ${url}`);
1137
+ }),
1138
+ );
1139
+
1140
+ const report = await t.action((api.lib as any).runFlyCleanup, {});
1141
+
1142
+ expect(report.appName).toBe("agent-factory-workers-test");
1143
+ expect(report.machinesFound).toBe(2);
1144
+ expect(report.machinesDeleted).toBe(2);
1145
+ expect(report.machinesRemaining).toBe(0);
1146
+ expect(report.volumesFound).toBe(3);
1147
+ expect(report.volumesDeleted).toBe(3);
1148
+ expect(report.volumesRemaining).toBe(0);
1149
+ expect(report.errors).toEqual([]);
1150
+ expect(report.warnings).toEqual([]);
1151
+ expect(deletedMachines.sort()).toEqual(["machine-1", "machine-2"]);
1152
+ expect(deletedVolumes.sort()).toEqual(["volume-1", "volume-2", "volume-3"]);
1153
+ });
1154
+
1155
+ test("runFlyCleanup should require an active fly secret", async () => {
1156
+ const t = initConvexTest();
1157
+ await t.mutation(api.queue.setProviderRuntimeConfig, {
1158
+ providerConfig: TEST_PROVIDER_CONFIG,
1159
+ });
1160
+
1161
+ await expect(t.action((api.lib as any).runFlyCleanup, {})).rejects.toThrow(
1162
+ "Missing active 'fly.apiToken' secret.",
1163
+ );
1164
+ });
1165
+
980
1166
  test("scheduler count includes queued and in-progress conversations", async () => {
981
1167
  const t = initConvexTest();
982
1168
  await t.mutation(api.queue.upsertAgentProfile, {
@@ -2555,6 +2741,9 @@ describe("component lib", () => {
2555
2741
  if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "POST") {
2556
2742
  throw new Error("simulated spawn failure");
2557
2743
  }
2744
+ if (url.endsWith(`/volumes/vol-spawn-failure-1`) && method === "DELETE") {
2745
+ return emptyResponse();
2746
+ }
2558
2747
  throw new Error(`Unexpected fetch ${method} ${url}`);
2559
2748
  });
2560
2749
  vi.stubGlobal("fetch", fetchMock);
@@ -2604,6 +2793,7 @@ describe("component lib", () => {
2604
2793
  const worker = workers.find((row: { workerId: string }) => row.workerId === `afw-${nowMs}-0`);
2605
2794
  expect(worker?.status).toBe("stopped");
2606
2795
  expect(worker?.machineId).toBeNull();
2796
+ expect(worker?.volumeId).toBeNull();
2607
2797
  });
2608
2798
 
2609
2799
  test("push jobs should dispatch scheduled messages with a stable user-agent conversation id", async () => {
@@ -2664,11 +2854,13 @@ describe("component lib", () => {
2664
2854
  agentKey: "push-telegram-manual-agent",
2665
2855
  version: "1.0.0",
2666
2856
  secretsRef: [],
2857
+ botIdentity: "push-telegram-manual-bot",
2667
2858
  enabled: true,
2668
2859
  });
2669
2860
  await t.mutation(api.lib.bindUserAgent, {
2670
2861
  consumerUserId: "user-push-telegram-manual",
2671
2862
  agentKey: "push-telegram-manual-agent",
2863
+ botIdentity: "push-telegram-manual-bot",
2672
2864
  source: "telegram_pairing",
2673
2865
  telegramUserId: "tg-user-manual-1",
2674
2866
  telegramChatId: "8246761447",
@@ -2709,11 +2901,13 @@ describe("component lib", () => {
2709
2901
  agentKey: "push-telegram-scheduled-agent",
2710
2902
  version: "1.0.0",
2711
2903
  secretsRef: [],
2904
+ botIdentity: "push-telegram-scheduled-bot",
2712
2905
  enabled: true,
2713
2906
  });
2714
2907
  await t.mutation(api.lib.bindUserAgent, {
2715
2908
  consumerUserId: "user-push-telegram-scheduled",
2716
2909
  agentKey: "push-telegram-scheduled-agent",
2910
+ botIdentity: "push-telegram-scheduled-bot",
2717
2911
  source: "telegram_pairing",
2718
2912
  telegramUserId: "tg-user-scheduled-1",
2719
2913
  telegramChatId: "9988776655",
@@ -34,6 +34,7 @@ export {
34
34
  checkIdleShutdowns,
35
35
  } from "./scheduler.js";
36
36
  export { deleteFlyVolumeManual as deleteFlyVolume } from "./providers/fly.js";
37
+ export { runFlyCleanup } from "./flyCleanup.js";
37
38
 
38
39
  export {
39
40
  bindUserAgent,
@@ -52,12 +53,14 @@ export {
52
53
  createUserAgentPairing,
53
54
  getUserAgentPairingStatus,
54
55
  importTelegramTokenForAgent,
56
+ reconcileTelegramBotIdentityForAgent,
55
57
  getUserAgentOnboardingState,
56
58
  getRequiredSecretRefs,
57
59
  getProviderOperationalReadiness,
58
60
  getTelegramAgentReadiness,
59
61
  getAgentOperationalReadiness,
60
62
  getWebhookReadiness,
63
+ softResetTelegramBindingsMissingBotIdentity,
61
64
  } from "./identity.js";
62
65
 
63
66
  export {
@@ -10,9 +10,8 @@ export type SpawnWorkerInput = {
10
10
  appName: string;
11
11
  image: string;
12
12
  region: string;
13
- volumeName: string;
13
+ volumeId: string;
14
14
  volumePath: string;
15
- volumeSizeGb: number;
16
15
  cpuKind?: string;
17
16
  cpus?: number;
18
17
  memoryMb?: number;
@@ -22,13 +21,27 @@ export type SpawnWorkerInput = {
22
21
  export type ProviderWorker = {
23
22
  workerId: string;
24
23
  machineId: string;
24
+ volumeId?: string;
25
25
  region?: string;
26
26
  image?: string;
27
27
  status: WorkerProviderStatus;
28
28
  rawState?: string;
29
29
  };
30
30
 
31
+ export type WorkerVolume = {
32
+ volumeId: string;
33
+ volumeName: string;
34
+ region?: string;
35
+ };
36
+
31
37
  export interface WorkerProvider {
38
+ ensureWorkerVolume(input: {
39
+ appName: string;
40
+ workerId: string;
41
+ region: string;
42
+ volumeName: string;
43
+ volumeSizeGb: number;
44
+ }): Promise<WorkerVolume>;
32
45
  spawnWorker(input: SpawnWorkerInput): Promise<ProviderWorker>;
33
46
  listWorkers(appName: string): Promise<Array<ProviderWorker>>;
34
47
  terminateWorker(appName: string, machineId: string): Promise<void>;
@@ -40,6 +53,7 @@ export interface WorkerProvider {
40
53
  machineId?: string | null;
41
54
  region?: string;
42
55
  volumeName: string;
56
+ volumeId?: string | null;
43
57
  }): Promise<void>;
44
58
  }
45
59
 
@@ -72,7 +86,14 @@ export class FlyMachinesProvider implements WorkerProvider {
72
86
  private readonly baseUrl: string = "https://api.machines.dev/v1",
73
87
  ) {}
74
88
 
75
- async spawnWorker(input: SpawnWorkerInput): Promise<ProviderWorker> {
89
+ async ensureWorkerVolume(input: {
90
+ appName: string;
91
+ workerId: string;
92
+ region: string;
93
+ volumeName: string;
94
+ volumeSizeGb: number;
95
+ }): Promise<WorkerVolume> {
96
+ const volumeName = buildDedicatedVolumeName(input.volumeName, input.workerId);
76
97
  const volumeId = await this.resolveOrCreateVolumeId(
77
98
  input.appName,
78
99
  input.volumeName,
@@ -80,6 +101,14 @@ export class FlyMachinesProvider implements WorkerProvider {
80
101
  input.region,
81
102
  input.volumeSizeGb,
82
103
  );
104
+ return {
105
+ volumeId,
106
+ volumeName,
107
+ region: input.region,
108
+ };
109
+ }
110
+
111
+ async spawnWorker(input: SpawnWorkerInput): Promise<ProviderWorker> {
83
112
  const payload = {
84
113
  name: input.workerId,
85
114
  region: input.region,
@@ -92,7 +121,7 @@ export class FlyMachinesProvider implements WorkerProvider {
92
121
  },
93
122
  mounts: [
94
123
  {
95
- volume: volumeId,
124
+ volume: input.volumeId,
96
125
  path: input.volumePath,
97
126
  } satisfies FlyMachineMount,
98
127
  ],
@@ -110,6 +139,7 @@ export class FlyMachinesProvider implements WorkerProvider {
110
139
  return {
111
140
  workerId: input.workerId,
112
141
  machineId: machine.id,
142
+ volumeId: input.volumeId,
113
143
  region: machine.region ?? input.region,
114
144
  image: machine.config?.image_ref ?? machine.config?.image ?? input.image,
115
145
  status: mapFlyStateToProviderStatus(machine.state),
@@ -182,9 +212,13 @@ export class FlyMachinesProvider implements WorkerProvider {
182
212
  machineId?: string | null;
183
213
  region?: string;
184
214
  volumeName: string;
215
+ volumeId?: string | null;
185
216
  }): Promise<void> {
186
217
  const volumeIds = new Set<string>();
187
- if (input.machineId) {
218
+ if (input.volumeId) {
219
+ volumeIds.add(input.volumeId);
220
+ }
221
+ if (!input.volumeId && input.machineId) {
188
222
  const machineVolumeIds = await this.getMachineVolumeIds(input.appName, input.machineId);
189
223
  for (const volumeId of machineVolumeIds) {
190
224
  volumeIds.add(volumeId);
@@ -181,6 +181,7 @@ const bridgeRuntimeConfigValidator = v.object({
181
181
  appKey: v.union(v.null(), v.string()),
182
182
  serviceKey: v.union(v.null(), v.string()),
183
183
  serviceKeySecretRef: v.union(v.null(), v.string()),
184
+ botIdentity: v.union(v.null(), v.string()),
184
185
  });
185
186
  const workerSpawnOpenClawEnvValidator = v.object({
186
187
  OPENCLAW_SERVICE_ID: v.optional(v.string()),
@@ -343,6 +344,7 @@ export const upsertAgentProfile = mutation({
343
344
  agentKey: v.string(),
344
345
  version: v.string(),
345
346
  secretsRef: v.array(v.string()),
347
+ botIdentity: v.optional(v.string()),
346
348
  bridgeConfig: v.optional(bridgeProfileConfigValidator),
347
349
  enabled: v.boolean(),
348
350
  },
@@ -2323,9 +2325,11 @@ export const upsertWorkerState = internalMutation({
2323
2325
  machineId: v.optional(v.string()),
2324
2326
  appName: v.optional(v.string()),
2325
2327
  region: v.optional(v.string()),
2328
+ volumeId: v.optional(v.string()),
2326
2329
  assignment: v.optional(v.union(v.null(), workerAssignmentValidator)),
2327
2330
  clearLastSnapshotId: v.optional(v.boolean()),
2328
2331
  clearMachineRef: v.optional(v.boolean()),
2332
+ clearVolumeId: v.optional(v.boolean()),
2329
2333
  },
2330
2334
  returns: v.null(),
2331
2335
  handler: async (ctx, args) => {
@@ -2346,6 +2350,7 @@ export const upsertWorkerState = internalMutation({
2346
2350
  args.status === "stopped" || args.status === "stopping"
2347
2351
  ? (args.stoppedAt ?? nowMs)
2348
2352
  : undefined,
2353
+ volumeId: args.volumeId,
2349
2354
  assignment: args.assignment ?? undefined,
2350
2355
  machineRef:
2351
2356
  args.machineId && args.appName
@@ -2376,6 +2381,7 @@ export const upsertWorkerState = internalMutation({
2376
2381
  ? (args.stoppedAt ?? worker.stoppedAt ?? nowMs)
2377
2382
  : undefined,
2378
2383
  lastSnapshotId: args.clearLastSnapshotId ? undefined : worker.lastSnapshotId,
2384
+ volumeId: args.clearVolumeId ? undefined : (args.volumeId ?? worker.volumeId),
2379
2385
  assignment: args.assignment === undefined ? worker.assignment : (args.assignment ?? undefined),
2380
2386
  machineRef:
2381
2387
  args.clearMachineRef
@@ -2595,6 +2601,7 @@ export const listWorkersForScheduler = internalQuery({
2595
2601
  machineId: v.union(v.null(), v.string()),
2596
2602
  appName: v.union(v.null(), v.string()),
2597
2603
  region: v.union(v.null(), v.string()),
2604
+ volumeId: v.union(v.null(), v.string()),
2598
2605
  }),
2599
2606
  ),
2600
2607
  handler: async (ctx) => {
@@ -2612,6 +2619,7 @@ export const listWorkersForScheduler = internalQuery({
2612
2619
  machineId: worker.machineRef?.machineId ?? null,
2613
2620
  appName: worker.machineRef?.appName ?? null,
2614
2621
  region: worker.machineRef?.region ?? null,
2622
+ volumeId: worker.volumeId ?? null,
2615
2623
  }));
2616
2624
  },
2617
2625
  });
@@ -2725,6 +2733,7 @@ async function resolveBridgeRuntimeConfig(
2725
2733
  ctx: any,
2726
2734
  profile: {
2727
2735
  agentKey: string;
2736
+ botIdentity?: string;
2728
2737
  bridgeConfig?: {
2729
2738
  enabled: boolean;
2730
2739
  baseUrl?: string;
@@ -2741,6 +2750,7 @@ async function resolveBridgeRuntimeConfig(
2741
2750
  appKey: string | null;
2742
2751
  serviceKey: string | null;
2743
2752
  serviceKeySecretRef: string | null;
2753
+ botIdentity: string | null;
2744
2754
  } | null> {
2745
2755
  if (!profile.bridgeConfig?.enabled) {
2746
2756
  return null;
@@ -2785,6 +2795,7 @@ async function resolveBridgeRuntimeConfig(
2785
2795
  appKey: profile.bridgeConfig.appKey ?? appKeyFromSecret,
2786
2796
  serviceKey,
2787
2797
  serviceKeySecretRef,
2798
+ botIdentity: profile.botIdentity ?? null,
2788
2799
  };
2789
2800
  }
2790
2801
 
@@ -73,6 +73,7 @@ type SchedulerWorkerRow = {
73
73
  machineId: string | null;
74
74
  appName: string | null;
75
75
  region: string | null;
76
+ volumeId: string | null;
76
77
  };
77
78
 
78
79
  type SchedulerConversationTarget = {
@@ -372,6 +373,13 @@ async function runWorkerLifecycleCycle(
372
373
  assignedAt: input.nowMs,
373
374
  }
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
+ });
375
383
  await ctx.runMutation(internal.queue.upsertWorkerState, {
376
384
  workerId,
377
385
  provider: input.providerConfig.kind,
@@ -379,6 +387,7 @@ async function runWorkerLifecycleCycle(
379
387
  load: 0,
380
388
  nowMs: input.nowMs,
381
389
  scheduledShutdownAt: input.nowMs + input.scaling.idleTimeoutMs,
390
+ volumeId: workerVolume.volumeId,
382
391
  assignment,
383
392
  });
384
393
  let created;
@@ -388,9 +397,8 @@ async function runWorkerLifecycleCycle(
388
397
  appName: input.providerConfig.appName,
389
398
  image: input.providerConfig.image,
390
399
  region: input.providerConfig.region,
391
- volumeName: input.providerConfig.volumeName,
400
+ volumeId: workerVolume.volumeId,
392
401
  volumePath: input.providerConfig.volumePath,
393
- volumeSizeGb: input.providerConfig.volumeSizeGb,
394
402
  env: compactEnv({
395
403
  ...DEFAULT_WORKER_RUNTIME_ENV,
396
404
  ...forwardedOpenClawEnv,
@@ -408,6 +416,13 @@ async function runWorkerLifecycleCycle(
408
416
  error instanceof Error ? error.message : String(error)
409
417
  }`,
410
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
+ });
411
426
  await transitionWorkerToDraining(
412
427
  ctx,
413
428
  {
@@ -423,6 +438,7 @@ async function runWorkerLifecycleCycle(
423
438
  machineId: null,
424
439
  appName: input.providerConfig.appName,
425
440
  region: input.providerConfig.region,
441
+ volumeId: workerVolume.volumeId,
426
442
  },
427
443
  input.providerConfig,
428
444
  input.nowMs,
@@ -443,6 +459,7 @@ async function runWorkerLifecycleCycle(
443
459
  machineId: null,
444
460
  appName: input.providerConfig.appName,
445
461
  region: input.providerConfig.region,
462
+ volumeId: workerVolume.volumeId,
446
463
  },
447
464
  input.providerConfig,
448
465
  input.nowMs,
@@ -463,6 +480,7 @@ async function runWorkerLifecycleCycle(
463
480
  machineId: null,
464
481
  appName: input.providerConfig.appName,
465
482
  region: input.providerConfig.region,
483
+ volumeId: workerVolume.volumeId,
466
484
  },
467
485
  input.providerConfig,
468
486
  input.nowMs,
@@ -479,6 +497,7 @@ async function runWorkerLifecycleCycle(
479
497
  machineId: created.machineId,
480
498
  appName: input.providerConfig.appName,
481
499
  region: created.region,
500
+ volumeId: created.volumeId ?? workerVolume.volumeId,
482
501
  assignment,
483
502
  });
484
503
  await scheduleIdleShutdownWatch(
@@ -679,6 +698,7 @@ async function transitionWorkerToStopped(
679
698
  scheduledShutdownAt: worker.scheduledShutdownAt ?? nowMs,
680
699
  stoppedAt: worker.stoppedAt ?? nowMs,
681
700
  clearMachineRef: true,
701
+ clearVolumeId: true,
682
702
  });
683
703
  }
684
704
 
@@ -706,6 +726,7 @@ async function finalizeWorkerTeardown(input: {
706
726
  machineId,
707
727
  region: input.worker.region ?? input.providerConfig.region,
708
728
  volumeName: input.providerConfig.volumeName,
729
+ volumeId: input.worker.volumeId,
709
730
  });
710
731
  return true;
711
732
  }
@@ -7,6 +7,7 @@ export default defineSchema({
7
7
  agentKey: v.string(),
8
8
  version: v.string(),
9
9
  secretsRef: v.array(v.string()),
10
+ botIdentity: v.optional(v.string()),
10
11
  bridgeConfig: v.optional(
11
12
  v.object({
12
13
  enabled: v.boolean(),
@@ -20,6 +21,7 @@ export default defineSchema({
20
21
  enabled: v.boolean(),
21
22
  })
22
23
  .index("by_agentKey", ["agentKey"])
24
+ .index("by_botIdentity", ["botIdentity"])
23
25
  .index("by_enabled", ["enabled"]),
24
26
 
25
27
  conversations: defineTable({
@@ -127,6 +129,7 @@ export default defineSchema({
127
129
  workers: defineTable({
128
130
  workerId: v.string(),
129
131
  provider: v.string(),
132
+ volumeId: v.optional(v.string()),
130
133
  machineRef: v.optional(
131
134
  v.object({
132
135
  appName: v.string(),
@@ -255,6 +258,7 @@ export default defineSchema({
255
258
  consumerUserId: v.string(),
256
259
  agentKey: v.string(),
257
260
  conversationId: v.string(),
261
+ botIdentity: v.optional(v.string()),
258
262
  status: v.union(v.literal("active"), v.literal("revoked")),
259
263
  source: v.union(
260
264
  v.literal("manual"),
@@ -273,6 +277,16 @@ export default defineSchema({
273
277
  "agentKey",
274
278
  "boundAt",
275
279
  ])
280
+ .index("by_botIdentity_and_telegramUserId_and_status", [
281
+ "botIdentity",
282
+ "telegramUserId",
283
+ "status",
284
+ ])
285
+ .index("by_botIdentity_and_telegramChatId_and_status", [
286
+ "botIdentity",
287
+ "telegramChatId",
288
+ "status",
289
+ ])
276
290
  .index("by_telegramUserId_and_status", ["telegramUserId", "status"])
277
291
  .index("by_telegramChatId_and_status", ["telegramChatId", "status"])
278
292
  .index("by_agentKey_and_status", ["agentKey", "status"]),
@@ -281,6 +295,7 @@ export default defineSchema({
281
295
  code: v.string(),
282
296
  consumerUserId: v.string(),
283
297
  agentKey: v.string(),
298
+ botIdentity: v.optional(v.string()),
284
299
  status: v.union(v.literal("pending"), v.literal("used"), v.literal("expired")),
285
300
  createdAt: v.number(),
286
301
  expiresAt: v.number(),
@@ -295,6 +310,7 @@ export default defineSchema({
295
310
  "agentKey",
296
311
  "createdAt",
297
312
  ])
313
+ .index("by_botIdentity_and_status", ["botIdentity", "status"])
298
314
  .index("by_expiresAt", ["expiresAt"]),
299
315
 
300
316
  globalSkills: defineTable({