Compaction Ate the Tool Result Your Model Was Waiting For


You ask the agent to run the integration suite. It does — thousands of lines of output scroll past. Then you ask which tests failed, and it tells you it only saw a one-line placeholder, and the screenshots it captured along the way are gone too. Nothing failed loudly. The command really did run; the result really did land in the conversation. It just never survived the trip from “the tool finished” to “the model read it”. Three fixes merged on 2026-09-26 (PRs #124208, #124207, #124231) close that gap, and this is what they protect — and what they deliberately still do not.

What “mid-turn compaction” actually is

A long session fills its context window eventually. Hermes handles that with compaction: older history gets replaced by a summary so the conversation can keep going. Most of the time compaction happens between turns, and you never notice.

The dangerous moment is a different one — the preparation step before a request goes out (the code calls it preflight). Right then, the context holds a tool result that has just arrived and has not been read yet. The command has executed, the output is sitting in the transcript, and the model’s next request has not been sent.

The old implementation treated that round like any other history: the pressure pass demoted “all but the newest”, the image-retirement logic ignored the pending round entirely, and the tail cut walked down to a floor of zero. A compaction firing in that window could reduce the result to a one-line stub, or retire its images. The model woke up with nothing to read.

Fix one: the round you are waiting for is spared verbatim (PR #124208)

The fix centres on a new helper, _pending_tool_round, which finds the trailing run of tool results — and skips the contiguous /steer rows that can sit between a result and the model’s read when you send a course correction mid-run. Find that round, and it is now exempt in four places:

Stage What used to happen What happens now
Pass 2 (pressure pass) The round was demoted to a stub or summary Kept verbatim
Image retirement (both scans) Only the newest image survived All images in a multi-image round survive
Tail cut Walk floor of 0, so it could be cut away The round is out of reach
Pass 3 (argument truncation) The issuing assistant row could be cut The owning tool_calls row is spared too

One boundary condition is worth knowing: a round larger than 20% of the effective input window is not spared. That is deliberate — an oversized result still takes the original overflow escape hatch, because otherwise compaction would fail exactly when it is most needed.

Fix two: every model-visible elision goes through the guarded marker (PR #124207)

Truncation used to be announced plainly: ...[truncated]. The problem is that Hermes keeps a guard at the tool-dispatch boundary (_COMPRESSION_MARKER_RE in agent/tool_dispatch_helpers.py) whose whole job is to catch a model that copies an elision marker into a file write. It only recognises the special marker. It cannot see ...[truncated].

The consequence is not a cosmetic log line; it is the data-loss class. A model that copies text it never fully saw, marker and all, into write_file or patch gets waved through, and the file is written wrong.

Every model-visible truncation now goes through the non-imitable marker ⟪HERMES-CONTEXT-COMPRESSION: …⟫: the compressor itself, skill inline-shell output, the LSP reporter, the verification stop, memory context, and the agent-facing Slack serializer and trajectory compressor. A few sites under tools/* remain open — the PR says so rather than pretending otherwise.

Fix three: the “unchanged” that wasn’t (PR #124231)

read_file and skill_view both keep a dedup cache: read the same file a second time and you get a cheap “unchanged” instead of the body. That is a good trade — until the cache and the transcript disagree.

Three rewrite paths drop tool-result bodies without resetting it:

  • post-turn micro-compaction;
  • a replayable native Responses checkpoint;
  • Codex app-server compaction.

The model then has neither the body (dropped) nor the content (“unchanged” implies it still has it). All three now reset the caches; micro-compaction only resets when it actually rewrote the transcript, so a no-op pass does not throw away a still-valid cache.

What you should do

  • The three PRs merged on 2026-09-26, after the v0.21.5 release tag, so they live on main. hermes update gets them; a deployment pinned to tags waits for the next one.
  • No configuration changes. There is no new switch here, only corrected behaviour.
  • On a long batch session, the tell-tale symptom was the model claiming it never saw an output the transcript shows it received. That contradiction should stop happening.

The principle underneath

Mid-turn compaction moved from “demote everything but the newest” to “never touch the round the model has not read”. The elision marker stopped being a per-site string and became one guarded token. The read cache was re-aligned with reality. All three point at the same rule: compaction may sacrifice old information, never information that is in use. For the surrounding picture, start with one compaction request instead of nineteen for the speed story, what forty rounds of compaction do to a session for the measured numbers, and the lean-tail trade-off for what gets sacrificed on purpose. The rest of this window is on the v0.21.5 release page.