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
This commit is contained in:
spiceoogway 2026-01-30 10:46:54 -05:00
parent 41b2dd1f27
commit 22b5201d14
2 changed files with 28 additions and 7 deletions

View File

@ -35,6 +35,7 @@ import {
validateAnthropicTurns, validateAnthropicTurns,
validateGeminiTurns, validateGeminiTurns,
} from "../pi-embedded-helpers.js"; } from "../pi-embedded-helpers.js";
import { sanitizeToolUseResultPairing } from "../session-transcript-repair.js";
import { import {
ensurePiCompactionReserveTokens, ensurePiCompactionReserveTokens,
resolveCompactionReserveTokensFloor, resolveCompactionReserveTokensFloor,
@ -421,8 +422,19 @@ export async function compactEmbeddedPiSessionDirect(
validated, validated,
getDmHistoryLimitFromSessionKey(params.sessionKey, params.config), getDmHistoryLimitFromSessionKey(params.sessionKey, params.config),
); );
if (limited.length > 0) { // Fix: Repair tool_use/tool_result pairings AFTER truncation (issue #4650)
session.agent.replaceMessages(limited); 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); const result = await session.compact(params.customInstructions);
// Estimate tokens after compaction by summing token estimates for remaining messages // Estimate tokens after compaction by summing token estimates for remaining messages

View File

@ -536,11 +536,20 @@ export async function runEmbeddedAttempt(
validated, validated,
getDmHistoryLimitFromSessionKey(params.sessionKey, params.config), getDmHistoryLimitFromSessionKey(params.sessionKey, params.config),
); );
// Fix: Repair tool_use/tool_result pairings AFTER truncation (issue #4367) // Fix: Repair tool_use/tool_result pairings AFTER truncation (issue #4367, #4650)
const repaired = sanitizeToolUseResultPairing(limited); const repaired = transcriptPolicy.repairToolUseResultPairing
cacheTrace?.recordStage("session:limited", { messages: repaired }); ? sanitizeToolUseResultPairing(limited)
if (repaired.length > 0) { : limited;
activeSession.agent.replaceMessages(repaired); // 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) { } catch (err) {
sessionManager.flushPendingToolResults?.(); sessionManager.flushPendingToolResults?.();