Merge pull request #2915

This commit is contained in:
nesquena-hermes
2026-05-25 17:46:56 +00:00
2 changed files with 147 additions and 4 deletions
+49 -4
View File
@@ -5060,11 +5060,56 @@ def _run_agent_streaming(
# Notify the frontend that compression happened
if _compressed:
visible_after = visible_messages_for_anchor(s.messages, auto_compression=True)
s.compression_anchor_visible_idx = (
max(0, len(visible_after) - 1) if visible_after else None
)
# Find the LAST [CONTEXT COMPACTION] marker in s.messages
# and count visible messages before it. This is the correct
# anchor — it points to the compression boundary regardless
# of how many turns have been added since the boundary was
# established. Using len(visible_before)-1 is fragile when
# _previous_messages doesn't include markers or when extra
# messages accumulate between compression and the done event.
_last_marker_raw_idx = None
for _mi, _m in enumerate(s.messages):
if _is_context_compression_marker(_m):
_last_marker_raw_idx = _mi
if _last_marker_raw_idx is not None:
_visible_before_marker = visible_messages_for_anchor(
s.messages[:_last_marker_raw_idx], auto_compression=True,
)
s.compression_anchor_visible_idx = max(0, len(_visible_before_marker) - 1)
logger.info(
'[ANCHOR-MARKER] session=%s marker_raw=%d vis_before=%d anchor=%d',
getattr(s, 'session_id', '?'),
_last_marker_raw_idx,
len(_visible_before_marker),
s.compression_anchor_visible_idx,
)
else:
# Fallback: use pre-turn display messages
visible_before = visible_messages_for_anchor(
_previous_messages, auto_compression=True,
)
if visible_before:
s.compression_anchor_visible_idx = max(0, len(visible_before) - 1)
elif visible_after:
s.compression_anchor_visible_idx = 0
else:
s.compression_anchor_visible_idx = None
logger.info(
'[ANCHOR-FALLBACK] session=%s vis_before=%d anchor=%d',
getattr(s, 'session_id', '?'),
len(visible_before) if visible_before else 0,
s.compression_anchor_visible_idx if s.compression_anchor_visible_idx is not None else -1,
)
# Pick anchor_msg for _compression_anchor_message_key
_anchor_vis_idx = s.compression_anchor_visible_idx
if _anchor_vis_idx is not None and visible_after and _anchor_vis_idx < len(visible_after):
anchor_msg = visible_after[_anchor_vis_idx]
elif visible_after:
anchor_msg = visible_after[-1]
else:
anchor_msg = None
s.compression_anchor_message_key = (
_compression_anchor_message_key(visible_after[-1]) if visible_after else None
_compression_anchor_message_key(anchor_msg) if anchor_msg else None
)
s.compression_anchor_summary = _compact_summary_text(
_compression_summary_from_messages(s.messages)
@@ -97,3 +97,101 @@ def test_compression_summary_ignores_tool_output_that_mentions_compression():
assert _compression_summary_from_messages([marker, skill_tool_output]) == marker["content"]
assert _compression_summary_from_messages([skill_tool_output]) is None
def test_marker_based_anchor_calculation():
"""Verify that the marker-based anchor logic used in streaming.py's done
handler correctly computes compression_anchor_visible_idx from the last
[CONTEXT COMPACTION] marker position. This prevents the reference card
from being pushed behind the render window after subsequent turns."""
messages = [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "first reply"},
{"role": "tool", "content": "tool output"},
{"role": "user", "content": "second question"},
{"role": "assistant", "content": "second reply"},
{"role": "assistant", "content": "[CONTEXT COMPACTION \u2014 REFERENCE ONLY] summary"},
{"role": "user", "content": "third question"},
{"role": "assistant", "content": "third reply"},
{"role": "tool", "content": "more tool output"},
{"role": "user", "content": "fourth question"},
{"role": "assistant", "content": "fourth reply"},
]
# Simulate the fix logic from streaming.py
_last_marker_raw_idx = None
for _mi, _m in enumerate(messages):
if is_context_compression_marker(_m):
_last_marker_raw_idx = _mi
assert _last_marker_raw_idx == 5, "expected marker at index 5"
_visible_before_marker = visible_messages_for_anchor(
messages[:_last_marker_raw_idx], auto_compression=True,
)
anchor = max(0, len(_visible_before_marker) - 1)
# Visible before marker: 4 messages (hello, first reply, second question, second reply)
# tool at index 2 is filtered out. Anchor = 3 (last visible before marker)
assert anchor == 3, f"expected anchor 3, got {anchor}"
# Verify the anchor points to the right message
full_vis = visible_messages_for_anchor(messages, auto_compression=True)
# full_vis: hello, first reply, second question, second reply, third question, third reply, fourth question, fourth reply = 8
assert len(full_vis) == 8, f"expected 8, got {len(full_vis)}"
assert full_vis[anchor]["content"] == "second reply"
# After additional turns, anchor stays at marker boundary
messages_extended = messages + [
{"role": "user", "content": "fifth question"},
{"role": "assistant", "content": "fifth reply"},
{"role": "tool", "content": "hidden"},
]
full_vis_ext = visible_messages_for_anchor(messages_extended, auto_compression=True)
# The anchor should still point to the same position in full_vis_ext
assert full_vis_ext[anchor]["content"] == "second reply"
assert anchor < len(full_vis_ext) - 3 # anchor is not at the end
def test_marker_based_anchor_multiple_compressions():
"""With multiple compression markers, the anchor uses the LAST marker."""
messages = [
{"role": "user", "content": "A"},
{"role": "assistant", "content": "B"},
{"role": "assistant", "content": "[CONTEXT COMPACTION \u2014 REFERENCE ONLY] first"},
{"role": "user", "content": "C"},
{"role": "assistant", "content": "D"},
{"role": "assistant", "content": "[CONTEXT COMPACTION \u2014 REFERENCE ONLY] second"},
{"role": "user", "content": "E"},
{"role": "assistant", "content": "F"},
]
# Find last marker
_last_marker_raw_idx = None
for _mi, _m in enumerate(messages):
if is_context_compression_marker(_m):
_last_marker_raw_idx = _mi
assert _last_marker_raw_idx == 5
_visible_before_marker = visible_messages_for_anchor(
messages[:_last_marker_raw_idx], auto_compression=True,
)
anchor = max(0, len(_visible_before_marker) - 1)
# Visible before last marker at raw[5]: messages[0:5] = [A, B, marker, C, D]
# After filtering: A, B, C, D = 4 visible. Anchor = 3 (last visible = D)
assert anchor == 3, f"expected anchor 3, got {anchor}"
assert _visible_before_marker[anchor]["content"] == "D"
def test_marker_based_anchor_fallback_when_no_marker():
"""When no compression marker exists, fall back to old behavior."""
messages = [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "reply"},
]
_last_marker_raw_idx = None
for _mi, _m in enumerate(messages):
if is_context_compression_marker(_m):
_last_marker_raw_idx = _mi
assert _last_marker_raw_idx is None
# Should fall through (not crash) - verified by the old path
visible_after = visible_messages_for_anchor(messages, auto_compression=True)
assert len(visible_after) > 0