import type { createOpencodeClient } from "@opencode-ai/sdk" import type { MessageData } from "./types" import { extractMessageIndex } from "./detect-error-type" import { META_TYPES, THINKING_TYPES } from "./constants" type Client = ReturnType type ReplaceEmptyTextPartsAsync = ( client: Client, sessionID: string, messageID: string, replacementText: string ) => Promise type InjectTextPartAsync = ( client: Client, sessionID: string, messageID: string, text: string ) => Promise type FindMessagesWithEmptyTextPartsFromSDK = ( client: Client, sessionID: string ) => Promise export async function recoverEmptyContentMessageFromSDK( client: Client, sessionID: string, failedAssistantMsg: MessageData, error: unknown, dependencies: { placeholderText: string replaceEmptyTextPartsAsync: ReplaceEmptyTextPartsAsync injectTextPartAsync: InjectTextPartAsync findMessagesWithEmptyTextPartsFromSDK: FindMessagesWithEmptyTextPartsFromSDK } ): Promise { const targetIndex = extractMessageIndex(error) const failedID = failedAssistantMsg.info?.id let anySuccess = false const messagesWithEmptyText = await dependencies.findMessagesWithEmptyTextPartsFromSDK(client, sessionID) for (const messageID of messagesWithEmptyText) { if ( await dependencies.replaceEmptyTextPartsAsync( client, sessionID, messageID, dependencies.placeholderText ) ) { anySuccess = true } } const messages = await readMessagesFromSDK(client, sessionID) const thinkingOnlyIDs = findMessagesWithThinkingOnlyFromSDK(messages) for (const messageID of thinkingOnlyIDs) { if (await dependencies.injectTextPartAsync(client, sessionID, messageID, dependencies.placeholderText)) { anySuccess = true } } if (targetIndex !== null) { const targetMessageID = findEmptyMessageByIndexFromSDK(messages, targetIndex) if (targetMessageID) { if ( await dependencies.replaceEmptyTextPartsAsync( client, sessionID, targetMessageID, dependencies.placeholderText ) ) { return true } if (await dependencies.injectTextPartAsync(client, sessionID, targetMessageID, dependencies.placeholderText)) { return true } } } if (failedID) { if (await dependencies.replaceEmptyTextPartsAsync(client, sessionID, failedID, dependencies.placeholderText)) { return true } if (await dependencies.injectTextPartAsync(client, sessionID, failedID, dependencies.placeholderText)) { return true } } const emptyMessageIDs = findEmptyMessagesFromSDK(messages) for (const messageID of emptyMessageIDs) { if ( await dependencies.replaceEmptyTextPartsAsync( client, sessionID, messageID, dependencies.placeholderText ) ) { anySuccess = true } if (await dependencies.injectTextPartAsync(client, sessionID, messageID, dependencies.placeholderText)) { anySuccess = true } } return anySuccess } type SdkPart = NonNullable[number] function sdkPartHasContent(part: SdkPart): boolean { if (THINKING_TYPES.has(part.type)) return false if (META_TYPES.has(part.type)) return false if (part.type === "text") { return !!part.text?.trim() } if (part.type === "tool" || part.type === "tool_use" || part.type === "tool_result") { return true } return false } function sdkMessageHasContent(message: MessageData): boolean { return (message.parts ?? []).some(sdkPartHasContent) } async function readMessagesFromSDK(client: Client, sessionID: string): Promise { try { const response = await client.session.messages({ path: { id: sessionID } }) return (response.data ?? []) as MessageData[] } catch { return [] } } function findMessagesWithThinkingOnlyFromSDK(messages: MessageData[]): string[] { const result: string[] = [] for (const msg of messages) { if (msg.info?.role !== "assistant") continue if (!msg.info?.id) continue if (!msg.parts || msg.parts.length === 0) continue const hasThinking = msg.parts.some((part) => THINKING_TYPES.has(part.type)) const hasContent = msg.parts.some(sdkPartHasContent) if (hasThinking && !hasContent) { result.push(msg.info.id) } } return result } function findEmptyMessagesFromSDK(messages: MessageData[]): string[] { const emptyIds: string[] = [] for (const msg of messages) { if (!msg.info?.id) continue if (!sdkMessageHasContent(msg)) { emptyIds.push(msg.info.id) } } return emptyIds } function findEmptyMessageByIndexFromSDK(messages: MessageData[], targetIndex: number): string | null { const indicesToTry = [ targetIndex, targetIndex - 1, targetIndex + 1, targetIndex - 2, targetIndex + 2, targetIndex - 3, targetIndex - 4, targetIndex - 5, ] for (const index of indicesToTry) { if (index < 0 || index >= messages.length) continue const targetMessage = messages[index] if (!targetMessage.info?.id) continue if (!sdkMessageHasContent(targetMessage)) { return targetMessage.info.id } } return null }