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.
This commit is contained in:
@@ -138,6 +138,11 @@ async function startDaemon() {
|
|||||||
// Map<sessionKey, { postId, agentId, channelId, rootPostId, children: Map }>
|
// Map<sessionKey, { postId, agentId, channelId, rootPostId, children: Map }>
|
||||||
const activeBoxes = new Map();
|
const activeBoxes = new Map();
|
||||||
|
|
||||||
|
// Completed sessions: Map<sessionKey, { postId, lastOffset }>
|
||||||
|
// 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)
|
// Pending sub-agents: spawnedBy key -> [info] (arrived before parent was added)
|
||||||
const pendingSubAgents = new Map();
|
const pendingSubAgents = new Map();
|
||||||
let globalMetrics = {
|
let globalMetrics = {
|
||||||
@@ -266,12 +271,22 @@ async function startDaemon() {
|
|||||||
|
|
||||||
let postId;
|
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)
|
// Check for existing post (restart recovery)
|
||||||
|
if (!postId) {
|
||||||
const saved = savedOffsets[sessionKey]; // eslint-disable-line security/detect-object-injection
|
const saved = savedOffsets[sessionKey]; // eslint-disable-line security/detect-object-injection
|
||||||
if (saved) {
|
if (saved) {
|
||||||
// Try to find existing post in channel history
|
// Try to find existing post in channel history
|
||||||
postId = await findExistingPost(sharedStatusBox, channelId, sessionKey, logger);
|
postId = await findExistingPost(sharedStatusBox, channelId, sessionKey, logger);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create new post if none found
|
// Create new post if none found
|
||||||
if (!postId) {
|
if (!postId) {
|
||||||
@@ -304,8 +319,9 @@ async function startDaemon() {
|
|||||||
globalMetrics.activeSessions = activeBoxes.size;
|
globalMetrics.activeSessions = activeBoxes.size;
|
||||||
|
|
||||||
// Register in watcher
|
// Register in watcher
|
||||||
const initialState = saved
|
const savedState = savedOffsets[sessionKey]; // eslint-disable-line security/detect-object-injection
|
||||||
? { lastOffset: saved.lastOffset, startTime: saved.startTime, agentId }
|
const initialState = savedState
|
||||||
|
? { lastOffset: savedState.lastOffset, startTime: savedState.startTime, agentId }
|
||||||
: { agentId };
|
: { agentId };
|
||||||
watcher.addSession(sessionKey, transcriptFile, initialState);
|
watcher.addSession(sessionKey, transcriptFile, initialState);
|
||||||
|
|
||||||
@@ -405,9 +421,17 @@ async function startDaemon() {
|
|||||||
logger.error({ sessionKey, err }, 'Failed to update final status');
|
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);
|
activeBoxes.delete(sessionKey);
|
||||||
watcher.removeSession(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);
|
monitor.forgetSession(sessionKey);
|
||||||
globalMetrics.activeSessions = activeBoxes.size;
|
globalMetrics.activeSessions = activeBoxes.size;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user