feat(core,cli): migrate console.debug to debugLogger (M3 Phase 1-3)

This commit is contained in:
tanzhenxin 2026-01-25 20:57:25 +08:00
parent ba2824b0b0
commit 3959b73bce
63 changed files with 554 additions and 538 deletions

View file

@ -47,16 +47,6 @@ import { mcpCommand } from '../commands/mcp.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { buildWebSearchConfig } from './webSearch.js';
// Simple console logger for now - replace with actual logger if available
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args: any[]) => console.debug('[DEBUG]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
warn: (...args: any[]) => console.warn('[WARN]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) => console.error('[ERROR]', ...args),
};
const VALID_APPROVAL_MODE_VALUES = [
'plan',
'default',
@ -626,7 +616,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
export async function loadHierarchicalGeminiMemory(
currentWorkingDirectory: string,
includeDirectoriesToReadGemini: readonly string[] = [],
debugMode: boolean,
fileService: FileDiscoveryService,
extensionContextFilePaths: string[] = [],
folderTrust: boolean,
@ -641,17 +630,10 @@ export async function loadHierarchicalGeminiMemory(
// function to signal that it should skip the workspace search.
const effectiveCwd = isHomeDirectory ? '' : currentWorkingDirectory;
if (debugMode) {
logger.debug(
`CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory} (memoryImportFormat: ${memoryImportFormat})`,
);
}
// Directly call the server function with the corrected path.
return loadServerHierarchicalMemory(
effectiveCwd,
includeDirectoriesToReadGemini,
debugMode,
fileService,
extensionContextFilePaths,
folderTrust,
@ -698,11 +680,7 @@ export async function loadCliConfig(
'output-language.md',
);
if (fs.existsSync(outputLanguageFilePath)) {
if (debugMode) {
logger.debug(
`Found output-language.md, adding to context files: ${outputLanguageFilePath}`,
);
}
// output-language.md found - will be added to context files
} else {
outputLanguageFilePath = undefined;
}
@ -749,7 +727,7 @@ export async function loadCliConfig(
approvalMode !== ApprovalMode.DEFAULT &&
approvalMode !== ApprovalMode.PLAN
) {
logger.warn(
console.warn(
`Approval mode overridden to "default" because the current folder is not trusted.`,
);
approvalMode = ApprovalMode.DEFAULT;

View file

@ -467,7 +467,7 @@ export async function runNonInteractive(
process.removeListener('SIGINT', shutdownHandler);
process.removeListener('SIGTERM', shutdownHandler);
if (isTelemetrySdkInitialized()) {
await shutdownTelemetry(config);
await shutdownTelemetry();
}
}
});

View file

@ -139,10 +139,6 @@ describe('CommandService', () => {
const commands = service.getCommands();
expect(commands).toHaveLength(1);
expect(commands).toEqual([mockCommandA]);
expect(console.debug).toHaveBeenCalledWith(
'A command loader failed:',
error,
);
});
it('getCommands should return a readonly array that cannot be mutated', async () => {

View file

@ -6,6 +6,9 @@
import type { SlashCommand } from '../ui/commands/types.js';
import type { ICommandLoader } from './types.js';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
const debugLogger = createDebugLogger('CLI_COMMANDS');
/**
* Orchestrates the discovery and loading of all slash commands for the CLI.
@ -57,7 +60,7 @@ export class CommandService {
if (result.status === 'fulfilled') {
allCommands.push(...result.value);
} else {
console.debug('A command loader failed:', result.reason);
debugLogger.debug('A command loader failed:', result.reason);
}
}

View file

@ -584,10 +584,13 @@ export const AppContainer = (props: AppContainerProps) => {
[visionSwitchResolver],
);
// onDebugMessage should log to console, not update footer debugMessage
const onDebugMessage = useCallback((message: string) => {
console.debug(message);
}, []);
// onDebugMessage should log to debug logfile, not update footer debugMessage
const onDebugMessage = useCallback(
(message: string) => {
config.getDebugLogger().debug(message);
},
[config],
);
const performMemoryRefresh = useCallback(async () => {
historyManager.addItem(
@ -603,7 +606,6 @@ export const AppContainer = (props: AppContainerProps) => {
settings.merged.context?.loadMemoryFromIncludeDirectories
? config.getWorkspaceContext().getDirectories()
: [],
config.getDebugMode(),
config.getFileService(),
config.getExtensionContextFilePaths(),
config.isTrustedFolder(),

View file

@ -48,7 +48,7 @@ export const copyCommand: SlashCommand = {
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.debug(message);
context.services.config?.getDebugLogger().debug(message);
return {
type: 'message',

View file

@ -113,7 +113,6 @@ export const directoryCommand: SlashCommand = {
...config.getWorkspaceContext().getDirectories(),
...pathsToAdd,
],
config.getDebugMode(),
config.getFileService(),
config.getExtensionContextFilePaths(),
config.getFolderTrust(),

View file

@ -309,7 +309,6 @@ export const memoryCommand: SlashCommand = {
config.shouldLoadMemoryFromIncludeDirectories()
? config.getWorkspaceContext().getDirectories()
: [],
config.getDebugMode(),
config.getFileService(),
config.getExtensionContextFilePaths(),
config.getFolderTrust(),

View file

@ -219,20 +219,13 @@ describe('updateGitignore', () => {
});
it('handles permission errors gracefully', async () => {
const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
const fsModule = await import('node:fs');
const writeFileSpy = vi
.spyOn(fsModule.promises, 'writeFile')
.mockRejectedValue(new Error('Permission denied'));
await expect(updateGitignore(scratchDir)).resolves.toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to update .gitignore:',
expect.any(Error),
);
writeFileSpy.mockRestore();
consoleSpy.mockRestore();
});
});

View file

@ -21,6 +21,9 @@ import type { SlashCommand, SlashCommandActionReturn } from './types.js';
import { CommandKind } from './types.js';
import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js';
import { t } from '../../i18n/index.js';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
const debugLogger = createDebugLogger('SETUP_GITHUB');
export const GITHUB_WORKFLOW_PATHS = [
'qwen-dispatch/qwen-dispatch.yml',
@ -85,7 +88,7 @@ export async function updateGitignore(gitRepoRoot: string): Promise<void> {
}
}
} catch (error) {
console.debug('Failed to update .gitignore:', error);
debugLogger.debug('Failed to update .gitignore:', error);
// Continue without failing the whole command
}
}
@ -112,7 +115,7 @@ export const setupGithubCommand: SlashCommand = {
try {
gitRepoRoot = getGitRepoRoot();
} catch (_error) {
console.debug(`Failed to get git repo root:`, _error);
debugLogger.debug(`Failed to get git repo root:`, _error);
throw new Error(
'Unable to determine the GitHub repository. /setup-github must be run from a git repository.',
);
@ -128,7 +131,7 @@ export const setupGithubCommand: SlashCommand = {
try {
await fs.promises.mkdir(githubWorkflowsDir, { recursive: true });
} catch (_error) {
console.debug(
debugLogger.debug(
`Failed to create ${githubWorkflowsDir} directory:`,
_error,
);

View file

@ -57,9 +57,9 @@ export function useWelcomeBack(
}
} catch (error) {
// Silently ignore errors - welcome back is not critical
console.debug('Welcome back check failed:', error);
config.getDebugLogger().debug('Welcome back check failed:', error);
}
}, [settings.ui?.enableWelcomeBack]);
}, [config, settings.ui?.enableWelcomeBack]);
// Handle welcome back dialog selection
const handleWelcomeBackSelection = useCallback(

View file

@ -31,6 +31,9 @@ import { promisify } from 'node:util';
import { isKittyProtocolEnabled } from './kittyProtocolDetector.js';
import { VSCODE_SHIFT_ENTER_SEQUENCE } from './platformConstants.js';
import { t } from '../../i18n/index.js';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
const debugLogger = createDebugLogger('TERMINAL_SETUP');
const execAsync = promisify(exec);
@ -96,7 +99,7 @@ async function detectTerminal(): Promise<SupportedTerminal | null> {
return 'trae';
} catch (error) {
// Continue detection even if process check fails
console.debug('Parent process detection failed:', error);
debugLogger.debug('Parent process detection failed:', error);
}
}

View file

@ -6,6 +6,9 @@
import { execSync } from 'node:child_process';
import { ProxyAgent } from 'undici';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
const debugLogger = createDebugLogger('GIT');
/**
* Checks if a directory is within a git repository hosted on GitHub.
@ -24,7 +27,7 @@ export const isGitHubRepository = (): boolean => {
return pattern.test(remotes);
} catch (_error) {
// If any filesystem error occurs, assume not a git repo
console.debug(`Failed to get git remote:`, _error);
debugLogger.debug(`Failed to get git remote:`, _error);
return false;
}
};
@ -83,7 +86,7 @@ export const getLatestGitHubRelease = async (
}
return releaseTag;
} catch (_error) {
console.debug(
debugLogger.debug(
`Failed to determine latest qwen-code-action release:`,
_error,
);

View file

@ -301,11 +301,8 @@ describe('extractUsageFromGeminiClient', () => {
throw new Error('Test error');
}),
};
const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
const result = extractUsageFromGeminiClient(client);
expect(result).toBeUndefined();
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it('should skip responses without usageMetadata', () => {

View file

@ -16,6 +16,7 @@ import type {
import {
OutputFormat,
ToolErrorType,
createDebugLogger,
getMCPServerStatus,
} from '@qwen-code/qwen-code-core';
import type { Part, PartListUnion } from '@google/genai';
@ -29,6 +30,8 @@ import type { JsonOutputAdapterInterface } from '../nonInteractive/io/BaseJsonOu
import { computeSessionStats } from '../ui/utils/computeStats.js';
import { getAvailableCommands } from '../nonInteractiveCliCommands.js';
const debugLogger = createDebugLogger('NON_INTERACTIVE');
/**
* Normalizes various part list formats into a consistent Part[] array.
*
@ -144,7 +147,7 @@ export function extractUsageFromGeminiClient(
}
}
} catch (error) {
console.debug('Failed to extract usage metadata:', error);
debugLogger.debug('Failed to extract usage metadata:', error);
}
return undefined;

View file

@ -107,7 +107,11 @@ import {
} from '../services/sessionService.js';
import { randomUUID } from 'node:crypto';
import { loadServerHierarchicalMemory } from '../utils/memoryDiscovery.js';
import { createDebugLogger, type DebugLogger } from '../utils/debugLogger.js';
import {
createDebugLogger,
setDebugLogSession,
type DebugLogger,
} from '../utils/debugLogger.js';
import {
ModelsConfig,
@ -513,7 +517,8 @@ export class Config {
constructor(params: ConfigParameters) {
this.sessionId = params.sessionId ?? randomUUID();
this.sessionData = params.sessionData;
this.debugLogger = createDebugLogger(this);
setDebugLogSession(this);
this.debugLogger = createDebugLogger();
this.embeddingModel = params.embeddingModel ?? DEFAULT_QWEN_EMBEDDING_MODEL;
this.fileSystemService = new StandardFileSystemService();
this.sandbox = params.sandbox;
@ -713,7 +718,6 @@ export class Config {
this.shouldLoadMemoryFromIncludeDirectories()
? this.getWorkspaceContext().getDirectories()
: [],
this.getDebugMode(),
this.getFileService(),
this.getExtensionContextFilePaths(),
this.getFolderTrust(),
@ -831,7 +835,8 @@ export class Config {
): string {
this.sessionId = sessionId ?? randomUUID();
this.sessionData = sessionData;
this.debugLogger = createDebugLogger(this);
setDebugLogSession(this);
this.debugLogger = createDebugLogger();
this.chatRecordingService = this.chatRecordingEnabled
? new ChatRecordingService(this)
: undefined;
@ -1654,7 +1659,7 @@ export class Config {
}
await registry.discoverAllTools();
console.debug('ToolRegistry created', registry.getAllToolNames());
this.debugLogger.debug('ToolRegistry created', registry.getAllToolNames());
return registry;
}
}

View file

@ -72,6 +72,20 @@ vi.mock('../utils/session.js', () => ({
sessionId: 'test-session-id',
}));
vi.mock('../utils/debugLogger.js', async (importOriginal) => {
const original =
await importOriginal<typeof import('../utils/debugLogger.js')>();
return {
...original,
createDebugLogger: () => ({
debug: (...args: unknown[]) => console.debug(...args),
info: (...args: unknown[]) => console.info(...args),
warn: (...args: unknown[]) => console.warn(...args),
error: (...args: unknown[]) => console.error(...args),
}),
};
});
describe('Logger', () => {
let logger: Logger;
const testSessionId = 'test-session-id';

View file

@ -8,6 +8,7 @@ import path from 'node:path';
import { promises as fs } from 'node:fs';
import type { Content } from '@google/genai';
import type { Storage } from '../config/storage.js';
import { createDebugLogger, type DebugLogger } from '../utils/debugLogger.js';
const LOG_FILE_NAME = 'logs.json';
@ -74,12 +75,14 @@ export class Logger {
private messageId = 0; // Instance-specific counter for the next messageId
private initialized = false;
private logs: LogEntry[] = []; // In-memory cache, ideally reflects the last known state of the file
private debugLogger: DebugLogger;
constructor(
sessionId: string,
private readonly storage: Storage,
) {
this.sessionId = sessionId;
this.debugLogger = createDebugLogger('LOGGER');
}
private async _readLogFile(): Promise<LogEntry[]> {
@ -90,7 +93,7 @@ export class Logger {
const fileContent = await fs.readFile(this.logFilePath, 'utf-8');
const parsedLogs = JSON.parse(fileContent);
if (!Array.isArray(parsedLogs)) {
console.debug(
this.debugLogger.debug(
`Log file at ${this.logFilePath} is not a valid JSON array. Starting with empty logs.`,
);
await this._backupCorruptedLogFile('malformed_array');
@ -110,14 +113,14 @@ export class Logger {
return [];
}
if (error instanceof SyntaxError) {
console.debug(
this.debugLogger.debug(
`Invalid JSON in log file ${this.logFilePath}. Backing up and starting fresh.`,
error,
);
await this._backupCorruptedLogFile('invalid_json');
return [];
}
console.debug(
this.debugLogger.debug(
`Failed to read or parse log file ${this.logFilePath}:`,
error,
);
@ -130,7 +133,7 @@ export class Logger {
const backupPath = `${this.logFilePath}.${reason}.${Date.now()}.bak`;
try {
await fs.rename(this.logFilePath, backupPath);
console.debug(`Backed up corrupted log file to ${backupPath}`);
this.debugLogger.debug(`Backed up corrupted log file to ${backupPath}`);
} catch (_backupError) {
// If rename fails (e.g. file doesn't exist), no need to log an error here as the primary error (e.g. invalid JSON) is already handled.
}
@ -165,7 +168,7 @@ export class Logger {
: 0;
this.initialized = true;
} catch (err) {
console.error('Failed to initialize logger:', err);
this.debugLogger.error('Failed to initialize logger:', err);
this.initialized = false;
}
}
@ -174,7 +177,9 @@ export class Logger {
entryToAppend: LogEntry,
): Promise<LogEntry | null> {
if (!this.logFilePath) {
console.debug('Log file path not set. Cannot persist log entry.');
this.debugLogger.debug(
'Log file path not set. Cannot persist log entry.',
);
throw new Error('Log file path not set during update attempt.');
}
@ -182,7 +187,7 @@ export class Logger {
try {
currentLogsOnDisk = await this._readLogFile();
} catch (readError) {
console.debug(
this.debugLogger.debug(
'Critical error reading log file before append:',
readError,
);
@ -213,7 +218,7 @@ export class Logger {
);
if (entryExists) {
console.debug(
this.debugLogger.debug(
`Duplicate log entry detected and skipped: session ${entryToAppend.sessionId}, messageId ${entryToAppend.messageId}`,
);
this.logs = currentLogsOnDisk; // Ensure in-memory is synced with disk
@ -231,7 +236,7 @@ export class Logger {
this.logs = currentLogsOnDisk;
return entryToAppend; // Return the successfully appended entry
} catch (error) {
console.debug('Error writing to log file:', error);
this.debugLogger.debug('Error writing to log file:', error);
throw error;
}
}
@ -250,7 +255,7 @@ export class Logger {
async logMessage(type: MessageSenderType, message: string): Promise<void> {
if (!this.initialized || this.sessionId === undefined) {
console.debug(
this.debugLogger.debug(
'Logger not initialized or session ID missing. Cannot log message.',
);
return;
@ -322,7 +327,7 @@ export class Logger {
async saveCheckpoint(conversation: Content[], tag: string): Promise<void> {
if (!this.initialized) {
console.error(
this.debugLogger.error(
'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.',
);
return;
@ -332,13 +337,13 @@ export class Logger {
try {
await fs.writeFile(path, JSON.stringify(conversation, null, 2), 'utf-8');
} catch (error) {
console.error('Error writing to checkpoint file:', error);
this.debugLogger.error('Error writing to checkpoint file:', error);
}
}
async loadCheckpoint(tag: string): Promise<Content[]> {
if (!this.initialized) {
console.error(
this.debugLogger.error(
'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.',
);
return [];
@ -349,7 +354,7 @@ export class Logger {
const fileContent = await fs.readFile(path, 'utf-8');
const parsedContent = JSON.parse(fileContent);
if (!Array.isArray(parsedContent)) {
console.warn(
this.debugLogger.warn(
`Checkpoint file at ${path} is not a valid JSON array. Returning empty checkpoint.`,
);
return [];
@ -361,14 +366,17 @@ export class Logger {
// This is okay, it just means the checkpoint doesn't exist in either format.
return [];
}
console.error(`Failed to read or parse checkpoint file ${path}:`, error);
this.debugLogger.error(
`Failed to read or parse checkpoint file ${path}:`,
error,
);
return [];
}
}
async deleteCheckpoint(tag: string): Promise<boolean> {
if (!this.initialized || !this.qwenDir) {
console.error(
this.debugLogger.error(
'Logger not initialized or checkpoint file path not set. Cannot delete checkpoint.',
);
return false;
@ -384,7 +392,10 @@ export class Logger {
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code !== 'ENOENT') {
console.error(`Failed to delete checkpoint file ${newPath}:`, error);
this.debugLogger.error(
`Failed to delete checkpoint file ${newPath}:`,
error,
);
throw error; // Rethrow unexpected errors
}
// It's okay if it doesn't exist.
@ -399,7 +410,10 @@ export class Logger {
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code !== 'ENOENT') {
console.error(`Failed to delete checkpoint file ${oldPath}:`, error);
this.debugLogger.error(
`Failed to delete checkpoint file ${oldPath}:`,
error,
);
throw error; // Rethrow unexpected errors
}
// It's okay if it doesn't exist.
@ -428,7 +442,7 @@ export class Logger {
return false; // It truly doesn't exist in either format.
}
// A different error occurred.
console.error(
this.debugLogger.error(
`Failed to check checkpoint existence for ${
filePath ?? `path for tag "${tag}"`
}:`,

View file

@ -25,13 +25,9 @@ import * as path from 'node:path';
import { EnvHttpProxyAgent } from 'undici';
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';
import { IDE_REQUEST_TIMEOUT_MS } from './constants.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args: any[]) => console.debug('[DEBUG] [IDEClient]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) => console.error('[ERROR] [IDEClient]', ...args),
};
const debugLogger = createDebugLogger('IDE');
export type DiffUpdateResult =
| {
@ -261,7 +257,7 @@ export class IdeClient {
);
const errorMessage =
textPart?.text ?? `Tool 'openDiff' reported an error.`;
logger.debug(
debugLogger.debug(
`Request for openDiff ${filePath} failed with isError:`,
errorMessage,
);
@ -270,7 +266,7 @@ export class IdeClient {
}
})
.catch((err) => {
logger.debug(`Request for openDiff ${filePath} failed:`, err);
debugLogger.debug(`Request for openDiff ${filePath} failed:`, err);
this.diffResponses.delete(filePath);
reject(err);
});
@ -342,7 +338,7 @@ export class IdeClient {
);
const errorMessage =
textPart?.text ?? `Tool 'closeDiff' reported an error.`;
logger.debug(
debugLogger.debug(
`Request for closeDiff ${filePath} failed with isError:`,
errorMessage,
);
@ -361,14 +357,14 @@ export class IdeClient {
return undefined;
}
} catch (_e) {
logger.debug(
debugLogger.debug(
`Invalid JSON in closeDiff response for ${filePath}:`,
textPart.text,
);
}
}
} catch (err) {
logger.debug(`Request for closeDiff ${filePath} failed:`, err);
debugLogger.debug(`Request for closeDiff ${filePath} failed:`, err);
}
return undefined;
}
@ -434,7 +430,7 @@ export class IdeClient {
return;
}
try {
logger.debug('Discovering tools from IDE...');
debugLogger.debug('Discovering tools from IDE...');
const response = await this.client.request(
{ method: 'tools/list', params: {} },
ListToolsResultSchema,
@ -444,11 +440,11 @@ export class IdeClient {
this.availableTools = response.tools.map((tool) => tool.name);
if (this.availableTools.length > 0) {
logger.debug(
debugLogger.debug(
`Discovered ${this.availableTools.length} tools from IDE: ${this.availableTools.join(', ')}`,
);
} else {
logger.debug(
debugLogger.debug(
'IDE supports tool discovery, but no tools are available.',
);
}
@ -459,9 +455,9 @@ export class IdeClient {
error instanceof Error &&
!error.message?.includes('Method not found')
) {
logger.error(`Error discovering tools from IDE: ${error.message}`);
debugLogger.error(`Error discovering tools from IDE: ${error.message}`);
} else {
logger.debug('IDE does not support tool discovery.');
debugLogger.debug('IDE does not support tool discovery.');
}
this.availableTools = [];
}
@ -485,11 +481,11 @@ export class IdeClient {
}
if (details) {
if (logToConsole) {
logger.error(details);
debugLogger.error(details);
} else {
// We only want to log disconnect messages to debug
// if they are not already being logged to the console.
logger.debug(details);
debugLogger.debug(details);
}
}
}
@ -557,12 +553,15 @@ export class IdeClient {
if (Array.isArray(parsedArgs)) {
args = parsedArgs;
} else {
logger.error(
debugLogger.error(
'QWEN_CODE_IDE_SERVER_STDIO_ARGS must be a JSON array string.',
);
}
} catch (e) {
logger.error('Failed to parse QWEN_CODE_IDE_SERVER_STDIO_ARGS:', e);
debugLogger.error(
'Failed to parse QWEN_CODE_IDE_SERVER_STDIO_ARGS:',
e,
);
}
}
@ -640,7 +639,7 @@ export class IdeClient {
fileRegex.test(file),
);
} catch (e) {
logger.debug('Failed to read IDE connection directory:', e);
debugLogger.debug('Failed to read IDE connection directory:', e);
return [];
}
@ -654,13 +653,13 @@ export class IdeClient {
const parsed = JSON.parse(content);
return { file, mtimeMs: stat.mtimeMs, parsed };
} catch (e) {
logger.debug('Failed to parse JSON from lock file: ', e);
debugLogger.debug('Failed to parse JSON from lock file: ', e);
return { file, mtimeMs: stat.mtimeMs, parsed: undefined };
}
} catch (e) {
// If we can't stat/read the file, treat it as very old so it doesn't
// win ties, and skip parsing by returning undefined content.
logger.debug('Failed to read/stat IDE lock file:', e);
debugLogger.debug('Failed to read/stat IDE lock file:', e);
return { file, mtimeMs: -Infinity, parsed: undefined };
}
}),
@ -742,7 +741,7 @@ export class IdeClient {
resolver({ status: 'accepted', content });
this.diffResponses.delete(filePath);
} else {
logger.debug(`No resolver found for ${filePath}`);
debugLogger.debug(`No resolver found for ${filePath}`);
}
},
);
@ -756,7 +755,7 @@ export class IdeClient {
resolver({ status: 'rejected', content: undefined });
this.diffResponses.delete(filePath);
} else {
logger.debug(`No resolver found for ${filePath}`);
debugLogger.debug(`No resolver found for ${filePath}`);
}
},
);
@ -772,7 +771,7 @@ export class IdeClient {
resolver({ status: 'rejected', content: undefined });
this.diffResponses.delete(filePath);
} else {
logger.debug(`No resolver found for ${filePath}`);
debugLogger.debug(`No resolver found for ${filePath}`);
}
},
);
@ -781,7 +780,7 @@ export class IdeClient {
private async establishHttpConnection(port: string): Promise<boolean> {
let transport: StreamableHTTPClientTransport | undefined;
try {
logger.debug('Attempting to connect to IDE via HTTP SSE');
debugLogger.debug('Attempting to connect to IDE via HTTP SSE');
this.client = new Client({
name: 'streamable-http-client',
// TODO(#3487): use the CLI version here.
@ -812,7 +811,7 @@ export class IdeClient {
try {
await transport.close();
} catch (closeError) {
logger.debug('Failed to close transport:', closeError);
debugLogger.debug('Failed to close transport:', closeError);
}
}
return false;
@ -825,7 +824,7 @@ export class IdeClient {
}: StdioConfig): Promise<boolean> {
let transport: StdioClientTransport | undefined;
try {
logger.debug('Attempting to connect to IDE via stdio');
debugLogger.debug('Attempting to connect to IDE via stdio');
this.client = new Client({
name: 'stdio-client',
// TODO(#3487): use the CLI version here.
@ -846,7 +845,7 @@ export class IdeClient {
try {
await transport.close();
} catch (closeError) {
logger.debug('Failed to close transport:', closeError);
debugLogger.debug('Failed to close transport:', closeError);
}
}
return false;

View file

@ -13,6 +13,7 @@ import type { OAuthToken } from './token-storage/types.js';
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
import { getErrorMessage } from '../utils/errors.js';
import { OAuthUtils } from './oauth-utils.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import {
MCP_OAUTH_CLIENT_NAME,
OAUTH_REDIRECT_PORT,
@ -21,6 +22,8 @@ import {
export const OAUTH_DISPLAY_MESSAGE_EVENT = 'oauth-display-message' as const;
const debugLogger = createDebugLogger('MCP_OAUTH');
/**
* OAuth configuration for an MCP server.
*/
@ -610,7 +613,7 @@ export class MCPOAuthProvider {
// If no authorization URL is provided, try to discover OAuth configuration
if (!config.authorizationUrl && mcpServerUrl) {
console.debug(`Starting OAuth for MCP server "${serverName}"…
debugLogger.debug(`Starting OAuth for MCP server "${serverName}"…
No authorization URL; using OAuth discovery`);
// First check if the server requires authentication via WWW-Authenticate header
@ -647,7 +650,7 @@ export class MCPOAuthProvider {
}
}
} catch (error) {
console.debug(
debugLogger.debug(
`Failed to check endpoint for authentication requirements: ${getErrorMessage(error)}`,
);
}
@ -692,7 +695,7 @@ export class MCPOAuthProvider {
const authUrl = new URL(config.authorizationUrl);
const serverUrl = `${authUrl.protocol}//${authUrl.host}`;
console.debug('→ Attempting dynamic client registration...');
debugLogger.debug('→ Attempting dynamic client registration...');
// Get the authorization server metadata for registration
const authServerMetadata =
@ -718,7 +721,7 @@ export class MCPOAuthProvider {
config.clientSecret = clientRegistration.client_secret;
}
console.debug('✓ Dynamic client registration successful');
debugLogger.debug('✓ Dynamic client registration successful');
} else {
throw new Error(
'No client ID provided and dynamic registration not supported',
@ -767,7 +770,9 @@ ${authUrl}
// Wait for callback
const { code } = await callbackPromise;
console.debug('✓ Authorization code received, exchanging for tokens...');
debugLogger.debug(
'✓ Authorization code received, exchanging for tokens...',
);
// Exchange code for tokens
const tokenResponse = await this.exchangeCodeForToken(
@ -802,7 +807,7 @@ ${authUrl}
config.tokenUrl,
mcpServerUrl,
);
console.debug('✓ Authentication successful! Token saved.');
debugLogger.debug('✓ Authentication successful! Token saved.');
// Verify token was saved
const savedToken = await this.tokenStorage.getCredentials(serverName);
@ -813,7 +818,7 @@ ${authUrl}
.update(savedToken.token.accessToken)
.digest('hex')
.slice(0, 8);
console.debug(
debugLogger.debug(
`✓ Token verification successful (fingerprint: ${tokenFingerprint})`,
);
} else {
@ -840,22 +845,22 @@ ${authUrl}
serverName: string,
config: MCPOAuthConfig,
): Promise<string | null> {
console.debug(`Getting valid token for server: ${serverName}`);
debugLogger.debug(`Getting valid token for server: ${serverName}`);
const credentials = await this.tokenStorage.getCredentials(serverName);
if (!credentials) {
console.debug(`No credentials found for server: ${serverName}`);
debugLogger.debug(`No credentials found for server: ${serverName}`);
return null;
}
const { token } = credentials;
console.debug(
debugLogger.debug(
`Found token for server: ${serverName}, expired: ${this.tokenStorage.isTokenExpired(token)}`,
);
// Check if token is expired
if (!this.tokenStorage.isTokenExpired(token)) {
console.debug(`Returning valid token for server: ${serverName}`);
debugLogger.debug(`Returning valid token for server: ${serverName}`);
return token.accessToken;
}

View file

@ -6,6 +6,9 @@
import type { MCPOAuthConfig } from './oauth-provider.js';
import { getErrorMessage } from '../utils/errors.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('MCP_OAUTH');
/**
* OAuth authorization server metadata as per RFC 8414.
@ -94,7 +97,7 @@ export class OAuthUtils {
}
return (await response.json()) as OAuthProtectedResourceMetadata;
} catch (error) {
console.debug(
debugLogger.debug(
`Failed to fetch protected resource metadata from ${resourceMetadataUrl}: ${getErrorMessage(error)}`,
);
return null;
@ -117,7 +120,7 @@ export class OAuthUtils {
}
return (await response.json()) as OAuthAuthorizationServerMetadata;
} catch (error) {
console.debug(
debugLogger.debug(
`Failed to fetch authorization server metadata from ${authServerMetadataUrl}: ${getErrorMessage(error)}`,
);
return null;
@ -205,7 +208,7 @@ export class OAuthUtils {
}
}
console.debug(
debugLogger.debug(
`Metadata discovery failed for authorization server ${authServerUrl}`,
);
return null;
@ -259,7 +262,7 @@ export class OAuthUtils {
}
// Fallback: try well-known endpoints at the base URL
console.debug(`Trying OAuth discovery fallback at ${serverUrl}`);
debugLogger.debug(`Trying OAuth discovery fallback at ${serverUrl}`);
const authServerMetadata =
await this.discoverAuthorizationServerMetadata(serverUrl);
@ -276,7 +279,7 @@ export class OAuthUtils {
return null;
} catch (error) {
console.debug(
debugLogger.debug(
`Failed to discover OAuth configuration: ${getErrorMessage(error)}`,
);
return null;

View file

@ -14,12 +14,15 @@ import { EventEmitter } from 'events';
import type { Config } from '../config/config.js';
import { randomUUID } from 'node:crypto';
import { formatFetchErrorForUser } from '../utils/fetch.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import {
SharedTokenManager,
TokenManagerError,
TokenError,
} from './sharedTokenManager.js';
const debugLogger = createDebugLogger('QWEN_OAUTH');
// OAuth Endpoints
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
@ -312,7 +315,7 @@ export class QwenOAuth2Client implements IQwenOAuth2Client {
}
const result = (await response.json()) as DeviceAuthorizationResponse;
console.debug('Device authorization result:', result);
debugLogger.debug('Device authorization result:', result);
// Check if the response indicates success
if (!isDeviceAuthorizationSuccess(result)) {
@ -498,20 +501,22 @@ export async function getQwenOAuthClient(
if (error instanceof TokenManagerError) {
switch (error.type) {
case TokenError.NO_REFRESH_TOKEN:
console.debug(
debugLogger.debug(
'No refresh token available, proceeding with device flow',
);
break;
case TokenError.REFRESH_FAILED:
console.debug('Token refresh failed, proceeding with device flow');
debugLogger.debug(
'Token refresh failed, proceeding with device flow',
);
break;
case TokenError.NETWORK_ERROR:
console.warn(
debugLogger.warn(
'Network error during token refresh, trying device flow',
);
break;
default:
console.warn('Token manager error:', (error as Error).message);
debugLogger.warn('Token manager error:', (error as Error).message);
}
}
@ -678,7 +683,7 @@ async function authWithQwenDeviceFlow(
return null;
}
const message = 'Authentication cancelled by user.';
console.debug('\n' + message);
debugLogger.debug('\n' + message);
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', message);
return { success: false, reason: 'cancelled', message };
};
@ -702,14 +707,11 @@ async function authWithQwenDeviceFlow(
// causing the entire Node.js process to crash.
if (childProcess) {
childProcess.on('error', (err) => {
console.debug(
'Browser launch failed:',
err.message || 'Unknown error',
);
debugLogger.debug('Browser launch failed:', err.message || err);
});
}
} catch (err) {
console.debug(
debugLogger.debug(
'Failed to open browser:',
err instanceof Error ? err.message : 'Unknown error',
);
@ -748,7 +750,7 @@ async function authWithQwenDeviceFlow(
}
emitAuthProgress('polling', 'Waiting for authorization...');
console.debug('Waiting for authorization...\n');
debugLogger.debug('Waiting for authorization...\n');
// Poll for the token
let pollInterval = 2000; // 2 seconds, can be increased if slow_down is received
@ -764,7 +766,7 @@ async function authWithQwenDeviceFlow(
}
try {
console.debug('polling for token...');
debugLogger.debug('polling for token...');
const tokenResponse = await client.pollDeviceToken({
device_code: deviceAuth.device_code,
code_verifier,
@ -808,7 +810,9 @@ async function authWithQwenDeviceFlow(
'Authentication successful! Access token obtained.',
);
console.debug('Authentication successful! Access token obtained.');
debugLogger.debug(
'Authentication successful! Access token obtained.',
);
return { success: true };
}
@ -819,7 +823,7 @@ async function authWithQwenDeviceFlow(
// Handle slow_down error by increasing poll interval
if (pendingData.slowDown) {
pollInterval = Math.min(pollInterval * 1.5, 10000); // Increase by 50%, max 10 seconds
console.debug(
debugLogger.debug(
`\nServer requested to slow down, increasing poll interval to ${pollInterval}ms'`,
);
} else {
@ -981,7 +985,7 @@ export async function clearQwenCredentials(): Promise<void> {
try {
const filePath = getQwenCachedCredentialPath();
await fs.unlink(filePath);
console.debug('Cached Qwen credentials cleared successfully.');
debugLogger.debug('Cached Qwen credentials cleared successfully.');
} catch (error: unknown) {
// If file doesn't exist or can't be deleted, we consider it cleared
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {

View file

@ -17,6 +17,9 @@ import {
isErrorResponse,
CredentialsClearRequiredError,
} from './qwenOAuth2.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('QWEN_OAUTH');
// File System Configuration
const QWEN_DIR = '.qwen';
@ -549,7 +552,7 @@ export class SharedTokenManager {
} catch (error) {
// Handle credentials clear required error (400 status from refresh)
if (error instanceof CredentialsClearRequiredError) {
console.debug(
debugLogger.debug(
'SharedTokenManager: Clearing memory cache due to credentials clear requirement',
);
// Clear memory cache when credentials need to be cleared

View file

@ -644,6 +644,12 @@ describe('LoopDetectionService LLM Checks', () => {
getGeminiClient: () => mockGeminiClient,
getBaseLlmClient: () => mockBaseLlmClient,
getDebugMode: () => false,
getDebugLogger: () => ({
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
}),
getTelemetryEnabled: () => true,
getModel: () => 'test-model',
} as unknown as Config;

View file

@ -431,7 +431,7 @@ export class LoopDetectionService {
});
} catch (e) {
// Do nothing, treat it as a non-loop.
this.config.getDebugMode() ? console.error(e) : console.debug(e);
this.config.getDebugLogger().error(e);
return false;
}

View file

@ -33,6 +33,10 @@ import type {
} from '../types.js';
import { EventMetadataKey } from './event-metadata-key.js';
import type { Config } from '../../config/config.js';
import {
createDebugLogger,
type DebugLogger,
} from '../../utils/debugLogger.js';
import { InstallationManager } from '../../utils/installationManager.js';
import { safeJsonStringify } from '../../utils/safeJsonStringify.js';
import { FixedDeque } from 'mnemonist';
@ -153,6 +157,7 @@ const MAX_RETRY_EVENTS = 100;
export class ClearcutLogger {
private static instance: ClearcutLogger;
private config?: Config;
private debugLogger: DebugLogger;
private sessionData: EventValue[] = [];
private promptId: string = '';
private readonly installationManager: InstallationManager;
@ -181,6 +186,7 @@ export class ClearcutLogger {
private constructor(config: Config) {
this.config = config;
this.debugLogger = createDebugLogger('CLEARCUT');
this.events = new FixedDeque<LogEventEntry[]>(Array, MAX_EVENTS);
this.promptId = config?.getSessionId() ?? '';
this.installationManager = new InstallationManager();
@ -217,15 +223,16 @@ export class ClearcutLogger {
},
]);
if (wasAtCapacity && this.config?.getDebugMode()) {
console.debug(
if (wasAtCapacity) {
this.debugLogger.debug(
`ClearcutLogger: Dropped old event to prevent memory leak (queue size: ${this.events.size})`,
);
}
} catch (error) {
if (this.config?.getDebugMode()) {
console.error('ClearcutLogger: Failed to enqueue log event.', error);
}
this.debugLogger.error(
'ClearcutLogger: Failed to enqueue log event.',
error,
);
}
}
@ -254,25 +261,21 @@ export class ClearcutLogger {
}
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
async flushToClearcut(): Promise<LogResponse> {
if (this.flushing) {
if (this.config?.getDebugMode()) {
console.debug(
'ClearcutLogger: Flush already in progress, marking pending flush.',
);
}
this.debugLogger.debug(
'ClearcutLogger: Flush already in progress, marking pending flush.',
);
this.pendingFlush = true;
return Promise.resolve({});
}
this.flushing = true;
if (this.config?.getDebugMode()) {
console.log('Flushing log events to Clearcut.');
}
this.debugLogger.debug('Flushing log events to Clearcut.');
const eventsToSend = this.events.toArray() as LogEventEntry[][];
this.events.clear();
@ -305,19 +308,15 @@ export class ClearcutLogger {
nextRequestWaitMs,
};
} else {
if (this.config?.getDebugMode()) {
console.error(
`Error flushing log events: HTTP ${response.status}: ${response.statusText}`,
);
}
this.debugLogger.error(
`Error flushing log events: HTTP ${response.status}: ${response.statusText}`,
);
// Re-queue failed events for retry
this.requeueFailedEvents(eventsToSend);
}
} catch (e: unknown) {
if (this.config?.getDebugMode()) {
console.error('Error flushing log events:', e as Error);
}
this.debugLogger.error('Error flushing log events:', e as Error);
// Re-queue failed events for retry
this.requeueFailedEvents(eventsToSend);
@ -330,9 +329,7 @@ export class ClearcutLogger {
this.pendingFlush = false;
// Fire and forget the pending flush
this.flushToClearcut().catch((error) => {
if (this.config?.getDebugMode()) {
console.debug('Error in pending flush to Clearcut:', error);
}
this.debugLogger.debug('Error in pending flush to Clearcut:', error);
});
}
@ -423,7 +420,7 @@ export class ClearcutLogger {
// Flush start event immediately
this.enqueueLogEvent(this.createLogEvent(EventNames.START_SESSION, data));
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
@ -644,14 +641,14 @@ export class ClearcutLogger {
logFlashFallbackEvent(): void {
this.enqueueLogEvent(this.createLogEvent(EventNames.FLASH_FALLBACK, []));
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
logRipgrepFallbackEvent(): void {
this.enqueueLogEvent(this.createLogEvent(EventNames.RIPGREP_FALLBACK, []));
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
@ -791,7 +788,7 @@ export class ClearcutLogger {
// Flush immediately on session end.
this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, []));
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
@ -890,7 +887,7 @@ export class ClearcutLogger {
this.createLogEvent(EventNames.EXTENSION_INSTALL, data),
);
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
@ -910,7 +907,7 @@ export class ClearcutLogger {
this.createLogEvent(EventNames.EXTENSION_UNINSTALL, data),
);
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
@ -964,7 +961,7 @@ export class ClearcutLogger {
this.createLogEvent(EventNames.EXTENSION_ENABLE, data),
);
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
@ -999,7 +996,7 @@ export class ClearcutLogger {
this.createLogEvent(EventNames.EXTENSION_DISABLE, data),
);
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
this.debugLogger.debug('Error flushing to Clearcut:', error);
});
}
@ -1078,8 +1075,8 @@ export class ClearcutLogger {
const eventsToRetry = eventsToSend.slice(-MAX_RETRY_EVENTS); // Keep only the most recent events
// Log a warning if we're dropping events
if (eventsToSend.length > MAX_RETRY_EVENTS && this.config?.getDebugMode()) {
console.warn(
if (eventsToSend.length > MAX_RETRY_EVENTS) {
this.debugLogger.warn(
`ClearcutLogger: Dropping ${
eventsToSend.length - MAX_RETRY_EVENTS
} events due to retry queue limit. Total events: ${
@ -1093,11 +1090,9 @@ export class ClearcutLogger {
const numEventsToRequeue = Math.min(eventsToRetry.length, availableSpace);
if (numEventsToRequeue === 0) {
if (this.config?.getDebugMode()) {
console.debug(
`ClearcutLogger: No events re-queued (queue size: ${this.events.size})`,
);
}
this.debugLogger.debug(
`ClearcutLogger: No events re-queued (queue size: ${this.events.size})`,
);
return;
}
@ -1116,11 +1111,9 @@ export class ClearcutLogger {
this.events.pop();
}
if (this.config?.getDebugMode()) {
console.debug(
`ClearcutLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`,
);
}
this.debugLogger.debug(
`ClearcutLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`,
);
}
}

View file

@ -26,6 +26,13 @@ import {
} from '../types.js';
import type { RumEvent, RumPayload } from './event-types.js';
const debugLoggerSpy = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
// Mock dependencies
vi.mock('../../utils/user_id.js', () => ({
getInstallationId: vi.fn(() => 'test-installation-id'),
@ -35,6 +42,15 @@ vi.mock('../../utils/safeJsonStringify.js', () => ({
safeJsonStringify: vi.fn((obj) => JSON.stringify(obj)),
}));
vi.mock('../../utils/debugLogger.js', async (importOriginal) => {
const original =
await importOriginal<typeof import('../../utils/debugLogger.js')>();
return {
...original,
createDebugLogger: () => debugLoggerSpy,
};
});
// Mock https module
vi.mock('https', () => ({
request: vi.fn(),
@ -72,6 +88,10 @@ describe('QwenLogger', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-01-01T12:00:00.000Z'));
mockConfig = makeFakeConfig();
debugLoggerSpy.debug.mockClear();
debugLoggerSpy.info.mockClear();
debugLoggerSpy.warn.mockClear();
debugLoggerSpy.error.mockClear();
// Clear singleton instance
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(QwenLogger as any).instance = undefined;
@ -126,11 +146,7 @@ describe('QwenLogger', () => {
describe('event queue management', () => {
it('should handle event overflow gracefully', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
const logger = QwenLogger.getInstance(mockConfig)!;
// Fill the queue beyond capacity
for (let i = 0; i < TEST_ONLY.MAX_EVENTS + 10; i++) {
@ -143,7 +159,7 @@ describe('QwenLogger', () => {
}
// Should have logged debug messages about dropping events
expect(consoleSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.debug).toHaveBeenCalledWith(
expect.stringContaining(
'QwenLogger: Dropped old event to prevent memory leak',
),
@ -151,11 +167,7 @@ describe('QwenLogger', () => {
});
it('should handle enqueue errors gracefully', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const logger = QwenLogger.getInstance(mockConfig)!;
// Mock the events deque to throw an error
const originalPush = logger['events'].push;
@ -170,7 +182,7 @@ describe('QwenLogger', () => {
name: 'test-event',
});
expect(consoleSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'QwenLogger: Failed to enqueue log event.',
expect.any(Error),
);
@ -182,11 +194,7 @@ describe('QwenLogger', () => {
describe('concurrent flush protection', () => {
it('should handle concurrent flush requests', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
const logger = QwenLogger.getInstance(mockConfig)!;
// Manually set the flush in progress flag to simulate concurrent access
logger['isFlushInProgress'] = true;
@ -195,7 +203,7 @@ describe('QwenLogger', () => {
const result = logger.flushToRum();
// Should have logged about pending flush
expect(consoleSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.debug).toHaveBeenCalledWith(
expect.stringContaining(
'QwenLogger: Flush already in progress, marking pending flush',
),
@ -211,11 +219,7 @@ describe('QwenLogger', () => {
describe('failed event retry mechanism', () => {
it('should requeue failed events with size limits', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
const logger = QwenLogger.getInstance(mockConfig)!;
const failedEvents: RumEvent[] = [];
for (let i = 0; i < TEST_ONLY.MAX_RETRY_EVENTS + 50; i++) {
@ -232,17 +236,13 @@ describe('QwenLogger', () => {
(logger as any).requeueFailedEvents(failedEvents);
// Should have logged about dropping events due to retry limit
expect(consoleSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.debug).toHaveBeenCalledWith(
expect.stringContaining('QwenLogger: Re-queued'),
);
});
it('should handle empty retry queue gracefully', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
const logger = QwenLogger.getInstance(mockConfig)!;
// Fill the queue to capacity first
for (let i = 0; i < TEST_ONLY.MAX_EVENTS; i++) {
@ -267,7 +267,7 @@ describe('QwenLogger', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(logger as any).requeueFailedEvents(failedEvents);
expect(consoleSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.debug).toHaveBeenCalledWith(
expect.stringContaining('QwenLogger: No events re-queued'),
);
});
@ -386,11 +386,7 @@ describe('QwenLogger', () => {
describe('error handling', () => {
it('should handle flush errors gracefully with debug mode', async () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
const logger = QwenLogger.getInstance(mockConfig)!;
// Add an event first
logger.enqueueLogEvent({
@ -412,7 +408,7 @@ describe('QwenLogger', () => {
// Wait for async operations
await vi.runAllTimersAsync();
expect(consoleSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.debug).toHaveBeenCalledWith(
'Error flushing to RUM:',
expect.any(Error),
);

View file

@ -54,6 +54,10 @@ import type {
RumOS,
} from './event-types.js';
import type { Config } from '../../config/config.js';
import {
createDebugLogger,
type DebugLogger,
} from '../../utils/debugLogger.js';
import { safeJsonStringify } from '../../utils/safeJsonStringify.js';
import { InstallationManager } from '../../utils/installationManager.js';
import { FixedDeque } from 'mnemonist';
@ -91,6 +95,7 @@ export interface LogResponse {
export class QwenLogger {
private static instance: QwenLogger;
private config?: Config;
private debugLogger: DebugLogger;
private readonly installationManager: InstallationManager;
/**
@ -121,6 +126,7 @@ export class QwenLogger {
private constructor(config: Config) {
this.config = config;
this.debugLogger = createDebugLogger('QWEN_LOGGER');
this.events = new FixedDeque<RumEvent>(Array, MAX_EVENTS);
this.installationManager = new InstallationManager();
this.userId = this.generateUserId();
@ -154,15 +160,13 @@ export class QwenLogger {
this.events.push(event);
if (wasAtCapacity && this.config?.getDebugMode()) {
console.debug(
if (wasAtCapacity) {
this.debugLogger.debug(
`QwenLogger: Dropped old event to prevent memory leak (queue size: ${this.events.size})`,
);
}
} catch (error) {
if (this.config?.getDebugMode()) {
console.error('QwenLogger: Failed to enqueue log event.', error);
}
this.debugLogger.error('QwenLogger: Failed to enqueue log event.', error);
}
}
@ -266,27 +270,21 @@ export class QwenLogger {
}
this.flushToRum().catch((error) => {
if (this.config?.getDebugMode()) {
console.debug('Error flushing to RUM:', error);
}
this.debugLogger.debug('Error flushing to RUM:', error);
});
}
async flushToRum(): Promise<LogResponse> {
if (this.isFlushInProgress) {
if (this.config?.getDebugMode()) {
console.debug(
'QwenLogger: Flush already in progress, marking pending flush.',
);
}
this.debugLogger.debug(
'QwenLogger: Flush already in progress, marking pending flush.',
);
this.pendingFlush = true;
return Promise.resolve({});
}
this.isFlushInProgress = true;
if (this.config?.getDebugMode()) {
console.log('Flushing log events to RUM.');
}
this.debugLogger.debug('Flushing log events to RUM.');
if (this.events.size === 0) {
this.isFlushInProgress = false;
return {};
@ -338,9 +336,7 @@ export class QwenLogger {
this.lastFlushTime = Date.now();
return {};
} catch (error) {
if (this.config?.getDebugMode()) {
console.error('RUM flush failed.', error);
}
this.debugLogger.error('RUM flush failed.', error);
// Re-queue failed events for retry
this.requeueFailedEvents(eventsToSend);
@ -353,9 +349,7 @@ export class QwenLogger {
this.pendingFlush = false;
// Fire and forget the pending flush
this.flushToRum().catch((error) => {
if (this.config?.getDebugMode()) {
console.debug('Error in pending flush to RUM:', error);
}
this.debugLogger.debug('Error in pending flush to RUM:', error);
});
}
}
@ -366,12 +360,10 @@ export class QwenLogger {
// Flush all pending events with the old session ID first.
// If flush fails, discard the pending events to avoid mixing sessions.
await this.flushToRum().catch((error: unknown) => {
if (this.config?.getDebugMode()) {
console.debug(
'Error flushing pending events before session start:',
error,
);
}
this.debugLogger.debug(
'Error flushing pending events before session start:',
error,
);
});
// Clear any remaining events (discard if flush failed)
@ -402,9 +394,7 @@ export class QwenLogger {
// Flush start event immediately
this.enqueueLogEvent(applicationEvent);
this.flushToRum().catch((error: unknown) => {
if (this.config?.getDebugMode()) {
console.debug('Error flushing to RUM:', error);
}
this.debugLogger.debug('Error flushing to RUM:', error);
});
}
@ -414,9 +404,7 @@ export class QwenLogger {
// Flush immediately on session end.
this.enqueueLogEvent(applicationEvent);
this.flushToRum().catch((error: unknown) => {
if (this.config?.getDebugMode()) {
console.debug('Error flushing to RUM:', error);
}
this.debugLogger.debug('Error flushing to RUM:', error);
});
}
@ -917,8 +905,8 @@ export class QwenLogger {
const eventsToRetry = eventsToSend.slice(-MAX_RETRY_EVENTS); // Keep only the most recent events
// Log a warning if we're dropping events
if (eventsToSend.length > MAX_RETRY_EVENTS && this.config?.getDebugMode()) {
console.warn(
if (eventsToSend.length > MAX_RETRY_EVENTS) {
this.debugLogger.warn(
`QwenLogger: Dropping ${
eventsToSend.length - MAX_RETRY_EVENTS
} events due to retry queue limit. Total events: ${
@ -932,11 +920,9 @@ export class QwenLogger {
const numEventsToRequeue = Math.min(eventsToRetry.length, availableSpace);
if (numEventsToRequeue === 0) {
if (this.config?.getDebugMode()) {
console.debug(
`QwenLogger: No events re-queued (queue size: ${this.events.size})`,
);
}
this.debugLogger.debug(
`QwenLogger: No events re-queued (queue size: ${this.events.size})`,
);
return;
}
@ -955,11 +941,9 @@ export class QwenLogger {
this.events.pop();
}
if (this.config?.getDebugMode()) {
console.debug(
`QwenLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`,
);
}
this.debugLogger.debug(
`QwenLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`,
);
}
}

View file

@ -46,7 +46,7 @@ describe('Telemetry SDK', () => {
});
afterEach(async () => {
await shutdownTelemetry(mockConfig);
await shutdownTelemetry();
});
it('should use gRPC exporters when protocol is grpc', () => {

View file

@ -36,6 +36,7 @@ import {
FileMetricExporter,
FileSpanExporter,
} from './file-exporters.js';
import { createDebugLogger } from '../utils/debugLogger.js';
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
@ -77,6 +78,7 @@ export function initializeTelemetry(config: Config): void {
return;
}
const debugLogger = createDebugLogger('OTEL');
const resource = resourceFromAttributes({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
[SemanticResourceAttributes.SERVICE_VERSION]: process.version,
@ -159,37 +161,34 @@ export function initializeTelemetry(config: Config): void {
try {
sdk.start();
if (config.getDebugMode()) {
console.log('OpenTelemetry SDK started successfully.');
}
debugLogger.debug('OpenTelemetry SDK started successfully.');
telemetryInitialized = true;
initializeMetrics(config);
} catch (error) {
console.error('Error starting OpenTelemetry SDK:', error);
debugLogger.error('Error starting OpenTelemetry SDK:', error);
}
process.on('SIGTERM', () => {
shutdownTelemetry(config);
shutdownTelemetry();
});
process.on('SIGINT', () => {
shutdownTelemetry(config);
shutdownTelemetry();
});
process.on('exit', () => {
shutdownTelemetry(config);
shutdownTelemetry();
});
}
export async function shutdownTelemetry(config: Config): Promise<void> {
export async function shutdownTelemetry(): Promise<void> {
if (!telemetryInitialized || !sdk) {
return;
}
const debugLogger = createDebugLogger('OTEL');
try {
await sdk.shutdown();
if (config.getDebugMode()) {
console.log('OpenTelemetry SDK shut down successfully.');
}
debugLogger.debug('OpenTelemetry SDK shut down successfully.');
} catch (error) {
console.error('Error shutting down SDK:', error);
debugLogger.error('Error shutting down SDK:', error);
} finally {
telemetryInitialized = false;
}

View file

@ -44,7 +44,7 @@ describe('telemetry', () => {
afterEach(async () => {
// Ensure we shut down telemetry even if a test fails.
if (isTelemetrySdkInitialized()) {
await shutdownTelemetry(mockConfig);
await shutdownTelemetry();
}
});
@ -56,7 +56,7 @@ describe('telemetry', () => {
it('should shutdown the telemetry service', async () => {
initializeTelemetry(mockConfig);
await shutdownTelemetry(mockConfig);
await shutdownTelemetry();
expect(mockNodeSdk.shutdown).toHaveBeenCalled();
});

View file

@ -34,6 +34,7 @@ import type {
} from './modifiable-tool.js';
import { IdeClient } from '../ide/ide-client.js';
import { safeLiteralReplace } from '../utils/textUtils.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import {
countOccurrences,
extractEditSnippet,
@ -41,6 +42,8 @@ import {
normalizeEditStrings,
} from '../utils/editHelper.js';
const debugLogger = createDebugLogger('EDIT');
export function applyReplacement(
currentContent: string | null,
oldString: string,
@ -256,12 +259,12 @@ class EditToolInvocation implements ToolInvocation<EditToolParams, ToolResult> {
throw error;
}
const errorMsg = error instanceof Error ? error.message : String(error);
console.log(`Error preparing edit: ${errorMsg}`);
debugLogger.warn(`Error preparing edit: ${errorMsg}`);
return false;
}
if (editData.error) {
console.log(`Error: ${editData.error.display}`);
debugLogger.warn(`Error: ${editData.error.display}`);
return false;
}

View file

@ -15,6 +15,9 @@ import type { FunctionDeclaration } from '@google/genai';
import type { Config } from '../config/config.js';
import { ApprovalMode } from '../config/config.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('EXIT_PLAN_MODE');
export interface ExitPlanModeParams {
plan: string;
@ -102,7 +105,7 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation<
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
debugLogger.error(
`[ExitPlanModeTool] Failed to set approval mode to "${mode}": ${errorMessage}`,
);
}
@ -135,7 +138,7 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation<
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
debugLogger.error(
`[ExitPlanModeTool] Error executing exit_plan_mode: ${errorMessage}`,
);

View file

@ -19,6 +19,9 @@ import {
import { ToolErrorType } from './tool-error.js';
import { getErrorMessage } from '../utils/errors.js';
import type { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('GLOB');
const MAX_FILE_COUNT = 100;
@ -203,7 +206,7 @@ class GlobToolInvocation extends BaseToolInvocation<
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`GlobLogic execute Error: ${errorMessage}`, error);
debugLogger.error(`GlobLogic execute Error: ${errorMessage}`, error);
const rawError = `Error during glob search operation: ${errorMessage}`;
return {
llmContent: rawError,

View file

@ -12,6 +12,9 @@ import { globStream } from 'glob';
import type { ToolInvocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolNames, ToolDisplayNames } from './tool-names.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('GREP');
import { resolveAndValidatePath } from '../utils/paths.js';
import { getErrorMessage, isNodeError } from '../utils/errors.js';
import { isGitRepository } from '../utils/gitUtils.js';
@ -183,7 +186,7 @@ class GrepToolInvocation extends BaseToolInvocation<
returnDisplay: displayMessage,
};
} catch (error) {
console.error(`Error during GrepLogic execution: ${error}`);
debugLogger.error(`Error during GrepLogic execution: ${error}`);
const errorMessage = getErrorMessage(error);
return {
llmContent: `Error during grep search operation: ${errorMessage}`,
@ -319,7 +322,7 @@ class GrepToolInvocation extends BaseToolInvocation<
});
return this.parseGrepOutput(output, absolutePath);
} catch (gitError: unknown) {
console.debug(
debugLogger.debug(
`GrepLogic: git grep failed: ${getErrorMessage(
gitError,
)}. Falling back...`,
@ -421,7 +424,7 @@ class GrepToolInvocation extends BaseToolInvocation<
});
return this.parseGrepOutput(output, absolutePath);
} catch (grepError: unknown) {
console.debug(
debugLogger.debug(
`GrepLogic: System grep failed: ${getErrorMessage(
grepError,
)}. Falling back...`,
@ -430,7 +433,7 @@ class GrepToolInvocation extends BaseToolInvocation<
}
// --- Strategy 3: Pure JavaScript Fallback ---
console.debug(
debugLogger.debug(
'GrepLogic: Falling back to JavaScript grep implementation.',
);
strategyUsed = 'javascript fallback';
@ -468,7 +471,7 @@ class GrepToolInvocation extends BaseToolInvocation<
} catch (readError: unknown) {
// Ignore errors like permission denied or file gone during read
if (!isNodeError(readError) || readError.code !== 'ENOENT') {
console.debug(
debugLogger.debug(
`GrepLogic: Could not read/process ${fileAbsolutePath}: ${getErrorMessage(
readError,
)}`,
@ -479,7 +482,7 @@ class GrepToolInvocation extends BaseToolInvocation<
return allMatches;
} catch (error: unknown) {
console.error(
debugLogger.error(
`GrepLogic: Error in performGrepSearch (Strategy: ${strategyUsed}): ${getErrorMessage(
error,
)}`,

View file

@ -253,11 +253,6 @@ describe('LSTool', () => {
return originalStat(p);
});
// Spy on console.error to verify it's called
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const invocation = lsTool.build({ path: tempRootDir });
const result = await invocation.execute(abortSignal);
@ -266,13 +261,7 @@ describe('LSTool', () => {
expect(result.llmContent).not.toContain('problematic.txt');
expect(result.returnDisplay).toBe('Listed 1 item(s).');
// Verify error was logged
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringMatching(/Error accessing.*problematic\.txt/s),
);
statSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
});

View file

@ -14,6 +14,9 @@ import type { Config } from '../config/config.js';
import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js';
import { ToolErrorType } from './tool-error.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('LS');
/**
* Parameters for the LS tool
@ -202,7 +205,7 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
});
} catch (error) {
// Log error internally but don't fail the whole listing
console.error(`Error accessing ${fullPath}: ${error}`);
debugLogger.warn(`Error accessing ${fullPath}: ${error}`);
}
}

View file

@ -38,6 +38,7 @@ import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js';
import { OAuthUtils } from '../mcp/oauth-utils.js';
import type { PromptRegistry } from '../prompts/prompt-registry.js';
import { getErrorMessage } from '../utils/errors.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import type {
Unsubscribe,
WorkspaceContext,
@ -54,6 +55,8 @@ export type SendSdkMcpMessage = (
export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
const debugLogger = createDebugLogger('MCP');
export type DiscoveredMCPPrompt = Prompt & {
serverName: string;
invoke: (params: Record<string, unknown>) => Promise<GetPromptResult>;
@ -910,7 +913,7 @@ export async function connectToMcpServer(
}
}
} catch (fetchError) {
console.debug(
debugLogger.debug(
`Failed to fetch www-authenticate header: ${getErrorMessage(
fetchError,
)}`,
@ -1360,7 +1363,7 @@ export async function createTransport(
if (debugMode) {
transport.stderr!.on('data', (data) => {
const stderrStr = data.toString().trim();
console.debug(`[DEBUG] [MCP STDERR (${mcpServerName})]: `, stderrStr);
debugLogger.debug(`MCP STDERR (${mcpServerName}):`, stderrStr);
});
}
return transport;

View file

@ -24,6 +24,9 @@ import type {
ModifyContext,
} from './modifiable-tool.js';
import { ToolErrorType } from './tool-error.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('MEMORY_TOOL');
const memoryToolSchemaData: FunctionDeclaration = {
name: 'save_memory',
@ -361,7 +364,7 @@ Project: ${projectPath} (current project only)`;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
debugLogger.error(
`[MemoryTool] Error executing save_memory for fact "${fact}" in ${scope}: ${errorMessage}`,
);
@ -435,7 +438,7 @@ export class MemoryTool
await fsAdapter.writeFile(memoryFilePath, newContent, 'utf-8');
} catch (error) {
console.error(
debugLogger.error(
`[MemoryTool] Error adding memory entry to ${memoryFilePath}:`,
error,
);

View file

@ -263,10 +263,7 @@ describe('modifyWithEditor', () => {
});
it('should handle temp file cleanup errors gracefully', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {
const unlinkSpy = vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {
throw new Error('Failed to delete file');
});
@ -278,12 +275,7 @@ describe('modifyWithEditor', () => {
vi.fn(),
);
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Error deleting temp diff file:'),
);
consoleErrorSpy.mockRestore();
expect(unlinkSpy).toHaveBeenCalledTimes(2);
});
it('should create temp files with correct naming with extension', async () => {

View file

@ -12,12 +12,15 @@ import fs from 'node:fs';
import * as Diff from 'diff';
import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js';
import { isNodeError } from '../utils/errors.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import type {
AnyDeclarativeTool,
DeclarativeTool,
ToolResult,
} from './tools.js';
const debugLogger = createDebugLogger('MODIFIABLE_TOOL');
/**
* A declarative tool that supports a modify operation.
*/
@ -128,13 +131,13 @@ function deleteTempFiles(oldPath: string, newPath: string): void {
try {
fs.unlinkSync(oldPath);
} catch {
console.error(`Error deleting temp diff file: ${oldPath}`);
debugLogger.warn(`Error deleting temp diff file: ${oldPath}`);
}
try {
fs.unlinkSync(newPath);
} catch {
console.error(`Error deleting temp diff file: ${newPath}`);
debugLogger.warn(`Error deleting temp diff file: ${newPath}`);
}
}

View file

@ -16,6 +16,9 @@ import { runRipgrep } from '../utils/ripgrepUtils.js';
import { SchemaValidator } from '../utils/schemaValidator.js';
import type { FileFilteringOptions } from '../config/constants.js';
import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('RIPGREP');
/**
* Parameters for the GrepTool (Simplified)
@ -157,7 +160,7 @@ class GrepToolInvocation extends BaseToolInvocation<
returnDisplay: displayMessage,
};
} catch (error) {
console.error(`Error during ripgrep search operation: ${error}`);
debugLogger.error('Error during ripgrep search operation:', error);
const errorMessage = getErrorMessage(error);
return {
llmContent: `Error during grep search operation: ${errorMessage}`,

View file

@ -41,6 +41,9 @@ import {
isCommandNeedsPermission,
stripShellWrapper,
} from '../utils/shell-utils.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('SHELL');
export const OUTPUT_UPDATE_INTERVAL_MS = 1000;
const DEFAULT_FOREGROUND_TIMEOUT_MS = 120000;
@ -271,7 +274,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
.filter(Boolean);
for (const line of pgrepLines) {
if (!/^\d+$/.test(line)) {
console.error(`pgrep: ${line}`);
debugLogger.warn(`pgrep: ${line}`);
}
const pid = Number(line);
if (pid !== result.pid) {
@ -280,7 +283,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
}
} else {
if (!signal.aborted) {
console.error('missing pgrep output');
debugLogger.warn('missing pgrep output');
}
}
}
@ -573,7 +576,7 @@ export class ShellTool extends BaseDeclarativeTool<
const commandCheck = isCommandAllowed(params.command, this.config);
if (!commandCheck.allowed) {
if (!commandCheck.reason) {
console.error(
debugLogger.error(
'Unexpected: isCommandAllowed returned false without a reason',
);
return `Command is not allowed: ${params.command}`;

View file

@ -156,16 +156,12 @@ describe('SkillTool', () => {
new Error('Loading failed'),
);
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
new SkillTool(config);
const failedSkillTool = new SkillTool(config);
await vi.runAllTimersAsync();
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to load skills for Skills tool:',
expect.any(Error),
expect(failedSkillTool.description).toContain(
'No skills are currently configured',
);
consoleSpy.mockRestore();
});
});
@ -375,10 +371,6 @@ describe('SkillTool', () => {
new Error('Loading failed'),
);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const params: SkillParams = {
skill: 'code-review',
};
@ -391,8 +383,6 @@ describe('SkillTool', () => {
const llmText = partToString(result.llmContent);
expect(llmText).toContain('Failed to load skill');
expect(llmText).toContain('Loading failed');
consoleSpy.mockRestore();
});
it('should not require confirmation', async () => {

View file

@ -12,6 +12,9 @@ import type { SkillManager } from '../skills/skill-manager.js';
import type { SkillConfig } from '../skills/types.js';
import { logSkillLaunch, SkillLaunchEvent } from '../telemetry/index.js';
import path from 'path';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('SKILL');
export interface SkillParams {
skill: string;
@ -71,7 +74,7 @@ export class SkillTool extends BaseDeclarativeTool<SkillParams, ToolResult> {
this.availableSkills = await this.skillManager.listSkills();
this.updateDescriptionAndSchema();
} catch (error) {
console.warn('Failed to load skills for Skills tool:', error);
debugLogger.warn('Failed to load skills for Skills tool:', error);
this.availableSkills = [];
this.updateDescriptionAndSchema();
} finally {
@ -251,7 +254,7 @@ class SkillToolInvocation extends BaseToolInvocation<SkillParams, ToolResult> {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[SkillsTool] Error using skill: ${errorMessage}`);
debugLogger.error(`[SkillsTool] Error using skill: ${errorMessage}`);
// Log failed skill launch
logSkillLaunch(

View file

@ -33,6 +33,7 @@ import { IdeClient } from '../ide/ide-client.js';
import { FixLLMEditWithInstruction } from '../utils/llm-edit-fixer.js';
import { applyReplacement } from './edit.js';
import { safeLiteralReplace } from '../utils/textUtils.js';
import { createDebugLogger } from '../utils/debugLogger.js';
interface ReplacementContext {
params: EditToolParams;
@ -47,6 +48,8 @@ interface ReplacementResult {
finalNewString: string;
}
const debugLogger = createDebugLogger('SMART_EDIT');
function restoreTrailingNewline(
originalContent: string,
modifiedContent: string,
@ -571,12 +574,12 @@ class EditToolInvocation implements ToolInvocation<EditToolParams, ToolResult> {
throw error;
}
const errorMsg = error instanceof Error ? error.message : String(error);
console.log(`Error preparing edit: ${errorMsg}`);
debugLogger.warn(`Error preparing edit: ${errorMsg}`);
return false;
}
if (editData.error) {
console.log(`Error: ${editData.error.display}`);
debugLogger.warn(`Error: ${editData.error.display}`);
return false;
}

View file

@ -150,16 +150,12 @@ describe('TaskTool', () => {
new Error('Loading failed'),
);
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
new TaskTool(config);
const failedTaskTool = new TaskTool(config);
await vi.runAllTimersAsync();
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to load subagents for Task tool:',
expect.any(Error),
expect(failedTaskTool.description).toContain(
'No subagents are currently configured',
);
consoleSpy.mockRestore();
});
});

View file

@ -34,6 +34,7 @@ import type {
SubAgentErrorEvent,
SubAgentApprovalRequestEvent,
} from '../subagents/subagent-events.js';
import { createDebugLogger } from '../utils/debugLogger.js';
export interface TaskParams {
description: string;
@ -41,6 +42,8 @@ export interface TaskParams {
subagent_type: string;
}
const debugLogger = createDebugLogger('TASK');
/**
* Task tool that enables primary agents to delegate tasks to specialized subagents.
* The tool dynamically loads available subagents and includes them in its description
@ -103,7 +106,7 @@ export class TaskTool extends BaseDeclarativeTool<TaskParams, ToolResult> {
this.availableSubagents = await this.subagentManager.listSubagents();
this.updateDescriptionAndSchema();
} catch (error) {
console.warn('Failed to load subagents for Task tool:', error);
debugLogger.warn('Failed to load subagents for Task tool:', error);
this.availableSubagents = [];
this.updateDescriptionAndSchema();
} finally {
@ -550,7 +553,7 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[TaskTool] Error running subagent: ${errorMessage}`);
debugLogger.error(`[TaskTool] Error running subagent: ${errorMessage}`);
const errorDisplay: TaskResultDisplay = {
...this.currentDisplay!,

View file

@ -15,6 +15,9 @@ import * as process from 'process';
import { QWEN_DIR } from '../utils/paths.js';
import type { Config } from '../config/config.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('TODO_WRITE');
export interface TodoItem {
id: string;
@ -370,7 +373,7 @@ ${todosJson}. Continue on with the tasks at hand if applicable.
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
debugLogger.error(
`[TodoWriteTool] Error executing todo_write: ${errorMessage}`,
);

View file

@ -118,10 +118,6 @@ describe('ToolRegistry', () => {
} as fs.Stats);
config = new Config(baseConfigParams);
toolRegistry = new ToolRegistry(config);
vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(console, 'debug').mockImplementation(() => {});
vi.spyOn(console, 'log').mockImplementation(() => {});
mockMcpClientConnect.mockReset().mockResolvedValue(undefined);
mockStdioTransportClose.mockReset();

View file

@ -23,9 +23,12 @@ import { parse } from 'shell-quote';
import { ToolErrorType } from './tool-error.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import type { EventEmitter } from 'node:events';
import { createDebugLogger } from '../utils/debugLogger.js';
type ToolParams = Record<string, unknown>;
const debugLogger = createDebugLogger('TOOL_REGISTRY');
class DiscoveredToolInvocation extends BaseToolInvocation<
ToolParams,
ToolResult
@ -198,7 +201,7 @@ export class ToolRegistry {
tool = tool.asFullyQualifiedTool();
} else {
// Decide on behavior: throw error, log warning, or allow overwrite
console.warn(
debugLogger.warn(
`Tool with name "${tool.name}" is already registered. Overwriting.`,
);
}
@ -356,8 +359,10 @@ export class ToolRegistry {
}
if (code !== 0) {
console.error(`Command failed with code ${code}`);
console.error(stderr);
debugLogger.error(
`Tool discovery command failed with code ${code}`,
);
debugLogger.error(stderr);
return reject(
new Error(`Tool discovery command failed with exit code ${code}`),
);
@ -390,7 +395,7 @@ export class ToolRegistry {
// register each function as a tool
for (const func of functions) {
if (!func.name) {
console.warn('Discovered a tool with no name. Skipping.');
debugLogger.warn('Discovered a tool with no name. Skipping.');
continue;
}
const parameters =
@ -409,7 +414,7 @@ export class ToolRegistry {
);
}
} catch (e) {
console.error(`Tool discovery command "${discoveryCmd}" failed:`, e);
debugLogger.error(`Tool discovery command "${discoveryCmd}" failed:`, e);
throw e;
}
}

View file

@ -36,6 +36,7 @@ describe('WebFetchTool', () => {
setApprovalMode: vi.fn(),
getProxy: vi.fn(),
getGeminiClient: mockGetGeminiClient,
getSessionId: vi.fn(() => 'test-session-id'),
} as unknown as Config;
});

View file

@ -24,6 +24,7 @@ import {
} from './tools.js';
import { DEFAULT_QWEN_MODEL } from '../config/models.js';
import { ToolNames, ToolDisplayNames } from './tool-names.js';
import { createDebugLogger, type DebugLogger } from '../utils/debugLogger.js';
const URL_FETCH_TIMEOUT_MS = 10000;
const MAX_CONTENT_LENGTH = 100000;
@ -49,11 +50,14 @@ class WebFetchToolInvocation extends BaseToolInvocation<
WebFetchToolParams,
ToolResult
> {
private readonly debugLogger: DebugLogger;
constructor(
private readonly config: Config,
params: WebFetchToolParams,
) {
super(params);
this.debugLogger = createDebugLogger('WEB_FETCH');
}
private async executeDirectFetch(signal: AbortSignal): Promise<ToolResult> {
@ -64,22 +68,24 @@ class WebFetchToolInvocation extends BaseToolInvocation<
url = url
.replace('github.com', 'raw.githubusercontent.com')
.replace('/blob/', '/');
console.debug(
this.debugLogger.debug(
`[WebFetchTool] Converted GitHub blob URL to raw URL: ${url}`,
);
}
try {
console.debug(`[WebFetchTool] Fetching content from: ${url}`);
this.debugLogger.debug(`[WebFetchTool] Fetching content from: ${url}`);
const response = await fetchWithTimeout(url, URL_FETCH_TIMEOUT_MS);
if (!response.ok) {
const errorMessage = `Request failed with status code ${response.status} ${response.statusText}`;
console.error(`[WebFetchTool] ${errorMessage}`);
this.debugLogger.error(`[WebFetchTool] ${errorMessage}`);
throw new Error(errorMessage);
}
console.debug(`[WebFetchTool] Successfully fetched content from ${url}`);
this.debugLogger.debug(
`[WebFetchTool] Successfully fetched content from ${url}`,
);
const html = await response.text();
const textContent = convert(html, {
wordwrap: false,
@ -89,7 +95,7 @@ class WebFetchToolInvocation extends BaseToolInvocation<
],
}).substring(0, MAX_CONTENT_LENGTH);
console.debug(
this.debugLogger.debug(
`[WebFetchTool] Converted HTML to text (${textContent.length} characters)`,
);
@ -102,7 +108,7 @@ I have fetched the content from ${this.params.url}. Please use the following con
${textContent}
---`;
console.debug(
this.debugLogger.debug(
`[WebFetchTool] Processing content with prompt: "${this.params.prompt}"`,
);
@ -114,7 +120,7 @@ ${textContent}
);
const resultText = getResponseText(result) || '';
console.debug(
this.debugLogger.debug(
`[WebFetchTool] Successfully processed content from ${this.params.url}`,
);
@ -125,7 +131,7 @@ ${textContent}
} catch (e) {
const error = e as Error;
const errorMessage = `Error during fetch for ${url}: ${error.message}`;
console.error(`[WebFetchTool] ${errorMessage}`, error);
this.debugLogger.error(`[WebFetchTool] ${errorMessage}`, error);
return {
llmContent: `Error: ${errorMessage}`,
returnDisplay: `Error: ${errorMessage}`,
@ -171,11 +177,11 @@ ${textContent}
const isPrivate = isPrivateIp(this.params.url);
if (isPrivate) {
console.debug(
this.debugLogger.debug(
`[WebFetchTool] Private IP detected for ${this.params.url}, using direct fetch`,
);
} else {
console.debug(
this.debugLogger.debug(
`[WebFetchTool] Public URL detected for ${this.params.url}, using direct fetch`,
);
}

View file

@ -18,6 +18,7 @@ import { ToolErrorType } from '../tool-error.js';
import type { Config } from '../../config/config.js';
import { ApprovalMode } from '../../config/config.js';
import { getErrorMessage } from '../../utils/errors.js';
import { createDebugLogger } from '../../utils/debugLogger.js';
import { buildContentWithSources } from './utils.js';
import { TavilyProvider } from './providers/tavily-provider.js';
import { GoogleProvider } from './providers/google-provider.js';
@ -32,6 +33,8 @@ import type {
} from './types.js';
import { ToolNames, ToolDisplayNames } from '../tool-names.js';
const debugLogger = createDebugLogger('WEB_SEARCH');
class WebSearchToolInvocation extends BaseToolInvocation<
WebSearchToolParams,
WebSearchToolResult
@ -110,7 +113,7 @@ class WebSearchToolInvocation extends BaseToolInvocation<
providers.set(config.type, provider);
}
} catch (error) {
console.warn(`Failed to create ${config.type} provider:`, error);
debugLogger.warn(`Failed to create ${config.type} provider:`, error);
}
}
@ -255,7 +258,7 @@ class WebSearchToolInvocation extends BaseToolInvocation<
};
} catch (error: unknown) {
const errorMessage = `Error during web search: ${getErrorMessage(error)}`;
console.error(errorMessage, error);
debugLogger.error(errorMessage, error);
return {
llmContent: errorMessage,
returnDisplay: 'Error performing web search.',

View file

@ -38,6 +38,9 @@ import { FileOperationEvent } from '../telemetry/types.js';
import { FileOperation } from '../telemetry/metrics.js';
import { getSpecificMimeType } from '../utils/fileUtils.js';
import { getLanguageFromFilePath } from '../utils/language-detection.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const debugLogger = createDebugLogger('WRITE_FILE');
/**
* Parameters for the WriteFile tool
@ -329,7 +332,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
// Include stack trace in debug mode for better troubleshooting
if (this.config.getDebugMode() && error.stack) {
console.error('Write file error stack:', error.stack);
debugLogger.debug('Write file error stack:', error.stack);
}
} else if (error instanceof Error) {
errorMsg = `Error writing to file: ${error.message}`;

View file

@ -9,6 +9,7 @@ import {
createDebugLogger,
isDebugLoggingDegraded,
resetDebugLoggingState,
setDebugLogSession,
type DebugLogSession,
} from './debugLogger.js';
import { promises as fs } from 'node:fs';
@ -31,20 +32,31 @@ describe('debugLogger', () => {
getSessionId: () => 'test-session-123',
};
const previousDebugLogFileEnv = process.env['QWEN_DEBUG_LOG_FILE'];
beforeEach(() => {
process.env['QWEN_DEBUG_LOG_FILE'] = '1';
vi.clearAllMocks();
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-24T10:30:00.000Z'));
resetDebugLoggingState();
setDebugLogSession(mockSession);
});
afterEach(() => {
vi.useRealTimers();
setDebugLogSession(null);
if (previousDebugLogFileEnv === undefined) {
delete process.env['QWEN_DEBUG_LOG_FILE'];
} else {
process.env['QWEN_DEBUG_LOG_FILE'] = previousDebugLogFileEnv;
}
});
describe('createDebugLogger', () => {
it('returns no-op logger when session is null', () => {
const logger = createDebugLogger(null);
it('returns no-op logger when session is unset', () => {
setDebugLogSession(null);
const logger = createDebugLogger();
// Should not throw
logger.debug('test');
logger.info('test');
@ -53,14 +65,8 @@ describe('debugLogger', () => {
expect(fs.appendFile).not.toHaveBeenCalled();
});
it('returns no-op logger when session is undefined', () => {
const logger = createDebugLogger(undefined);
logger.debug('test');
expect(fs.appendFile).not.toHaveBeenCalled();
});
it('writes debug log with correct format', async () => {
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
logger.debug('Hello world');
await vi.runAllTimersAsync();
@ -76,7 +82,7 @@ describe('debugLogger', () => {
});
it('writes log with tag when provided', async () => {
const logger = createDebugLogger(mockSession, 'STARTUP');
const logger = createDebugLogger('STARTUP');
logger.info('Server started');
await vi.runAllTimersAsync();
@ -89,7 +95,7 @@ describe('debugLogger', () => {
});
it('writes different log levels correctly', async () => {
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
logger.debug('debug message');
logger.info('info message');
@ -106,7 +112,7 @@ describe('debugLogger', () => {
});
it('formats multiple arguments', async () => {
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
logger.debug('Count:', 42, 'items');
await vi.runAllTimersAsync();
@ -119,7 +125,7 @@ describe('debugLogger', () => {
});
it('formats Error objects with stack trace', async () => {
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
const error = new Error('Something went wrong');
logger.error('Failed:', error);
@ -131,7 +137,7 @@ describe('debugLogger', () => {
});
it('formats objects using util.inspect', async () => {
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
logger.debug('Data:', { foo: 'bar', count: 123 });
await vi.runAllTimersAsync();
@ -150,7 +156,7 @@ describe('debugLogger', () => {
it('returns true when mkdir fails', async () => {
vi.mocked(fs.mkdir).mockRejectedValueOnce(new Error('Permission denied'));
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
logger.debug('test');
await vi.runAllTimersAsync();
@ -161,7 +167,7 @@ describe('debugLogger', () => {
it('returns true when appendFile fails', async () => {
vi.mocked(fs.appendFile).mockRejectedValueOnce(new Error('Disk full'));
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
logger.debug('test');
await vi.runAllTimersAsync();
@ -174,7 +180,7 @@ describe('debugLogger', () => {
new Error('Temporary error'),
);
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
logger.debug('first write fails');
await vi.runAllTimersAsync();
@ -194,7 +200,7 @@ describe('debugLogger', () => {
it('resets the degraded state', async () => {
vi.mocked(fs.appendFile).mockRejectedValueOnce(new Error('Disk full'));
const logger = createDebugLogger(mockSession);
const logger = createDebugLogger();
logger.debug('test');
await vi.runAllTimersAsync();

View file

@ -5,6 +5,7 @@
*/
import { promises as fs } from 'node:fs';
import { AsyncLocalStorage } from 'node:async_hooks';
import util from 'node:util';
import { Storage } from '../config/storage.js';
@ -23,6 +24,19 @@ export interface DebugLogger {
let ensureDebugDirPromise: Promise<void> | null = null;
let hasWriteFailure = false;
let globalSession: DebugLogSession | null = null;
const sessionContext = new AsyncLocalStorage<DebugLogSession>();
function isDebugLogFileEnabled(): boolean {
const value = process.env['QWEN_DEBUG_LOG_FILE'];
if (!value) return true;
const normalized = value.trim().toLowerCase();
return !['0', 'false', 'off', 'no'].includes(normalized);
}
function getActiveSession(): DebugLogSession | null {
return sessionContext.getStore() ?? globalSession;
}
function ensureDebugDirExists(): Promise<void> {
if (!ensureDebugDirPromise) {
@ -68,6 +82,10 @@ function writeLog(
tag: string | undefined,
args: unknown[],
): void {
if (!isDebugLogFileEnabled()) {
return;
}
const sessionId = session.getSessionId();
const logFilePath = Storage.getDebugLogPath(sessionId);
const message = formatArgs(args);
@ -98,28 +116,58 @@ export function resetDebugLoggingState(): void {
}
/**
* Creates a debug logger that writes to a session-specific log file.
* Sets the process-wide debug log session used by createDebugLogger().
*
* Log files are written to `~/.qwen/debug/<sessionId>.txt`.
* Write failures are silently ignored to avoid disrupting the user.
* This is the default session used when there is no async-local session bound
* via runWithDebugLogSession().
*/
export function createDebugLogger(
export function setDebugLogSession(
session: DebugLogSession | null | undefined,
tag?: string,
): DebugLogger {
if (!session) {
return {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
}
) {
globalSession = session ?? null;
}
/**
* Runs a function with a session bound to the current async context.
*
* This is optional; createDebugLogger() falls back to the process-wide session
* set via setDebugLogSession().
*/
export function runWithDebugLogSession<T>(
session: DebugLogSession,
fn: () => T,
): T {
return sessionContext.run(session, fn);
}
/**
* Creates a debug logger that writes to the current debug log session.
*
* Session resolution order:
* 1) async-local session (runWithDebugLogSession)
* 2) process-wide session (setDebugLogSession)
*/
export function createDebugLogger(tag?: string): DebugLogger {
return {
debug: (...args: unknown[]) => writeLog(session, 'DEBUG', tag, args),
info: (...args: unknown[]) => writeLog(session, 'INFO', tag, args),
warn: (...args: unknown[]) => writeLog(session, 'WARN', tag, args),
error: (...args: unknown[]) => writeLog(session, 'ERROR', tag, args),
debug: (...args: unknown[]) => {
const session = getActiveSession();
if (!session) return;
writeLog(session, 'DEBUG', tag, args);
},
info: (...args: unknown[]) => {
const session = getActiveSession();
if (!session) return;
writeLog(session, 'INFO', tag, args);
},
warn: (...args: unknown[]) => {
const session = getActiveSession();
if (!session) return;
writeLog(session, 'WARN', tag, args);
},
error: (...args: unknown[]) => {
const session = getActiveSession();
if (!session) return;
writeLog(session, 'ERROR', tag, args);
},
};
}

View file

@ -85,7 +85,6 @@ describe('loadServerHierarchicalMemory', () => {
const { fileCount } = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
false, // untrusted
@ -109,7 +108,6 @@ describe('loadServerHierarchicalMemory', () => {
const { fileCount, memoryContent } = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
false, // untrusted
@ -124,7 +122,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -145,7 +142,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -169,7 +165,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -197,7 +192,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -222,7 +216,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -248,7 +241,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -273,7 +265,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -311,7 +302,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -333,7 +323,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[],
false,
new FileDiscoveryService(projectRoot),
[extensionFilePath],
DEFAULT_FOLDER_TRUST,
@ -357,7 +346,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
[includedDir],
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -389,7 +377,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
cwd,
createdFiles.map((f) => path.dirname(f)),
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
@ -422,7 +409,6 @@ describe('loadServerHierarchicalMemory', () => {
const result = await loadServerHierarchicalMemory(
parentDir,
[childDir, parentDir], // Deliberately include duplicates
false,
new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,

View file

@ -12,19 +12,9 @@ import { getAllGeminiMdFilenames } from '../tools/memoryTool.js';
import type { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { processImports } from './memoryImportProcessor.js';
import { QWEN_DIR } from './paths.js';
import { createDebugLogger } from './debugLogger.js';
// Simple console logger, similar to the one previously in CLI's config.ts
// TODO: Integrate with a more robust server-side logger if available/appropriate.
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args: any[]) =>
console.debug('[DEBUG] [MemoryDiscovery]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
warn: (...args: any[]) => console.warn('[WARN] [MemoryDiscovery]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) =>
console.error('[ERROR] [MemoryDiscovery]', ...args),
};
const logger = createDebugLogger('MEMORY_DISCOVERY');
interface GeminiFileContent {
filePath: string;
@ -79,7 +69,6 @@ async function getGeminiMdFilePathsInternal(
currentWorkingDirectory: string,
includeDirectoriesToReadGemini: readonly string[],
userHomePath: string,
debugMode: boolean,
fileService: FileDiscoveryService,
extensionContextFilePaths: string[] = [],
folderTrust: boolean,
@ -100,7 +89,6 @@ async function getGeminiMdFilePathsInternal(
getGeminiMdFilePathsInternalForEachDir(
dir,
userHomePath,
debugMode,
fileService,
extensionContextFilePaths,
folderTrust,
@ -128,7 +116,6 @@ async function getGeminiMdFilePathsInternal(
async function getGeminiMdFilePathsInternalForEachDir(
dir: string,
userHomePath: string,
debugMode: boolean,
fileService: FileDiscoveryService,
extensionContextFilePaths: string[] = [],
folderTrust: boolean,
@ -148,10 +135,9 @@ async function getGeminiMdFilePathsInternalForEachDir(
try {
await fs.access(globalMemoryPath, fsSync.constants.R_OK);
allPaths.add(globalMemoryPath);
if (debugMode)
logger.debug(
`Found readable global ${geminiMdFilename}: ${globalMemoryPath}`,
);
logger.debug(
`Found readable global ${geminiMdFilename}: ${globalMemoryPath}`,
);
} catch {
// It's okay if it's not found.
}
@ -167,10 +153,9 @@ async function getGeminiMdFilePathsInternalForEachDir(
await fs.access(homeContextPath, fsSync.constants.R_OK);
if (homeContextPath !== globalMemoryPath) {
allPaths.add(homeContextPath);
if (debugMode)
logger.debug(
`Found readable home ${geminiMdFilename}: ${homeContextPath}`,
);
logger.debug(
`Found readable home ${geminiMdFilename}: ${homeContextPath}`,
);
}
} catch {
// Not found, which is okay
@ -179,14 +164,12 @@ async function getGeminiMdFilePathsInternalForEachDir(
// FIX: Only perform the workspace search (upward scan from CWD to project root)
// if a valid currentWorkingDirectory is provided and it's not the home directory.
const resolvedCwd = path.resolve(dir);
if (debugMode)
logger.debug(
`Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`,
);
logger.debug(
`Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`,
);
const projectRoot = await findProjectRoot(resolvedCwd);
if (debugMode)
logger.debug(`Determined project root: ${projectRoot ?? 'None'}`);
logger.debug(`Determined project root: ${projectRoot ?? 'None'}`);
const upwardPaths: string[] = [];
let currentDir = resolvedCwd;
@ -226,18 +209,16 @@ async function getGeminiMdFilePathsInternalForEachDir(
const finalPaths = Array.from(allPaths);
if (debugMode)
logger.debug(
`Final ordered ${getAllGeminiMdFilenames()} paths to read: ${JSON.stringify(
finalPaths,
)}`,
);
logger.debug(
`Final ordered ${getAllGeminiMdFilenames()} paths to read: ${JSON.stringify(
finalPaths,
)}`,
);
return finalPaths;
}
async function readGeminiMdFiles(
filePaths: string[],
debugMode: boolean,
importFormat: 'flat' | 'tree' = 'tree',
): Promise<GeminiFileContent[]> {
// Process files in parallel with concurrency limit to prevent EMFILE errors
@ -255,15 +236,13 @@ async function readGeminiMdFiles(
const processedResult = await processImports(
content,
path.dirname(filePath),
debugMode,
undefined,
undefined,
importFormat,
);
if (debugMode)
logger.debug(
`Successfully read and processed imports: ${filePath} (Length: ${processedResult.content.length})`,
);
logger.debug(
`Successfully read and processed imports: ${filePath} (Length: ${processedResult.content.length})`,
);
return { filePath, content: processedResult.content };
} catch (error: unknown) {
@ -276,7 +255,7 @@ async function readGeminiMdFiles(
`Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`,
);
}
if (debugMode) logger.debug(`Failed to read: ${filePath}`);
logger.debug(`Failed to read: ${filePath}`);
return { filePath, content: null }; // Still include it with null content
}
},
@ -333,16 +312,14 @@ export interface LoadServerHierarchicalMemoryResponse {
export async function loadServerHierarchicalMemory(
currentWorkingDirectory: string,
includeDirectoriesToReadGemini: readonly string[],
debugMode: boolean,
fileService: FileDiscoveryService,
extensionContextFilePaths: string[] = [],
folderTrust: boolean,
importFormat: 'flat' | 'tree' = 'tree',
): Promise<LoadServerHierarchicalMemoryResponse> {
if (debugMode)
logger.debug(
`Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`,
);
logger.debug(
`Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`,
);
// For the server, homedir() refers to the server process's home.
// This is consistent with how MemoryTool already finds the global path.
@ -351,20 +328,15 @@ export async function loadServerHierarchicalMemory(
currentWorkingDirectory,
includeDirectoriesToReadGemini,
userHomePath,
debugMode,
fileService,
extensionContextFilePaths,
folderTrust,
);
if (filePaths.length === 0) {
if (debugMode) logger.debug('No QWEN.md files found in hierarchy.');
logger.debug('No QWEN.md files found in hierarchy.');
return { memoryContent: '', fileCount: 0 };
}
const contentsWithPaths = await readGeminiMdFiles(
filePaths,
debugMode,
importFormat,
);
const contentsWithPaths = await readGeminiMdFiles(filePaths, importFormat);
// Pass CWD for relative path display in concatenated content
const combinedInstructions = concatenateInstructions(
contentsWithPaths,
@ -378,14 +350,12 @@ export async function loadServerHierarchicalMemory(
memoryFilenames.has(path.basename(item.filePath)),
).length;
if (debugMode)
logger.debug(
`Combined instructions length: ${combinedInstructions.length}`,
);
if (debugMode && combinedInstructions.length > 0)
logger.debug(`Combined instructions length: ${combinedInstructions.length}`);
if (combinedInstructions.length > 0) {
logger.debug(
`Combined instructions (snippet): ${combinedInstructions.substring(0, 500)}...`,
);
}
return {
memoryContent: combinedInstructions,
fileCount, // Only count the context files

View file

@ -119,7 +119,7 @@ describe('memoryImportProcessor', () => {
mockedFs.access.mockResolvedValue(undefined);
mockedFs.readFile.mockResolvedValue(importedContent);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
// Use marked to find HTML comments (import markers)
const comments = findMarkdownComments(result.content);
@ -153,7 +153,7 @@ describe('memoryImportProcessor', () => {
mockedFs.access.mockResolvedValue(undefined);
mockedFs.readFile.mockResolvedValue(importedContent);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
// Use marked to find import comments
const comments = findMarkdownComments(result.content);
@ -200,7 +200,7 @@ describe('memoryImportProcessor', () => {
currentFile: testPath('test', 'path', 'main.md'), // Simulate we're processing main.md
};
const result = await processImports(content, basePath, true, importState);
const result = await processImports(content, basePath, importState);
// The circular import should be detected when processing the nested import
expect(result.content).toContain(
@ -219,7 +219,7 @@ describe('memoryImportProcessor', () => {
}),
);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
// Content should be preserved as-is when file doesn't exist
expect(result.content).toBe(content);
@ -236,16 +236,13 @@ describe('memoryImportProcessor', () => {
Object.assign(new Error('Permission denied'), { code: 'EACCES' }),
);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
// Should show error comment for non-ENOENT errors
expect(result.content).toContain(
'<!-- Import failed: ./permission-denied.md - Permission denied -->',
);
expect(console.error).toHaveBeenCalledWith(
'[ERROR] [ImportProcessor]',
'Failed to import ./permission-denied.md: Permission denied',
);
expect(console.error).not.toHaveBeenCalled();
});
it('should respect max depth limit', async () => {
@ -262,12 +259,9 @@ describe('memoryImportProcessor', () => {
currentDepth: 1,
};
const result = await processImports(content, basePath, true, importState);
const result = await processImports(content, basePath, importState);
expect(console.warn).toHaveBeenCalledWith(
'[WARN] [ImportProcessor]',
'Maximum import depth (1) reached. Stopping import processing.',
);
expect(console.warn).not.toHaveBeenCalled();
expect(result.content).toBe(content);
});
@ -282,7 +276,7 @@ describe('memoryImportProcessor', () => {
.mockResolvedValueOnce(nestedContent)
.mockResolvedValueOnce(innerContent);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
expect(result.content).toContain('<!-- Imported from: ./nested.md -->');
expect(result.content).toContain('<!-- Imported from: ./inner.md -->');
@ -297,7 +291,7 @@ describe('memoryImportProcessor', () => {
mockedFs.access.mockResolvedValue(undefined);
mockedFs.readFile.mockResolvedValue(importedContent);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
expect(result.content).toContain(
'<!-- Import failed: /absolute/path/file.md - Path traversal attempt -->',
@ -315,7 +309,7 @@ describe('memoryImportProcessor', () => {
.mockResolvedValueOnce(firstContent)
.mockResolvedValueOnce(secondContent);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
expect(result.content).toContain('<!-- Imported from: ./first.md -->');
expect(result.content).toContain('<!-- Imported from: ./second.md -->');
@ -343,7 +337,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
);
@ -383,7 +376,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
);
@ -432,7 +424,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
);
@ -458,7 +449,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
);
@ -488,7 +478,7 @@ describe('memoryImportProcessor', () => {
); // 中文路径 doesn't exist
mockedFs.readFile.mockResolvedValue(importedContent);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
// Should import valid.md
expect(result.content).toContain(importedContent);
@ -509,7 +499,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
);
@ -533,7 +522,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
);
@ -548,7 +536,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
);
@ -571,7 +558,7 @@ describe('memoryImportProcessor', () => {
.mockResolvedValueOnce(simpleContent)
.mockResolvedValueOnce(innerContent);
const result = await processImports(content, basePath, true);
const result = await processImports(content, basePath);
// Use marked to find and validate import comments
const comments = findMarkdownComments(result.content);
@ -639,7 +626,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
'flat',
@ -715,7 +701,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true, // followImports
undefined, // allowedPaths
projectRoot,
'flat', // outputFormat
@ -747,7 +732,6 @@ describe('memoryImportProcessor', () => {
const result = await processImports(
content,
basePath,
true,
undefined,
projectRoot,
'flat',

View file

@ -8,18 +8,9 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { isSubpath } from './paths.js';
import { marked, type Token } from 'marked';
import { createDebugLogger } from './debugLogger.js';
// Simple console logger for import processing
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args: any[]) =>
console.debug('[DEBUG] [ImportProcessor]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
warn: (...args: any[]) => console.warn('[WARN] [ImportProcessor]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) =>
console.error('[ERROR] [ImportProcessor]', ...args),
};
const logger = createDebugLogger('IMPORT_PROCESSOR');
/**
* Interface for tracking import processing state to prevent circular imports
@ -201,7 +192,6 @@ function findCodeRegions(content: string): Array<[number, number]> {
* Supports @path/to/file syntax for importing content from other files
* @param content - The content to process for imports
* @param basePath - The directory path where the current file is located
* @param debugMode - Whether to enable debug logging
* @param importState - State tracking for circular import prevention
* @param projectRoot - The project root directory for allowed directories
* @param importFormat - The format of the import tree
@ -210,7 +200,6 @@ function findCodeRegions(content: string): Array<[number, number]> {
export async function processImports(
content: string,
basePath: string,
debugMode: boolean = false,
importState: ImportState = {
processedFiles: new Set(),
maxDepth: 5,
@ -224,11 +213,9 @@ export async function processImports(
}
if (importState.currentDepth >= importState.maxDepth) {
if (debugMode) {
logger.warn(
`Maximum import depth (${importState.maxDepth}) reached. Stopping import processing.`,
);
}
logger.warn(
`Maximum import depth (${importState.maxDepth}) reached. Stopping import processing.`,
);
return {
content,
importTree: { path: importState.currentFile || 'unknown' },
@ -306,7 +293,7 @@ export async function processImports(
} catch (error) {
// If file doesn't exist, silently skip this import (it's not a real import)
// Only log warnings for other types of errors
if (!isFileNotFoundError(error) && debugMode) {
if (!isFileNotFoundError(error)) {
logger.warn(
`Failed to import ${fullPath}: ${hasMessage(error) ? error.message : 'Unknown error'}`,
);
@ -377,7 +364,6 @@ export async function processImports(
const imported = await processImports(
fileContent,
path.dirname(fullPath),
debugMode,
newImportState,
projectRoot,
importFormat,

View file

@ -10,6 +10,9 @@ import {
isQwenQuotaExceededError,
isQwenThrottlingError,
} from './quotaErrorDetection.js';
import { createDebugLogger } from './debugLogger.js';
const debugLogger = createDebugLogger('RETRY');
export interface HttpError extends Error {
status?: number;
@ -141,7 +144,7 @@ export async function retryWithBackoff<T>(
consecutive429Count = 0;
}
console.debug('consecutive429Count', consecutive429Count);
debugLogger.debug('consecutive429Count', consecutive429Count);
// Check if we've exhausted retries or shouldn't retry
if (attempt >= maxAttempts || !shouldRetryOnError(error as Error)) {

View file

@ -11,6 +11,12 @@ if (process.env['NO_COLOR'] !== undefined) {
import { setSimulate429 } from './src/utils/testUtils.js';
// Avoid writing per-session debug log files during tests.
// Unit tests can opt-in by overriding this env var.
if (process.env['QWEN_DEBUG_LOG_FILE'] === undefined) {
process.env['QWEN_DEBUG_LOG_FILE'] = '0';
}
// Disable 429 simulation globally for all tests
setSimulate429(false);