Skip to content

Commit 610fff2

Browse files
lee-fuhrclaude
andcommitted
fix(consolidator): strip system-reminder blocks before memory extraction
- Added _SYSTEM_REMINDER_RE regex and _strip_system_reminders() to session_consolidator.py - Applied stripping in extract_conversation_text() for both list and string content types - Root cause: hooks inject memory YAML/JSON as <system-reminder> blocks in user messages; these were fed verbatim to the LLM extractor, producing garbled "Correction:" memories containing frontmatter fragments and JSON field names - Added 3 targeted tests covering text-block, string-content, and multi-reminder cases - Full suite: 3176 passed, 2 skipped, no regressions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 356ee19 commit 610fff2

2 files changed

Lines changed: 81 additions & 5 deletions

File tree

src/session_consolidator.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import json
11+
import re
1112
from dataclasses import dataclass, field
1213
from pathlib import Path
1314
from typing import Any
@@ -21,6 +22,16 @@
2122
from .dedup import deduplicate as _deduplicate_memories
2223
from .dedup import smart_dedup_decision as _smart_dedup_decision
2324

25+
# Strip <system-reminder> blocks injected by hooks — these contain memory YAML/JSON
26+
# and cause the LLM extractor to produce garbled "corrections"
27+
_SYSTEM_REMINDER_RE = re.compile(r'<system-reminder>.*?</system-reminder>', re.DOTALL | re.IGNORECASE)
28+
29+
30+
def _strip_system_reminders(text: str) -> str:
31+
"""Remove injected system-reminder blocks before extraction."""
32+
return _SYSTEM_REMINDER_RE.sub('', text).strip()
33+
34+
2435
# Imported at module level so monkeypatch can target it for tests
2536
try:
2637
from .hook_state import get_session_state
@@ -157,17 +168,19 @@ def extract_conversation_text(self, messages: list[dict[str, Any]]) -> str:
157168
text_parts = []
158169
for block in content:
159170
if isinstance(block, dict) and block.get('type') == 'text':
160-
text = block.get('text', '')
171+
text = _strip_system_reminders(block.get('text', ''))
161172
if text and not _is_garbage_content(text):
162173
text_parts.append(text)
163174
elif isinstance(block, str):
164-
if not _is_garbage_content(block):
165-
text_parts.append(block)
175+
cleaned = _strip_system_reminders(block)
176+
if cleaned and not _is_garbage_content(cleaned):
177+
text_parts.append(cleaned)
166178
if text_parts:
167179
parts.append(f"{role}: {' '.join(text_parts)}")
168180
elif isinstance(content, str):
169-
if not _is_garbage_content(content):
170-
parts.append(f"{role}: {content}")
181+
cleaned = _strip_system_reminders(content)
182+
if cleaned and not _is_garbage_content(cleaned):
183+
parts.append(f"{role}: {cleaned}")
171184

172185
return "\n\n".join(parts)
173186

tests/test_session_consolidator.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,69 @@ def test_extract_conversation_text(self, consolidator, sample_session_file):
8484
assert "client objections" in conversation_text.lower()
8585
assert "acknowledge their concern" in conversation_text.lower()
8686

87+
def test_system_reminders_stripped_from_text_blocks(self, consolidator):
88+
"""system-reminder blocks injected by hooks are stripped before extraction"""
89+
real_content = "We should always delegate to specialist agents before executing work ourselves."
90+
messages = [
91+
{
92+
"role": "user",
93+
"content": [
94+
{
95+
"type": "text",
96+
"text": (
97+
f"{real_content}\n"
98+
"<system-reminder>\n"
99+
"---\n"
100+
"name: voice preference\n"
101+
"type: feedback\n"
102+
"---\n"
103+
"Never use em-dashes.\n"
104+
"</system-reminder>"
105+
)
106+
}
107+
]
108+
}
109+
]
110+
result = consolidator.extract_conversation_text(messages)
111+
assert "<system-reminder>" not in result
112+
assert "name: voice preference" not in result
113+
assert "type: feedback" not in result
114+
assert real_content in result
115+
116+
def test_system_reminders_stripped_from_string_content(self, consolidator):
117+
"""system-reminder stripping works on plain string content too"""
118+
real_content = "Always load the voice skill before drafting communications on Lee's behalf."
119+
messages = [
120+
{
121+
"role": "user",
122+
"content": (
123+
f"{real_content}\n"
124+
"<system-reminder>injected YAML frontmatter here with type: feedback fields</system-reminder>"
125+
)
126+
}
127+
]
128+
result = consolidator.extract_conversation_text(messages)
129+
assert "injected YAML frontmatter here" not in result
130+
assert real_content in result
131+
132+
def test_multiple_system_reminders_stripped(self, consolidator):
133+
"""Multiple system-reminder blocks all removed"""
134+
real_content = "Delegate to the specialist agent before attempting to execute the work directly."
135+
messages = [
136+
{
137+
"role": "user",
138+
"content": (
139+
"<system-reminder>first injected block with memory content</system-reminder> "
140+
f"{real_content} "
141+
"<system-reminder>second block: {\"type\": \"correction\", \"content\": \"garbled\"}</system-reminder>"
142+
)
143+
}
144+
]
145+
result = consolidator.extract_conversation_text(messages)
146+
assert "first injected block" not in result
147+
assert "second block" not in result
148+
assert real_content in result
149+
87150
def test_handle_nonexistent_session(self, consolidator):
88151
"""Handle missing session file gracefully"""
89152
with pytest.raises(FileNotFoundError):

0 commit comments

Comments
 (0)