From d7c6482e4ea8da67bef7d2dc3faac59dd4426d75 Mon Sep 17 00:00:00 2001 From: syn Date: Tue, 24 Feb 2026 08:58:31 -0600 Subject: [PATCH 1/4] fix(kiloclaw): persist and display actual volume size, bump default to 20GB - Add volumeSizeGb to DO persisted state schema and getStatus response - Backfill from Fly API via fire-and-forget in getStatus() and reconcileVolume() - Persist volume size on all 4 creation paths (provision, ensureVolume, fork, replace) - Display real volume size in dashboard badge (fallback 10GB before backfill) - Bump DEFAULT_VOLUME_SIZE_GB from 10 to 20 for new provisions --- kiloclaw/src/config.ts | 2 +- .../src/durable-objects/kiloclaw-instance.ts | 56 +++++++++++++++++-- kiloclaw/src/schemas/instance-config.ts | 2 + .../claw/components/InstanceControls.tsx | 2 +- .../(app)/claw/components/changelog-data.ts | 6 ++ .../withStatusQueryBoundary.test.ts | 1 + src/lib/kiloclaw/types.ts | 1 + 7 files changed, 64 insertions(+), 6 deletions(-) diff --git a/kiloclaw/src/config.ts b/kiloclaw/src/config.ts index 715d4404d..e98b3b0ea 100644 --- a/kiloclaw/src/config.ts +++ b/kiloclaw/src/config.ts @@ -29,7 +29,7 @@ export const DEFAULT_MACHINE_GUEST = { }; /** Default Fly Volume size in GB */ -export const DEFAULT_VOLUME_SIZE_GB = 10; +export const DEFAULT_VOLUME_SIZE_GB = 20; /** Default Fly region priority list when FLY_REGION env var is not set */ export const DEFAULT_FLY_REGION = 'dfw,yyz,cdg'; diff --git a/kiloclaw/src/durable-objects/kiloclaw-instance.ts b/kiloclaw/src/durable-objects/kiloclaw-instance.ts index 7a229ce8b..026b14a40 100644 --- a/kiloclaw/src/durable-objects/kiloclaw-instance.ts +++ b/kiloclaw/src/durable-objects/kiloclaw-instance.ts @@ -343,6 +343,7 @@ export class KiloClawInstance extends DurableObject { private flyMachineId: string | null = null; private flyVolumeId: string | null = null; private flyRegion: string | null = null; + private volumeSizeGb: number | null = null; private machineSize: MachineSize | null = null; private healthCheckFailCount = 0; private pendingDestroyMachineId: string | null = null; @@ -383,6 +384,7 @@ export class KiloClawInstance extends DurableObject { this.flyMachineId = s.flyMachineId; this.flyVolumeId = s.flyVolumeId; this.flyRegion = s.flyRegion; + this.volumeSizeGb = s.volumeSizeGb; this.machineSize = s.machineSize; this.healthCheckFailCount = s.healthCheckFailCount; this.pendingDestroyMachineId = s.pendingDestroyMachineId; @@ -452,6 +454,7 @@ export class KiloClawInstance extends DurableObject { ); this.flyVolumeId = volume.id; this.flyRegion = volume.region; + this.volumeSizeGb = volume.size_gb; console.log('[DO] Created Fly Volume:', volume.id, 'region:', volume.region); } @@ -500,6 +503,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: this.flyMachineId, flyVolumeId: this.flyVolumeId, flyRegion: this.flyRegion, + volumeSizeGb: this.volumeSizeGb, healthCheckFailCount: 0, pendingDestroyMachineId: null, pendingDestroyVolumeId: null, @@ -1190,6 +1194,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: string | null; flyVolumeId: string | null; flyRegion: string | null; + volumeSizeGb: number | null; openclawVersion: string | null; imageVariant: string | null; trackedImageTag: string | null; @@ -1209,6 +1214,12 @@ export class KiloClawInstance extends DurableObject { this.ctx.waitUntil(this.syncStatusFromLiveCheck()); } + // Fire-and-forget backfill: if we have a volume but no cached size, + // fetch it from the Fly API so the next poll returns the real value. + if (this.volumeSizeGb === null && this.flyVolumeId) { + this.ctx.waitUntil(this.backfillVolumeSizeGb()); + } + return { userId: this.userId, sandboxId: this.sandboxId, @@ -1223,6 +1234,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: this.flyMachineId, flyVolumeId: this.flyVolumeId, flyRegion: this.flyRegion, + volumeSizeGb: this.volumeSizeGb, openclawVersion: this.openclawVersion, imageVariant: this.imageVariant, trackedImageTag: this.trackedImageTag, @@ -1468,9 +1480,13 @@ export class KiloClawInstance extends DurableObject { return; } - // Verify volume still exists on Fly + // Verify volume still exists on Fly (and backfill size if missing) try { - await fly.getVolume(flyConfig, this.flyVolumeId); + const volume = await fly.getVolume(flyConfig, this.flyVolumeId); + if (this.volumeSizeGb === null) { + this.volumeSizeGb = volume.size_gb; + await this.ctx.storage.put(storageUpdate({ volumeSizeGb: volume.size_gb })); + } } catch (err) { if (fly.isFlyNotFound(err)) { reconcileLog(reason, 'replace_lost_volume', { @@ -1684,6 +1700,25 @@ export class KiloClawInstance extends DurableObject { } } + /** + * Fire-and-forget backfill for volumeSizeGb. Called from getStatus() and + * reconcileVolume() when the field is null but a volume exists. + */ + private async backfillVolumeSizeGb(): Promise { + if (!this.flyVolumeId) return; + + try { + const flyConfig = this.getFlyConfig(); + const volume = await fly.getVolume(flyConfig, this.flyVolumeId); + this.volumeSizeGb = volume.size_gb; + await this.ctx.storage.put(storageUpdate({ volumeSizeGb: volume.size_gb })); + console.log('[DO] Backfilled volumeSizeGb:', volume.size_gb); + } catch (err) { + // Non-fatal — will retry on next getStatus() or reconciliation cycle + console.warn('[DO] Failed to backfill volumeSizeGb:', err); + } + } + /** * Check that a running machine has the correct volume mount. * If the mount is wrong/missing, repair via stop → update → start. @@ -1930,7 +1965,14 @@ export class KiloClawInstance extends DurableObject { this.flyVolumeId = volume.id; this.flyRegion = volume.region; - await this.ctx.storage.put(storageUpdate({ flyVolumeId: volume.id, flyRegion: volume.region })); + this.volumeSizeGb = volume.size_gb; + await this.ctx.storage.put( + storageUpdate({ + flyVolumeId: volume.id, + flyRegion: volume.region, + volumeSizeGb: volume.size_gb, + }) + ); reconcileLog(reason, 'create_volume', { volume_id: volume.id, @@ -1996,6 +2038,7 @@ export class KiloClawInstance extends DurableObject { ); this.flyVolumeId = forkedVolume.id; this.flyRegion = forkedVolume.region; + this.volumeSizeGb = forkedVolume.size_gb; reconcileLog(reason, 'fork_stranded_volume', { old_volume_id: oldVolumeId, old_region: oldRegion, @@ -2019,6 +2062,7 @@ export class KiloClawInstance extends DurableObject { ); this.flyVolumeId = freshVolume.id; this.flyRegion = freshVolume.region; + this.volumeSizeGb = freshVolume.size_gb; reconcileLog(reason, 'create_replacement_volume', { old_volume_id: oldVolumeId, old_region: oldRegion, @@ -2029,7 +2073,11 @@ export class KiloClawInstance extends DurableObject { // Persist new volume state await this.ctx.storage.put( - storageUpdate({ flyVolumeId: this.flyVolumeId, flyRegion: this.flyRegion }) + storageUpdate({ + flyVolumeId: this.flyVolumeId, + flyRegion: this.flyRegion, + volumeSizeGb: this.volumeSizeGb, + }) ); // Delete old volume (best-effort cleanup) diff --git a/kiloclaw/src/schemas/instance-config.ts b/kiloclaw/src/schemas/instance-config.ts index bc9b376db..ef995522c 100644 --- a/kiloclaw/src/schemas/instance-config.ts +++ b/kiloclaw/src/schemas/instance-config.ts @@ -118,6 +118,8 @@ export const PersistedStateSchema = z.object({ flyMachineId: z.string().nullable().default(null), flyVolumeId: z.string().nullable().default(null), flyRegion: z.string().nullable().default(null), + // Actual provisioned volume size in GB (backfilled from Fly API for existing instances) + volumeSizeGb: z.number().nullable().default(null), machineSize: MachineSizeSchema.nullable().default(null), // Health check tracking healthCheckFailCount: z.number().default(0), diff --git a/src/app/(app)/claw/components/InstanceControls.tsx b/src/app/(app)/claw/components/InstanceControls.tsx index 0400c10ff..5bee49a77 100644 --- a/src/app/(app)/claw/components/InstanceControls.tsx +++ b/src/app/(app)/claw/components/InstanceControls.tsx @@ -43,7 +43,7 @@ export function InstanceControls({ - 20 GB SSD + {status.volumeSizeGb ?? 10} GB SSD diff --git a/src/app/(app)/claw/components/changelog-data.ts b/src/app/(app)/claw/components/changelog-data.ts index 64872a8b5..0762b96d5 100644 --- a/src/app/(app)/claw/components/changelog-data.ts +++ b/src/app/(app)/claw/components/changelog-data.ts @@ -10,6 +10,12 @@ export type ChangelogEntry = { // Newest entries first. Developers add new entries to the top of this array. export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ + { + date: '2026-02-24', + description: 'Fix instance type and size badges.', + category: 'bugfix', + deployHint: null, + }, { date: '2026-02-23', description: diff --git a/src/app/(app)/claw/components/withStatusQueryBoundary.test.ts b/src/app/(app)/claw/components/withStatusQueryBoundary.test.ts index 0bac5266d..e4b484666 100644 --- a/src/app/(app)/claw/components/withStatusQueryBoundary.test.ts +++ b/src/app/(app)/claw/components/withStatusQueryBoundary.test.ts @@ -18,6 +18,7 @@ const baseStatus: KiloClawDashboardStatus = { flyMachineId: null, flyVolumeId: null, flyRegion: null, + volumeSizeGb: null, gatewayToken: 'token', workerUrl: 'https://claw.kilo.ai', }; diff --git a/src/lib/kiloclaw/types.ts b/src/lib/kiloclaw/types.ts index f3a4e0383..32b8c4645 100644 --- a/src/lib/kiloclaw/types.ts +++ b/src/lib/kiloclaw/types.ts @@ -109,6 +109,7 @@ export type PlatformStatusResponse = { flyMachineId: string | null; flyVolumeId: string | null; flyRegion: string | null; + volumeSizeGb: number | null; }; /** A Fly volume snapshot. */ From e6da0433f0cbf292194478fa8f94a1369f6d974f Mon Sep 17 00:00:00 2001 From: syn Date: Tue, 24 Feb 2026 09:13:57 -0600 Subject: [PATCH 2/4] fix(kiloclaw): add backfill throttle and null volumeSizeGb when volume is cleared - Add backfillingVolumeSize in-flight guard to prevent duplicate Fly API calls - Clear volumeSizeGb at all 6 sites where flyVolumeId is set to null (start 404, reconcileVolume 404, tryDeleteVolume, finalizeDestroy, replaceStrandedVolume, Postgres restore) --- .../src/durable-objects/kiloclaw-instance.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/kiloclaw/src/durable-objects/kiloclaw-instance.ts b/kiloclaw/src/durable-objects/kiloclaw-instance.ts index 026b14a40..565ec6f25 100644 --- a/kiloclaw/src/durable-objects/kiloclaw-instance.ts +++ b/kiloclaw/src/durable-objects/kiloclaw-instance.ts @@ -355,6 +355,7 @@ export class KiloClawInstance extends DurableObject { // In-memory only (not persisted to SQLite) — throttles live Fly checks in getStatus() private lastLiveCheckAt: number | null = null; + private backfillingVolumeSize = false; // ---- State loading ---- @@ -999,7 +1000,10 @@ export class KiloClawInstance extends DurableObject { console.warn('[DO] Volume not found during region check, clearing'); this.flyVolumeId = null; this.flyRegion = null; - await this.ctx.storage.put(storageUpdate({ flyVolumeId: null, flyRegion: null })); + this.volumeSizeGb = null; + await this.ctx.storage.put( + storageUpdate({ flyVolumeId: null, flyRegion: null, volumeSizeGb: null }) + ); await this.ensureVolume(flyConfig, 'start'); } // Other errors: proceed with cached region, createMachine will fail @@ -1216,8 +1220,13 @@ export class KiloClawInstance extends DurableObject { // Fire-and-forget backfill: if we have a volume but no cached size, // fetch it from the Fly API so the next poll returns the real value. - if (this.volumeSizeGb === null && this.flyVolumeId) { - this.ctx.waitUntil(this.backfillVolumeSizeGb()); + if (this.volumeSizeGb === null && this.flyVolumeId && !this.backfillingVolumeSize) { + this.backfillingVolumeSize = true; + this.ctx.waitUntil( + this.backfillVolumeSizeGb().finally(() => { + this.backfillingVolumeSize = false; + }) + ); } return { @@ -1494,7 +1503,8 @@ export class KiloClawInstance extends DurableObject { old_volume_id: this.flyVolumeId, }); this.flyVolumeId = null; - await this.ctx.storage.put(storageUpdate({ flyVolumeId: null })); + this.volumeSizeGb = null; + await this.ctx.storage.put(storageUpdate({ flyVolumeId: null, volumeSizeGb: null })); await this.ensureVolume(flyConfig, reason); } // Other errors: leave as-is, retry next alarm @@ -1847,7 +1857,10 @@ export class KiloClawInstance extends DurableObject { // Success or 404: clear this.pendingDestroyVolumeId = null; this.flyVolumeId = null; - await this.ctx.storage.put(storageUpdate({ pendingDestroyVolumeId: null, flyVolumeId: null })); + this.volumeSizeGb = null; + await this.ctx.storage.put( + storageUpdate({ pendingDestroyVolumeId: null, flyVolumeId: null, volumeSizeGb: null }) + ); } /** @@ -1885,6 +1898,7 @@ export class KiloClawInstance extends DurableObject { this.flyMachineId = null; this.flyVolumeId = null; this.flyRegion = null; + this.volumeSizeGb = null; this.machineSize = null; this.healthCheckFailCount = 0; this.pendingDestroyMachineId = null; @@ -2049,7 +2063,10 @@ export class KiloClawInstance extends DurableObject { // Fresh provision (never started) — no user data to preserve this.flyVolumeId = null; this.flyRegion = null; - await this.ctx.storage.put(storageUpdate({ flyVolumeId: null, flyRegion: null })); + this.volumeSizeGb = null; + await this.ctx.storage.put( + storageUpdate({ flyVolumeId: null, flyRegion: null, volumeSizeGb: null }) + ); const freshVolume = await fly.createVolumeWithFallback( flyConfig, @@ -2204,6 +2221,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: null, flyVolumeId: null, flyRegion: null, + volumeSizeGb: null, machineSize: null, healthCheckFailCount: 0, pendingDestroyMachineId: null, @@ -2227,6 +2245,7 @@ export class KiloClawInstance extends DurableObject { this.flyMachineId = null; this.flyVolumeId = null; this.flyRegion = null; + this.volumeSizeGb = null; this.machineSize = null; this.healthCheckFailCount = 0; this.pendingDestroyMachineId = null; From 55da692fb8862c2349b10982d4bfab2bed4db6c6 Mon Sep 17 00:00:00 2001 From: syn Date: Tue, 24 Feb 2026 09:22:30 -0600 Subject: [PATCH 3/4] fix(kiloclaw): gate volumeSizeGb on flyVolumeId in getStatus output Prevents stale volume size from leaking via the API when flyVolumeId has been cleared but volumeSizeGb persists in legacy state. --- kiloclaw/src/durable-objects/kiloclaw-instance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiloclaw/src/durable-objects/kiloclaw-instance.ts b/kiloclaw/src/durable-objects/kiloclaw-instance.ts index 565ec6f25..c0d791bdd 100644 --- a/kiloclaw/src/durable-objects/kiloclaw-instance.ts +++ b/kiloclaw/src/durable-objects/kiloclaw-instance.ts @@ -504,7 +504,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: this.flyMachineId, flyVolumeId: this.flyVolumeId, flyRegion: this.flyRegion, - volumeSizeGb: this.volumeSizeGb, + volumeSizeGb: this.flyVolumeId ? this.volumeSizeGb : null, healthCheckFailCount: 0, pendingDestroyMachineId: null, pendingDestroyVolumeId: null, From dc2d8f37009ee76a4ddad462c7e6f43a413a18a3 Mon Sep 17 00:00:00 2001 From: syn Date: Tue, 24 Feb 2026 16:17:31 -0600 Subject: [PATCH 4/4] default to 10 --- kiloclaw/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiloclaw/src/config.ts b/kiloclaw/src/config.ts index e98b3b0ea..715d4404d 100644 --- a/kiloclaw/src/config.ts +++ b/kiloclaw/src/config.ts @@ -29,7 +29,7 @@ export const DEFAULT_MACHINE_GUEST = { }; /** Default Fly Volume size in GB */ -export const DEFAULT_VOLUME_SIZE_GB = 20; +export const DEFAULT_VOLUME_SIZE_GB = 10; /** Default Fly region priority list when FLY_REGION env var is not set */ export const DEFAULT_FLY_REGION = 'dfw,yyz,cdg';