Skip to content
UHPUHPDeveloper Guide
Independent developer guide. Not affiliated with HarnessRouter or the official UHP project.

Context Compaction Control in Agent Harnesses

Long-context agent reliability depends on more than deciding when to compact. A robust harness separately controls the compaction trigger, the amount of recent context protected after compaction, the continuity anchors that may exceed that budget, and the boundary between rewritten history and an already-dispatched tool call.

Page reviewed 4 Oct 2026 Source and review policy

Evidence: Hermes PRs #115986, #115987, #115988, #120187, #120494; current-main commits f27417fc, 2ad268b3, 3591bf3b, d79b8fb6Case-study protocol scope: 2026-09-12

Context compaction is a two-budget control problem plus a provenance boundary. The harness needs one policy for when compaction starts and another for how much recent material survives it. It must then preserve required conversational and tool-call continuity without pretending those requirements are hard token limits. Finally, any pruning or truncation applied to stored history must remain distinct from the payload of a tool call that was already dispatched for execution.

Hermes PRs #115986, #115987 and #115988, merged on 19 September 2026, provide a useful implementation example. PR #115986 places a default absolute ceiling of 256,000 tokens on the compression trigger for large-context models. PR #115987 prevents the recent-message count floor from expanding the optional protected tail beyond its 1.5× soft token ceiling. PR #115988 clarifies an important execution boundary: pressure pruning can rewrite the history copy of recent tool arguments/results that the model will read later, but it does not alter the arguments of an already-dispatched tool call.

The 19 September compaction changes are included in stable v2026.9.21 / Hermes Agent v0.21.4; the final reviewed merge 7f020b4ad70d6959a099b6c2ddf798bdcae63a7b is an ancestor of that tag. Commits f27417fc and 2ad268b3, PR #120187 / 3591bf3b, and PR #120494 / d79b8fb6, all merged on 23 September, close the later mid-turn re-anchoring, turn-start durability and delivered-reply continuity gaps described below. GitHub ancestry now confirms all four later fixes are included in stable v2026.9.24 / Hermes Agent v0.21.5 at tag commit f97608f1, so they are no longer current-main-only behavior. None of these changes revises UHP, MCP, ACP or A2A wire semantics, establishes native Hermes UHP adoption, or constitutes fresh HarnessRouter Hermes conformance evidence.

Classify an oversized tail before tuning the trigger

Section titled “Classify an oversized tail before tuning the trigger”

Reducing the trigger does not fix an oversized protected tail. Capture three measurements for one compaction: the effective trigger, optional retained tokens before continuity restoration, and final retained tokens after required anchors/tool groups are restored. The Hermes tail-budget change makes that distinction explicit.

ObservationLikely categoryUseful next check
Optional tail already exceeds its soft ceilingSelector/budget defectCount-floor widening and token estimator
Optional tail fits; final tail exceedsRequired continuity exceptionLast-user anchor and atomic tool group size
Diagnostics show a different trigger from executionPolicy derivation driftModel-switch preview versus installed compressor policy
Tool ran with full arguments but stored arguments shortenedHistory projection changedCompare dispatch evidence with compacted history

For example, with a 10K tail budget and 1.5× optional ceiling, a 28K required last-user anchor is a visible continuity exception; a 28K optional assistant block is a different defect. Keep those cases separate in metrics so a legitimate overrun cannot conceal unbounded optional retention.

Before changing defaults, replay a synthetic history containing a large user anchor, an oversized optional assistant message and a tool-call/result pair. Check both the resulting prompt and the exported preservation view. This is a proposed diagnostic fixture; the historical upstream measurements below remain unchanged.

Trigger budget: ratio alone is not enough for very large windows

Section titled “Trigger budget: ratio alone is not enough for very large windows”

A ratio-based trigger scales naturally with the model context window, but that can become undesirable once context windows reach hundreds of thousands or millions of tokens. A 50% threshold on a 1,000,000-token window waits until roughly 500,000 tokens before compaction begins. That can create very large repeated input prefixes, long recovery/replay surfaces and substantial token cost before the first compaction occurs.

PR #115986 changes Hermes’ shipped default compression.threshold_tokens from null to 256000. The effective trigger is now the lower of the ratio-derived threshold and the absolute cap. Explicit null keeps ratio-only behavior.

Conceptually:

effective_trigger = min(ratio_trigger, absolute_cap)

The important design property is first-fires-wins. The absolute cap should not delay a model whose proportional trigger is already lower. Hermes’ own examples retain a 96K trigger on a 128K window and about 204K on the referenced 272K Codex window, while a 1M window that previously reached 500K now compacts at 256K. Upstream characterizes the behavioral change as applying only once the normal ratio trigger rises above the cap, roughly above a 341K window under the referenced defaults.

This separates two operator intents that are easy to conflate:

  • a ratio expresses how much of a model’s available context the operator is willing to consume before compaction;
  • an absolute ceiling bounds worst-case context growth across models with very different windows.

A model switch must recompute both parts rather than carry forward a threshold derived for the previous model.

Diagnostics must report the trigger the runtime will actually use

Section titled “Diagnostics must report the trigger the runtime will actually use”

A safety or cost warning is misleading if it reports window × percentage while the runtime applies a lower cap, a model-specific override or a small-window floor. PR #115986 therefore routes the model-switch preview and the compressor’s installed threshold through one shared trigger derivation.

The general invariant is:

The number shown to an operator should be derived by the same policy function that installs the runtime limit.

This avoids a class of control-plane drift where configuration, warning text and actual runtime behavior each implement a slightly different formula. The same rule applies to admission limits, timeouts, retry budgets and authorization previews elsewhere in an agent host.

Tail budget: message count is not a token guarantee

Section titled “Tail budget: message count is not a token guarantee”

After compaction starts, a harness usually preserves a recent tail so the model retains the current conversation and tool context. A simple protect_last_n rule is not enough: eight recent messages can be tiny, or each can contain thousands of tokens.

Before PR #115987, Hermes’ lean-tail selector could calculate a token-bounded cut and then unconditionally widen the result to satisfy the recent-message count floor. In the reported failure, protected_tail_tokens reached 54,818 against a 10,000-token budget. That incompressible tail consumed about 40% of the compression trigger.

The fix makes the message-count floor opportunistic. If the newest optional rows fit under the tail’s 1.5× soft ceiling, the count floor can preserve them. If they do not, the token-bounded cut wins.

For a 10,000-token tail budget, that means optional rows are normally held to a 15,000-token soft ceiling. Upstream’s production-shaped probe moved one oversized-assistant case from 35,050 tokens to 14,020 tokens. This is a much stronger invariant than saying “keep the last eight messages,” because the retained-state cost is now bounded by tokens rather than message cardinality.

Continuity anchors can legitimately exceed the soft ceiling

Section titled “Continuity anchors can legitimately exceed the soft ceiling”

The 1.5× ceiling is deliberately not a hard limit over every retained byte. PR #115987 preserves required conversational continuity after the optional-tail cut. The last relevant user/assistant anchors, multi-user continuity rules and atomic tool-call groups may still force the protected tail above the nominal ceiling.

That exception is important. A hard token slicer that breaks a tool-call/result pair or removes the active user request can produce an internally inconsistent transcript even though it meets a numeric budget.

The safe hierarchy is therefore:

  1. bound optional recent history by tokens;
  2. preserve required conversation anchors;
  3. preserve tool-call groups atomically;
  4. make any resulting ceiling overrun observable rather than claiming the budget was absolute.

Upstream’s probe demonstrates the distinction: an oversized last-user anchor remained at 28,051 tokens against the 15K soft ceiling because continuity won by design. That is not the same failure as optional history expanding without a bound.

Pressure pruning may rewrite protected history

Section titled “Pressure pruning may rewrite protected history”

“Protected tail” is also easy to over-interpret. PR #115988 corrects Hermes documentation to state that its pressure pass may truncate tool-call arguments and demote tool results inside the protected tail when that tail alone exceeds its pressure budget. That pass runs as part of compaction independently of the proactive-pruning option described by the upstream docs.

The durable lesson is that a protection label needs a precise object and guarantee. A harness can protect a span from being summarized away while still allowing a later pressure stage to shorten large fields inside that span. Reviewers should ask:

  • Is the tail protected from deletion, from summarization, from field truncation, or from all three?
  • Which pass runs first when several budgets conflict?
  • Are truncation markers model-visible?
  • Can required atomic groups still be represented coherently after pressure reduction?

Without those answers, protect_last_n sounds stronger than the implementation actually is.

Rewritten history is not the same object as an executing tool call

Section titled “Rewritten history is not the same object as an executing tool call”

PR #115988 also records a particularly important harness boundary. Hermes executes a tool call from the provider response object before post-tool-result compaction rewrites the persisted/history representation. Therefore truncating arguments in the history copy does not retroactively modify the payload already delivered to the tool or delegated subagent.

This distinction is reusable beyond Hermes:

  • Execution object: the authoritative payload admitted for the current tool invocation.
  • History object: the representation later stored or reconstructed so the model can reason about what happened.
  • Replay object: a canonicalized representation used after restart/recovery, which must not silently redispatch an old side effect merely because a historical tool call is present.

A system that mutates one representation should not imply that the other two changed automatically. This is closely related to tool-call execution finality and session recovery: the model’s later transcript can be lossy or normalized while the execution record still needs enough provenance to say what actually ran.

The upstream documentation notes a separate model-level risk: once clipped JSON is visible in history, a model can imitate that clipped representation when constructing a new call. That is different from the original dispatched call having been altered in flight.

Mid-turn compaction must preserve the active-turn boundary

Section titled “Mid-turn compaction must preserve the active-turn boundary”

A second provenance failure appears when compaction happens during an active turn. Hermes splits the next provider request at current_turn_user_idx: everything before that index is treated as replayable historical prefix and canonicalized, while the active turn remains on the live side of the split. If compaction rebuilds messages but the index still points to its pre-compaction position, that split can land inside the current turn’s tool rows.

Commit f27417fc re-anchors the user boundary immediately after successful post-tool compression and carries the corrected index back through the tool-round result. Commit 2ad268b3 then hardens every request path by validating the index in prepare_iteration before a provider call. The index is accepted only when it still points to this turn’s user row, either verbatim or through Hermes’ user-originated view; otherwise the runtime re-anchors it.

The failure mode matters because it creates a persisted-versus-sent divergence without deleting the data from storage. With a stale split, prefix canonicalization can drop an assistant tool call whose result falls on the live side; the sanitizer then removes the now-orphaned result. state.db and in-memory history can still contain that tool round while the next live model request silently omits it. A later resumed session can therefore send a different history from the one used during the original live turn.

The reusable invariant is stronger than “compaction preserved the current user message”:

After any history rewrite, every positional boundary used to build the next model request must be revalidated against semantic identity before reuse.

This applies to user-turn indexes, tool-group spans, replay-prefix offsets and similar cursors. A compactor can preserve all required rows and still be incorrect if downstream indexes continue to address the old layout.

Turn-start compaction must distinguish carried rows from durable originals

Section titled “Turn-start compaction must distinguish carried rows from durable originals”

A separate failure occurs when compaction runs before the current turn has been fully persisted. Hermes in-place compaction keeps a carried tail and then rewinds over the newest tail_count durable database rows to mark the tail’s old copies as superseded. That rewind is only correct when every counted carried row actually has a durable original to match.

PR #120187 found that the classic CLI and gateway violate that assumption during turn-start preflight: the current user row is already present in the carried in-memory tail, but those surfaces persist it only after preflight. Counting that row in tail_count makes the positional rewind move one durable row too far and mark the newest summarized assistant row as a superseded duplicate. The row remains in storage but is set inactive and uncompacted, so it disappears from resume display and session_search. Upstream’s two-compaction reproduction hid A5 after the first turn-start compaction and A16 after the second. TUI/Desktop do not hit this exact path because they persist the user row at submit time.

Commit 3591bf3b narrows the rewind count instead of changing the archive model. While a session turn lease is actually held, current-turn tail rows with neither Hermes’ persisted marker nor an integer _row_id are excluded from tail_count. That covers the unpersisted user row and other unflushed scaffolding. The lease condition matters: between turns, a manual /compress or gateway pre-turn hygiene can see a stale turn anchor or a durable gateway transcript that is not marked the same way. Treating those rows as unpersisted would leave real carried originals unreconciled and reintroduce duplicate recall.

The reusable invariant is:

A positional durability rewind may count only carried items that have a corresponding durable original under the same lifecycle phase.

This is distinct from the mid-turn index bug above. The mid-turn fix protects the boundary used to construct the next model request after history has been rewritten. PR #120187 protects the mapping between the carried in-memory tail and durable archived rows during turn-start persistence. Both show why positional accounting needs semantic/lifecycle guards whenever persistence timing differs by host surface.

A delivered reply needs an active anchor across the compaction commit

Section titled “A delivered reply needs an active anchor across the compaction commit”

A third failure appears at the user-visible boundary. Hermes PR #120494 found that compaction can run immediately after an assistant reply has already been delivered to the user. If the compaction engine folds that reply into the new summary and _commit_compaction then marks the original durable row compacted, the reply disappears from the active transcript on Desktop reload and from the history sent on the next turn even though the user already saw it.

The fix treats the just-delivered reply as a continuity anchor, but it does not blindly append it. When the engine dropped the reply, Hermes tries to restore only that row at its original semantic position relative to messages that survived. Tool-call assistants are matched by call id and tool rows by tool_call_id, both through coalesce_tool_call_id; a normalized twin already retained by the engine counts as present, so the reply is not duplicated. If the position is ambiguous, if reused tool-call ids make the anchor unsafe, or if reinsertion would make the compacted transcript larger than it was before compaction, the runtime skips the repair and logs a warning instead of guessing. When reinsertion is safe, the corresponding durable reply row remains active.

The reusable invariant is:

A reply exposed to the user becomes a continuity anchor until a later transition explicitly and consistently supersedes it across display, model-sent and durable-history views.

This differs from the two earlier 23 September fixes. The mid-turn re-anchor protects the positional boundary used to construct the next provider request. PR #120187 protects the mapping between carried in-memory rows and durable originals during a rewind. PR #120494 protects an already observed assistant outcome across the compaction commit itself. Together they show that compaction correctness requires agreement among what the user saw, what storage marks active, and what the model receives next.

The PR also keeps a limitation explicit: when a mid-turn compaction drops the new user message while retaining the tool chain, the delivered reply is not recovered by this anchor logic; that case is logged rather than reconstructed speculatively.

Delegated agents need the same budget semantics

Section titled “Delegated agents need the same budget semantics”

Compaction policy becomes harder in a parent/child harness. PR #115986 updates Hermes’ delegation comments so child agents without a child-specific cap compact where the parent policy says they should: the lower of the ratio trigger and the global compression.threshold_tokens cap. A dedicated child cap can narrow that further.

This suggests a general rule for delegated runtimes: a child should not silently escape a global resource ceiling merely because its context window is larger. At the same time, a child-specific lower cap is a policy choice, not evidence that every child should always compact sooner than its parent.

The relevant coordinates should remain explicit: model window, ratio policy, global cap, optional child cap and the actual installed trigger.

The evidence here is source-pinned upstream validation, not an independent benchmark by this site.

  • PR #115986: upstream’s final review note reports 1,356 tests across the 39 files that mention the touched trigger symbols passing after trigger derivation was centralized. Its broader earlier sweep reported 3,614 passes and seven SQLite failures that the author says reproduced on untouched upstream main.
  • PR #115987: upstream reports 178 targeted tests across five tail/boundary/pressure files, plus a 40,000-transcript randomized differential probe with zero mismatches for the final one-pass refactor relative to the reviewed two-walk logic.
  • PR #115988: this is documentation/source-trace evidence clarifying the ordering and object boundary; it is not a new runtime algorithm or benchmark.
  • Commit f27417fc: adds the post-tool re-anchor and a focused regression proving that successful post-tool compression moves both the returned active-user index and _persist_user_message_idx to the rebuilt current-user row.
  • Commit 2ad268b3: centralizes a second semantic boundary check before every request. Its commit record says the failure was found by the compaction E2E suite’s persisted-prefix invariant on a seeded tool-heavy session crossing the trigger after a tool round. No independent site reproduction is claimed.
  • PR #120187 / commit 3591bf3b: upstream reports 3 focused tests passed in tests/agent/test_in_place_preflight_rewind.py, including CLI and gateway turn-start paths plus the between-turn manual-compress guard. The PR author also reports running the 75 test files that reference the affected compaction paths; all were green except two failures stated to reproduce on main in that environment (missing Anthropic SDK and one unrelated TUI-gateway model-options test). The author did not run the full suite, and this site does not treat that evidence as cross-platform certification.
  • PR #120494 / commit d79b8fb6: upstream reports 4 invariant tests covering 8 cases in tests/agent/test_compression_last_assistant_anchor.py, 57 tests across five compaction test files, and 88 tests across the rotation-state and concurrent-fork suites passing. A real SessionDB probe changed the reproduced reply_in_active result from 0 on the pre-fix main path to 1 with the fix while preserving the next-turn role sequence [system, user, assistant, user]; reverting the production files makes 4 of the 7 original anchor cases fail. The PR explicitly records the mid-turn dropped-user-message/tool-chain limitation above. This site does not claim an independent reproduction or cross-platform certification.

Stable Hermes is now v2026.9.24 / v0.21.5. The 19 September compaction changes were already included in v0.21.4; the 23 September re-anchoring, turn-start durability and delivered-reply fixes are included in v0.21.5. The cited commits are also ancestors of checked reviewed main snapshot at this review cutoff. The validation figures above remain upstream evidence for the specific changes; stable inclusion or current-main ancestry does not turn them into an independent site benchmark or production certification.

Context compaction is primarily host/runtime policy. UHP can expose task/session lifecycle, streaming, artifacts and errors; MCP can expose tools/resources; ACP can connect an editor/client to an agent; A2A can coordinate agents. None of those facts, by themselves, standardizes how a harness should choose a compression threshold, preserve its recent tail or rewrite its local transcript.

An adapter may surface token usage or a runtime error caused by context pressure, but that does not make a local compaction algorithm a protocol conformance rule. Keep Hermes integration evidence separate from UHP behavior and from any HarnessRouter validation claim.

When reviewing context compaction in an agent harness, verify:

  1. Trigger policy is explicit: ratio thresholds, absolute caps and model-specific overrides have defined precedence.
  2. Preview equals execution: warnings and diagnostics use the same trigger derivation as the runtime.
  3. Model switches recompute policy: a threshold derived for one window is not reused blindly for another.
  4. Tail limits are token-aware: message count alone cannot override the optional-tail token budget without a documented reason.
  5. Soft exceptions are named: required conversation anchors and atomic tool groups may exceed a soft ceiling, and that overrun is observable.
  6. Protection semantics are precise: “protected” says whether deletion, summarization and field truncation are each allowed.
  7. Execution and history are distinct: rewriting stored history cannot retroactively change the payload already dispatched to a tool.
  8. Active-turn boundaries are semantic, not positional: indexes or offsets retained across a rewrite are revalidated and re-anchored before the next model request.
  9. Persisted and sent histories agree on executed tool rounds: compaction cannot leave a tool call/result in durable state while silently omitting it from the next live request.
  10. Durable-tail rewinds count real originals only: an in-memory carried row that has not yet been persisted cannot consume one position in a rewind over durable superseded originals; lifecycle/surface differences must be explicit.
  11. Delivered replies remain live: a response already shown to the user cannot be compacted out of reload/display and next-turn model history while its durable row is simultaneously retired; ambiguous reconstruction must fail closed.
  12. Recovery is side-effect safe: canonicalized replay state must not cause old side effects to be redispatched accidentally.
  13. Delegation inherits bounded policy: child agents cannot escape an intended global ceiling without an explicit policy decision.
  14. Evidence stays versioned: behavior on the reviewed development snapshot is not described as stable-release or protocol behavior until primary sources establish that transition.