Skip to content

Commit a1a2d2f

Browse files
committed
hotfix: add empty content message recovery to session recovery
1 parent 2c57204 commit a1a2d2f

File tree

1 file changed

+42
-2
lines changed

1 file changed

+42
-2
lines changed

src/hooks/session-recovery.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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
*
@@ -10,14 +10,17 @@
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

1518
import type { PluginInput } from "@opencode-ai/plugin"
1619
import type { createOpencodeClient } from "@opencode-ai/sdk"
1720

1821
type 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

2225
interface 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+
207243
async 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

Comments
 (0)