NousResearch/hermes-agent

Context compression summary leaking into visible chat; end-marker changes

Cluster 33256 · agent. Data as of

Nothing open on GitHub. Fix merged in #45153 on 12 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #45153. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #45153. That is separate from the fix event.
Still open
Nothing open on GitHub.

One issue reports that a context compression summary appears in user-visible chat output. Two pull requests with the same title propose appending an "END OF CONTEXT SUMMARY" marker to standalone summaries; the titles alone do not show whether that marker addresses the reported leak.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #45153 fix(compression): append END OF CONTEXT SUMMARY marker to all standalone summaries (salvages #33346)

    merged to main

    files: agent/context_compressor.py, tests/agent/test_context_compressor.py

    Show the diff
    • agent/context_compressor.py
      @@ -69,6 +69,16 @@
       )
       LEGACY_SUMMARY_PREFIX = "[CONTEXT SUMMARY]:"
       
      +# Appended to every standalone summary message (and to the merged-into-tail
      +# prefix) so the model has an unambiguous "summary ends here" boundary.
      +# Without it, weak models read the verbatim "## Active Task" quote as fresh
      +# user input (#11475, #14521) or regurgitate an assistant-role summary as
      +# their own output (#33256).
      +_SUMMARY_END_MARKER = (
      +    "--- END OF CONTEXT SUMMARY — "
      +    "respond to the message below, not the summary above ---"
      +)
      +
       # Handoff prefixes that shipped in earlier releases. A summary persisted under
       # one of these can be inherited into a resumed lineage (#35344); when it is
       # re-normalized on re-compaction we must strip the OLD prefix too, otherwise the
      @@ -1616,7 +1626,13 @@ def _strip_summary_prefix(summary: str) -> str:
               text = (summary or "").strip()
               for prefix in (SUMMARY_PREFIX, LEGACY_SUMMARY_PREFIX, *_HISTORICAL_SUMMARY_PREFIXES):
                   if text.startswith(prefix):
      -                return text[len(prefix):].lstrip()
      +                text = text[len(prefix):].lstrip()
      +                break
      +        # Strip the trailing end marker too — a rehydrated handoff body that
      +        # keeps it would leak the boundary directive into the iterative-update
      +        # summarizer prompt (and the marker is re-appended on insertion anyway).
      +        if text.endswith(_SUMMARY_END_MARKER):
      +            text = text[: -len(_SUMMARY_END_MARKER)].rstrip()
               return text
       
           @classmethod
      @@ -2193,15 +2209,13 @@ def compress(self, messages: List[Dict[str, Any]], current_tokens: int = None, f
       
               # When the summary lands as a standalone role="user" message,
               # weak models read the verbatim "## Active Task" quote of a past
      -        # user request as fresh input (#11475, #14521). Append the explicit
      -        # end marker — the same one used in the merge-into-tail path — so
      -        # the model has a clear "summary above, not new input" signal.
      -        if not _merge_summary_into_tail and summary_role == "user":
      -            summary = (
      -                summary
      -                + "\n\n--- END OF CONTEXT SUMMARY — "
      -                "respond to the message below, not the summary above ---"
      -            )
      +        # user request as fresh input (#11475, #14521).
      +        # When it lands as role="assistant", models may regurgitate the
      +        # summary text as their own output (#33256). In both cases, append
      +        # the explicit end marker so the model has a clear "summary ends
      +        # here, respond to the message below" signal.
      +        if not _merge_summary_into_tail:
      +            summary = summary + "\n\n" + _SUMMARY_END_MARKER
       
               if not _merge_summary_into_tail:
                   compressed.append({"role": summary_role, "content": summary})
      @@ -2209,11 +2223,7 @@ def compress(self, messages: List[Dict[str, Any]], current_tokens: int = None, f
               for i in range(compress_end, n_messages):
                   msg = messages[i].copy()
                   if _merge_summary_into_tail and i == compress_end:
      -                merged_prefix = (
      -                    summary
      -                    + "\n\n--- END OF CONTEXT SUMMARY — "
      -                    "respond to the message below, not the summary above ---\n\n"
      -                )
      +                merged_prefix = summary + "\n\n" + _SUMMARY_END_MARKER + "\n\n"
                       msg["content"] = _append_text_to_content(
                           msg.get("content"),
                           merged_prefix,
    • tests/agent/test_context_compressor.py
      @@ -1252,6 +1252,48 @@ def test_user_role_summary_carries_end_marker(self):
                   "respond to the message below, not the summary above ---"
               )
       
      +    def test_assistant_role_summary_carries_end_marker(self):
      +        """When the summary lands as standalone role='assistant' (head ends
      +        with user), the message body must include the explicit
      +        '--- END OF CONTEXT SUMMARY ---' marker. Without it, models may
      +        regurgitate the summary text as their own output (#33256).
      +        """
      +        mock_client = MagicMock()
      +        mock_response = MagicMock()
      +        mock_response.choices = [MagicMock()]
      +        mock_response.choices[0].message.content = "[CONTEXT SUMMARY]: stuff happened"
      +        mock_client.chat.completions.create.return_value = mock_response
      +
      +        with patch("agent.context_compressor.get_model_context_length", return_value=100000):
      +            c = ContextCompressor(model="test", quiet_mode=True, protect_first_n=2, protect_last_n=2)
      +
      +        # head_last=user → summary_role="assistant" (same setup as
      +        # test_summary_role_avoids_consecutive_user_when_head_ends_with_user).
      +        # With min_tail=3, tail = last 3 messages (indices 5-7).
      +        # head_last=user, tail_first=user → the assistant-role summary does
      +        # not collide with either neighbor and should be inserted standalone.
      +        msgs = [
      +            {"role": "system", "content": "system prompt"},
      +            {"role": "user", "content": "msg 1"},
      +            {"role": "user", "content": "msg 2"},  # last head — user
      +            {"role": "assistant", "content": "msg 3"},
      +            {"role": "user", "content": "msg 4"},
      +            {"role": "user", "content": "msg 5"},
      +            {"role": "assistant", "content": "msg 6"},
      +            {"role": "user", "content": "msg 7"},
      +        ]
      +        with patch("agent.context_compressor.call_llm", return_value=mock_response):
      +            result = c.compress(msgs)
      +
      +        summary_msg = next(
      +            m for m in result if (m.get("content") or "").startswith(SUMMARY_PREFIX)
      +        )
      +        assert summary_msg["role"] == "assistant"
      +        assert "END OF CONTEXT SUMMARY" in summary_msg["content"]
      +        assert summary_msg["content"].rstrip().endswith(
      +            "respond to the message below, not the summary above ---"
      +        )
      +
           def test_summary_role_avoids_consecutive_user_messages(self):
               """Summary role should alternate with the last head message to avoid consecutive same-role messages."""
               mock_client = MagicMock()

1 PR closed without merging

From the close comments: merged via another change 1

  • #33346 fix(compression): append END OF CONTEXT SUMMARY marker to all standalone summaries (#33256) · merged via another change → 749b721, #45153
Closed issues · 1
  • Closed#33256Context compression summary leaks into user-visible chat output
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#45153
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
12 Jun 2026

For your agent

The same facts, typed.