fix(session-recovery): Replace empty text parts before injecting new ones
Directly modify empty text parts in storage files before attempting to inject new parts. This ensures that existing empty text parts are replaced with placeholder text, fixing the issue where Anthropic API returns 'messages.X: all messages must have non-empty content' error even after recovery. - Added replaceEmptyTextParts function to directly replace empty text parts - Added findMessagesWithEmptyTextParts function to identify affected messages - Modified recoverEmptyContentMessage to prioritize replacing existing empty parts 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -4,12 +4,14 @@ import {
|
|||||||
findEmptyMessages,
|
findEmptyMessages,
|
||||||
findEmptyMessageByIndex,
|
findEmptyMessageByIndex,
|
||||||
findMessageByIndexNeedingThinking,
|
findMessageByIndexNeedingThinking,
|
||||||
|
findMessagesWithEmptyTextParts,
|
||||||
findMessagesWithOrphanThinking,
|
findMessagesWithOrphanThinking,
|
||||||
findMessagesWithThinkingBlocks,
|
findMessagesWithThinkingBlocks,
|
||||||
findMessagesWithThinkingOnly,
|
findMessagesWithThinkingOnly,
|
||||||
injectTextPart,
|
injectTextPart,
|
||||||
prependThinkingPart,
|
prependThinkingPart,
|
||||||
readParts,
|
readParts,
|
||||||
|
replaceEmptyTextParts,
|
||||||
stripThinkingParts,
|
stripThinkingParts,
|
||||||
} from "./storage"
|
} from "./storage"
|
||||||
import type { MessageData } from "./types"
|
import type { MessageData } from "./types"
|
||||||
@@ -222,28 +224,48 @@ async function recoverEmptyContentMessage(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const targetIndex = extractMessageIndex(error)
|
const targetIndex = extractMessageIndex(error)
|
||||||
const failedID = failedAssistantMsg.info?.id
|
const failedID = failedAssistantMsg.info?.id
|
||||||
|
let anySuccess = false
|
||||||
|
|
||||||
|
const messagesWithEmptyText = findMessagesWithEmptyTextParts(sessionID)
|
||||||
|
for (const messageID of messagesWithEmptyText) {
|
||||||
|
if (replaceEmptyTextParts(messageID, PLACEHOLDER_TEXT)) {
|
||||||
|
anySuccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const thinkingOnlyIDs = findMessagesWithThinkingOnly(sessionID)
|
const thinkingOnlyIDs = findMessagesWithThinkingOnly(sessionID)
|
||||||
for (const messageID of thinkingOnlyIDs) {
|
for (const messageID of thinkingOnlyIDs) {
|
||||||
injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)
|
if (injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)) {
|
||||||
|
anySuccess = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetIndex !== null) {
|
if (targetIndex !== null) {
|
||||||
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
|
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
|
||||||
if (targetMessageID) {
|
if (targetMessageID) {
|
||||||
return injectTextPart(sessionID, targetMessageID, PLACEHOLDER_TEXT)
|
if (replaceEmptyTextParts(targetMessageID, PLACEHOLDER_TEXT)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (injectTextPart(sessionID, targetMessageID, PLACEHOLDER_TEXT)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedID) {
|
if (failedID) {
|
||||||
|
if (replaceEmptyTextParts(failedID, PLACEHOLDER_TEXT)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if (injectTextPart(sessionID, failedID, PLACEHOLDER_TEXT)) {
|
if (injectTextPart(sessionID, failedID, PLACEHOLDER_TEXT)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyMessageIDs = findEmptyMessages(sessionID)
|
const emptyMessageIDs = findEmptyMessages(sessionID)
|
||||||
let anySuccess = thinkingOnlyIDs.length > 0
|
|
||||||
for (const messageID of emptyMessageIDs) {
|
for (const messageID of emptyMessageIDs) {
|
||||||
|
if (replaceEmptyTextParts(messageID, PLACEHOLDER_TEXT)) {
|
||||||
|
anySuccess = true
|
||||||
|
}
|
||||||
if (injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)) {
|
if (injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)) {
|
||||||
anySuccess = true
|
anySuccess = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,6 +271,55 @@ export function stripThinkingParts(messageID: string): boolean {
|
|||||||
return anyRemoved
|
return anyRemoved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function replaceEmptyTextParts(messageID: string, replacementText: string): boolean {
|
||||||
|
const partDir = join(PART_STORAGE, messageID)
|
||||||
|
if (!existsSync(partDir)) return false
|
||||||
|
|
||||||
|
let anyReplaced = false
|
||||||
|
for (const file of readdirSync(partDir)) {
|
||||||
|
if (!file.endsWith(".json")) continue
|
||||||
|
try {
|
||||||
|
const filePath = join(partDir, file)
|
||||||
|
const content = readFileSync(filePath, "utf-8")
|
||||||
|
const part = JSON.parse(content) as StoredPart
|
||||||
|
|
||||||
|
if (part.type === "text") {
|
||||||
|
const textPart = part as StoredTextPart
|
||||||
|
if (!textPart.text?.trim()) {
|
||||||
|
textPart.text = replacementText
|
||||||
|
textPart.synthetic = true
|
||||||
|
writeFileSync(filePath, JSON.stringify(textPart, null, 2))
|
||||||
|
anyReplaced = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyReplaced
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMessagesWithEmptyTextParts(sessionID: string): string[] {
|
||||||
|
const messages = readMessages(sessionID)
|
||||||
|
const result: string[] = []
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
const parts = readParts(msg.id)
|
||||||
|
const hasEmptyTextPart = parts.some((p) => {
|
||||||
|
if (p.type !== "text") return false
|
||||||
|
const textPart = p as StoredTextPart
|
||||||
|
return !textPart.text?.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasEmptyTextPart) {
|
||||||
|
result.push(msg.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export function findMessageByIndexNeedingThinking(sessionID: string, targetIndex: number): string | null {
|
export function findMessageByIndexNeedingThinking(sessionID: string, targetIndex: number): string | null {
|
||||||
const messages = readMessages(sessionID)
|
const messages = readMessages(sessionID)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user