diff --git a/src/hooks/anthropic-auto-compact/executor.ts b/src/hooks/anthropic-auto-compact/executor.ts index d0cbeab..c0ef482 100644 --- a/src/hooks/anthropic-auto-compact/executor.ts +++ b/src/hooks/anthropic-auto-compact/executor.ts @@ -2,7 +2,12 @@ import type { AutoCompactState, FallbackState, RetryState, TruncateState } from import type { ExperimentalConfig } from "../../config" import { FALLBACK_CONFIG, RETRY_CONFIG, TRUNCATE_CONFIG } from "./types" import { findLargestToolResult, truncateToolResult, truncateUntilTargetTokens } from "./storage" -import { findEmptyMessages, injectTextPart } from "../session-recovery/storage" +import { + findEmptyMessages, + findEmptyMessageByIndex, + injectTextPart, + replaceEmptyTextParts, +} from "../session-recovery/storage" import { log } from "../../shared/logger" type Client = { @@ -168,30 +173,61 @@ function getOrCreateEmptyContentAttempt( async function fixEmptyMessages( sessionID: string, autoCompactState: AutoCompactState, - client: Client + client: Client, + messageIndex?: number ): Promise { const attempt = getOrCreateEmptyContentAttempt(autoCompactState, sessionID) autoCompactState.emptyContentAttemptBySession.set(sessionID, attempt + 1) - const emptyMessageIds = findEmptyMessages(sessionID) - if (emptyMessageIds.length === 0) { - await client.tui - .showToast({ - body: { - title: "Empty Content Error", - message: "No empty messages found in storage. Cannot auto-recover.", - variant: "error", - duration: 5000, - }, - }) - .catch(() => {}) - return false + let fixed = false + const fixedMessageIds: string[] = [] + + if (messageIndex !== undefined) { + const targetMessageId = findEmptyMessageByIndex(sessionID, messageIndex) + if (targetMessageId) { + const replaced = replaceEmptyTextParts(targetMessageId, "[user interrupted]") + if (replaced) { + fixed = true + fixedMessageIds.push(targetMessageId) + } else { + const injected = injectTextPart(sessionID, targetMessageId, "[user interrupted]") + if (injected) { + fixed = true + fixedMessageIds.push(targetMessageId) + } + } + } } - let fixed = false - for (const messageID of emptyMessageIds) { - const success = injectTextPart(sessionID, messageID, "[user interrupted]") - if (success) fixed = true + if (!fixed) { + const emptyMessageIds = findEmptyMessages(sessionID) + if (emptyMessageIds.length === 0) { + await client.tui + .showToast({ + body: { + title: "Empty Content Error", + message: "No empty messages found in storage. Cannot auto-recover.", + variant: "error", + duration: 5000, + }, + }) + .catch(() => {}) + return false + } + + for (const messageID of emptyMessageIds) { + const replaced = replaceEmptyTextParts(messageID, "[user interrupted]") + if (replaced) { + fixed = true + fixedMessageIds.push(messageID) + } else { + const injected = injectTextPart(sessionID, messageID, "[user interrupted]") + if (injected) { + fixed = true + fixedMessageIds.push(messageID) + } + } + } } if (fixed) { @@ -199,7 +235,7 @@ async function fixEmptyMessages( .showToast({ body: { title: "Session Recovery", - message: `Fixed ${emptyMessageIds.length} empty messages. Retrying...`, + message: `Fixed ${fixedMessageIds.length} empty message(s). Retrying...`, variant: "warning", duration: 3000, }, @@ -361,10 +397,15 @@ export async function executeCompact( const retryState = getOrCreateRetryState(autoCompactState, sessionID) - if (experimental?.empty_message_recovery && errorData?.errorType?.includes("non-empty content")) { + if (errorData?.errorType?.includes("non-empty content")) { const attempt = getOrCreateEmptyContentAttempt(autoCompactState, sessionID) if (attempt < 3) { - const fixed = await fixEmptyMessages(sessionID, autoCompactState, client as Client) + const fixed = await fixEmptyMessages( + sessionID, + autoCompactState, + client as Client, + errorData.messageIndex + ) if (fixed) { autoCompactState.compactionInProgress.delete(sessionID) setTimeout(() => { diff --git a/src/hooks/anthropic-auto-compact/parser.ts b/src/hooks/anthropic-auto-compact/parser.ts index 6666480..8d1170f 100644 --- a/src/hooks/anthropic-auto-compact/parser.ts +++ b/src/hooks/anthropic-auto-compact/parser.ts @@ -28,6 +28,8 @@ const TOKEN_LIMIT_KEYWORDS = [ "non-empty content", ] +const MESSAGE_INDEX_PATTERN = /messages\.(\d+)/ + function extractTokensFromMessage(message: string): { current: number; max: number } | null { for (const pattern of TOKEN_LIMIT_PATTERNS) { const match = message.match(pattern) @@ -40,6 +42,14 @@ function extractTokensFromMessage(message: string): { current: number; max: numb return null } +function extractMessageIndex(text: string): number | undefined { + const match = text.match(MESSAGE_INDEX_PATTERN) + if (match) { + return parseInt(match[1], 10) + } + return undefined +} + function isTokenLimitError(text: string): boolean { const lower = text.toLowerCase() return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase())) @@ -52,6 +62,7 @@ export function parseAnthropicTokenLimitError(err: unknown): ParsedTokenLimitErr currentTokens: 0, maxTokens: 0, errorType: "non-empty content", + messageIndex: extractMessageIndex(err), } } if (isTokenLimitError(err)) { @@ -155,6 +166,7 @@ export function parseAnthropicTokenLimitError(err: unknown): ParsedTokenLimitErr currentTokens: 0, maxTokens: 0, errorType: "non-empty content", + messageIndex: extractMessageIndex(combinedText), } } diff --git a/src/hooks/anthropic-auto-compact/types.ts b/src/hooks/anthropic-auto-compact/types.ts index 6fdb9d1..c97af58 100644 --- a/src/hooks/anthropic-auto-compact/types.ts +++ b/src/hooks/anthropic-auto-compact/types.ts @@ -5,6 +5,7 @@ export interface ParsedTokenLimitError { errorType: string providerID?: string modelID?: string + messageIndex?: number } export interface RetryState {