@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.
- package/README.md +185 -31
- package/dist/client/index.d.ts +11 -6
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +16 -0
- 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 +60 -0
- 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/lib.d.ts +1 -0
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +1 -0
- 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 +32 -16
- package/dist/component/queue.d.ts.map +1 -1
- package/dist/component/queue.js +44 -2
- 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 +59 -12
- package/dist/component/scheduler.js.map +1 -1
- package/dist/component/schema.d.ts +30 -28
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +1 -0
- package/dist/component/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +16 -0
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +72 -0
- package/src/component/flyCleanup.ts +386 -0
- package/src/component/lib.test.ts +280 -4
- package/src/component/lib.ts +1 -0
- package/src/component/providers/fly.ts +39 -5
- package/src/component/queue.ts +55 -2
- package/src/component/scheduler.ts +100 -16
- package/src/component/schema.ts +1 -0
|
@@ -833,6 +833,7 @@ describe("component lib", () => {
|
|
|
833
833
|
machineId,
|
|
834
834
|
appName: TEST_PROVIDER_CONFIG.appName,
|
|
835
835
|
region: TEST_PROVIDER_CONFIG.region,
|
|
836
|
+
volumeId,
|
|
836
837
|
});
|
|
837
838
|
|
|
838
839
|
const completionTime = claimTime + 60_000;
|
|
@@ -926,9 +927,6 @@ describe("component lib", () => {
|
|
|
926
927
|
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "GET") {
|
|
927
928
|
return jsonResponse([]);
|
|
928
929
|
}
|
|
929
|
-
if (url.endsWith(`/machines/machine-orphan-1`) && method === "GET") {
|
|
930
|
-
return new Response("not found", { status: 404 });
|
|
931
|
-
}
|
|
932
930
|
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "GET") {
|
|
933
931
|
return jsonResponse([
|
|
934
932
|
{
|
|
@@ -955,6 +953,7 @@ describe("component lib", () => {
|
|
|
955
953
|
machineId: "machine-orphan-1",
|
|
956
954
|
appName: TEST_PROVIDER_CONFIG.appName,
|
|
957
955
|
region: TEST_PROVIDER_CONFIG.region,
|
|
956
|
+
volumeId,
|
|
958
957
|
});
|
|
959
958
|
|
|
960
959
|
const result = await t.action(api.scheduler.checkIdleShutdowns, {
|
|
@@ -971,12 +970,195 @@ describe("component lib", () => {
|
|
|
971
970
|
((call[1] as RequestInit | undefined)?.method ?? "GET") === "DELETE",
|
|
972
971
|
);
|
|
973
972
|
expect(deleteVolumeCalls).toHaveLength(1);
|
|
973
|
+
const machineDetailCalls = fetchMock.mock.calls.filter(
|
|
974
|
+
(call) =>
|
|
975
|
+
String(call[0]).endsWith(`/machines/machine-orphan-1`) &&
|
|
976
|
+
((call[1] as RequestInit | undefined)?.method ?? "GET") === "GET",
|
|
977
|
+
);
|
|
978
|
+
expect(machineDetailCalls).toHaveLength(0);
|
|
974
979
|
|
|
975
980
|
const workers = await t.query((internal.queue as any).listWorkersForScheduler, {});
|
|
976
981
|
const worker = workers.find((row: { workerId: string }) => row.workerId === workerId);
|
|
977
982
|
expect(worker?.status).toBe("stopped");
|
|
978
983
|
});
|
|
979
984
|
|
|
985
|
+
test("reconcile should persist volumeId on the worker row after spawn", async () => {
|
|
986
|
+
const t = initConvexTest();
|
|
987
|
+
const nowMs = Date.UTC(2026, 0, 2, 9, 0, 0);
|
|
988
|
+
const volumeId = "vol-persist-1";
|
|
989
|
+
const machineId = "machine-persist-1";
|
|
990
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
991
|
+
const url = String(input);
|
|
992
|
+
const method = init?.method ?? "GET";
|
|
993
|
+
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "GET") {
|
|
994
|
+
return jsonResponse(
|
|
995
|
+
fetchMock.mock.calls.some(
|
|
996
|
+
(call) =>
|
|
997
|
+
String(call[0]).endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) &&
|
|
998
|
+
((call[1] as RequestInit | undefined)?.method ?? "GET") === "POST",
|
|
999
|
+
)
|
|
1000
|
+
? [
|
|
1001
|
+
{
|
|
1002
|
+
id: machineId,
|
|
1003
|
+
name: `afw-${nowMs}-0`,
|
|
1004
|
+
region: TEST_PROVIDER_CONFIG.region,
|
|
1005
|
+
state: "started",
|
|
1006
|
+
config: { image: TEST_PROVIDER_CONFIG.image },
|
|
1007
|
+
},
|
|
1008
|
+
]
|
|
1009
|
+
: [],
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "GET") {
|
|
1013
|
+
return jsonResponse([]);
|
|
1014
|
+
}
|
|
1015
|
+
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/volumes`) && method === "POST") {
|
|
1016
|
+
return jsonResponse({
|
|
1017
|
+
id: volumeId,
|
|
1018
|
+
name: buildDedicatedVolumeName(TEST_PROVIDER_CONFIG.volumeName, `afw-${nowMs}-0`),
|
|
1019
|
+
region: TEST_PROVIDER_CONFIG.region,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "POST") {
|
|
1023
|
+
return jsonResponse({
|
|
1024
|
+
id: machineId,
|
|
1025
|
+
region: TEST_PROVIDER_CONFIG.region,
|
|
1026
|
+
state: "started",
|
|
1027
|
+
config: { image: TEST_PROVIDER_CONFIG.image },
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
throw new Error(`Unexpected fetch ${method} ${url}`);
|
|
1031
|
+
});
|
|
1032
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1033
|
+
|
|
1034
|
+
await t.mutation(api.queue.upsertAgentProfile, {
|
|
1035
|
+
agentKey: "support-agent",
|
|
1036
|
+
version: "1.0.0",
|
|
1037
|
+
secretsRef: [],
|
|
1038
|
+
enabled: true,
|
|
1039
|
+
});
|
|
1040
|
+
await t.mutation(api.queue.importPlaintextSecret, {
|
|
1041
|
+
secretRef: "fly.apiToken",
|
|
1042
|
+
plaintextValue: "fly-token",
|
|
1043
|
+
});
|
|
1044
|
+
await t.mutation(api.queue.importPlaintextSecret, {
|
|
1045
|
+
secretRef: "convex.url",
|
|
1046
|
+
plaintextValue: "https://example.convex.cloud",
|
|
1047
|
+
});
|
|
1048
|
+
await t.mutation(api.queue.enqueueMessage, {
|
|
1049
|
+
conversationId: "telegram:chat:persist-volume",
|
|
1050
|
+
agentKey: "support-agent",
|
|
1051
|
+
payload: {
|
|
1052
|
+
provider: "telegram",
|
|
1053
|
+
providerUserId: "u-persist-volume",
|
|
1054
|
+
messageText: "hello",
|
|
1055
|
+
},
|
|
1056
|
+
nowMs,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
await t.action(api.scheduler.reconcileWorkerPool, {
|
|
1060
|
+
nowMs,
|
|
1061
|
+
flyApiToken: "fly-token",
|
|
1062
|
+
convexUrl: "https://example.convex.cloud",
|
|
1063
|
+
scalingPolicy: {
|
|
1064
|
+
maxWorkers: 5,
|
|
1065
|
+
queuePerWorkerTarget: 1,
|
|
1066
|
+
spawnStep: 1,
|
|
1067
|
+
idleTimeoutMs: 300_000,
|
|
1068
|
+
reconcileIntervalMs: 15_000,
|
|
1069
|
+
},
|
|
1070
|
+
providerConfig: TEST_PROVIDER_CONFIG,
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
const workers = await t.query((internal.queue as any).listWorkersForScheduler, {});
|
|
1074
|
+
const worker = workers.find((row: { workerId: string }) => row.workerId === `afw-${nowMs}-0`);
|
|
1075
|
+
expect(worker?.machineId).toBe(machineId);
|
|
1076
|
+
expect(worker?.volumeId).toBe(volumeId);
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
test("runFlyCleanup should destroy machines and volumes with final verification", async () => {
|
|
1080
|
+
const t = initConvexTest();
|
|
1081
|
+
await t.mutation(api.queue.importPlaintextSecret, {
|
|
1082
|
+
secretRef: "fly.apiToken",
|
|
1083
|
+
plaintextValue: "fly-token",
|
|
1084
|
+
});
|
|
1085
|
+
await t.mutation(api.queue.setProviderRuntimeConfig, {
|
|
1086
|
+
providerConfig: TEST_PROVIDER_CONFIG,
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
let machineListCalls = 0;
|
|
1090
|
+
let volumeListCalls = 0;
|
|
1091
|
+
const deletedMachines: Array<string> = [];
|
|
1092
|
+
const deletedVolumes: Array<string> = [];
|
|
1093
|
+
|
|
1094
|
+
vi.stubGlobal(
|
|
1095
|
+
"fetch",
|
|
1096
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
1097
|
+
const url = String(input);
|
|
1098
|
+
const method = init?.method ?? "GET";
|
|
1099
|
+
const appBase = "https://api.machines.dev/v1/apps/agent-factory-workers-test";
|
|
1100
|
+
|
|
1101
|
+
if (url === `${appBase}` && method === "GET") {
|
|
1102
|
+
return jsonResponse({ name: "agent-factory-workers-test" });
|
|
1103
|
+
}
|
|
1104
|
+
if (url === `${appBase}/machines` && method === "GET") {
|
|
1105
|
+
machineListCalls += 1;
|
|
1106
|
+
return jsonResponse(machineListCalls === 1 ? [{ id: "machine-1" }, { id: "machine-2" }] : []);
|
|
1107
|
+
}
|
|
1108
|
+
if (url === `${appBase}/volumes` && method === "GET") {
|
|
1109
|
+
volumeListCalls += 1;
|
|
1110
|
+
return jsonResponse(
|
|
1111
|
+
volumeListCalls === 1
|
|
1112
|
+
? [{ id: "volume-1" }, { id: "volume-2" }, { id: "volume-3" }]
|
|
1113
|
+
: [],
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
if (url.includes("/machines/") && url.endsWith("/cordon") && method === "POST") {
|
|
1117
|
+
return jsonResponse({});
|
|
1118
|
+
}
|
|
1119
|
+
if (url.includes("/machines/") && url.endsWith("/stop") && method === "POST") {
|
|
1120
|
+
return jsonResponse({});
|
|
1121
|
+
}
|
|
1122
|
+
if (url.includes("/machines/") && method === "DELETE") {
|
|
1123
|
+
const segments = url.split("/");
|
|
1124
|
+
deletedMachines.push(segments[segments.length - 1]!);
|
|
1125
|
+
return new Response(null, { status: 204 });
|
|
1126
|
+
}
|
|
1127
|
+
if (url.includes("/volumes/") && method === "DELETE") {
|
|
1128
|
+
const segments = url.split("/");
|
|
1129
|
+
deletedVolumes.push(segments[segments.length - 1]!);
|
|
1130
|
+
return new Response(null, { status: 204 });
|
|
1131
|
+
}
|
|
1132
|
+
throw new Error(`Unexpected fetch ${method} ${url}`);
|
|
1133
|
+
}),
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
const report = await t.action((api.lib as any).runFlyCleanup, {});
|
|
1137
|
+
|
|
1138
|
+
expect(report.appName).toBe("agent-factory-workers-test");
|
|
1139
|
+
expect(report.machinesFound).toBe(2);
|
|
1140
|
+
expect(report.machinesDeleted).toBe(2);
|
|
1141
|
+
expect(report.machinesRemaining).toBe(0);
|
|
1142
|
+
expect(report.volumesFound).toBe(3);
|
|
1143
|
+
expect(report.volumesDeleted).toBe(3);
|
|
1144
|
+
expect(report.volumesRemaining).toBe(0);
|
|
1145
|
+
expect(report.errors).toEqual([]);
|
|
1146
|
+
expect(report.warnings).toEqual([]);
|
|
1147
|
+
expect(deletedMachines.sort()).toEqual(["machine-1", "machine-2"]);
|
|
1148
|
+
expect(deletedVolumes.sort()).toEqual(["volume-1", "volume-2", "volume-3"]);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
test("runFlyCleanup should require an active fly secret", async () => {
|
|
1152
|
+
const t = initConvexTest();
|
|
1153
|
+
await t.mutation(api.queue.setProviderRuntimeConfig, {
|
|
1154
|
+
providerConfig: TEST_PROVIDER_CONFIG,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
await expect(t.action((api.lib as any).runFlyCleanup, {})).rejects.toThrow(
|
|
1158
|
+
"Missing active 'fly.apiToken' secret.",
|
|
1159
|
+
);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
980
1162
|
test("scheduler count includes queued and in-progress conversations", async () => {
|
|
981
1163
|
const t = initConvexTest();
|
|
982
1164
|
await t.mutation(api.queue.upsertAgentProfile, {
|
|
@@ -1248,6 +1430,14 @@ describe("component lib", () => {
|
|
|
1248
1430
|
const t = initConvexTest();
|
|
1249
1431
|
const nowMs = Date.UTC(2026, 0, 1, 17, 0, 0);
|
|
1250
1432
|
vi.setSystemTime(nowMs);
|
|
1433
|
+
let machineCreateBody:
|
|
1434
|
+
| {
|
|
1435
|
+
name?: string;
|
|
1436
|
+
config?: {
|
|
1437
|
+
env?: Record<string, string>;
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
| null = null;
|
|
1251
1441
|
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
1252
1442
|
const url = String(input);
|
|
1253
1443
|
const method = init?.method ?? "GET";
|
|
@@ -1273,7 +1463,13 @@ describe("component lib", () => {
|
|
|
1273
1463
|
});
|
|
1274
1464
|
}
|
|
1275
1465
|
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "POST") {
|
|
1276
|
-
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
|
1466
|
+
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
|
1467
|
+
name?: string;
|
|
1468
|
+
config?: {
|
|
1469
|
+
env?: Record<string, string>;
|
|
1470
|
+
};
|
|
1471
|
+
};
|
|
1472
|
+
machineCreateBody = body;
|
|
1277
1473
|
return jsonResponse({
|
|
1278
1474
|
id: "machine-new-worker",
|
|
1279
1475
|
name: body.name,
|
|
@@ -1349,6 +1545,28 @@ describe("component lib", () => {
|
|
|
1349
1545
|
|
|
1350
1546
|
expect(reconcile.spawned).toBe(1);
|
|
1351
1547
|
expect(reconcile.activeWorkers).toBe(2);
|
|
1548
|
+
const machineCreateEnv = (
|
|
1549
|
+
machineCreateBody as
|
|
1550
|
+
| {
|
|
1551
|
+
config?: {
|
|
1552
|
+
env?: Record<string, string>;
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
| null
|
|
1556
|
+
)?.config?.env;
|
|
1557
|
+
expect(machineCreateEnv?.OPENCLAW_CONVERSATION_ID).toBe("telegram:chat:spawn-b");
|
|
1558
|
+
expect(machineCreateEnv?.OPENCLAW_AGENT_KEY).toBe("support-agent");
|
|
1559
|
+
|
|
1560
|
+
const workers = await t.query((internal.queue as any).listWorkersForScheduler, {});
|
|
1561
|
+
const spawnedWorker = workers.find(
|
|
1562
|
+
(row: { workerId: string }) => row.workerId === `afw-${nowMs}-0`,
|
|
1563
|
+
);
|
|
1564
|
+
expect(spawnedWorker?.assignment).toEqual({
|
|
1565
|
+
conversationId: "telegram:chat:spawn-b",
|
|
1566
|
+
agentKey: "support-agent",
|
|
1567
|
+
leaseId: `spawn:afw-${nowMs}-0`,
|
|
1568
|
+
assignedAt: nowMs,
|
|
1569
|
+
});
|
|
1352
1570
|
});
|
|
1353
1571
|
|
|
1354
1572
|
test("scheduler should forward OpenClaw bridge env to spawned machines", async () => {
|
|
@@ -1623,6 +1841,60 @@ describe("component lib", () => {
|
|
|
1623
1841
|
});
|
|
1624
1842
|
});
|
|
1625
1843
|
|
|
1844
|
+
test("preassigned workers should claim only their assigned conversation on first claim", async () => {
|
|
1845
|
+
const t = initConvexTest();
|
|
1846
|
+
const nowMs = Date.UTC(2026, 0, 1, 17, 35, 0);
|
|
1847
|
+
vi.setSystemTime(nowMs);
|
|
1848
|
+
await t.mutation(api.queue.upsertAgentProfile, {
|
|
1849
|
+
agentKey: "support-agent",
|
|
1850
|
+
version: "1.0.0",
|
|
1851
|
+
secretsRef: [],
|
|
1852
|
+
enabled: true,
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
await t.mutation(api.queue.enqueueMessage, {
|
|
1856
|
+
conversationId: "telegram:chat:preassign-a",
|
|
1857
|
+
agentKey: "support-agent",
|
|
1858
|
+
payload: {
|
|
1859
|
+
provider: "telegram",
|
|
1860
|
+
providerUserId: "u-preassign-a",
|
|
1861
|
+
messageText: "first",
|
|
1862
|
+
},
|
|
1863
|
+
nowMs,
|
|
1864
|
+
});
|
|
1865
|
+
const messageB = await t.mutation(api.queue.enqueueMessage, {
|
|
1866
|
+
conversationId: "telegram:chat:preassign-b",
|
|
1867
|
+
agentKey: "support-agent",
|
|
1868
|
+
payload: {
|
|
1869
|
+
provider: "telegram",
|
|
1870
|
+
providerUserId: "u-preassign-b",
|
|
1871
|
+
messageText: "second",
|
|
1872
|
+
},
|
|
1873
|
+
nowMs: nowMs + 1,
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
await t.mutation(internal.queue.upsertWorkerState, {
|
|
1877
|
+
workerId: "worker-preassigned-1",
|
|
1878
|
+
provider: "fly",
|
|
1879
|
+
status: "active",
|
|
1880
|
+
load: 0,
|
|
1881
|
+
nowMs,
|
|
1882
|
+
assignment: {
|
|
1883
|
+
conversationId: "telegram:chat:preassign-b",
|
|
1884
|
+
agentKey: "support-agent",
|
|
1885
|
+
leaseId: "spawn:worker-preassigned-1",
|
|
1886
|
+
assignedAt: nowMs,
|
|
1887
|
+
},
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
const claim = await t.mutation(api.lib.claim, {
|
|
1891
|
+
workerId: "worker-preassigned-1",
|
|
1892
|
+
nowMs: nowMs + 10,
|
|
1893
|
+
});
|
|
1894
|
+
expect(claim?.messageId).toBe(messageB);
|
|
1895
|
+
expect(claim?.conversationId).toBe("telegram:chat:preassign-b");
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1626
1898
|
test("exclusive ownership should block another worker and let the owner reclaim", async () => {
|
|
1627
1899
|
const t = initConvexTest();
|
|
1628
1900
|
const nowMs = Date.UTC(2026, 0, 1, 17, 40, 0);
|
|
@@ -2465,6 +2737,9 @@ describe("component lib", () => {
|
|
|
2465
2737
|
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "POST") {
|
|
2466
2738
|
throw new Error("simulated spawn failure");
|
|
2467
2739
|
}
|
|
2740
|
+
if (url.endsWith(`/volumes/vol-spawn-failure-1`) && method === "DELETE") {
|
|
2741
|
+
return emptyResponse();
|
|
2742
|
+
}
|
|
2468
2743
|
throw new Error(`Unexpected fetch ${method} ${url}`);
|
|
2469
2744
|
});
|
|
2470
2745
|
vi.stubGlobal("fetch", fetchMock);
|
|
@@ -2514,6 +2789,7 @@ describe("component lib", () => {
|
|
|
2514
2789
|
const worker = workers.find((row: { workerId: string }) => row.workerId === `afw-${nowMs}-0`);
|
|
2515
2790
|
expect(worker?.status).toBe("stopped");
|
|
2516
2791
|
expect(worker?.machineId).toBeNull();
|
|
2792
|
+
expect(worker?.volumeId).toBeNull();
|
|
2517
2793
|
});
|
|
2518
2794
|
|
|
2519
2795
|
test("push jobs should dispatch scheduled messages with a stable user-agent conversation id", async () => {
|
package/src/component/lib.ts
CHANGED
|
@@ -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
|
@@ -188,6 +188,11 @@ const workerSpawnOpenClawEnvValidator = v.object({
|
|
|
188
188
|
OPENCLAW_LINKING_SHARED_SECRET: v.optional(v.string()),
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
const schedulerConversationTargetValidator = v.object({
|
|
192
|
+
conversationId: v.string(),
|
|
193
|
+
agentKey: v.string(),
|
|
194
|
+
});
|
|
195
|
+
|
|
191
196
|
const messageRuntimeConfigValidator = v.object({
|
|
192
197
|
systemPrompt: v.optional(v.string()),
|
|
193
198
|
telegramAttachmentRetentionMs: v.optional(v.number()),
|
|
@@ -489,6 +494,47 @@ export const getWorkerSpawnOpenClawEnv = internalQuery({
|
|
|
489
494
|
},
|
|
490
495
|
});
|
|
491
496
|
|
|
497
|
+
export const getActiveConversationsForScheduler = internalQuery({
|
|
498
|
+
args: {
|
|
499
|
+
nowMs: v.optional(v.number()),
|
|
500
|
+
limit: v.optional(v.number()),
|
|
501
|
+
},
|
|
502
|
+
returns: v.array(schedulerConversationTargetValidator),
|
|
503
|
+
handler: async (ctx, args) => {
|
|
504
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
505
|
+
const limit = Math.max(1, args.limit ?? 1000);
|
|
506
|
+
const queuedJobs = await ctx.db
|
|
507
|
+
.query("messageQueue")
|
|
508
|
+
.withIndex("by_status_and_scheduledFor", (q) =>
|
|
509
|
+
q.eq("status", "queued").lte("scheduledFor", nowMs),
|
|
510
|
+
)
|
|
511
|
+
.take(limit);
|
|
512
|
+
const processingJobs = await ctx.db
|
|
513
|
+
.query("messageQueue")
|
|
514
|
+
.withIndex("by_status_and_leaseExpiresAt", (q) =>
|
|
515
|
+
q.eq("status", "processing").gt("leaseExpiresAt", nowMs),
|
|
516
|
+
)
|
|
517
|
+
.take(limit);
|
|
518
|
+
|
|
519
|
+
const conversations = new Map<string, { conversationId: string; agentKey: string }>();
|
|
520
|
+
for (const job of [...queuedJobs, ...processingJobs]) {
|
|
521
|
+
const key = `${job.agentKey}::${job.conversationId}`;
|
|
522
|
+
if (!conversations.has(key)) {
|
|
523
|
+
conversations.set(key, {
|
|
524
|
+
conversationId: job.conversationId,
|
|
525
|
+
agentKey: job.agentKey,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return Array.from(conversations.values()).sort(
|
|
531
|
+
(left, right) =>
|
|
532
|
+
left.agentKey.localeCompare(right.agentKey) ||
|
|
533
|
+
left.conversationId.localeCompare(right.conversationId),
|
|
534
|
+
);
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
492
538
|
export const getProviderRuntimeConfig = internalQuery({
|
|
493
539
|
args: {},
|
|
494
540
|
returns: v.union(v.null(), providerConfigValidator),
|
|
@@ -2277,8 +2323,11 @@ export const upsertWorkerState = internalMutation({
|
|
|
2277
2323
|
machineId: v.optional(v.string()),
|
|
2278
2324
|
appName: v.optional(v.string()),
|
|
2279
2325
|
region: v.optional(v.string()),
|
|
2326
|
+
volumeId: v.optional(v.string()),
|
|
2327
|
+
assignment: v.optional(v.union(v.null(), workerAssignmentValidator)),
|
|
2280
2328
|
clearLastSnapshotId: v.optional(v.boolean()),
|
|
2281
2329
|
clearMachineRef: v.optional(v.boolean()),
|
|
2330
|
+
clearVolumeId: v.optional(v.boolean()),
|
|
2282
2331
|
},
|
|
2283
2332
|
returns: v.null(),
|
|
2284
2333
|
handler: async (ctx, args) => {
|
|
@@ -2299,7 +2348,8 @@ export const upsertWorkerState = internalMutation({
|
|
|
2299
2348
|
args.status === "stopped" || args.status === "stopping"
|
|
2300
2349
|
? (args.stoppedAt ?? nowMs)
|
|
2301
2350
|
: undefined,
|
|
2302
|
-
|
|
2351
|
+
volumeId: args.volumeId,
|
|
2352
|
+
assignment: args.assignment ?? undefined,
|
|
2303
2353
|
machineRef:
|
|
2304
2354
|
args.machineId && args.appName
|
|
2305
2355
|
? {
|
|
@@ -2329,7 +2379,8 @@ export const upsertWorkerState = internalMutation({
|
|
|
2329
2379
|
? (args.stoppedAt ?? worker.stoppedAt ?? nowMs)
|
|
2330
2380
|
: undefined,
|
|
2331
2381
|
lastSnapshotId: args.clearLastSnapshotId ? undefined : worker.lastSnapshotId,
|
|
2332
|
-
|
|
2382
|
+
volumeId: args.clearVolumeId ? undefined : (args.volumeId ?? worker.volumeId),
|
|
2383
|
+
assignment: args.assignment === undefined ? worker.assignment : (args.assignment ?? undefined),
|
|
2333
2384
|
machineRef:
|
|
2334
2385
|
args.clearMachineRef
|
|
2335
2386
|
? undefined
|
|
@@ -2548,6 +2599,7 @@ export const listWorkersForScheduler = internalQuery({
|
|
|
2548
2599
|
machineId: v.union(v.null(), v.string()),
|
|
2549
2600
|
appName: v.union(v.null(), v.string()),
|
|
2550
2601
|
region: v.union(v.null(), v.string()),
|
|
2602
|
+
volumeId: v.union(v.null(), v.string()),
|
|
2551
2603
|
}),
|
|
2552
2604
|
),
|
|
2553
2605
|
handler: async (ctx) => {
|
|
@@ -2565,6 +2617,7 @@ export const listWorkersForScheduler = internalQuery({
|
|
|
2565
2617
|
machineId: worker.machineRef?.machineId ?? null,
|
|
2566
2618
|
appName: worker.machineRef?.appName ?? null,
|
|
2567
2619
|
region: worker.machineRef?.region ?? null,
|
|
2620
|
+
volumeId: worker.volumeId ?? null,
|
|
2568
2621
|
}));
|
|
2569
2622
|
},
|
|
2570
2623
|
});
|