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 @@ -20,6 +20,7 @@ import { logExceptInTest, errorExceptInTest } from '@/lib/utils.server';
import { captureException, captureMessage } from '@sentry/nextjs';
import { INTERNAL_API_SECRET } from '@/lib/config.server';
import type { FixStatus } from '@/lib/auto-fix/core/schemas';
import { formatAutoFixErrorMessage } from '@/lib/auto-fix/core/format-error-message';

interface StatusUpdatePayload {
sessionId?: string;
Expand Down Expand Up @@ -57,6 +58,7 @@ export async function POST(
cliSessionId,
status,
hasError: !!errorMessage,
errorMessage,
});

// Get current ticket to check if update is needed
Expand Down Expand Up @@ -95,7 +97,8 @@ export async function POST(
await updateFixTicketStatus(ticketId, status, {
sessionId,
cliSessionId,
errorMessage,
errorMessage:
errorMessage !== undefined ? formatAutoFixErrorMessage(errorMessage) : undefined,
startedAt: status === 'running' ? new Date() : undefined,
completedAt:
status === 'completed' || status === 'failed' || status === 'cancelled'
Expand Down
11 changes: 8 additions & 3 deletions apps/web/src/app/api/internal/auto-fix/pr-callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
sanitizePublicErrorMessage,
} from '@/lib/auto-fix/github/handle-comment-reply';
import { handleCreateIssuePR } from '@/lib/auto-fix/github/handle-create-issue-pr';
import { formatAutoFixErrorMessage } from '@/lib/auto-fix/core/format-error-message';
import { z } from 'zod';

const callbackStatusEnum = z.enum(['completed', 'failed', 'interrupted']);
Expand Down Expand Up @@ -167,6 +168,10 @@ export async function POST(req: NextRequest) {

// Handle failure/interruption
if (status === 'failed' || status === 'interrupted') {
const failureMessage = formatAutoFixErrorMessage(
errorMessage || `Auto-fix execution ${status}`
);

logExceptInTest('[auto-fix-pr-callback] Auto-fix execution failed', {
ticketId,
sessionId,
Expand All @@ -179,7 +184,7 @@ export async function POST(req: NextRequest) {
ticketId,
sessionId,
outcome: 'failed',
errorMessage: errorMessage || `Auto-fix execution ${status}`,
errorMessage: failureMessage,
});

if (!replyResult.ok) {
Expand All @@ -192,7 +197,7 @@ export async function POST(req: NextRequest) {
} else {
// Update ticket to failed
await updateFixTicketStatus(ticketId, 'failed', {
errorMessage: errorMessage || `Auto-fix execution ${status}`,
errorMessage: failureMessage,
completedAt: new Date(),
});
}
Expand All @@ -212,7 +217,7 @@ export async function POST(req: NextRequest) {
await postIssueComment({
repoFullName: ticket.repo_full_name,
issueNumber: ticket.issue_number,
body: `🤖 **Auto-Fix Update**\n\nI attempted to create a pull request to fix this issue, but encountered an error:\n\n\`\`\`\n${sanitizePublicErrorMessage(errorMessage || 'Unknown error')}\n\`\`\`\n\nThis issue may require manual attention.`,
body: `🤖 **Auto-Fix Update**\n\nI attempted to create a pull request to fix this issue, but encountered an error:\n\n\`\`\`\n${sanitizePublicErrorMessage(failureMessage)}\n\`\`\`\n\nThis issue may require manual attention.`,
githubToken: tokenData.token,
});

Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/auto-fix/AutoFixTicketsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useTRPC } from '@/lib/trpc/utils';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { formatDistanceToNow } from 'date-fns';
import { toast } from 'sonner';
import { formatAutoFixErrorMessage } from '@/lib/auto-fix/core/format-error-message';

type AutoFixTicketsCardProps = {
organizationId?: string;
Expand Down Expand Up @@ -476,7 +477,7 @@ export function AutoFixTicketsCard({ organizationId }: AutoFixTicketsCardProps)
{/* Error Message */}
{ticket.error_message && (
<div className="text-destructive mt-1 text-xs">
Error: {ticket.error_message}
Error: {formatAutoFixErrorMessage(ticket.error_message)}
</div>
)}

Expand Down
45 changes: 45 additions & 0 deletions apps/web/src/lib/auto-fix/core/format-error-message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE,
formatAutoFixErrorMessage,
isAutoFixBillingErrorMessage,
} from './format-error-message';

describe('formatAutoFixErrorMessage', () => {
it('maps raw initiate 402 payloads to a clear credits message', () => {
const raw =
'initiateFromKilocodeSessionV2 failed (402): {"error":{"message":"Insufficient credits: $1 minimum required","code":-32002,"data":{"code":"PAYMENT_REQUIRED","httpStatus":402}}}';

expect(formatAutoFixErrorMessage(raw)).toBe(AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE);
});

it('maps short insufficient-credits text', () => {
expect(formatAutoFixErrorMessage('Insufficient credits: $1 minimum required')).toBe(
AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE
);
});

it('leaves unrelated errors unchanged', () => {
expect(formatAutoFixErrorMessage('Failed to get PR config: 500 - boom')).toBe(
'Failed to get PR config: 500 - boom'
);
});

it('does not treat generic "minimum required" text as a billing error', () => {
const raw = 'Sandbox image node20 is the minimum required version';
expect(formatAutoFixErrorMessage(raw)).toBe(raw);
});

it('handles empty input', () => {
expect(formatAutoFixErrorMessage(' ')).toBe('Unknown error');
});
});

describe('isAutoFixBillingErrorMessage', () => {
it('detects payment_required codes in dumped JSON', () => {
expect(isAutoFixBillingErrorMessage('{"data":{"code":"PAYMENT_REQUIRED"}}')).toBe(true);
});

it('returns false for non-billing errors', () => {
expect(isAutoFixBillingErrorMessage('timeout waiting for sandbox')).toBe(false);
});
});
40 changes: 40 additions & 0 deletions apps/web/src/lib/auto-fix/core/format-error-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* User-facing auto-fix error formatting.
*
* Raw orchestrator/tRPC failures often include status codes and JSON envelopes.
* Map known billing failures to a clear credit-minimum explanation.
*/

import { AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE } from '@kilocode/worker-utils/cloud-agent-next-client';

export { AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE };

const BILLING_ERROR_PATTERNS = [
'insufficient credits',
'payment_required',
'payment required',
'credits required',
'credit balance is too low',
'insufficient funds',
'add credits',
'paid model',
] as const;

export function isAutoFixBillingErrorMessage(message: string): boolean {
const normalized = message.toLowerCase();
return BILLING_ERROR_PATTERNS.some(pattern => normalized.includes(pattern));
}

/** Map raw auto-fix failure text to a concise user-facing message. */
export function formatAutoFixErrorMessage(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) {
return 'Unknown error';
}

if (isAutoFixBillingErrorMessage(trimmed)) {
return AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE;
}

return trimmed;
}
14 changes: 13 additions & 1 deletion apps/web/src/lib/auto-fix/github/handle-comment-reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
*/

import { getFixTicketById, updateFixTicketStatus } from '@/lib/auto-fix/db/fix-tickets';
import {
formatAutoFixErrorMessage,
isAutoFixBillingErrorMessage,
} from '@/lib/auto-fix/core/format-error-message';
import { logExceptInTest, errorExceptInTest } from '@/lib/utils.server';
import { captureException } from '@sentry/nextjs';
import {
Expand Down Expand Up @@ -79,6 +83,14 @@ type FriendlyFailure = {
function getFriendlyFailure(rawError: string): FriendlyFailure {
const normalized = rawError.toLowerCase();

if (isAutoFixBillingErrorMessage(rawError)) {
return {
summary:
'I could not start this fix because the account needs at least $1 in available credits.',
suggestedAction: 'Add credits to your Kilo account, then retry the fix.',
};
}

if (normalized.includes('failed to verify balance') || normalized.includes('balance check')) {
return {
summary: 'I could not start this fix because the account balance check failed.',
Expand Down Expand Up @@ -312,7 +324,7 @@ export async function handleCommentReply(

await updateFixTicketStatus(ticketId, 'failed', {
sessionId,
errorMessage: failureReason,
errorMessage: formatAutoFixErrorMessage(failureReason),
completedAt: new Date(),
});

Expand Down
7 changes: 7 additions & 0 deletions packages/worker-utils/src/cloud-agent-next-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@ export const CLOUD_AGENT_NEXT_BILLING_ERROR_PATTERNS = [
'payment required',
] as const;

/**
* User-facing message for auto-fix billing failures.
* Shared by the web app and auto-fix-infra so the copy stays in sync.
*/
export const AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE =
'Insufficient credits. Auto-fix requires at least $1 in available credits. Add credits, then retry.';

export function isCloudAgentNextBillingErrorBody(body: string): boolean {
const normalizedBody = body.toLowerCase();

Expand Down
1 change: 1 addition & 0 deletions packages/worker-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export { createNotFoundHandler } from './not-found-handler.js';
export type { Owner, MCPServerConfig } from './types.js';

export {
AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE,
CLOUD_AGENT_NEXT_BILLING_ERROR_PATTERNS,
createCloudAgentNextFetchClient,
isCloudAgentNextBillingErrorBody,
Expand Down
11 changes: 11 additions & 0 deletions services/auto-fix-infra/src/services/cloud-agent-next-client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE,
isCloudAgentNextBillingErrorBody,
} from '@kilocode/worker-utils';

type CallbackTarget = {
url: string;
headers?: Record<string, string>;
Expand Down Expand Up @@ -53,6 +58,9 @@ export class CloudAgentNextClient {

if (!response.ok) {
const errorText = await response.text();
if (response.status === 402 || isCloudAgentNextBillingErrorBody(errorText)) {
throw new Error(AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE);
}
throw new Error(`prepareSession failed (${response.status}): ${errorText}`);
}

Expand Down Expand Up @@ -93,6 +101,9 @@ export class CloudAgentNextClient {

if (!response.ok) {
const errorText = await response.text();
if (response.status === 402 || isCloudAgentNextBillingErrorBody(errorText)) {
throw new Error(AUTO_FIX_INSUFFICIENT_CREDITS_MESSAGE);
}
throw new Error(`initiateFromKilocodeSessionV2 failed (${response.status}): ${errorText}`);
}

Expand Down