From 5ad011de3253c895f3b50beb9b743edabd53d1ff Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 6 Apr 2026 17:01:10 +0900 Subject: [PATCH] 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. --- .../preemptive-compaction-degradation-monitor.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/hooks/preemptive-compaction-degradation-monitor.ts b/src/hooks/preemptive-compaction-degradation-monitor.ts index 4eb3dfc37..64a29eb03 100644 --- a/src/hooks/preemptive-compaction-degradation-monitor.ts +++ b/src/hooks/preemptive-compaction-degradation-monitor.ts @@ -74,12 +74,16 @@ export function createPostCompactionDegradationMonitor(args: { const postCompactionNoTextStreak = new Map() const postCompactionRecoveryTriggered = new Set() const postCompactionEpoch = new Map() + const postCompactionRecoveryCount = new Map() + + const MAX_RECOVERY_ATTEMPTS = 3 const clear = (sessionID: string): void => { postCompactionRemaining.delete(sessionID) postCompactionNoTextStreak.delete(sessionID) postCompactionRecoveryTriggered.delete(sessionID) postCompactionEpoch.delete(sessionID) + postCompactionRecoveryCount.delete(sessionID) } const onSessionCompacted = (sessionID: string): void => { @@ -93,6 +97,16 @@ export function createPostCompactionDegradationMonitor(args: { const triggerRecovery = async (sessionID: string): Promise => { 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) if (!cached?.modelID) { log("[preemptive-compaction] No-text tail detected but compaction model is unavailable", { sessionID })