# How Save-Game Context Compaction Fixes Spec Drift in Multi-Step Coding Agents (-91.9% Input Tokens)

> 💡 **AI Labo Benchmark Series — Experiment `E12` (Ranked #6 out of 41 Patterns)**
>
> This post is part of our empirical benchmark series testing AI coding agent harness patterns inside network-isolated Proxmox VMs against a hidden 20-case grading suite.
> * 🚀 **[View All 41 Benchmark Results & Master Ranking](https://tappers.hashnode.dev/ai-agent-harness-patterns-benchmark)**
> * 🛡️ **[Read How Our Proxmox & systemd Anti-Cheating Sandbox Works](https://tappers.hashnode.dev/air-gapped-proxmox-systemd-agent-sandbox)**

---

## Metadata & Experiment Identity
* **Experiment ID:** `E12` (Backlog Candidate `C008`)
* **Run ID:** `e12-20261028-vm216-01`
* **Sandbox Environment:** Proxmox Isolated VM `VM216` (`ai-e12-216`, 0 NICs, `virtio-serial` QGA relay, `systemd` UID separation)
* **Model:** `gemini-3.8-flash` (`thinkingLevel: LOW`)
* **Pattern Under Test:** Save-Game Context Compaction (`/compact` at Milestones) — *"Unbounded History Accumulation vs. Milestone JSON State Compaction"*

---

## 1. Executive Summary (TL;DR)

When an AI coding agent tackles a multi-step engineering task across several modules, keeping every prior file dump, telemetry log, and intermediate code output inside a single growing `messages[]` array (**Unbounded Context Accumulation**) causes two compounding problems:

1. **Quadratic Token Inflation:** Every past turn's tool output is re-billed on every subsequent turn (`6,556` ➔ `11,769` ➔ `17,544` input tokens).
2. **Mid-Task Spec Drift (Lost-in-the-Middle):** By Turn 2 or Turn 3, fine-grained string normalization rules and boundary conditions buried in the middle of the history get drowned out by verbose telemetry logs—causing subtle bugs that break end-to-end integration tests.

To test the solution—**Milestone Context Compaction (the "Save-Game" Pattern)**—we benchmarked a 3-milestone power-grid telemetry and carbon-credit clearing pipeline (Milestone 1: `collector.py` ➔ Milestone 2: `clearing.py` ➔ Milestone 3: `dispatch.py`) inside isolated Proxmox VM `VM216`:

* **Mode A (`AccumulatedHistoryRunner`):** Appends all 3 milestone specifications, code outputs, and verbose telemetry audit dumps (`VERBOSE_TELEMETRY_AUDIT_DUMP`) into one continuous chat thread (`0` compactions).
* **Mode B (`MilestoneCompactionRunner`):** Immediately after each milestone passes its deterministic verification gate, executes a `/compact` reset—discarding raw logs and replacing the chat history with a **`120-token` structured JSON checkpoint (`SAVE_GAME_COMPACTION_V1`)** plus only the next milestone's specification.

### Key Result
Mode A's accumulated history (`35,869` cumulative input tokens) suffered a classic **Lost-in-the-Middle failure** on Milestone 2 (`clearing.py`), forgetting to apply `.strip().upper()` to `contract_tier` and failing **2 out of 20** hidden E2E tests (`18 / 20`, 90.0%). Mode B's **Save-Game Milestone Compaction** kept every turn under `1,400` input tokens (`2,922` cumulative input tokens, **`-91.9%` reduction**), cut API cost by **`-72.2%` (`$0.0340` ➔ `$0.0095`)**, and achieved a flawless **`20 / 20` (100%) pass**.

| Metric | Mode A: Accumulated History (`AccumulatedHistory`) | Mode B: Milestone State Compaction (`MilestoneCompaction`) | Delta / Improvement |
| :--- | :---: | :---: | :---: |
| **Hidden Grading Suite Score (20 Cases)** | `18 / 20` (90.0% — failed 2 E2E cases) | **`20 / 20` (100% Perfect Pass)** | **+10.0 pt (Zero spec drift)** |
| **Context Compactions (`compactions`)** | `0 compactions` | **`2 compactions` (After M1 & M2)** | Reset history at each milestone gate |
| **Turn 1 Input Tokens (M1: `collector.py`)** | `6,556 tokens` (`$0.00725`) | **`650 tokens` (`$0.00290`)** | **-90.1% reduction** |
| **Turn 2 Input Tokens (M2: `clearing.py`)** | `11,769 tokens` (`$0.01247`) | **`1,388 tokens` (`$0.00461`)** | **-88.2% reduction** |
| **Turn 3 Input Tokens (M3: `dispatch.py`)** | `17,544 tokens` (`$0.01429`) | **`884 tokens` (`$0.00195`)** | **-95.0% reduction on final turn** |
| **Cumulative Input Tokens (`promptTokenCount`)** | `35,869 tokens` | **`2,922 tokens`** | **-91.9% (12.28x token efficiency)** |
| **Cumulative Output Tokens (`candidatesTokenCount`)** | `1,894 tokens` | **`1,937 tokens`** | Equivalent code generation volume |
| **LLM Calls (`model_calls`)** | `3 calls` | **`3 calls` (1 per milestone)** | Identical call count |
| **Elapsed Execution Time (`elapsed_seconds`)** | `24.81 sec` | **`22.52 sec`** | **-9.2% (1.10x faster)** |
| **Estimated API Cost (`estimated_micro_usd`)** | `$0.0340` (`34,005 micro-USD`) | **`$0.0095` (`9,457 micro-USD`)** | **-72.2% (3.60x cheaper)** |

---

## 2. Experiment Architecture: Unbounded History vs. Save-Game Compaction

```text
+-------------------------------------------------------------------------+
| Proxmox Isolated VM216 (0 NICs, virtio-serial QGA, systemd UID split)   |
|  Target Task: 3-Milestone Power Grid & Carbon Clearing Pipeline         |
|               (M1: collector.py --> M2: clearing.py --> M3: dispatch.py)|
|                                                                         |
|  +-------------------------------------------------------------------+  |
|  | Mode A: AccumulatedHistoryRunner (No Compaction)                  |  |
|  |  - Turn 1 (M1): 6,556 in  --> collector.py PASS                   |  |
|  |  - Turn 2 (M2): 11,769 in --> Audit logs drown out M2 spec!       |  |
|  |                               Forgets contract_tier.strip().upper()|  |
|  |  - Turn 3 (M3): 17,544 in --> Cumulative 35,869 input tokens      |  |
|  |  --> Final Score: 18 / 20 (Crashes on lowercase/padded tiers!)    |  |
|  +-------------------------------------------------------------------+  |
|                                                                         |
|  +-------------------------------------------------------------------+  |
|  | Mode B: MilestoneCompactionRunner (Save-Game JSON Compaction)     |  |
|  |  - Turn 1 (M1): 650 in   --> collector.py PASS                    |  |
|  |  - [Compaction #1]: Discards raw logs; writes 120-tok JSON state  |  |
|  |  - Turn 2 (M2): 1,388 in --> Fresh context nails all M2 rules!    |  |
|  |  - [Compaction #2]: Updates verified_contracts in JSON checkpoint |  |
|  |  - Turn 3 (M3): 884 in   --> Cumulative 2,922 input tokens (-91.9%)| |
|  |  --> Final Score: 20 / 20 (100% Perfect Pass)                     |  |
|  +-------------------------------------------------------------------+  |
+-------------------------------------------------------------------------+
```

---

## 3. Code-Level Postmortem: How Unbounded History Caused a Lost-in-the-Middle Bug

### 3.1 Mode A (`AccumulatedHistory`): The Missing `.strip().upper()` on Turn 2

Inspecting `accum-bundle.json` (Mode A's final submitted code) reveals a textbook example of **Attention Dilution**:

* On **Turn 1 (`collector.py`)**, Mode A properly implemented `.strip().upper()` normalization for `station_id` and `grid_zone`.
* By **Turn 2 (`clearing.py`)**, however, 45 lines of `VERBOSE_TELEMETRY_AUDIT_DUMP_M1` plus the full Turn 1 code had bloated the prompt to **`11,769 tokens`**. Buried in the middle of the prompt was the requirement that `clearing.py` must also normalize `contract_tier` via `.strip().upper()`.
* Mode A completely missed that normalization rule in `clearing.py`:

```python
# ❌ Mode A (accum-bundle.json — clearing.py generated at 11,769 input tokens)
def settle_grid(readings, contract_tier="BASE", credit_offset=0):
    if not isinstance(readings, list) or len(readings) == 0:
        raise ValueError("readings must be a non-empty list")
    # BUG: Forgot isinstance(contract_tier, str) and contract_tier.strip().upper()!
    if contract_tier not in ALLOWED_TIERS:
        raise ValueError(f"contract_tier must be one of {ALLOWED_TIERS}")
```

Because of that single omitted `.strip().upper()`, Mode A crashed with a `ValueError` on two hidden E2E integration tests that passed padded/lowercase tier strings (`contract_tier=" green "` in `e2e_01` and `contract_tier="hydro"` in `e2e_02`).

### 3.2 Mode B (`MilestoneCompaction`): The Exact Save-Game JSON Checkpoint

In Mode B, as soon as Milestone 1 (`collector.py`) passed verification, the harness wiped the raw conversation history and started Turn 2 with this compact **`SAVE_GAME_COMPACTION_V1` JSON checkpoint**:

```json
{
  "checkpoint_type": "SAVE_GAME_COMPACTION_V1",
  "completed_modules": ["collector.py"],
  "history_compacted": true,
  "milestone_completed": 1,
  "next_target_module": "clearing.py",
  "verified_contracts": {
    "collector.collect_readings": "PASS (returns list of dicts with station_id, grid_zone in NORTH/SOUTH/WEST, mwh_units>0, rate_cents>=0, 0<=carbon_bps<=1500, non-mutating)"
  }
}
```

Because Turn 2 ran in a fresh **`1,388-token`** context containing only the verified upstream contract and Milestone 2's specification, Mode B had zero attention dilution—properly implementing `clean_tier = contract_tier.strip().upper()` and passing all **20 / 20** test cases:

| Test Case Group | Verification Target | Mode A (`Accumulated`) | Mode B (`Save-Game Compact`) |
| :--- | :--- | :---: | :---: |
| `collector_01` – `collector_06` | Milestone 1: Non-mutation, type checks, bounds (`0..1500` bps) | `6 / 6` PASS | **`6 / 6` PASS** |
| `clearing_01` – `clearing_08` | Milestone 2: First-seen order, `400 vs 401` bps levy, `12000` tier boundary | `8 / 8` PASS | **`8 / 8` PASS** |
| `e2e_01_green_below_12000_minus_zero_and_at_separator` | **E2E: Normalize `contract_tier=" green "` + `@` delimiter + `-0`** | **❌ FAIL (`ValueError`)** | **✅ PASS** |
| `e2e_02_hydro_levy_and_credit_dispatch` | **E2E: Normalize `contract_tier="hydro"` + 20% discount + credit offset** | **❌ FAIL (`ValueError`)** | **✅ PASS** |
| `e2e_03` – `e2e_06` | E2E: Zero rate cents, line order preservation, cross-stage error propagation | `4 / 4` PASS | **`4 / 4` PASS** |
| **Total** | **20 Hidden Cases** | **`18 / 20` (90.0%)** | **`20 / 20` (100%)** |

---

## 4. Practical Takeaway: Compact to Structured State, Not Prose Summaries

When building multi-step agent workflows (or using `/compact` in interactive coding agents):

1. **Don't wait until the 200k context window is 95% full to compact:**
   Attention degradation (`Lost-in-the-Middle`) starts happening well above 10,000–15,000 tokens of noisy tool logs. Compact **at every milestone boundary** as soon as a module's tests turn green.
2. **Use deterministic JSON checkpoints instead of free-form prose summaries:**
   As we saw in `E05` (Subagent Folding), natural-language summaries often drop exact type contracts. Instead, record a structured JSON object containing `completed_modules`, `verified_contracts` (exact function signatures & invariants), and `next_target_module`.

---

### 🔗 Continue Reading the Series
* 🚀 **Master Benchmark Hub (All 41 Patterns Ranked):** [I Benchmarked 41 AI Agent Harness Patterns in Isolated VMs: How to Cut Tokens by 90% Without Losing Accuracy](https://tappers.hashnode.dev/ai-agent-harness-patterns-benchmark)
* 🛡️ **Methodology & Sandbox Architecture:** [How to Benchmark AI Agents Without Eval Cheating or API Key Leaks: Designing a Proxmox & systemd Sandbox](https://tappers.hashnode.dev/air-gapped-proxmox-systemd-agent-sandbox)
* 🥇 **Rank #1 Deep Dive (`E20`):** [Stop Your Coding Agent From Stepping on the Same Rake Twice: Cold-Start Exploration vs. a 5-Item `MEMORY.md`](https://tappers.hashnode.dev/e20-episodic-gotchas-memory)
* 🥈 **Rank #2 Deep Dive (`E21`):** [Why Feeding Full Dependency Files to Your Coding Agent Hurts Accuracy: Full Source vs. Tree-sitter Interface Skeletons](https://tappers.hashnode.dev/e21-treesitter-skeleton-context)
* 🥉 **Rank #3 Deep Dive (`E16`):** [Stop Dumping Raw Pytest Logs Into Your Coding Agent: How a Structured JSON Test Filter Cuts Input Tokens by 94.1%](https://tappers.hashnode.dev/e16-cli-test-output-token-killer)
* 🌿 **Rank #4 Deep Dive (`E19`):** [Why Keeping Failed Code Attempts in Chat History Poisons Your Coding Agent: Error-Trace Pruning vs. Accumulated History](https://tappers.hashnode.dev/e19-error-trace-pruning)
* 🔍 **Rank #5 Deep Dive (`E14`):** [The Hidden "Schema Tax" of MCP Tools: Dumping 32 Tool Schemas vs. On-Demand Tool Search (-92.7% Input Tokens)](https://tappers.hashnode.dev/e14-progressive-tool-search)
