feat(anthropic-auto-compact): add retry mechanism with exponential backoff

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
This commit is contained in:
YeonGyu-Kim
2025-12-12 14:28:40 +09:00
parent f6bdc45fe7
commit f007437991
3 changed files with 88 additions and 4 deletions

View File

@@ -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<unknown>
showToast: (opts: {
body: { title: string; message: string; variant: string; duration: number }
}) => Promise<unknown>
}
}
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<string, unknown>,
@@ -50,6 +82,27 @@ export async function executeCompact(
client: any,
directory: string
): Promise<void> {
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)
}
}

View File

@@ -7,6 +7,7 @@ function createAutoCompactState(): AutoCompactState {
return {
pendingCompact: new Set<string>(),
errorDataBySession: new Map<string, ParsedTokenLimitError>(),
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
}

View File

@@ -7,7 +7,20 @@ export interface ParsedTokenLimitError {
modelID?: string
}
export interface RetryState {
attempt: number
lastAttemptTime: number
}
export interface AutoCompactState {
pendingCompact: Set<string>
errorDataBySession: Map<string, ParsedTokenLimitError>
retryStateBySession: Map<string, RetryState>
}
export const RETRY_CONFIG = {
maxAttempts: 5,
initialDelayMs: 2000,
backoffFactor: 2,
maxDelayMs: 30000,
} as const