From 1ef4095feec076b5d2a0867198102e93e3f3c196 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 20:36:45 +0000 Subject: [PATCH 1/2] refactor: improve libsignal error logging and deduplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements 4 key improvements to reduce log noise and duplicates: 1. ✅ Remove console.log interceptor - Libsignal only uses console.error for errors - Eliminates duplicate interception (was logging 2x per error) - Reduces code by 50+ lines 2. ✅ Increase deduplication window (100ms → 150ms) - Better coverage for rapid sequential errors - Configurable via DEDUP_WINDOW_MS constant 3. ✅ Type + JID tracking for smart deduplication - Uses Map instead of single variable - Deduplication key: "errorType:maskedJID" - Prevents false positives (different error types now tracked separately) - Memory-safe: auto-cleanup keeps last 50 entries max 4. ✅ Concise session recreation summary - Before: Verbose multi-line debug logs - After: Single line "🔄 Session Reset | JID: 4680****_1.0 | Cleared: 6 devices" - Shows masked JID, deleted count, and next action - cleanupCorruptedSession() now returns deletion count Expected log improvement: - Before: 3-9 duplicate logs per error burst - After: 1 log per unique error type per contact https://claude.ai/code/session_01TvSrN9JmHZDdKMZDFWEcUS --- src/Utils/decode-wa-message.ts | 20 ++++---- src/index.ts | 83 +++++++++------------------------- 2 files changed, 33 insertions(+), 70 deletions(-) diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index 1ff3f7d3..7a5fa635 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -425,10 +425,15 @@ export const decryptMessageNode = ( // Automatic cleanup of corrupted session (if enabled) if (autoCleanCorrupted) { try { - await cleanupCorruptedSession(decryptionJid, repository, logger) + const deletedCount = await cleanupCorruptedSession(decryptionJid, repository, logger) + + // Mask JID for privacy (show first 4 and last 4 digits) + const maskedJid = decryptionJid.length > 8 + ? `${decryptionJid.substring(0, 4)}****${decryptionJid.substring(decryptionJid.length - 4)}` + : decryptionJid + logger.info( - { decryptionJid, author }, - '✅ Corrupted session cleaned up. New session will be created on next message.' + `🔄 Session Reset | JID: ${maskedJid} | Cleared: ${deletedCount} devices | Will recreate on next message` ) } catch (cleanupErr) { logger.error( @@ -500,11 +505,11 @@ async function cleanupCorruptedSession( jid: string, repository: SignalRepositoryWithLIDStore, logger: ILogger -): Promise { +): Promise { const { user, device } = jidDecode(jid) || {} if (!user) { logger.warn({ jid }, 'Cannot cleanup session - invalid JID') - return + return 0 } // Build list of JIDs to delete (primary + secondary devices) @@ -541,8 +546,5 @@ async function cleanupCorruptedSession( await repository.deleteSession(jidsToDelete) - logger.debug( - { jid, user, device, deletedSessions: jidsToDelete }, - 'Deleted corrupted sessions for user' - ) + return jidsToDelete.length } diff --git a/src/index.ts b/src/index.ts index 48ee4303..644f156a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,62 +5,9 @@ const _origConsoleLog = console.log const _origConsoleError = console.error -// Track last error to avoid duplicates -let _lastErrorMsg = '' -let _lastErrorTime = 0 - -console.log = function(...args: unknown[]) { - if (args.length > 0 && typeof args[0] === 'string') { - const msg = args[0] - const stack = new Error().stack || '' - const isFromLibsignal = stack.includes('libsignal') || stack.includes('session_cipher') - - if (isFromLibsignal) { - // Suppress session lifecycle dumps - if (msg.startsWith('Closing session')) { - return - } - - // Format session errors cleanly - if ( - msg.includes('Session error') || - msg.includes('Bad MAC') || - msg.includes('MessageCounterError') || - msg.includes('Key used already') || - msg.includes('Failed to decrypt') - ) { - // Extract error type - let errorType = '⚠️ Session Error' - if (msg.includes('Bad MAC')) errorType = '🔐 Bad MAC Error' - else if (msg.includes('MessageCounterError') || msg.includes('Key used already')) errorType = '🔢 Counter Error' - else if (msg.includes('Failed to decrypt')) errorType = '🔌 Decryption Failed' - - // Extract JID if present (format: 46802258641027_1.0 or similar) - const jidMatch = msg.match(/(\d{10,}(?:_\d+\.\d+)?)/); - const jid = jidMatch ? jidMatch[1] : null - const maskedJid = jid && jid.length > 8 ? `${jid.substring(0, 4)}****${jid.substring(jid.length - 4)}` : jid - - // Format clean message - const cleanMsg = maskedJid - ? `${errorType} | JID: ${maskedJid}` - : errorType - - // Avoid duplicate logs (within 100ms) - const now = Date.now() - if (cleanMsg === _lastErrorMsg && now - _lastErrorTime < 100) { - return - } - _lastErrorMsg = cleanMsg - _lastErrorTime = now - - _origConsoleError(cleanMsg) - return - } - } - } - - _origConsoleLog.apply(console, args) -} +// Track errors by type + JID to avoid duplicates (using Map for better performance) +const _errorTimestamps = new Map() +const DEDUP_WINDOW_MS = 150 console.error = function(...args: unknown[]) { if (args.length > 0 && typeof args[0] === 'string') { @@ -69,6 +16,11 @@ console.error = function(...args: unknown[]) { const isFromLibsignal = stack.includes('libsignal') || stack.includes('session_cipher') if (isFromLibsignal) { + // Suppress session lifecycle logs + if (msg.startsWith('Closing session')) { + return + } + // Format session errors cleanly if ( msg.includes('Session error') || @@ -93,13 +45,22 @@ console.error = function(...args: unknown[]) { ? `${errorType} | JID: ${maskedJid}` : errorType - // Avoid duplicate logs (within 100ms) + // Deduplication key: type + JID (prevents duplicate logs for same error type on same contact) + const dedupeKey = `${errorType}:${maskedJid || 'unknown'}` const now = Date.now() - if (cleanMsg === _lastErrorMsg && now - _lastErrorTime < 100) { - return + const lastTime = _errorTimestamps.get(dedupeKey) + + if (lastTime && now - lastTime < DEDUP_WINDOW_MS) { + return // Skip duplicate within 150ms window + } + + _errorTimestamps.set(dedupeKey, now) + + // Cleanup old entries (keep only last 50 to prevent memory leak) + if (_errorTimestamps.size > 50) { + const oldestKey = _errorTimestamps.keys().next().value + if (oldestKey) _errorTimestamps.delete(oldestKey) } - _lastErrorMsg = cleanMsg - _lastErrorTime = now _origConsoleError(cleanMsg) return From be88bc870c551c06cc25ba13db2987065a747524 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 21:27:37 +0000 Subject: [PATCH 2/2] fix: address all 6 Copilot AI review comments from PR #154 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes all valid issues identified by Copilot AI review: 1. 🔴 CRITICAL: Fix deduplication key collision risk - Before: Used maskedJid in dedupe key (e.g., "4680****7890") - Problem: Different JIDs with same first/last 4 digits collide - After: Use original unmasked JID for dedup key - Impact: Prevents legitimate errors from being suppressed 2. 🟡 Fix JID masking logic flaw - Before: Masked full JID string including domain (e.g., "1234****.net") - Problem: Obscures useful info while masking wrong part - After: Decode JID, mask only user portion, preserve domain - Result: "4680****7890@s.whatsapp.net" instead of "1234****.net" 3. 🟡 Add structured logging context - Before: logger.info(string) - loses queryability - After: logger.info({ jid, maskedJid, targetedDevices }, message) - Impact: Maintains observability and log filtering capability 4. 🟡 Fix misleading deletion count wording - Before: "Cleared: X devices" (implies actual deletions) - Problem: deleteSession() returns void, count is targeted - After: "Targeted: X devices" (reflects actual behavior) 5. 🟢 Remove unused variable - Removed _origConsoleLog (dead code after removing console.log interceptor) - Prevents no-unused-vars lint failure 6. 🟢 Fix semicolon format violation - Repository config: semi: false (Prettier) - Removed trailing semicolon from JID extraction line - Prevents eslint-plugin-prettier check failure All changes maintain backward compatibility while fixing correctness, observability, and code quality issues. https://claude.ai/code/session_01TvSrN9JmHZDdKMZDFWEcUS --- src/Utils/decode-wa-message.ts | 13 ++++++++----- src/index.ts | 7 +++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index 7a5fa635..2ef0d008 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -427,13 +427,16 @@ export const decryptMessageNode = ( try { const deletedCount = await cleanupCorruptedSession(decryptionJid, repository, logger) - // Mask JID for privacy (show first 4 and last 4 digits) - const maskedJid = decryptionJid.length > 8 - ? `${decryptionJid.substring(0, 4)}****${decryptionJid.substring(decryptionJid.length - 4)}` - : decryptionJid + // Mask only user portion of JID for privacy (preserve domain info) + const { user, server } = jidDecode(decryptionJid) || {} + const maskedUser = user && user.length > 8 + ? `${user.substring(0, 4)}****${user.substring(user.length - 4)}` + : user + const maskedJid = maskedUser && server ? `${maskedUser}@${server}` : decryptionJid logger.info( - `🔄 Session Reset | JID: ${maskedJid} | Cleared: ${deletedCount} devices | Will recreate on next message` + { jid: decryptionJid, maskedJid, targetedDevices: deletedCount }, + `🔄 Session Reset | JID: ${maskedJid} | Targeted: ${deletedCount} devices | Will recreate on next message` ) } catch (cleanupErr) { logger.error( diff --git a/src/index.ts b/src/index.ts index 644f156a..7449b7ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ // Format session error logs from libsignal BEFORE any imports // This MUST be at the very top to intercept console before libsignal loads // ============================================ -const _origConsoleLog = console.log const _origConsoleError = console.error // Track errors by type + JID to avoid duplicates (using Map for better performance) @@ -36,7 +35,7 @@ console.error = function(...args: unknown[]) { else if (msg.includes('Failed to decrypt')) errorType = '🔌 Decryption Failed' // Extract JID from stack trace or message - const jidMatch = (msg + String(args[1] ?? '')).match(/(\d{10,}(?:_\d+\.\d+)?)/); + const jidMatch = (msg + String(args[1] ?? '')).match(/(\d{10,}(?:_\d+\.\d+)?)/) const jid = jidMatch ? jidMatch[1] : null const maskedJid = jid && jid.length > 8 ? `${jid.substring(0, 4)}****${jid.substring(jid.length - 4)}` : jid @@ -45,8 +44,8 @@ console.error = function(...args: unknown[]) { ? `${errorType} | JID: ${maskedJid}` : errorType - // Deduplication key: type + JID (prevents duplicate logs for same error type on same contact) - const dedupeKey = `${errorType}:${maskedJid || 'unknown'}` + // Deduplication key: type + ORIGINAL JID (use unmasked to prevent collisions) + const dedupeKey = `${errorType}:${jid || 'unknown'}` const now = Date.now() const lastTime = _errorTimestamps.get(dedupeKey)