Skip to content

feat(sources): split collapsed Claude Code attachment sidecar events - #3423

Merged
Sinity merged 2 commits into
masterfrom
feature/sources/claude-sidecar-attachment-fidelity
Jul 31, 2026
Merged

feat(sources): split collapsed Claude Code attachment sidecar events#3423
Sinity merged 2 commits into
masterfrom
feature/sources/claude-sidecar-attachment-fidelity

Conversation

@Sinity

@Sinity Sinity commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the three gaps flagged in the _SKIPPED_SIDECAR_RECORD_TYPES disposition audit (polylogue-pbuh follow-up): the attachment record type's 20+ subtypes were collapsed into one claude_attachment session_event; init/mode were dismissed as transient without a fresh re-check; and the frozenset's own name ("skipped") had been wrong since most of its members started persisting as session_events.

Problem

Gap 1 (attachment collapse). code_parser.py's own comment conceded it: "20 distinct attachment.type payloads incl. real files, edited-file records, diagnostics; per-subtype fidelity split is a follow-up, not this pass." A full-corpus enumeration (~/.claude/projects, ~11,700 session files, 2026-07-31) found 38 distinct subtypes, 87,539 total occurrences — a real referenced file's content and a hook_success ping were both landing as event_type="claude_attachment", distinguishable only by JSON-inspecting payload["type"].

Gap 2 (init/mode dismissal). permission-mode was kept ("real operational signal, varies") while its sibling mode was dropped as transient, on the strength of a single earlier pass. That needed independent re-verification, not a repeated assertion.

Gap 3 (misleading name). _SKIPPED_SIDECAR_RECORD_TYPES said "skipped." 13 of its 15 members are persisted as session_events today (polylogue-pbuh). The name described the pre-polylogue-pbuh behavior only.

Solution

Gap 1 — attachment subtype dispatch (_attachment_sidecar_event, _ATTACHMENT_SUBTYPE_EVENT_TYPES)

Full per-subtype disposition (live corpus, ~/.claude/projects, 2026-07-31; counts are subtype occurrences, not full-corpus row totals — see Verification for the total):

subtype count disposition event_type
file 711 EVIDENCE — real referenced file, full content claude_attachment_file
edited_text_file 2,446 EVIDENCE — editor-buffer snippet at edit time claude_attachment_edited_file
nested_memory 487 EVIDENCE — nested CLAUDE.md/memory content claude_attachment_nested_memory
plan_file_reference 43 EVIDENCE — full plan file content claude_attachment_plan_reference
compact_file_reference 460 EVIDENCE — bare path reference, no content claude_attachment_file_reference
directory 16 EVIDENCE — directory listing claude_attachment_directory_listing
hook_success 27,678 EVIDENCE — hook lifecycle (outcome=success) claude_hook_event
hook_non_blocking_error 558 EVIDENCE — hook lifecycle (outcome=non-blocking error) claude_hook_event
hook_blocking_error 60 EVIDENCE — hook lifecycle (outcome=blocking error) claude_hook_event
hook_cancelled 303 EVIDENCE — hook lifecycle (outcome=cancelled) claude_hook_event
hook_system_message 129 EVIDENCE — hook lifecycle (system message) claude_hook_event
hook_additional_context 1,003 EVIDENCE — hook lifecycle (injected context) claude_hook_event
auto_mode 644 EVIDENCE — agent-mode transition claude_agent_mode_event
auto_mode_exit 130 EVIDENCE — agent-mode transition claude_agent_mode_event
plan_mode 84 EVIDENCE — agent-mode transition claude_agent_mode_event
plan_mode_exit 99 EVIDENCE — agent-mode transition claude_agent_mode_event
plan_mode_reentry 15 EVIDENCE — agent-mode transition claude_agent_mode_event
deferred_tools_delta 2,856 EVIDENCE, bounded (names/counts, drops body text) claude_capability_delta
mcp_instructions_delta 1,050 EVIDENCE, bounded claude_capability_delta
agent_listing_delta 200 EVIDENCE, bounded claude_capability_delta
skill_listing 2,690 EVIDENCE, bounded (names extracted, not full text) claude_capability_snapshot
invoked_skills 117 EVIDENCE, bounded claude_capability_snapshot
output_style 10,055 EVIDENCE — real operational signal (style mode) claude_output_style
command_permissions 570 EVIDENCE — real operational signal (allowed tools) claude_command_permissions
queued_command 7,387 EVIDENCE — same entity as top-level queue-operation, reused type claude_queue_operation
task_status 80 EVIDENCE — polled background task status claude_task_status
task_reminder 19,751 EVIDENCE — 37% non-empty, carries real todo id/subject/status/blocks/blockedBy claude_task_reminder
diagnostics 1,583 EVIDENCE, bounded (per-file finding counts, not full LSP messages) claude_diagnostics
goal_status 367 EVIDENCE — sentinel goal condition claude_agent_goal_status
date_change 163 EVIDENCE — session crossed a calendar day claude_date_change
read_truncation_notice 97 EVIDENCE — a Read tool output was truncated claude_read_truncation_notice
ultrathink_effort 11 EVIDENCE — explicit reasoning-effort signal claude_agent_effort
structured_output 1 EVIDENCE — rare, potentially meaningful claude_structured_output
max_turns_reached 1 EVIDENCE — session hit a turn budget claude_max_turns_reached
total_tokens_reminder 5,677 TRANSIENT — constant string "<total_tokens>Infinite tokens left</total_tokens>" in every sample — (dropped)
todo_reminder 5 TRANSIENT — always {"content": [], "itemCount": 0} in every occurrence observed — (dropped)
context_tip 11 TRANSIENT — CLI feature-adoption UI hints, not session evidence — (dropped)
companion_intro 1 TRANSIENT — novelty/branding record (pet companion name) — (dropped)

Grouping rule: subtypes reporting the same real-world entity (a hook firing, an agent-mode transition, a capability-surface delta) share one event_type distinguished by payload fields — not 38 near-empty types. Real file/reference content gets its own event_type per subtype since each is a structurally distinct artifact.

An unrecognized future subtype (39th+) routes to claude_attachment_unclassified rather than silently merging into a known bucket — FAIL LOUD, independent of whether #3419's claude_parse_coverage event lands first.

Deliberately not wired into ParsedAttachment/the blob store this pass: _acquire_attachment_blob in storage/sqlite/archive_tiers/write.py raises on inline_bytes unless routed through an archive-owned blob publisher, and write.py is out of this lane's scope (owned by #3419's neighboring work). Real file content (file, edited_text_file, nested_memory, plan_file_reference) is kept at full fidelity as session_events payloads instead — promoting these to first-class attachments rows is a bigger, separately-scoped change through the blob-publisher path.

Capability-delta/skill-listing/diagnostics payloads are bounded (names/counts, not full injected instruction text or full LSP message text) — same precedent as the existing _tool_execution_result_payload (hunk counts, not full diffs).

Gap 2 — init/mode re-verification

Re-ran the corpus scan independently (not just re-asserting the prior comment): init — 0 occurrences (confirmed twice now); mode — 21,545/21,545 (100%) literal "normal" (up from 20,779 total on the prior pass, same zero-variance result). Disposition unchanged; the comment now cites two independent measurements instead of one, and explicitly contrasts with permission-mode (5 distinct values, kept).

Gap 3 — rename

_SKIPPED_SIDECAR_RECORD_TYPES_NON_MESSAGE_SIDECAR_RECORD_TYPES. Hard rename, no compat alias (repo policy). Updated the one other reference (origin_specs.py's admission-declaration comment) and an illustrative event-type mention in api/archive.py's docstring.

Verification

  • devtools test tests/unit/sources/test_claude_code_sidecar_evidence.py tests/unit/sources/test_parsers_claude_code_artifacts.py tests/unit/sources/test_assembly_claude_code_history.py tests/unit/sources/test_source_laws.py204 passed.
  • devtools verify --quick (ruff format/check, mypy --strict, render all --check, topology/layering/closure-matrix/schema-versioning/schema-promotion gates) — exit 0.
  • ruff format --check / ruff check / mypy on the touched file individually — clean.
  • Projected row counts (no session inflation — attachment records only, never new sessions): total attachment session_events emitted goes from 87,539 (all event_type="claude_attachment") to 81,845 across ~22 event_types, i.e. a net reduction of 5,694 rows (the 4 confirmed-transient subtypes now correctly emit nothing instead of noise). init/mode disposition is unchanged (still 0 / still dropped) so no additional rows from Gap 2.

Out of scope (explicitly, per instructions)

  • The progress record type's disposition (only agent_progress persists; the other six subtypes are superseded streaming ticks) — untouched, per instructions.
  • Promoting real-file-content attachment subtypes (file, edited_text_file, nested_memory, plan_file_reference) to first-class ParsedAttachment/blob-store rows — needs the archive-owned blob-publisher wiring in storage/sqlite/archive_tiers/write.py, which this lane was told to avoid. Filing a follow-up bead is reasonable next-session work if the operator wants that fidelity bump.
  • #3419's claude_parse_coverage seen/persisted counter — independent PR, not merged as of this branch; this PR's fail-loud mechanism (claude_attachment_unclassified) works standalone and composes cleanly if feat(sources): report per-type sidecar coverage for Claude Code parse #3419 lands first or second.

Sinity and others added 2 commits July 31, 2026 07:09
Problem: the Claude Code JSONL parser collapsed all attachment.type
subtypes into one claude_attachment session_event, so a real referenced
file and a hook_success ping were structurally indistinguishable at the
event_type level. A full-corpus enumeration (~11.7K session files,
~/.claude/projects, 2026-07-31) found 38 distinct subtypes (up from the
20 the prior pass estimated), of which 4 carry zero information content
in every occurrence observed (total_tokens_reminder is a constant string,
todo_reminder is always empty, context_tip/companion_intro are UI/novelty
chatter unrelated to session evidence).

Solution: _attachment_sidecar_event dispatches on the nested
attachment.type to one of ~22 semantically grouped event_types (real
file/reference content gets its own type per structurally distinct
artifact; hook lifecycle pings share one type distinguished by outcome;
mode transitions, capability deltas, task/todo evidence, and small
single-field signals are grouped analogously). An unrecognized future
subtype routes to claude_attachment_unclassified rather than silently
merging into a known bucket (FAIL LOUD). queued_command (an attachment
subtype) reuses the existing claude_queue_operation event_type since it
is the same message-queue entity as the top-level queue-operation record
type, just reached through a different provider code path.

Also renamed _SKIPPED_SIDECAR_RECORD_TYPES -> _NON_MESSAGE_SIDECAR_RECORD_TYPES:
13 of its 15 members persist as session_events today, so "skipped" has
described the pre-polylogue-pbuh behavior for several PRs. Hard rename,
no compat alias.

Re-verified init/mode (previously dismissed as transient) against the
current corpus: init still has 0 occurrences, mode is still 100%
"normal" (21,545/21,545) -- disposition unchanged but now backed by a
second independent measurement instead of the original single pass.

No new row-count inflation: total attachment session_events emitted goes
from 87,539 (all one type) to 81,845 across ~22 types (5,694 confirmed-
transient records now correctly emit nothing instead of noise).

Verification: devtools test tests/unit/sources/test_claude_code_sidecar_evidence.py
tests/unit/sources/test_parsers_claude_code_artifacts.py
tests/unit/sources/test_assembly_claude_code_history.py (68 passed);
devtools verify --quick (ruff format/check, mypy --strict, render all
--check, all quick gates) exit 0.

Co-Authored-By: Claude <noreply@anthropic.com>
… payloads

The previous commit's comments claimed deferred_tools_delta,
mcp_instructions_delta, agent_listing_delta, skill_listing, invoked_skills,
and diagnostics were bounded (names/counts, not full text), but the
implementation still fell through to the generic dict(attachment)
pass-through -- an unverified claim the diff didn't support. Added
dedicated bounded builders (_bounded_delta_payload,
_bounded_capability_snapshot_payload, _bounded_diagnostics_payload) so the
event payload matches what the comments actually describe: names/counts
for capability deltas and skill listings, per-file finding counts for
diagnostics, matching the existing bounded-summary precedent in
_tool_execution_result_payload (hunk counts, not full diffs).

Verification: devtools test tests/unit/sources/test_claude_code_sidecar_evidence.py
(28 passed, 3 new tests pin the bounded shapes); ruff format --check /
ruff check / mypy --strict on the touched file all clean.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Sinity, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d0802330-3517-4282-bcda-2117f05da1b9

📥 Commits

Reviewing files that changed from the base of the PR and between 561a14f and e8944eb.

📒 Files selected for processing (4)
  • polylogue/api/archive.py
  • polylogue/sources/origin_specs.py
  • polylogue/sources/parsers/claude/code_parser.py
  • tests/unit/sources/test_claude_code_sidecar_evidence.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8944ebd0d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Real file/reference content -- each subtype is a structurally distinct
# artifact, so each gets its own event_type (no collapsing these into
# each other, let alone into the hook/capability buckets below).
"file": "claude_attachment_file", # 711: a real referenced file's full content

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bump the index schema for the parser semantic change

When an existing archive upgrades, these new event types will not be materialized for previously ingested Claude Code sessions: this commit changes parser output from claude_attachment to subtype-specific events, but leaves INDEX_SCHEMA_VERSION at 47 with no SEMANTIC_REPARSE declaration, while the live parser fingerprint also remains unchanged. Consequently, unchanged source files retain the old rows indefinitely, producing a mixed taxonomy where only newly changed or manually reprocessed sessions expose the feature. Add a declared semantic-reparse index bump so existing raw evidence is replayed on upgrade.

AGENTS.md reference: AGENTS.md:L169-L175

Useful? React with 👍 / 👎.

@Sinity
Sinity merged commit f840653 into master Jul 31, 2026
3 checks passed
@Sinity
Sinity deleted the feature/sources/claude-sidecar-attachment-fidelity branch July 31, 2026 07:49
Sinity added a commit that referenced this pull request Jul 31, 2026
…nts (#3437)

## Summary

Restores master to green. Two independently-correct PRs — #3419
(per-type sidecar coverage) and #3423 (attachment sidecar split) —
produced a broken combination, and the merge that joined them was
verified against a narrower test selection than the change warranted.

## Problem

`devtools test -k "sidecar or code_parser"` on master: **7 failed, 138
passed**.

All seven are `test_attachment_*` in
`tests/unit/sources/test_claude_code_sidecar_evidence.py`. They assert
exact equality on `parsed.session_events`; #3419 unconditionally appends
a `claude_parse_coverage` event, so every expected list gained an
unexpected member:

```
Left contains one more item: ('claude_parse_coverage',
  {'sidecar_seen': {'attachment': 1}, 'sidecar_persisted': {}, ...})
```

That payload also exposes a real defect rather than only a
test-reconciliation problem: `sidecar_persisted` is empty while
`sidecar_seen` counts the record. The attachment branch appends a
`session_event` but never sets `persisted_this_record`, so per-type
coverage understated persistence for every attachment record — the exact
metric #3419 exists to report.

## Solution

- `polylogue/sources/parsers/claude/code_parser.py` — the attachment
branch sets `persisted_this_record = True` when it emits an event,
matching every sibling branch in the same dispatch.
- `tests/unit/sources/test_claude_code_sidecar_evidence.py` — the seven
exact-equality assertions exclude `claude_parse_coverage`. It is a
parse-level report orthogonal to the attachment evidence each test is
about; filtering keeps each test pinned to its own subject rather than
loosening the assertion.

## Verification

```
devtools test -k "sidecar or code_parser"
  before:  7 failed, 138 passed
  after:       145 passed
```

`devtools test tests/unit/sources/test_claude_code_sidecar_evidence.py`
→ 30 passed.

## Note

The conflict resolution that produced this was verified with `devtools
test tests/unit/sources/test_assembly_claude_code_history.py` (14
passed) — a file that does not exercise the attachment path. A
cross-feature merge deserves the affected-area selection, not the file
the conflict happened to sit in.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant