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:
@@ -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") {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user