Skip to content

Commit 1aaa6e6

Browse files
committed
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
1 parent 7cb8210 commit 1aaa6e6

File tree

2 files changed

+39
-14
lines changed

2 files changed

+39
-14
lines changed

src/hooks/session-recovery/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
findMessageByIndexNeedingThinking,
77
findMessagesWithOrphanThinking,
88
findMessagesWithThinkingBlocks,
9+
findMessagesWithThinkingOnly,
910
injectTextPart,
1011
prependThinkingPart,
1112
stripThinkingParts,
@@ -177,6 +178,8 @@ async function recoverThinkingDisabledViolation(
177178
return anySuccess
178179
}
179180

181+
const PLACEHOLDER_TEXT = "[user interrupted]"
182+
180183
async function recoverEmptyContentMessage(
181184
_client: Client,
182185
sessionID: string,
@@ -187,23 +190,28 @@ async function recoverEmptyContentMessage(
187190
const targetIndex = extractMessageIndex(error)
188191
const failedID = failedAssistantMsg.info?.id
189192

193+
const thinkingOnlyIDs = findMessagesWithThinkingOnly(sessionID)
194+
for (const messageID of thinkingOnlyIDs) {
195+
injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)
196+
}
197+
190198
if (targetIndex !== null) {
191199
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
192200
if (targetMessageID) {
193-
return injectTextPart(sessionID, targetMessageID, "(interrupted)")
201+
return injectTextPart(sessionID, targetMessageID, PLACEHOLDER_TEXT)
194202
}
195203
}
196204

197205
if (failedID) {
198-
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
206+
if (injectTextPart(sessionID, failedID, PLACEHOLDER_TEXT)) {
199207
return true
200208
}
201209
}
202210

203211
const emptyMessageIDs = findEmptyMessages(sessionID)
204-
let anySuccess = false
212+
let anySuccess = thinkingOnlyIDs.length > 0
205213
for (const messageID of emptyMessageIDs) {
206-
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
214+
if (injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)) {
207215
anySuccess = true
208216
}
209217
}

src/hooks/session-recovery/storage.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,20 +133,15 @@ export function findEmptyMessages(sessionID: string): string[] {
133133

134134
export function findEmptyMessageByIndex(sessionID: string, targetIndex: number): string | null {
135135
const messages = readMessages(sessionID)
136-
137-
// Try multiple indices to handle system message offset
138-
// API includes system message at index 0, storage may not
139-
const indicesToTry = [targetIndex, targetIndex - 1]
140-
136+
137+
// API index may differ from storage index due to system messages
138+
const indicesToTry = [targetIndex, targetIndex - 1, targetIndex - 2]
139+
141140
for (const idx of indicesToTry) {
142141
if (idx < 0 || idx >= messages.length) continue
143142

144143
const targetMsg = messages[idx]
145-
146-
// NOTE: Do NOT skip last assistant message here
147-
// If API returned an error, this message is NOT the final assistant message
148-
// (the API only allows empty content for the ACTUAL final assistant message)
149-
144+
150145
if (!messageHasContent(targetMsg.id)) {
151146
return targetMsg.id
152147
}
@@ -177,6 +172,28 @@ export function findMessagesWithThinkingBlocks(sessionID: string): string[] {
177172
return result
178173
}
179174

175+
export function findMessagesWithThinkingOnly(sessionID: string): string[] {
176+
const messages = readMessages(sessionID)
177+
const result: string[] = []
178+
179+
for (const msg of messages) {
180+
if (msg.role !== "assistant") continue
181+
182+
const parts = readParts(msg.id)
183+
if (parts.length === 0) continue
184+
185+
const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type))
186+
const hasTextContent = parts.some(hasContent)
187+
188+
// Has thinking but no text content = orphan thinking
189+
if (hasThinking && !hasTextContent) {
190+
result.push(msg.id)
191+
}
192+
}
193+
194+
return result
195+
}
196+
180197
export function findMessagesWithOrphanThinking(sessionID: string): string[] {
181198
const messages = readMessages(sessionID)
182199
const result: string[] = []

0 commit comments

Comments
 (0)