fix(hooks): improve session recovery for empty content messages

- Extract message index from Anthropic error messages (messages.N format)
- Sort messages by time.created instead of id for accurate ordering
- Remove last message skip logic that prevented recovery
- Prioritize recovery targets: index-matched > failedMsg > all empty
- Add error logging for debugging recovery failures

Fixes issue where 'messages.83: all messages must have non-empty content' errors were not being recovered properly due to incorrect message ordering and overly restrictive filtering.

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-10 11:07:17 +09:00
parent 7b19177c8a
commit 4f019f8fe5
2 changed files with 50 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import type { createOpencodeClient } from "@opencode-ai/sdk"
import {
findEmptyMessages,
findEmptyMessageByIndex,
findMessagesWithOrphanThinking,
findMessagesWithThinkingBlocks,
injectTextPart,
@@ -54,6 +55,12 @@ function getErrorMessage(error: unknown): string {
return (errorObj.data?.message || errorObj.error?.message || errorObj.message || "").toLowerCase()
}
function extractMessageIndex(error: unknown): number | null {
const message = getErrorMessage(error)
const match = message.match(/messages\.(\d+)/)
return match ? parseInt(match[1], 10) : null
}
function detectErrorType(error: unknown): RecoveryErrorType {
const message = getErrorMessage(error)
@@ -161,16 +168,26 @@ async function recoverEmptyContentMessage(
_client: Client,
sessionID: string,
failedAssistantMsg: MessageData,
_directory: string
_directory: string,
error: unknown
): Promise<boolean> {
const emptyMessageIDs = findEmptyMessages(sessionID)
const targetIndex = extractMessageIndex(error)
const failedID = failedAssistantMsg.info?.id
if (emptyMessageIDs.length === 0) {
const fallbackID = failedAssistantMsg.info?.id
if (!fallbackID) return false
return injectTextPart(sessionID, fallbackID, "(interrupted)")
if (targetIndex !== null) {
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
if (targetMessageID) {
return injectTextPart(sessionID, targetMessageID, "(interrupted)")
}
}
if (failedID) {
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
return true
}
}
const emptyMessageIDs = findEmptyMessages(sessionID)
let anySuccess = false
for (const messageID of emptyMessageIDs) {
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
@@ -262,15 +279,16 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
} else if (errorType === "thinking_disabled_violation") {
success = await recoverThinkingDisabledViolation(ctx.client, sessionID, failedMsg)
} else if (errorType === "empty_content_message") {
success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory)
success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory, info.error)
}
return success
} catch {
return false
} finally {
processingErrors.delete(assistantMsgID)
}
return success
} catch (err) {
console.error("[session-recovery] Recovery failed:", err)
return false
} finally {
processingErrors.delete(assistantMsgID)
}
}
return {

View File

@@ -42,7 +42,12 @@ export function readMessages(sessionID: string): StoredMessageMeta[] {
}
}
return messages.sort((a, b) => a.id.localeCompare(b.id))
return messages.sort((a, b) => {
const aTime = a.time?.created ?? 0
const bTime = b.time?.created ?? 0
if (aTime !== bTime) return aTime - bTime
return a.id.localeCompare(b.id)
})
}
export function readParts(messageID: string): StoredPart[] {
@@ -117,13 +122,9 @@ export function findEmptyMessages(sessionID: string): string[] {
const messages = readMessages(sessionID)
const emptyIds: string[] = []
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
for (const msg of messages) {
if (msg.role !== "assistant") continue
const isLastMessage = i === messages.length - 1
if (isLastMessage) continue
if (!messageHasContent(msg.id)) {
emptyIds.push(msg.id)
}
@@ -132,6 +133,18 @@ export function findEmptyMessages(sessionID: string): string[] {
return emptyIds
}
export function findEmptyMessageByIndex(sessionID: string, targetIndex: number): string | null {
const messages = readMessages(sessionID)
if (targetIndex < 0 || targetIndex >= messages.length) return null
const targetMsg = messages[targetIndex]
if (targetMsg.role !== "assistant") return null
if (messageHasContent(targetMsg.id)) return null
return targetMsg.id
}
export function findFirstEmptyMessage(sessionID: string): string | null {
const emptyIds = findEmptyMessages(sessionID)
return emptyIds.length > 0 ? emptyIds[0] : null