Skip to content

fix(gateway): Responses API stores bloated context after compression - #58133

Closed
LiangYang666 wants to merge 11 commits into
NousResearch:mainfrom
LiangYang666:fix/responses-compress-store-bloat
Closed

fix(gateway): Responses API stores bloated context after compression#58133
LiangYang666 wants to merge 11 commits into
NousResearch:mainfrom
LiangYang666:fix/responses-compress-store-bloat

Conversation

@LiangYang666

@LiangYang666 LiangYang666 commented Jul 4, 2026

Copy link
Copy Markdown

Fixes #58081

What does this PR do?

Fix a bug where POST /v1/responses stores a bloated conversation history in ResponseStore after context compression, causing repeated re-compression on every subsequent request via previous_response_id or conversation name chaining.

Root Cause

After compression, _build_response_conversation_history receives the original uncompressed conversation_history AND the compressed result["messages"]. Its prefix-matching detection (_response_messages_turn_start_index) fails for compressed transcripts (because the compressed transcript starts with a summary, not the original history prefix), falling back to concatenating both:

prior (uncompressed) + current_user + result["messages"] (compressed)

This doubles the stored context. On the next request, the bloated history is loaded from ResponseStore, compression fires again, and the cycle repeats indefinitely.

Fix

Two changes in gateway/platforms/api_server.py:

  1. _run_agent — After the agent completes, detect compression via the two signals that compress_context already sets:

    • Rotation mode (legacy): agent.session_id differs from the input session_id (a new child session was created).
    • In-place mode (compression.in_place: true): agent._last_compaction_in_place is True (messages archived under the same session id).
      Set result["_compressed"] = True when either is true.
  2. _build_response_conversation_history — When turn_start == 0 and result["_compressed"] is set, return result["messages"] directly instead of concatenating it with conversation_history.

Why this approach

  • Does not modify _response_messages_turn_start_index (a shared utility also used by _extract_output_items), avoiding side effects on response output construction.
  • Uses signals from compress_context itself — the canonical source of truth for whether compression happened — rather than heuristic message-content scanning.
  • Covers both rotation and in-place compression modes.
  • The _compressed flag is only True when compression actually occurred — zero impact on normal (non-compressed) turns.

Related

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

  • gateway/platforms/api_server.py_run_agent: detect compression via session-id rotation / in-place flag, set result["_compressed"]
  • gateway/platforms/api_server.py_build_response_conversation_history: skip concatenation when _compressed is set
  • tests/gateway/test_api_server.py — add test_previous_response_id_stores_compressed_transcript_directly

How to Test

  1. Start a conversation via POST /v1/responses with store: true
  2. Continue via previous_response_id chaining until compression fires
  3. Verify stored conversation_history in ResponseStore is the compressed transcript, not original + compressed
pytest tests/gateway/test_api_server.py — 194 passed, 0 failed

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run pytest tests/gateway/test_api_server.py and all tests pass
  • I've added tests for my changes
  • I've tested on: macOS 26.2

Compression produces a compact transcript in result['messages'],
but _build_response_conversation_history detected a prefix mismatch
and concatenated the original conversation_history on front.

Detect compression via _last_compaction_in_place / session_id
rotation and signal through result['_compressed'] so the builder
uses the compressed transcript directly.
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery provider/openai OpenAI / Codex Responses API sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state P2 Medium — degraded but workaround exists labels Jul 4, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #58118 (competing fix for the same bug, same file) and #58081 (the bug this fixes). Both PRs target the /v1/responses re-compression loop where the compressed transcript isn't stored correctly in ResponseStore, but via DIFFERENT mechanisms:

@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

Small fix (+88/-1) placing envelope cache breakpoints on message carriers that providers actually honor.

Looks Good

  • Minimal, targeted change
  • No security or performance concerns

Reviewed by Hermes Agent

@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.

Supplementary review: confirms LGTM. Responses API context bloat fix is a targeted performance fix. Prior COMMENT found no issues. No additional concerns.


Reviewed by Hermes Agent

Compression produces a compact transcript in result['messages'],
but _build_response_conversation_history detected a prefix mismatch
and concatenated the original conversation_history on front.

Detect compression via _last_compaction_in_place / session_id
rotation and signal through result['_compressed'] so the builder
uses the compressed transcript directly.

@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 targeting the Responses compression path. The reported bloat is still present on current main: gateway/platforms/api_server.py:4001-4013 falls back to prior + current_user + agent_messages when a compacted summary no longer shares the original-history prefix.

Problems

  • The added test mocks _run_agent with "_compressed": true, so it does not verify the new detection path for either a rotated agent.session_id or _last_compaction_in_place.
  • For the legacy rotation mode described in this PR, _run_agent already returns the effective ID (gateway/platforms/api_server.py:4259-4265), but the Responses handler persists the original request ID (gateway/platforms/api_server.py:3628-3633) and later reloads it for chaining (:3441-3467). The rotation branch should propagate that effective ID if it is intended to be supported end-to-end.

Suggested changes

  • Add focused tests for both real compression signals and a chained legacy-rotation case that asserts the stored continuation session ID.

Automated hermes-sweeper review.

"_compressed": True,
"api_calls": 1,
},
{"input_tokens": 0, "output_tokens": 0, "total_tokens": 0},

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 mocks the flag consumed by the builder, but never exercises the new _run_agent detection. Please add focused fake-agent tests for both a rotated session_id and _last_compaction_in_place=True so the production signal propagation is covered.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 15, 2026
- Detect when history is loaded from response_store (via previous_response_id)
- Add history_from_store parameter to distinguish history source
- When compression occurs, persist compressed messages instead of original
- Add persist_in_response_store config option (default True)
- Update session_id and response headers to reflect session rotation

Cherry-picked from alidev 2eb816f6b
The persist logic only checked _result_sid != session_id (rotation),
missing in-place mode where session_id is unchanged but _compressed
flag is set. response_store history doubled every turn (11->26->55->110->225)
causing repeated re-compression.

Fix: detect compression via _did_compress or _rotated, and only update
_effective_session_id on actual rotation (not in-place).

Note: preflight loop break (turn_context.py) from original commit
eee64097a is excluded — it's an optimization, not a bug fix.

Cherry-picked from alidev eee64097a (api_server.py only)
Address review feedback (PR NousResearch#58133): the original test mocked _run_agent
with _compressed=True directly, bypassing the detection logic.

New tests mock _create_agent instead, so _run_agent's detection path
runs naturally and reads agent.session_id / _last_compaction_in_place:

1. test_rotation_compression_exercises_detection_and_persists_rotated_session_id
   - Fake agent with rotated session_id -> verifies _compressed is set,
     compressed history is stored, and rotated session_id propagates to
     both response_store and X-Hermes-Session-Id header.

2. test_inplace_compression_exercises_detection_and_persists_compressed_history
   - Fake agent with _last_compaction_in_place=True, session_id unchanged
     -> verifies _compressed is set, compressed history is stored, and
     session_id does NOT rotate.

3. test_chained_rotation_propagates_effective_session_id
   - Two-request chain: first request triggers rotation, second request
     loads history using the rotated session_id stored by the first.
     Asserts the compressed transcript is loaded correctly for chaining.
…o fix/responses-compress-store-bloat-local

# Conflicts:
#	tests/gateway/test_api_server.py
@LiangYang666

Copy link
Copy Markdown
Author

Hi @teknium1 , first off — thank you so much for taking the time to review this PR. I'm genuinely thrilled that a maintainer on a project of this scale looked at my contribution, it means a lot!

You're absolutely right on both points, and I've just pushed updates that address them:

1. Test doesn't exercise detection path

Fixed in c277a2b — the new tests mock _create_agent instead of _run_agent, so the real detection logic runs and reads agent.session_id / _last_compaction_in_place naturally. Covers both rotation and in-place modes, plus a chained rotation case that asserts the stored session ID.

2. Rotation mode doesn't propagate effective session ID end-to-end

Also fixed. We found in production that the Responses handler was persisting the original request session ID instead of the rotated one, so previous_response_id chaining would reload the wrong session. This is now addressed in commits c8e3c78 and 560df09:

  • _effective_session_id is now propagated on rotation (both to response_store and the response header).
  • Added history_from_store to distinguish history loaded via previous_response_id vs supplied by the client.
  • Added persist_in_response_store config option (default True) so compressed messages are persisted back to response_store instead of the original uncompressed history.

Additional production finding:

While running this in production, we also discovered that the persist logic only checked for rotation (session_id change), completely missing in-place compression mode where session_id is unchanged but compression still occurred. This caused the stored history to double on every turn (11 → 26 → 55 → 110 → 225 messages), triggering repeated re-compression. Fixed in 560df09 by detecting compression via _did_compress or _rotated instead of rotation alone.

These fixes have been running in our production environment for several days now with no recurrence of the bloat issue. I've also merged the latest upstream/main to keep everything current.

Thanks again for the review — it directly led to a much more robust fix!

@LiangYang666

Copy link
Copy Markdown
Author

@teknium1 I've just pushed an update that rebases this branch on the latest upstream/main and resolves the resulting conflict in _run_agent.

The conflict was because upstream refactored _run_agent to create the agent inside _run() with try/finally. I kept that new structure and re-inserted our _compressed detection logic in the same place (after the effective session-id assignment, before return result, usage). No functional change — rotation and in-place detection still work as before.

Also added a small improvement based on code review: the compressed response_store persist paths now log a warning instead of silently swallowing exceptions, so any production failures there are observable.

@teknium1 teknium1 added the area/compression Context compression and continuation sessions label Jul 19, 2026
teknium1 pushed a commit that referenced this pull request Jul 22, 2026
Address review feedback (PR #58133): the original test mocked _run_agent
with _compressed=True directly, bypassing the detection logic.

New tests mock _create_agent instead, so _run_agent's detection path
runs naturally and reads agent.session_id / _last_compaction_in_place:

1. test_rotation_compression_exercises_detection_and_persists_rotated_session_id
   - Fake agent with rotated session_id -> verifies _compressed is set,
     compressed history is stored, and rotated session_id propagates to
     both response_store and X-Hermes-Session-Id header.

2. test_inplace_compression_exercises_detection_and_persists_compressed_history
   - Fake agent with _last_compaction_in_place=True, session_id unchanged
     -> verifies _compressed is set, compressed history is stored, and
     session_id does NOT rotate.

3. test_chained_rotation_propagates_effective_session_id
   - Two-request chain: first request triggers rotation, second request
     loads history using the rotated session_id stored by the first.
     Asserts the compressed transcript is loaded correctly for chaining.
teknium1 added a commit that referenced this pull request Jul 22, 2026
…g opt-out

Rework on top of the salvaged #58133 commits:

- Remove the compression.persist_in_response_store config key — this is
  a bug fix (stored transcripts must reflect what the agent will actually
  replay), not behavior that should be opt-out-able.
- Drop the per-request load_config() imports the handler-level persist
  blocks added.
- Dedupe the two handler-level persist blocks: the compressed-transcript
  substitution already lives in _build_response_conversation_history
  (via result["_compressed"]), so the handlers only need to propagate
  the effective (possibly rotation-changed) session_id. The streaming
  path does this via a new session_id_snapshot arg on
  _persist_response_snapshot; the non-streaming path picks up
  result["session_id"] directly.
- Rotation propagation no longer gates on history-from-store: the first
  request in a chain can also rotate, and its stored session_id must be
  the child session or the next previous_response_id request resumes the
  pre-rotation session and re-compresses every turn.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #69306 (commit 1c21e96). All six of your commits were cherry-picked with authorship preserved. The salvage rework removed the config opt-out key and deduped the persist sites per review. Thanks!

@teknium1 teknium1 closed this Jul 22, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
Address review feedback (PR NousResearch#58133): the original test mocked _run_agent
with _compressed=True directly, bypassing the detection logic.

New tests mock _create_agent instead, so _run_agent's detection path
runs naturally and reads agent.session_id / _last_compaction_in_place:

1. test_rotation_compression_exercises_detection_and_persists_rotated_session_id
   - Fake agent with rotated session_id -> verifies _compressed is set,
     compressed history is stored, and rotated session_id propagates to
     both response_store and X-Hermes-Session-Id header.

2. test_inplace_compression_exercises_detection_and_persists_compressed_history
   - Fake agent with _last_compaction_in_place=True, session_id unchanged
     -> verifies _compressed is set, compressed history is stored, and
     session_id does NOT rotate.

3. test_chained_rotation_propagates_effective_session_id
   - Two-request chain: first request triggers rotation, second request
     loads history using the rotated session_id stored by the first.
     Asserts the compressed transcript is loaded correctly for chaining.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…g opt-out

Rework on top of the salvaged NousResearch#58133 commits:

- Remove the compression.persist_in_response_store config key — this is
  a bug fix (stored transcripts must reflect what the agent will actually
  replay), not behavior that should be opt-out-able.
- Drop the per-request load_config() imports the handler-level persist
  blocks added.
- Dedupe the two handler-level persist blocks: the compressed-transcript
  substitution already lives in _build_response_conversation_history
  (via result["_compressed"]), so the handlers only need to propagate
  the effective (possibly rotation-changed) session_id. The streaming
  path does this via a new session_id_snapshot arg on
  _persist_response_snapshot; the non-streaming path picks up
  result["session_id"] directly.
- Rotation propagation no longer gates on history-from-store: the first
  request in a chain can also rotate, and its stored session_id must be
  the child session or the next previous_response_id request resumes the
  pre-rotation session and re-compresses every turn.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compression Context compression and continuation sessions comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists provider/openai OpenAI / Codex Responses API sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform 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.

[Bug] Responses API: compressed transcript not stored in ResponseStore, causing re-compression loop

4 participants