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);