fix(api): preserve compaction summaries at any history position during Responses auto-truncation

The gateway /compress path can force a user-leading layout that leaves
the compaction summary after a retained system head, so scanning only
the leading block misses it. Preserve marker-carrying messages wherever
they sit, filling the remaining budget with the most recent other
messages, and cover the non-leading position with a test.
This commit is contained in:
Teknium 2026-07-22 04:36:32 -07:00
parent 3807df7a06
commit 28b6bd6570
2 changed files with 72 additions and 16 deletions

View file

@ -200,28 +200,37 @@ def _auto_truncate_response_history(
*,
limit: int = RESPONSES_AUTO_TRUNCATION_HISTORY_LIMIT,
) -> List[Dict[str, Any]]:
"""Keep recent Responses history without dropping the compaction handoff."""
"""Keep recent Responses history without dropping the compaction handoff.
Compaction summaries are preserved wherever they sit in the history
the gateway /compress path can leave them after a retained system head
(see ``context_compressor`` force-user-leading handling), so a
leading-block-only scan would silently drop them.
"""
if limit <= 0 or len(conversation_history) <= limit:
return conversation_history
leading_summaries: List[Dict[str, Any]] = []
first_conversation_index = 0
for message in conversation_history:
if not _is_compressed_summary_message(message):
break
leading_summaries.append(message)
first_conversation_index += 1
if not leading_summaries:
summary_indices = [
index
for index, message in enumerate(conversation_history)
if _is_compressed_summary_message(message)
]
if not summary_indices:
return conversation_history[-limit:]
preserved = leading_summaries[:limit]
remaining = limit - len(preserved)
if remaining <= 0:
return preserved
kept_indices = set(summary_indices[:limit])
remaining = limit - len(kept_indices)
if remaining > 0:
summary_index_set = set(summary_indices)
for index in range(len(conversation_history) - 1, -1, -1):
if index in summary_index_set:
continue
kept_indices.add(index)
remaining -= 1
if remaining <= 0:
break
recent_tail = conversation_history[first_conversation_index:][-remaining:]
return preserved + recent_tail
return [conversation_history[index] for index in sorted(kept_indices)]
def _normalize_chat_content(

View file

@ -3580,6 +3580,53 @@ class TestTruncation:
assert history[1]["content"] == "msg 50"
assert history[-1]["content"] == "msg 148"
@pytest.mark.asyncio
async def test_truncation_auto_preserves_non_leading_compaction_summary(self, adapter):
"""A summary sitting after a retained system head must survive too.
The gateway /compress path can force a user-leading layout that
leaves the compaction summary after a kept system message, so the
preservation predicate must not assume the summary is at index 0.
"""
mock_result = {"final_response": "OK", "messages": [], "api_calls": 1}
system_head = {"role": "system", "content": "You are a helpful agent."}
summary = {
"role": "user",
"content": "[CONTEXT COMPACTION — REFERENCE ONLY]\nEarlier work.",
"_compressed_summary": True,
}
long_history = [system_head, summary] + [
{"role": "user", "content": f"msg {i}"}
for i in range(148)
]
adapter._response_store.put("resp_summary_mid", {
"response": {"id": "resp_summary_mid", "object": "response"},
"conversation_history": long_history,
"instructions": None,
})
app = _create_app(adapter)
async with TestClient(TestServer(app)) as cli:
with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run:
mock_run.return_value = (mock_result, {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0})
resp = await cli.post(
"/v1/responses",
json={
"model": "hermes-agent",
"input": "follow up",
"previous_response_id": "resp_summary_mid",
"truncation": "auto",
},
)
assert resp.status == 200
history = mock_run.call_args.kwargs["conversation_history"]
assert len(history) == 100
assert history[0] == summary
assert history[1]["content"] == "msg 49"
assert history[-1]["content"] == "msg 147"
@pytest.mark.asyncio
async def test_no_truncation_keeps_full_history(self, adapter):
"""Without truncation=auto, long history is passed as-is."""