diff --git a/src/watcher-manager.js b/src/watcher-manager.js index 6be0ac2..e2baaf0 100644 --- a/src/watcher-manager.js +++ b/src/watcher-manager.js @@ -270,6 +270,32 @@ async function startDaemon() { return; } + // Dedup: skip if another active session already owns this channel. + // This prevents duplicate status boxes when a threadless parent session and a + // thread-specific child session both resolve to the same MM channel/DM. + // Thread sessions (containing ':thread:') take priority over bare channel sessions. + const isThreadSession = sessionKey.includes(':thread:'); + for (const [existingKey, existingBox] of activeBoxes) { + if (existingBox.channelId === channelId) { + const existingIsThread = existingKey.includes(':thread:'); + if (isThreadSession && !existingIsThread) { + // New session is a thread — it takes priority. Remove the parent box. + logger.info({ sessionKey, displaced: existingKey }, 'Thread session displacing bare channel session'); + activeBoxes.delete(existingKey); + watcher.removeSession(existingKey); + } else if (!isThreadSession && existingIsThread) { + // Existing session is a thread — skip this bare channel session. + logger.debug({ sessionKey, existingKey }, 'Bare channel session skipped — thread session already owns this channel'); + return; + } else { + // Same type — skip the newcomer, first-in wins. + logger.debug({ sessionKey, existingKey }, 'Duplicate channel session skipped'); + return; + } + break; + } + } + // Enforce MAX_ACTIVE_SESSIONS if (activeBoxes.size >= config.maxActiveSessions) { logger.warn(