fix: reactivate session on new turn via sessions.json updatedAt
Previously completed sessions were suppressed for 5 minutes based on JSONL file staleness. With fast-idle (cache-ttl detection), sessions complete in ~3s — but the gateway immediately appends the next user message, keeping the file 'fresh'. This blocked reactivation entirely. Fix: compare sessions.json updatedAt against the completion timestamp. If the gateway updated the session AFTER we marked it complete, a new turn has started — reactivate immediately. Pure infrastructure: timestamp comparison between two on-disk files. No AI model state or memory involved.
This commit is contained in:
@@ -48,6 +48,9 @@ class SessionMonitor extends EventEmitter {
|
||||
this._knownSessions = new Map();
|
||||
// Set<sessionKey> — sessions that were skipped as stale; re-check on next poll
|
||||
this._staleSessions = new Set();
|
||||
// Map<sessionKey, expiresAt> — sessions that completed idle; suppressed from re-detection
|
||||
// until the transcript file stops being written to (checked on each poll).
|
||||
this._completedSessions = new Map();
|
||||
// Cache: "user:XXXX" -> channelId (resolved DM channels)
|
||||
this._dmChannelCache = new Map();
|
||||
this._pollTimer = null;
|
||||
@@ -80,10 +83,29 @@ class SessionMonitor extends EventEmitter {
|
||||
/**
|
||||
* Remove a session from known sessions so it can be re-detected on next poll.
|
||||
* Called when the watcher marks a session as idle/done.
|
||||
*
|
||||
* The session is placed in a cooldown set (_completedSessions). It will only be
|
||||
* re-emitted as 'session-added' once the transcript file goes stale (>5 min no
|
||||
* writes), preventing the complete→reactivate loop that occurs when the gateway
|
||||
* keeps appending events to a session file after the agent finishes its turn.
|
||||
*
|
||||
* @param {string} sessionKey
|
||||
*/
|
||||
forgetSession(sessionKey) {
|
||||
this._knownSessions.delete(sessionKey);
|
||||
// Mark as completed — suppress re-detection while transcript is still active.
|
||||
// The stale check in _handleNewSession will naturally unblock re-detection
|
||||
// once the file stops being modified (>5 min gap).
|
||||
this._completedSessions.set(sessionKey, Date.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly clear a session from the completed cooldown, allowing immediate re-detection.
|
||||
* Use this when a new gateway session replaces an old one with the same key.
|
||||
* @param {string} sessionKey
|
||||
*/
|
||||
clearCompleted(sessionKey) {
|
||||
this._completedSessions.delete(sessionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,6 +372,25 @@ class SessionMonitor extends EventEmitter {
|
||||
// Detect new or previously-stale sessions
|
||||
for (const [sessionKey, entry] of currentSessions) {
|
||||
if (!this._knownSessions.has(sessionKey) || this._staleSessions.has(sessionKey)) {
|
||||
// Skip sessions in completed cooldown — only allow re-detection once the
|
||||
// transcript has gone stale (file not modified in >5 min). This prevents
|
||||
// the reactivation loop caused by the gateway appending bookkeeping events
|
||||
// to the JSONL after a session's agent turn finishes.
|
||||
if (this._completedSessions.has(sessionKey)) {
|
||||
const completedAt = this._completedSessions.get(sessionKey);
|
||||
// Re-activate if sessions.json shows a newer updatedAt than when we
|
||||
// marked the session complete. This is the infrastructure-level signal
|
||||
// that the gateway started a new turn — no AI involvement, pure timestamp
|
||||
// comparison between two on-disk data sources.
|
||||
const sessionUpdatedAt = entry.updatedAt || 0;
|
||||
if (sessionUpdatedAt > completedAt) {
|
||||
// New turn started after completion — reactivate
|
||||
this._completedSessions.delete(sessionKey);
|
||||
} else {
|
||||
// No new turn yet — suppress re-detection
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this._onSessionAdded(entry);
|
||||
}
|
||||
}
|
||||
@@ -359,6 +400,7 @@ class SessionMonitor extends EventEmitter {
|
||||
if (!currentSessions.has(sessionKey)) {
|
||||
this._onSessionRemoved(sessionKey);
|
||||
this._staleSessions.delete(sessionKey);
|
||||
this._completedSessions.delete(sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user