Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions[bot]
c2e128e467 chore(release): v0.0.11-nightly.6 2025-09-12 08:50:47 +00:00
DS-Controller2
67e2e270bd Fix performance issues with SharedTokenManager causing 20-minute delays (#586)
- Optimize lock acquisition strategy with exponential backoff
- Reduce excessive I/O operations by increasing cache check interval
- Add timeout monitoring for token refresh operations
- Add timeout wrappers for file operations to prevent hanging
- Fixes issue #585 where users experienced extreme performance issues with Qwen Code
2025-09-12 16:35:22 +08:00
Mingholy
adabd96a42 fix: clear saved creds when switching authType (#587) 2025-09-12 16:34:53 +08:00
pomelo
4a96646732 Merge pull request #588 from QwenLM/update-docs
feat: Replace all Gemini CLI brand references with Qwen Code.
2025-09-12 16:26:22 +08:00
10 changed files with 120 additions and 26 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"workspaces": [
"packages/*"
],
@@ -12512,7 +12512,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"dependencies": {
"@google/genai": "1.9.0",
"@iarna/toml": "^2.2.5",
@@ -12696,7 +12696,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"dependencies": {
"@google/genai": "1.13.0",
"@modelcontextprotocol/sdk": "^1.11.0",
@@ -12861,7 +12861,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -12872,7 +12872,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"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.10"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.11-nightly.6"
},
"scripts": {
"start": "node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.10"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.11-nightly.6"
},
"dependencies": {
"@google/genai": "1.9.0",

View File

@@ -321,7 +321,8 @@ const ToolInfo: React.FC<ToolInfo> = ({
>
<Text color={nameColor} bold>
{name}
</Text>{' '}
</Text>
<Text> </Text>
<Text color={Colors.Gray}>{description}</Text>
</Text>
</Box>

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -548,6 +548,17 @@ describe('oauth2', () => {
expect(updatedAccountData.old).toContain('test@example.com');
});
it('should handle Qwen module clearing gracefully', async () => {
// This test verifies that clearCachedCredentialFile doesn't throw
// when Qwen modules are available and can be cleared
// Since dynamic imports in tests are complex, we'll just verify
// that the function completes without error and doesn't throw
await expect(clearCachedCredentialFile()).resolves.not.toThrow();
// The actual Qwen clearing logic is tested separately in the Qwen module tests
});
it('should clear the in-memory OAuth client cache', async () => {
const mockSetCredentials = vi.fn();
const mockGetAccessToken = vi

View File

@@ -402,6 +402,25 @@ export async function clearCachedCredentialFile() {
await clearCachedGoogleAccount();
// Clear the in-memory OAuth client cache to force re-authentication
clearOauthClientCache();
/**
* Also clear Qwen SharedTokenManager cache and credentials file to prevent stale credentials
* when switching between auth types
* TODO: We do not depend on code_assist, we'll have to build an independent auth-cleaning procedure.
*/
try {
const { SharedTokenManager } = await import(
'../qwen/sharedTokenManager.js'
);
const { clearQwenCredentials } = await import('../qwen/qwenOAuth2.js');
const sharedManager = SharedTokenManager.getInstance();
sharedManager.clearCache();
await clearQwenCredentials();
} catch (qwenError) {
console.debug('Could not clear Qwen credentials:', qwenError);
}
} catch (e) {
console.error('Failed to clear cached credentials:', e);
}

View File

@@ -26,17 +26,20 @@ const QWEN_LOCK_FILENAME = 'oauth_creds.lock';
// Token and Cache Configuration
const TOKEN_REFRESH_BUFFER_MS = 30 * 1000; // 30 seconds
const LOCK_TIMEOUT_MS = 10000; // 10 seconds lock timeout
const CACHE_CHECK_INTERVAL_MS = 1000; // 1 second cache check interval
const CACHE_CHECK_INTERVAL_MS = 5000; // 5 seconds cache check interval (increased from 1 second)
// Lock acquisition configuration (can be overridden for testing)
interface LockConfig {
maxAttempts: number;
attemptInterval: number;
// Add exponential backoff parameters
maxInterval: number;
}
const DEFAULT_LOCK_CONFIG: LockConfig = {
maxAttempts: 50,
attemptInterval: 200,
maxAttempts: 20, // Reduced from 50 to prevent excessive waiting
attemptInterval: 100, // Reduced from 200ms to check more frequently
maxInterval: 2000, // Maximum interval for exponential backoff
};
/**
@@ -300,7 +303,25 @@ export class SharedTokenManager {
try {
const filePath = this.getCredentialFilePath();
const stats = await fs.stat(filePath);
// Add timeout to file stat operation
const withTimeout = async <T>(
promise: Promise<T>,
timeoutMs: number,
): Promise<T> =>
Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(
() =>
reject(
new Error(`File operation timed out after ${timeoutMs}ms`),
),
timeoutMs,
),
),
]);
const stats = await withTimeout(fs.stat(filePath), 3000);
const fileModTime = stats.mtimeMs;
// Reload credentials if file has been modified since last cache
@@ -423,6 +444,7 @@ export class SharedTokenManager {
qwenClient: IQwenOAuth2Client,
forceRefresh = false,
): Promise<QwenCredentials> {
const startTime = Date.now();
const lockPath = this.getLockFilePath();
try {
@@ -439,6 +461,15 @@ export class SharedTokenManager {
// Acquire distributed file lock
await this.acquireLock(lockPath);
// Check if the operation is taking too long
const lockAcquisitionTime = Date.now() - startTime;
if (lockAcquisitionTime > 5000) {
// 5 seconds warning threshold
console.warn(
`Token refresh lock acquisition took ${lockAcquisitionTime}ms`,
);
}
// Double-check if another process already refreshed the token (unless force refresh is requested)
// Skip the time-based throttling since we're already in a locked refresh operation
await this.forceFileCheck(qwenClient);
@@ -456,6 +487,13 @@ export class SharedTokenManager {
// Perform the actual token refresh
const response = await qwenClient.refreshAccessToken();
// Check if the token refresh is taking too long
const totalOperationTime = Date.now() - startTime;
if (totalOperationTime > 10000) {
// 10 seconds warning threshold
console.warn(`Token refresh operation took ${totalOperationTime}ms`);
}
if (!response || isErrorResponse(response)) {
const errorData = response as ErrorData;
throw new TokenManagerError(
@@ -551,9 +589,27 @@ export class SharedTokenManager {
const dirPath = path.dirname(filePath);
const tempPath = `${filePath}.tmp.${randomUUID()}`;
// Add timeout wrapper for file operations
const withTimeout = async <T>(
promise: Promise<T>,
timeoutMs: number,
): Promise<T> =>
Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)),
timeoutMs,
),
),
]);
// Create directory with restricted permissions
try {
await fs.mkdir(dirPath, { recursive: true, mode: 0o700 });
await withTimeout(
fs.mkdir(dirPath, { recursive: true, mode: 0o700 }),
5000,
);
} catch (error) {
throw new TokenManagerError(
TokenError.FILE_ACCESS_ERROR,
@@ -566,18 +622,21 @@ export class SharedTokenManager {
try {
// Write to temporary file first with restricted permissions
await fs.writeFile(tempPath, credString, { mode: 0o600 });
await withTimeout(
fs.writeFile(tempPath, credString, { mode: 0o600 }),
5000,
);
// Atomic move to final location
await fs.rename(tempPath, filePath);
await withTimeout(fs.rename(tempPath, filePath), 5000);
// Update cached file modification time atomically after successful write
const stats = await fs.stat(filePath);
const stats = await withTimeout(fs.stat(filePath), 5000);
this.memoryCache.fileModTime = stats.mtimeMs;
} catch (error) {
// Clean up temp file if it exists
try {
await fs.unlink(tempPath);
await withTimeout(fs.unlink(tempPath), 1000);
} catch (_cleanupError) {
// Ignore cleanup errors - temp file might not exist
}
@@ -628,9 +687,11 @@ export class SharedTokenManager {
* @throws TokenManagerError if lock cannot be acquired within timeout period
*/
private async acquireLock(lockPath: string): Promise<void> {
const { maxAttempts, attemptInterval } = this.lockConfig;
const { maxAttempts, attemptInterval, maxInterval } = this.lockConfig;
const lockId = randomUUID(); // Use random UUID instead of PID for security
let currentInterval = attemptInterval;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
// Attempt to create lock file atomically (exclusive mode)
@@ -671,8 +732,10 @@ export class SharedTokenManager {
);
}
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, attemptInterval));
// Wait before retrying with exponential backoff
await new Promise((resolve) => setTimeout(resolve, currentInterval));
// Increase interval for next attempt (exponential backoff), but cap at maxInterval
currentInterval = Math.min(currentInterval * 1.5, maxInterval);
} else {
throw new TokenManagerError(
TokenError.FILE_ACCESS_ERROR,

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.0.10",
"version": "0.0.11-nightly.6",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -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.10",
"version": "0.0.11-nightly.6",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {