11/**
22 * Session Recovery - Message State Error Recovery
33 *
4- * Handles THREE specific scenarios:
4+ * Handles FOUR specific scenarios:
55 * 1. tool_use block exists without tool_result
66 * - Recovery: inject tool_result with "cancelled" content
77 *
1010 *
1111 * 3. Thinking disabled but message contains thinking blocks
1212 * - Recovery: strip thinking/redacted_thinking blocks
13+ *
14+ * 4. Empty content message (non-empty content required)
15+ * - Recovery: delete the empty message via revert
1316 */
1417
1518import type { PluginInput } from "@opencode-ai/plugin"
1619import type { createOpencodeClient } from "@opencode-ai/sdk"
1720
1821type Client = ReturnType < typeof createOpencodeClient >
1922
20- type RecoveryErrorType = "tool_result_missing" | "thinking_block_order" | "thinking_disabled_violation" | null
23+ type RecoveryErrorType = "tool_result_missing" | "thinking_block_order" | "thinking_disabled_violation" | "empty_content_message" | null
2124
2225interface MessageInfo {
2326 id ?: string
@@ -75,6 +78,10 @@ function detectErrorType(error: unknown): RecoveryErrorType {
7578 return "thinking_disabled_violation"
7679 }
7780
81+ if ( message . includes ( "non-empty content" ) || message . includes ( "must have non-empty content" ) ) {
82+ return "empty_content_message"
83+ }
84+
7885 return null
7986}
8087
@@ -204,6 +211,35 @@ async function recoverThinkingDisabledViolation(
204211 return false
205212}
206213
214+ async function recoverEmptyContentMessage (
215+ client : Client ,
216+ sessionID : string ,
217+ failedAssistantMsg : MessageData ,
218+ directory : string
219+ ) : Promise < boolean > {
220+ const messageID = failedAssistantMsg . info ?. id
221+ const parentMsgID = failedAssistantMsg . info ?. parentID
222+
223+ if ( ! messageID ) {
224+ return false
225+ }
226+
227+ // Revert to parent message (delete the empty message)
228+ const revertTargetID = parentMsgID || messageID
229+
230+ try {
231+ await client . session . revert ( {
232+ path : { id : sessionID } ,
233+ body : { messageID : revertTargetID } ,
234+ query : { directory } ,
235+ } )
236+
237+ return true
238+ } catch {
239+ return false
240+ }
241+ }
242+
207243async function fallbackRevertStrategy (
208244 client : Client ,
209245 sessionID : string ,
@@ -308,11 +344,13 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
308344 tool_result_missing : "Tool Crash Recovery" ,
309345 thinking_block_order : "Thinking Block Recovery" ,
310346 thinking_disabled_violation : "Thinking Strip Recovery" ,
347+ empty_content_message : "Empty Message Recovery" ,
311348 }
312349 const toastMessages : Record < RecoveryErrorType & string , string > = {
313350 tool_result_missing : "Injecting cancelled tool results..." ,
314351 thinking_block_order : "Fixing message structure..." ,
315352 thinking_disabled_violation : "Stripping thinking blocks..." ,
353+ empty_content_message : "Deleting empty message..." ,
316354 }
317355 const toastTitle = toastTitles [ errorType ]
318356 const toastMessage = toastMessages [ errorType ]
@@ -336,6 +374,8 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
336374 success = await recoverThinkingBlockOrder ( ctx . client , sessionID , failedMsg , ctx . directory )
337375 } else if ( errorType === "thinking_disabled_violation" ) {
338376 success = await recoverThinkingDisabledViolation ( ctx . client , sessionID , failedMsg )
377+ } else if ( errorType === "empty_content_message" ) {
378+ success = await recoverEmptyContentMessage ( ctx . client , sessionID , failedMsg , ctx . directory )
339379 }
340380
341381 return success
0 commit comments