After thorough investigation of GitHub issue #313 regarding subagent token tracking, we discovered that the reported issue is more nuanced than initially described. While users are experiencing real problems with token limits, the root cause is not exactly what was initially documented. This report details our findings, corrections to initial assumptions, and the improvements we've implemented.
Users reported hitting 5-hour token limits while ccusage showed only 10-20% usage when using subagents (Task tool). The initial assessment suggested that:
- Subagent tokens were being missed entirely
- Tokens from
isSidechain: trueentries were being miscounted as the wrong model - The
toolUseResultfield containing aggregated totals was being ignored
Initial Assumption: Entries with isSidechain: true were being parsed but attributed to the wrong model (opus-4 instead of sonnet-4).
Actual Finding:
- The existing code (v15.9.3+) already respects the
message.modelfield for each entry - Sidechain entries from assistant messages contain the correct model:
"model": "claude-sonnet-4-20250514" - These tokens ARE being correctly attributed to sonnet-4 in the current version
Evidence:
// From main branch src/data-loader.ts line 895:
allEntries.push({ data, date, cost, model: data.message.model, project });
// ^^^^^^^^^^^^^^^^^^^^^^
// The code already uses the individual entry's model, not a session defaultReal Data Analysis:
# Checking actual sidechain entries in the test file:
$ grep '"isSidechain":true' d094b187-*.jsonl | grep '"role":"assistant"' | head -1 | jq '.message.model'
"claude-sonnet-4-20250514"
# ✅ Sidechain entries have the correct model setThe original documentation showed a sidechain entry example with:
{
"isSidechain": true,
"message": {
"model": "claude-opus-4-1-20250805", // ← This is WRONG
"usage": { ... }
}
}But in actual JSONL files, sidechain assistant entries have:
{
"isSidechain": true,
"message": {
"model": "claude-sonnet-4-20250514", // ← Correct model!
"usage": { ... }
}
}This incorrect example led to the mistaken belief that all sidechain tokens were being attributed to opus-4.
Initial Assumption: The toolUseResult field contains token usage data that's being ignored.
Actual Finding:
- The
toolUseResultfield exists but its structure varies significantly - In our test data, most
toolUseResultentries are either:- Strings:
"toolUseResult": "Error: No such tool available" - Objects without usage data:
{"filenames": [...], "mode": "...", "numFiles": ...}
- Strings:
- The example in the documentation showing
toolUseResult.usagewith token counts appears to be from a different version or specific use case
Evidence:
$ grep '"toolUseResult":{' test.jsonl | jq '.toolUseResult | has("usage")'
false # No usage field in the actual dataWhen comparing the global version (15.9.3) with our fixed version (15.9.4):
# Both versions show the same token attribution for Aug 11, 2025:
- opus-4: 26,648,xxx tokens
- sonnet-4: 1,248,xxx tokens (includes the subagent tokens!)This confirms that the current version is already handling model attribution correctly.
Despite sidechain tokens being counted correctly, users may still experience discrepancies due to:
- Cache Tokens: The
toolUseResult.totalTokensmight exclude cache tokens while the actual billing includes them - Timing Issues: Live monitoring might not update immediately during subagent execution
- Different Token Types: Some token types might be counted differently in billing vs. reporting
- Version Differences: Users might be running older versions that don't respect individual entry models
While the core issue was already addressed, our changes add value through:
export function extractUsageFromEntry(data: UsageData): UsageData['message']['usage'] | undefined {
// Prioritize toolUseResult.usage if present (future-proofing)
if (data.toolUseResult != null && typeof data.toolUseResult === 'object'
&& 'usage' in data.toolUseResult && data.toolUseResult.usage != null) {
return data.toolUseResult.usage;
}
// Fall back to message.usage
return data.message.usage;
}- Added
isSidechainfield to the schema - Added
toolUseResultfield with proper typing - These additions ensure forward compatibility as Claude Code evolves
// Provides a fallback for edge cases where model might be missing
const model = data.message.model ?? (data.isSidechain === true ? 'claude-sonnet-4-20250514' : undefined);- Centralized usage extraction logic
- Consistent handling across all load functions
- Better maintainability for future updates
-
Update the issue to clarify that:
- Recent versions (15.9.3+) already handle model attribution correctly
- The issue might be version-specific or related to other factors
- Users should ensure they're running the latest version
-
Request more information from users experiencing issues:
- Exact version of ccusage being used
- Sample JSONL files showing the discrepancy
- Screenshots of both ccusage output and Claude Code's actual limit message
The improvements we made are still valuable for:
- Future-proofing: Handles
toolUseResult.usagewhen it contains token data - Edge cases: Provides fallbacks for entries without model fields
- Code quality: Cleaner, more maintainable code structure
- Schema validation: Proper typing for subagent-related fields
The investigation revealed that the core issue described in the original report was based on incorrect assumptions about the data structure. The current version of ccusage already handles subagent token attribution correctly when the model field is present in each entry.
However, our improvements add robustness and future-proofing to handle:
- Variations in
toolUseResultstructure - Edge cases where model information might be missing
- Future changes to Claude Code's JSONL format
The real value of this work is not in fixing a critical bug (which doesn't exist in current versions) but in:
- Clarifying the actual behavior of the system
- Adding defensive coding for edge cases
- Preparing for future Claude Code updates
- Improving code maintainability
File: d094b187-1785-445c-8fbc-a353149cbabe.jsonl
- Date: 2025-08-11
- Total entries: 193
- Sidechain entries: 63 (with
isSidechain: true) - Main agent model:
claude-opus-4-1-20250805 - Subagent model:
claude-sonnet-4-20250514 - Subagent tokens: 1,086,368 (correctly attributed to sonnet-4)
- Update GitHub Issue #313 with these findings
- Submit PR with the improvements for future-proofing
- Document the actual JSONL structure variations in the README
- Consider adding debug logging to help users troubleshoot discrepancies
- Monitor for new reports with specific version and data examples