mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
feat(core,cli): migrate console.debug to debugLogger (M3 Phase 1-3)
This commit is contained in:
parent
ba2824b0b0
commit
3959b73bce
63 changed files with 554 additions and 538 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ export async function runNonInteractive(
|
|||
process.removeListener('SIGINT', shutdownHandler);
|
||||
process.removeListener('SIGTERM', shutdownHandler);
|
||||
if (isTelemetrySdkInitialized()) {
|
||||
await shutdownTelemetry(config);
|
||||
await shutdownTelemetry();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ export const directoryCommand: SlashCommand = {
|
|||
...config.getWorkspaceContext().getDirectories(),
|
||||
...pathsToAdd,
|
||||
],
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionContextFilePaths(),
|
||||
config.getFolderTrust(),
|
||||
|
|
|
|||
|
|
@ -309,7 +309,6 @@ export const memoryCommand: SlashCommand = {
|
|||
config.shouldLoadMemoryFromIncludeDirectories()
|
||||
? config.getWorkspaceContext().getDirectories()
|
||||
: [],
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionContextFilePaths(),
|
||||
config.getFolderTrust(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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}"`
|
||||
}:`,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ describe('Telemetry SDK', () => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await shutdownTelemetry(mockConfig);
|
||||
await shutdownTelemetry();
|
||||
});
|
||||
|
||||
it('should use gRPC exporters when protocol is grpc', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}`,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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!,
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ describe('WebFetchTool', () => {
|
|||
setApprovalMode: vi.fn(),
|
||||
getProxy: vi.fn(),
|
||||
getGeminiClient: mockGetGeminiClient,
|
||||
getSessionId: vi.fn(() => 'test-session-id'),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue