Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions kiloclaw/src/app/(app)/claw/components/changelog-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
type ChangelogCategory = 'feature' | 'bugfix';
type ChangelogDeployHint = 'redeploy_suggested' | 'redeploy_required' | null;

export type ChangelogEntry = {
date: string; // ISO date string, e.g. "2026-02-18"
description: string;
category: ChangelogCategory;
deployHint: ChangelogDeployHint;
};

// Newest entries first. Developers add new entries to the top of this array.
export const CHANGELOG_ENTRIES: ChangelogEntry[] = [
{
date: '2026-02-24',
description:
'Improve OpenClaw restart handling when restarts are triggered via the OpenClaw Control UI.',
category: 'bugfix',
deployHint: 'redeploy_suggested',
},
{
date: '2026-02-24',
description: 'Fix instance type and size badges.',
category: 'bugfix',
deployHint: null,
},
{
date: '2026-02-23',
description:
'Redesigned dashboard: live gateway process status, added ability to restart OpenClaw gateway.',
category: 'feature',
deployHint: null,
},
{
date: '2026-02-23',
description: 'Deploy OpenClaw 2026.2.22. Added device pairing support to the dashboard.',
category: 'feature',
deployHint: 'redeploy_required',
},
{
date: '2026-02-23',
description:
'OpenClaw now binds only to the loopback interface and is managed by a Kilo controller.',
category: 'feature',
deployHint: 'redeploy_required',
},
{
date: '2026-02-20',
description:
'Added OpenClaw Doctor: run diagnostics and auto-fix from the dashboard. Renamed "Restart Gateway" to "Redeploy" to reflect actual behavior.',
category: 'feature',
deployHint: null,
},
{
date: '2026-02-19',
description: 'Added Discord and Slack channel configuration',
category: 'feature',
deployHint: 'redeploy_suggested',
},
{
date: '2026-02-18',
description: 'Fixed an issue where pending pair requests were not displayed',
category: 'bugfix',
deployHint: null,
},
{
date: '2026-02-18',
description: 'Initial support for Telegram Channel pairing',
category: 'feature',
deployHint: 'redeploy_suggested',
},
{
date: '2026-02-17',
description: 'Fixed errors on stopping an instance',
category: 'bugfix',
deployHint: null,
},
];
83 changes: 75 additions & 8 deletions kiloclaw/src/durable-objects/kiloclaw-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
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;
Expand All @@ -367,6 +368,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {

// In-memory only (not persisted to SQLite) — throttles live Fly checks in getStatus()
private lastLiveCheckAt: number | null = null;
private backfillingVolumeSize = false;

// ---- State loading ----

Expand Down Expand Up @@ -396,6 +398,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
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;
Expand Down Expand Up @@ -467,6 +470,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
);
this.flyVolumeId = volume.id;
this.flyRegion = volume.region;
this.volumeSizeGb = volume.size_gb;
console.log('[DO] Created Fly Volume:', volume.id, 'region:', volume.region);
}

Expand Down Expand Up @@ -515,6 +519,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
flyMachineId: this.flyMachineId,
flyVolumeId: this.flyVolumeId,
flyRegion: this.flyRegion,
volumeSizeGb: this.flyVolumeId ? this.volumeSizeGb : null,
healthCheckFailCount: 0,
pendingDestroyMachineId: null,
pendingDestroyVolumeId: null,
Expand Down Expand Up @@ -1011,7 +1016,10 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
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
Expand Down Expand Up @@ -1215,6 +1223,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
flyMachineId: string | null;
flyVolumeId: string | null;
flyRegion: string | null;
volumeSizeGb: number | null;
openclawVersion: string | null;
imageVariant: string | null;
trackedImageTag: string | null;
Expand All @@ -1234,6 +1243,17 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
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.backfillingVolumeSize) {
this.backfillingVolumeSize = true;
this.ctx.waitUntil(
this.backfillVolumeSizeGb().finally(() => {
this.backfillingVolumeSize = false;
})
);
}

return {
userId: this.userId,
sandboxId: this.sandboxId,
Expand All @@ -1248,6 +1268,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
flyMachineId: this.flyMachineId,
flyVolumeId: this.flyVolumeId,
flyRegion: this.flyRegion,
volumeSizeGb: this.volumeSizeGb,
openclawVersion: this.openclawVersion,
imageVariant: this.imageVariant,
trackedImageTag: this.trackedImageTag,
Expand Down Expand Up @@ -1495,17 +1516,22 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
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', {
data_loss: true,
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
Expand Down Expand Up @@ -1711,6 +1737,25 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
}
}

/**
* Fire-and-forget backfill for volumeSizeGb. Called from getStatus() and
* reconcileVolume() when the field is null but a volume exists.
*/
private async backfillVolumeSizeGb(): Promise<void> {
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.
Expand Down Expand Up @@ -1839,7 +1884,10 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
// 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 })
);
}

/**
Expand Down Expand Up @@ -1877,6 +1925,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
this.flyMachineId = null;
this.flyVolumeId = null;
this.flyRegion = null;
this.volumeSizeGb = null;
this.machineSize = null;
this.healthCheckFailCount = 0;
this.pendingDestroyMachineId = null;
Expand Down Expand Up @@ -2032,7 +2081,14 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {

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,
Expand Down Expand Up @@ -2098,6 +2154,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
);
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,
Expand All @@ -2108,7 +2165,10 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
// 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,
Expand All @@ -2121,6 +2181,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
);
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,
Expand All @@ -2131,7 +2192,11 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {

// 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)
Expand Down Expand Up @@ -2258,6 +2323,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
flyMachineId: null,
flyVolumeId: null,
flyRegion: null,
volumeSizeGb: null,
machineSize: null,
healthCheckFailCount: 0,
pendingDestroyMachineId: null,
Expand All @@ -2281,6 +2347,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
this.flyMachineId = null;
this.flyVolumeId = null;
this.flyRegion = null;
this.volumeSizeGb = null;
this.machineSize = null;
this.healthCheckFailCount = 0;
this.pendingDestroyMachineId = null;
Expand Down
2 changes: 2 additions & 0 deletions kiloclaw/src/schemas/instance-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/claw/components/InstanceControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function InstanceControls({
</Badge>
<Badge variant="outline" className="text-muted-foreground gap-1.5 font-normal">
<HardDrive className="h-3.5 w-3.5" />
20 GB SSD
{status.volumeSizeGb ?? 10} GB SSD
</Badge>
</div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/app/(app)/claw/components/changelog-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@
export const CHANGELOG_ENTRIES: ChangelogEntry[] = [
{
date: '2026-02-24',
<<<<<<< HEAD

Check failure on line 15 in src/app/(app)/claw/components/changelog-data.ts

View workflow job for this annotation

GitHub Actions / test

Merge conflict marker encountered.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Unresolved merge conflict markers

This file contains <<<<<<< HEAD, =======, and >>>>>>> main conflict markers (lines 15–24). This will cause a runtime syntax error. The conflict needs to be resolved — likely by keeping both entries (as done in the kiloclaw/ copy of this file).

description: 'Fix instance type and size badges.',
category: 'bugfix',
deployHint: null,
=======

Check failure on line 19 in src/app/(app)/claw/components/changelog-data.ts

View workflow job for this annotation

GitHub Actions / test

Merge conflict marker encountered.
description:
'Improve OpenClaw restart handling when restarts are triggered via the OpenClaw Control UI.',
category: 'bugfix',
deployHint: 'redeploy_suggested',
>>>>>>> main

Check failure on line 24 in src/app/(app)/claw/components/changelog-data.ts

View workflow job for this annotation

GitHub Actions / test

Merge conflict marker encountered.
},
{
date: '2026-02-23',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const baseStatus: KiloClawDashboardStatus = {
flyMachineId: null,
flyVolumeId: null,
flyRegion: null,
volumeSizeGb: null,
gatewayToken: 'token',
workerUrl: 'https://claw.kilo.ai',
};
Expand Down
1 change: 1 addition & 0 deletions src/lib/kiloclaw/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export type PlatformStatusResponse = {
flyMachineId: string | null;
flyVolumeId: string | null;
flyRegion: string | null;
volumeSizeGb: number | null;
};

/** A Fly volume snapshot. */
Expand Down
Loading