fix(session_search): cap per-message content on the read shape
#69334 capped discovery bookends (1200 chars) and scroll windows (4000) via _shape_message(max_content_len=...) but _read_session still shaped rows with no cap, so one archived tool result stored as a message came back whole - a single read returned 74K chars and took a request from ~50K to ~89K tokens in one step. Cap read-shape messages at 2000 chars with the same content_truncated / original_content_chars metadata, and add a regression test. Fixes #114344
This commit is contained in:
@@ -19,6 +19,7 @@ from hermes_state import SessionDB
|
||||
from tools.session_search_tool import (
|
||||
SESSION_SEARCH_SCHEMA,
|
||||
_format_timestamp,
|
||||
_READ_MAX_CONTENT,
|
||||
_is_compacted_message,
|
||||
_resolve_to_parent,
|
||||
_session_link,
|
||||
@@ -491,6 +492,22 @@ class TestReadShape:
|
||||
assert result["truncated"] is True
|
||||
assert len(result["messages"]) == 30 # head 20 + tail 10
|
||||
|
||||
def test_read_caps_oversized_message_content(self, db):
|
||||
# #114344: a huge archived tool result stored as a message must not come
|
||||
# back whole on the read shape - discovery/scroll already cap (#69334).
|
||||
db.create_session("s_huge", source="cli")
|
||||
db.append_message("s_huge", role="user", content="run it")
|
||||
db.append_message("s_huge", role="assistant", content="x" * 80_000)
|
||||
db.append_message("s_huge", role="user", content="thanks")
|
||||
db._conn.commit()
|
||||
result = json.loads(session_search(session_id="s_huge", db=db))
|
||||
assert result["mode"] == "read"
|
||||
assert result["truncated"] is False # 3 messages, count-wise it all fits
|
||||
big = next(m for m in result["messages"] if m.get("content_truncated"))
|
||||
assert len(big["content"]) <= _READ_MAX_CONTENT + 1 # cap plus ellipsis
|
||||
assert big["original_content_chars"] == 80_000
|
||||
assert sum(len(m.get("content") or "") for m in result["messages"]) < 5_000
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Session links — the value the agent writes to point the user at a session
|
||||
|
||||
@@ -29,6 +29,14 @@ _HIDDEN_SESSION_SOURCES = ("kanban", "subagent", "tool")
|
||||
# Demoting — not excluding — keeps cron content reachable when it's the only match, while interactive
|
||||
# sessions always win when both match.
|
||||
_DEMOTED_SESSION_SOURCES = ("cron",)
|
||||
|
||||
# Read-shape per-message content cap. #69334 capped discovery bookends (1200) and
|
||||
# scroll windows (4000) but left ``_read_session`` returning whole messages, so a
|
||||
# single archived tool result stored as a message could come back verbatim - one
|
||||
# read returned 74K chars and took a request from ~50K to ~89K tokens in a step.
|
||||
# Bounding message COUNT (head/tail) is not enough when content per message is
|
||||
# unbounded; the agent can scroll around a message for detail (#114344).
|
||||
_READ_MAX_CONTENT = 2000
|
||||
# FTS rows scanned before dedup-by-lineage — well above the distinct sessions a query
|
||||
# returns, so interactive matches buried under cron hits survive the demotion pass.
|
||||
_DISCOVER_SCAN_LIMIT = 300
|
||||
@@ -441,7 +449,7 @@ def _read_session(db, session_id: str, head: int = 20, tail: int = 10, link_prof
|
||||
session_id)
|
||||
if err:
|
||||
return err
|
||||
shaped = [_shape_message(m) for m in rows]
|
||||
shaped = [_shape_message(m, max_content_len=_READ_MAX_CONTENT) for m in rows]
|
||||
total, truncated = len(shaped), len(shaped) > head + tail
|
||||
return _ok(mode="read", session_id=session_id, link=_session_link(session_id, link_profile),
|
||||
session_meta=_session_meta_block(meta), message_count=total, truncated=truncated,
|
||||
|
||||
Reference in New Issue
Block a user