From a1a2d2fdb3571cfe4ca98bff6b4f5f4963b2f12c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 5 Dec 2025 03:54:51 +0900 Subject: [PATCH] hotfix: add empty content message recovery to session recovery --- src/hooks/session-recovery.ts | 44 +++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/hooks/session-recovery.ts b/src/hooks/session-recovery.ts index 8137c10..ccd7cc0 100644 --- a/src/hooks/session-recovery.ts +++ b/src/hooks/session-recovery.ts @@ -1,7 +1,7 @@ /** * Session Recovery - Message State Error Recovery * - * Handles THREE specific scenarios: + * Handles FOUR specific scenarios: * 1. tool_use block exists without tool_result * - Recovery: inject tool_result with "cancelled" content * @@ -10,6 +10,9 @@ * * 3. Thinking disabled but message contains 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" @@ -17,7 +20,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk" type Client = ReturnType -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 { id?: string @@ -75,6 +78,10 @@ function detectErrorType(error: unknown): RecoveryErrorType { return "thinking_disabled_violation" } + if (message.includes("non-empty content") || message.includes("must have non-empty content")) { + return "empty_content_message" + } + return null } @@ -204,6 +211,35 @@ async function recoverThinkingDisabledViolation( return false } +async function recoverEmptyContentMessage( + client: Client, + sessionID: string, + failedAssistantMsg: MessageData, + directory: string +): Promise { + 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( client: Client, sessionID: string, @@ -308,11 +344,13 @@ export function createSessionRecoveryHook(ctx: PluginInput) { tool_result_missing: "Tool Crash Recovery", thinking_block_order: "Thinking Block Recovery", thinking_disabled_violation: "Thinking Strip Recovery", + empty_content_message: "Empty Message Recovery", } const toastMessages: Record = { tool_result_missing: "Injecting cancelled tool results...", thinking_block_order: "Fixing message structure...", thinking_disabled_violation: "Stripping thinking blocks...", + empty_content_message: "Deleting empty message...", } const toastTitle = toastTitles[errorType] const toastMessage = toastMessages[errorType] @@ -336,6 +374,8 @@ export function createSessionRecoveryHook(ctx: PluginInput) { success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory) } 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) } return success