reader: pair Task subagents via toolUseResult.agentId, surface orphans as UnattachedGroup (#435)#449
Conversation
…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
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (13)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
| // `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(); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
1 issue found across 13 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…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
Closes #435.
Summary
Pairs Claude Task subagents (sidecar
agent-*.jsonlfiles) to their parent'stool_resultrow viatoolUseResult.agentId. Surfaces unpaired subagents asUnattachedGroup(e.g., slash-command synthetic dispatches).crates/relayburn-sdk/src/reader/claude/subagents.rs:discover_subagents,pair_to_main,count_subagents_under,SubagentTranscript,SubagentCounts.<sessionId>/subagents/when summary asks for the count. Default ingest path unchanged; no-subagent sessions stay zero-cost.agent-<id>.meta.jsonfor{agentType, worktreePath, description, toolUseId}. ThetoolUseIdfield acts as a fallback pairing key whentoolUseResult.agentIdisn't present.subagent_id TEXTcolumn onturns, partial indexidx_turns_subagent_id, idempotentALTER TABLE ADD COLUMNwith duplicate-column swallow.TurnRecord.subagent.agent_idinto the new column.burn summaryaddssubagents: X paired, Y orphanline + matching JSON block. Skipped on empty so existing consumers get byte-identical output.Where
agentIdactually livesConfirmed by reading real
~/.claude/projects/.../*.jsonlsessions on this machine:toolUseResult.agentIdsits at the top level of the user-shaped tool_result envelope (sibling ofmessage,sessionId,timestamp) — NOT insidemessage.content[]. The matchingtool_use_idis found insidemessage.content[].tool_use_idon the same row. Implementation handles both linkage paths (canonicalagentId+ fallback via.meta.json'stoolUseId).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 inschema.rscalls out the renumber path.Test plan
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)legacy_v1_ledger_migrates_…to assertsubagent_idcolumn lands and stays NULL on pre-v4 rowsformat_subagents_line+ JSON omission-gatecargo build --workspacecleancargo test --workspace— 694 SDK + 50 CLI summary tests, 0 failedBURN_GOLDEN=1 cargo test --test golden— 5/5state-statusonly (schema 3→4).summary*untouched because thesubagentsblock is skipped when both counts are zero and the golden's sealedHOMEhas no~/.claude/projects/.Out of scope
burn subagentsverb. 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