Skip to content

Fix #2336: Feedback classifier leaks raw LLM completion (think tags, special tokens) into p - #2339

Closed
Memtensor-AI wants to merge 2 commits into
MemTensor:dev-v2.0.33from
Memtensor-AI:bugfix/autodev-2336-20260903031435996
Closed

Fix #2336: Feedback classifier leaks raw LLM completion (think tags, special tokens) into p#2339
Memtensor-AI wants to merge 2 commits into
MemTensor:dev-v2.0.33from
Memtensor-AI:bugfix/autodev-2336-20260903031435996

Conversation

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Description

Fixes issue #2336 — feedback classifier and other ops were leaking raw LLM completion tokens (<think>...</think> blocks and DeepSeek gateway tokens like <|end▁of▁sentence|>) into persisted FeedbackRow.rationale, which then polluted retrieval-visible memory on subsequent turns.

Root cause: apps/memos-local-plugin/core/llm/providers/openai.ts::complete returned choice.message.content verbatim, and no downstream sanitizer knew about <think> tags or DeepSeek special tokens. Thinking-enabled DeepSeek-family models served through gateways that keep the reasoning block inside content (rather than a separate reasoning_content field) leaked it end-to-end into storage. Adopts the reporter's preferred fix (option 1: sanitize at the provider choke point) — one transform covers all current and future ops (feedback.classify, feedback.refine, l3.abstraction, retrieval.filter, …) without requiring per-op thinking-disable coverage.

Changes: new core/llm/sanitize.ts module (strips matched <think>...</think> blocks, orphan closing </think>, orphan opening <think> + trailing text, DeepSeek special tokens, and collapses excess blank lines), applied to openai_compatible, anthropic, gemini, bedrock complete() return values. Streaming path is deliberately out of scope (chunk boundaries make safe chunk-level sanitization stateful — deferred; the reported leak is on the storage path which uses complete()).

Tests: added tests/unit/llm/sanitize.test.ts (17 unit tests including the exact artifact soup captured in the #2336 report, an ASCII-pipe safety guard, and idempotency) plus 2 provider-level regression tests in tests/unit/llm/providers.test.ts pinning end-to-end behavior. Verification: npm run test — 1576 passed / 2 skipped across 184 files; npm run lint (tsc --noEmit) — clean.

Related Issue (Required): Fixes #2336

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • Documentation update

How Has This Been Tested?

Automated tests are pending.

  • Unit Test
  • Test Script Or Test Steps (please provide)
  • Pipeline Automated API Test (please provide)

Checklist

  • I have performed a self-review of my own code
  • I have commented my code in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • I have created related documentation issue/PR in MemOS-Docs (if applicable)
  • I have linked the issue to this PR (if applicable)
  • I have mentioned the person who will review this PR

@whipser030, @hijzy please review this PR.

Reviewer Checklist

Thinking-enabled DeepSeek-family models on gateways that keep the
reasoning block inside `choice.message.content` leaked `<think>...</think>`
blocks and DeepSeek special tokens (`<|end▁of▁sentence|>`,
`<|end▁of▁session|>`) into `ProviderCompletion.text`. That text then
flowed through `feedback.classify` into persisted `FeedbackRow.rationale`
rows — polluting retrieval-visible memory.

Fix at the provider choke point: introduce `core/llm/sanitize.ts` and
apply it to `openai_compatible`, `anthropic`, `gemini`, `bedrock`
`complete()` return values. One transform covers all current + future
ops (including `feedback.classify`, `feedback.refine`, `l3.abstraction`,
`retrieval.filter`) without needing per-op thinking-disable coverage.

Streaming path is out of scope: chunks arrive independently and safe
chunk-level sanitization requires buffer-based state; the reported leak
is on the storage path, which uses `complete()`.

Adds:
- `core/llm/sanitize.ts` — the transform (matched `<think>` blocks,
  orphan `</think>` / `<think>` fragments, DeepSeek special tokens,
  excess blank-line collapse).
- `tests/unit/llm/sanitize.test.ts` — 17 sanitizer unit tests.
- `tests/unit/llm/providers.test.ts` — two regression tests pinning
  provider-level behavior, including the exact orphan-`</think>`
  fragment shape captured in the MemTensor#2336 report.

All 1576 tests + tsc --noEmit pass.
@Memtensor-AI Memtensor-AI added ai:generated Generated or modified by AI | 由 AI 生成或修改 area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Sep 3, 2026
@Memtensor-AI

Memtensor-AI commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Open Code Review

Target: PR #2339
Task: 372e4cf70b86ec48
Base: dev-v2.0.33
Head: bugfix/autodev-2336-20260903031435996
Head SHA: a32eba77e8acb54f59918faeee35951fc1ca061d

OpenCodeReview: Review complete: 0 finding(s) across 5 selected item(s).

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

🔧 Open Code Review requested Agent fix

Open Code Review found 2 issue(s). I have resumed the development Agent to fix them.

  • Task: 372e4cf70b86ec48
  • Fix attempt: 1/2
  • Finding delta: 0 repeated / 2 new / 0 likely resolved

The Agent will push a new commit to this PR branch. OCR will recheck after the commit is pushed.

…emTensor#2336)

The initial MemTensor#2336 fix only wired sanitizeCompletionText into the
non-streaming complete() path of both providers. The stream() path still
yielded raw SSE deltas, so any consumer that accumulates chunk.delta —
including core/llm/client.ts::stream, which logs the joined text and
forwards chunks to storage-adjacent callers like the DSH bridge — could
still receive <think>...</think> blocks and DeepSeek session tokens.

Per-chunk sanitization is unsafe because a <think> block can straddle
two SSE frames (the opener arrives in one delta, the closer in another),
so a stateless regex would miss the pair. Buffer the entire stream in
the provider, sanitize the accumulated text once at finish, and yield
one sanitized delta before the done chunk. Callers still see
chunks.map(c => c.delta).join("") === sanitized text, matching the
existing stream contract exercised by the provider tests.

Tests: two new streaming regression cases per provider — one splits
a <think> block across SSE chunks, the other reproduces the orphan
</think> soup captured in the MemTensor#2336 evidence. Both assert the joined
delta stream matches the sanitizer output and that finishReason + usage
still surface on the done chunk.

Verification: npm run lint clean; npm run test — 1579 passed / 2 skipped
across 184 files.
@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Sep 3, 2026
@CarltonXiang
CarltonXiang deleted the branch MemTensor:dev-v2.0.33 September 3, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:generated Generated or modified by AI | 由 AI 生成或修改 area:plugin OpenClaw & Hermes status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants