Skip to content

Commit 8406f3d

Browse files
committed
fix(anthropic-auto-compact): handle empty messages at arbitrary indices
- Add messageIndex field to ParsedTokenLimitError type for tracking message position - Extract message index from 'messages.N' format in error messages using regex - Update fixEmptyMessages to accept optional messageIndex parameter - Target specific empty message by index instead of fixing all empty messages - Apply replaceEmptyTextParts before injectTextPart for better coverage - Remove experimental flag requirement - non-empty content errors now auto-recover by default - Fixes issue where compaction could create empty messages at positions other than the last message 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
1 parent 4f24423 commit 8406f3d

File tree

3 files changed

+76
-22
lines changed

3 files changed

+76
-22
lines changed

src/hooks/anthropic-auto-compact/executor.ts

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import type { AutoCompactState, FallbackState, RetryState, TruncateState } from
22
import type { ExperimentalConfig } from "../../config"
33
import { FALLBACK_CONFIG, RETRY_CONFIG, TRUNCATE_CONFIG } from "./types"
44
import { findLargestToolResult, truncateToolResult, truncateUntilTargetTokens } from "./storage"
5-
import { findEmptyMessages, injectTextPart } from "../session-recovery/storage"
5+
import {
6+
findEmptyMessages,
7+
findEmptyMessageByIndex,
8+
injectTextPart,
9+
replaceEmptyTextParts,
10+
} from "../session-recovery/storage"
611
import { log } from "../../shared/logger"
712

813
type Client = {
@@ -168,38 +173,69 @@ function getOrCreateEmptyContentAttempt(
168173
async function fixEmptyMessages(
169174
sessionID: string,
170175
autoCompactState: AutoCompactState,
171-
client: Client
176+
client: Client,
177+
messageIndex?: number
172178
): Promise<boolean> {
173179
const attempt = getOrCreateEmptyContentAttempt(autoCompactState, sessionID)
174180
autoCompactState.emptyContentAttemptBySession.set(sessionID, attempt + 1)
175181

176-
const emptyMessageIds = findEmptyMessages(sessionID)
177-
if (emptyMessageIds.length === 0) {
178-
await client.tui
179-
.showToast({
180-
body: {
181-
title: "Empty Content Error",
182-
message: "No empty messages found in storage. Cannot auto-recover.",
183-
variant: "error",
184-
duration: 5000,
185-
},
186-
})
187-
.catch(() => {})
188-
return false
182+
let fixed = false
183+
const fixedMessageIds: string[] = []
184+
185+
if (messageIndex !== undefined) {
186+
const targetMessageId = findEmptyMessageByIndex(sessionID, messageIndex)
187+
if (targetMessageId) {
188+
const replaced = replaceEmptyTextParts(targetMessageId, "[user interrupted]")
189+
if (replaced) {
190+
fixed = true
191+
fixedMessageIds.push(targetMessageId)
192+
} else {
193+
const injected = injectTextPart(sessionID, targetMessageId, "[user interrupted]")
194+
if (injected) {
195+
fixed = true
196+
fixedMessageIds.push(targetMessageId)
197+
}
198+
}
199+
}
189200
}
190201

191-
let fixed = false
192-
for (const messageID of emptyMessageIds) {
193-
const success = injectTextPart(sessionID, messageID, "[user interrupted]")
194-
if (success) fixed = true
202+
if (!fixed) {
203+
const emptyMessageIds = findEmptyMessages(sessionID)
204+
if (emptyMessageIds.length === 0) {
205+
await client.tui
206+
.showToast({
207+
body: {
208+
title: "Empty Content Error",
209+
message: "No empty messages found in storage. Cannot auto-recover.",
210+
variant: "error",
211+
duration: 5000,
212+
},
213+
})
214+
.catch(() => {})
215+
return false
216+
}
217+
218+
for (const messageID of emptyMessageIds) {
219+
const replaced = replaceEmptyTextParts(messageID, "[user interrupted]")
220+
if (replaced) {
221+
fixed = true
222+
fixedMessageIds.push(messageID)
223+
} else {
224+
const injected = injectTextPart(sessionID, messageID, "[user interrupted]")
225+
if (injected) {
226+
fixed = true
227+
fixedMessageIds.push(messageID)
228+
}
229+
}
230+
}
195231
}
196232

197233
if (fixed) {
198234
await client.tui
199235
.showToast({
200236
body: {
201237
title: "Session Recovery",
202-
message: `Fixed ${emptyMessageIds.length} empty messages. Retrying...`,
238+
message: `Fixed ${fixedMessageIds.length} empty message(s). Retrying...`,
203239
variant: "warning",
204240
duration: 3000,
205241
},
@@ -361,10 +397,15 @@ export async function executeCompact(
361397

362398
const retryState = getOrCreateRetryState(autoCompactState, sessionID)
363399

364-
if (experimental?.empty_message_recovery && errorData?.errorType?.includes("non-empty content")) {
400+
if (errorData?.errorType?.includes("non-empty content")) {
365401
const attempt = getOrCreateEmptyContentAttempt(autoCompactState, sessionID)
366402
if (attempt < 3) {
367-
const fixed = await fixEmptyMessages(sessionID, autoCompactState, client as Client)
403+
const fixed = await fixEmptyMessages(
404+
sessionID,
405+
autoCompactState,
406+
client as Client,
407+
errorData.messageIndex
408+
)
368409
if (fixed) {
369410
autoCompactState.compactionInProgress.delete(sessionID)
370411
setTimeout(() => {

src/hooks/anthropic-auto-compact/parser.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const TOKEN_LIMIT_KEYWORDS = [
2828
"non-empty content",
2929
]
3030

31+
const MESSAGE_INDEX_PATTERN = /messages\.(\d+)/
32+
3133
function extractTokensFromMessage(message: string): { current: number; max: number } | null {
3234
for (const pattern of TOKEN_LIMIT_PATTERNS) {
3335
const match = message.match(pattern)
@@ -40,6 +42,14 @@ function extractTokensFromMessage(message: string): { current: number; max: numb
4042
return null
4143
}
4244

45+
function extractMessageIndex(text: string): number | undefined {
46+
const match = text.match(MESSAGE_INDEX_PATTERN)
47+
if (match) {
48+
return parseInt(match[1], 10)
49+
}
50+
return undefined
51+
}
52+
4353
function isTokenLimitError(text: string): boolean {
4454
const lower = text.toLowerCase()
4555
return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()))
@@ -52,6 +62,7 @@ export function parseAnthropicTokenLimitError(err: unknown): ParsedTokenLimitErr
5262
currentTokens: 0,
5363
maxTokens: 0,
5464
errorType: "non-empty content",
65+
messageIndex: extractMessageIndex(err),
5566
}
5667
}
5768
if (isTokenLimitError(err)) {
@@ -155,6 +166,7 @@ export function parseAnthropicTokenLimitError(err: unknown): ParsedTokenLimitErr
155166
currentTokens: 0,
156167
maxTokens: 0,
157168
errorType: "non-empty content",
169+
messageIndex: extractMessageIndex(combinedText),
158170
}
159171
}
160172

src/hooks/anthropic-auto-compact/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface ParsedTokenLimitError {
55
errorType: string
66
providerID?: string
77
modelID?: string
8+
messageIndex?: number
89
}
910

1011
export interface RetryState {

0 commit comments

Comments
 (0)