From a2801908e39bff289a73b3865627ef3deafa66df Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Mon, 16 Feb 2026 10:29:51 +0200 Subject: [PATCH 1/2] fix(utils): prevent ERR_STREAM_WRITE_AFTER_END crash during log rotation Fixes race condition in date-based log rotation that caused crashes when Claude sessions crossed midnight. The async rotation fired but writeToLogFile continued to write to the stream being closed, causing stream write-after-end errors. Changes: - Add defensive checks in writeToLogFile() to skip writes during rotation - Check isRotating flag and stream.writable property before writing - Enhance close() method to wait for in-progress rotation (max 5 seconds) - Add detailed comments explaining the race condition scenario Impact: Prevents application crashes in long-running sessions that span midnight. Trade-off: May skip 1-2 log entries during rotation (acceptable vs crash). Generated with AI Co-Authored-By: codemie-ai --- src/utils/logger.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index fcedcf9..dc25d4f 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -180,6 +180,14 @@ class Logger { }); } + // Defensive check: Skip write if rotation is in progress or stream is not writable + // This prevents ERR_STREAM_WRITE_AFTER_END race condition when log rotation occurs + // Race scenario: rotateLogFileIfNeeded() closes stream while writeToLogFile() tries to write + if (this.isRotating || !this.writeStream.writable) { + // Skip this write - next log call will use the new rotated stream + return; + } + try { const timestamp = new Date().toISOString(); @@ -210,8 +218,17 @@ class Logger { /** * Flush and close the write stream * Returns a Promise that resolves when all data is flushed + * Waits for any in-progress rotation to complete before closing */ - close(): Promise { + async close(): Promise { + // Wait for any in-progress rotation to complete + // This prevents race conditions where close() is called during rotation + const maxWaitTime = 5000; // 5 seconds max wait + const startTime = Date.now(); + while (this.isRotating && (Date.now() - startTime) < maxWaitTime) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return new Promise((resolve) => { if (this.writeStream) { this.writeStream.end(() => { From 1fedd56da516441e755121d34800ca760301f889 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Mon, 16 Feb 2026 11:29:22 +0200 Subject: [PATCH 2/2] fix(providers): enable graceful termination during SSO browser authentication Fixes issue where SSO authentication flow cannot be interrupted when waiting for browser callback, requiring manual process termination. Changes: - Add AbortController for cancellation support - Register SIGINT/SIGTERM handlers for graceful shutdown - Replace recursive polling with setInterval in waitForCallback - Enhance cleanup() to force-close server connections - Add proper error handling for user cancellation Pattern follows agent.ts signal handling implementation for consistency across the codebase. Generated with AI Co-Authored-By: codemie-ai --- src/providers/plugins/sso/sso.auth.ts | 97 +++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 12 deletions(-) diff --git a/src/providers/plugins/sso/sso.auth.ts b/src/providers/plugins/sso/sso.auth.ts index a08bbb6..a90efe6 100644 --- a/src/providers/plugins/sso/sso.auth.ts +++ b/src/providers/plugins/sso/sso.auth.ts @@ -34,12 +34,34 @@ export class CodeMieSSO { private server?: Server; private callbackResult?: SSOAuthResult; private codeMieUrl!: string; + private abortController?: AbortController; + private isAuthenticating = false; /** * Authenticate via browser SSO */ async authenticate(config: SSOAuthConfig): Promise { this.codeMieUrl = config.codeMieUrl; + this.isAuthenticating = true; + this.abortController = new AbortController(); + + // Register signal handlers for graceful termination (following agent.ts pattern) + const sigintHandler = () => { + if (this.isAuthenticating) { + console.log(chalk.yellow('\n⚠️ Authentication cancelled by user')); + this.abortController?.abort(); + } + }; + + const sigtermHandler = () => { + if (this.isAuthenticating) { + console.log(chalk.yellow('\n⚠️ Authentication terminated')); + this.abortController?.abort(); + } + }; + + process.once('SIGINT', sigintHandler); + process.once('SIGTERM', sigtermHandler); try { // 1. Start local callback server @@ -53,8 +75,11 @@ export class CodeMieSSO { console.log(chalk.white(`Opening browser for authentication...`)); await open(ssoUrl); - // 4. Wait for callback with timeout - const result = await this.waitForCallback(config.timeout || 120000); + // 4. Wait for callback with timeout and abort signal + const result = await this.waitForCallback( + config.timeout || 120000, + this.abortController.signal + ); // 5. Store credentials if successful if (result.success && result.apiUrl && result.cookies) { @@ -71,11 +96,25 @@ export class CodeMieSSO { return result; } catch (error) { + // Handle abort as user cancellation + if (error instanceof Error && error.name === 'AbortError') { + return { + success: false, + error: 'Authentication cancelled by user' + }; + } + return { success: false, error: error instanceof Error ? error.message : String(error) }; } finally { + this.isAuthenticating = false; + + // Remove signal handlers to prevent memory leaks (following agent.ts pattern) + process.off('SIGINT', sigintHandler); + process.off('SIGTERM', sigtermHandler); + this.cleanup(); } } @@ -292,24 +331,47 @@ export class CodeMieSSO { } /** - * Wait for OAuth callback with timeout + * Wait for OAuth callback with timeout and abort support */ - private async waitForCallback(timeout: number): Promise { + private async waitForCallback( + timeout: number, + abortSignal: AbortSignal + ): Promise { return new Promise((resolve, reject) => { - const timer = setTimeout(() => { + let timer: NodeJS.Timeout | undefined; + let pollInterval: NodeJS.Timeout | undefined; + + // Handle abort signal + const abortHandler = () => { + if (timer) clearTimeout(timer); + if (pollInterval) clearInterval(pollInterval); + reject(new Error('AbortError')); + }; + + // Handle timeout + timer = setTimeout(() => { + if (pollInterval) clearInterval(pollInterval); + abortSignal.removeEventListener('abort', abortHandler); reject(new Error('Authentication timeout - no response received')); }, timeout); - const checkResult = () => { + // Register abort handler + if (abortSignal.aborted) { + clearTimeout(timer); + reject(new Error('AbortError')); + return; + } + abortSignal.addEventListener('abort', abortHandler); + + // Poll for callback result (non-recursive) + pollInterval = setInterval(() => { if (this.callbackResult) { clearTimeout(timer); + clearInterval(pollInterval); + abortSignal.removeEventListener('abort', abortHandler); resolve(this.callbackResult); - } else { - setTimeout(checkResult, 100); } - }; - - checkResult(); + }, 100); }); } @@ -318,8 +380,19 @@ export class CodeMieSSO { */ private cleanup(): void { if (this.server) { - this.server.close(); + // Force close all connections immediately + this.server.closeAllConnections?.(); + + // Close the server + this.server.close(() => { + // Server closed callback (optional) + }); + delete this.server; } + + // Reset state + this.callbackResult = undefined; + this.abortController = undefined; } }