From 22b5201d14abd8267a3ad83fe42baa22df73ef61 Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Fri, 30 Jan 2026 10:46:54 -0500 Subject: [PATCH] Fix #4650: Re-run tool pairing and turn validation after history limiting After limitHistoryTurns or compaction operations, the message history can end up with: 1. Orphaned tool_result blocks (tool_use IDs not in preceding message) 2. Consecutive assistant messages that violate API format requirements Root cause: sanitizeToolUseResultPairing and turn validation functions (validateGeminiTurns, validateAnthropicTurns) were only running BEFORE limitHistoryTurns, but history limiting can create new issues by cutting off tool_use blocks or creating consecutive assistant messages during compaction. Solution: Re-run both sanitizeToolUseResultPairing and turn validation AFTER limitHistoryTurns in both main code paths: - src/agents/pi-embedded-runner/run/attempt.ts - src/agents/pi-embedded-runner/compact.ts This ensures tool_use/tool_result pairing is maintained and consecutive assistant/user messages are properly merged after any history modification. Changes: - Added transcriptPolicy checks before applying repairs (respects config) - Re-run validateGeminiTurns after sanitizeToolUseResultPairing - Re-run validateAnthropicTurns after validateGeminiTurns - Added import for sanitizeToolUseResultPairing in compact.ts --- src/agents/pi-embedded-runner/compact.ts | 16 ++++++++++++++-- src/agents/pi-embedded-runner/run/attempt.ts | 19 ++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 2dc4c5325..c625b0413 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -35,6 +35,7 @@ import { validateAnthropicTurns, validateGeminiTurns, } from "../pi-embedded-helpers.js"; +import { sanitizeToolUseResultPairing } from "../session-transcript-repair.js"; import { ensurePiCompactionReserveTokens, resolveCompactionReserveTokensFloor, @@ -421,8 +422,19 @@ export async function compactEmbeddedPiSessionDirect( validated, getDmHistoryLimitFromSessionKey(params.sessionKey, params.config), ); - if (limited.length > 0) { - session.agent.replaceMessages(limited); + // Fix: Repair tool_use/tool_result pairings AFTER truncation (issue #4650) + const repaired = transcriptPolicy.repairToolUseResultPairing + ? sanitizeToolUseResultPairing(limited) + : limited; + // Re-run turn validation after limiting (issue #4650) to merge consecutive assistant/user messages + const revalidatedGemini = transcriptPolicy.validateGeminiTurns + ? validateGeminiTurns(repaired) + : repaired; + const revalidatedAnthropic = transcriptPolicy.validateAnthropicTurns + ? validateAnthropicTurns(revalidatedGemini) + : revalidatedGemini; + if (revalidatedAnthropic.length > 0) { + session.agent.replaceMessages(revalidatedAnthropic); } const result = await session.compact(params.customInstructions); // Estimate tokens after compaction by summing token estimates for remaining messages diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 622bdb7f4..89cffecd2 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -536,11 +536,20 @@ export async function runEmbeddedAttempt( validated, getDmHistoryLimitFromSessionKey(params.sessionKey, params.config), ); - // Fix: Repair tool_use/tool_result pairings AFTER truncation (issue #4367) - const repaired = sanitizeToolUseResultPairing(limited); - cacheTrace?.recordStage("session:limited", { messages: repaired }); - if (repaired.length > 0) { - activeSession.agent.replaceMessages(repaired); + // Fix: Repair tool_use/tool_result pairings AFTER truncation (issue #4367, #4650) + const repaired = transcriptPolicy.repairToolUseResultPairing + ? sanitizeToolUseResultPairing(limited) + : limited; + // Re-run turn validation after limiting (issue #4650) to merge consecutive assistant/user messages + const revalidatedGemini = transcriptPolicy.validateGeminiTurns + ? validateGeminiTurns(repaired) + : repaired; + const revalidatedAnthropic = transcriptPolicy.validateAnthropicTurns + ? validateAnthropicTurns(revalidatedGemini) + : revalidatedGemini; + cacheTrace?.recordStage("session:limited", { messages: revalidatedAnthropic }); + if (revalidatedAnthropic.length > 0) { + activeSession.agent.replaceMessages(revalidatedAnthropic); } } catch (err) { sessionManager.flushPendingToolResults?.();