Skip to content

session-recovery: Fix tool_result_missing and thinking_block_order recovery failures #483

@starcomo

Description

@starcomo

문제 설명

세션 복구 기능이 특정 상황에서 실패하여 다음과 같은 에러가 반복 발생합니다:

1. tool_result_missing 에러

messages.141: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01MiWair1KGbNe4CPdq8fmnT. Each `tool_use` block must have a corresponding `tool_result` block in the next message.

2. thinking_block_order 에러

messages.57.content.0: If an assistant message contains any thinking blocks, the first block must be `thinking` or `redacted_thinking`. Found `text`.

원인 분석

tool_result_missing

recoverToolResultMissing() 함수에서:

  1. failedAssistantMsg.parts에서 tool_use ID 추출 시도
  2. parts가 비어있으면 스토리지에서 읽기 시도
  3. 둘 다 실패하면 toolUseIds가 빈 배열 → return false → 복구 실패

하지만 에러 메시지 자체에 tool_use ID가 포함되어 있음 (toolu_01MiWair1KGbNe4CPdq8fmnT)

thinking_block_order

findMessageByIndexNeedingThinking() 함수에서:

  1. API에서 반환하는 메시지 인덱스와 스토리지 인덱스가 다를 수 있음 (시스템 메시지 등으로 인해)
  2. 정확한 인덱스만 시도하므로 메시지를 찾지 못함
  3. failedAssistantMsg.info.id를 직접 사용하는 fallback이 없음

제안하는 수정

1. index.ts - recoverToolResultMissing

async function recoverToolResultMissing(
  client: Client,
  sessionID: string,
  failedAssistantMsg: MessageData,
  error?: unknown  // 에러 파라미터 추가
): Promise<boolean> {
  // ... 기존 parts 추출 로직 ...
  
  let toolUseIds = extractToolUseIds(parts)

  // Fallback: 에러 메시지에서 직접 tool_use ID 추출
  if (toolUseIds.length === 0 && error) {
    toolUseIds = extractToolUseIdsFromError(error)
  }

  if (toolUseIds.length === 0) {
    return false
  }
  // ...
}

그리고 호출부에서 info.error 전달:

if (errorType === "tool_result_missing") {
  success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg, info.error)
}

2. storage.ts - findMessageByIndexNeedingThinking

export function findMessageByIndexNeedingThinking(sessionID: string, targetIndex: number): string | null {
  const messages = readMessages(sessionID)

  // API 인덱스와 스토리지 인덱스 차이를 고려하여 여러 오프셋 시도
  const indicesToTry = [
    targetIndex,
    targetIndex - 1,
    targetIndex + 1,
    targetIndex - 2,
    targetIndex + 2,
    targetIndex - 3,
    targetIndex - 4,
    targetIndex - 5,
  ]

  for (const idx of indicesToTry) {
    if (idx < 0 || idx >= messages.length) continue
    // ... 기존 로직 ...
  }

  return null
}

3. index.ts - recoverThinkingBlockOrder

async function recoverThinkingBlockOrder(
  _client: Client,
  sessionID: string,
  failedAssistantMsg: MessageData,  // _failedAssistantMsg → failedAssistantMsg (사용)
  _directory: string,
  error: unknown
): Promise<boolean> {
  // ... 기존 인덱스 기반 복구 시도 ...

  // Fallback: failedAssistantMsg ID 직접 사용
  const failedMsgId = failedAssistantMsg.info?.id
  if (failedMsgId) {
    if (prependThinkingPart(sessionID, failedMsgId)) {
      return true
    }
  }

  // ... orphanMessages fallback ...
}

환경

  • OpenCode 버전: 최신
  • oh-my-opencode 버전: 2.12.2
  • 모델: claude-opus-4-5

추가 정보

이 수정으로 세션이 중단되지 않고 자동으로 복구되어 작업을 이어갈 수 있습니다.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions