Skip to content

fix: prevent memory drift for referential queries in fast mode#2154

Open
Zhe-SH-CN wants to merge 4 commits into
MemTensor:dev-v2.0.24from
Zhe-SH-CN:fix/query-context-drift
Open

fix: prevent memory drift for referential queries in fast mode#2154
Zhe-SH-CN wants to merge 4 commits into
MemTensor:dev-v2.0.24from
Zhe-SH-CN:fix/query-context-drift

Conversation

@Zhe-SH-CN

Copy link
Copy Markdown

Description

In fast search mode, TaskGoalParser._parse_fast ignored conversation history entirely. Referential queries like "再找找其他价格优惠的" were embedded as-is, causing them to match unrelated older memories instead of the current conversation topic (issue #1365).

This PR adds lightweight context-aware query expansion in fast mode:

  • When conversation history is available and the query looks referential (short, or contains referential hints like 其他/这个/再/more/another), prepend the most recent user message to the query before embedding
  • This is a zero-LLM-call heuristic — no latency added to fast mode
  • Long user messages are truncated to 120 chars to avoid over-long embedding inputs
  • Self-contained long queries (> 20 chars, no referential hints) are not modified

Related Issue: Fixes #1365

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested

  • Unit tests covering: no-conversation, referential query augmentation, non-referential query passthrough, English referential words, empty conversation, conversation without user messages, long message truncation, and public parse() method integration
  • python3 -c "import ast; ast.parse(...)" syntax verification

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 linked the issue to this PR

@Memtensor-AI Memtensor-AI added area:memory 记忆存储、检索、更新、召回逻辑 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 23, 2026
@Memtensor-AI
Memtensor-AI requested a review from bittergreen July 23, 2026 10:37
@Memtensor-AI

Memtensor-AI commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2154
Task: 56119b2d819611c1
Base: dev-v2.0.24
Head: fix/query-context-drift

🔍 OpenCodeReview found 6 issue(s) in this PR.


1. tests/memories/textual/test_task_goal_parser_context.py (L83-L86)

The truncation assertion is vacuously weak. "A" * 120 is a substring of the full 300-character string, so assert "A" * 120 in result.rephrased_query would pass even if truncation is never applied (i.e., if the full 300 'A's were kept). While len(result.rephrased_query) < 300 partially closes the gap (the un-truncated form would be ~304 chars), it does not directly assert the key invariant. Add a strong negative assertion to make the test meaningful:

assert "A" * 121 not in result.rephrased_query
💡 Suggested Change

Before:

        result = parser._parse_fast("再找找", conversation=conversation, context="")
        # The augmented query should contain the truncated version (last 120 chars)
        assert "A" * 120 in result.rephrased_query
        assert len(result.rephrased_query) < 300

After:

        result = parser._parse_fast("再找找", conversation=conversation, context="")
        # The augmented query should contain the truncated version (last 120 chars)
        assert "A" * 120 in result.rephrased_query
        assert "A" * 121 not in result.rephrased_query  # Verify truncation actually occurred

2. tests/memories/textual/test_task_goal_parser_context.py (L61-L62)

The test only verifies that the prepended context appears in rephrased_query, but never asserts that the original query ("show me more") is also preserved. If the implementation accidentally dropped the query and only kept the context prefix, this test would still pass. Add an assertion for the original query:

assert "show me more" in result.rephrased_query
💡 Suggested Change

Before:

        result = parser._parse_fast("show me more", conversation=conversation, context="")
        assert "Find me a red dress" in result.rephrased_query

After:

        result = parser._parse_fast("show me more", conversation=conversation, context="")
        assert "Find me a red dress" in result.rephrased_query
        assert "show me more" in result.rephrased_query

3. src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py (L98-L106)

False-positive referential detection due to single-character substring matching.

Several hints in _REFERENTIAL_HINTS are single Chinese characters ('也', '还', '再', '他', '她', '它'). Because the check is a plain substring test (h in task_description), any query that contains these characters as part of a non-referential word will be misclassified. For example:

  • '还款计划查询' ("loan repayment plan query") contains '还' → incorrectly augmented
  • '他山之石' (an idiom) contains '他' → incorrectly augmented
  • '也门旅游攻略' ("Yemen travel guide") contains '也' → incorrectly augmented

This silently pollutes the embedding input with irrelevant prior-message context, degrading retrieval quality.

Suggestion: For the single-character hints, consider requiring word-boundary context (e.g., check that the character is preceded/followed by whitespace or punctuation), or use jieba tokenization to check at the word level rather than character level.


4. src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py (L82-L86)

msg["content"] collected without type-guarding for string, causing potential AttributeError on .strip().

The list comprehension guards with msg.get('content') (a truthiness check), which passes for any non-empty, non-None value — including lists (e.g., multi-modal content blocks like [{"type": "text", "text": "..."}]). The collected value is then assigned to last_user_msg and .strip() is called on it at lines 91/93. If content is a list, .strip() raises AttributeError, crashing the entire retrieval path.

Suggestion: Add an isinstance(msg.get('content'), str) guard in the comprehension filter:

recent_user_msgs = [
    msg["content"]
    for msg in conversation
    if isinstance(msg, dict)
    and msg.get("role") == "user"
    and isinstance(msg.get("content"), str)
    and msg["content"].strip()
]
💡 Suggested Change

Before:

            recent_user_msgs = [
                msg["content"]
                for msg in conversation
                if isinstance(msg, dict) and msg.get("role") == "user" and msg.get("content")
            ]

After:

            recent_user_msgs = [
                msg["content"]
                for msg in conversation
                if isinstance(msg, dict)
                and msg.get("role") == "user"
                and isinstance(msg.get("content"), str)
                and msg["content"].strip()
            ]

5. src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py (L98-L102)

_REFERENTIAL_HINTS is a constant reconstructed on every call.

Defining this tuple inside _parse_fast means Python allocates and populates a new tuple object on every invocation. Move it to module level or as a class-level constant to avoid unnecessary allocations and to make it easier to maintain/extend the hint vocabulary.

# At module level
_REFERENTIAL_HINTS = (
    "其他", "别的", "这个", "那个", "它", "他", "她",
    "再", "也", "还", "继续", "上面", "刚说",
    "other", "another", "it", "this", "that", "more",
)

6. src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py (L110-L111)

Tail truncation discards the topic anchor of long prior messages.

last_user_msg[-max_ctx:] keeps the last 120 characters. For messages where the topic appears at the beginning (e.g., 'I am looking for hotels near the Eiffel Tower — can you also find me ...'), the subject 'hotels near the Eiffel Tower' is silently dropped, and only the trailing filler text is prepended to the query. This defeats the purpose of the augmentation.

Consider truncating from the front (head truncation) instead, keeping the first 120 characters which typically contain the topic anchor:

last_user_msg = last_user_msg[:max_ctx]
💡 Suggested Change

Before:

                    if len(last_user_msg) > max_ctx:
                        last_user_msg = last_user_msg[-max_ctx:]

After:

                    if len(last_user_msg) > max_ctx:
                        last_user_msg = last_user_msg[:max_ctx]

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

❌ Automated Test Results: FAILED

Auto-fix retry 1/2 triggered.

Failed tests:

  • test_no_conversation_keeps_original_query
  • test_referential_query_gets_augmented
  • test_non_referential_long_query_not_augmented
  • test_english_referential_query
  • test_empty_conversation
  • test_conversation_without_user_messages
  • test_long_last_user_message_truncated
  • test_parse_method_passes_conversation_in_fast_mode
Error details
Tests failed. Failed cases: test_no_conversation_keeps_original_query, test_referential_query_gets_augmented, test_non_referential_long_query_not_augmented, test_english_referential_query, test_empty_conversation

Branch: fix/query-context-drift

harvey_xiang and others added 3 commits July 23, 2026 19:07
* Fix MemTensor#1540: fix: (MemTensor#1824)

docs(memos-local-plugin): clarify install path and stale dir names (MemTensor#1540)

The README's 'Quick start' section told users to use install.sh instead
of npm install, but the warning was buried and users still tried
'npm install -g @memtensor/memos-local-plugin' first. The reporter in
MemTensor#1540 encountered this on a Hermes deployment.

This change:

- Promotes the 'do not run npm install -g' notice to a prominent
  IMPORTANT callout explaining why global install is wrong (no
  agent-home deploy, no config.yaml, no bridge/viewer) and that the
  tarball intentionally ships built artifacts only.
- Adds a Troubleshooting subsection covering the two specific symptoms
  in the bug report: the 'package not found' misread, and the stale
  web/ and site/ directory names (web/ is now viewer/, site/ was
  removed by commit 26e7e3d).
- Mentions install.ps1 for Windows alongside install.sh.
- CHANGELOG: record the docs fix and reference MemTensor#1540.

Documentation-only change; no code or runtime behavior touched.

Co-authored-by: MemOS AutoDev <autodev@memtensor.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>

* Fix MemTensor#1888: [Bug] test_system_parser.py: SystemParser.__init__() got an unexpected keyword a (MemTensor#1889)

fix: remove invalid chunker parameter from SystemParser test instantiation

- SystemParser.__init__() signature changed to (embedder, llm=None)
- Test was still passing chunker=None causing TypeError
- Fixes all 5 failing tests in test_system_parser.py

Fixes MemTensor#1888

Co-authored-by: MemOS AutoDev <autodev@memos.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>

* Fix MemTensor#1525: [Bug] clean_json_response crashes with cryptic AttributeError when given None (MemTensor#1884)

* test: add comprehensive tests for clean_json_response (issue MemTensor#1525)

- Add test suite in tests/mem_os/test_format_utils.py
- Cover None input ValueError with diagnostic message
- Cover markdown removal, whitespace stripping, edge cases
- Verify fix for AttributeError when LLM returns None

* style: format clean_json_response tests

---------

Co-authored-by: MemOS AutoDev <autodev@memos.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>

* Fix MemTensor#1901: share_cube_with_user passes swapped args to _validate_cube_access — fails for ev (MemTensor#1903)

fix: validate current user not target in share_cube_with_user (MemTensor#1901)

share_cube_with_user(cube_id, target_user_id) called
_validate_cube_access(cube_id, target_user_id), but the validator
signature is (user_id, cube_id). The cube_id therefore landed in the
user_id slot and _validate_user_exists raised
"User '<cube_id>' does not exist or is inactive" for every well-formed
call, making the API unusable.

The in-code comment "Validate current user has access to this cube"
already documented the correct intent: the sharing user (self.user_id)
must have access to the cube being shared, not the target. Switch the
call to self._validate_cube_access(self.user_id, cube_id). The target
user's existence is independently checked on the next line via
validate_user(target_user_id), so that path is unchanged.

Add regression tests in tests/mem_os/test_memos_core.py that pin down:
- validate_user_cube_access is consulted with (self.user_id, cube_id),
- add_user_to_cube is called with (target_user_id, cube_id) on success,
- a missing target raises "Target user '<id>' does not exist".

Closes MemTensor#1901

Co-authored-by: MemOS AutoDev Bot <autodev@memtensor.local>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>

* feat(memos): chunk batch reflection scoring

* Fix MemTensor#1897: bug(memos-local-plugin): Hermes background model status checks can trigger paid  (MemTensor#1899)

* Fix MemTensor#1897: fix(memos-local-plugin): add LLM circuit breaker for terminal provider errors

Issue MemTensor#1897 reported ~12,900 paid LLM requests in 24 h on Hermes
against a DeepSeek key with insufficient balance. The local
`system_model_status` row count (12,900) closely tracked the
provider-side `request_count` (11,344) for the same billing window.
The naming is misleading: `system_model_status` is not a health probe;
it is the audit row written once per LLM call (ok / fallback / error)
inside `core/llm/client.ts`. With no circuit breaker, every pipeline
subscriber (capture / session-relation / reward / L2 / L3 / skill /
retrieval LLM filter / world-model) kept firing on every turn / closed
episode / induction, generating one paid request each.

Add a per-`LlmClient` circuit breaker:

- Trips on terminal errors: HTTP 401/402/403 or messages containing
  `insufficient balance` / `invalid api key` / `unauthorized` /
  `account suspended` / `billing`.
- Open: short-circuits subsequent calls inside the facade without
  contacting the provider. Throws `MemosError(LLM_UNAVAILABLE)` with
  `details.circuitOpen=true` so existing catch blocks still work.
- Half-open after cool-down (default 5 min, configurable, min 30 s):
  next call probes the provider; success closes the breaker, terminal
  failure re-opens it for another cool-down.
- Host fallback rescues a call without tripping the breaker —
  fallback exists precisely to keep going when the primary is down.
- Coalesces `system_model_status="circuit_open"` audit rows to at
  most one per ~25 s while the breaker stays open, so we don't
  replace paid spam with audit-row spam.
- Exposes `circuitOpen` / `circuitOpenUntil` / `circuitOpenedReason`
  via `LlmClientStats` for the Overview viewer card.
- Enabled by default; legacy behaviour available via
  `circuitBreaker.enabled = false`.

Tests: 9 new vitest cases under `tests/unit/llm/client.test.ts`
covering trip on 402, trip on "insufficient balance" message, no
trip on generic transient, coalescing, half-open close on success,
host-fallback rescues without trip, disabled mode, stats fields,
and re-open on terminal probe failure. All 59 LLM and 28 pipeline
tests pass; `tsc --noEmit` clean.

Out of scope (tracked separately): 429 `Retry-After` handling
(issue MemTensor#1620), per-tool rate limits, daily budget caps.

* Fix LLM breaker with host fallback

---------

Co-authored-by: autodev-bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: GU TIANCHUN <96930846+TianchunGu@users.noreply.github.com>
Co-authored-by: Dubberman <48425266+whipser030@users.noreply.github.com>

---------

Co-authored-by: Memtensor-AI <project@memtensor.cn>
Co-authored-by: MemOS AutoDev <autodev@memtensor.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>
Co-authored-by: MemOS AutoDev <autodev@memos.ai>
Co-authored-by: MemOS AutoDev Bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: GU TIANCHUN <96930846+TianchunGu@users.noreply.github.com>
Co-authored-by: Dubberman <48425266+whipser030@users.noreply.github.com>
…nsor#2145)

This reverts commit 21a27ed.

Co-authored-by: jiachengzhen <jiacz@memtensor.cn>
@Zhe-SH-CN
Zhe-SH-CN force-pushed the fix/query-context-drift branch from 0bc447e to b572073 Compare July 23, 2026 11:07
@Zhe-SH-CN
Zhe-SH-CN changed the base branch from main to dev-v2.0.24 July 23, 2026 11:07
@Memtensor-AI Memtensor-AI added area:api 云服务 / FastAPI / OpenAPI / MCP area:core MOS 编排层 / 框架底座 / 跨模块问题 area:docs 文档、示例 area:plugin OpenClaw & Hermes labels Jul 23, 2026
Fixes MemTensor#1365

In fast search mode, TaskGoalParser._parse_fast ignored conversation
history entirely. Referential queries like "再找找其他价格优惠的" were
embedded as-is, causing them to match unrelated older memories instead
of the current conversation topic.

Fix: when conversation history is available and the query looks
referential (short or contains referential hints like 其他/这个/再/more),
prepend the most recent user message to the query before embedding.
This is a zero-LLM-call heuristic that disambiguates context without
adding latency to fast mode.
@Zhe-SH-CN
Zhe-SH-CN force-pushed the fix/query-context-drift branch from b572073 to a5b1378 Compare July 23, 2026 11:17
@Memtensor-AI Memtensor-AI removed area:plugin OpenClaw & Hermes area:api 云服务 / FastAPI / OpenAPI / MCP area:docs 文档、示例 labels Jul 23, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: INCONCLUSIVE

Automated tests inconclusive (auto-generated test defect); treated as non-blocking. Manual review recommended. Details: The AI-generated test passes a tokenizer keyword argument to TaskGoalParser.__init__(), but the class does not accept such a parameter. [advisory, non-gating] AI-generated tests on branch test/auto-gen-56119b2d819611c1-20260723195034: 62/62 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/query-context-drift

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

Labels

area:core MOS 编排层 / 框架底座 / 跨模块问题 area:memory 记忆存储、检索、更新、召回逻辑 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: 记忆漂移

5 participants