Skip to content

Commit 8f39575

Browse files
authored
fix(session-recovery): handle empty content from interrupted reasoning (#6)
* fix(publish): make git operations idempotent - Check for staged changes before commit - Check if tag exists before creating - Check if release exists before creating * fix(session-recovery): handle empty content from interrupted reasoning - Add THINKING_TYPES set including 'reasoning' type (OpenCode's thinking) - Add hasNonEmptyOutput() to detect messages with only thinking/meta parts - Add findEmptyContentMessage() to scan all messages for empty content - Handle step-start/step-finish meta parts in empty content detection - Patch interrupted messages with '(interrupted)' text before falling back to revert
1 parent 2464473 commit 8f39575

File tree

1 file changed

+73
-9
lines changed

1 file changed

+73
-9
lines changed

src/hooks/session-recovery.ts

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -211,29 +211,93 @@ async function recoverThinkingDisabledViolation(
211211
return false
212212
}
213213

214+
const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"])
215+
216+
function hasNonEmptyOutput(msg: MessageData): boolean {
217+
const parts = msg.parts
218+
if (!parts || parts.length === 0) return false
219+
220+
return parts.some((p) => {
221+
if (THINKING_TYPES.has(p.type)) return false
222+
if (p.type === "step-start" || p.type === "step-finish") return false
223+
if (p.type === "text" && p.text && p.text.trim()) return true
224+
if (p.type === "tool_use" && p.id) return true
225+
if (p.type === "tool_result") return true
226+
return false
227+
})
228+
}
229+
230+
function findEmptyContentMessage(msgs: MessageData[]): MessageData | null {
231+
for (let i = 0; i < msgs.length; i++) {
232+
const msg = msgs[i]
233+
const isLastMessage = i === msgs.length - 1
234+
const isAssistant = msg.info?.role === "assistant"
235+
236+
if (isLastMessage && isAssistant) continue
237+
238+
if (!hasNonEmptyOutput(msg)) {
239+
return msg
240+
}
241+
}
242+
return null
243+
}
244+
214245
async function recoverEmptyContentMessage(
215246
client: Client,
216247
sessionID: string,
217248
failedAssistantMsg: MessageData,
218249
directory: string
219250
): Promise<boolean> {
220-
const messageID = failedAssistantMsg.info?.id
221-
const parentMsgID = failedAssistantMsg.info?.parentID
251+
try {
252+
const messagesResp = await client.session.messages({
253+
path: { id: sessionID },
254+
query: { directory },
255+
})
256+
const msgs = (messagesResp as { data?: MessageData[] }).data
222257

223-
if (!messageID) {
224-
return false
225-
}
258+
if (!msgs || msgs.length === 0) return false
226259

227-
// Revert to parent message (delete the empty message)
228-
const revertTargetID = parentMsgID || messageID
260+
const emptyMsg = findEmptyContentMessage(msgs) || failedAssistantMsg
261+
const messageID = emptyMsg.info?.id
262+
if (!messageID) return false
229263

230-
try {
264+
const existingParts = emptyMsg.parts || []
265+
const hasOnlyThinkingOrMeta = existingParts.length > 0 && existingParts.every(
266+
(p) => THINKING_TYPES.has(p.type) || p.type === "step-start" || p.type === "step-finish"
267+
)
268+
269+
if (hasOnlyThinkingOrMeta) {
270+
const strippedParts: MessagePart[] = [{ type: "text", text: "(interrupted)" }]
271+
272+
try {
273+
// @ts-expect-error - Experimental API
274+
await client.message?.update?.({
275+
path: { id: messageID },
276+
body: { parts: strippedParts },
277+
})
278+
return true
279+
} catch {
280+
// message.update not available
281+
}
282+
283+
try {
284+
// @ts-expect-error - Experimental API
285+
await client.session.patch?.({
286+
path: { id: sessionID },
287+
body: { messageID, parts: strippedParts },
288+
})
289+
return true
290+
} catch {
291+
// session.patch not available
292+
}
293+
}
294+
295+
const revertTargetID = emptyMsg.info?.parentID || messageID
231296
await client.session.revert({
232297
path: { id: sessionID },
233298
body: { messageID: revertTargetID },
234299
query: { directory },
235300
})
236-
237301
return true
238302
} catch {
239303
return false

0 commit comments

Comments
 (0)