mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-09 18:39:12 +00:00
Compare commits
1 Commits
v0.0.9
...
v0.0.9-nig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f6de13fce |
@@ -52,7 +52,10 @@ jobs:
|
||||
{
|
||||
"maxSessionTurns": 25,
|
||||
"coreTools": [
|
||||
"run_shell_command"
|
||||
"run_shell_command(echo)",
|
||||
"run_shell_command(gh label list)",
|
||||
"run_shell_command(gh issue edit)",
|
||||
"run_shell_command(gh issue list)"
|
||||
],
|
||||
"sandbox": false
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ npm run lint
|
||||
### Coding Conventions
|
||||
|
||||
- Please adhere to the coding style, patterns, and conventions used throughout the existing codebase.
|
||||
- Consult [QWEN.md](https://github.com/QwenLM/qwen-code/blob/main/QWEN.md) (typically found in the project root) for specific instructions related to AI-assisted development, including conventions for React, comments, and Git usage.
|
||||
- Consult [GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md) (typically found in the project root) for specific instructions related to AI-assisted development, including conventions for React, comments, and Git usage.
|
||||
- **Imports:** Pay special attention to import paths. The project uses ESLint to enforce restrictions on relative imports between packages.
|
||||
|
||||
### Project Structure
|
||||
|
||||
@@ -438,7 +438,7 @@ Arguments passed directly when running the CLI can override other configurations
|
||||
- `auto_edit`: Automatically approve edit tools (replace, write_file) while prompting for others
|
||||
- `yolo`: Automatically approve all tool calls (equivalent to `--yolo`)
|
||||
- Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach.
|
||||
- Example: `qwen --approval-mode auto_edit`
|
||||
- Example: `gemini --approval-mode auto_edit`
|
||||
- **`--telemetry`**:
|
||||
- Enables [telemetry](../telemetry.md).
|
||||
- **`--telemetry-target`**:
|
||||
|
||||
@@ -89,7 +89,7 @@ The verbose output is formatted to clearly identify the source of the logs:
|
||||
|
||||
```
|
||||
--- TEST: <log dir>:<test-name> ---
|
||||
... output from the qwen command ...
|
||||
... output from the gemini command ...
|
||||
--- END TEST: <log dir>:<test-name> ---
|
||||
```
|
||||
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -12336,7 +12336,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"dependencies": {
|
||||
"@google/genai": "1.9.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
@@ -12520,7 +12520,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"dependencies": {
|
||||
"@google/genai": "1.13.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
@@ -12671,7 +12671,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
@@ -12682,7 +12682,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "qwen-code-vscode-ide-companion",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -13,7 +13,7 @@
|
||||
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.9"
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.9-nightly.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"description": "Qwen Code",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.9"
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.9-nightly.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "1.9.0",
|
||||
|
||||
@@ -218,7 +218,7 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||
.option('proxy', {
|
||||
type: 'string',
|
||||
description:
|
||||
'Proxy for qwen client, like schema://user:password@host:port',
|
||||
'Proxy for gemini client, like schema://user:password@host:port',
|
||||
})
|
||||
.option('include-directories', {
|
||||
type: 'array',
|
||||
@@ -577,7 +577,6 @@ export async function loadCliConfig(
|
||||
'SYSTEM_TEMPLATE:{"name":"qwen3_coder","params":{"is_git_repository":{RUNTIME_VARS_IS_GIT_REPO},"sandbox":"{RUNTIME_VARS_SANDBOX}"}}',
|
||||
},
|
||||
]) as ConfigParameters['systemPromptMappings'],
|
||||
authType: settings.selectedAuthType,
|
||||
contentGenerator: settings.contentGenerator,
|
||||
cliVersion,
|
||||
tavilyApiKey:
|
||||
|
||||
@@ -88,7 +88,7 @@ export function IdeIntegrationNudge({
|
||||
<Box marginBottom={1} flexDirection="column">
|
||||
<Text>
|
||||
<Text color="yellow">{'> '}</Text>
|
||||
{`Do you want to connect ${ideName ?? 'your'} editor to Qwen Code?`}
|
||||
{`Do you want to connect ${ideName ?? 'your'} editor to Gemini CLI?`}
|
||||
</Text>
|
||||
<Text dimColor>{installText}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -91,34 +91,35 @@ export const directoryCommand: SlashCommand = {
|
||||
}
|
||||
}
|
||||
|
||||
if (added.length > 0) {
|
||||
try {
|
||||
if (config.shouldLoadMemoryFromIncludeDirectories()) {
|
||||
const { memoryContent, fileCount } =
|
||||
await loadServerHierarchicalMemory(
|
||||
config.getWorkingDir(),
|
||||
[...config.getWorkspaceContext().getDirectories()],
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionContextFilePaths(),
|
||||
context.services.settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree'
|
||||
config.getFileFilteringOptions(),
|
||||
context.services.settings.merged.memoryDiscoveryMaxDirs,
|
||||
);
|
||||
config.setUserMemory(memoryContent);
|
||||
config.setGeminiMdFileCount(fileCount);
|
||||
context.ui.setGeminiMdFileCount(fileCount);
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Successfully added memory files from the following directories if there are:\n- ${added.join('\n- ')}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} catch (error) {
|
||||
errors.push(`Error refreshing memory: ${(error as Error).message}`);
|
||||
try {
|
||||
if (config.shouldLoadMemoryFromIncludeDirectories()) {
|
||||
const { memoryContent, fileCount } =
|
||||
await loadServerHierarchicalMemory(
|
||||
config.getWorkingDir(),
|
||||
[
|
||||
...config.getWorkspaceContext().getDirectories(),
|
||||
...pathsToAdd,
|
||||
],
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionContextFilePaths(),
|
||||
context.services.settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree'
|
||||
config.getFileFilteringOptions(),
|
||||
context.services.settings.merged.memoryDiscoveryMaxDirs,
|
||||
);
|
||||
config.setUserMemory(memoryContent);
|
||||
config.setGeminiMdFileCount(fileCount);
|
||||
context.ui.setGeminiMdFileCount(fileCount);
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Successfully added GEMINI.md files from the following directories if there are:\n- ${added.join('\n- ')}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} catch (error) {
|
||||
errors.push(`Error refreshing memory: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
if (added.length > 0) {
|
||||
|
||||
@@ -130,7 +130,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
|
||||
({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: ${Object.values(
|
||||
content: `IDE integration is not supported in your current environment. To use this feature, run Gemini CLI in one of these supported IDEs: ${Object.values(
|
||||
DetectedIde,
|
||||
)
|
||||
.map((ide) => getIdeInfo(ide).displayName)
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('mcpCommand', () => {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'No MCP servers configured. Please view MCP documentation in your browser: https://qwenlm.github.io/qwen-code-docs/en/tools/mcp-server/#how-to-set-up-your-mcp-server or use the cli /docs command',
|
||||
'No MCP servers configured. Please view MCP documentation in your browser: https://goo.gle/gemini-cli-docs-mcp or use the cli /docs command',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,8 +58,7 @@ const getMcpStatus = async (
|
||||
const blockedMcpServers = config.getBlockedMcpServers() || [];
|
||||
|
||||
if (serverNames.length === 0 && blockedMcpServers.length === 0) {
|
||||
const docsUrl =
|
||||
'https://qwenlm.github.io/qwen-code-docs/en/tools/mcp-server/#how-to-set-up-your-mcp-server';
|
||||
const docsUrl = 'https://goo.gle/gemini-cli-docs-mcp';
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
|
||||
@@ -27,7 +27,7 @@ const renderWithWidth = (
|
||||
describe('<ContextSummaryDisplay />', () => {
|
||||
const baseProps = {
|
||||
geminiMdFileCount: 1,
|
||||
contextFileNames: ['QWEN.md'],
|
||||
contextFileNames: ['GEMINI.md'],
|
||||
mcpServers: { 'test-server': { command: 'test' } },
|
||||
showToolDescriptions: false,
|
||||
ideContext: {
|
||||
@@ -41,7 +41,7 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
const { lastFrame } = renderWithWidth(120, baseProps);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(
|
||||
'Using: 1 open file (ctrl+e to view) | 1 QWEN.md file | 1 MCP server (ctrl+t to view)',
|
||||
'Using: 1 open file (ctrl+e to view) | 1 GEMINI.md file | 1 MCP server (ctrl+t to view)',
|
||||
);
|
||||
// Check for absence of newlines
|
||||
expect(output.includes('\n')).toBe(false);
|
||||
@@ -53,7 +53,7 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
const expectedLines = [
|
||||
'Using:',
|
||||
' - 1 open file (ctrl+e to view)',
|
||||
' - 1 QWEN.md file',
|
||||
' - 1 GEMINI.md file',
|
||||
' - 1 MCP server (ctrl+t to view)',
|
||||
];
|
||||
const actualLines = output.split('\n');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"description": "Qwen Code Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -208,7 +208,6 @@ export interface ConfigParameters {
|
||||
modelNames: string[];
|
||||
template: string;
|
||||
}>;
|
||||
authType?: AuthType;
|
||||
contentGenerator?: {
|
||||
timeout?: number;
|
||||
maxRetries?: number;
|
||||
@@ -289,7 +288,6 @@ export class Config {
|
||||
private readonly summarizeToolOutput:
|
||||
| Record<string, SummarizeToolOutputSettings>
|
||||
| undefined;
|
||||
private authType?: AuthType;
|
||||
private readonly enableOpenAILogging: boolean;
|
||||
private readonly contentGenerator?: {
|
||||
timeout?: number;
|
||||
@@ -370,7 +368,6 @@ export class Config {
|
||||
this.ideMode = params.ideMode ?? false;
|
||||
this.ideClient = IdeClient.getInstance();
|
||||
this.systemPromptMappings = params.systemPromptMappings;
|
||||
this.authType = params.authType;
|
||||
this.enableOpenAILogging = params.enableOpenAILogging ?? false;
|
||||
this.contentGenerator = params.contentGenerator;
|
||||
this.cliVersion = params.cliVersion;
|
||||
@@ -454,8 +451,6 @@ export class Config {
|
||||
|
||||
// Reset the session flag since we're explicitly changing auth and using default model
|
||||
this.inFallbackMode = false;
|
||||
|
||||
this.authType = authMethod;
|
||||
}
|
||||
|
||||
getSessionId(): string {
|
||||
@@ -550,7 +545,6 @@ export class Config {
|
||||
getDebugMode(): boolean {
|
||||
return this.debugMode;
|
||||
}
|
||||
|
||||
getQuestion(): string | undefined {
|
||||
return this.question;
|
||||
}
|
||||
@@ -769,10 +763,6 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
getAuthType(): AuthType | undefined {
|
||||
return this.authType;
|
||||
}
|
||||
|
||||
getEnableOpenAILogging(): boolean {
|
||||
return this.enableOpenAILogging;
|
||||
}
|
||||
|
||||
@@ -3410,10 +3410,7 @@ describe('OpenAIContentGenerator', () => {
|
||||
model: 'qwen-turbo',
|
||||
};
|
||||
|
||||
await dashscopeGenerator.generateContentStream(
|
||||
request,
|
||||
'dashscope-prompt-id',
|
||||
);
|
||||
await dashscopeGenerator.generateContent(request, 'dashscope-prompt-id');
|
||||
|
||||
// Should include cache control in last message
|
||||
expect(mockOpenAIClient.chat.completions.create).toHaveBeenCalledWith(
|
||||
@@ -3425,6 +3422,7 @@ describe('OpenAIContentGenerator', () => {
|
||||
expect.objectContaining({
|
||||
type: 'text',
|
||||
text: 'Hello, how are you?',
|
||||
cache_control: { type: 'ephemeral' },
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
|
||||
@@ -130,7 +130,6 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
? {
|
||||
'X-DashScope-CacheControl': 'enable',
|
||||
'X-DashScope-UserAgent': userAgent,
|
||||
'X-DashScope-AuthType': contentGeneratorConfig.authType,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
@@ -236,18 +235,8 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
private async buildCreateParams(
|
||||
request: GenerateContentParameters,
|
||||
userPromptId: string,
|
||||
streaming: boolean = false,
|
||||
): Promise<Parameters<typeof this.client.chat.completions.create>[0]> {
|
||||
let messages = this.convertToOpenAIFormat(request);
|
||||
|
||||
// Add cache control to system and last messages for DashScope providers
|
||||
// Only add cache control to system message for non-streaming requests
|
||||
if (this.isDashScopeProvider()) {
|
||||
messages = this.addDashScopeCacheControl(
|
||||
messages,
|
||||
streaming ? 'both' : 'system',
|
||||
);
|
||||
}
|
||||
const messages = this.convertToOpenAIFormat(request);
|
||||
|
||||
// Build sampling parameters with clear priority:
|
||||
// 1. Request-level parameters (highest priority)
|
||||
@@ -270,11 +259,6 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
);
|
||||
}
|
||||
|
||||
if (streaming) {
|
||||
createParams.stream = true;
|
||||
createParams.stream_options = { include_usage: true };
|
||||
}
|
||||
|
||||
return createParams;
|
||||
}
|
||||
|
||||
@@ -283,11 +267,7 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
userPromptId: string,
|
||||
): Promise<GenerateContentResponse> {
|
||||
const startTime = Date.now();
|
||||
const createParams = await this.buildCreateParams(
|
||||
request,
|
||||
userPromptId,
|
||||
false,
|
||||
);
|
||||
const createParams = await this.buildCreateParams(request, userPromptId);
|
||||
|
||||
try {
|
||||
const completion = (await this.client.chat.completions.create(
|
||||
@@ -378,11 +358,10 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
userPromptId: string,
|
||||
): Promise<AsyncGenerator<GenerateContentResponse>> {
|
||||
const startTime = Date.now();
|
||||
const createParams = await this.buildCreateParams(
|
||||
request,
|
||||
userPromptId,
|
||||
true,
|
||||
);
|
||||
const createParams = await this.buildCreateParams(request, userPromptId);
|
||||
|
||||
createParams.stream = true;
|
||||
createParams.stream_options = { include_usage: true };
|
||||
|
||||
try {
|
||||
const stream = (await this.client.chat.completions.create(
|
||||
@@ -963,13 +942,14 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
const mergedMessages =
|
||||
this.mergeConsecutiveAssistantMessages(cleanedMessages);
|
||||
|
||||
return mergedMessages;
|
||||
// Add cache control to system and last messages for DashScope providers
|
||||
return this.addCacheControlFlag(mergedMessages, 'both');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add cache control flag to specified message(s) for DashScope providers
|
||||
*/
|
||||
private addDashScopeCacheControl(
|
||||
private addCacheControlFlag(
|
||||
messages: OpenAI.Chat.ChatCompletionMessageParam[],
|
||||
target: 'system' | 'last' | 'both' = 'both',
|
||||
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
||||
|
||||
@@ -8,23 +8,12 @@
|
||||
* Integration test to verify circular reference handling with proxy agents
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { QwenLogger } from './qwen-logger/qwen-logger.js';
|
||||
import { RumEvent } from './qwen-logger/event-types.js';
|
||||
import { Config } from '../config/config.js';
|
||||
|
||||
describe('Circular Reference Integration Test', () => {
|
||||
beforeEach(() => {
|
||||
// Clear singleton instance before each test
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(QwenLogger as any).instance = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(QwenLogger as any).instance = undefined;
|
||||
});
|
||||
|
||||
it('should handle HttpsProxyAgent-like circular references in qwen logging', () => {
|
||||
// Create a mock config with proxy
|
||||
const mockConfig = {
|
||||
@@ -75,36 +64,4 @@ describe('Circular Reference Integration Test', () => {
|
||||
logger?.enqueueLogEvent(problematicEvent);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle event overflow without memory leaks', () => {
|
||||
const mockConfig = {
|
||||
getTelemetryEnabled: () => true,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getSessionId: () => 'test-session',
|
||||
getDebugMode: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
const logger = QwenLogger.getInstance(mockConfig);
|
||||
|
||||
// Add more events than the maximum capacity
|
||||
for (let i = 0; i < 1100; i++) {
|
||||
logger?.enqueueLogEvent({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: `overflow-test-${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Logger should still be functional
|
||||
expect(logger).toBeDefined();
|
||||
expect(() => {
|
||||
logger?.enqueueLogEvent({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: 'final-test',
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LogAttributes, LogRecord, logs } from '@opentelemetry/api-logs';
|
||||
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
|
||||
import { Config } from '../config/config.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
import {
|
||||
EVENT_API_ERROR,
|
||||
EVENT_API_REQUEST,
|
||||
@@ -149,7 +150,7 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
|
||||
}
|
||||
|
||||
export function logApiRequest(config: Config, event: ApiRequestEvent): void {
|
||||
// QwenLogger.getInstance(config)?.logApiRequestEvent(event);
|
||||
QwenLogger.getInstance(config)?.logApiRequestEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -363,7 +364,6 @@ export function logIdeConnection(
|
||||
config: Config,
|
||||
event: IdeConnectionEvent,
|
||||
): void {
|
||||
QwenLogger.getInstance(config)?.logIdeConnectionEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -384,7 +384,7 @@ export function logKittySequenceOverflow(
|
||||
config: Config,
|
||||
event: KittySequenceOverflowEvent,
|
||||
): void {
|
||||
QwenLogger.getInstance(config)?.logKittySequenceOverflowEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logKittySequenceOverflowEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
|
||||
@@ -79,6 +79,5 @@ export interface RumPayload {
|
||||
session: RumSession;
|
||||
view: RumView;
|
||||
events: RumEvent[];
|
||||
properties?: Record<string, unknown>;
|
||||
_v: string;
|
||||
}
|
||||
|
||||
@@ -1,407 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
afterAll,
|
||||
} from 'vitest';
|
||||
import { QwenLogger, TEST_ONLY } from './qwen-logger.js';
|
||||
import { Config } from '../../config/config.js';
|
||||
import {
|
||||
StartSessionEvent,
|
||||
EndSessionEvent,
|
||||
IdeConnectionEvent,
|
||||
KittySequenceOverflowEvent,
|
||||
IdeConnectionType,
|
||||
} from '../types.js';
|
||||
import { RumEvent } from './event-types.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../utils/user_id.js', () => ({
|
||||
getInstallationId: vi.fn(() => 'test-installation-id'),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/safeJsonStringify.js', () => ({
|
||||
safeJsonStringify: vi.fn((obj) => JSON.stringify(obj)),
|
||||
}));
|
||||
|
||||
// Mock https module
|
||||
vi.mock('https', () => ({
|
||||
request: vi.fn(),
|
||||
}));
|
||||
|
||||
const makeFakeConfig = (overrides: Partial<Config> = {}): Config => {
|
||||
const defaults = {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getDebugMode: () => false,
|
||||
getSessionId: () => 'test-session-id',
|
||||
getCliVersion: () => '1.0.0',
|
||||
getProxy: () => undefined,
|
||||
getContentGeneratorConfig: () => ({ authType: 'test-auth' }),
|
||||
getMcpServers: () => ({}),
|
||||
getModel: () => 'test-model',
|
||||
getEmbeddingModel: () => 'test-embedding',
|
||||
getSandbox: () => false,
|
||||
getCoreTools: () => [],
|
||||
getApprovalMode: () => 'auto',
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
...overrides,
|
||||
};
|
||||
return defaults as Config;
|
||||
};
|
||||
|
||||
describe('QwenLogger', () => {
|
||||
let mockConfig: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2025-01-01T12:00:00.000Z'));
|
||||
mockConfig = makeFakeConfig();
|
||||
// Clear singleton instance
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(QwenLogger as any).instance = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(QwenLogger as any).instance = undefined;
|
||||
});
|
||||
|
||||
describe('getInstance', () => {
|
||||
it('returns undefined when usage statistics are disabled', () => {
|
||||
const config = makeFakeConfig({ getUsageStatisticsEnabled: () => false });
|
||||
const logger = QwenLogger.getInstance(config);
|
||||
expect(logger).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns an instance when usage statistics are enabled', () => {
|
||||
const logger = QwenLogger.getInstance(mockConfig);
|
||||
expect(logger).toBeInstanceOf(QwenLogger);
|
||||
});
|
||||
|
||||
it('is a singleton', () => {
|
||||
const logger1 = QwenLogger.getInstance(mockConfig);
|
||||
const logger2 = QwenLogger.getInstance(mockConfig);
|
||||
expect(logger1).toBe(logger2);
|
||||
});
|
||||
});
|
||||
|
||||
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(() => {});
|
||||
|
||||
// Fill the queue beyond capacity
|
||||
for (let i = 0; i < TEST_ONLY.MAX_EVENTS + 10; i++) {
|
||||
logger.enqueueLogEvent({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: `test-event-${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Should have logged debug messages about dropping events
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'QwenLogger: Dropped old event to prevent memory leak',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle enqueue errors gracefully', () => {
|
||||
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
|
||||
const logger = QwenLogger.getInstance(debugConfig)!;
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
// Mock the events deque to throw an error
|
||||
const originalPush = logger['events'].push;
|
||||
logger['events'].push = vi.fn(() => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
|
||||
logger.enqueueLogEvent({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: 'test-event',
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'QwenLogger: Failed to enqueue log event.',
|
||||
expect.any(Error),
|
||||
);
|
||||
|
||||
// Restore original method
|
||||
logger['events'].push = originalPush;
|
||||
});
|
||||
});
|
||||
|
||||
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(() => {});
|
||||
|
||||
// Manually set the flush in progress flag to simulate concurrent access
|
||||
logger['isFlushInProgress'] = true;
|
||||
|
||||
// Try to flush while another flush is in progress
|
||||
const result = logger.flushToRum();
|
||||
|
||||
// Should have logged about pending flush
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'QwenLogger: Flush already in progress, marking pending flush',
|
||||
),
|
||||
);
|
||||
|
||||
// Should return a resolved promise
|
||||
expect(result).toBeInstanceOf(Promise);
|
||||
|
||||
// Reset the flag
|
||||
logger['isFlushInProgress'] = false;
|
||||
});
|
||||
});
|
||||
|
||||
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 failedEvents: RumEvent[] = [];
|
||||
for (let i = 0; i < TEST_ONLY.MAX_RETRY_EVENTS + 50; i++) {
|
||||
failedEvents.push({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: `failed-event-${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Call the private method using bracket notation
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(logger as any).requeueFailedEvents(failedEvents);
|
||||
|
||||
// Should have logged about dropping events due to retry limit
|
||||
expect(consoleSpy).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(() => {});
|
||||
|
||||
// Fill the queue to capacity first
|
||||
for (let i = 0; i < TEST_ONLY.MAX_EVENTS; i++) {
|
||||
logger.enqueueLogEvent({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: `event-${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Try to requeue when no space is available
|
||||
const failedEvents: RumEvent[] = [
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: 'failed-event',
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(logger as any).requeueFailedEvents(failedEvents);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('QwenLogger: No events re-queued'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('event handlers', () => {
|
||||
it('should log IDE connection events', () => {
|
||||
const logger = QwenLogger.getInstance(mockConfig)!;
|
||||
const enqueueSpy = vi.spyOn(logger, 'enqueueLogEvent');
|
||||
|
||||
const event = new IdeConnectionEvent(IdeConnectionType.SESSION);
|
||||
|
||||
logger.logIdeConnectionEvent(event);
|
||||
|
||||
expect(enqueueSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event_type: 'action',
|
||||
type: 'connection',
|
||||
name: 'ide_connection',
|
||||
snapshots: JSON.stringify({
|
||||
connection_type: IdeConnectionType.SESSION,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log Kitty sequence overflow events', () => {
|
||||
const logger = QwenLogger.getInstance(mockConfig)!;
|
||||
const enqueueSpy = vi.spyOn(logger, 'enqueueLogEvent');
|
||||
|
||||
const event = new KittySequenceOverflowEvent(1024, 'truncated...');
|
||||
|
||||
logger.logKittySequenceOverflowEvent(event);
|
||||
|
||||
expect(enqueueSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event_type: 'exception',
|
||||
type: 'overflow',
|
||||
name: 'kitty_sequence_overflow',
|
||||
subtype: 'kitty_sequence_overflow',
|
||||
snapshots: JSON.stringify({
|
||||
sequence_length: 1024,
|
||||
truncated_sequence: 'truncated...',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should flush start session events immediately', async () => {
|
||||
const logger = QwenLogger.getInstance(mockConfig)!;
|
||||
const flushSpy = vi.spyOn(logger, 'flushToRum').mockResolvedValue({});
|
||||
|
||||
const testConfig = makeFakeConfig({
|
||||
getModel: () => 'test-model',
|
||||
getEmbeddingModel: () => 'test-embedding',
|
||||
});
|
||||
const event = new StartSessionEvent(testConfig);
|
||||
|
||||
logger.logStartSessionEvent(event);
|
||||
|
||||
expect(flushSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should flush end session events immediately', async () => {
|
||||
const logger = QwenLogger.getInstance(mockConfig)!;
|
||||
const flushSpy = vi.spyOn(logger, 'flushToRum').mockResolvedValue({});
|
||||
|
||||
const event = new EndSessionEvent(mockConfig);
|
||||
|
||||
logger.logEndSessionEvent(event);
|
||||
|
||||
expect(flushSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('flush timing', () => {
|
||||
it('should not flush if interval has not passed', () => {
|
||||
const logger = QwenLogger.getInstance(mockConfig)!;
|
||||
const flushSpy = vi.spyOn(logger, 'flushToRum');
|
||||
|
||||
// Add an event and try to flush immediately
|
||||
logger.enqueueLogEvent({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: 'test-event',
|
||||
});
|
||||
|
||||
logger.flushIfNeeded();
|
||||
|
||||
expect(flushSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should flush when interval has passed', () => {
|
||||
const logger = QwenLogger.getInstance(mockConfig)!;
|
||||
const flushSpy = vi.spyOn(logger, 'flushToRum').mockResolvedValue({});
|
||||
|
||||
// Add an event
|
||||
logger.enqueueLogEvent({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: 'test-event',
|
||||
});
|
||||
|
||||
// Advance time beyond flush interval
|
||||
vi.advanceTimersByTime(TEST_ONLY.FLUSH_INTERVAL_MS + 1000);
|
||||
|
||||
logger.flushIfNeeded();
|
||||
|
||||
expect(flushSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
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(() => {});
|
||||
|
||||
// Add an event first
|
||||
logger.enqueueLogEvent({
|
||||
timestamp: Date.now(),
|
||||
event_type: 'action',
|
||||
type: 'test',
|
||||
name: 'test-event',
|
||||
});
|
||||
|
||||
// Mock flushToRum to throw an error
|
||||
const originalFlush = logger.flushToRum.bind(logger);
|
||||
logger.flushToRum = vi.fn().mockRejectedValue(new Error('Network error'));
|
||||
|
||||
// Advance time to trigger flush
|
||||
vi.advanceTimersByTime(TEST_ONLY.FLUSH_INTERVAL_MS + 1000);
|
||||
|
||||
logger.flushIfNeeded();
|
||||
|
||||
// Wait for async operations
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error flushing to RUM:',
|
||||
expect.any(Error),
|
||||
);
|
||||
|
||||
// Restore original method
|
||||
logger.flushToRum = originalFlush;
|
||||
});
|
||||
});
|
||||
|
||||
describe('constants export', () => {
|
||||
it('should export test constants', () => {
|
||||
expect(TEST_ONLY.MAX_EVENTS).toBe(1000);
|
||||
expect(TEST_ONLY.MAX_RETRY_EVENTS).toBe(100);
|
||||
expect(TEST_ONLY.FLUSH_INTERVAL_MS).toBe(60000);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import * as https from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
import {
|
||||
StartSessionEvent,
|
||||
@@ -21,8 +22,6 @@ import {
|
||||
NextSpeakerCheckEvent,
|
||||
SlashCommandEvent,
|
||||
MalformedJsonResponseEvent,
|
||||
IdeConnectionEvent,
|
||||
KittySequenceOverflowEvent,
|
||||
} from '../types.js';
|
||||
import {
|
||||
RumEvent,
|
||||
@@ -32,12 +31,12 @@ import {
|
||||
RumExceptionEvent,
|
||||
RumPayload,
|
||||
} from './event-types.js';
|
||||
// Removed unused EventMetadataKey import
|
||||
import { Config } from '../../config/config.js';
|
||||
import { safeJsonStringify } from '../../utils/safeJsonStringify.js';
|
||||
// Removed unused import
|
||||
import { HttpError, retryWithBackoff } from '../../utils/retry.js';
|
||||
import { getInstallationId } from '../../utils/user_id.js';
|
||||
import { FixedDeque } from 'mnemonist';
|
||||
import { AuthType } from '../../core/contentGenerator.js';
|
||||
|
||||
// Usage statistics collection endpoint
|
||||
const USAGE_STATS_HOSTNAME = 'gb4w8c3ygj-default-sea.rum.aliyuncs.com';
|
||||
@@ -45,23 +44,6 @@ const USAGE_STATS_PATH = '/';
|
||||
|
||||
const RUN_APP_ID = 'gb4w8c3ygj@851d5d500f08f92';
|
||||
|
||||
/**
|
||||
* Interval in which buffered events are sent to RUM.
|
||||
*/
|
||||
const FLUSH_INTERVAL_MS = 1000 * 60;
|
||||
|
||||
/**
|
||||
* Maximum amount of events to keep in memory. Events added after this amount
|
||||
* are dropped until the next flush to RUM, which happens periodically as
|
||||
* defined by {@link FLUSH_INTERVAL_MS}.
|
||||
*/
|
||||
const MAX_EVENTS = 1000;
|
||||
|
||||
/**
|
||||
* Maximum events to retry after a failed RUM flush
|
||||
*/
|
||||
const MAX_RETRY_EVENTS = 100;
|
||||
|
||||
export interface LogResponse {
|
||||
nextRequestWaitMs?: number;
|
||||
}
|
||||
@@ -71,42 +53,23 @@ export interface LogResponse {
|
||||
export class QwenLogger {
|
||||
private static instance: QwenLogger;
|
||||
private config?: Config;
|
||||
|
||||
/**
|
||||
* Queue of pending events that need to be flushed to the server. New events
|
||||
* are added to this queue and then flushed on demand (via `flushToRum`)
|
||||
*/
|
||||
private readonly events: FixedDeque<RumEvent>;
|
||||
|
||||
/**
|
||||
* The last time that the events were successfully flushed to the server.
|
||||
*/
|
||||
private lastFlushTime: number = Date.now();
|
||||
|
||||
private readonly events: RumEvent[] = [];
|
||||
private last_flush_time: number = Date.now();
|
||||
private flush_interval_ms: number = 1000 * 60; // Wait at least a minute before flushing events.
|
||||
private userId: string;
|
||||
private sessionId: string;
|
||||
|
||||
/**
|
||||
* The value is true when there is a pending flush happening. This prevents
|
||||
* concurrent flush operations.
|
||||
*/
|
||||
private viewId: string;
|
||||
private isFlushInProgress: boolean = false;
|
||||
|
||||
/**
|
||||
* This value is true when a flush was requested during an ongoing flush.
|
||||
*/
|
||||
private pendingFlush: boolean = false;
|
||||
|
||||
private isShutdown: boolean = false;
|
||||
|
||||
private constructor(config?: Config) {
|
||||
this.config = config;
|
||||
this.events = new FixedDeque<RumEvent>(Array, MAX_EVENTS);
|
||||
this.userId = this.generateUserId();
|
||||
this.sessionId =
|
||||
typeof this.config?.getSessionId === 'function'
|
||||
? this.config.getSessionId()
|
||||
: '';
|
||||
this.viewId = randomUUID();
|
||||
}
|
||||
|
||||
private generateUserId(): string {
|
||||
@@ -129,26 +92,7 @@ export class QwenLogger {
|
||||
}
|
||||
|
||||
enqueueLogEvent(event: RumEvent): void {
|
||||
try {
|
||||
// Manually handle overflow for FixedDeque, which throws when full.
|
||||
const wasAtCapacity = this.events.size >= MAX_EVENTS;
|
||||
|
||||
if (wasAtCapacity) {
|
||||
this.events.shift(); // Evict oldest element to make space.
|
||||
}
|
||||
|
||||
this.events.push(event);
|
||||
|
||||
if (wasAtCapacity && this.config?.getDebugMode()) {
|
||||
console.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.events.push(event);
|
||||
}
|
||||
|
||||
createRumEvent(
|
||||
@@ -199,7 +143,6 @@ export class QwenLogger {
|
||||
}
|
||||
|
||||
async createRumPayload(): Promise<RumPayload> {
|
||||
const authType = this.config?.getAuthType();
|
||||
const version = this.config?.getCliVersion() || 'unknown';
|
||||
|
||||
return {
|
||||
@@ -216,59 +159,40 @@ export class QwenLogger {
|
||||
id: this.sessionId,
|
||||
},
|
||||
view: {
|
||||
id: this.sessionId,
|
||||
id: this.viewId,
|
||||
name: 'qwen-code-cli',
|
||||
},
|
||||
|
||||
events: this.events.toArray() as RumEvent[],
|
||||
properties: {
|
||||
auth_type: authType,
|
||||
model: this.config?.getModel(),
|
||||
base_url:
|
||||
authType === AuthType.USE_OPENAI ? process.env.OPENAI_BASE_URL : '',
|
||||
},
|
||||
events: [...this.events],
|
||||
_v: `qwen-code@${version}`,
|
||||
};
|
||||
}
|
||||
|
||||
flushIfNeeded(): void {
|
||||
if (Date.now() - this.lastFlushTime < FLUSH_INTERVAL_MS) {
|
||||
if (Date.now() - this.last_flush_time < this.flush_interval_ms) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent concurrent flush operations
|
||||
if (this.isFlushInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.flushToRum().catch((error) => {
|
||||
if (this.config?.getDebugMode()) {
|
||||
console.debug('Error flushing to RUM:', error);
|
||||
}
|
||||
console.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.pendingFlush = true;
|
||||
return Promise.resolve({});
|
||||
}
|
||||
this.isFlushInProgress = true;
|
||||
|
||||
if (this.config?.getDebugMode()) {
|
||||
console.log('Flushing log events to RUM.');
|
||||
}
|
||||
if (this.events.size === 0) {
|
||||
this.isFlushInProgress = false;
|
||||
if (this.events.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const eventsToSend = this.events.toArray() as RumEvent[];
|
||||
this.events.clear();
|
||||
this.isFlushInProgress = true;
|
||||
|
||||
const rumPayload = await this.createRumPayload();
|
||||
// Override events with the ones we're sending
|
||||
rumPayload.events = eventsToSend;
|
||||
const flushFn = () =>
|
||||
new Promise<Buffer>((resolve, reject) => {
|
||||
const body = safeJsonStringify(rumPayload);
|
||||
@@ -322,29 +246,16 @@ export class QwenLogger {
|
||||
},
|
||||
});
|
||||
|
||||
this.lastFlushTime = Date.now();
|
||||
this.events.splice(0, this.events.length);
|
||||
this.last_flush_time = Date.now();
|
||||
return {};
|
||||
} catch (error) {
|
||||
if (this.config?.getDebugMode()) {
|
||||
console.error('RUM flush failed after multiple retries.', error);
|
||||
}
|
||||
|
||||
// Re-queue failed events for retry
|
||||
this.requeueFailedEvents(eventsToSend);
|
||||
return {};
|
||||
} finally {
|
||||
this.isFlushInProgress = false;
|
||||
|
||||
// If a flush was requested while we were flushing, flush again
|
||||
if (this.pendingFlush) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,9 +282,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);
|
||||
}
|
||||
console.debug('Error flushing to RUM:', error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -542,41 +451,13 @@ export class QwenLogger {
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logIdeConnectionEvent(event: IdeConnectionEvent): void {
|
||||
const rumEvent = this.createActionEvent('connection', 'ide_connection', {
|
||||
snapshots: JSON.stringify({ connection_type: event.connection_type }),
|
||||
});
|
||||
|
||||
this.enqueueLogEvent(rumEvent);
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logKittySequenceOverflowEvent(event: KittySequenceOverflowEvent): void {
|
||||
const rumEvent = this.createExceptionEvent(
|
||||
'overflow',
|
||||
'kitty_sequence_overflow',
|
||||
{
|
||||
subtype: 'kitty_sequence_overflow',
|
||||
snapshots: JSON.stringify({
|
||||
sequence_length: event.sequence_length,
|
||||
truncated_sequence: event.truncated_sequence,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
this.enqueueLogEvent(rumEvent);
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logEndSessionEvent(_event: EndSessionEvent): void {
|
||||
const applicationEvent = this.createViewEvent('session', 'session_end', {});
|
||||
|
||||
// Flush immediately on session end.
|
||||
this.enqueueLogEvent(applicationEvent);
|
||||
this.flushToRum().catch((error: unknown) => {
|
||||
if (this.config?.getDebugMode()) {
|
||||
console.debug('Error flushing to RUM:', error);
|
||||
}
|
||||
console.debug('Error flushing to RUM:', error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -599,60 +480,4 @@ export class QwenLogger {
|
||||
const event = new EndSessionEvent(this.config);
|
||||
this.logEndSessionEvent(event);
|
||||
}
|
||||
|
||||
private requeueFailedEvents(eventsToSend: RumEvent[]): void {
|
||||
// Add the events back to the front of the queue to be retried, but limit retry queue size
|
||||
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(
|
||||
`QwenLogger: Dropping ${
|
||||
eventsToSend.length - MAX_RETRY_EVENTS
|
||||
} events due to retry queue limit. Total events: ${
|
||||
eventsToSend.length
|
||||
}, keeping: ${MAX_RETRY_EVENTS}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Determine how many events can be re-queued
|
||||
const availableSpace = MAX_EVENTS - this.events.size;
|
||||
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})`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the most recent events to re-queue
|
||||
const eventsToRequeue = eventsToRetry.slice(
|
||||
eventsToRetry.length - numEventsToRequeue,
|
||||
);
|
||||
|
||||
// Prepend events to the front of the deque to be retried first.
|
||||
// We iterate backwards to maintain the original order of the failed events.
|
||||
for (let i = eventsToRequeue.length - 1; i >= 0; i--) {
|
||||
this.events.unshift(eventsToRequeue[i]);
|
||||
}
|
||||
// Clear any potential overflow
|
||||
while (this.events.size > MAX_EVENTS) {
|
||||
this.events.pop();
|
||||
}
|
||||
|
||||
if (this.config?.getDebugMode()) {
|
||||
console.debug(
|
||||
`QwenLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const TEST_ONLY = {
|
||||
MAX_RETRY_EVENTS,
|
||||
MAX_EVENTS,
|
||||
FLUSH_INTERVAL_MS,
|
||||
};
|
||||
|
||||
@@ -210,16 +210,16 @@ describe('bfsFileSearch', () => {
|
||||
for (let i = 0; i < numTargetDirs; i++) {
|
||||
// Add target files in some directories
|
||||
fileCreationPromises.push(
|
||||
createTestFile('content', `dir${i}`, 'QWEN.md'),
|
||||
createTestFile('content', `dir${i}`, 'GEMINI.md'),
|
||||
);
|
||||
fileCreationPromises.push(
|
||||
createTestFile('content', `dir${i}`, 'subdir1', 'QWEN.md'),
|
||||
createTestFile('content', `dir${i}`, 'subdir1', 'GEMINI.md'),
|
||||
);
|
||||
}
|
||||
const expectedFiles = await Promise.all(fileCreationPromises);
|
||||
|
||||
const result = await bfsFileSearch(testRootDir, {
|
||||
fileName: 'QWEN.md',
|
||||
fileName: 'GEMINI.md',
|
||||
// Provide a generous maxDirs limit to ensure it doesn't prematurely stop
|
||||
// in this large test case. Total dirs created is 200.
|
||||
maxDirs: 250,
|
||||
|
||||
@@ -143,28 +143,9 @@ async function getGeminiMdFilePathsInternalForEachDir(
|
||||
// It's okay if it's not found.
|
||||
}
|
||||
|
||||
// Handle the case where we're in the home directory (dir is empty string or home path)
|
||||
const resolvedDir = dir ? path.resolve(dir) : resolvedHome;
|
||||
const isHomeDirectory = resolvedDir === resolvedHome;
|
||||
|
||||
if (isHomeDirectory) {
|
||||
// For home directory, only check for QWEN.md directly in the home directory
|
||||
const homeContextPath = path.join(resolvedHome, geminiMdFilename);
|
||||
try {
|
||||
await fs.access(homeContextPath, fsSync.constants.R_OK);
|
||||
if (homeContextPath !== globalMemoryPath) {
|
||||
allPaths.add(homeContextPath);
|
||||
if (debugMode)
|
||||
logger.debug(
|
||||
`Found readable home ${geminiMdFilename}: ${homeContextPath}`,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Not found, which is okay
|
||||
}
|
||||
} else if (dir) {
|
||||
// FIX: Only perform the workspace search (upward and downward scans)
|
||||
// if a valid currentWorkingDirectory is provided and it's not the home directory.
|
||||
// FIX: Only perform the workspace search (upward and downward scans)
|
||||
// if a valid currentWorkingDirectory is provided.
|
||||
if (dir) {
|
||||
const resolvedCwd = path.resolve(dir);
|
||||
if (debugMode)
|
||||
logger.debug(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "qwen-code-vscode-ide-companion",
|
||||
"displayName": "Qwen Code Companion",
|
||||
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-nightly.4",
|
||||
"publisher": "qwenlm",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
@@ -136,7 +136,7 @@ function buildImage(imageName, dockerfile) {
|
||||
if (isWindows) {
|
||||
// PowerShell doesn't support <() process substitution.
|
||||
// Create a temporary auth file that we will clean up after.
|
||||
tempAuthFile = join(os.tmpdir(), `qwen-auth-${Date.now()}.json`);
|
||||
tempAuthFile = join(os.tmpdir(), `gemini-auth-${Date.now()}.json`);
|
||||
writeFileSync(tempAuthFile, '{}');
|
||||
buildCommandArgs = `--authfile="${tempAuthFile}"`;
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
|
||||
# Determine the project directory
|
||||
PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
ALIAS_COMMAND="alias qwen='node "${PROJECT_DIR}/scripts/start.js"'"
|
||||
ALIAS_COMMAND="alias gemini='node "${PROJECT_DIR}/scripts/start.js"'"
|
||||
|
||||
# Detect shell and set config file path
|
||||
if [[ "${SHELL}" == *"/bash" ]]; then
|
||||
@@ -22,8 +22,8 @@ echo " ${ALIAS_COMMAND}"
|
||||
echo ""
|
||||
|
||||
# Check if the alias already exists
|
||||
if grep -q "alias qwen=" "${CONFIG_FILE}"; then
|
||||
echo "A 'qwen' alias already exists in ${CONFIG_FILE}. No changes were made."
|
||||
if grep -q "alias gemini=" "${CONFIG_FILE}"; then
|
||||
echo "A 'gemini' alias already exists in ${CONFIG_FILE}. No changes were made."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -33,7 +33,7 @@ if [[ "${REPLY}" =~ ^[Yy]$ ]]; then
|
||||
echo "${ALIAS_COMMAND}" >> "${CONFIG_FILE}"
|
||||
echo ""
|
||||
echo "Alias added to ${CONFIG_FILE}."
|
||||
echo "Please run 'source ${CONFIG_FILE}' or open a new terminal to use the 'qwen' command."
|
||||
echo "Please run 'source ${CONFIG_FILE}' or open a new terminal to use the 'gemini' command."
|
||||
else
|
||||
echo "Aborted. No changes were made."
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user