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
This commit is contained in:
@@ -211,29 +211,93 @@ async function recoverThinkingDisabledViolation(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"])
|
||||||
|
|
||||||
|
function hasNonEmptyOutput(msg: MessageData): boolean {
|
||||||
|
const parts = msg.parts
|
||||||
|
if (!parts || parts.length === 0) return false
|
||||||
|
|
||||||
|
return parts.some((p) => {
|
||||||
|
if (THINKING_TYPES.has(p.type)) return false
|
||||||
|
if (p.type === "step-start" || p.type === "step-finish") return false
|
||||||
|
if (p.type === "text" && p.text && p.text.trim()) return true
|
||||||
|
if (p.type === "tool_use" && p.id) return true
|
||||||
|
if (p.type === "tool_result") return true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEmptyContentMessage(msgs: MessageData[]): MessageData | null {
|
||||||
|
for (let i = 0; i < msgs.length; i++) {
|
||||||
|
const msg = msgs[i]
|
||||||
|
const isLastMessage = i === msgs.length - 1
|
||||||
|
const isAssistant = msg.info?.role === "assistant"
|
||||||
|
|
||||||
|
if (isLastMessage && isAssistant) continue
|
||||||
|
|
||||||
|
if (!hasNonEmptyOutput(msg)) {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
async function recoverEmptyContentMessage(
|
async function recoverEmptyContentMessage(
|
||||||
client: Client,
|
client: Client,
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
failedAssistantMsg: MessageData,
|
failedAssistantMsg: MessageData,
|
||||||
directory: string
|
directory: string
|
||||||
): Promise<boolean> {
|
): 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 {
|
try {
|
||||||
|
const messagesResp = await client.session.messages({
|
||||||
|
path: { id: sessionID },
|
||||||
|
query: { directory },
|
||||||
|
})
|
||||||
|
const msgs = (messagesResp as { data?: MessageData[] }).data
|
||||||
|
|
||||||
|
if (!msgs || msgs.length === 0) return false
|
||||||
|
|
||||||
|
const emptyMsg = findEmptyContentMessage(msgs) || failedAssistantMsg
|
||||||
|
const messageID = emptyMsg.info?.id
|
||||||
|
if (!messageID) return false
|
||||||
|
|
||||||
|
const existingParts = emptyMsg.parts || []
|
||||||
|
const hasOnlyThinkingOrMeta = existingParts.length > 0 && existingParts.every(
|
||||||
|
(p) => THINKING_TYPES.has(p.type) || p.type === "step-start" || p.type === "step-finish"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasOnlyThinkingOrMeta) {
|
||||||
|
const strippedParts: MessagePart[] = [{ type: "text", text: "(interrupted)" }]
|
||||||
|
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - Experimental API
|
||||||
|
await client.message?.update?.({
|
||||||
|
path: { id: messageID },
|
||||||
|
body: { parts: strippedParts },
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
// message.update not available
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - Experimental API
|
||||||
|
await client.session.patch?.({
|
||||||
|
path: { id: sessionID },
|
||||||
|
body: { messageID, parts: strippedParts },
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
// session.patch not available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const revertTargetID = emptyMsg.info?.parentID || messageID
|
||||||
await client.session.revert({
|
await client.session.revert({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: { messageID: revertTargetID },
|
body: { messageID: revertTargetID },
|
||||||
query: { directory },
|
query: { directory },
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user