Skip to content

fix(seahorse): prevent tool-call format leakage into LLM summaries - #3279

Closed
MrTreasure wants to merge 3 commits into
sipeed:mainfrom
MrTreasure:fix/seahorse-tool-call-leak
Closed

fix(seahorse): prevent tool-call format leakage into LLM summaries#3279
MrTreasure wants to merge 3 commits into
sipeed:mainfrom
MrTreasure:fix/seahorse-tool-call-leak

Conversation

@MrTreasure

Copy link
Copy Markdown

Related: same symptom triggered by seahorse summaries

I just hit this exact class of bug — tool-call format leaking into user messages — but triggered through a different path: seahorse's partsToReadableContent.

The seahorse variant

partsToReadableContent (in pkg/seahorse/store.go) serializes tool calls as [tool_use: name, args: {...}] for FTS5 indexing. This format was also used as a fallback in two summary-generation paths (formatMessagesForSummary and truncateSummary in short_compaction.go). When summaries containing [tool_use: exec, args: {...}] were fed back into the LLM context, the model learned to mimic this format in its own responses — outputting mock tool calls as visible text content.

The pipeline

partsToReadableContent -> "[tool_use: exec, args: {...}]"
     -> seahorse summary / truncateSummary
     -> LLM context (as summary text)
     -> LLM mimics format in its content output
     -> User sees raw [tool_use: exec, args: {...}] in chat

Fix

PR incoming at: https://github.com/sipeed/picoclaw/pulls (MrTreasure:fix/seahorse-tool-call-leak)

Three changes:

  1. short_compaction.go: Skip messages with empty content in summary/truncation paths — tool calls aren't conversational content
  2. store.go: Change FTS5 format from [tool_use: name, args: {...}] to emoji markers — the bracket-colon pattern is what LLMs latch onto
  3. agent_utils.go: Strip residual [tool_use: ...] from outbound messages as a safety net

Same root cause as this issue: LLM context leaking internal serialization format → model mimics it → user sees raw tool-call text. Different trigger (seahorse vs Doubao provider), same outcome.

@github-actions

Copy link
Copy Markdown

This PR has had no activity for 7 days and has been marked as stale. If you are still working on it, please push an update or leave a comment; otherwise it will be closed automatically in 7 days.

@MrTreasure

Copy link
Copy Markdown
Author

Status update: this fix is still actively maintained. The original issue remains reproducible in long-lived Seahorse sessions: raw tool-call protocol text can enter compacted summaries and later surface to users. I have continued validating the summary/compaction path against current upstream master and am reviewing compatibility with tool-only history entries before the next branch update. Feedback on the preferred serialization behavior is welcome.

MrTreasure and others added 3 commits July 30, 2026 13:02
partsToReadableContent used [tool_use: name, args: {...}] format for
FTS5 indexing and summary generation. When summaries containing this
format were fed back to LLMs, the models learned to mimic it and output
mock [tool_use: ...] text as content, which was then displayed to users.

Two fixes:
1. formatMessagesForSummary / truncateSummary: skip messages with empty
   content (pure tool calls/results) — these are mechanical, not
   conversational, and have no place in summaries.
2. partsToReadableContent: use emoji markers (🔧/📋) instead of the
   bracket-colon format that LLMs are prone to mimic.
3. outboundMessageForTurn: strip any residual [tool_use: ...] patterns
   from content before sending to channels, as a safety net.

Root cause: same class of bug as sipeed#3153 (Doubao <seed:tool_call> leak).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MrTreasure
MrTreasure force-pushed the fix/seahorse-tool-call-leak branch from ffa4674 to 78ca23f Compare July 30, 2026 05:06
@MrTreasure

Copy link
Copy Markdown
Author

Updated this PR with the complete fix reproduced from a live DeepSeek/OpenAI-compatible response. The provider returned both native tool_calls and a duplicate [tool_use: ...] string in assistant content. The update now sanitizes content, reasoning, and tool feedback immediately after AfterLLM hooks and before publishing, persistence, or Seahorse compaction. The parser handles nested JSON, quoted brackets, legacy tool_user/tool_result variants, and truncated blocks. Seahorse still preserves searchable tool names, arguments, and results using protocol-neutral human-readable text. Rebased onto current upstream main. Verified locally: go test ./pkg/agent ./pkg/seahorse ./pkg/providers/openai_compat ./pkg/providers/common.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

This PR has had no activity for 7 days and has been marked as stale. If you are still working on it, please push an update or leave a comment; otherwise it will be closed automatically in 7 days.

@github-actions

Copy link
Copy Markdown

This PR has been closed after 14 days of inactivity. If you would like to continue, feel free to reopen it or submit a new PR.

@github-actions github-actions Bot closed this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant