Skip to content

Include Claude Code subagent transcripts - #209

Merged
mike1858 merged 2 commits into
mainfrom
fix/claude-subagent-transcripts
Jul 21, 2026
Merged

mike1858 merged 2 commits into
mainfrom
fix/claude-subagent-transcripts

Conversation

@mike1858

@mike1858 mike1858 commented Jul 21, 2026 •

Copy link
Copy Markdown
Member

Why

Claude Code writes Task, Explore, general-purpose, and custom-agent transcripts beneath ~/.claude/projects/<project>/<session>/subagents/**. Splitrail only discovered main transcripts at ~/.claude/projects/<project>/<session>.jsonl, so subagent-heavy workflows could omit most Claude messages and token usage from local statistics, live TUI updates, uploads, and leaderboard totals.

Simply lifting the directory-depth cap is insufficient. Main and subagent transcripts can repeat the same Claude response, while Splitrail's TUI cache and incremental upload paths calculate per-source contributions. Those paths need the same cross-file local_hash and token-fingerprint semantics as complete CLI statistics or the newly discovered files can inflate totals.

What changed

  • Discover main Claude transcripts plus JSONL files under <project>/<session>/subagents/**, while rejecting metadata and unrelated nested JSONL formats.
  • Use the same transcript classifier for cold-start discovery, availability checks, watcher validation, and advertised glob patterns.
  • Prune unrelated directory trees during recursive discovery and preserve readable sources when an individual path cannot be traversed.
  • Keep SQLite history pruning disabled after an incomplete discovery so a transient unreadable subtree cannot erase retained usage.
  • Derive nested transcript project identity from the top-level Claude project slug.
  • Order main transcripts before subagent sources and assign canonical, deduplicated messages to one source before constructing TUI contributions.
  • Preserve Claude's existing deduplication behavior: merge shared local_hash records, accumulate distinct token fingerprints once, and keep records without a local_hash independent.
  • Rebuild Claude's analyzer-wide view after watched source changes because one file can affect cross-source message ownership.
  • Load the complete canonical Claude message set for incremental uploads instead of deduplicating dirty files in isolation.
  • Add a success-gated, one-time Claude upload backfill so existing users receive historical subagent usage that predates last_date_uploaded; preserve monotonic upload checkpoints and retry the backfill after failures.
  • Add regression coverage for path classification, recursive discovery, project identity, deterministic source ownership, and main/subagent token accounting.

User impact

Claude Code users who rely on subagents will see previously omitted usage appear after upgrading. Local totals should increase to include subagent work without double-counting responses repeated in main transcripts. Existing configured Cloud users will perform one canonical Claude history backfill; stable message hashes allow already-uploaded records to be deduplicated server-side.

Validation

  • cargo build --quiet
  • cargo test --quiet — 393 passed
  • cargo clippy --quiet -- -D warnings
  • cargo doc --quiet
  • cargo fmt --all --quiet --check
  • git diff --check

Summary by CodeRabbit

  • New Features

    • Claude Code usage tracking now discovers transcripts from nested subagent sessions.
    • Subagent activity is included in project usage totals.
  • Bug Fixes

    • Prevented duplicate transcript records from inflating token and usage totals.
    • Improved project identification across main and nested subagent transcripts.
    • File changes and deletions now update statistics more reliably, including cases requiring a full refresh.
    • Upload progress and backfill state are preserved more safely across updates.

Discover nested Claude subagent JSONL files and preserve their top-level
project identity. Reconcile cross-file deduplication across CLI, TUI,
watcher, history, and upload paths, and backfill newly visible Claude
usage for existing cloud users after a successful upload.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Claude Code now discovers nested subagent transcripts, deduplicates cross-source messages, and requests full reloads when source changes affect shared contributions. Watcher, analyzer upload preparation, and persisted backfill state are updated to support this behavior.

Changes

Claude reload and backfill

Layer / File(s) Summary
Analyzer reload and cache contracts
src/analyzer.rs
Adds analyzer and registry full-reload capabilities, centralizes source-state removal, and reloads complete analyzer stats when required.
Claude transcript discovery and deduplication
src/analyzers/claude_code.rs, src/analyzers/claude_code_history.rs, src/analyzers/tests/claude_code.rs
Discovers nested subagent transcripts, preserves project hashes, deduplicates shared records, controls missing-session pruning, and adds coverage for these behaviors.
Watcher full-reload integration
src/watcher.rs
Applies full-reload handling to Claude file changes and deletions while retaining incremental cache paths for other analyzers.
Backfill state and upload checkpoints
src/config.rs, src/upload.rs
Persists Claude backfill completion, preserves existing upload state during checkpoint saves, and gates Claude upload filtering and completion updates.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FileWatcher
  participant RealtimeStatsManager
  participant AnalyzerRegistry
  participant ClaudeCodeAnalyzer
  participant UploadState
  participant BackgroundUpload
  FileWatcher->>RealtimeStatsManager: FileChanged or FileDeleted
  RealtimeStatsManager->>AnalyzerRegistry: Check full-reload capability
  AnalyzerRegistry->>ClaudeCodeAnalyzer: Reload complete stats
  ClaudeCodeAnalyzer->>AnalyzerRegistry: Canonical messages
  BackgroundUpload->>UploadState: Load backfill state
  BackgroundUpload->>BackgroundUpload: Select Claude messages
  BackgroundUpload->>UploadState: Mark backfill completed
Loading

Possibly related PRs

Poem

A rabbit watched the transcripts grow,
Through nested paths where sidechains flow.
Shared hops merged, no bytes inflated,
Full reloads kept the cache updated.
Backfill flags now softly glow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding support for Claude Code subagent transcripts.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/claude-subagent-transcripts

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/analyzers/claude_code.rs (1)

209-213: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

is_available() does a full discovery scan instead of short-circuiting.

The trait doc explicitly invites overriding is_available() to stop after finding one file, but this implementation still runs the entire discover_sources_in traversal (allocating and sorting the full Vec<DataSource>) just to check non-emptiness — and as a side effect it overwrites discovery_was_complete from this unrelated scan (compounding the concern raised above).

⚡ Early-exit availability check
 fn is_available(&self) -> bool {
     Self::data_dir()
         .filter(|directory| directory.is_dir())
-        .is_some_and(|projects_dir| !self.discover_sources_in(&projects_dir).is_empty())
+        .is_some_and(|projects_dir| {
+            WalkDir::new(&projects_dir)
+                .min_depth(2)
+                .into_iter()
+                .filter_entry(|entry| is_claude_transcript_tree_path(&projects_dir, entry.path()))
+                .filter_map(Result::ok)
+                .any(|entry| {
+                    entry.file_type().is_file()
+                        && is_claude_transcript_path(&projects_dir, entry.path())
+                })
+        })
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/analyzers/claude_code.rs` around lines 209 - 213, Update
ClaudeCodeAnalyzer::is_available to perform a lightweight early-exit search for
the first discoverable source instead of calling discover_sources_in and
building the full sorted Vec<DataSource>. Ensure this availability-only check
does not mutate discovery_was_complete, while preserving the existing
data-directory validation and boolean result.
src/upload.rs (1)

516-542: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Magic string duplicates ClaudeCodeAnalyzer::display_name().

analyzer_stats.analyzer_name == "Claude Code" re-derives a literal that's already defined as ClaudeCodeAnalyzer::display_name() in src/analyzers/claude_code.rs. If that display name ever changes, this comparison silently stops matching (no compile error), quietly breaking the backfill gate.

♻️ Reference a shared constant instead of a literal
-            if backfill_claude && analyzer_stats.analyzer_name == "Claude Code" {
+            if backfill_claude
+                && analyzer_stats.analyzer_name == crate::analyzers::claude_code::ClaudeCodeAnalyzer::DISPLAY_NAME
+            {

(requires exposing a pub const DISPLAY_NAME: &str = "Claude Code"; used by both display_name() and this comparison)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/upload.rs` around lines 516 - 542, Replace the duplicated "Claude Code"
literal in the upload-state backfill comparison with a shared public constant
exposed by ClaudeCodeAnalyzer, such as DISPLAY_NAME. Update
ClaudeCodeAnalyzer::display_name() to use that constant, then reference it in
the comparison so both paths stay synchronized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/analyzer.rs`:
- Around line 783-800: The full-reload analyzer messages are computed twice
during one watcher event. Update src/watcher.rs lines 167-199 so
reload_analyzer_stats passes the messages from its get_stats call through
trigger_auto_upload_if_enabled, and update src/analyzer.rs lines 783-800 so
load_messages_for_upload reuses those messages instead of calling get_stats
again; preserve the existing incremental parsing behavior for non-full-reload
analyzers.

---

Nitpick comments:
In `@src/analyzers/claude_code.rs`:
- Around line 209-213: Update ClaudeCodeAnalyzer::is_available to perform a
lightweight early-exit search for the first discoverable source instead of
calling discover_sources_in and building the full sorted Vec<DataSource>. Ensure
this availability-only check does not mutate discovery_was_complete, while
preserving the existing data-directory validation and boolean result.

In `@src/upload.rs`:
- Around line 516-542: Replace the duplicated "Claude Code" literal in the
upload-state backfill comparison with a shared public constant exposed by
ClaudeCodeAnalyzer, such as DISPLAY_NAME. Update
ClaudeCodeAnalyzer::display_name() to use that constant, then reference it in
the comparison so both paths stay synchronized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 180ba064-5e73-4653-9e56-a20bfb15b4e0

📥 Commits

Reviewing files that changed from the base of the PR and between e2f1959 and 3e4c739.

📒 Files selected for processing (7)
  • src/analyzer.rs
  • src/analyzers/claude_code.rs
  • src/analyzers/claude_code_history.rs
  • src/analyzers/tests/claude_code.rs
  • src/config.rs
  • src/upload.rs
  • src/watcher.rs

Comment thread src/analyzer.rs
Pass canonical messages from analyzer-wide watcher reloads into the upload path instead of reparsing the full Claude corpus. Short-circuit availability checks and share the Claude display name used by the backfill gate.
@mike1858
mike1858 merged commit 658a746 into main Jul 21, 2026
6 checks passed
@mike1858
mike1858 deleted the fix/claude-subagent-transcripts branch July 21, 2026 14:47
NickAme03 added a commit to NickAme03/splitrail that referenced this pull request Jul 31, 2026
…ng snapshots

A streaming assistant message is written as several JSONL records under one
(requestId, message.id) key. Only output_tokens grows between snapshots; the
input and cache fields are identical on every record. merge_message_into summed
every token field whenever a record carried a fingerprint not seen before, so
those identical fields were counted once per snapshot, and cost followed.

Main transcripts repeat the final usage on every record, so the fingerprint
never changes there and the rule was harmless. Subagent transcripts do not, and
they became reachable when Piebald-AI#209 lifted the depth cap, which is what armed this.

Selection now keeps the whole record with the higher output_tokens, falling back
to the four-field total when output ties. A per-field maximum would assemble a
record that was never written, and cost derives from those fields, so the
resulting cost would correspond to no observation. The comment states the
protocol assumption the rule rests on, so a future writer path emitting additive
chunks has somewhere obvious to start.

The non-token counters are merged with max on both paths now rather than summed
on one of them: they are per-message totals restated on every record, not
per-record increments.

Three existing assertions changed with the rule. Their fixtures give the two
records under one key different input and cache values, a shape that does not
occur in real transcripts. A fixture with the real shape is added.

Refs Piebald-AI#220
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