Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions hindsight-integrations/claude-code/scripts/lib/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ def strip_memory_tags(content: str) -> str:
return content


def strip_harness_blocks(content: str) -> str:
"""Remove <system-reminder> and <task-notification> blocks.

Claude Code injects these into user turns. They are harness output rather
than conversation, so retaining them teaches Hindsight about hook and tool
plumbing instead of the user's work.
"""
content = re.sub(r"<system-reminder>[\s\S]*?</system-reminder>", "", content)
content = re.sub(r"<task-notification>[\s\S]*?</task-notification>", "", content)
return content


def is_synthetic_user_message(content: str) -> bool:
"""Return True for skill bodies Claude Code persists as user chat.

Claude Code records an invoked skill's full SKILL.md as a normal user
message. Retaining it stores documentation as if the user had written it,
and can create very large retain payloads.
"""
return content.lstrip().startswith("Base directory for this skill: ")


# ---------------------------------------------------------------------------
# Recall: query composition and truncation
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -244,7 +266,7 @@ def _extract_message_blocks(content, role: str = "") -> list:
- Channel message tool_use blocks get their text extracted inline.
"""
if isinstance(content, str):
cleaned = strip_channel_envelope(strip_memory_tags(content)).strip()
cleaned = strip_harness_blocks(strip_channel_envelope(strip_memory_tags(content))).strip()
return [{"type": "text", "text": cleaned}] if cleaned else []

if not isinstance(content, list):
Expand All @@ -257,7 +279,7 @@ def _extract_message_blocks(content, role: str = "") -> list:
block_type = block.get("type", "")

if block_type == "text":
text = strip_channel_envelope(strip_memory_tags(block.get("text", ""))).strip()
text = strip_harness_blocks(strip_channel_envelope(strip_memory_tags(block.get("text", "")))).strip()
if text:
blocks.append({"type": "text", "text": text})

Expand Down Expand Up @@ -342,6 +364,15 @@ def prepare_retention_transcript(

allowed_roles = set(retain_roles or ["user", "assistant"])

target_messages = [
msg
for msg in target_messages
if not (
msg.get("role") == "user"
and is_synthetic_user_message(_extract_text_content(msg.get("content", ""), role="user"))
)
]

if include_tool_calls:
return _prepare_json_transcript(target_messages, allowed_roles)
return _prepare_text_transcript(target_messages, allowed_roles)
Expand Down Expand Up @@ -384,7 +415,7 @@ def _prepare_text_transcript(messages: list, allowed_roles: set) -> tuple:

content = _extract_text_content(msg.get("content", ""), role=role)
content = strip_channel_envelope(content)
content = strip_memory_tags(content).strip()
content = strip_harness_blocks(strip_memory_tags(content)).strip()

if not content:
continue
Expand Down
80 changes: 80 additions & 0 deletions hindsight-integrations/claude-code/tests/test_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
compose_recall_query,
format_current_time,
format_memories,
is_synthetic_user_message,
prepare_retention_transcript,
slice_last_turns_by_user_boundary,
strip_channel_envelope,
strip_harness_blocks,
strip_memory_tags,
truncate_recall_query,
)
Expand Down Expand Up @@ -67,6 +69,54 @@ def test_strips_multiline_block(self):
assert strip_memory_tags(raw).strip() == ""


# ---------------------------------------------------------------------------
# strip_harness_blocks
# ---------------------------------------------------------------------------


class TestStripHarnessBlocks:
def test_strips_system_reminder_block(self):
raw = "before\n<system-reminder>plugin chatter</system-reminder>\nafter"
result = strip_harness_blocks(raw)
assert "plugin chatter" not in result
assert "before" in result
assert "after" in result

def test_strips_task_notification_block(self):
raw = "text <task-notification>background job done</task-notification> text"
result = strip_harness_blocks(raw)
assert "background job done" not in result

def test_strips_multiline_block(self):
raw = "<system-reminder>\nline1\nline2\n</system-reminder>"
assert strip_harness_blocks(raw).strip() == ""

def test_passthrough_clean_text(self):
raw = "no harness blocks here"
assert strip_harness_blocks(raw) == raw

def test_keeps_unpaired_mention(self):
raw = "the <system-reminder> tag is injected by the harness"
assert strip_harness_blocks(raw) == raw


# ---------------------------------------------------------------------------
# is_synthetic_user_message
# ---------------------------------------------------------------------------


class TestIsSyntheticUserMessage:
def test_detects_skill_body(self):
raw = "Base directory for this skill: /tmp/skills/foo\n\n# Foo\n\nDocs."
assert is_synthetic_user_message(raw) is True

def test_detects_leading_whitespace(self):
assert is_synthetic_user_message("\n Base directory for this skill: /tmp/x") is True

def test_rejects_ordinary_message(self):
assert is_synthetic_user_message("what is the base directory for this skill?") is False


# ---------------------------------------------------------------------------
# slice_last_turns_by_user_boundary
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -323,6 +373,36 @@ def test_strips_memory_tags(self):
assert "leaked" not in transcript
assert "actual question" in transcript

def test_strips_harness_blocks(self):
msgs = _msgs(("user", "<system-reminder>plugin chatter</system-reminder> actual question"))
transcript, _ = prepare_retention_transcript(msgs, retain_full_window=True)
assert "plugin chatter" not in transcript
assert "actual question" in transcript

def test_skips_skill_body_user_message(self):
msgs = _msgs(
("user", "Base directory for this skill: /tmp/skills/foo\n\n# Foo\n\nDocs."),
("user", "real question"),
("assistant", "real reply"),
)
transcript, count = prepare_retention_transcript(msgs, retain_full_window=True)
assert "Docs." not in transcript
assert "real question" in transcript
assert count == 2

def test_skips_skill_body_in_json_mode(self):
msgs = _msgs(
("user", "Base directory for this skill: /tmp/skills/foo\n\n# Foo\n\nDocs."),
("user", "real question"),
)
transcript, count = prepare_retention_transcript(msgs, retain_full_window=True, include_tool_calls=True)
assert "Docs." not in transcript
assert count == 1

def test_returns_none_when_only_skill_body(self):
msgs = _msgs(("user", "Base directory for this skill: /tmp/skills/foo\n\n# Foo\n\nDocs."))
assert prepare_retention_transcript(msgs, retain_full_window=True) == (None, 0)

def test_filters_by_retain_roles(self):
msgs = _msgs(("user", "user msg"), ("assistant", "assistant msg"))
transcript, _ = prepare_retention_transcript(msgs, retain_roles=["user"], retain_full_window=True)
Expand Down