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:
@@ -1,4 +1,5 @@
|
|||||||
import type { AutoCompactState } from "./types"
|
import type { AutoCompactState, RetryState } from "./types"
|
||||||
|
import { RETRY_CONFIG } from "./types"
|
||||||
|
|
||||||
type Client = {
|
type Client = {
|
||||||
session: {
|
session: {
|
||||||
@@ -11,9 +12,34 @@ type Client = {
|
|||||||
}
|
}
|
||||||
tui: {
|
tui: {
|
||||||
submitPrompt: (opts: { query: { directory: string } }) => Promise<unknown>
|
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(
|
export async function getLastAssistant(
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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(
|
export async function executeCompact(
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
msg: Record<string, unknown>,
|
msg: Record<string, unknown>,
|
||||||
@@ -50,6 +82,27 @@ export async function executeCompact(
|
|||||||
client: any,
|
client: any,
|
||||||
directory: string
|
directory: string
|
||||||
): Promise<void> {
|
): 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 {
|
try {
|
||||||
const providerID = msg.providerID as string | undefined
|
const providerID = msg.providerID as string | undefined
|
||||||
const modelID = msg.modelID as string | undefined
|
const modelID = msg.modelID as string | undefined
|
||||||
@@ -61,14 +114,30 @@ export async function executeCompact(
|
|||||||
query: { directory },
|
query: { directory },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
clearSessionState(autoCompactState, sessionID)
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await (client as Client).tui.submitPrompt({ query: { directory } })
|
await (client as Client).tui.submitPrompt({ query: { directory } })
|
||||||
} catch {}
|
} catch {}
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
const delay = calculateRetryDelay(retryState.attempt)
|
||||||
|
|
||||||
autoCompactState.pendingCompact.delete(sessionID)
|
await (client as Client).tui
|
||||||
autoCompactState.errorDataBySession.delete(sessionID)
|
.showToast({
|
||||||
} catch {}
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ function createAutoCompactState(): AutoCompactState {
|
|||||||
return {
|
return {
|
||||||
pendingCompact: new Set<string>(),
|
pendingCompact: new Set<string>(),
|
||||||
errorDataBySession: new Map<string, ParsedTokenLimitError>(),
|
errorDataBySession: new Map<string, ParsedTokenLimitError>(),
|
||||||
|
retryStateBySession: new Map(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export function createAnthropicAutoCompactHook(ctx: PluginInput) {
|
|||||||
if (sessionInfo?.id) {
|
if (sessionInfo?.id) {
|
||||||
autoCompactState.pendingCompact.delete(sessionInfo.id)
|
autoCompactState.pendingCompact.delete(sessionInfo.id)
|
||||||
autoCompactState.errorDataBySession.delete(sessionInfo.id)
|
autoCompactState.errorDataBySession.delete(sessionInfo.id)
|
||||||
|
autoCompactState.retryStateBySession.delete(sessionInfo.id)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,20 @@ export interface ParsedTokenLimitError {
|
|||||||
modelID?: string
|
modelID?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RetryState {
|
||||||
|
attempt: number
|
||||||
|
lastAttemptTime: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface AutoCompactState {
|
export interface AutoCompactState {
|
||||||
pendingCompact: Set<string>
|
pendingCompact: Set<string>
|
||||||
errorDataBySession: Map<string, ParsedTokenLimitError>
|
errorDataBySession: Map<string, ParsedTokenLimitError>
|
||||||
|
retryStateBySession: Map<string, RetryState>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const RETRY_CONFIG = {
|
||||||
|
maxAttempts: 5,
|
||||||
|
initialDelayMs: 2000,
|
||||||
|
backoffFactor: 2,
|
||||||
|
maxDelayMs: 30000,
|
||||||
|
} as const
|
||||||
|
|||||||
Reference in New Issue
Block a user