diff --git a/packages/cli/src/acp-integration/acp.ts b/packages/cli/src/acp-integration/acp.ts index 9c705c11e..9725cdb01 100644 --- a/packages/cli/src/acp-integration/acp.ts +++ b/packages/cli/src/acp-integration/acp.ts @@ -8,6 +8,7 @@ import { z } from 'zod'; import * as schema from './schema.js'; +import { ACP_ERROR_CODES } from './errorCodes.js'; export * from './schema.js'; import type { WritableStream, ReadableStream } from 'node:stream/web'; @@ -349,27 +350,51 @@ export class RequestError extends Error { } static parseError(details?: string): RequestError { - return new RequestError(-32700, 'Parse error', details); + return new RequestError( + ACP_ERROR_CODES.PARSE_ERROR, + 'Parse error', + details, + ); } static invalidRequest(details?: string): RequestError { - return new RequestError(-32600, 'Invalid request', details); + return new RequestError( + ACP_ERROR_CODES.INVALID_REQUEST, + 'Invalid request', + details, + ); } static methodNotFound(details?: string): RequestError { - return new RequestError(-32601, 'Method not found', details); + return new RequestError( + ACP_ERROR_CODES.METHOD_NOT_FOUND, + 'Method not found', + details, + ); } static invalidParams(details?: string): RequestError { - return new RequestError(-32602, 'Invalid params', details); + return new RequestError( + ACP_ERROR_CODES.INVALID_PARAMS, + 'Invalid params', + details, + ); } static internalError(details?: string): RequestError { - return new RequestError(-32603, 'Internal error', details); + return new RequestError( + ACP_ERROR_CODES.INTERNAL_ERROR, + 'Internal error', + details, + ); } static authRequired(details?: string): RequestError { - return new RequestError(-32000, 'Authentication required', details); + return new RequestError( + ACP_ERROR_CODES.AUTH_REQUIRED, + 'Authentication required', + details, + ); } toResult(): Result { diff --git a/packages/cli/src/acp-integration/errorCodes.ts b/packages/cli/src/acp-integration/errorCodes.ts new file mode 100644 index 000000000..e8a0aab94 --- /dev/null +++ b/packages/cli/src/acp-integration/errorCodes.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ACP_ERROR_CODES = { + // Parse error: invalid JSON received by server. + PARSE_ERROR: -32700, + // Invalid request: JSON is not a valid Request object. + INVALID_REQUEST: -32600, + // Method not found: method does not exist or is unavailable. + METHOD_NOT_FOUND: -32601, + // Invalid params: invalid method parameter(s). + INVALID_PARAMS: -32602, + // Internal error: implementation-defined server error. + INTERNAL_ERROR: -32603, + // Authentication required: must authenticate before operation. + AUTH_REQUIRED: -32000, + // Resource not found: e.g. missing file. + RESOURCE_NOT_FOUND: -32002, +} as const; + +export type AcpErrorCode = + (typeof ACP_ERROR_CODES)[keyof typeof ACP_ERROR_CODES]; diff --git a/packages/cli/src/acp-integration/service/filesystem.ts b/packages/cli/src/acp-integration/service/filesystem.ts index 7bcaee2d6..18aef1bec 100644 --- a/packages/cli/src/acp-integration/service/filesystem.ts +++ b/packages/cli/src/acp-integration/service/filesystem.ts @@ -6,6 +6,7 @@ import type { FileSystemService } from '@qwen-code/qwen-code-core'; import type * as acp from '../acp.js'; +import { ACP_ERROR_CODES } from '../errorCodes.js'; /** * ACP client-based implementation of FileSystemService @@ -23,25 +24,31 @@ export class AcpFileSystemService implements FileSystemService { return this.fallback.readTextFile(filePath); } - const response = await this.client.readTextFile({ - path: filePath, - sessionId: this.sessionId, - line: null, - limit: null, - }); + let response: { content: string }; + try { + response = await this.client.readTextFile({ + path: filePath, + sessionId: this.sessionId, + line: null, + limit: null, + }); + } catch (error) { + const errorCode = + typeof error === 'object' && error !== null && 'code' in error + ? (error as { code?: unknown }).code + : undefined; - if (response.content.startsWith('ERROR: ENOENT:')) { - // Treat ACP error strings as structured ENOENT errors without - // assuming a specific platform format. - const match = /^ERROR:\s*ENOENT:\s*(?.*)$/i.exec(response.content); - const err = new Error(response.content) as NodeJS.ErrnoException; - err.code = 'ENOENT'; - err.errno = -2; - const rawPath = match?.groups?.['path']?.trim(); - err['path'] = rawPath - ? rawPath.replace(/^['"]|['"]$/g, '') || filePath - : filePath; - throw err; + if (errorCode === ACP_ERROR_CODES.RESOURCE_NOT_FOUND) { + const err = new Error( + `File not found: ${filePath}`, + ) as NodeJS.ErrnoException; + err.code = 'ENOENT'; + err.errno = -2; + err.path = filePath; + throw err; + } + + throw error; } return response.content; diff --git a/packages/vscode-ide-companion/src/constants/acpSchema.ts b/packages/vscode-ide-companion/src/constants/acpSchema.ts index 9f06e4fad..edbfdd5a8 100644 --- a/packages/vscode-ide-companion/src/constants/acpSchema.ts +++ b/packages/vscode-ide-companion/src/constants/acpSchema.ts @@ -23,3 +23,23 @@ export const CLIENT_METHODS = { session_request_permission: 'session/request_permission', session_update: 'session/update', } as const; + +export const ACP_ERROR_CODES = { + // Parse error: invalid JSON received by server. + PARSE_ERROR: -32700, + // Invalid request: JSON is not a valid Request object. + INVALID_REQUEST: -32600, + // Method not found: method does not exist or is unavailable. + METHOD_NOT_FOUND: -32601, + // Invalid params: invalid method parameter(s). + INVALID_PARAMS: -32602, + // Internal error: implementation-defined server error. + INTERNAL_ERROR: -32603, + // Authentication required: must authenticate before operation. + AUTH_REQUIRED: -32000, + // Resource not found: e.g. missing file. + RESOURCE_NOT_FOUND: -32002, +} as const; + +export type AcpErrorCode = + (typeof ACP_ERROR_CODES)[keyof typeof ACP_ERROR_CODES]; diff --git a/packages/vscode-ide-companion/src/ide-server.ts b/packages/vscode-ide-companion/src/ide-server.ts index f7712399d..6a8ff82a1 100644 --- a/packages/vscode-ide-companion/src/ide-server.ts +++ b/packages/vscode-ide-companion/src/ide-server.ts @@ -28,6 +28,7 @@ import * as os from 'node:os'; import type { z } from 'zod'; import type { DiffManager } from './diff-manager.js'; import { OpenFilesManager } from './open-files-manager.js'; +import { ACP_ERROR_CODES } from './constants/acpSchema.js'; class CORSError extends Error { constructor(message: string) { @@ -264,7 +265,7 @@ export class IDEServer { res.status(400).json({ jsonrpc: '2.0', error: { - code: -32000, + code: ACP_ERROR_CODES.AUTH_REQUIRED, message: 'Bad Request: No valid session ID provided for non-initialize request.', }, @@ -283,7 +284,7 @@ export class IDEServer { res.status(500).json({ jsonrpc: '2.0' as const, error: { - code: -32603, + code: ACP_ERROR_CODES.INTERNAL_ERROR, message: 'Internal server error', }, id: null, diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index e4bdc0cf6..c999a983b 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -5,6 +5,7 @@ */ import { JSONRPC_VERSION } from '../types/acpTypes.js'; +import { ACP_ERROR_CODES } from '../constants/acpSchema.js'; import type { AcpMessage, AcpPermissionRequest, @@ -232,12 +233,34 @@ export class AcpConnection { }) .catch((error) => { if ('id' in message && typeof message.id === 'number') { + const errorMessage = + error instanceof Error + ? error.message + : typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as { message: unknown }).message === 'string' + ? (error as { message: string }).message + : String(error); + + let errorCode: number = ACP_ERROR_CODES.INTERNAL_ERROR; + const errorCodeValue = + typeof error === 'object' && error !== null && 'code' in error + ? (error as { code?: unknown }).code + : undefined; + + if (typeof errorCodeValue === 'number') { + errorCode = errorCodeValue; + } else if (errorCodeValue === 'ENOENT') { + errorCode = ACP_ERROR_CODES.RESOURCE_NOT_FOUND; + } + this.messageHandler.sendResponseMessage(this.child, { jsonrpc: JSONRPC_VERSION, id: message.id, error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), + code: errorCode, + message: errorMessage, }, }); } diff --git a/packages/vscode-ide-companion/src/services/acpFileHandler.ts b/packages/vscode-ide-companion/src/services/acpFileHandler.ts index 8dce3c7b7..2416ceb37 100644 --- a/packages/vscode-ide-companion/src/services/acpFileHandler.ts +++ b/packages/vscode-ide-companion/src/services/acpFileHandler.ts @@ -66,6 +66,11 @@ export class AcpFileHandler { const errorMsg = error instanceof Error ? error.message : String(error); console.error(`[ACP] Failed to read file ${params.path}:`, errorMsg); + const nodeError = error as NodeJS.ErrnoException; + if (nodeError?.code === 'ENOENT') { + throw error; + } + throw new Error(`Failed to read file '${params.path}': ${errorMsg}`); } } diff --git a/packages/vscode-ide-companion/src/utils/authErrors.ts b/packages/vscode-ide-companion/src/utils/authErrors.ts index 8b0e6af9d..85d652045 100644 --- a/packages/vscode-ide-companion/src/utils/authErrors.ts +++ b/packages/vscode-ide-companion/src/utils/authErrors.ts @@ -4,9 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ACP_ERROR_CODES } from '../constants/acpSchema.js'; + const AUTH_ERROR_PATTERNS = [ 'Authentication required', // Standard authentication request message - '(code: -32000)', // RPC error code -32000 indicates authentication failure + `(code: ${ACP_ERROR_CODES.AUTH_REQUIRED})`, // RPC error code indicates auth failure 'Unauthorized', // HTTP unauthorized error 'Invalid token', // Invalid token 'Session expired', // Session expired diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index d8861b95a..beaacde60 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -8,6 +8,9 @@ import * as vscode from 'vscode'; import { BaseMessageHandler } from './BaseMessageHandler.js'; import type { ChatMessage } from '../../services/qwenAgentManager.js'; import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js'; +import { ACP_ERROR_CODES } from '../../constants/acpSchema.js'; + +const AUTH_REQUIRED_CODE_PATTERN = `(code: ${ACP_ERROR_CODES.AUTH_REQUIRED})`; /** * Session message handler @@ -355,7 +358,7 @@ export class SessionMessageHandler extends BaseMessageHandler { createErr instanceof Error ? createErr.message : String(createErr); if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ) { await this.promptLogin( 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', @@ -421,7 +424,7 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('Session not found') || errorMsg.includes('No active ACP session') || errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') ) { @@ -512,7 +515,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') || errorMsg.includes('No active ACP session') @@ -622,7 +625,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') || errorMsg.includes('No active ACP session') @@ -682,7 +685,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors in session creation if ( createErrorMsg.includes('Authentication required') || - createErrorMsg.includes('(code: -32000)') || + createErrorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || createErrorMsg.includes('Unauthorized') || createErrorMsg.includes('Invalid token') || createErrorMsg.includes('No active ACP session') @@ -722,7 +725,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') || errorMsg.includes('No active ACP session') @@ -777,7 +780,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') || errorMsg.includes('No active ACP session') @@ -827,7 +830,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') || errorMsg.includes('No active ACP session') @@ -855,7 +858,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') || errorMsg.includes('No active ACP session') @@ -961,7 +964,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') || errorMsg.includes('No active ACP session') @@ -989,7 +992,7 @@ export class SessionMessageHandler extends BaseMessageHandler { // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || - errorMsg.includes('(code: -32000)') || + errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) || errorMsg.includes('Unauthorized') || errorMsg.includes('Invalid token') || errorMsg.includes('No active ACP session')