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:
@@ -8,29 +8,29 @@ const ANTHROPIC_ACTUAL_LIMIT =
|
||||
|
||||
const PREEMPTIVE_COMPACTION_THRESHOLD = 0.78
|
||||
|
||||
interface AssistantMessageInfo {
|
||||
role: "assistant"
|
||||
providerID: string
|
||||
modelID?: string
|
||||
tokens: {
|
||||
input: number
|
||||
output: number
|
||||
reasoning: number
|
||||
cache: { read: number; write: number }
|
||||
}
|
||||
interface TokenInfo {
|
||||
input: number
|
||||
output: number
|
||||
reasoning: number
|
||||
cache: { read: number; write: number }
|
||||
}
|
||||
|
||||
interface MessageWrapper {
|
||||
info: { role: string } & Partial<AssistantMessageInfo>
|
||||
interface CachedCompactionState {
|
||||
providerID: string
|
||||
modelID: string
|
||||
tokens: TokenInfo
|
||||
}
|
||||
|
||||
type PluginInput = {
|
||||
client: {
|
||||
session: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
messages: (...args: any[]) => any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
summarize: (...args: any[]) => any
|
||||
}
|
||||
tui: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
showToast: (...args: any[]) => any
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ type PluginInput = {
|
||||
export function createPreemptiveCompactionHook(ctx: PluginInput) {
|
||||
const compactionInProgress = new Set<string>()
|
||||
const compactedSessions = new Set<string>()
|
||||
const tokenCache = new Map<string, CachedCompactionState>()
|
||||
|
||||
const toolExecuteAfter = async (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
@@ -48,38 +49,29 @@ export function createPreemptiveCompactionHook(ctx: PluginInput) {
|
||||
const { sessionID } = input
|
||||
if (compactedSessions.has(sessionID) || compactionInProgress.has(sessionID)) return
|
||||
|
||||
const cached = tokenCache.get(sessionID)
|
||||
if (!cached) return
|
||||
|
||||
const actualLimit =
|
||||
cached.providerID === "anthropic"
|
||||
? ANTHROPIC_ACTUAL_LIMIT
|
||||
: DEFAULT_ACTUAL_LIMIT
|
||||
|
||||
const lastTokens = cached.tokens
|
||||
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0)
|
||||
const usageRatio = totalInputTokens / actualLimit
|
||||
|
||||
if (usageRatio < PREEMPTIVE_COMPACTION_THRESHOLD) return
|
||||
|
||||
const modelID = cached.modelID
|
||||
if (!modelID) return
|
||||
|
||||
compactionInProgress.add(sessionID)
|
||||
|
||||
try {
|
||||
const response = await ctx.client.session.messages({
|
||||
path: { id: sessionID },
|
||||
})
|
||||
const payload = response as { data?: MessageWrapper[] } | MessageWrapper[]
|
||||
const messages = Array.isArray(payload) ? payload : (payload.data ?? [])
|
||||
const assistantMessages = messages
|
||||
.filter((m) => m.info.role === "assistant")
|
||||
.map((m) => m.info as AssistantMessageInfo)
|
||||
|
||||
if (assistantMessages.length === 0) return
|
||||
|
||||
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
||||
const actualLimit =
|
||||
lastAssistant.providerID === "anthropic"
|
||||
? ANTHROPIC_ACTUAL_LIMIT
|
||||
: DEFAULT_ACTUAL_LIMIT
|
||||
|
||||
const lastTokens = lastAssistant.tokens
|
||||
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0)
|
||||
const usageRatio = totalInputTokens / actualLimit
|
||||
|
||||
if (usageRatio < PREEMPTIVE_COMPACTION_THRESHOLD) return
|
||||
|
||||
const modelID = lastAssistant.modelID
|
||||
if (!modelID) return
|
||||
|
||||
compactionInProgress.add(sessionID)
|
||||
|
||||
await ctx.client.session.summarize({
|
||||
path: { id: sessionID },
|
||||
body: { providerID: lastAssistant.providerID, modelID, auto: true } as never,
|
||||
body: { providerID: cached.providerID, modelID, auto: true } as never,
|
||||
query: { directory: ctx.directory },
|
||||
})
|
||||
|
||||
@@ -92,12 +84,36 @@ export function createPreemptiveCompactionHook(ctx: PluginInput) {
|
||||
}
|
||||
|
||||
const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
||||
if (event.type !== "session.deleted") return
|
||||
const props = event.properties as Record<string, unknown> | undefined
|
||||
const sessionInfo = props?.info as { id?: string } | undefined
|
||||
if (sessionInfo?.id) {
|
||||
compactionInProgress.delete(sessionInfo.id)
|
||||
compactedSessions.delete(sessionInfo.id)
|
||||
|
||||
if (event.type === "session.deleted") {
|
||||
const sessionInfo = props?.info as { id?: string } | undefined
|
||||
if (sessionInfo?.id) {
|
||||
compactionInProgress.delete(sessionInfo.id)
|
||||
compactedSessions.delete(sessionInfo.id)
|
||||
tokenCache.delete(sessionInfo.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "message.updated") {
|
||||
const info = props?.info as {
|
||||
role?: string
|
||||
sessionID?: string
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
finish?: boolean
|
||||
tokens?: TokenInfo
|
||||
} | undefined
|
||||
|
||||
if (!info || info.role !== "assistant" || !info.finish) return
|
||||
if (!info.sessionID || !info.providerID || !info.tokens) return
|
||||
|
||||
tokenCache.set(info.sessionID, {
|
||||
providerID: info.providerID,
|
||||
modelID: info.modelID ?? "",
|
||||
tokens: info.tokens,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user