feat(server): filter system-injected messages from Claude conversations
Add _is_system_injected() filter to conversation.py that drops user
messages starting with known system-injected prefixes. These messages
(hook outputs, system reminders, teammate notifications) appear in
JSONL session logs as type: "user" with string content but are not
human-typed input.
Filtered prefixes:
- <system-reminder> — Claude Code system context injection
- <local-command-caveat> — local command hook output
- <available-deferred-tools> — deferred tool discovery messages
- <teammate-message — team agent message delivery (no closing >
because tag has attributes)
This brings Claude parsing to parity with the Codex parser, which
already filters system-injected content via SKIP_PREFIXES (line 222).
The filter uses str.startswith(tuple) with lstrip() to handle leading
whitespace. Applied at line 75 in the existing content-type guard chain.
Affects both chat display and input history navigation — system noise
is removed at the source so all consumers benefit.
tests/test_conversation.py:
- TestIsSystemInjected: 10 unit tests for the filter function covering
each prefix, leading whitespace, normal messages, mid-string tags,
empty strings, slash commands, and multiline content
- TestClaudeSystemInjectedFiltering: 5 integration tests through the
full parser verifying exclusion, sequential ID preservation after
filtering, and all-system-message edge case
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,21 @@ import os
|
||||
|
||||
from amc_server.config import EVENTS_DIR
|
||||
|
||||
# Prefixes for system-injected content that appears as user messages
|
||||
# but was not typed by the human (hook outputs, system reminders, etc.)
|
||||
_SYSTEM_INJECTED_PREFIXES = (
|
||||
"<system-reminder>",
|
||||
"<local-command-caveat>",
|
||||
"<available-deferred-tools>",
|
||||
"<teammate-message",
|
||||
)
|
||||
|
||||
|
||||
def _is_system_injected(content):
|
||||
"""Return True if user message content is system-injected, not human-typed."""
|
||||
stripped = content.lstrip()
|
||||
return stripped.startswith(_SYSTEM_INJECTED_PREFIXES)
|
||||
|
||||
|
||||
class ConversationMixin:
|
||||
def _serve_events(self, session_id):
|
||||
@@ -57,7 +72,7 @@ class ConversationMixin:
|
||||
if msg_type == "user":
|
||||
content = entry.get("message", {}).get("content", "")
|
||||
# Only include actual human messages (strings), not tool results (arrays)
|
||||
if content and isinstance(content, str):
|
||||
if content and isinstance(content, str) and not _is_system_injected(content):
|
||||
messages.append({
|
||||
"id": f"claude-{session_id[:8]}-{msg_id}",
|
||||
"role": "user",
|
||||
|
||||
Reference in New Issue
Block a user