Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,52 @@ describe('ClawOnboardingFlow state machine', () => {
}
);

test('renders an error when the setup request failed', () => {
expect(
getClawOnboardingFlowState(
createInput({
createSetupStarted: true,
setupFailed: true,
onboardingStep: 'provisioning',
hasBotIdentity: true,
selectedPreset: 'always-ask',
status: undefined,
})
).renderStep
).toBe('error');
expect(
getClawOnboardingFlowState(
createInput({
mode: 'post-provisioning',
setupFailed: true,
status: createStatus(null),
})
).renderStep
).toBe('error');
expect(
getClawOnboardingFlowState(
createInput({
mode: 'post-provisioning',
setupFailed: true,
status: createStatus('starting'),
})
).renderStep
).toBe('error');
});

test('does not let an old setup failure override a running instance', () => {
const state = getClawOnboardingFlowState(
createInput({
mode: 'post-provisioning',
setupFailed: true,
status: createStatus('running'),
})
);

expect(state.renderStep).toBe('complete');
expect(state.postProvisioningReady).toBe(true);
});

test.each(CLAW_ONBOARDING_ERROR_STATUSES)(
'renders an error when machine status is %s',
status => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type ClawOnboardingFlowStateInput = {
status: KiloClawDashboardStatus | undefined;
mode: ClawOnboardingMode;
createSetupStarted: boolean;
setupFailed?: boolean;
onboardingStep: OnboardingStep;
selectedPreset: ExecPreset | null;
hasBotIdentity: boolean;
Expand Down Expand Up @@ -103,6 +104,7 @@ export function getClawOnboardingFlowState({
status,
mode,
createSetupStarted,
setupFailed = false,
onboardingStep,
selectedPreset,
hasBotIdentity,
Expand All @@ -122,6 +124,7 @@ export function getClawOnboardingFlowState({
const renderStepDecision = getRenderStepDecision({
mode,
createSetupStarted,
setupFailed,
instanceStatus,
postProvisioningReady,
onboardingStep,
Expand All @@ -145,6 +148,7 @@ export function getClawOnboardingFlowState({
status,
mode,
createSetupStarted,
setupFailed,
onboardingStep,
selectedPreset,
hasBotIdentity,
Expand All @@ -167,7 +171,12 @@ export function getClawOnboardingFlowState({

type RenderStepInput = Pick<
ClawOnboardingFlowStateInput,
'mode' | 'createSetupStarted' | 'onboardingStep' | 'selectedPreset' | 'hasBotIdentity'
| 'mode'
| 'createSetupStarted'
| 'setupFailed'
| 'onboardingStep'
| 'selectedPreset'
| 'hasBotIdentity'
> & {
instanceStatus: PopulatedClawStatus | null;
postProvisioningReady: boolean;
Expand Down Expand Up @@ -204,6 +213,7 @@ const clawOnboardingFlowDebugSnapshots = new Map<string, ClawOnboardingFlowDebug
function getRenderStepDecision({
mode,
createSetupStarted,
setupFailed,
instanceStatus,
postProvisioningReady,
onboardingStep,
Expand All @@ -218,6 +228,13 @@ function getRenderStepDecision({
};
}

if (setupFailed && !postProvisioningReady) {
return {
renderStep: 'error',
reason: 'the setup request failed, so setup cannot continue automatically',
};
}

if (mode === 'post-provisioning') {
if (postProvisioningReady) {
return {
Expand Down Expand Up @@ -304,6 +321,7 @@ function logClawOnboardingFlowStateDecision({
status,
mode,
createSetupStarted,
setupFailed,
onboardingStep,
selectedPreset,
hasBotIdentity,
Expand All @@ -326,6 +344,7 @@ function logClawOnboardingFlowStateDecision({
{
mode,
createSetupStarted,
setupFailed,
onboardingStep,
selectedPreset,
hasBotIdentity,
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/app/(app)/claw/components/ClawOnboardingFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ export function ClawOnboardingFlow({
mode,
organizationId,
createFlowStarted = false,
setupFailed = false,
onCreateFlowStarted,
onCreateFlowFailed,
}: {
status: KiloClawDashboardStatus | undefined;
mode: ClawOnboardingMode;
organizationId?: string;
createFlowStarted?: boolean;
setupFailed?: boolean;
onCreateFlowStarted?: () => void;
onCreateFlowFailed?: () => void;
}) {
Expand All @@ -71,6 +73,7 @@ export function ClawOnboardingFlow({
status={status}
mode={mode}
createFlowStarted={createFlowStarted}
setupFailed={setupFailed}
onCreateFlowStarted={onCreateFlowStarted}
onCreateFlowFailed={onCreateFlowFailed}
/>
Expand All @@ -82,12 +85,14 @@ function ClawOnboardingFlowInner({
status,
mode,
createFlowStarted,
setupFailed,
onCreateFlowStarted,
onCreateFlowFailed,
}: {
status: KiloClawDashboardStatus | undefined;
mode: ClawOnboardingMode;
createFlowStarted: boolean;
setupFailed: boolean;
onCreateFlowStarted?: () => void;
onCreateFlowFailed?: () => void;
}) {
Expand All @@ -113,6 +118,7 @@ function ClawOnboardingFlowInner({
status,
mode,
createSetupStarted,
setupFailed,
onboardingStep,
selectedPreset,
hasBotIdentity: botIdentity !== null,
Expand Down Expand Up @@ -394,8 +400,8 @@ export function ClawSetupErrorStep({ basePath }: { basePath: string }) {
<div className="flex flex-col items-center gap-2">
<h2 className="text-2xl font-bold">Something went wrong</h2>
<p className="text-muted-foreground max-w-md text-center">
Your KiloClaw instance stopped during setup. Please reach out to support for help
getting it back online.
Your KiloClaw instance stopped or failed during setup. Please reach out to support for
help getting it back online.
</p>
</div>

Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/app/(app)/claw/components/CreateInstanceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ export function CreateInstanceCard({
},
{
onError: err => {
posthog?.capture('claw_setup_provision_failed', {
selected_model: selectedModel,
reason: 'provision_request_failed',
});
onProvisionFailed?.();
toast.error(`Failed to create: ${err.message}`);
},
Expand Down
27 changes: 22 additions & 5 deletions apps/web/src/app/(app)/claw/new/ClawNewClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ function ClawNewLoader({
createFlowStartedAt,
billingUpdatedAt,
onCreateFlowStarted,
setupFailed,
onCreateFlowFailed,
}: {
mode: ClawOnboardingMode;
createFlowStartedAt: number | null;
setupFailed: boolean;
billingUpdatedAt: number;
onCreateFlowStarted: () => void;
onCreateFlowFailed: () => void;
Expand All @@ -53,14 +55,20 @@ function ClawNewLoader({
status={status}
mode={mode}
createFlowStarted={createFlowStartedAt !== null}
setupFailed={setupFailed}
onCreateFlowStarted={onCreateFlowStarted}
onCreateFlowFailed={onCreateFlowFailed}
/>
);
}

const statusQueryForBoundary =
statusQuery.error || statusQuery.dataUpdatedAt >= billingUpdatedAt
const statusQueryForBoundary = setupFailed
? {
data: statusQuery.data,
isLoading: false,
error: null,
}
: statusQuery.error || statusQuery.dataUpdatedAt >= billingUpdatedAt
? statusQuery
: {
data: undefined,
Expand All @@ -73,6 +81,7 @@ function ClawNewLoader({
statusQuery={statusQueryForBoundary}
mode={mode}
createFlowStarted={createFlowStartedAt !== null}
setupFailed={setupFailed}
onCreateFlowStarted={onCreateFlowStarted}
onCreateFlowFailed={onCreateFlowFailed}
/>
Expand All @@ -95,8 +104,15 @@ function ClawNewLiveClient() {
const trpc = useTRPC();
const billingQuery = useQuery(trpc.kiloclaw.getBillingStatus.queryOptions());
const [createFlowStartedAt, setCreateFlowStartedAt] = useState<number | null>(null);
const onCreateFlowStarted = useCallback(() => setCreateFlowStartedAt(Date.now()), []);
const onCreateFlowFailed = useCallback(() => setCreateFlowStartedAt(null), []);
const [setupFailed, setSetupFailed] = useState(false);
const onCreateFlowStarted = useCallback(() => {
setSetupFailed(false);
setCreateFlowStartedAt(Date.now());
}, []);
const onCreateFlowFailed = useCallback(() => {
setSetupFailed(true);
setCreateFlowStartedAt(null);
}, []);

if (billingQuery.isLoading) {
return <LoadingState />;
Expand All @@ -115,7 +131,7 @@ function ClawNewLiveClient() {
);
}

if (createFlowStartedAt === null && billingQuery.isFetching) {
if (!setupFailed && createFlowStartedAt === null && billingQuery.isFetching) {
return <LoadingState />;
}

Expand Down Expand Up @@ -144,6 +160,7 @@ function ClawNewLiveClient() {
<ClawNewLoader
mode={mode}
createFlowStartedAt={createFlowStartedAt}
setupFailed={setupFailed}
billingUpdatedAt={billingQuery.dataUpdatedAt}
onCreateFlowStarted={onCreateFlowStarted}
onCreateFlowFailed={onCreateFlowFailed}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,16 @@ export function OrgClawNewClient({
function OrgClawNewLiveClient({ organizationId }: { organizationId: string }) {
const statusQuery = useOrgKiloClawStatus(organizationId);
const [createFlowStartedAt, setCreateFlowStartedAt] = useState<number | null>(null);
const [setupFailed, setSetupFailed] = useState(false);
const [hasSettledStatus, setHasSettledStatus] = useState(false);
const onCreateFlowStarted = useCallback(() => setCreateFlowStartedAt(Date.now()), []);
const onCreateFlowFailed = useCallback(() => setCreateFlowStartedAt(null), []);
const onCreateFlowStarted = useCallback(() => {
setSetupFailed(false);
setCreateFlowStartedAt(Date.now());
}, []);
const onCreateFlowFailed = useCallback(() => {
setSetupFailed(true);
setCreateFlowStartedAt(null);
}, []);

useEffect(() => {
if (!statusQuery.isFetching && (statusQuery.data !== undefined || statusQuery.error)) {
Expand All @@ -54,19 +61,21 @@ function OrgClawNewLiveClient({ organizationId }: { organizationId: string }) {
mode="create-first"
organizationId={organizationId}
createFlowStarted
setupFailed={setupFailed}
onCreateFlowStarted={onCreateFlowStarted}
onCreateFlowFailed={onCreateFlowFailed}
/>
);
}

if (statusQuery.error) {
if (!setupFailed && statusQuery.error) {
return (
<ClawOnboardingWithBoundary
statusQuery={statusQuery}
mode="post-provisioning"
organizationId={organizationId}
createFlowStarted={createFlowStartedAt !== null}
setupFailed={setupFailed}
onCreateFlowStarted={onCreateFlowStarted}
onCreateFlowFailed={onCreateFlowFailed}
/>
Expand All @@ -75,13 +84,14 @@ function OrgClawNewLiveClient({ organizationId }: { organizationId: string }) {

const isFetchingEmptyStatus = statusQuery.isFetching && statusQuery.data?.status === null;

if (statusQuery.isLoading || !hasSettledStatus || isFetchingEmptyStatus) {
if (!setupFailed && (statusQuery.isLoading || !hasSettledStatus || isFetchingEmptyStatus)) {
return (
<ClawOnboardingWithBoundary
statusQuery={{ data: undefined, isLoading: true, error: null }}
mode="post-provisioning"
organizationId={organizationId}
createFlowStarted={createFlowStartedAt !== null}
setupFailed={setupFailed}
onCreateFlowStarted={onCreateFlowStarted}
onCreateFlowFailed={onCreateFlowFailed}
/>
Expand All @@ -94,10 +104,15 @@ function OrgClawNewLiveClient({ organizationId }: { organizationId: string }) {

return (
<ClawOnboardingWithBoundary
statusQuery={{ ...statusQuery, data: settledStatus }}
statusQuery={{
...statusQuery,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: setupFailed can still leave the org flow stuck on the loading boundary

This branch clears error, but it still spreads statusQuery.isLoading. If provisioning fails before the first org status poll settles, withStatusQueryBoundary keeps rendering Loading... and the new error step never appears. The personal flow already forces isLoading: false; the org path needs the same override.

data: settledStatus,
error: setupFailed ? null : statusQuery.error,
}}
mode={mode}
organizationId={organizationId}
createFlowStarted={createFlowStartedAt !== null}
setupFailed={setupFailed}
onCreateFlowStarted={onCreateFlowStarted}
onCreateFlowFailed={onCreateFlowFailed}
/>
Expand Down
Loading
Loading