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,
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

View File

@ -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?.();