MCPcopy Create free account
hub / github.com/codeaashu/claude-code / ensureToolResultPairing

Function ensureToolResultPairing

src/utils/messages.ts:5133–5460  ·  view source on GitHub ↗
(
  messages: (UserMessage | AssistantMessage)[],
)

Source from the content-addressed store, hash-verified

5131 * that will be rejected at submission anyway.
5132 */
5133export function ensureToolResultPairing(
5134 messages: (UserMessage | AssistantMessage)[],
5135): (UserMessage | AssistantMessage)[] {
5136 const result: (UserMessage | AssistantMessage)[] = []
5137 let repaired = false
5138
5139 // Cross-message tool_use ID tracking. The per-message seenToolUseIds below
5140 // only caught duplicates within a single assistant's content array (the
5141 // normalizeMessagesForAPI-merged case). When two assistants with DIFFERENT
5142 // message.id carry the same tool_use ID — e.g. orphan handler re-pushed an
5143 // assistant already present in mutableMessages with a fresh message.id, or
5144 // normalizeMessagesForAPI's backward walk broke on an intervening user
5145 // message — the dup lived in separate result entries and the API rejected
5146 // with "tool_use ids must be unique", deadlocking the session (CC-1212).
5147 const allSeenToolUseIds = new Set<string>()
5148
5149 for (let i = 0; i < messages.length; i++) {
5150 const msg = messages[i]!
5151
5152 if (msg.type !== 'assistant') {
5153 // A user message with tool_result blocks but NO preceding assistant
5154 // message in the output has orphaned tool_results. The assistant
5155 // lookahead below only validates assistant→user adjacency; it never
5156 // sees user messages at index 0 or user messages preceded by another
5157 // user. This happens on resume when the transcript starts mid-turn
5158 // (e.g. messages[0] is a tool_result whose assistant pair was dropped
5159 // by earlier compaction — API rejects with "messages.0.content:
5160 // unexpected tool_use_id").
5161 if (
5162 msg.type === 'user' &&
5163 Array.isArray(msg.message.content) &&
5164 result.at(-1)?.type !== 'assistant'
5165 ) {
5166 const stripped = msg.message.content.filter(
5167 block =>
5168 !(
5169 typeof block === 'object' &&
5170 'type' in block &&
5171 block.type === 'tool_result'
5172 ),
5173 )
5174 if (stripped.length !== msg.message.content.length) {
5175 repaired = true
5176 // If stripping emptied the message and nothing has been pushed yet,
5177 // keep a placeholder so the payload still starts with a user
5178 // message (normalizeMessagesForAPI runs before us, so messages[1]
5179 // is an assistant — dropping messages[0] entirely would yield a
5180 // payload starting with assistant, a different 400).
5181 const content =
5182 stripped.length > 0
5183 ? stripped
5184 : result.length === 0
5185 ? [
5186 {
5187 type: 'text' as const,
5188 text: '[Orphaned tool result removed due to conversation resume]',
5189 },
5190 ]

Callers 1

queryModelFunction · 0.85

Calls 9

createUserMessageFunction · 0.85
logEventFunction · 0.85
logErrorFunction · 0.70
pushMethod · 0.45
addMethod · 0.45
hasMethod · 0.45

Tested by

no test coverage detected