← Back to blog

The 25K Token Blind Spot: How Hermes Compression Was Underestimating Context by Half

hermescompressiontoken-optimizationdebugging
The 25K Token Blind Spot: How Hermes Compression Was Underestimating Context by Half

In late April, a Hermes user running a local model noticed something wrong. Their agent would trigger context compression, report success with a clean low token count, then keep running — but the next compression cycle wouldn't fire until the conversation had already exceeded the model's context window. The core loop was lying to itself about how many tokens it was actually using.

The root cause turned out to be a single measurement gap: after compression ran, the agent counted only message tokens and ignored the full tools schema. With 50+ tools registered, that blind spot was 20,000 to 30,000 tokens wide.

The Misreport

Context compression is Hermes's safety valve. When a conversation approaches the model's context limit, the agent summarizes and condenses the conversation history to free up room. It then stores the post-compression token count so it knows when the next cycle is needed.

The bug was in the compression function _compress_context() in run_agent.py. After compressing, the agent calculated its remaining context with:

_compressed_est = (
    estimate_tokens_rough(new_system_prompt)
    + estimate_messages_tokens_rough(compressed)
)
self.context_compressor.last_prompt_tokens = _compressed_est

The problem: estimate_messages_tokens_rough() only counts message content — the user's input and the agent's responses. It completely skips the tools schema that gets appended to every API call.

For comparison, the codebase already had a function that did this correctly:

def estimate_request_tokens_rough(messages, *, system_prompt="", tools=None):
    total_chars = 0
    if system_prompt:
        total_chars += len(system_prompt)
    if messages:
        total_chars += sum(len(str(msg)) for msg in messages)
    if tools:
        total_chars += len(str(tools))  # <-- The missing piece
    return (total_chars + 3) // 4

The tools schema line was already there — it just wasn't being called after compression.

The Cascade Failure

Here's how the math plays out for a 64K context model with a 70% compression threshold (44,800 tokens):

Step Reported (without fix) Actual
After compression 25,000 50,000
Compression threshold 44,800 44,800
Next compression triggers when reported hits 44,800 44,800
Actual context at trigger time ~69,800 44,800

The agent thinks it has 19,800 tokens of runway to the threshold. In reality, it's already 5,800 tokens past the context limit. The model starts dropping older messages, the conversation degrades, and the user has no indication anything is wrong.

One-Line Fix

The reporter, devilardis, submitted the fix as a one-line replacement in _compress_context():

# Before:
_compressed_est = (
    estimate_tokens_rough(new_system_prompt)
    + estimate_messages_tokens_rough(compressed)
)

# After:
_compressed_est = estimate_request_tokens_rough(
    compressed,
    system_prompt=new_system_prompt or "",
    tools=self.tools or None,
)

The function estimate_request_tokens_rough() was already imported in run_agent.py and used for pre-compression checks — it just wasn't being used for the post-compression state save. The fix brings the post-compression estimate in line with how the rest of the codebase already measures tokens.

Broader Implications

This bug existed because post-compression accounting used a different measurement function than pre-compression checks. The code had two token-counting paths that served the same purpose but produced different results. The gap wasn't obvious because on short conversations with few tools, the difference is small — it only becomes destructive when you have many tools and long-running sessions.

The 20-30K range comes from environments with 50+ tools enabled (standard for Hermes with skills, plugins, and MCP servers all loaded). If you run a minimal setup with only a few built-in tools, the blind spot is much smaller and you'd likely never hit the edge case. But the Hermes ecosystem encourages tool accumulation — plugins, MCP servers, community skills all add tool definitions to every request.

The fix landed in PR #18265, merged by Teknium (Nous Research founder) on May 1, 2026. It's in the current release.

What This Means for Self-Hosted Users

If you're running Hermes self-hosted and using context compression (enabled by default), this fix is already in main. The practical takeaway: if you've noticed conversations degrading after compression runs, especially with many tools and MCP servers loaded, this was likely the cause. The compression was working — the measurement of whether it was working was broken.

For those interested in the mechanics, the full issue at [#14695][^1] has the complete chain, and PR [#18265][^2] shows the exact diff. A follow-up at issue [#23902][^3] and PR [#23934][^4] further refined how compression state tracks provider-exact vs projected token counts, preventing a related class of overcorrection.

[^1]: devilardis. "BUG: Post-compression token estimate excludes tools schema, delaying next compression cycle." NousResearch/hermes-agent. April 23, 2026. [^2]: teknium1. "fix(compression): include system prompt + tool schemas in token estimates." NousResearch/hermes-agent. May 1, 2026. [^3]: A-CF0369. "Context compression fires prematurely — estimated token count overwrites precise API value." NousResearch/hermes-agent. May 11, 2026. [^4]: heathley. "fix(compression): separate provider-exact vs projected token state." NousResearch/hermes-agent. May 11, 2026.

Termagotchi
_

Ryan Underdown

Autodidact. Rarely listens to advice.

Follow on X @catamarammed or GitHub @underdown