From 4d644e7a43979cbd7479cd883de13fd27bb50290 Mon Sep 17 00:00:00 2001 From: sol Date: Sun, 8 Mar 2026 08:05:15 +0000 Subject: [PATCH] fix: prevent duplicate status boxes on session idle/reactivation cycle - Added completedBoxes map to track idle sessions and their post IDs - On session reactivation, reuse existing post instead of creating new one - Fixed variable scoping bug (saved -> savedState) in session-added handler - Root cause: idle -> forgetSession -> re-detect -> new post -> repeat This was creating 10+ duplicate status boxes per session per hour. --- src/watcher-manager.js | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/watcher-manager.js b/src/watcher-manager.js index 8a4d494..02faca1 100644 --- a/src/watcher-manager.js +++ b/src/watcher-manager.js @@ -138,6 +138,11 @@ async function startDaemon() { // Map const activeBoxes = new Map(); + // Completed sessions: Map + // Tracks sessions that went idle so we can reuse their post on reactivation + // instead of creating duplicate status boxes. + const completedBoxes = new Map(); + // Pending sub-agents: spawnedBy key -> [info] (arrived before parent was added) const pendingSubAgents = new Map(); let globalMetrics = { @@ -266,11 +271,21 @@ async function startDaemon() { let postId; + // Check if this session was previously completed (reactivation after idle) + const completed = completedBoxes.get(sessionKey); + if (completed) { + postId = completed.postId; + completedBoxes.delete(sessionKey); + logger.info({ sessionKey, postId }, 'Reactivating completed session — reusing existing post'); + } + // Check for existing post (restart recovery) - const saved = savedOffsets[sessionKey]; // eslint-disable-line security/detect-object-injection - if (saved) { - // Try to find existing post in channel history - postId = await findExistingPost(sharedStatusBox, channelId, sessionKey, logger); + if (!postId) { + const saved = savedOffsets[sessionKey]; // eslint-disable-line security/detect-object-injection + if (saved) { + // Try to find existing post in channel history + postId = await findExistingPost(sharedStatusBox, channelId, sessionKey, logger); + } } // Create new post if none found @@ -304,8 +319,9 @@ async function startDaemon() { globalMetrics.activeSessions = activeBoxes.size; // Register in watcher - const initialState = saved - ? { lastOffset: saved.lastOffset, startTime: saved.startTime, agentId } + const savedState = savedOffsets[sessionKey]; // eslint-disable-line security/detect-object-injection + const initialState = savedState + ? { lastOffset: savedState.lastOffset, startTime: savedState.startTime, agentId } : { agentId }; watcher.addSession(sessionKey, transcriptFile, initialState); @@ -405,9 +421,17 @@ async function startDaemon() { logger.error({ sessionKey, err }, 'Failed to update final status'); } - // Clean up — remove from all tracking so session can be re-detected if it becomes active again + // Save to completedBoxes so we can reuse the post ID if the session reactivates + completedBoxes.set(sessionKey, { + postId: box.postId, + lastOffset: state.lastOffset || 0, + }); + + // Clean up active tracking activeBoxes.delete(sessionKey); watcher.removeSession(sessionKey); + // Forget from monitor so it CAN be re-detected — but completedBoxes + // ensures we reuse the existing post instead of creating a new one. monitor.forgetSession(sessionKey); globalMetrics.activeSessions = activeBoxes.size;