Skip to content
Merged
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
55 changes: 50 additions & 5 deletions polylogue/sources/parsers/claude/code_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,28 +586,32 @@ def _accumulate_delegation_progress(
item: dict[str, object],
timestamp: str | None,
accumulator: dict[str, _DelegationProgressStats],
) -> None:
) -> bool:
"""Fold one ``progress``/``agent_progress`` record into its dispatch edge.

Only ``data.type == "agent_progress"`` carries a genuine dispatcher edge
(see the classification comment above ``_NON_MESSAGE_SIDECAR_RECORD_TYPES``);
every occurrence under the same ``parentToolUseID`` is one streaming tick
of the same subagent dispatch, so ticks are counted and time-bounded
rather than persisted as one row apiece.
rather than persisted as one row apiece. Returns whether this record
instance folded into a dispatch edge (polylogue-pbuh AC5 coverage
counter) -- the other six ``progress`` subtypes return ``False`` and stay
genuinely transient, see the classification comment.
"""
data = item.get("data")
if not isinstance(data, dict) or data.get("type") != "agent_progress":
return
return False
parent_tool_use_id = _string_field(item, "parentToolUseID")
if not parent_tool_use_id:
return
return False
entry = accumulator.setdefault(parent_tool_use_id, _DelegationProgressStats())
entry.count += 1
if timestamp:
if entry.first_seen is None or timestamp < entry.first_seen:
entry.first_seen = timestamp
if entry.last_seen is None or timestamp > entry.last_seen:
entry.last_seen = timestamp
return True


def _safe_float(value: object) -> float:
Expand Down Expand Up @@ -1293,6 +1297,16 @@ def _parse_code_records(
# non-empty value wins, since it is constant within one file.
session_slug_value: str | None = None
session_refs: list[ParsedSessionRef] = []
# polylogue-pbuh AC5: per-record-type seen/persisted counts for the
# sidecar types this parser used to drop wholesale, plus a bounded
# sample of record types that fell all the way through ordinary message
# parsing to the empty-content drop below with no text/blocks -- so a
# *future* silently-dropped type is visible in the archive (one
# ``claude_parse_coverage`` session_event) instead of requiring another
# rg-the-corpus audit to notice.
sidecar_seen_counts: dict[str, int] = {}
sidecar_persisted_counts: dict[str, int] = {}
empty_drop_counts: dict[str, int] = {}

for index, item in enumerate(records, start=record_index_start + 1):
if not isinstance(item, dict):
Expand Down Expand Up @@ -1401,23 +1415,28 @@ def _parse_code_records(
# produced empty ``tool_result``-shaped message rows before the
# record-type check below existed; see #1617 for that forensic.
if record_type in _NON_MESSAGE_SIDECAR_RECORD_TYPES:
sidecar_seen_counts[record_type] = sidecar_seen_counts.get(record_type, 0) + 1
persisted_this_record = False
if notification is not None:
background_notifications.append((notification, record_uuid, timestamp))
if record_type == "progress":
_accumulate_delegation_progress(item, timestamp, delegation_progress)
persisted_this_record = _accumulate_delegation_progress(item, timestamp, delegation_progress)
else:
if record_type == "ai-title":
ai_title_text = _string_field(item, "aiTitle")
if ai_title_text:
latest_ai_title = ai_title_text
persisted_this_record = True
elif record_type == "custom-title":
custom_title_text = _string_field(item, "customTitle")
if custom_title_text:
latest_custom_title = custom_title_text
persisted_this_record = True
elif record_type == "agent-name":
agent_name_text = _string_field(item, "agentName")
if agent_name_text:
latest_agent_name = agent_name_text
persisted_this_record = True
elif record_type == "pr-link":
# polylogue-2qx.4 / polylogue-cgfy: generalized,
# tracker-agnostic evidence alongside the existing
Expand All @@ -1438,6 +1457,7 @@ def _parse_code_records(
else None,
)
)
persisted_this_record = True
elif record_type == "attachment":
# Subtype-dispatched -- see ``_attachment_sidecar_event``
# and ``_ATTACHMENT_SUBTYPE_EVENT_TYPES`` above. Handled
Expand All @@ -1453,6 +1473,7 @@ def _parse_code_records(
if event_type is not None:
evidence_payload = _sidecar_evidence_payload(record_type, item)
if evidence_payload is not None:
persisted_this_record = True
# Message linkage belongs in the typed field, not buried
# in the payload dict: source_message_provider_id is what
# the archive joins on, and every other claude_* emitter
Expand All @@ -1470,6 +1491,8 @@ def _parse_code_records(
),
)
)
if persisted_this_record:
sidecar_persisted_counts[record_type] = sidecar_persisted_counts.get(record_type, 0) + 1
continue
if timestamp:
created_at = timestamp if created_at is None or timestamp < created_at else created_at
Expand Down Expand Up @@ -1529,6 +1552,7 @@ def _parse_code_records(
and material_origin is MaterialOrigin.HUMAN_AUTHORED
)
if not keep_empty_human_turn:
empty_drop_counts[record_type] = empty_drop_counts.get(record_type, 0) + 1
Comment on lines 1554 to +1555

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound the set of empty-drop record types

For a large or malformed JSONL stream containing many distinct unknown type strings with empty content, this dictionary grows once per unique attacker/provider-controlled value and is later serialized wholesale into the coverage event. Thus the newly described “bounded sample” breaks the parser's memory-bounded streaming guarantee; cap the number of retained type names and aggregate excess entries into an overflow bucket.

AGENTS.md reference: AGENTS.md:L132-L133

Useful? React with 👍 / 👎.

continue
# Paste markers only appear in user prompts; restricting detection to the
# user role avoids false positives from assistant text that quotes a marker.
Expand Down Expand Up @@ -1716,6 +1740,27 @@ def _parse_code_records(
)
)

# polylogue-pbuh AC5: one bounded coverage event per session so a future
# silently-dropped record type is visible without another corpus audit --
# counts for the known sidecar types (seen vs. actually persisted as
# evidence, distinguishing e.g. an ``attachment`` record whose payload
# was empty from one that produced an event) plus a sample of record
# types that reached ordinary message parsing but carried no text/blocks
# and were dropped there (the pre-#1617 failure mode this bead's method
# note warns future readers not to repeat by assumption).
if sidecar_seen_counts or empty_drop_counts:
session_events.append(
ParsedSessionEvent(
event_type="claude_parse_coverage",
Comment on lines +1751 to +1754

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Merge streaming coverage before emitting the session event

When a streamed aggregate returns to the same session after records from another session, _parse_code_records runs once per contiguous chunk and emits this event each time; merge_parsed_session_chunks concatenates those events, while reconcile_code_session_chunks only coalesces background notifications. The resulting session therefore has multiple claude_parse_coverage events with per-chunk counts rather than the promised single event with per-session totals. Aggregate these payloads during reconciliation or defer emission until chunk merging is complete.

AGENTS.md reference: AGENTS.md:L129-L133

Useful? React with 👍 / 👎.

timestamp=updated_at,
payload={
"sidecar_seen": dict(sorted(sidecar_seen_counts.items())),
"sidecar_persisted": dict(sorted(sidecar_persisted_counts.items())),
"empty_dropped_by_record_type": dict(sorted(empty_drop_counts.items())),
},
)
)

title = str(composed_session_id)
title_source: TitleSource | None = None
title_ref: str | None = None
Expand Down
86 changes: 75 additions & 11 deletions tests/unit/sources/test_claude_code_sidecar_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,22 @@
from __future__ import annotations

from polylogue.core.enums import TitleSource
from polylogue.sources.parsers.base import ParsedSession, ParsedSessionEvent
from polylogue.sources.parsers.claude import parse_code

# polylogue-pbuh AC5: every parse now also emits one bounded
# ``claude_parse_coverage`` event per session (seen/persisted counts by
# record type). It is orthogonal to the specific-record-type behavior each
# test below pins, so the assertions here look through it rather than
# hard-coding it into every expected event list -- see
# ``test_parse_coverage_event_reports_seen_and_persisted_counts`` for the
# dedicated coverage-event test.
_COVERAGE_EVENT_TYPE = "claude_parse_coverage"


def _typed_events(session: ParsedSession) -> list[ParsedSessionEvent]:
return [e for e in session.session_events if e.event_type != _COVERAGE_EVENT_TYPE]


def test_ai_title_wins_session_title_over_uuid_fallback() -> None:
"""``ai-title`` must resolve the session title, not just exist as a sidecar.
Expand Down Expand Up @@ -75,7 +89,7 @@ def test_agent_name_persists_as_typed_event() -> None:
[{"type": "agent-name", "sessionId": "sess-agent", "agentName": "orchestration-docs-6np"}],
"sess-agent",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [
("claude_agent_name", {"agent_name": "orchestration-docs-6np", "summary": "orchestration-docs-6np"})
]
Expand Down Expand Up @@ -132,7 +146,7 @@ def test_pr_link_persists_typed_pr_fields() -> None:
],
"sess-pr",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [
(
"claude_pr_link",
Expand All @@ -158,7 +172,7 @@ def test_bridge_session_persists_cross_session_link() -> None:
],
"sess-bridge",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [
(
"claude_bridge_session",
Expand Down Expand Up @@ -192,7 +206,7 @@ def test_file_history_snapshot_persists_tracked_file_count() -> None:
],
"sess-fhs",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [
(
"claude_file_history_snapshot",
Expand All @@ -217,7 +231,7 @@ def test_permission_mode_persists_operational_signal() -> None:
[{"type": "permission-mode", "sessionId": "sess-perm", "permissionMode": "bypassPermissions"}],
"sess-perm",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [
("claude_permission_mode", {"permission_mode": "bypassPermissions", "summary": "bypassPermissions"})
]
Expand All @@ -228,7 +242,7 @@ def test_last_prompt_persists_resume_continuity_signal() -> None:
[{"type": "last-prompt", "sessionId": "sess-lp", "lastPrompt": "hello world."}],
"sess-lp",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [("claude_last_prompt", {"last_prompt": "hello world.", "summary": "hello world."})]


Expand All @@ -254,7 +268,7 @@ def test_queue_operation_enqueue_persists_content_dequeue_does_not() -> None:
],
"sess-queue",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [
(
"claude_queue_operation",
Expand Down Expand Up @@ -282,7 +296,7 @@ def test_attachment_file_subtype_gets_its_own_event_type() -> None:
],
"sess-att",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [
(
"claude_attachment_file",
Expand Down Expand Up @@ -549,7 +563,7 @@ def test_progress_bash_progress_and_hook_progress_stay_transient() -> None:
},
]
parsed = parse_code(records, "sess-bash")
assert parsed.session_events == []
assert _typed_events(parsed) == []
assert parsed.messages == []


Expand Down Expand Up @@ -587,7 +601,7 @@ def test_file_history_delta_persists_tracking_path() -> None:
],
"sess-delta",
)
events = [(e.event_type, e.payload) for e in parsed.session_events]
events = [(e.event_type, e.payload) for e in _typed_events(parsed)]
assert events == [
(
"claude_file_history_delta",
Expand Down Expand Up @@ -763,5 +777,55 @@ def test_init_and_mode_records_produce_no_events_or_messages() -> None:
],
"sess-init",
)
assert parsed.session_events == []
assert _typed_events(parsed) == []
assert parsed.messages == []


def test_parse_coverage_event_reports_seen_and_persisted_counts() -> None:
"""polylogue-pbuh AC5: coverage is reported per type -- seen vs. actually
persisted -- so a future silently-dropped record type is visible in the
archive itself rather than requiring another corpus rg audit to notice.

``permission-mode`` here always persists (its record always carries a
``permissionMode`` string), while a ``bash_progress`` tick under
``progress`` is seen but never persisted (see the classification comment
above ``_SKIPPED_SIDECAR_RECORD_TYPES``) -- pinning that seen and
persisted counts can genuinely diverge, not just mirror each other.
"""
parsed = parse_code(
[
{"type": "permission-mode", "sessionId": "sess-cov", "permissionMode": "plan"},
{"type": "permission-mode", "sessionId": "sess-cov", "permissionMode": "acceptEdits"},
{
"type": "progress",
"sessionId": "sess-cov",
"toolUseID": "bash-progress-0",
"parentToolUseID": "toolu_bash_1",
"data": {"type": "bash_progress", "output": "", "elapsedTimeSeconds": 1},
},
],
"sess-cov",
)
coverage_events = [e for e in parsed.session_events if e.event_type == _COVERAGE_EVENT_TYPE]
assert len(coverage_events) == 1
payload = coverage_events[0].payload
assert payload["sidecar_seen"] == {"permission-mode": 2, "progress": 1}
assert payload["sidecar_persisted"] == {"permission-mode": 2}
assert payload["empty_dropped_by_record_type"] == {}


def test_parse_coverage_event_absent_when_only_ordinary_messages_parsed() -> None:
"""A session with no sidecar/empty-drop activity gets no coverage event
at all -- keeps the common case from carrying a useless empty payload."""
parsed = parse_code(
[
{
"type": "user",
"uuid": "u1",
"sessionId": "sess-plain",
"message": {"role": "user", "content": "plain session, nothing skipped"},
},
],
"sess-plain",
)
assert [e for e in parsed.session_events if e.event_type == _COVERAGE_EVENT_TYPE] == []