From f9b8edcb426adbcd27a51142a6a7c9a78e518683 Mon Sep 17 00:00:00 2001 From: Chris Romp Date: Thu, 2 Apr 2026 10:40:49 -0700 Subject: [PATCH 1/2] fix: add error logging to all silent catch blocks (#178) HIGH (6): Add log.warn to user-visible silent failures: - getModelInfo, readPlan, deletePlan, getAuthStatus in session-manager - Mid-turn and command listModels in index.ts destroySession (8): Create safeDestroySession() helper withMEDIUM log.debug, replacing 8 identical silent catch blocks misc (11): Add log.debug/warn to:MEDIUM - listModels fire-and-forget (context cache, fallback, plan summarization) - getSessionMode fallbacks - Skill toggle RPCs - .env parse (ENOENT suppressed, other errors warned) - Config reload notification, cancelStream, plan surfacing LOW (12): Add log.debug to remaining best-effort catches: - Temp file cleanup, setTyping, addReaction - FS directory walks (MCP plugins, skill discovery) - SKILL.md reads, realpathSync, git diff in tool handlers Zero silent catch blocks remain in session-manager.ts and index.ts. Tests: 3 new tests for parseEnvFile error/ENOENT handling (683 total) Fixes #178 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/error-logging.test.ts | 58 ++++++++++++++++++++++++++ src/core/session-manager.ts | 75 +++++++++++++++++++++------------- src/index.ts | 33 +++++++-------- 3 files changed, 122 insertions(+), 44 deletions(-) create mode 100644 src/core/error-logging.test.ts diff --git a/src/core/error-logging.test.ts b/src/core/error-logging.test.ts new file mode 100644 index 0000000..191bdb8 --- /dev/null +++ b/src/core/error-logging.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import path from 'node:path'; +import os from 'node:os'; +import fs from 'node:fs'; + +// Capture the logger mock so we can assert on calls +const mockLog = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; +vi.mock('../logger.js', () => ({ + createLogger: () => mockLog, + setLogLevel: vi.fn(), + initLogFile: vi.fn(), +})); + +// Must import after mocking +const { parseEnvFile } = await import('./session-manager.js'); + +describe('error logging', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('parseEnvFile', () => { + it('does not log on ENOENT (missing file is expected)', () => { + const result = parseEnvFile('/tmp/nonexistent-env-file-test'); + expect(result).toEqual({}); + expect(mockLog.warn).not.toHaveBeenCalled(); + }); + + it('logs warn on non-ENOENT errors', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-test-')); + const dirPath = path.join(tmpDir, 'a-directory'); + fs.mkdirSync(dirPath); + + // Reading a directory as a file throws EISDIR, not ENOENT + const result = parseEnvFile(dirPath); + expect(result).toEqual({}); + expect(mockLog.warn).toHaveBeenCalledTimes(1); + expect(mockLog.warn).toHaveBeenCalledWith( + expect.stringContaining('Failed to parse .env file'), + expect.anything(), + ); + + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('parses valid .env file without warnings', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-test-')); + const envPath = path.join(tmpDir, '.env'); + fs.writeFileSync(envPath, 'FOO=bar\nBAZ="quoted"\n# comment\n'); + + const result = parseEnvFile(envPath); + expect(result).toEqual({ FOO: 'bar', BAZ: 'quoted' }); + expect(mockLog.warn).not.toHaveBeenCalled(); + + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + }); +}); diff --git a/src/core/session-manager.ts b/src/core/session-manager.ts index ce2074e..4a9319b 100644 --- a/src/core/session-manager.ts +++ b/src/core/session-manager.ts @@ -63,7 +63,10 @@ export function parseEnvFile(filePath: string): Record { if (key) vars[key] = value; } return vars; - } catch { + } catch (err: any) { + if (err?.code !== 'ENOENT') { + log.warn(`Failed to parse .env file ${filePath}:`, err); + } return {}; } } @@ -158,7 +161,7 @@ function loadMcpServers(): Record { walk(full); } } - } catch { /* permission errors etc */ } + } catch (err) { log.debug('FS walk permission error:', err); } }; walk(pluginsDir); } @@ -345,7 +348,7 @@ function discoverSkillDirectories(workingDirectory: string): string[] { walk(full, depth + 1); } } - } catch { /* permission errors etc */ } + } catch (err) { log.debug('Skill dir walk permission error:', err); } }; walk(pluginsDir, 0); } @@ -360,7 +363,7 @@ function discoverSkillDirectories(workingDirectory: string): string[] { dirs.push(path.join(skillsRoot, entry.name)); } } - } catch { /* permission errors etc */ } + } catch (err) { log.debug('Skill root readdir failed:', err); } } if (dirs.length > 0) { @@ -440,6 +443,15 @@ export class SessionManager { ensureWorkspacesDir(); } + /** Best-effort session destroy with logging. */ + private async safeDestroySession(sessionId: string): Promise { + try { + await this.bridge.destroySession(sessionId); + } catch (err) { + log.debug(`destroySession(${sessionId.slice(0, 8)}) failed (best-effort):`, err); + } + } + /** Resolve hooks for a workspace, caching the result. */ private async resolveHooks(workingDirectory: string): Promise { const cached = this.workspaceHooks.get(workingDirectory); @@ -594,7 +606,7 @@ export class SessionManager { const content = fs.readFileSync(skillFile, 'utf8'); const descMatch = content.match(/^description:\s*["']?(.+?)["']?\s*$/m); if (descMatch) description = descMatch[1]; - } catch { /* skip */ } + } catch (err) { log.debug(`Failed to read SKILL.md for ${name}:`, err); } } skills.push({ name, description, source, pending: sessionDirs ? !sessionDirs.has(dir) : undefined, disabled: disabledSet.has(name) }); @@ -657,7 +669,7 @@ export class SessionManager { if (unsub) { unsub(); this.sessionUnsubscribes.delete(existingId); } try { await this.bridge.destroySession(existingId); - } catch { /* best-effort */ } + } catch (err) { log.debug(`destroySession(${existingId.slice(0, 8)}) failed (best-effort):`, err); } this.channelSessions.delete(channelId); this.sessionChannels.delete(existingId); this.contextUsage.delete(channelId); @@ -686,7 +698,7 @@ export class SessionManager { // in-memory state (including MCP connections), allowing a clean re-init. const unsub = this.sessionUnsubscribes.get(existingId); if (unsub) { unsub(); this.sessionUnsubscribes.delete(existingId); } - try { await this.bridge.destroySession(existingId); } catch { /* best-effort */ } + await this.safeDestroySession(existingId); // Re-read global MCP servers so /reload picks up user-level config changes this.mcpServers = loadMcpServers(); @@ -733,7 +745,7 @@ export class SessionManager { if (existingId) { const unsub = this.sessionUnsubscribes.get(existingId); if (unsub) { unsub(); this.sessionUnsubscribes.delete(existingId); } - try { await this.bridge.destroySession(existingId); } catch { /* best-effort */ } + await this.safeDestroySession(existingId); this.channelSessions.delete(channelId); this.sessionChannels.delete(existingId); this.contextUsage.delete(channelId); @@ -751,7 +763,7 @@ export class SessionManager { if (otherChannel) { const unsub = this.sessionUnsubscribes.get(targetSessionId); if (unsub) { unsub(); this.sessionUnsubscribes.delete(targetSessionId); } - try { await this.bridge.destroySession(targetSessionId); } catch { /* best-effort */ } + await this.safeDestroySession(targetSessionId); this.channelSessions.delete(otherChannel); this.sessionChannels.delete(targetSessionId); this.contextUsage.delete(otherChannel); @@ -805,7 +817,7 @@ export class SessionManager { try { const models = await this.bridge.listModels(getConfig().providers); availableModels = models.map(m => m.id); - } catch { /* best-effort */ } + } catch (err) { log.debug('listModels for fallback chain failed:', err); } const byokPrefixes = Object.keys(getConfig().providers ?? {}); const chain = buildFallbackChain(prefs.model, availableModels, configFallbacks, byokPrefixes); @@ -845,7 +857,7 @@ export class SessionManager { log.info(`Attempting to re-attach session ${sessionId}...`); const unsub = this.sessionUnsubscribes.get(sessionId); if (unsub) { unsub(); this.sessionUnsubscribes.delete(sessionId); } - try { await this.bridge.destroySession(sessionId); } catch { /* best-effort */ } + await this.safeDestroySession(sessionId); await this.attachSession(channelId, sessionId); const reconnected = this.bridge.getSession(sessionId); if (reconnected) { @@ -918,7 +930,7 @@ export class SessionManager { try { const unsub = this.sessionUnsubscribes.get(sessionId); if (unsub) { unsub(); this.sessionUnsubscribes.delete(sessionId); } - try { await this.bridge.destroySession(sessionId); } catch { /* best-effort */ } + await this.safeDestroySession(sessionId); await this.attachSession(channelId, sessionId); } catch (attachErr: any) { log.warn(`Re-attach failed for ${sessionId}:`, attachErr?.message ?? attachErr); @@ -1060,7 +1072,7 @@ export class SessionManager { if (prefs.model === model) { await this.cacheContextWindowTokens(channelId, model, models); } - }).catch(() => { /* best-effort */ }); + }).catch((err) => { log.debug("listModels (context cache) failed:", err); }); } /** Check if two models on the same provider have different wireApi settings. */ @@ -1125,7 +1137,8 @@ export class SessionManager { try { const models = await this.bridge.listModels(getConfig().providers); return models.find(m => m.id === modelId) ?? null; - } catch { + } catch (err) { + log.warn('getModelInfo: listModels failed:', err); return null; } } @@ -1142,7 +1155,7 @@ export class SessionManager { try { const result = await this.withSessionRetry(channelId, (sid) => this.bridge.getSessionMode(sid), false); return result.mode; - } catch { /* fall through to prefs */ } + } catch (err) { log.debug(`getSessionMode(${channelId.slice(0, 8)}) failed, falling through to prefs:`, err); } } const prefs = await getChannelPrefs(channelId); return prefs?.sessionMode ?? 'interactive'; @@ -1163,7 +1176,8 @@ export class SessionManager { try { const result = await this.withSessionRetry(channelId, (sid) => this.bridge.readPlan(sid), false); return { exists: result.exists, content: result.content }; - } catch { + } catch (err) { + log.warn(`readPlan failed for ${channelId.slice(0, 8)}:`, err); return { exists: false, content: null }; } } @@ -1175,7 +1189,8 @@ export class SessionManager { try { await this.withSessionRetry(channelId, (sid) => this.bridge.deletePlan(sid), false); return true; - } catch { + } catch (err) { + log.warn(`deletePlan failed for ${channelId.slice(0, 8)}:`, err); return false; } } @@ -1217,7 +1232,7 @@ export class SessionManager { try { const models = await this.bridge.listModels(getConfig().providers); availableIds = models.map(m => m.id); - } catch { /* best-effort */ } + } catch (err) { log.debug('listModels for plan summarization failed:', err); } let model: string | undefined; for (const candidate of SessionManager.SUMMARIZER_MODELS) { @@ -1244,7 +1259,7 @@ export class SessionManager { return null; } finally { if (session) { - try { await this.bridge.destroySession(session.sessionId); } catch { /* best-effort */ } + await this.safeDestroySession(session.sessionId); } } } @@ -1262,7 +1277,7 @@ export class SessionManager { try { const mode = await this.getSessionMode(channelId); inPlanMode = mode === 'plan'; - } catch { /* best-effort */ } + } catch (err) { log.debug(`getSessionMode for plan surfacing (${channelId.slice(0, 8)}) failed:`, err); } return { exists: true, summary, inPlanMode }; } @@ -1271,7 +1286,8 @@ export class SessionManager { async getAuthStatus(): Promise<{ isAuthenticated: boolean; statusMessage?: string; login?: string }> { try { return await this.bridge.getAuthStatus(); - } catch { + } catch (err) { + log.warn('getAuthStatus failed:', err); return { isAuthenticated: false, statusMessage: 'Unable to check auth status' }; } } @@ -1571,8 +1587,8 @@ export class SessionManager { try { modelList = await this.bridge.listModels(getConfig().providers); availableModels = modelList.map(m => m.id); - } catch { - log.warn('Failed to fetch model list for fallback resolution'); + } catch (err) { + log.warn('Failed to fetch model list for fallback resolution:', err); } const resolvedMcpServers = this.resolveMcpServers(workingDirectory); @@ -1725,7 +1741,7 @@ export class SessionManager { if (currentPrefs.model === resumeModel) { await this.cacheContextWindowTokens(channelId, resumeModel, models); } - }).catch(() => { /* best-effort */ }); + }).catch((err) => { log.debug("listModels (context cache) failed:", err); }); } /** @@ -1850,7 +1866,7 @@ export class SessionManager { } finally { if (session) { - try { await this.bridge.destroySession(session.sessionId); } catch { /* best-effort */ } + await this.safeDestroySession(session.sessionId); } } } @@ -2115,7 +2131,8 @@ export class SessionManager { let realPath: string; try { realPath = fs.realpathSync(resolved); - } catch { + } catch (err) { + log.debug(`send_file: realpathSync failed for "${resolved}":`, err); return { content: 'File not found.' }; } // Validate the real file path is within workspace or allowed paths @@ -2164,7 +2181,8 @@ export class SessionManager { let realPath: string; try { realPath = fs.realpathSync(resolved); - } catch { + } catch (err) { + log.debug(`show_file: realpathSync failed for "${resolved}":`, err); return { content: 'File not found.' }; } const allowed = [showWorkDir, ...showAllowPaths]; @@ -2187,7 +2205,8 @@ export class SessionManager { try { content = execFileSync('git', ['diff', '--', realPath], { cwd: dir, encoding: 'utf-8', timeout: 5000 }); if (!content.trim()) content = '(no pending changes)'; - } catch { + } catch (err) { + log.debug(`show_file: git diff failed for "${realPath}":`, err); content = '(not a git repository or git diff failed)'; } await adapter.sendMessage(channelId, `**${fileName}** (diff)\n\`\`\`\`diff\n${content}\n\`\`\`\``); diff --git a/src/index.ts b/src/index.ts index f7271b7..150bcd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -205,14 +205,14 @@ async function cleanupTempFiles(channelId: string): Promise { for (const file of files) { try { fs.unlinkSync(path.join(tempDir, file)); - } catch { /* best effort */ } + } catch (err) { log.debug(`Failed to remove temp file ${file}:`, err); } } // Remove the now-empty channel temp directory - try { fs.rmdirSync(tempDir); } catch { /* best effort */ } + try { fs.rmdirSync(tempDir); } catch (err) { log.debug(`Failed to remove temp dir ${tempDir}:`, err); } if (files.length > 0) { log.info(`Cleaned up ${files.length} temp file(s) for ${channelId.slice(0, 8)}...`); } - } catch { /* best effort */ } + } catch (err) { log.debug(`cleanupTempFiles(${channelId.slice(0, 8)}) failed:`, err); } } async function getAdapterForChannel(channelId: string): Promise<{ adapter: ChannelAdapter; streaming: StreamingHandler } | null> { @@ -398,7 +398,7 @@ async function main(): Promise { for (const ch of getConfig().channels) { if (ch.bot === botName && !ch.isDM) { const warnings = result.restartNeeded.map(r => ` ⚠️ ${r}`).join('\n'); - adapter.sendMessage(ch.id, `**Config reloaded** with changes that need a restart:\n${warnings}`).catch(() => {}); + adapter.sendMessage(ch.id, `**Config reloaded** with changes that need a restart:\n${warnings}`).catch((err) => { log.debug('Failed to send config reload notification:', err); }); break; // one admin channel is enough } } @@ -660,7 +660,7 @@ async function main(): Promise { const failedStream = activeStreams.get(channelId); if (failedStream) { const r = await getAdapterForChannel(channelId); - if (r) await r.streaming.cancelStream(failedStream, err?.message ?? 'Scheduled job failed').catch(() => {}); + if (r) await r.streaming.cancelStream(failedStream, err?.message ?? 'Scheduled job failed').catch((e: any) => { log.debug('cancelStream failed:', e); }); activeStreams.delete(channelId); } throw err; @@ -866,7 +866,7 @@ async function handleMidTurnMessage( const effPrefs = await sessionManager.getEffectivePrefs(msg.channelId); let models: any[] | undefined; if (['model', 'models', 'status'].includes(parsed.command)) { - try { models = await sessionManager.listModels(); } catch { models = undefined; } + try { models = await sessionManager.listModels(); } catch (err) { log.warn('listModels failed (mid-turn):', err); models = undefined; } } const mcpInfo = undefined; const contextUsage = sessionManager.getContextUsage(msg.channelId); @@ -915,7 +915,7 @@ async function handleMidTurnMessage( await sessionManager.sendMidTurn(msg.channelId, text, msg.userId); // Acknowledge with ⚡ reaction (best-effort) - try { adapter.addReaction?.(msg.postId, 'zap')?.catch(() => {}); } catch { /* best-effort */ } + try { adapter.addReaction?.(msg.postId, 'zap')?.catch((err: any) => { log.debug('addReaction failed:', err); }); } catch (err) { log.debug('addReaction threw:', err); } } /** Test BYOK provider connectivity by hitting its models endpoint. */ @@ -1076,7 +1076,8 @@ async function handleInboundMessage( if (parsed && ['model', 'models', 'status', 'reasoning'].includes(parsed.command)) { try { models = await sessionManager.listModels(); - } catch { + } catch (err) { + log.warn('listModels failed:', err); // Check if the failure is an auth issue const auth = await sessionManager.getAuthStatus(); if (!auth.isAuthenticated) { @@ -1240,7 +1241,7 @@ async function handleInboundMessage( { threadRootId: threadRoot }); } } - } catch { /* plan surfacing is best-effort */ } + } catch (planErr) { log.debug('Plan surfacing failed (best-effort):', planErr); /* plan surfacing is best-effort */ } } catch (err: any) { await adapter.updateMessage(msg.channelId, resumeAck, `❌ Failed to resume session: ${err?.message ?? 'unknown error'}`); } @@ -1557,14 +1558,14 @@ async function handleInboundMessage( await setChannelPrefs(msg.channelId, { disabledSkills: allNames }); // Apply via RPC for each skill for (const name of allNames) { - try { await sessionManager.toggleSkillRpc(msg.channelId, name, 'disable'); } catch { /* best-effort */ } + try { await sessionManager.toggleSkillRpc(msg.channelId, name, 'disable'); } catch (err) { log.debug(`toggleSkillRpc disable ${name} failed:`, err); } } await adapter.sendMessage(msg.channelId, `🔴 Disabled all ${allNames.length} skills.`, { threadRootId: threadRoot }); } else { const allNames = [...currentDisabled]; await setChannelPrefs(msg.channelId, { disabledSkills: [] }); for (const name of allNames) { - try { await sessionManager.toggleSkillRpc(msg.channelId, name, 'enable'); } catch { /* best-effort */ } + try { await sessionManager.toggleSkillRpc(msg.channelId, name, 'enable'); } catch (err) { log.debug(`toggleSkillRpc enable ${name} failed:`, err); } } await adapter.sendMessage(msg.channelId, `🟢 Enabled all skills.`, { threadRootId: threadRoot }); } @@ -1601,7 +1602,7 @@ async function handleInboundMessage( await setChannelPrefs(msg.channelId, { disabledSkills: [...currentDisabled] }); // Apply each toggle via RPC (best-effort — pref is already persisted) for (const name of matched) { - try { await sessionManager.toggleSkillRpc(msg.channelId, name, toggleAction); } catch { /* best-effort */ } + try { await sessionManager.toggleSkillRpc(msg.channelId, name, toggleAction); } catch (err) { log.debug(`toggleSkillRpc ${toggleAction} ${name} failed:`, err); } } } @@ -1891,7 +1892,7 @@ async function handleInboundMessage( console.log(`[bridge] Forwarding to Copilot: "${text}"`); log.info(`Forwarding to Copilot: "${text.slice(0, 100)}"`); - adapter.setTyping(msg.channelId).catch(() => {}); + adapter.setTyping(msg.channelId).catch((err: any) => { log.debug("setTyping failed:", err); }); // Atomically swap streams via eventLocks to prevent event interleaving const threadRoot = resolveThreadRoot(msg, threadRequested, channelConfig); @@ -2288,7 +2289,7 @@ async function handleSessionEvent( } else if (formatted.content) { streaming.appendDelta(streamKey, formatted.content); } - adapter.setTyping(channelId).catch(() => {}); + adapter.setTyping(channelId).catch((err: any) => { log.debug("setTyping failed:", err); }); break; } } @@ -2311,7 +2312,7 @@ async function handleSessionEvent( streaming.appendDelta(streamKey, formatted.content); } } - adapter.setTyping(channelId).catch(() => {}); + adapter.setTyping(channelId).catch((err: any) => { log.debug("setTyping failed:", err); }); break; } case 'tool_start': @@ -2462,7 +2463,7 @@ async function handleSessionEvent( log.warn(`Failed to revert yolo state on idle for ${channelId.slice(0, 8)}...:`, err); } // Clean up temp files from downloaded attachments - void cleanupTempFiles(channelId).catch(() => { /* best-effort */ }); + void cleanupTempFiles(channelId).catch((err) => { log.debug('cleanupTempFiles failed:', err); }); } break; } From b6f2ec1620416ad51ac09e42fc948dc68a347b8c Mon Sep 17 00:00:00 2001 From: Chris Romp Date: Thu, 2 Apr 2026 14:42:52 -0700 Subject: [PATCH 2/2] fix: address CCR feedback on error logging PR - Add logging to lock chain .catch() patterns (eventLocks, channelLocks) - Add logging to surfacePlanIfExists promise catch - Remove unused afterEach import in tests - Use os.tmpdir() for ENOENT test instead of hardcoded /tmp path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/error-logging.test.ts | 7 +++++-- src/index.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/core/error-logging.test.ts b/src/core/error-logging.test.ts index 191bdb8..06e136a 100644 --- a/src/core/error-logging.test.ts +++ b/src/core/error-logging.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import path from 'node:path'; import os from 'node:os'; import fs from 'node:fs'; @@ -21,9 +21,12 @@ describe('error logging', () => { describe('parseEnvFile', () => { it('does not log on ENOENT (missing file is expected)', () => { - const result = parseEnvFile('/tmp/nonexistent-env-file-test'); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-test-')); + const missingPath = path.join(tmpDir, 'nonexistent.env'); + const result = parseEnvFile(missingPath); expect(result).toEqual({}); expect(mockLog.warn).not.toHaveBeenCalled(); + fs.rmSync(tmpDir, { recursive: true, force: true }); }); it('logs warn on non-ENOENT errors', () => { diff --git a/src/index.ts b/src/index.ts index 150bcd1..ea77f56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -643,7 +643,7 @@ async function main(): Promise { activeStreams.delete(channelId); } }); - eventLocks.set(channelId, evTask.catch(() => {})); + eventLocks.set(channelId, evTask.catch((err) => { log.debug("Event lock task failed:", err); })); await evTask; markBusy(channelId); } @@ -668,7 +668,7 @@ async function main(): Promise { clearQuiet(); } }); - channelLocks.set(channelId, task.catch(() => {})); + channelLocks.set(channelId, task.catch((err) => { log.debug("Channel lock task failed:", err); })); await task; return ''; }, @@ -909,7 +909,7 @@ async function handleMidTurnMessage( const newKey = await resolved.streaming.startStream(msg.channelId); activeStreams.set(msg.channelId, newKey); }); - eventLocks.set(msg.channelId, evTask.catch(() => {})); + eventLocks.set(msg.channelId, evTask.catch((err) => { log.debug("Event lock task failed:", err); })); await evTask; await sessionManager.sendMidTurn(msg.channelId, text, msg.userId); @@ -1800,7 +1800,7 @@ async function handleInboundMessage( const streamKey = await streaming.startStream(msg.channelId, threadRoot); activeStreams.set(msg.channelId, streamKey); }); - eventLocks.set(msg.channelId, evTask.catch(() => {})); + eventLocks.set(msg.channelId, evTask.catch((err) => { log.debug("Event lock task failed:", err); })); await evTask; markBusy(msg.channelId); @@ -1907,7 +1907,7 @@ async function handleInboundMessage( const streamKey = await streaming.startStream(msg.channelId, threadRoot); activeStreams.set(msg.channelId, streamKey); }); - eventLocks.set(msg.channelId, evTask.catch(() => {})); + eventLocks.set(msg.channelId, evTask.catch((err) => { log.debug("Event lock task failed:", err); })); await evTask; // Mark busy before send so mid-turn messages arriving during the await are steered @@ -1940,7 +1940,7 @@ async function handleInboundMessage( `📋 **Existing plan found** — ${result.summary}. \`/plan show\` to review.`, { threadRootId: threadRootForPlan }); } - }).catch(() => { /* best-effort */ }); + }).catch((err) => { log.debug('surfacePlanIfExists failed:', err); }); } // Hold the channelLock until session.idle so queued work (scheduler, etc.)