hotfix: add empty content message recovery to session recovery

This commit is contained in:
YeonGyu-Kim
2025-12-05 03:54:51 +09:00
parent 2c57204142
commit a1a2d2fdb3

View File

@@ -1,7 +1,7 @@
/** /**
* Session Recovery - Message State Error Recovery * Session Recovery - Message State Error Recovery
* *
* Handles THREE specific scenarios: * Handles FOUR specific scenarios:
* 1. tool_use block exists without tool_result * 1. tool_use block exists without tool_result
* - Recovery: inject tool_result with "cancelled" content * - Recovery: inject tool_result with "cancelled" content
* *
@@ -10,6 +10,9 @@
* *
* 3. Thinking disabled but message contains thinking blocks * 3. Thinking disabled but message contains thinking blocks
* - Recovery: strip thinking/redacted_thinking blocks * - Recovery: strip thinking/redacted_thinking blocks
*
* 4. Empty content message (non-empty content required)
* - Recovery: delete the empty message via revert
*/ */
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
@@ -17,7 +20,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk"
type Client = ReturnType<typeof createOpencodeClient> type Client = ReturnType<typeof createOpencodeClient>
type RecoveryErrorType = "tool_result_missing" | "thinking_block_order" | "thinking_disabled_violation" | null type RecoveryErrorType = "tool_result_missing" | "thinking_block_order" | "thinking_disabled_violation" | "empty_content_message" | null
interface MessageInfo { interface MessageInfo {
id?: string id?: string
@@ -75,6 +78,10 @@ function detectErrorType(error: unknown): RecoveryErrorType {
return "thinking_disabled_violation" return "thinking_disabled_violation"
} }
if (message.includes("non-empty content") || message.includes("must have non-empty content")) {
return "empty_content_message"
}
return null return null
} }
@@ -204,6 +211,35 @@ async function recoverThinkingDisabledViolation(
return false return false
} }
async function recoverEmptyContentMessage(
client: Client,
sessionID: string,
failedAssistantMsg: MessageData,
directory: string
): Promise<boolean> {
const messageID = failedAssistantMsg.info?.id
const parentMsgID = failedAssistantMsg.info?.parentID
if (!messageID) {
return false
}
// Revert to parent message (delete the empty message)
const revertTargetID = parentMsgID || messageID
try {
await client.session.revert({
path: { id: sessionID },
body: { messageID: revertTargetID },
query: { directory },
})
return true
} catch {
return false
}
}
async function fallbackRevertStrategy( async function fallbackRevertStrategy(
client: Client, client: Client,
sessionID: string, sessionID: string,
@@ -308,11 +344,13 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
tool_result_missing: "Tool Crash Recovery", tool_result_missing: "Tool Crash Recovery",
thinking_block_order: "Thinking Block Recovery", thinking_block_order: "Thinking Block Recovery",
thinking_disabled_violation: "Thinking Strip Recovery", thinking_disabled_violation: "Thinking Strip Recovery",
empty_content_message: "Empty Message Recovery",
} }
const toastMessages: Record<RecoveryErrorType & string, string> = { const toastMessages: Record<RecoveryErrorType & string, string> = {
tool_result_missing: "Injecting cancelled tool results...", tool_result_missing: "Injecting cancelled tool results...",
thinking_block_order: "Fixing message structure...", thinking_block_order: "Fixing message structure...",
thinking_disabled_violation: "Stripping thinking blocks...", thinking_disabled_violation: "Stripping thinking blocks...",
empty_content_message: "Deleting empty message...",
} }
const toastTitle = toastTitles[errorType] const toastTitle = toastTitles[errorType]
const toastMessage = toastMessages[errorType] const toastMessage = toastMessages[errorType]
@@ -336,6 +374,8 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory) success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory)
} 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") {
success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory)
} }
return success return success