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:
@@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
|
|||||||
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
||||||
import {
|
import {
|
||||||
findEmptyMessages,
|
findEmptyMessages,
|
||||||
|
findEmptyMessageByIndex,
|
||||||
findMessagesWithOrphanThinking,
|
findMessagesWithOrphanThinking,
|
||||||
findMessagesWithThinkingBlocks,
|
findMessagesWithThinkingBlocks,
|
||||||
injectTextPart,
|
injectTextPart,
|
||||||
@@ -54,6 +55,12 @@ function getErrorMessage(error: unknown): string {
|
|||||||
return (errorObj.data?.message || errorObj.error?.message || errorObj.message || "").toLowerCase()
|
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 {
|
function detectErrorType(error: unknown): RecoveryErrorType {
|
||||||
const message = getErrorMessage(error)
|
const message = getErrorMessage(error)
|
||||||
|
|
||||||
@@ -161,16 +168,26 @@ async function recoverEmptyContentMessage(
|
|||||||
_client: Client,
|
_client: Client,
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
failedAssistantMsg: MessageData,
|
failedAssistantMsg: MessageData,
|
||||||
_directory: string
|
_directory: string,
|
||||||
|
error: unknown
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const emptyMessageIDs = findEmptyMessages(sessionID)
|
const targetIndex = extractMessageIndex(error)
|
||||||
|
const failedID = failedAssistantMsg.info?.id
|
||||||
|
|
||||||
if (emptyMessageIDs.length === 0) {
|
if (targetIndex !== null) {
|
||||||
const fallbackID = failedAssistantMsg.info?.id
|
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
|
||||||
if (!fallbackID) return false
|
if (targetMessageID) {
|
||||||
return injectTextPart(sessionID, fallbackID, "(interrupted)")
|
return injectTextPart(sessionID, targetMessageID, "(interrupted)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (failedID) {
|
||||||
|
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyMessageIDs = findEmptyMessages(sessionID)
|
||||||
let anySuccess = false
|
let anySuccess = false
|
||||||
for (const messageID of emptyMessageIDs) {
|
for (const messageID of emptyMessageIDs) {
|
||||||
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
|
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
|
||||||
@@ -262,15 +279,16 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
|
|||||||
} else if (errorType === "thinking_disabled_violation") {
|
} else if (errorType === "thinking_disabled_violation") {
|
||||||
success = await recoverThinkingDisabledViolation(ctx.client, sessionID, failedMsg)
|
success = await recoverThinkingDisabledViolation(ctx.client, sessionID, failedMsg)
|
||||||
} else if (errorType === "empty_content_message") {
|
} 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
|
return success
|
||||||
} catch {
|
} catch (err) {
|
||||||
return false
|
console.error("[session-recovery] Recovery failed:", err)
|
||||||
} finally {
|
return false
|
||||||
processingErrors.delete(assistantMsgID)
|
} finally {
|
||||||
}
|
processingErrors.delete(assistantMsgID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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[] {
|
export function readParts(messageID: string): StoredPart[] {
|
||||||
@@ -117,13 +122,9 @@ export function findEmptyMessages(sessionID: string): string[] {
|
|||||||
const messages = readMessages(sessionID)
|
const messages = readMessages(sessionID)
|
||||||
const emptyIds: string[] = []
|
const emptyIds: string[] = []
|
||||||
|
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (const msg of messages) {
|
||||||
const msg = messages[i]
|
|
||||||
if (msg.role !== "assistant") continue
|
if (msg.role !== "assistant") continue
|
||||||
|
|
||||||
const isLastMessage = i === messages.length - 1
|
|
||||||
if (isLastMessage) continue
|
|
||||||
|
|
||||||
if (!messageHasContent(msg.id)) {
|
if (!messageHasContent(msg.id)) {
|
||||||
emptyIds.push(msg.id)
|
emptyIds.push(msg.id)
|
||||||
}
|
}
|
||||||
@@ -132,6 +133,18 @@ export function findEmptyMessages(sessionID: string): string[] {
|
|||||||
return emptyIds
|
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 {
|
export function findFirstEmptyMessage(sessionID: string): string | null {
|
||||||
const emptyIds = findEmptyMessages(sessionID)
|
const emptyIds = findEmptyMessages(sessionID)
|
||||||
return emptyIds.length > 0 ? emptyIds[0] : null
|
return emptyIds.length > 0 ? emptyIds[0] : null
|
||||||
|
|||||||
Reference in New Issue
Block a user