Skip to content

reader: pair Task subagents via toolUseResult.agentId, surface orphans as UnattachedGroup (#435)#449

Merged
willwashburn merged 2 commits into
mainfrom
claude/burn-435-subagent-pairing
May 25, 2026
Merged

reader: pair Task subagents via toolUseResult.agentId, surface orphans as UnattachedGroup (#435)#449
willwashburn merged 2 commits into
mainfrom
claude/burn-435-subagent-pairing

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Closes #435.

Summary

Pairs Claude Task subagents (sidecar agent-*.jsonl files) to their parent's tool_result row via toolUseResult.agentId. Surfaces unpaired subagents as UnattachedGroup (e.g., slash-command synthetic dispatches).

  • New module crates/relayburn-sdk/src/reader/claude/subagents.rs: discover_subagents, pair_to_main, count_subagents_under, SubagentTranscript, SubagentCounts.
  • Lazy discovery — only walks <sessionId>/subagents/ when summary asks for the count. Default ingest path unchanged; no-subagent sessions stay zero-cost.
  • Companion metadata: reads agent-<id>.meta.json for {agentType, worktreePath, description, toolUseId}. The toolUseId field acts as a fallback pairing key when toolUseResult.agentId isn't present.
  • Schema v4 (chained on hotspots: rank tools by raw output bytes alongside tokens (#436) #444's v3): nullable subagent_id TEXT column on turns, partial index idx_turns_subagent_id, idempotent ALTER TABLE ADD COLUMN with duplicate-column swallow.
  • Writer denormalizes TurnRecord.subagent.agent_id into the new column.
  • Surface: burn summary adds subagents: X paired, Y orphan line + matching JSON block. Skipped on empty so existing consumers get byte-identical output.

Where agentId actually lives

Confirmed by reading real ~/.claude/projects/.../*.jsonl sessions on this machine: toolUseResult.agentId sits at the top level of the user-shaped tool_result envelope (sibling of message, sessionId, timestamp) — NOT inside message.content[]. The matching tool_use_id is found inside message.content[].tool_use_id on the same row. Implementation handles both linkage paths (canonical agentId + fallback via .meta.json's toolUseId).

Merge note

This PR bumps schema v3 → v4. #434 (requestId dedup) ALSO uses v4 in its branch. Whichever lands second needs to become v5 with a chained ALTER TABLE. Migration shape is idempotent — mechanical reconcile. Doc comment in schema.rs calls out the renumber path.

Test plan

  • 9 new SDK tests in subagents.rs: discovery empty/lazy, JSONL+meta read, meta-missing, 4 pairing variants (canonical, orphan, meta-fallback, phantom-rejection), end-to-end 2-paired+1-orphan acceptance fixture, count_subagents_under_* (missing-root + multi-project sums)
  • Extended legacy_v1_ledger_migrates_… to assert subagent_id column lands and stays NULL on pre-v4 rows
  • 2 new CLI tests: human format_subagents_line + JSON omission-gate
  • cargo build --workspace clean
  • cargo test --workspace — 694 SDK + 50 CLI summary tests, 0 failed
  • BURN_GOLDEN=1 cargo test --test golden — 5/5
  • Golden updates: state-status only (schema 3→4). summary* untouched because the subagents block is skipped when both counts are zero and the golden's sealed HOME has no ~/.claude/projects/.

Out of scope

  • Integrating sidecar JSONLs into the main ingest pipeline — explicit follow-up. The schema column is populated from existing sidechain-detected subagents today; lazy summary-time discovery surfaces the orphan/paired split.
  • New top-level burn subagents verb. Took the summary-line route per issue recommendation ("cheaper to ship; CLI surface stays clean"). Can be added later if wanted.
  • #[non_exhaustive] on new types.

Generated by Claude Code

…agent_id

Adds `crate::reader::claude::subagents` (new module) with `discover_subagents`,
`pair_to_main`, and `count_subagents_under`. Discovery is lazy — the
`<sessionId>/subagents/` directory is stat'd once and skipped past on the hot
no-sidecar path, so default ingest of a session WITHOUT subagent dispatches
does not pay any walk cost. Pairing keys on `toolUseResult.agentId` (verified
against real `~/.claude/projects/` sessions) with `meta.json.toolUseId` as a
fallback for slash-command synthetic dispatches; unpaired sidecars surface as
the `UnattachedGroup` bucket per issue #435.

Schema bumped to v4: nullable `turns.subagent_id TEXT` column denormalizes
`TurnRecord.subagent.agent_id` so subagent rows are queryable structurally
without re-deserializing `record_json`. Chained `ALTER TABLE … ADD COLUMN`
in `migrate_burn_schema` (idempotent — re-open swallows the duplicate-column
failure). Partial index gates the index size by `subagent_id IS NOT NULL`.

Surface: `burn summary` gets a `subagents: X paired, Y orphan` line (and
matching `subagents` block in `--json`) populated by a lazy walk over
`~/.claude/projects/`. Skipped entirely when both counts are zero so the
existing summary golden stays byte-identical; honors
`BURN_CLAUDE_PROJECTS_DIR` for test sandboxing.

Golden updates: `state-status` + `state-status-json` snapshots advance
schemaVersion 3 → 4 to track the v4 bump. Justification: only the
archive_state version number moves; row counts and rebuild metadata are
unchanged.

Test counts: +8 subagents-module tests (discovery, lazy gate, meta parse,
4 pairing variants, end-to-end 2-paired + 1-orphan acceptance fixture),
+1 cross-project `count_subagents_under` test, +2 CLI summary tests
(human line + JSON omission gate). Existing legacy-v1-migration ledger
test extended to assert the new `subagent_id` column lands and stays
NULL on pre-v4 rows.

Issue: #435.

https://claude.ai/code/session_01QEpNZbWEYNwxzqQjTN5LCY
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@willwashburn, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 26 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 20df9a5d-207c-4f2b-98eb-fe55e146b55b

📥 Commits

Reviewing files that changed from the base of the PR and between 33cc446 and c0c5fd8.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • crates/relayburn-cli/src/commands/summary.rs
  • crates/relayburn-sdk/src/ledger/db.rs
  • crates/relayburn-sdk/src/ledger/schema.rs
  • crates/relayburn-sdk/src/ledger/tests.rs
  • crates/relayburn-sdk/src/ledger/writer.rs
  • crates/relayburn-sdk/src/lib.rs
  • crates/relayburn-sdk/src/query_verbs.rs
  • crates/relayburn-sdk/src/reader.rs
  • crates/relayburn-sdk/src/reader/claude.rs
  • crates/relayburn-sdk/src/reader/claude/subagents.rs
  • tests/fixtures/cli-golden/snapshots/state-status-json.stdout.txt
  • tests/fixtures/cli-golden/snapshots/state-status.stdout.txt
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/burn-435-subagent-pairing

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 and usage tips.

@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: b604ce4fd7

ℹ️ 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".

Comment thread crates/relayburn-sdk/src/query_verbs.rs Outdated
// `subagents/` subdir — i.e. zero cost on the vast
// majority of summaries that don't hit a session with
// sidecar transcripts.
let subagents = compute_summary_subagent_counts();

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 Scope subagent counts to the summary filter set

Compute subagent counts from the same turn/session scope as the summary query instead of scanning the entire Claude projects tree unconditionally. compute_summary_subagent_counts() has no access to session, project, since, provider, or agent filters, so burn summary --session ... / --project ... can report paired/orphan counts from unrelated sessions on disk, producing misleading totals in both human and JSON output. This should be derived from the filtered summary input (or constrained to matching session IDs) so the new subagents metric reflects the report being requested.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in c0c5fd8. compute_summary_subagent_counts() and count_subagents_under() now take an Option<&HashSet<String>> session filter. When any of session, project, since, workflow, tags, agent, or providers is set on SummaryReportOptions, the helper summary_subagent_session_filter builds the set from the already-filtered turns slice and threads it through — so the subagents line scopes to exactly the same sessions the rest of the summary numbers cover. The lazy walk contract is preserved: the filter check happens before the subagents/ read_dir, so we never stat sidecar directories for out-of-scope sessions. Un-filtered burn summary keeps the original global walk byte-for-byte (covered by a regression test). New unit tests cover: filtered count returns only the named session's tallies, empty/unknown-session filters return zeros, and the wiring helper engages for every scoping filter.


Generated by Claude Code

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 13 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/relayburn-sdk/src/query_verbs.rs Outdated
…t of summary (#449)

`compute_summary_subagent_counts()` previously walked
`~/.claude/projects/*/<sessionId>/subagents/` unconditionally, so
`burn summary --session A` / `--project B` / `--since 24h` reported
paired/orphan totals from sessions the user had excluded. The rest of
the summary's numbers were scoped to the active filter; only the new
`subagents: X paired, Y orphan` line was not.

Fix:

- `count_subagents_under` now takes
  `session_filter: Option<&HashSet<String>>`. `None` keeps the original
  global walk (the un-filtered `burn summary` path); `Some(set)` skips
  every `<sessionId>/` directory whose name is not in `set` *before*
  the `subagents/` `read_dir`, preserving the laziness contract for
  the filtered path too.
- `compute_summary_subagent_counts` threads the filter through to the
  walker.
- A new helper `summary_subagent_session_filter` returns `None` when
  `SummaryReportOptions` carries no scoping filters and `Some(set)`
  otherwise — `set` is the session-id set derived from the already-
  filtered `turns` slice, so the count can never diverge from the
  numbers in the rest of the report.

Un-filtered `burn summary` behavior is byte-identical to the prior
commit. Tests:

- Walker filter behavior (subagents.rs): scoped count returns only
  the matching session's tallies, empty filter returns zeros, filter
  containing an unknown session returns zeros, un-filtered call still
  observes every reachable sidecar.
- Wiring (query_verbs.rs): helper returns `None` for the default opts
  and `Some(set)` whenever any of `session`, `project`, `since`,
  `workflow`, `tags`, `agent`, `providers` is set.

https://claude.ai/code/session_01QEpNZbWEYNwxzqQjTN5LCY
@willwashburn willwashburn merged commit 892ac11 into main May 25, 2026
12 checks passed
@willwashburn willwashburn deleted the claude/burn-435-subagent-pairing branch May 25, 2026 18:32
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.

reader: pair Task subagents via toolUseResult.agentId; surface orphans as UnattachedGroup

2 participants