Skip to content

fix(context-compressor): clamp out-of-range tail-cut and summary-scan bounds (#75588) - #75635

Closed
Ahmett101 wants to merge 1 commit into
NousResearch:mainfrom
Ahmett101:fix/75588-context-compressor-index-bounds
Closed

fix(context-compressor): clamp out-of-range tail-cut and summary-scan bounds (#75588)#75635
Ahmett101 wants to merge 1 commit into
NousResearch:mainfrom
Ahmett101:fix/75588-context-compressor-index-bounds

Conversation

@Ahmett101

Copy link
Copy Markdown
Contributor

Summary

A short conversation whose protected head / tool-group alignment reached the
end of the message list previously made _find_tail_cut_by_tokens() return
len(messages) + 1. compress() then passed that value into
_find_latest_context_summary() / _find_context_summaries(), which indexed
messages[idx] without clamping and raised IndexError, escaping the
compression path and failing the active gateway turn.

Three defensive bounds:

  1. _find_tail_cut_by_tokens() early-returns len(messages) when
    head_end >= len(messages) so the caller's existing
    compress_start >= compress_end no-op path fires.
  2. _find_context_summaries() clamps start/end to [0, len(messages)]
    and tolerates non-dict rows.
  3. _find_latest_context_summary() inherits the same bounds via the helper.

Changes

  • agent/context_compressor.py: three minimal bounds guards.
  • tests/agent/test_context_compressor_short_transcript_tail_index_75588.py:
    11 new regression tests pinning the post-fix invariants.

How to Test

PYTHONHASHSEED=0 ~/.hermes/hermes-agent/venv/bin/python -m pytest   tests/agent/test_context_compressor_short_transcript_tail_index_75588.py -v
# ✅ 11/11 passed

PYTHONHASHSEED=0 ~/.hermes/hermes-agent/venv/bin/python -m pytest   tests/agent/test_context_compressor.py   tests/agent/test_compressor_assistant_tail_anchor.py
# ✅ 114/114 passed (no regressions in adjacent compressor suites)

Checklist

  • Tests pass — 11/11 new + 114/114 existing
  • Follows Conventional Commits
  • Changes scoped to this fix only
  • Cross-platform impact assessed (Linux / macOS / WSL2 / Windows / Termux)
    — compressor is platform-neutral Python; no path/signal/process
    primitives touched.
  • profile-safe paths used — no paths touched.
  • .env not used for non-credential settings — no config touched.

Risk & Impact

Low. Three bounds guards inside an already-defensive compression path; no
behaviour change for any transcript that previously succeeded. The
head_end >= n early-return is strictly more conservative than the prior
max(cut_idx, head_end + 1) floor — it returns n instead of n + 1,
which is exactly the value the existing compress_start >= compress_end
guard expects.

Closes #75588

… bounds (NousResearch#75588)

A short conversation whose protected head / tool-group alignment reached the
end of the message list previously made `_find_tail_cut_by_tokens()` return
`len(messages) + 1`. `compress()` then passed that value into
`_find_latest_context_summary()` / `_find_context_summaries()`, which
indexed `messages[idx]` without clamping and raised `IndexError`,
escaping the compression path and failing the active gateway turn.

Three defensive bounds:

1. `_find_tail_cut_by_tokens()` now early-returns `len(messages)` when
   `head_end >= len(messages)` so the caller's existing
   `compress_start >= compress_end` no-op path fires (issue body shape:
   8-message transcript ending in tool-call/result group with protected
   head pushed to the end of the list).

2. `_find_context_summaries()` clamps `start`/`end` to `[0, len(messages)]`
   and tolerates non-dict rows so a stale `compress_end` from the tail-cut
   helper cannot index past the transcript.

3. `_find_latest_context_summary()` inherits the same defensive bounds via
   the helper above.

Regression coverage in
`tests/agent/test_context_compressor_short_transcript_tail_index_75588.py`:
- `compress()` on the exact 8-message shape returns without raising and
  takes the no-compressible-window path.
- `_find_tail_cut_by_tokens(messages, head_end=len(messages)) == len(messages)`.
- `_find_context_summaries` / `_find_latest_context_summary` clamp
  `end > len(messages)`, `start > end`, `start < 0`, and skip non-dict
  rows instead of raising.

No message is removed and no summary model is called when there is no
compressible window.

Closes NousResearch#75588
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/compression Context compression and continuation sessions sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state duplicate This issue or pull request already exists labels Jul 31, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #75604: both current patches repair the same short tool-tail out-of-range boundary that causes the #75588 IndexError.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused defensive fix. The helper-level premise remains valid on current main: agent/context_compressor.py:4940 can return len(messages) + 1, while agent/context_compressor.py:4172-4174 scans and indexes without bounding the range. The proposed guards address both.

Suggested changes

  • Add a regression for the remaining current-main public caller: ContextCompressor.has_content_to_compress() at agent/context_compressor.py:4953-4955, which the gateway checks at gateway/slash_commands.py:3975. For the reported fully protected shape it should return False. The new compress() assertions at tests/agent/test_context_compressor_short_transcript_tail_index_75588.py:115 already pass through the existing short-transcript return at agent/context_compressor.py:5028-5049, before the changed tail-cut path runs.

This is an automated hermes-sweeper review.

# compress() is the public entry point. It must never raise for a
# well-formed message list, even when the alignment pushes the
# protected head to ``len(messages)``.
result = c.compress(messages)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On current main this call returns through compress()'s existing short-transcript guard at agent/context_compressor.py:5028-5049, before _find_tail_cut_by_tokens() executes. Please add a has_content_to_compress(messages) is False assertion for this shape, since the gateway uses that preflight at gateway/slash_commands.py:3975 and it exercises the changed helper path.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 31, 2026
@Ahmett101 Ahmett101 closed this Jul 31, 2026
@Ahmett101
Ahmett101 deleted the fix/75588-context-compressor-index-bounds branch July 31, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compression Context compression and continuation sessions comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint duplicate This issue or pull request already exists P1 High — major feature broken, no workaround sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Short tool-only suffix can make context compressor scan past messages

3 participants