Skip to content

[Bug]: transform_llm_output appended content leaks into Honcho external memory sync #57282

Description

@kweiner

Bug Description

Content appended by a transform_llm_output plugin is passed to the external memory provider (Honcho) via _sync_external_memory_for_turn. Because the memory sync fires after transform_llm_output, it receives the fully-transformed final_response — including any content the plugin appended that was intended only for display. On future turns, Honcho retrieves that content as part of the conversation context, so the model sees the appended text as if it were part of the original assistant response.

Steps to Reproduce

  1. Configure Hermes with a Honcho memory provider (hermes memory set honcho).
  2. Install a plugin that implements transform_llm_output and appends a display-only suffix — for example, a citation, disclaimer, or any content that should not become part of the agent's memory.
  3. Send a message that triggers the plugin to append content.
  4. On the next turn, ask the agent to recall or summarize the previous exchange.
  5. Observe that the agent references the appended display-only content as if it were part of its own prior response.

Root Cause Analysis

In agent/conversation_loop.py, the execution order is:

# Line ~4375
agent._persist_session(messages, conversation_history)
# → SQLite session DB gets the raw LLM response ✓

# Line ~4448
transform_llm_output fires
# → final_response is now raw response + plugin-appended content

# Line ~4565
agent._sync_external_memory_for_turn(
    final_response=final_response,   # ← transformed value, includes appended content
    ...
)
# → Honcho receives the appended content and stores it as the assistant turn ✗

The SQLite session DB and the OpenAI messages list (used for in-context history) are both written before the transform fires, so they correctly contain only the raw LLM response. But the external memory sync fires after, so Honcho gets the polluted value.

Why this is hard to fix without a new API

There are two legitimate plugin archetypes with opposite requirements:

Plugin type What it does in transform_llm_output What memory should receive
Display-only append (e.g. citation, shout) Appends a suffix after the response Raw LLM response (no suffix)
Content restoration (e.g. PII de-redaction) Replaces placeholder tokens with real values Restored content (not placeholders)

Simply passing pre_transform_response to _sync_external_memory_for_turn instead of final_response would fix the append case but break the restoration case. There is no single mechanical fix that works for both without some form of plugin-declared intent.

Potential approaches

  1. Display-only flag in hook registration — when registering a transform_llm_output hook, plugins declare whether their transform is display_only=True (memory gets pre-transform) or display_only=False (memory gets post-transform, current behavior). The runtime passes the appropriate value to _sync_external_memory_for_turn.

  2. Structured return from transform_llm_output — instead of returning a plain string, a plugin can optionally return {"display": "...", "memory": "..."}, allowing different values for display and memory. Plain string return preserves current behavior.

  3. New pre-memory-sync hook — a transform_memory_assistant hook (analogous to the proposed transform_persisted_assistant in [Feature]: Add plugin hook for transforming assistant messages before session DB persistence (transform_persisted_assistant) #46574) fires immediately before _sync_external_memory_for_turn and lets plugins strip or modify what the memory provider receives, independently of what the display layer shows.

Option 3 is most flexible and avoids a breaking change to the transform_llm_output contract. It also composes naturally with #46574.

Related

Operating System

Ubuntu 24.04.4 LTS

Python Version

3.11.15

Hermes Version

0.18.0

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

This requires a design decision on the right API shape before implementation — happy to contribute a PR once there's consensus on approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low — cosmetic, nice to havearea/memoryMemory subsystem: store, providers, sync, background reviewscomp/agentCore agent runtime: loop, agent_init, prompt builder, context-compression, responses endpointcomp/pluginsPlugin system and bundled pluginstool/memoryMemory tool and memory providerstype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions