fix: stale offset reset + incremental _knownSessions tracking
src/status-watcher.js: - When lastOffset > fileSize (stale offset from compaction or previous session), reset offset to current file end rather than 0. Resetting to 0 caused re-parsing gigabytes of old content; resetting to fileSize means we only read new bytes from this point forward. This was the root cause of the status box receiving no updates — the offset was past EOF so every read returned 0 bytes silently. src/session-monitor.js: - _knownSessions is now maintained incrementally instead of being replaced wholesale at the end of every poll cycle. - Previously: _knownSessions = currentSessions at end of _poll() meant forgetSession() had no effect — the next poll immediately re-added the key as 'known' without firing _onSessionAdded, silently swallowing the reactivation. - Now: entries are added/updated individually, removals delete from the map directly. forgetSession() + clearCompleted() + pollNow() now correctly triggers reactivation. Verified: 3 consecutive 5s tests all show plugin KV updating with lines and timestamps.
This commit is contained in:
@@ -379,27 +379,31 @@ 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.
|
||||
const isKnown = this._knownSessions.has(sessionKey);
|
||||
const isStale = this._staleSessions.has(sessionKey);
|
||||
|
||||
if (!isKnown || isStale) {
|
||||
// Skip sessions in completed cooldown — only allow re-detection when:
|
||||
// (a) completedSessions cooldown was explicitly cleared (ghost watch / clearCompleted), OR
|
||||
// (b) sessions.json shows a newer updatedAt than when we marked complete.
|
||||
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
|
||||
// No new turn yet — add to knownSessions silently so we don't re-emit
|
||||
// on every poll cycle, but don't fire _onSessionAdded.
|
||||
if (!isKnown) this._knownSessions.set(sessionKey, entry);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this._knownSessions.set(sessionKey, entry);
|
||||
this._onSessionAdded(entry);
|
||||
} else {
|
||||
// Update the stored entry with latest data (e.g. updatedAt changes)
|
||||
this._knownSessions.set(sessionKey, entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +411,7 @@ class SessionMonitor extends EventEmitter {
|
||||
for (const [sessionKey] of this._knownSessions) {
|
||||
if (!currentSessions.has(sessionKey)) {
|
||||
this._onSessionRemoved(sessionKey);
|
||||
this._knownSessions.delete(sessionKey);
|
||||
this._staleSessions.delete(sessionKey);
|
||||
this._completedSessions.delete(sessionKey);
|
||||
}
|
||||
@@ -419,7 +424,7 @@ class SessionMonitor extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
this._knownSessions = currentSessions;
|
||||
// Note: _knownSessions is now maintained incrementally above, not replaced wholesale.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -258,16 +258,18 @@ class StatusWatcher extends EventEmitter {
|
||||
const stat = fs.fstatSync(fd);
|
||||
const fileSize = stat.size;
|
||||
|
||||
// Detect file truncation (compaction)
|
||||
// Detect file truncation (compaction) or stale offset from a previous session.
|
||||
// In both cases reset to current file end — we don't want to re-parse old content,
|
||||
// only new bytes written from this point forward.
|
||||
if (fileSize < state.lastOffset) {
|
||||
if (this.logger) {
|
||||
this.logger.warn(
|
||||
{ sessionKey, fileSize, lastOffset: state.lastOffset },
|
||||
'Transcript truncated (compaction detected) — resetting offset',
|
||||
'Transcript offset past file end (compaction/stale) — resetting to file end',
|
||||
);
|
||||
}
|
||||
state.lastOffset = 0;
|
||||
state.lines = ['[session compacted - continuing]'];
|
||||
state.lastOffset = fileSize;
|
||||
state.lines = [];
|
||||
state.pendingToolCalls = 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user