fix: reduce session.messages() calls with event-based caching to prevent memory leaks
- Replace session.messages() fetch in context-window-monitor with message.updated event cache - Replace session.messages() fetch in preemptive-compaction with message.updated event cache - Add per-session transcript cache (5min TTL) to avoid full rebuild per tool call - Remove session.messages() from background-agent polling (use event-based progress) - Add TTL pruning to todo-continuation-enforcer session state Map - Add setInterval.unref() to tool-input-cache cleanup timer Fixes #1222
This commit is contained in:
@@ -1,33 +1,69 @@
|
||||
import type { SessionState } from "./types"
|
||||
|
||||
// TTL for idle session state entries (10 minutes)
|
||||
const SESSION_STATE_TTL_MS = 10 * 60 * 1000
|
||||
// Prune interval (every 2 minutes)
|
||||
const SESSION_STATE_PRUNE_INTERVAL_MS = 2 * 60 * 1000
|
||||
|
||||
interface TrackedSessionState {
|
||||
state: SessionState
|
||||
lastAccessedAt: number
|
||||
}
|
||||
|
||||
export interface SessionStateStore {
|
||||
getState: (sessionID: string) => SessionState
|
||||
getExistingState: (sessionID: string) => SessionState | undefined
|
||||
cancelCountdown: (sessionID: string) => void
|
||||
cleanup: (sessionID: string) => void
|
||||
cancelAllCountdowns: () => void
|
||||
shutdown: () => void
|
||||
}
|
||||
|
||||
export function createSessionStateStore(): SessionStateStore {
|
||||
const sessions = new Map<string, SessionState>()
|
||||
const sessions = new Map<string, TrackedSessionState>()
|
||||
|
||||
// Periodic pruning of stale session states to prevent unbounded Map growth
|
||||
let pruneInterval: ReturnType<typeof setInterval> | undefined
|
||||
pruneInterval = setInterval(() => {
|
||||
const now = Date.now()
|
||||
for (const [sessionID, tracked] of sessions.entries()) {
|
||||
if (now - tracked.lastAccessedAt > SESSION_STATE_TTL_MS) {
|
||||
cancelCountdown(sessionID)
|
||||
sessions.delete(sessionID)
|
||||
}
|
||||
}
|
||||
}, SESSION_STATE_PRUNE_INTERVAL_MS)
|
||||
// Allow process to exit naturally even if interval is running
|
||||
if (typeof pruneInterval === "object" && "unref" in pruneInterval) {
|
||||
pruneInterval.unref()
|
||||
}
|
||||
|
||||
function getState(sessionID: string): SessionState {
|
||||
const existingState = sessions.get(sessionID)
|
||||
if (existingState) return existingState
|
||||
const existing = sessions.get(sessionID)
|
||||
if (existing) {
|
||||
existing.lastAccessedAt = Date.now()
|
||||
return existing.state
|
||||
}
|
||||
|
||||
const state: SessionState = {}
|
||||
sessions.set(sessionID, state)
|
||||
sessions.set(sessionID, { state, lastAccessedAt: Date.now() })
|
||||
return state
|
||||
}
|
||||
|
||||
function getExistingState(sessionID: string): SessionState | undefined {
|
||||
return sessions.get(sessionID)
|
||||
const existing = sessions.get(sessionID)
|
||||
if (existing) {
|
||||
existing.lastAccessedAt = Date.now()
|
||||
return existing.state
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function cancelCountdown(sessionID: string): void {
|
||||
const state = sessions.get(sessionID)
|
||||
if (!state) return
|
||||
const tracked = sessions.get(sessionID)
|
||||
if (!tracked) return
|
||||
|
||||
const state = tracked.state
|
||||
if (state.countdownTimer) {
|
||||
clearTimeout(state.countdownTimer)
|
||||
state.countdownTimer = undefined
|
||||
@@ -52,11 +88,18 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
}
|
||||
}
|
||||
|
||||
function shutdown(): void {
|
||||
clearInterval(pruneInterval)
|
||||
cancelAllCountdowns()
|
||||
sessions.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
getState,
|
||||
getExistingState,
|
||||
cancelCountdown,
|
||||
cleanup,
|
||||
cancelAllCountdowns,
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user