From a5983f1678df3e68d9f375e5ca1a352f0a3fff5f Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 1 Jan 2026 16:41:11 +0900 Subject: [PATCH] fix(anthropic-context-window-limit-recovery): add revert fallback when truncation insufficient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When over token limit after truncation, use session.revert to remove last message instead of attempting summarize (which would also fail). Skip summarize entirely when still over limit to prevent infinite loop. 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- .../executor.ts | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/src/hooks/anthropic-context-window-limit-recovery/executor.ts b/src/hooks/anthropic-context-window-limit-recovery/executor.ts index d357463..b963aec 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/executor.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/executor.ts @@ -158,6 +158,28 @@ export async function getLastAssistant( } } +async function getLastMessageId( + sessionID: string, + client: Client, + directory: string, +): Promise { + try { + const resp = await client.session.messages({ + path: { id: sessionID }, + query: { directory }, + }); + + const data = (resp as { data?: unknown[] }).data; + if (!Array.isArray(data) || data.length === 0) return null; + + const lastMsg = data[data.length - 1] as Record; + const info = lastMsg.info as Record | undefined; + return (info?.id as string) ?? null; + } catch { + return null; + } +} + function clearSessionState( autoCompactState: AutoCompactState, sessionID: string, @@ -399,7 +421,6 @@ export async function executeCompact( log("[auto-compact] aggressive truncation completed", aggressiveResult); - // If truncation was sufficient, try to continue without summarize if (aggressiveResult.sufficient) { clearSessionState(autoCompactState, sessionID); setTimeout(async () => { @@ -413,13 +434,81 @@ export async function executeCompact( }, 500); return; } - // If not sufficient, fall through to PHASE 3 (summarize) } else { log("[auto-compact] no tool outputs found to truncate", { sessionID }); } + + // PHASE 2.5: Revert fallback - if still over limit, remove last message + log("[auto-compact] PHASE 2.5: revert fallback - still over limit after truncation", { + sessionID, + currentTokens: errorData.currentTokens, + maxTokens: errorData.maxTokens, + }); + + const lastMessageId = await getLastMessageId( + sessionID, + client as Client, + directory, + ); + + if (lastMessageId) { + try { + await (client as Client).session.revert({ + path: { id: sessionID }, + body: { messageID: lastMessageId }, + query: { directory }, + }); + + await (client as Client).tui + .showToast({ + body: { + title: "Message Reverted", + message: "Removed last message to reduce context. Retrying...", + variant: "warning", + duration: 3000, + }, + }) + .catch(() => {}); + + clearSessionState(autoCompactState, sessionID); + setTimeout(async () => { + try { + await (client as Client).session.prompt_async({ + path: { sessionID }, + body: { parts: [{ type: "text", text: "Continue" }] }, + query: { directory }, + }); + } catch {} + }, 500); + return; + } catch (revertError) { + log("[auto-compact] revert failed", { error: String(revertError) }); + } + } } - // PHASE 3: Summarize - last resort after DCP and truncation + // PHASE 3: Summarize - only when under limit (otherwise it will also fail) + if (isOverLimit) { + log("[auto-compact] skipping summarize - still over token limit", { + sessionID, + currentTokens: errorData?.currentTokens, + maxTokens: errorData?.maxTokens, + }); + + clearSessionState(autoCompactState, sessionID); + + await (client as Client).tui + .showToast({ + body: { + title: "Recovery Failed", + message: `Still over token limit (${errorData?.currentTokens}/${errorData?.maxTokens}). Please start a new session or manually compact.`, + variant: "error", + duration: 10000, + }, + }) + .catch(() => {}); + return; + } const retryState = getOrCreateRetryState(autoCompactState, sessionID);