Skip to content

Feedback classifier leaks raw LLM completion (think tags, special tokens) into persisted feedback rationale #2336

Closed as not planned

Description

@hxyinan

Summary

Feedback classification responses can leak the raw LLM completion — including closing <think> tags and DeepSeek special tokens — into the persisted feedback rationale. The existing thinking-disable only covers capture.summarize; every other op (e.g. feedback.classify) can receive thinking-enabled model output, and nothing strips think-tags from message.content before it is stored.

This is related to #2308 (the op parameter was dropped at the LLM boundary): even with op correctly propagated, thinking is only disabled for capture.summarize.

Reproduction / evidence

Two feedback rows stored on 2026-09-02 in our deployment contain a rationale that is clearly a raw model completion, not classified output:

rationale: "</think>\n<|end▁of▁sentence|>\n<|end▁of▁session|>\n\n---\n\n[Writing Rule]..."
polarity: negative

Code path (v2.0.17)

  1. dist/core/feedback/llm-classifier.js → classifyByLlm() calls llm.complete(..., { op: "feedback.classify" })
  2. dist/core/llm/providers/openai.js only disables thinking for one op:
if (opts.op === "capture.summarize") {
    body.thinking = { type: "disabled" };
}
  1. For thinking-enabled models served through gateways that keep <think> blocks inside message.content, the raw completion (with </think>, <|end▁of▁sentence|>, …) flows back unmodified.
  2. dist/core/experience/feedback-builder.js then persists feedback.rationale (and raw verifier text) verbatim into the store — polluted text becomes retrieval-visible memory.

Suggested fix

Either (ideally both):

  1. Sanitize at the provider choke point: strip <think>…</think> blocks, orphan closing </think> fragments, and DeepSeek special tokens (<|begin▁of▁sentence|>, <|end▁of▁sentence|>, <|end▁of▁session|>) from choice.message.content before returning from the provider — one place, fixes all ops.
  2. Extend thinking-disable to deterministic/analytical ops (feedback.classify, l3.abstraction, verifier, retrieval filter), not just capture.summarize.

Environment

  • memos-local plugin 2.0.17 (npm memtensor-memos-local-plugin-2.0.17)
  • Adapter: Hermes (adapters/hermes/memos_provider)
  • Windows 11, Node v24, LLM provider openai_compatible (deepseek-family model)

Activity

  1. added
    ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务
    status:in-progressSomeone or AI is working on it | 人工或 AI 正在处理
    types:bugSomething isn't working | 功能异常
    on Sep 3, 2026
  2. self-assigned this
    on Sep 3, 2026
  3. Memtensor-AI commented on Sep 3, 2026

    @Memtensor-AI
    Collaborator

    🤖 AutoDev has picked up this issue and started working on it.

    Task ID: 372e4cf70b86ec48
    Working branch: bugfix/autodev-2336-20260903031435996
    Target branch: latest dev* branch
    Workflow: opsp (analysis → coding → testing → PR)

    I will post the PR link here once done. If I need more information, I will ask in the comments.

  4. Memtensor-AI commented on Sep 3, 2026

    @Memtensor-AI
    Collaborator

    ✅ AutoDev task 372e4cf70b86ec48 completed.

    Summary: 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.

    Base branch: main
    Branch: bugfix/autodev-2336-20260903031435996
    Commit: f334037
    PR: #2339
    Assigned to: @whipser030
    Reviewers: @whipser030, @hijzy

  5. added
    ai:testingAI agent is running tests | AI 正在运行测试
    and removed
    ai:codingAI agent is coding | AI 正在编码
    on Sep 3, 2026
  6. github-actions commented on Oct 4, 2026

    @github-actions
    Contributor

    This issue has been automatically marked as stale due to inactivity.

  7. added
    status:staleAuto-marked stale after 30d inactivity; closes 7d later unless exempt.
    on Oct 4, 2026
  8. github-actions commented on Oct 14, 2026

    @github-actions
    Contributor

    This issue has been automatically closed due to inactivity.

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

Metadata

Metadata

Labels

ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务ai:testingAI agent is running tests | AI 正在运行测试area:pluginOpenClaw & Hermesstatus:in-progressSomeone or AI is working on it | 人工或 AI 正在处理status:staleAuto-marked stale after 30d inactivity; closes 7d later unless exempt.types:bugSomething isn't working | 功能异常

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions