From f0074379919454cbb9af6c3913d5ddf5832ab331 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 12 Dec 2025 14:28:40 +0900 Subject: [PATCH] feat(anthropic-auto-compact): add retry mechanism with exponential backoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements retry logic with up to 5 attempts when compaction fails. Uses exponential backoff strategy (2s → 4s → 8s → 16s → 30s). Shows toast notifications for retry status and final failure. Prevents infinite loops by clearing state after max attempts. 🤖 GENERATED WITH ASSISTANCE OF OhMyOpenCode --- src/hooks/anthropic-auto-compact/executor.ts | 77 +++++++++++++++++++- src/hooks/anthropic-auto-compact/index.ts | 2 + src/hooks/anthropic-auto-compact/types.ts | 13 ++++ 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/hooks/anthropic-auto-compact/executor.ts b/src/hooks/anthropic-auto-compact/executor.ts index eb97d97..0105427 100644 --- a/src/hooks/anthropic-auto-compact/executor.ts +++ b/src/hooks/anthropic-auto-compact/executor.ts @@ -1,4 +1,5 @@ -import type { AutoCompactState } from "./types" +import type { AutoCompactState, RetryState } from "./types" +import { RETRY_CONFIG } from "./types" type Client = { session: { @@ -11,9 +12,34 @@ type Client = { } tui: { submitPrompt: (opts: { query: { directory: string } }) => Promise + showToast: (opts: { + body: { title: string; message: string; variant: string; duration: number } + }) => Promise } } +function calculateRetryDelay(attempt: number): number { + const delay = RETRY_CONFIG.initialDelayMs * Math.pow(RETRY_CONFIG.backoffFactor, attempt - 1) + return Math.min(delay, RETRY_CONFIG.maxDelayMs) +} + +function shouldRetry(retryState: RetryState | undefined): boolean { + if (!retryState) return true + return retryState.attempt < RETRY_CONFIG.maxAttempts +} + +function getOrCreateRetryState( + autoCompactState: AutoCompactState, + sessionID: string +): RetryState { + let state = autoCompactState.retryStateBySession.get(sessionID) + if (!state) { + state = { attempt: 0, lastAttemptTime: 0 } + autoCompactState.retryStateBySession.set(sessionID, state) + } + return state +} + export async function getLastAssistant( sessionID: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -42,6 +68,12 @@ export async function getLastAssistant( } } +function clearSessionState(autoCompactState: AutoCompactState, sessionID: string): void { + autoCompactState.pendingCompact.delete(sessionID) + autoCompactState.errorDataBySession.delete(sessionID) + autoCompactState.retryStateBySession.delete(sessionID) +} + export async function executeCompact( sessionID: string, msg: Record, @@ -50,6 +82,27 @@ export async function executeCompact( client: any, directory: string ): Promise { + const retryState = getOrCreateRetryState(autoCompactState, sessionID) + + if (!shouldRetry(retryState)) { + clearSessionState(autoCompactState, sessionID) + + await (client as Client).tui + .showToast({ + body: { + title: "Auto Compact Failed", + message: `Failed after ${RETRY_CONFIG.maxAttempts} attempts. Please try manual compact.`, + variant: "error", + duration: 5000, + }, + }) + .catch(() => {}) + return + } + + retryState.attempt++ + retryState.lastAttemptTime = Date.now() + try { const providerID = msg.providerID as string | undefined const modelID = msg.modelID as string | undefined @@ -61,14 +114,30 @@ export async function executeCompact( query: { directory }, }) + clearSessionState(autoCompactState, sessionID) + setTimeout(async () => { try { await (client as Client).tui.submitPrompt({ query: { directory } }) } catch {} }, 500) } + } catch { + const delay = calculateRetryDelay(retryState.attempt) - autoCompactState.pendingCompact.delete(sessionID) - autoCompactState.errorDataBySession.delete(sessionID) - } catch {} + await (client as Client).tui + .showToast({ + body: { + title: "Auto Compact Retry", + message: `Attempt ${retryState.attempt}/${RETRY_CONFIG.maxAttempts} failed. Retrying in ${Math.round(delay / 1000)}s...`, + variant: "warning", + duration: delay, + }, + }) + .catch(() => {}) + + setTimeout(() => { + executeCompact(sessionID, msg, autoCompactState, client, directory) + }, delay) + } } diff --git a/src/hooks/anthropic-auto-compact/index.ts b/src/hooks/anthropic-auto-compact/index.ts index 71f98d5..43c964e 100644 --- a/src/hooks/anthropic-auto-compact/index.ts +++ b/src/hooks/anthropic-auto-compact/index.ts @@ -7,6 +7,7 @@ function createAutoCompactState(): AutoCompactState { return { pendingCompact: new Set(), errorDataBySession: new Map(), + retryStateBySession: new Map(), } } @@ -21,6 +22,7 @@ export function createAnthropicAutoCompactHook(ctx: PluginInput) { if (sessionInfo?.id) { autoCompactState.pendingCompact.delete(sessionInfo.id) autoCompactState.errorDataBySession.delete(sessionInfo.id) + autoCompactState.retryStateBySession.delete(sessionInfo.id) } return } diff --git a/src/hooks/anthropic-auto-compact/types.ts b/src/hooks/anthropic-auto-compact/types.ts index a15f052..b8b55c7 100644 --- a/src/hooks/anthropic-auto-compact/types.ts +++ b/src/hooks/anthropic-auto-compact/types.ts @@ -7,7 +7,20 @@ export interface ParsedTokenLimitError { modelID?: string } +export interface RetryState { + attempt: number + lastAttemptTime: number +} + export interface AutoCompactState { pendingCompact: Set errorDataBySession: Map + retryStateBySession: Map } + +export const RETRY_CONFIG = { + maxAttempts: 5, + initialDelayMs: 2000, + backoffFactor: 2, + maxDelayMs: 30000, +} as const