Skip to content

fix(context): bound session recall payloads - #55640

Open
sk-holmes wants to merge 18 commits into
NousResearch:mainfrom
sk-holmes:sk/fix-bound-session-recall-payloads
Open

fix(context): bound session recall payloads#55640
sk-holmes wants to merge 18 commits into
NousResearch:mainfrom
sk-holmes:sk/fix-bound-session-recall-payloads

Conversation

@sk-holmes

@sk-holmes sk-holmes commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Bounds session_search output before recalled history is injected back into the active model context.

The original bug is recursive context bloat: one recall call can import prior messages, compaction summaries, tool metadata, and lineage/bookend payloads into the current model context. If that recalled result is itself large or contains stale generated summaries, it can consume another context window and reactivate obsolete task state.

This PR keeps recall source-linked and navigable while bounding every returned layer:

  • canonical standalone, merged, legacy, and structured compaction summaries are recognized and omitted from recalled content and snippets;
  • oversized message bodies, snippets, tool-call arguments, retained metadata, collections, and the final serialized response are capped with explicit omission/truncation metadata;
  • discovery prefers a real source message when a generated compaction summary ranks first;
  • lineage traversal has depth and lookup budgets plus path compression;
  • read-mode bookends and message counts come from active message rows instead of stale cached metadata;
  • default/profile databases opened by the tool have explicit ownership and are closed after use;
  • newly generated merged-summary wrappers escape quoted boundary text;
  • ambiguous historical wrappers remain inert summary material instead of being reintroduced as live user or assistant turns.

The result preserves session IDs, message IDs, roles, timestamps, tool names, source references, and navigation metadata so an agent can request a narrower scroll/read rather than receiving an unbounded transcript copy.

Related Issue

Related PRs:

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • tools/session_search_tool.py

    • bounds individual fields, collections, and the final serialized response;
    • omits canonical compaction summaries from recalled content and snippets;
    • prefers non-summary source messages during discovery;
    • bounds lineage traversal and stops once the requested result limit is satisfied;
    • explicitly closes internally owned database handles.
  • agent/context_compressor.py

    • centralizes canonical summary detection across standalone, merged, legacy, and structured forms;
    • preserves real prior-tail content across repeated compaction;
    • escapes quoted merged-wrapper boundaries;
    • scans persisted wrappers without suffix-sized copies;
    • fails closed when historical wrapper boundaries are ambiguous.
  • hermes_state.py

    • adds active-row point/bookend reads with source-of-truth message counts;
    • selects bounded head/tail IDs before hydrating full message blobs.
  • tests/agent/test_context_compressor.py

    • covers canonical and historical summary shapes, repeated compaction, quoted boundaries, ambiguous legacy wrappers, and prevention of stale-summary reinjection.
  • tests/tools/test_session_search.py

    • covers summary omission, source-hit preference, stale/malformed counts, inactive rows, field/tool/aggregate response budgets, lineage limits, database ownership, and bounded SQL work.
  • website/docs/user-guide/sessions.md

    • documents bounded recall and omission/truncation metadata instead of promising untruncated payloads.

How to Test

scripts/run_tests.sh \
  tests/agent/test_context_compressor.py \
  tests/tools/test_session_search.py \
  -q

Final local verification:

  • tests/agent/test_context_compressor.py: 162 passed
  • tests/tools/test_session_search.py: 79 passed
  • Total: 241 passed, 0 failed
  • Ruff passed
  • Python compilation passed
  • git diff --check passed

GitHub Actions on the final head also pass:

  • all eight Python test slices;
  • e2e tests;
  • Ruff and type-diff checks;
  • Windows-footgun checks;
  • docs-site checks;
  • Docker AMD64 and ARM64 builds;

Manual behavior checks:

  • compaction-summary bodies and snippets are replaced by omission metadata instead of being returned verbatim;
  • a real source message is preferred when its lineage also contains a matching compaction summary;
  • oversized content and tool metadata remain below field and aggregate response limits;
  • read mode returns the true active head, tail, and message count when cached session metadata is stale;
  • generated merged-summary wrappers remain parseable when payload text quotes the internal boundary;
  • ambiguous historical wrappers remain summary data and are never synthesized into live turns.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.5.1

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

For New Skills

N/A

Screenshots / Logs

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tui Terminal UI (ui-tui/ + tui_gateway/) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jun 30, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: LGTM

Adds profile forwarding to session_search tool calls and bounds session recall payloads with metadata constants.

✅ Looks Good

  • Clean implementation: profile parameter forwarded from agent to session_search
  • Good test coverage with 2 new tests (sequential and invoke paths)
  • Schema description updated to document bounded payloads
  • Well-scoped fix for cross-profile session search

Reviewed by Hermes Agent

@dante32683

Copy link
Copy Markdown

Hit this exact problem before finding this PR. Data point in case it helps review.

On an instance with a large session history, a single session_search discover-mode call (query, limit=5) returned a ~49400-char tool result.

~22,200 chars was actual message content.
~20,700 chars (~42% of the total) was the raw tool_calls blob on assistant messages caught in the bookend/window. Mostly opaque per-call bookkeeping (response_item_id, etc.) plus uncapped function.arguments.

The _MESSAGE_CONTENT_MAX_CHARS / _TOOL_CALL_ARGUMENTS_MAX_CHARS / _TOOL_CALLS_MAX_ITEMS caps here would have cut that call from ~49k chars to a small fraction of that.

One gap: _shape_tool_calls_for_recall copies each call dict (shaped = dict(call)) and only reshapes function.arguments / top-level arguments. id / call_id / response_item_id still pass through unchanged. Not as big a deal since arguments and count are capped which was the main bloat. Possible follow-up though.

Would like to see this merged.

@sk-holmes
sk-holmes force-pushed the sk/fix-bound-session-recall-payloads branch from a0f492c to e3cfc29 Compare July 6, 2026 22:28
@sk-holmes
sk-holmes force-pushed the sk/fix-bound-session-recall-payloads branch from e3cfc29 to 5ac7fef Compare July 15, 2026 03:13

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing a real recall-context bloat path. Current main still returns raw recalled content and tool-call blobs through tools/session_search_tool.py:123-141 and discovery bookends/windows at :588-603.

Problems

  • tools/session_search_tool.py:153 misses merged compaction summaries. Current compaction intentionally puts the summary prefix after _MERGED_SUMMARY_DELIMITER (agent/context_compressor.py:138-145, 2259-2270), so this supported form is not omitted.
  • tools/session_search_tool.py:278 copies retained call dictionaries wholesale. Only arguments is bounded, so an oversized response_item_id or other provider field still defeats the stated bounded-payload guarantee.
  • website/docs/user-guide/sessions.md:528 still promises “no truncation.”

Suggested changes

  • Reuse or precisely mirror the compressor's merged/legacy summary detection and add its regression shape.
  • Shape tool calls with an explicit bounded field set (or cap every retained field), plus a non-arguments oversized-field test.
  • Update the sessions guide for bounded recall output.

Automated hermes-sweeper review.

Comment thread tools/session_search_tool.py Outdated
return len(str(content))


def _is_compaction_summary_content(content: Any) -> bool:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only recognizes a summary when its prefix starts the string. Current compaction can merge a prior tail before the summary and place the prefix after _MERGED_SUMMARY_DELIMITER (agent/context_compressor.py:138-145, 2259-2270), so that supported summary form is returned rather than omitted. Please match the compressor's detection semantics and add a merged-summary regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread tools/session_search_tool.py Outdated
shaped_calls.append(call)
continue

shaped = dict(call)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copying the whole provider call object leaves every non-arguments field unbounded. A large response_item_id or another provider-specific field survives the count/arguments caps, so the recall payload is still not bounded. Please use an explicit shaped field set or cap all retained fields, with a regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@sk-holmes
sk-holmes force-pushed the sk/fix-bound-session-recall-payloads branch from 5ac7fef to b45bac2 Compare July 15, 2026 13:20
@sk-holmes
sk-holmes requested a review from teknium1 July 15, 2026 13:27
@sk-holmes
sk-holmes force-pushed the sk/fix-bound-session-recall-payloads branch from b45bac2 to 3a39f01 Compare July 16, 2026 00:25
@sk-holmes

Copy link
Copy Markdown
Contributor Author

enhanced with more tests and self reviews 🫡

@teknium1 teknium1 added the area/sessions Session lifecycle, resume, persistence, history label Jul 19, 2026
sk-holmes and others added 9 commits July 22, 2026 23:54
…n-recall-payloads

# Conflicts:
#	agent/context_compressor.py
#	tools/session_search_tool.py
session_search replays raw message content from past sessions verbatim,
with no scan and no untrusted-content wrapping. A message that carries an
injection payload -- a poisoned web page quoted earlier, a pasted phishing
email, a Brainworm-style payload from any prior turn -- gets served back
into the model's context as plain data on a later query, unmarked.

Every other tool that returns attacker-controllable content (web_extract,
web_search, browser_*, mcp_*) already gets wrapped in
<untrusted_tool_result> delimiters via make_tool_result_message(), telling
the model to treat the content as data, not instructions. session_search
was simply missing from that list. Add it.
@sk-holmes

Copy link
Copy Markdown
Contributor Author

resimplified this branch to stay on original target

@Skywind5487 Skywind5487 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Research note for #45/#47: the current PR body still says lineage traversal has “depth and lookup budgets plus path compression” and “stops once the requested result limit is satisfied”, but the current head diff (d0e5a364...) no longer contains lineage traversal/path-compression changes; its present tools/session_search_tool.py diff is summary detection/content bounding only. Please do not treat #55640's current head as an implementation of the path-compressed contender without checking an earlier commit/revision. This is just provenance clarification, not a request to change the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

session_search discovery can rehydrate huge compaction summaries via bookends

7 participants