Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions tests/tools/test_session_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from tools.session_search_tool import (
SESSION_SEARCH_SCHEMA,
_format_timestamp,
_READ_MAX_CONTENT,
_is_compacted_message,
_resolve_to_parent,
_session_link,
Expand Down Expand Up @@ -491,6 +492,22 @@ def test_read_truncates_large_session(self, db):
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
Expand Down
10 changes: 9 additions & 1 deletion tools/session_search_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
# 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
Expand Down Expand Up @@ -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,
Expand Down