diff --git a/kiloclaw/src/durable-objects/kiloclaw-instance/kilo-cli-run.ts b/kiloclaw/src/durable-objects/kiloclaw-instance/kilo-cli-run.ts index 85c5d343a7..43bba17bca 100644 --- a/kiloclaw/src/durable-objects/kiloclaw-instance/kilo-cli-run.ts +++ b/kiloclaw/src/durable-objects/kiloclaw-instance/kilo-cli-run.ts @@ -4,7 +4,7 @@ import { KiloCliRunStatusResponseSchema, GatewayCommandResponseSchema, } from '../gateway-controller-types'; -import { callGatewayController } from './gateway'; +import { callGatewayController, isErrorUnknownRoute } from './gateway'; import type { InstanceMutableState } from './types'; type KiloCliRunStartResponse = { @@ -29,19 +29,24 @@ export async function startKiloCliRun( state: InstanceMutableState, env: KiloClawEnv, prompt: string -): Promise { +): Promise { if (state.status !== 'running' || !state.flyMachineId) { throw Object.assign(new Error('Instance is not running'), { status: 409 }); } - return callGatewayController( - state, - env, - '/_kilo/cli-run/start', - 'POST', - KiloCliRunStartResponseSchema, - { prompt } - ); + try { + return await callGatewayController( + state, + env, + '/_kilo/cli-run/start', + 'POST', + KiloCliRunStartResponseSchema, + { prompt } + ); + } catch (error) { + if (isErrorUnknownRoute(error)) return null; + throw error; + } } /** diff --git a/kiloclaw/src/routes/platform.ts b/kiloclaw/src/routes/platform.ts index 7c52cf592a..350da8d23e 100644 --- a/kiloclaw/src/routes/platform.ts +++ b/kiloclaw/src/routes/platform.ts @@ -1097,10 +1097,17 @@ platform.post('/kilo-cli-run/start', async c => { stub => stub.startKiloCliRun(result.data.prompt), 'startKiloCliRun' ); + if (!response) { + return jsonError( + 'Kilo CLI agent not available (controller too old)', + 404, + 'controller_route_unavailable' + ); + } return c.json(response, 200); } catch (err) { - const { message, status } = sanitizeError(err, 'kilo-cli-run start'); - return jsonError(message, status); + const { message, status, code } = sanitizeOpenclawConfigError(err, 'kilo-cli-run start'); + return jsonError(message, status, code); } }); diff --git a/src/app/(app)/claw/components/InstanceControls.tsx b/src/app/(app)/claw/components/InstanceControls.tsx index 2ca4780834..c8373e2495 100644 --- a/src/app/(app)/claw/components/InstanceControls.tsx +++ b/src/app/(app)/claw/components/InstanceControls.tsx @@ -400,7 +400,11 @@ export function InstanceControls({ onOpenChange={setDoctorOpen} mutation={mutations.runDoctor} /> - + ); } diff --git a/src/app/(app)/claw/components/StartKiloCliRunDialog.tsx b/src/app/(app)/claw/components/StartKiloCliRunDialog.tsx index c2bef8d98b..2254498e77 100644 --- a/src/app/(app)/claw/components/StartKiloCliRunDialog.tsx +++ b/src/app/(app)/claw/components/StartKiloCliRunDialog.tsx @@ -1,8 +1,9 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Loader2, Terminal } from 'lucide-react'; +import { AlertTriangle, Loader2, RotateCw, Terminal } from 'lucide-react'; +import { toast } from 'sonner'; import { Dialog, DialogContent, @@ -14,18 +15,47 @@ import { import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { useKiloClawMutations } from '@/hooks/useKiloClaw'; +import type { PlatformStatusResponse } from '@/lib/kiloclaw/types'; +import { AnimatedDots } from './AnimatedDots'; + +function isNeedsRedeployError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'data' in error && + typeof (error as { data?: unknown }).data === 'object' && + (error as { data?: { upstreamCode?: unknown } }).data !== null && + (error as { data: { upstreamCode?: unknown } }).data.upstreamCode === + 'controller_route_unavailable' + ); +} export function StartKiloCliRunDialog({ open, onOpenChange, + machineStatus, }: { open: boolean; onOpenChange: (open: boolean) => void; + machineStatus: PlatformStatusResponse['status']; }) { const router = useRouter(); const [prompt, setPrompt] = useState(''); const mutations = useKiloClawMutations(); const startMutation = mutations.startKiloCliRun; + const redeployMutation = mutations.restartMachine; + + const needsRedeploy = startMutation.isError && isNeedsRedeployError(startMutation.error); + const machineReady = machineStatus === 'running'; + + // Clear stale "needs redeploy" error when the machine status changes away + // from running (e.g. restarting after a redeploy was dispatched). This + // ensures reopening the dialog shows the prompt form, not the old error. + useEffect(() => { + if (needsRedeploy && machineStatus !== 'running') { + startMutation.reset(); + } + }, [needsRedeploy, machineStatus, startMutation]); const handleStart = () => { const trimmed = prompt.trim(); @@ -41,7 +71,20 @@ export function StartKiloCliRunDialog({ ); }; + const handleRedeploy = () => { + redeployMutation.mutate( + { imageTag: 'latest' }, + { + onSuccess: () => { + startMutation.reset(); + }, + onError: err => toast.error(err.message, { duration: 10000 }), + } + ); + }; + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen && redeployMutation.isPending) return; if (!nextOpen) { setPrompt(''); startMutation.reset(); @@ -58,55 +101,107 @@ export function StartKiloCliRunDialog({ Recover with Kilo CLI Agent - If your KiloClaw instance is stuck or failing, the Kilo CLI agent can help diagnose and - fix the problem. Describe the issue below and the agent will work autonomously to - resolve it. + {needsRedeploy + ? 'Your instance needs to be redeployed before the recovery agent can run.' + : !machineReady + ? 'Waiting for your instance to come back online before the recovery agent can run.' + : 'If your KiloClaw instance is stuck or failing, the Kilo CLI agent can help diagnose and fix the problem. Describe the issue below and the agent will work autonomously to resolve it.'} -
-