@okrlinkhub/agent-factory 3.0.2 → 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 +19 -16
- package/dist/component/queue.d.ts.map +1 -1
- package/dist/component/queue.js +6 -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 +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 +189 -3
- package/src/component/lib.ts +1 -0
- package/src/component/providers/fly.ts +39 -5
- package/src/component/queue.ts +6 -0
- package/src/component/scheduler.ts +23 -2
- 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, {
|
|
@@ -2555,6 +2737,9 @@ describe("component lib", () => {
|
|
|
2555
2737
|
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "POST") {
|
|
2556
2738
|
throw new Error("simulated spawn failure");
|
|
2557
2739
|
}
|
|
2740
|
+
if (url.endsWith(`/volumes/vol-spawn-failure-1`) && method === "DELETE") {
|
|
2741
|
+
return emptyResponse();
|
|
2742
|
+
}
|
|
2558
2743
|
throw new Error(`Unexpected fetch ${method} ${url}`);
|
|
2559
2744
|
});
|
|
2560
2745
|
vi.stubGlobal("fetch", fetchMock);
|
|
@@ -2604,6 +2789,7 @@ describe("component lib", () => {
|
|
|
2604
2789
|
const worker = workers.find((row: { workerId: string }) => row.workerId === `afw-${nowMs}-0`);
|
|
2605
2790
|
expect(worker?.status).toBe("stopped");
|
|
2606
2791
|
expect(worker?.machineId).toBeNull();
|
|
2792
|
+
expect(worker?.volumeId).toBeNull();
|
|
2607
2793
|
});
|
|
2608
2794
|
|
|
2609
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
|
@@ -2323,9 +2323,11 @@ export const upsertWorkerState = internalMutation({
|
|
|
2323
2323
|
machineId: v.optional(v.string()),
|
|
2324
2324
|
appName: v.optional(v.string()),
|
|
2325
2325
|
region: v.optional(v.string()),
|
|
2326
|
+
volumeId: v.optional(v.string()),
|
|
2326
2327
|
assignment: v.optional(v.union(v.null(), workerAssignmentValidator)),
|
|
2327
2328
|
clearLastSnapshotId: v.optional(v.boolean()),
|
|
2328
2329
|
clearMachineRef: v.optional(v.boolean()),
|
|
2330
|
+
clearVolumeId: v.optional(v.boolean()),
|
|
2329
2331
|
},
|
|
2330
2332
|
returns: v.null(),
|
|
2331
2333
|
handler: async (ctx, args) => {
|
|
@@ -2346,6 +2348,7 @@ export const upsertWorkerState = internalMutation({
|
|
|
2346
2348
|
args.status === "stopped" || args.status === "stopping"
|
|
2347
2349
|
? (args.stoppedAt ?? nowMs)
|
|
2348
2350
|
: undefined,
|
|
2351
|
+
volumeId: args.volumeId,
|
|
2349
2352
|
assignment: args.assignment ?? undefined,
|
|
2350
2353
|
machineRef:
|
|
2351
2354
|
args.machineId && args.appName
|
|
@@ -2376,6 +2379,7 @@ export const upsertWorkerState = internalMutation({
|
|
|
2376
2379
|
? (args.stoppedAt ?? worker.stoppedAt ?? nowMs)
|
|
2377
2380
|
: undefined,
|
|
2378
2381
|
lastSnapshotId: args.clearLastSnapshotId ? undefined : worker.lastSnapshotId,
|
|
2382
|
+
volumeId: args.clearVolumeId ? undefined : (args.volumeId ?? worker.volumeId),
|
|
2379
2383
|
assignment: args.assignment === undefined ? worker.assignment : (args.assignment ?? undefined),
|
|
2380
2384
|
machineRef:
|
|
2381
2385
|
args.clearMachineRef
|
|
@@ -2595,6 +2599,7 @@ export const listWorkersForScheduler = internalQuery({
|
|
|
2595
2599
|
machineId: v.union(v.null(), v.string()),
|
|
2596
2600
|
appName: v.union(v.null(), v.string()),
|
|
2597
2601
|
region: v.union(v.null(), v.string()),
|
|
2602
|
+
volumeId: v.union(v.null(), v.string()),
|
|
2598
2603
|
}),
|
|
2599
2604
|
),
|
|
2600
2605
|
handler: async (ctx) => {
|
|
@@ -2612,6 +2617,7 @@ export const listWorkersForScheduler = internalQuery({
|
|
|
2612
2617
|
machineId: worker.machineRef?.machineId ?? null,
|
|
2613
2618
|
appName: worker.machineRef?.appName ?? null,
|
|
2614
2619
|
region: worker.machineRef?.region ?? null,
|
|
2620
|
+
volumeId: worker.volumeId ?? null,
|
|
2615
2621
|
}));
|
|
2616
2622
|
},
|
|
2617
2623
|
});
|
|
@@ -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