fix: orphan session cleanup + instant reactivation via ghost watch

plugin/server/store.go:
- CleanStaleSessions now handles last_update_ms=0 (pre-cleanup era orphans)
- Zero-timestamp sessions: mark active ones interrupted, delete non-active ones
- Previously these were silently skipped with 'continue', accumulating forever

src/status-watcher.js:
- removeSession() keeps fileToSession mapping as ghost entry ('\x00ghost:key')
- When ghost file changes, emits 'session-reactivate' immediately instead of
  waiting up to 2s for the session-monitor poll cycle
- Ghost removed after first trigger to avoid repeated events

src/session-monitor.js:
- Added pollNow() for immediate poll without waiting for interval tick
- Reactivation check now uses sessions.json updatedAt vs completedAt timestamp
  (pure infrastructure: two on-disk timestamps, no AI involvement)

src/watcher-manager.js:
- Wires session-reactivate event: clearCompleted() + pollNow() for instant re-detection
- New sessions now show up within ~100ms of first file change instead of 2s

Net result: status box appears reliably on every turn, clears 3s after reply,
zero orphan sessions accumulating in the KV store.
This commit is contained in:
sol
2026-03-09 18:31:41 +00:00
parent 3842adf562
commit f545cb00be

View File

@@ -146,6 +146,18 @@ func (s *Store) CleanStaleSessions(staleThresholdMs, expireThresholdMs int64) (c
lastUpdate = session.StartTimeMs lastUpdate = session.StartTimeMs
} }
if lastUpdate == 0 { if lastUpdate == 0 {
// No timestamps at all (pre-cleanup era orphan) — treat as expired immediately.
// Delete non-active sessions; mark active ones as interrupted so they get
// picked up on the next cleanup cycle with a real timestamp.
if session.Status == "active" {
session.Status = "interrupted"
session.LastUpdateMs = now
_ = s.SaveSession(session.SessionKey, session)
cleaned++
} else {
_ = s.DeleteSession(session.SessionKey)
expired++
}
continue continue
} }
age := now - lastUpdate age := now - lastUpdate