## Description Addresses #2195 Finding 1. `extract_memory_query_sources` (the memory retrieval query builder) was extended to harvest text blocks from Anthropic list-shaped user turns — the standard Claude Code shape — but it joins **every** text block in the turn. Claude Code appends `<system-reminder>` harness blocks to essentially every user turn, so those get concatenated into the embedding input alongside the real question. Per the reporter's measurements (`all-MiniLM-L6-v2`): the clean question scored top cosine **0.748** against a stored memory; the same question wrapped in harness boilerplate scored **0.232**. The default `memory_min_similarity` floor is **0.3**, so the diluted query falls under the floor and **nothing is retrieved** — memory silently no-ops for Claude Code clients. The reporter explicitly warned that a naive "concatenate all text blocks" harvest would still retrieve nothing, which is exactly the current behavior. ## Fix Filter out text blocks whose text starts with `<system-reminder` when building `user_text`, so the retrieval query keys on the substantive question and the embedding isn't diluted by harness boilerplate. A turn that is only a system-reminder yields no `user_text` (as before). All other harvesting (tool_result blocks, OpenAI string content, assistant/tool context) is unchanged. Note: the reporter also asked to expose `memory_min_similarity` as an env var / CLI flag (it lives on `ProxyConfig` with no surface today). That is a sensible companion but is a separate config-plumbing change; I kept this PR focused on the retrieval-query bug so it stays easy to review, and I'm happy to follow up with the env/CLI surface. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/proxy/memory_query_policy.py`: in `extract_memory_query_sources`, skip `<system-reminder>` text blocks when assembling the user query from a list-shaped Anthropic user turn. - `tests/test_memory_query_policy.py`: regressions that a system-reminder block is excluded (real question kept) and that a reminder-only turn yields no user text. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_memory_query_policy.py -q 7 passed # with the fix reverted, the two new tests fail: the system-reminder text is # concatenated into user_text (the diluted-query behavior) $ uvx ruff@0.15.17 check headroom/proxy/memory_query_policy.py tests/test_memory_query_policy.py All checks passed! $ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/memory_query_policy.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12, project venv (`uv sync --extra proxy`), `uvx ruff@0.15.17` / `uvx mypy@1.20.2`, pytest in the venv. - Exact command / steps: called `extract_memory_query_sources` with a Claude Code-shaped user turn (real question text block + an appended `<system-reminder>` text block), and with a reminder-only turn; then reverted the source and re-ran. - Observed result: with the fix `user_text` is exactly `"how do I add caching to the auth handler?"` (no `system-reminder` substring), and a reminder-only turn yields `""`; with the fix reverted `user_text` includes the full `<system-reminder>...</system-reminder>` text (the diluted embedding input). Ran against the actual module. - Not tested: an end-to-end embedding + backend retrieval against a live memory DB measuring the cosine recovery (the dilution figures are the reporter's; this change removes the boilerplate from the query text that produces them). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable
This commit is contained in:
@@ -63,11 +63,18 @@ def extract_memory_query_sources(
|
||||
# Anthropic user turns carry the actual prompt as text blocks
|
||||
# ({"type":"text","text":...}), not a plain string. Capture it
|
||||
# so the memory retrieval query keys on the user's question and
|
||||
# not just any tool_result blocks in the same turn.
|
||||
# not just any tool_result blocks in the same turn. Skip Claude
|
||||
# Code's <system-reminder> harness blocks: they are appended to
|
||||
# the same turn and, concatenated into the embedding input,
|
||||
# dilute the user's question below the similarity floor so
|
||||
# nothing is retrieved (#2195). Filtering them keeps the query
|
||||
# on the substantive question.
|
||||
user_text = "\n".join(
|
||||
b.get("text", "")
|
||||
text
|
||||
for b in content
|
||||
if isinstance(b, dict) and b.get("type") == "text"
|
||||
for text in (str(b.get("text", "")).strip(),)
|
||||
if text and not text.startswith("<system-reminder")
|
||||
).strip()
|
||||
if user_text:
|
||||
latest_user = user_text
|
||||
|
||||
@@ -69,6 +69,42 @@ def test_extract_sources_captures_anthropic_user_text_blocks() -> None:
|
||||
assert user_text == "help me refactor auth"
|
||||
|
||||
|
||||
def test_extract_sources_skips_system_reminder_blocks() -> None:
|
||||
"""Claude Code appends <system-reminder> harness blocks to the user turn.
|
||||
Concatenated into the embedding input they dilute the real question below the
|
||||
similarity floor so nothing is retrieved (#2195); they must be filtered out."""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "how do I add caching to the auth handler?"},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "<system-reminder>\nThe user opened file x.\n</system-reminder>",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
user_text, _tool_outputs, _assistant_turns = extract_memory_query_sources(messages)
|
||||
|
||||
assert user_text == "how do I add caching to the auth handler?"
|
||||
assert "system-reminder" not in user_text
|
||||
|
||||
|
||||
def test_extract_sources_reminder_only_turn_yields_no_user_text() -> None:
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "<system-reminder>x</system-reminder>"}],
|
||||
},
|
||||
]
|
||||
|
||||
user_text, _tool_outputs, _assistant_turns = extract_memory_query_sources(messages)
|
||||
|
||||
assert user_text == ""
|
||||
|
||||
|
||||
def test_extract_sources_captures_user_text_alongside_tool_result() -> None:
|
||||
"""A user turn mixing a tool_result and a text block yields both: the text as
|
||||
the user query and the tool output as context."""
|
||||
|
||||
Reference in New Issue
Block a user