fix(compaction): cap recovery attempts to prevent infinite compaction loop (#3127)

Add MAX_RECOVERY_ATTEMPTS (3) guard to post-compaction degradation recovery.
On models like Kimi/Minimax that repeatedly generate no-text responses,
the recovery loop would trigger indefinitely as each compaction reset the
recovery counter via onSessionCompacted.

The recovery count now persists across compaction cycles per session and
is only reset on session deletion (clear()), not on every compaction.
This commit is contained in:
YeonGyu-Kim
2026-04-06 17:01:10 +09:00
parent f69234ab7e
commit 5ad011de32
@@ -74,12 +74,16 @@ export function createPostCompactionDegradationMonitor(args: {
const postCompactionNoTextStreak = new Map<string, number>() const postCompactionNoTextStreak = new Map<string, number>()
const postCompactionRecoveryTriggered = new Set<string>() const postCompactionRecoveryTriggered = new Set<string>()
const postCompactionEpoch = new Map<string, number>() const postCompactionEpoch = new Map<string, number>()
const postCompactionRecoveryCount = new Map<string, number>()
const MAX_RECOVERY_ATTEMPTS = 3
const clear = (sessionID: string): void => { const clear = (sessionID: string): void => {
postCompactionRemaining.delete(sessionID) postCompactionRemaining.delete(sessionID)
postCompactionNoTextStreak.delete(sessionID) postCompactionNoTextStreak.delete(sessionID)
postCompactionRecoveryTriggered.delete(sessionID) postCompactionRecoveryTriggered.delete(sessionID)
postCompactionEpoch.delete(sessionID) postCompactionEpoch.delete(sessionID)
postCompactionRecoveryCount.delete(sessionID)
} }
const onSessionCompacted = (sessionID: string): void => { const onSessionCompacted = (sessionID: string): void => {
@@ -93,6 +97,16 @@ export function createPostCompactionDegradationMonitor(args: {
const triggerRecovery = async (sessionID: string): Promise<void> => { const triggerRecovery = async (sessionID: string): Promise<void> => {
if (postCompactionRecoveryTriggered.has(sessionID) || compactionInProgress.has(sessionID)) return if (postCompactionRecoveryTriggered.has(sessionID) || compactionInProgress.has(sessionID)) return
const recoveryCount = postCompactionRecoveryCount.get(sessionID) ?? 0
if (recoveryCount >= MAX_RECOVERY_ATTEMPTS) {
log("[preemptive-compaction] Max recovery attempts reached, giving up", {
sessionID,
recoveryCount,
})
return
}
postCompactionRecoveryCount.set(sessionID, recoveryCount + 1)
const cached = tokenCache.get(sessionID) const cached = tokenCache.get(sessionID)
if (!cached?.modelID) { if (!cached?.modelID) {
log("[preemptive-compaction] No-text tail detected but compaction model is unavailable", { sessionID }) log("[preemptive-compaction] No-text tail detected but compaction model is unavailable", { sessionID })