Skip to content

Chat rewrites the system message from each turn's retrieval and places history after it, so provider prefix caches miss #2524

Open

Description

@JunfengRan

Pre-submission checklist | 提交前检查

  • I have searched existing issues and this hasn't been mentioned before
  • I have read the project documentation and confirmed this issue doesn't already exist
  • This issue is specific to MemOS and not a general software issue

Bug Description | 问题描述

On every chat turn, MemOS rebuilds the system message from that turn's retrieval and sends the conversation history after it. Provider prefix caches keep the longest identical prefix from the start of the request. When the first message changes, the unchanged history behind it cannot be reused.

This is how the product chat API and the library chat entry build the provider request.

What happens:

  • ChatHandler.handle_chat_complete, handle_chat_stream, handle_chat_stream_playground, and handle_chat_stream_for_business_user search memories for the current query, then build one system string and place history after it.
  • _build_system_prompt writes that turn's fact memories and preference text into the system string ({memories}, or an appended ## Fact Memories block). get_cloud_chat_prompt also writes the current minute into the same string, so the system message changes on the next minute even when the retrieved memories are the same.
  • The playground stream uses _build_enhance_system_prompt, which writes the current time and appends PersonalMemory and OuterMemory to that same system message.
  • MOS.chat in src/memos/mem_os/core.py, the chain-of-thought synthesis path in src/memos/mem_os/main.py, and SimpleMemChat use the same order: a system message that contains the memories, then the prior turns, then the current user message.
  • src/memos/llms/openai.py sends that messages list to the provider unchanged.

What was expected:

The part that stays the same across turns — the fixed instructions and the prior turns — should remain a common prefix. The text that changes with this turn's retrieval should come after that prefix, so a provider prefix cache can reuse the history.

MemOSClient.chat only posts the query to the hosted /chat endpoint. It does not build the provider messages. The assembly above is in ChatHandler and in MOS.chat.

How to Reproduce | 如何重现

  1. Call POST /chat/complete or POST /chat/stream twice in one conversation. On the second call, pass the history that includes the first turn's user and assistant messages. Let the two calls retrieve different memories, or call them in different minutes with the same memories.
  2. Look at the messages list the handler passes to the LLM. In src/memos/api/handlers/chat_handler.py it is [system, *history[-20:], current user message]. The system content comes from _build_system_prompt or, on POST /chat/stream/playground, from _build_enhance_system_prompt. The business stream uses the same list.
  3. Compare the two requests. The first message differs because it contains that turn's memories and, for the cloud prompt, the current minute. The prior turns are after that message, so they are not part of the shared prefix.
  4. The same order is in MOS.chat (src/memos/mem_os/core.py): _build_system_prompt with the memories for this query, then chat_history.chat_history, then the current user message.
  5. src/memos/llms/openai.py _build_request_body puts that list in the provider request as messages and does not move the system message.

A provider that caches the longest identical prefix therefore misses from the system message through the rest of the prompt. Unchanged history is not a cache hit.

Environment | 环境信息

  • Python version: 3.12.15
  • Operating System: macOS (Darwin 27.0.0, arm64)
  • MemOS version: 2.0.33 (pip show memoryos reports Name: MemoryOS, Version: 2.0.33)

Additional Context | 其他信息

Open issues were searched for prefix cache, prompt cache, and system-prompt retrieval. None describe this message order. The chat API documentation says retrieved memories are injected into the prompt; it does not keep a stable prefix ahead of the per-turn retrieval. The open activation-memory cache report is a different problem: it is about an in-process DynamicCache growing during generation, not about the provider prompt prefix.

Willingness to Implement | 实现意愿

  • I'm willing to implement this myself
  • I would like someone else to implement this

Activity

  1. added
    ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务
    area:coreMOS 编排层 / 框架底座 / 跨模块问题
    status:in-progressSomeone or AI is working on it | 人工或 AI 正在处理
    types:bugSomething isn't working | 功能异常
    on Oct 21, 2026
  2. self-assigned this
    on Oct 21, 2026
  3. Memtensor-AI commented on Oct 21, 2026

    @Memtensor-AI
    Collaborator

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

    Task ID: e4f30089c2eb8ee3
    Working branch: bugfix/autodev-2524-20261021095707472
    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 Oct 21, 2026

    @Memtensor-AI
    Collaborator

    ✅ AutoDev task e4f30089c2eb8ee3 completed.

    Summary: Fix issue #2524: chat prompt rewrite busted provider prefix caches. The handler used to splice the current turn's retrieval and current minute into the system message and send that first, with chat history after it — so the first message changed every turn and the identical history behind it could not be reused.

    Split the system blob into a stable prompt (byte-identical across turns in one conversation) plus a per-turn dynamic context message carrying the current time, retrieved memories, and preferences. New message layout is [stable system, ...history, dynamic context, user], so the shared cross-turn prefix now grows with the chat history. Applied in ChatHandler.handle_chat_complete, handle_chat_stream, handle_chat_stream_playground, handle_chat_stream_for_business_user, MOSCore.chat, MOS._generate_synthesis_response, and SimpleMemChat.run. Legacy callers that pass a custom base_prompt containing {memories} keep the single-system shape for backward compatibility.

    Tests: added tests/api/test_chat_message_order.py (9 cases covering stable/dynamic split, cacheable assembly, cross-turn shared prefix, and legacy-base-prompt compat), updated tests/mem_os/test_memos_core.py::TestMOSChat::test_chat_with_custom_base_prompt for the new ordering, and added test_chat_places_history_before_dynamic_context_issue_2524. Full targeted run pytest tests/mem_os/ tests/api/ tests/mem_chat/ is green: 167 passed. Ruff lint + format clean on all modified files. No API schema change (no make openapi needed).

    Base branch: main
    Branch: bugfix/autodev-2524-20261021095707472
    Commit: 8849727
    PR: #2529
    Assigned to: @CarltonXiang
    Reviewers: @WeiminLee

  5. added
    ai:testingAI agent is running tests | AI 正在运行测试
    and removed
    ai:codingAI agent is coding | AI 正在编码
    on Oct 21, 2026
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:coreMOS 编排层 / 框架底座 / 跨模块问题status:in-progressSomeone or AI is working on it | 人工或 AI 正在处理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