fix(session-recovery): Add placeholder message for thinking-only messages
- Add findMessagesWithThinkingOnly() to detect orphan thinking messages - Inject [user interrupted] placeholder for thinking-only messages - Expand index offset handling from 2 to 3 attempts for better error recovery - Use constant PLACEHOLDER_TEXT for consistency across recovery functions
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
findMessageByIndexNeedingThinking,
|
findMessageByIndexNeedingThinking,
|
||||||
findMessagesWithOrphanThinking,
|
findMessagesWithOrphanThinking,
|
||||||
findMessagesWithThinkingBlocks,
|
findMessagesWithThinkingBlocks,
|
||||||
|
findMessagesWithThinkingOnly,
|
||||||
injectTextPart,
|
injectTextPart,
|
||||||
prependThinkingPart,
|
prependThinkingPart,
|
||||||
stripThinkingParts,
|
stripThinkingParts,
|
||||||
@@ -177,6 +178,8 @@ async function recoverThinkingDisabledViolation(
|
|||||||
return anySuccess
|
return anySuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PLACEHOLDER_TEXT = "[user interrupted]"
|
||||||
|
|
||||||
async function recoverEmptyContentMessage(
|
async function recoverEmptyContentMessage(
|
||||||
_client: Client,
|
_client: Client,
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
@@ -187,23 +190,28 @@ async function recoverEmptyContentMessage(
|
|||||||
const targetIndex = extractMessageIndex(error)
|
const targetIndex = extractMessageIndex(error)
|
||||||
const failedID = failedAssistantMsg.info?.id
|
const failedID = failedAssistantMsg.info?.id
|
||||||
|
|
||||||
|
const thinkingOnlyIDs = findMessagesWithThinkingOnly(sessionID)
|
||||||
|
for (const messageID of thinkingOnlyIDs) {
|
||||||
|
injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)
|
||||||
|
}
|
||||||
|
|
||||||
if (targetIndex !== null) {
|
if (targetIndex !== null) {
|
||||||
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
|
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
|
||||||
if (targetMessageID) {
|
if (targetMessageID) {
|
||||||
return injectTextPart(sessionID, targetMessageID, "(interrupted)")
|
return injectTextPart(sessionID, targetMessageID, PLACEHOLDER_TEXT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedID) {
|
if (failedID) {
|
||||||
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
|
if (injectTextPart(sessionID, failedID, PLACEHOLDER_TEXT)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyMessageIDs = findEmptyMessages(sessionID)
|
const emptyMessageIDs = findEmptyMessages(sessionID)
|
||||||
let anySuccess = false
|
let anySuccess = thinkingOnlyIDs.length > 0
|
||||||
for (const messageID of emptyMessageIDs) {
|
for (const messageID of emptyMessageIDs) {
|
||||||
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
|
if (injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)) {
|
||||||
anySuccess = true
|
anySuccess = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,20 +133,15 @@ export function findEmptyMessages(sessionID: string): string[] {
|
|||||||
|
|
||||||
export function findEmptyMessageByIndex(sessionID: string, targetIndex: number): string | null {
|
export function findEmptyMessageByIndex(sessionID: string, targetIndex: number): string | null {
|
||||||
const messages = readMessages(sessionID)
|
const messages = readMessages(sessionID)
|
||||||
|
|
||||||
// Try multiple indices to handle system message offset
|
// API index may differ from storage index due to system messages
|
||||||
// API includes system message at index 0, storage may not
|
const indicesToTry = [targetIndex, targetIndex - 1, targetIndex - 2]
|
||||||
const indicesToTry = [targetIndex, targetIndex - 1]
|
|
||||||
|
|
||||||
for (const idx of indicesToTry) {
|
for (const idx of indicesToTry) {
|
||||||
if (idx < 0 || idx >= messages.length) continue
|
if (idx < 0 || idx >= messages.length) continue
|
||||||
|
|
||||||
const targetMsg = messages[idx]
|
const targetMsg = messages[idx]
|
||||||
|
|
||||||
// NOTE: Do NOT skip last assistant message here
|
|
||||||
// If API returned an error, this message is NOT the final assistant message
|
|
||||||
// (the API only allows empty content for the ACTUAL final assistant message)
|
|
||||||
|
|
||||||
if (!messageHasContent(targetMsg.id)) {
|
if (!messageHasContent(targetMsg.id)) {
|
||||||
return targetMsg.id
|
return targetMsg.id
|
||||||
}
|
}
|
||||||
@@ -177,6 +172,28 @@ export function findMessagesWithThinkingBlocks(sessionID: string): string[] {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findMessagesWithThinkingOnly(sessionID: string): string[] {
|
||||||
|
const messages = readMessages(sessionID)
|
||||||
|
const result: string[] = []
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.role !== "assistant") continue
|
||||||
|
|
||||||
|
const parts = readParts(msg.id)
|
||||||
|
if (parts.length === 0) continue
|
||||||
|
|
||||||
|
const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type))
|
||||||
|
const hasTextContent = parts.some(hasContent)
|
||||||
|
|
||||||
|
// Has thinking but no text content = orphan thinking
|
||||||
|
if (hasThinking && !hasTextContent) {
|
||||||
|
result.push(msg.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export function findMessagesWithOrphanThinking(sessionID: string): string[] {
|
export function findMessagesWithOrphanThinking(sessionID: string): string[] {
|
||||||
const messages = readMessages(sessionID)
|
const messages = readMessages(sessionID)
|
||||||
const result: string[] = []
|
const result: string[] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user