diff --git a/tracker/api.py b/tracker/api.py index 1071411..d3edbd8 100644 --- a/tracker/api.py +++ b/tracker/api.py @@ -259,7 +259,9 @@ def breakdown_series( if tool: clauses.append("mc.tool = ?"); params.append(tool) if model: - clauses.append("s.model = ?"); params.append(model) + clauses.append("EXISTS (SELECT 1 FROM messages mm WHERE mm.source_file=mc.source_file " + "AND mm.source_line=mc.source_line AND mm.model = ?)") + params.append(model) if project: clauses.append("s.cwd = ?"); params.append(project) if start: @@ -490,6 +492,7 @@ def _per_call_lifecycle_cost(tokens: int, tool: str, model: str | None, subseque @app.get("/api/mcp") def mcp( tool: str | None = Query(None), + model: str | None = Query(None), project: str | None = Query(None), start: str | None = Query(None), end: str | None = Query(None), @@ -501,6 +504,11 @@ def mcp( params: list = [] if tool: clauses.append("mc.tool = ?"); params.append(tool) + if model: + # An MCP call is issued by the assistant message at the same source_file/source_line. + clauses.append("EXISTS (SELECT 1 FROM messages mm WHERE mm.source_file=mc.source_file " + "AND mm.source_line=mc.source_line AND mm.model = ?)") + params.append(model) if project: clauses.append("s.cwd = ?"); params.append(project) if start: diff --git a/tracker/parse_claude.py b/tracker/parse_claude.py index 6e1d1fd..333fea3 100644 --- a/tracker/parse_claude.py +++ b/tracker/parse_claude.py @@ -101,13 +101,22 @@ def _tool_result_chars(content) -> tuple[int, int]: return len(json.dumps(content, default=str)), 0 +def _session_uuid_for(path: Path) -> str: + """Sub-agent JSONLs live at //subagents/agent-*.jsonl + and must roll up into the parent session. Top-level files use their own stem. + """ + if path.parent.name == "subagents": + return path.parent.parent.name + return path.stem + + def parse_file(path: Path, *, start_offset: int = 0) -> tuple[ParsedFile, int]: """Parse from byte offset; returns (parsed, new_offset). new_offset is the byte position AFTER the last fully-parsed line. If a trailing partial line exists, we stop before it so the next run picks it up. """ - session_uuid = path.stem + session_uuid = _session_uuid_for(path) session_id = f"claude:{session_uuid}" meta = SessionMeta( session_id=session_id, @@ -244,4 +253,7 @@ def parse_file(path: Path, *, start_offset: int = 0) -> tuple[ParsedFile, int]: def discover_files() -> list[Path]: if not CLAUDE_ROOT.exists(): return [] - return sorted(CLAUDE_ROOT.glob("*/*.jsonl")) + top = CLAUDE_ROOT.glob("*/*.jsonl") + # Sub-agent (Task tool) sessions are written to //subagents/agent-*.jsonl. + sub = CLAUDE_ROOT.glob("*/*/subagents/agent-*.jsonl") + return sorted({*top, *sub})