MCPcopy Index your code
hub / github.com/codeaashu/claude-code / detectTurnInterruption

Function detectTurnInterruption

src/utils/conversationRecovery.ts:272–333  ·  view source on GitHub ↗

* Determines whether the conversation was interrupted mid-turn based on the * last message after filtering. An assistant as last message (after filtering * unresolved tool_uses) is treated as a completed turn because stop_reason is * always null on persisted messages in the streaming path. * *

(
  messages: NormalizedMessage[],
)

Source from the content-addressed store, hash-verified

270 * interruption. Attachments are kept as part of the turn.
271 */
272function detectTurnInterruption(
273 messages: NormalizedMessage[],
274): InternalInterruptionState {
275 if (messages.length === 0) {
276 return { kind: 'none' }
277 }
278
279 // Find the last turn-relevant message, skipping system/progress and
280 // synthetic API error assistants. Error assistants are already filtered
281 // before API send (normalizeMessagesForAPI) — skipping them here lets
282 // auto-resume fire after retry exhaustion instead of reading the error as
283 // a completed turn.
284 const lastMessageIdx = messages.findLastIndex(
285 m =>
286 m.type !== 'system' &&
287 m.type !== 'progress' &&
288 !(m.type === 'assistant' && m.isApiErrorMessage),
289 )
290 const lastMessage =
291 lastMessageIdx !== -1 ? messages[lastMessageIdx] : undefined
292
293 if (!lastMessage) {
294 return { kind: 'none' }
295 }
296
297 if (lastMessage.type === 'assistant') {
298 // In the streaming path, stop_reason is always null on persisted messages
299 // because messages are recorded at content_block_stop time, before
300 // message_delta delivers the stop_reason. After filterUnresolvedToolUses
301 // has removed assistant messages with unmatched tool_uses, an assistant as
302 // the last message means the turn most likely completed normally.
303 return { kind: 'none' }
304 }
305
306 if (lastMessage.type === 'user') {
307 if (lastMessage.isMeta || lastMessage.isCompactSummary) {
308 return { kind: 'none' }
309 }
310 if (isToolUseResultMessage(lastMessage)) {
311 // Brief mode (#20467) drops the trailing assistant text block, so a
312 // completed brief-mode turn legitimately ends on SendUserMessage's
313 // tool_result. Without this check, resume misclassifies every
314 // brief-mode session as interrupted mid-turn and injects a phantom
315 // "Continue from where you left off." before the user's real next
316 // prompt. Look back one step for the originating tool_use.
317 if (isTerminalToolResult(lastMessage, messages, lastMessageIdx)) {
318 return { kind: 'none' }
319 }
320 return { kind: 'interrupted_turn' }
321 }
322 // Plain text user prompt — CC hadn't started responding
323 return { kind: 'interrupted_prompt', message: lastMessage }
324 }
325
326 if (lastMessage.type === 'attachment') {
327 // Attachments are part of the user turn — the user provided context but
328 // the assistant never responded.
329 return { kind: 'interrupted_turn' }

Calls 2

isToolUseResultMessageFunction · 0.85
isTerminalToolResultFunction · 0.85

Tested by

no test coverage detected