fix(hooks): improve thinking block order recovery with error-based index targeting

- Add findMessageByIndexNeedingThinking for precise message targeting
- Detect "expected X found Y" error pattern for thinking block order
- Remove isLastMessage skip - recovery now handles final assistant messages
- Simplify orphan detection: any non-thinking first part is orphan

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-11 11:07:07 +09:00
parent 06e0285b9c
commit c12f73f774
2 changed files with 44 additions and 12 deletions

View File

@@ -3,6 +3,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk"
import { import {
findEmptyMessages, findEmptyMessages,
findEmptyMessageByIndex, findEmptyMessageByIndex,
findMessageByIndexNeedingThinking,
findMessagesWithOrphanThinking, findMessagesWithOrphanThinking,
findMessagesWithThinkingBlocks, findMessagesWithThinkingBlocks,
injectTextPart, injectTextPart,
@@ -70,7 +71,10 @@ function detectErrorType(error: unknown): RecoveryErrorType {
if ( if (
message.includes("thinking") && message.includes("thinking") &&
(message.includes("first block") || message.includes("must start with") || message.includes("preceeding")) (message.includes("first block") ||
message.includes("must start with") ||
message.includes("preceeding") ||
(message.includes("expected") && message.includes("found")))
) { ) {
return "thinking_block_order" return "thinking_block_order"
} }
@@ -125,8 +129,17 @@ async function recoverThinkingBlockOrder(
_client: Client, _client: Client,
sessionID: string, sessionID: string,
_failedAssistantMsg: MessageData, _failedAssistantMsg: MessageData,
_directory: string _directory: string,
error: unknown
): Promise<boolean> { ): Promise<boolean> {
const targetIndex = extractMessageIndex(error)
if (targetIndex !== null) {
const targetMessageID = findMessageByIndexNeedingThinking(sessionID, targetIndex)
if (targetMessageID) {
return prependThinkingPart(sessionID, targetMessageID)
}
}
const orphanMessages = findMessagesWithOrphanThinking(sessionID) const orphanMessages = findMessagesWithOrphanThinking(sessionID)
if (orphanMessages.length === 0) { if (orphanMessages.length === 0) {
@@ -275,7 +288,7 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
if (errorType === "tool_result_missing") { if (errorType === "tool_result_missing") {
success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg) success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg)
} else if (errorType === "thinking_block_order") { } else if (errorType === "thinking_block_order") {
success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory) success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory, info.error)
} 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") {

View File

@@ -154,13 +154,9 @@ export function findMessagesWithThinkingBlocks(sessionID: string): string[] {
const messages = readMessages(sessionID) const messages = readMessages(sessionID)
const result: string[] = [] const result: 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
const parts = readParts(msg.id) const parts = readParts(msg.id)
const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type)) const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type))
if (hasThinking) { if (hasThinking) {
@@ -179,8 +175,8 @@ export function findMessagesWithOrphanThinking(sessionID: string): string[] {
const msg = messages[i] const msg = messages[i]
if (msg.role !== "assistant") continue if (msg.role !== "assistant") continue
const isLastMessage = i === messages.length - 1 // NOTE: Removed isLastMessage skip - recovery needs to fix last message too
if (isLastMessage) continue // when "thinking must start with" errors occur on final assistant message
const parts = readParts(msg.id) const parts = readParts(msg.id)
if (parts.length === 0) continue if (parts.length === 0) continue
@@ -188,10 +184,11 @@ export function findMessagesWithOrphanThinking(sessionID: string): string[] {
const sortedParts = [...parts].sort((a, b) => a.id.localeCompare(b.id)) const sortedParts = [...parts].sort((a, b) => a.id.localeCompare(b.id))
const firstPart = sortedParts[0] const firstPart = sortedParts[0]
const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type))
const firstIsThinking = THINKING_TYPES.has(firstPart.type) const firstIsThinking = THINKING_TYPES.has(firstPart.type)
if (hasThinking && !firstIsThinking) { // NOTE: Changed condition - if first part is not thinking, it's orphan
// regardless of whether thinking blocks exist elsewhere in the message
if (!firstIsThinking) {
result.push(msg.id) result.push(msg.id)
} }
} }
@@ -246,3 +243,25 @@ export function stripThinkingParts(messageID: string): boolean {
return anyRemoved return anyRemoved
} }
export function findMessageByIndexNeedingThinking(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
const parts = readParts(targetMsg.id)
if (parts.length === 0) return null
const sortedParts = [...parts].sort((a, b) => a.id.localeCompare(b.id))
const firstPart = sortedParts[0]
const firstIsThinking = THINKING_TYPES.has(firstPart.type)
if (!firstIsThinking) {
return targetMsg.id
}
return null
}