@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.
Files changed (46) hide show
  1. package/README.md +185 -31
  2. package/dist/client/index.d.ts +11 -6
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +16 -0
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -0
  7. package/dist/component/_generated/api.d.ts.map +1 -1
  8. package/dist/component/_generated/api.js.map +1 -1
  9. package/dist/component/_generated/component.d.ts +60 -0
  10. package/dist/component/_generated/component.d.ts.map +1 -1
  11. package/dist/component/flyCleanup.d.ts +32 -0
  12. package/dist/component/flyCleanup.d.ts.map +1 -0
  13. package/dist/component/flyCleanup.js +272 -0
  14. package/dist/component/flyCleanup.js.map +1 -0
  15. package/dist/component/lib.d.ts +1 -0
  16. package/dist/component/lib.d.ts.map +1 -1
  17. package/dist/component/lib.js +1 -0
  18. package/dist/component/lib.js.map +1 -1
  19. package/dist/component/providers/fly.d.ts +23 -2
  20. package/dist/component/providers/fly.d.ts.map +1 -1
  21. package/dist/component/providers/fly.js +15 -3
  22. package/dist/component/providers/fly.js.map +1 -1
  23. package/dist/component/pushing.d.ts +4 -4
  24. package/dist/component/queue.d.ts +19 -16
  25. package/dist/component/queue.d.ts.map +1 -1
  26. package/dist/component/queue.js +6 -0
  27. package/dist/component/queue.js.map +1 -1
  28. package/dist/component/scheduler.d.ts +8 -8
  29. package/dist/component/scheduler.d.ts.map +1 -1
  30. package/dist/component/scheduler.js +22 -2
  31. package/dist/component/scheduler.js.map +1 -1
  32. package/dist/component/schema.d.ts +30 -28
  33. package/dist/component/schema.d.ts.map +1 -1
  34. package/dist/component/schema.js +1 -0
  35. package/dist/component/schema.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/client/index.ts +16 -0
  38. package/src/component/_generated/api.ts +2 -0
  39. package/src/component/_generated/component.ts +72 -0
  40. package/src/component/flyCleanup.ts +386 -0
  41. package/src/component/lib.test.ts +189 -3
  42. package/src/component/lib.ts +1 -0
  43. package/src/component/providers/fly.ts +39 -5
  44. package/src/component/queue.ts +6 -0
  45. package/src/component/scheduler.ts +23 -2
  46. 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 () => {
@@ -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,
@@ -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);
@@ -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
- 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
  }
@@ -127,6 +127,7 @@ export default defineSchema({
127
127
  workers: defineTable({
128
128
  workerId: v.string(),
129
129
  provider: v.string(),
130
+ volumeId: v.optional(v.string()),
130
131
  machineRef: v.optional(
131
132
  v.object({
132
133
  appName: v.string(),