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:
sol
2026-03-08 08:05:15 +00:00
parent 09441b34c1
commit 4d644e7a43

View File

@@ -138,6 +138,11 @@ async function startDaemon() {
// Map<sessionKey, { postId, agentId, channelId, rootPostId, children: 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)
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;