@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.
- package/README.md +235 -31
- package/dist/client/bridge.d.ts +1 -0
- package/dist/client/bridge.d.ts.map +1 -1
- package/dist/client/bridge.js.map +1 -1
- package/dist/client/index.d.ts +29 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +59 -3
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +140 -2
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/flyCleanup.d.ts +32 -0
- package/dist/component/flyCleanup.d.ts.map +1 -0
- package/dist/component/flyCleanup.js +272 -0
- package/dist/component/flyCleanup.js.map +1 -0
- package/dist/component/identity.d.ts +60 -2
- package/dist/component/identity.d.ts.map +1 -1
- package/dist/component/identity.js +372 -32
- package/dist/component/identity.js.map +1 -1
- package/dist/component/lib.d.ts +2 -1
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +2 -1
- package/dist/component/lib.js.map +1 -1
- package/dist/component/providers/fly.d.ts +23 -2
- package/dist/component/providers/fly.d.ts.map +1 -1
- package/dist/component/providers/fly.js +15 -3
- package/dist/component/providers/fly.js.map +1 -1
- package/dist/component/pushing.d.ts +4 -4
- package/dist/component/queue.d.ts +12 -7
- package/dist/component/queue.d.ts.map +1 -1
- package/dist/component/queue.js +9 -0
- package/dist/component/queue.js.map +1 -1
- package/dist/component/scheduler.d.ts +8 -8
- package/dist/component/scheduler.d.ts.map +1 -1
- package/dist/component/scheduler.js +22 -2
- package/dist/component/scheduler.js.map +1 -1
- package/dist/component/schema.d.ts +16 -4
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +16 -0
- package/dist/component/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/client/bridge.ts +1 -0
- package/src/client/index.ts +68 -3
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +188 -8
- package/src/component/flyCleanup.ts +386 -0
- package/src/component/identity.ts +425 -31
- package/src/component/lib.test.ts +197 -3
- package/src/component/lib.ts +3 -0
- package/src/component/providers/fly.ts +39 -5
- package/src/component/queue.ts +11 -0
- package/src/component/scheduler.ts +23 -2
- 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",
|
package/src/component/lib.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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);
|
package/src/component/queue.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/component/schema.ts
CHANGED
|
@@ -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({
|