Skip to content

Persist compression boundary summary for reload UI - #2004

Merged
1 commit merged into
nesquena:masterfrom
franksong2702:franksong2702/issue-1833-compression-boundary-marker-clean
May 10, 2026
Merged

Persist compression boundary summary for reload UI#2004
1 commit merged into
nesquena:masterfrom
franksong2702:franksong2702/issue-1833-compression-boundary-marker-clean

Conversation

@franksong2702

Copy link
Copy Markdown
Contributor

Summary

This PR resolves #1833.

  • Persist compression anchor metadata during manual /session/compress:
    • compression_anchor_summary
    • compression_anchor_visible_idx
    • compression_anchor_message_key
  • Persist equivalent compression-anchor metadata during auto-compression in streaming (run_conversation path), so the boundary marker survives session reloads.
  • Render compression reference cards from session.compression_anchor_summary when the raw context-compaction message is not present in S.messages.
  • Keep behavior unchanged for non-compression flows.

Testing

  • /.venv_test/bin/python -m pytest -q tests/test_sprint46.py tests/test_auto_compression_card.py
  • /.venv_test/bin/python -m py_compile api/routes.py api/streaming.py api/models.py
  • node --check static/ui.js

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Summary

Reading api/streaming.py:1539-1620, 3082-3093 on the PR branch + the matching _handle_session_compress block in api/routes.py:7505-7728, plus api/models.py:329-413, 575 for the persistence shape, plus static/ui.js:4753-4787 for the render path, the PR correctly closes the gap from #1833: compression-anchor metadata now persists across reloads on both manual /session/compress and the auto-compression path inside _run_agent_streaming.

Code reference

The persistence shape is symmetric across both paths:

# api/models.py:411-413 — METADATA_FIELDS now includes the new field
'compression_anchor_visible_idx', 'compression_anchor_message_key',
'compression_anchor_summary',

And _run_agent_streaming stamps all three immediately after the compression migration:

# api/streaming.py:3082-3093
if _compressed:
    visible_after = _visible_messages_for_compression_anchor(s.messages)
    s.compression_anchor_visible_idx = (
        max(0, len(visible_after) - 1) if visible_after else None
    )
    s.compression_anchor_message_key = (
        _compression_anchor_message_key(visible_after[-1]) if visible_after else None
    )
    s.compression_anchor_summary = _compact_summary_text(
        _compression_summary_from_messages(s.messages)
        or _compression_summary_from_messages(s.context_messages)
    )

The render path correctly falls back to the persisted summary when the raw context-compaction message is no longer in S.messages (the typical reload state):

// static/ui.js:4779-4784
const referenceText=referenceMessage
  ? msgContent(referenceMessage)||String(referenceMessage.content||'')
  : sessionCompressionSummary;
const referenceNode=(!compressionState && !!referenceText && (sessionCompressionAnchor!==null || sessionCompressionAnchorKey || sessionCompressionSummary))
  ? ...
  : null;

Diagnosis / recommendation

The change is well-scoped and the contract holds across the three layers (Session save/load, route response, render path). A few notes:

  1. Logic duplication between streaming.py and routes.py. Both files now have local copies of _compact_summary_text, _compression_summary_from_messages, _compression_anchor_message_key, and _visible_messages_for_compression_anchor. The streaming-side helpers (api/streaming.py:1539-1620) and the route-side helpers (api/routes.py:7505-7536 etc.) implement nearly the same logic with slightly different semantics — routes.py:_compression_summary_from_messages filters on role == "assistant" and matches "context compaction|compression" in the body, while streaming.py:_compression_summary_from_messages uses _is_context_compression_marker(m) (which has more aliases — see api/streaming.py:1527-1535). Worth consolidating to a single source of truth in a follow-up to avoid drift; the streaming-side variant is the more general one.

  2. Truncation limits differ across the two helpers. Streaming-side caps at 320 chars with ; routes-side also caps at 320 but uses a different ellipsis offset (txt[:314]… vs streaming's txt[: limit - 6]…). 6 vs 6 is the same in this case, but the duplication invites future drift.

  3. compression_anchor_summary == None path on the render side. static/ui.js:4763-4765 does String(...).trim() and falls back to ''. The reference card render path at :4781-4784 checks !!referenceText, which correctly excludes null/undefined/''. Good.

  4. Session.save() persists None for compression_anchor_summary. That's fine because the loader in Session.__init__ accepts None defaults at models.py:331. New sessions won't carry a stale summary on disk.

Test plan

tests/test_sprint46.py:test_session_compress_roundtrip is a strong pin — it walks the manual compress path, asserts the response payload contains the new field, and reloads via get_session() to confirm persistence. The tests/test_auto_compression_card.py:test_context_anchor_reference_uses_session_summary_fallback static check pins the static/ui.js render fallback. Together they cover both halves.

What's missing: a streaming-path test that drives _run_agent_streaming through compression and asserts s.compression_anchor_summary lands on disk (the equivalent for the auto-compression branch at streaming.py:3082-3093). The current test covers the manual path; the auto path is statically covered by the test file's existence but not behaviorally exercised. Worth a follow-up if not in scope here.

Cross-repo note

This is webui-side display-state only. The agent's compressor (agent/context_compressor.py in hermes-agent) emits the summary as the new context-compaction message at compression time; the webui captures it into compression_anchor_summary for reload survivability. No agent-side change required.

LGTM. Closes #1833 cleanly given the manual-path pin.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in a42adbe May 10, 2026
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 10, 2026
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ux): persistent in-conversation marker for compression boundaries

2 participants