From 3fbd46c2d261bc4428164d845403e872536b9839 Mon Sep 17 00:00:00 2001 From: Xen Date: Mon, 9 Mar 2026 21:10:41 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20dedup=20status=20boxes=20=E2=80=94=20one?= =?UTF-8?q?=20per=20channel,=20thread=20sessions=20take=20priority?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a bare channel session (no :thread: suffix) and a thread-specific session both resolve to the same MM channel/DM, two status boxes appeared simultaneously. Fix: in session-added handler, before creating a box, check if any existing active session already owns that channelId. Thread sessions displace bare channel sessions (and take priority). Bare channel sessions are skipped if a thread session already exists. First-in wins for same-type duplicates. --- src/watcher-manager.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) 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(