Preserve compacted context in provider history
Store the replacement summary as non-assistant context so provider normalization does not discard it as an orphaned assistant turn. Cover the compaction-to-history contract with a regression test.
Alessandro committed
Aug 23, 2026 at 14:15 UTC
52ec111d12af65cc0f4b5a02ed05e9549876925f
3 files changed
+13
-3
plugins/_chat_compaction/AGENTS.md
+1
@@ -19,6 +19,7 @@
19
- Backup JSON and transcript artifacts must remain UTF-8 writable when chat content contains malformed Unicode such as lone surrogates.
20
- Keep generated summaries bounded by configured model and token limits.
21
- Compacted summaries must be resumable task state: preserve the latest request, authorization boundaries, decisions, evidence, modified artifacts, pending jobs and their IDs, the next executable step, blockers, and checks not run.
22
+- Store the compacted summary as non-assistant history context so provider-history normalization cannot discard it as an orphaned assistant turn.
23
- Preserve loaded skill names from `skill_instructions` metadata without copying full skill bodies into compacted summaries.
24
- Preserve only secret references such as names, aliases, purposes, or storage locations; never preserve secret values.
25
- Clear the cached context window after replacing history so stale transcript content is not persisted as active resumable state; the cache rebuilds on the next model turn.
plugins/_chat_compaction/helpers/compactor.py
+3
-2
@@ -86,7 +86,7 @@ async def run_compaction(
86
2. Estimates token count and checks against model context window
87
3. If needed, splits history and summarizes iteratively
88
4. Calls the LLM to generate a comprehensive summary
89
- 5. Replaces the history with a single AI message containing the summary
89
+ 5. Replaces the history with a single context message containing the summary
90
6. Resets the log and creates a response log item
91
7. Persists the changes
92
@@ -144,7 +144,8 @@ async def run_compaction(
144
compacted_content = f"## Context compacted\n\n{summary}{backup_note}"
145
146
agent.history = History(agent=agent)
147
- agent.history.add_message(ai=True, content=compacted_content)
147
+ # History summaries are context, not orphaned assistant turns.
148
+ agent.history.add_message(ai=False, content=compacted_content)
149
clear_responses_provider_state(agent)
150
agent.data.pop(Agent.DATA_NAME_CTX_WINDOW, None)
151
tests/test_chat_compaction.py
+9
-1
@@ -3,6 +3,7 @@ from types import SimpleNamespace
3
from pathlib import Path
4
5
import pytest
6
+from langchain_core.messages import HumanMessage
7
8
PROJECT_ROOT = Path(__file__).resolve().parents[1]
9
if str(PROJECT_ROOT) not in sys.path:
@@ -189,7 +190,9 @@ async def test_large_compaction_does_not_send_unsplit_single_line_payload(monkey
190
191
192
@pytest.mark.asyncio
192
-async def test_manual_compaction_clears_active_responses_state(monkeypatch):
193
+async def test_manual_compaction_preserves_summary_and_clears_responses_state(
194
+ monkeypatch,
195
+):
196
async def fake_single_pass(*args, **kwargs):
197
return "summary"
198
@@ -218,6 +221,11 @@ async def test_manual_compaction_clears_active_responses_state(monkeypatch):
221
222
await compactor.run_compaction(context)
223
224
+ prompt_history = agent.history.current.output_langchain()
225
+ assert len(prompt_history) == 1
226
+ assert isinstance(prompt_history[0], HumanMessage)
227
+ assert "## Context compacted\n\nsummary" in prompt_history[0].content
228
+
229
state = agent.data["responses_state"]
230
assert "response_id" not in state
231
assert "previous_response_id" not in state