Count Claude Code tool calls in the AI Governance tool calls card and tile
The tool calls card and the summary tile now count Claude Code tool calls, and the agent filter can classify them. Before, the card skipped them: it did not read their event name, and they keep the tool name under a different key. It builds on the tool calls card (Add the tool calls card, ClickHouse reader and ... (!256578 - merged)), now merged. This is for #630971+s.
Changes
- The ClickHouse reader accepts
ai_claude_tool_invokedevents from the Claude Compliance API ingest. - The tool name is read from
details['tool_name'], then fromdetails['name']. Events with neither are still dropped. - With the
ai_governance_sessions_apiflag off, the DAP and Connected filters use the card's rule: an event with a workflow is classified by that workflow's agent type, and an event with a session and no workflow counts as Connected. - With the flag on, an event with a session is classified by the session's own source and agent type: a Duo session without an agent type is DAP, anything else is Connected, and deleted sessions are left out. An event without a session falls back to its workflow. This keeps the filters right once Duo events move to governance sessions. Two concrete differences from the flag-off rule: when an event names a workflow and a session that disagree, the session wins, so DAP plus Connected still equals All; and an event whose session was deleted is not counted.
- The summary tile counts the same events with the same tool-name keys, and classifies DAP and Connected through governance sessions behind the same flag, so the card and the tile agree.
- glab posts
ai_tool_invokedwithdetails['tool_name'](gitlab-org/cli,internal/commands/govern/audit/sync/sync.go), so its calls count like Duo's. - glab events are session-only since Update the external agent session API to use ai... (!257427 - merged) merged, and the Claude Compliance API ingest is expected to write the same shape. Both rules count them as Connected. Specs cover the session-only case for the card and the tile under both flag states.
ClickHouse only.
Out of scope
- The PostgreSQL reader in Add tool-call tally scopes and PostgreSQL tool ... (!256523 - closed) • Dheeraj Joshi • 19.6. It reads only
details['tool_name'], so whoever wires it to these event names should know Claude rows would fill its row cap and then be skipped.
ClickHouse review
The DAP and Connected filters add one IN (subquery) to the deduplicated event scan. With the flag off, only the workflow subquery runs; with it on, an event with a session uses the session subquery and an event without one uses the workflow subquery.
Workflow subquery (flag off, and events without a session)
workflow_id IN (
SELECT id FROM (
SELECT id,
argMax(agent_type, _siphon_replicated_at) AS agent_type,
argMax(_siphon_deleted, _siphon_replicated_at) AS deleted
FROM siphon_duo_workflows_workflows
WHERE startsWith(traversal_path, {traversal_path:String})
AND created_at < {to:DateTime64(6, 'UTC')}
GROUP BY traversal_path, created_at, id
)
WHERE deleted = false
AND agent_type IS NULL -- DAP; `IS NOT NULL` for Connected
)Session subquery (flag on, events with a session)
ai_governance_session_id IN (
SELECT id FROM (
SELECT id,
argMax(source, _siphon_replicated_at) AS source,
argMax(agent_type, _siphon_replicated_at) AS agent_type,
argMax(_siphon_deleted, _siphon_replicated_at) AS deleted
FROM siphon_ai_governance_sessions
WHERE startsWith(traversal_path, {traversal_path:String})
AND session_started_at < {to:DateTime64(6, 'UTC')}
GROUP BY traversal_path, session_started_at, id
)
WHERE deleted = false
AND (source = 0 AND coalesce(agent_type, '') = '') -- DAP; `NOT (...)` for Connected
)With the flag off, the card's rule applies: the workflow subquery, plus OR (workflow_id = 0 AND ai_governance_session_id != 0) for Connected. Combined, with the flag on: (ai_governance_session_id != 0 AND <session subquery>) OR (ai_governance_session_id = 0 AND <workflow subquery>).
EXPLAIN indexes=1 for the session subquery (GDK ClickHouse, 269 session rows)
ReadFromMergeTree (siphon_ai_governance_sessions)
Prewhere filter column: startsWith(traversal_path, '<prefix>') AND session_started_at < '2026-10-05 00:00:00'
Indexes:
PrimaryKey
Keys: traversal_path, session_started_at
Condition: and((session_started_at in (-Inf, '1791158400')), (traversal_path in ['<prefix>', '<prefix-next>')))
Parts: 2/2, Granules: 2/2Both primary key columns prune, then the GROUP BY on the full key (traversal_path, session_started_at, id) collapses the ReplacingMergeTree versions. There is no lower bound on session_started_at on purpose: a session can start long before the events it emits into the window. The workflow subquery has the same shape and was reviewed with Add the tool calls card, ClickHouse reader and ... (!256578 - merged) • Dheeraj Joshi • 19.5.
Testing
- ClickHouse tool calls service and tool calls service specs (card and dispatcher).
- ClickHouse metrics service and metrics service specs (tile and dispatcher).
- Local ClickHouse runs hit the shared memory cap in the test hooks, so CI is the check.
Screenshots
No frontend change in this MR. The card and the filter look the same; the card lists more tools once Claude Code events exist.
Related work this touches
- Summary tile (Add AI tool calls summary tile to the AI Govern... (!256856 - merged) • Dheeraj Joshi • 19.5, merged into Add the tool calls card, ClickHouse reader and ... (!256578 - merged) • Dheeraj Joshi • 19.5): changed here so it counts and classifies the same way as the card.
- Tool calls by risk Add "Tool calls by risk" donut card to the AI G... (!259004 - merged) • Dheeraj Joshi • 19.5 carries its own copy of the ClickHouse reader. When it rebases onto this,
click_house_tool_calls_service.rbconflicts, and Claude Code tool names (for exampleRead,Bash) need a risk mapping.