Add tool-call tally scopes and PostgreSQL tool calls reader
What does this MR do and why?
One of two MRs splitting Add "Top tools used by agents" card to AI Gover... (!252016 - closed) • Dheeraj Joshi • 19.5: this one adds the PostgreSQL reader, and Add the tool calls card, ClickHouse reader and ... (!256578 - merged) • Dheeraj Joshi • 19.5 adds the ClickHouse reader, the GraphQL field, the card and the tile. Nothing calls this reader yet.
- Scopes on
AuditEvents::AiAuditEvent:for_project,with_event_names,created_between,for_projects_in_hierarchy,for_groups_in_hierarchy,for_agent_class. tally_tool_calls(arms, max:): one unordered query per arm for up tomax + 1matching rows, counted bytool_namein Ruby becausedetailsis serialized YAML. An arm returning the extra row setsapproximate: true; past the cap the counted rows of that arm are whichevermaxthe scan reached first, not the newest. A group has two arms (events on its projects, events on its groups), each capped on its own so one cannot push the other out; a project has one arm.Ai::Governance::PostgresqlToolCallsService: top 10 tools for a group or project, capped at 10,000 rows per arm.
flowchart LR
in["group or project<br/>date range<br/>agent class"] --> q
subgraph q["one SQL query per arm on ai_audit_events"]
direction LR
s1["rows of this project,<br/>or one arm each for the<br/>group's projects and groups"] --> s2["event_name =<br/>ai_tool_invoked"] --> s3["created_at<br/>in range"] --> s4["agent class<br/>via workflow"] --> s5["LIMIT 10,001<br/>SELECT details"]
end
q --> r["Ruby: count tool_name<br/>from details (YAML)"] --> out["top 10 tools<br/>approximate: true if<br/>row 10,001 came back"]Every step inside the box is one scope; chained, they are still a single query per arm. The count happens in Ruby because details is serialized YAML and SQL cannot group by a key inside it.
Database review
ai_audit_events is empty on GitLab.com (count); these events go to ClickHouse there. This reader is meant for self-managed instances without ClickHouse, so plans from production show no cost. To measure, I loaded 7.4M synthetic rows for September 2026 into a Database Lab clone (same workflow shape as before) and ran the queries against gitlab-org (group) and gitlab-org/gitlab (project), 7-day window. About 613k ai_tool_invoked events fall in the window for gitlab-org, 589k on its projects and 24k on its groups.
A group runs two queries, one per arm: events on its projects (for_projects_in_hierarchy) and events on its groups (for_groups_in_hierarchy). A project runs one. Each arm is LIMIT 10001 with no ORDER BY, so it stops as soon as it has enough rows, and Ruby adds up the counts. Cold is the first run right after the load, warm is the second run.
| Query | agent_class |
Arm | Cold | Warm |
|---|---|---|---|---|
| group | :all |
projects | 3.73 s | 71 ms |
| group | :all |
groups | 1.31 s | 46 ms |
| group | :internal_dap |
projects | 1.40 s | 93 ms |
| group | :internal_dap |
groups | 758 ms | 62 ms |
| group | :external, no matching events |
projects | 31.2 s | 2.17 s |
| group | :external, no matching events |
groups | 14.9 s | 504 ms |
| project | :all |
project | 70 ms | 14 ms |
The two arms of a query run one after the other, so add them. Both arms return 10,001 rows in every case except the no-match one, which returns 0. The :internal_dap cold runs came after the :all cold runs and were partly cached, so their true cold cost is closer to the :all figures. The no-match case is the worst one: the cap never fills, so each arm reads the whole window and looks up the workflow for every event (109k disk reads on the clone when cold). Synthetic data is indicative, not a customer's distribution.
Indexes: the table is partitioned monthly by created_at, and each arm filters on project_id or namespace_id, so the existing (project_id, created_at DESC) and (namespace_id, created_at DESC) indexes serve one arm each. No new index. The agent-class EXISTS is a primary-key probe per event (plan).
Two queries rather than one UNION ALL: a row has exactly one of project_id / namespace_id (check_547b01c40f), so the arms never overlap. Under one unordered LIMIT, the project arm could fill the cap before the groups arm was read, so each arm has its own cap. Same hierarchy shape as Ai::DuoWorkflows::Workflow.in_namespace_hierarchy.
Group queries, :internal_dap (:all has no EXISTS, :external uses IS NOT NULL)
:internal_dap (:all has no EXISTS, :external uses IS NOT NULL)-- projects arm
SELECT "ai_audit_events"."details"
FROM "ai_audit_events"
WHERE "ai_audit_events"."event_name" = 'ai_tool_invoked'
AND "ai_audit_events"."created_at" >= '2026-09-21 00:00:00'
AND "ai_audit_events"."created_at" < '2026-09-28 04:22:52'
AND EXISTS (SELECT "duo_workflows_workflows".* FROM "duo_workflows_workflows" WHERE "duo_workflows_workflows"."agent_type" IS NULL AND "duo_workflows_workflows"."id" = "ai_audit_events"."workflow_id")
AND "ai_audit_events"."project_id" IN (SELECT "projects"."id" FROM "projects" WHERE "projects"."namespace_id" IN (SELECT "namespaces"."id" FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND (traversal_ids @> ('{9970}'))))
LIMIT 10001
-- groups arm
SELECT "ai_audit_events"."details"
FROM "ai_audit_events"
WHERE "ai_audit_events"."event_name" = 'ai_tool_invoked'
AND "ai_audit_events"."created_at" >= '2026-09-21 00:00:00'
AND "ai_audit_events"."created_at" < '2026-09-28 04:22:52'
AND EXISTS (SELECT "duo_workflows_workflows".* FROM "duo_workflows_workflows" WHERE "duo_workflows_workflows"."agent_type" IS NULL AND "duo_workflows_workflows"."id" = "ai_audit_events"."workflow_id")
AND "ai_audit_events"."namespace_id" IN (SELECT "namespaces"."id" FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND (traversal_ids @> ('{9970}')))
LIMIT 10001
Project query (:all shown)
:all shown)SELECT "ai_audit_events"."details"
FROM "ai_audit_events"
WHERE "ai_audit_events"."event_name" = 'ai_tool_invoked'
AND "ai_audit_events"."created_at" >= '2026-09-21 00:00:00'
AND "ai_audit_events"."created_at" < '2026-09-28 04:22:52'
AND "ai_audit_events"."project_id" = 278964
LIMIT 10001Plans (warm second runs):
Plan: projects arm, all agents, 71 ms
Limit (cost=5631.73..9358.81 rows=22 width=22) (actual time=18.804..71.483 rows=10001 loops=1)
Buffers: shared hit=42913
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=5631.73..9358.81 rows=22 width=22) (actual time=18.802..70.708 rows=10001 loops=1)
Buffers: shared hit=42913
I/O Timings: read=0.000 write=0.000
-> HashAggregate (cost=5631.30..5638.31 rows=701 width=4) (actual time=18.703..19.033 rows=910 loops=1)
Group Key: projects.id
Batches: 1 Memory Usage: 913kB
Buffers: shared hit=17103
I/O Timings: read=0.000 write=0.000
-> Gather (cost=1073.48..5629.55 rows=701 width=4) (actual time=1.568..16.706 rows=9055 loops=1)
Workers Planned: 1
Workers Launched: 1
Buffers: shared hit=17103
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=73.48..4559.45 rows=412 width=4) (actual time=0.704..13.525 rows=4528 loops=2)
Buffers: shared hit=17103
I/O Timings: read=0.000 write=0.000
-> Parallel Bitmap Heap Scan on public.namespaces (cost=72.92..1959.66 rows=707 width=4) (actual time=0.650..4.092 rows=802 loops=2)
Recheck Cond: ((namespaces.traversal_ids @> '{9970}'::integer[]) AND ((namespaces.type)::text = 'Group'::text))
Heap Blocks: exact=850
Buffers: shared hit=1518
I/O Timings: read=0.000 write=0.000
-> Bitmap Index Scan on index_namespaces_on_traversal_ids_for_groups (cost=0.00..72.61 rows=1202 width=0) (actual time=1.008..1.008 rows=1604 loops=1)
Index Cond: (namespaces.traversal_ids @> '{9970}'::integer[])
Buffers: shared hit=40
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_projects_on_namespace_id_and_id on public.projects (cost=0.56..3.32 rows=36 width=8) (actual time=0.008..0.011 rows=6 loops=1604)
Index Cond: (projects.namespace_id = namespaces.id)
Heap Fetches: 691
Buffers: shared hit=15585
I/O Timings: read=0.000 write=0.000
-> Index Scan using ai_audit_events_202609_project_id_created_at_idx on gitlab_partitions_dynamic.ai_audit_events_202609 ai_audit_events (cost=0.43..4.64 rows=67 width=30) (actual time=0.005..0.056 rows=11 loops=910)
Index Cond: ((ai_audit_events.project_id = projects.id) AND (ai_audit_events.created_at >= '2026-09-21 00:00:00+00'::timestamp with time zone) AND (ai_audit_events.created_at < '2026-09-28 04:22:52+00'::timestamp with time zone))
Filter: (ai_audit_events.event_name = 'ai_tool_invoked'::text)
Rows Removed by Filter: 16
Buffers: shared hit=25810
I/O Timings: read=0.000 write=0.000
Settings: random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off'
Query ID: -2268059212965670476Plan: groups arm, all agents, 46 ms
Limit (cost=3029.01..69843.52 rows=5 width=22) (actual time=19.864..46.070 rows=10001 loops=1)
Buffers: shared hit=33453 read=10
I/O Timings: read=6.090 write=0.000
-> Gather (cost=3029.01..69843.52 rows=5 width=22) (actual time=19.862..45.308 rows=10001 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=33453 read=10
I/O Timings: read=6.090 write=0.000
-> Merge Join (cost=2029.01..68843.02 rows=2 width=22) (actual time=16.025..31.232 rows=3417 loops=3)
Merge Cond: (ai_audit_events.namespace_id = namespaces.id)
Buffers: shared hit=33453 read=10
I/O Timings: read=6.090 write=0.000
-> Parallel Index Scan using ai_audit_events_202609_namespace_id_created_at_idx on gitlab_partitions_dynamic.ai_audit_events_202609 ai_audit_events (cost=0.43..367613.20 rows=863084 width=30) (actual time=0.037..22.114 rows=4140 loops=3)
Index Cond: ((ai_audit_events.created_at >= '2026-09-21 00:00:00+00'::timestamp with time zone) AND (ai_audit_events.created_at < '2026-09-28 04:22:52+00'::timestamp with time zone))
Filter: (ai_audit_events.event_name = 'ai_tool_invoked'::text)
Rows Removed by Filter: 6238
Buffers: shared hit=28878 read=10
I/O Timings: read=6.090 write=0.000
-> Sort (cost=2028.57..2031.58 rows=1202 width=4) (actual time=8.649..8.650 rows=1 loops=3)
Sort Key: namespaces.id
Sort Method: quicksort Memory: 49kB
Buffers: shared hit=4575
I/O Timings: read=0.000 write=0.000
-> Bitmap Heap Scan on public.namespaces (cost=72.92..1967.08 rows=1202 width=4) (actual time=0.945..8.419 rows=1604 loops=3)
Recheck Cond: ((namespaces.traversal_ids @> '{9970}'::integer[]) AND ((namespaces.type)::text = 'Group'::text))
Heap Blocks: exact=1478
Buffers: shared hit=4554
I/O Timings: read=0.000 write=0.000
-> Bitmap Index Scan on index_namespaces_on_traversal_ids_for_groups (cost=0.00..72.61 rows=1202 width=0) (actual time=0.740..0.740 rows=1604 loops=3)
Index Cond: (namespaces.traversal_ids @> '{9970}'::integer[])
Buffers: shared hit=120
I/O Timings: read=0.000 write=0.000
Settings: work_mem = '230MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB'
Query ID: -8163810560759420366Plan: projects arm, internal agents, 93 ms
Limit (cost=5632.16..9375.06 rows=22 width=22) (actual time=20.213..93.324 rows=10001 loops=1)
Buffers: shared hit=82919
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=5632.16..9375.06 rows=22 width=22) (actual time=20.211..92.442 rows=10001 loops=1)
Buffers: shared hit=82919
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=5631.73..9358.81 rows=22 width=30) (actual time=20.194..73.278 rows=10001 loops=1)
Buffers: shared hit=42915
I/O Timings: read=0.000 write=0.000
-> HashAggregate (cost=5631.30..5638.31 rows=701 width=4) (actual time=20.092..20.433 rows=910 loops=1)
Group Key: projects.id
Batches: 1 Memory Usage: 913kB
Buffers: shared hit=17105
I/O Timings: read=0.000 write=0.000
-> Gather (cost=1073.48..5629.55 rows=701 width=4) (actual time=1.720..18.022 rows=9055 loops=1)
Workers Planned: 1
Workers Launched: 1
Buffers: shared hit=17105
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=73.48..4559.45 rows=412 width=4) (actual time=0.786..14.207 rows=4528 loops=2)
Buffers: shared hit=17105
I/O Timings: read=0.000 write=0.000
-> Parallel Bitmap Heap Scan on public.namespaces (cost=72.92..1959.66 rows=707 width=4) (actual time=0.707..4.244 rows=802 loops=2)
Recheck Cond: ((namespaces.traversal_ids @> '{9970}'::integer[]) AND ((namespaces.type)::text = 'Group'::text))
Heap Blocks: exact=906
Buffers: shared hit=1518
I/O Timings: read=0.000 write=0.000
-> Bitmap Index Scan on index_namespaces_on_traversal_ids_for_groups (cost=0.00..72.61 rows=1202 width=0) (actual time=1.075..1.075 rows=1604 loops=1)
Index Cond: (namespaces.traversal_ids @> '{9970}'::integer[])
Buffers: shared hit=40
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_projects_on_namespace_id_and_id on public.projects (cost=0.56..3.32 rows=36 width=8) (actual time=0.008..0.012 rows=6 loops=1604)
Index Cond: (projects.namespace_id = namespaces.id)
Heap Fetches: 691
Buffers: shared hit=15587
I/O Timings: read=0.000 write=0.000
-> Index Scan using ai_audit_events_202609_project_id_created_at_idx on gitlab_partitions_dynamic.ai_audit_events_202609 ai_audit_events (cost=0.43..4.64 rows=67 width=38) (actual time=0.005..0.057 rows=11 loops=910)
Index Cond: ((ai_audit_events.project_id = projects.id) AND (ai_audit_events.created_at >= '2026-09-21 00:00:00+00'::timestamp with time zone) AND (ai_audit_events.created_at < '2026-09-28 04:22:52+00'::timestamp with time zone))
Filter: (ai_audit_events.event_name = 'ai_tool_invoked'::text)
Rows Removed by Filter: 16
Buffers: shared hit=25810
I/O Timings: read=0.000 write=0.000
-> Index Scan using duo_workflows_workflows_pkey on public.duo_workflows_workflows (cost=0.43..0.74 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=10001)
Index Cond: (duo_workflows_workflows.id = ai_audit_events.workflow_id)
Filter: (duo_workflows_workflows.agent_type IS NULL)
Buffers: shared hit=40004
I/O Timings: read=0.000 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: 2124969510998165540Plan: groups arm, internal agents, 62 ms
Limit (cost=3029.44..69844.99 rows=5 width=22) (actual time=20.348..61.910 rows=10001 loops=1)
Buffers: shared hit=73936
I/O Timings: read=0.000 write=0.000
-> Gather (cost=3029.44..69844.99 rows=5 width=22) (actual time=20.346..61.120 rows=10001 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=73936
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=2029.44..68844.49 rows=2 width=22) (actual time=15.973..45.216 rows=3389 loops=3)
Buffers: shared hit=73936
I/O Timings: read=0.000 write=0.000
-> Merge Join (cost=2029.01..68843.02 rows=2 width=30) (actual time=15.944..29.944 rows=3389 loops=3)
Merge Cond: (ai_audit_events.namespace_id = namespaces.id)
Buffers: shared hit=33263
I/O Timings: read=0.000 write=0.000
-> Parallel Index Scan using ai_audit_events_202609_namespace_id_created_at_idx on gitlab_partitions_dynamic.ai_audit_events_202609 ai_audit_events (cost=0.43..367613.20 rows=863084 width=38) (actual time=0.051..20.710 rows=4112 loops=3)
Index Cond: ((ai_audit_events.created_at >= '2026-09-21 00:00:00+00'::timestamp with time zone) AND (ai_audit_events.created_at < '2026-09-28 04:22:52+00'::timestamp with time zone))
Filter: (ai_audit_events.event_name = 'ai_tool_invoked'::text)
Rows Removed by Filter: 6194
Buffers: shared hit=28688
I/O Timings: read=0.000 write=0.000
-> Sort (cost=2028.57..2031.58 rows=1202 width=4) (actual time=8.643..8.644 rows=1 loops=3)
Sort Key: namespaces.id
Sort Method: quicksort Memory: 49kB
Buffers: shared hit=4575
I/O Timings: read=0.000 write=0.000
-> Bitmap Heap Scan on public.namespaces (cost=72.92..1967.08 rows=1202 width=4) (actual time=1.001..8.403 rows=1604 loops=3)
Recheck Cond: ((namespaces.traversal_ids @> '{9970}'::integer[]) AND ((namespaces.type)::text = 'Group'::text))
Heap Blocks: exact=1478
Buffers: shared hit=4554
I/O Timings: read=0.000 write=0.000
-> Bitmap Index Scan on index_namespaces_on_traversal_ids_for_groups (cost=0.00..72.61 rows=1202 width=0) (actual time=0.781..0.782 rows=1604 loops=3)
Index Cond: (namespaces.traversal_ids @> '{9970}'::integer[])
Buffers: shared hit=120
I/O Timings: read=0.000 write=0.000
-> Index Scan using duo_workflows_workflows_pkey on public.duo_workflows_workflows (cost=0.43..0.74 rows=1 width=8) (actual time=0.004..0.004 rows=1 loops=10167)
Index Cond: (duo_workflows_workflows.id = ai_audit_events.workflow_id)
Filter: (duo_workflows_workflows.agent_type IS NULL)
Buffers: shared hit=40672
I/O Timings: read=0.000 write=0.000
Settings: effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4'
Query ID: 8402754678533373881Plan: projects arm, no matching events, 2.2 s
Limit (cost=5632.16..9375.37 rows=1 width=22) (actual time=2173.594..2173.758 rows=0 loops=1)
Buffers: shared hit=3744578
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=5632.16..9375.37 rows=1 width=22) (actual time=2173.593..2173.757 rows=0 loops=1)
Buffers: shared hit=3744578
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=5631.73..9358.81 rows=22 width=30) (actual time=19.197..925.951 rows=588787 loops=1)
Buffers: shared hit=1387084
I/O Timings: read=0.000 write=0.000
-> HashAggregate (cost=5631.30..5638.31 rows=701 width=4) (actual time=19.074..21.238 rows=9055 loops=1)
Group Key: projects.id
Batches: 1 Memory Usage: 913kB
Buffers: shared hit=17100
I/O Timings: read=0.000 write=0.000
-> Gather (cost=1073.48..5629.55 rows=701 width=4) (actual time=1.795..17.011 rows=9055 loops=1)
Workers Planned: 1
Workers Launched: 1
Buffers: shared hit=17100
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=73.48..4559.45 rows=412 width=4) (actual time=0.788..13.780 rows=4528 loops=2)
Buffers: shared hit=17100
I/O Timings: read=0.000 write=0.000
-> Parallel Bitmap Heap Scan on public.namespaces (cost=72.92..1959.66 rows=707 width=4) (actual time=0.712..4.149 rows=802 loops=2)
Recheck Cond: ((namespaces.traversal_ids @> '{9970}'::integer[]) AND ((namespaces.type)::text = 'Group'::text))
Heap Blocks: exact=844
Buffers: shared hit=1518
I/O Timings: read=0.000 write=0.000
-> Bitmap Index Scan on index_namespaces_on_traversal_ids_for_groups (cost=0.00..72.61 rows=1202 width=0) (actual time=1.142..1.143 rows=1604 loops=1)
Index Cond: (namespaces.traversal_ids @> '{9970}'::integer[])
Buffers: shared hit=40
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_projects_on_namespace_id_and_id on public.projects (cost=0.56..3.32 rows=36 width=8) (actual time=0.008..0.011 rows=6 loops=1604)
Index Cond: (projects.namespace_id = namespaces.id)
Heap Fetches: 691
Buffers: shared hit=15582
I/O Timings: read=0.000 write=0.000
-> Index Scan using ai_audit_events_202609_project_id_created_at_idx on gitlab_partitions_dynamic.ai_audit_events_202609 ai_audit_events (cost=0.43..4.64 rows=67 width=38) (actual time=0.003..0.093 rows=65 loops=9055)
Index Cond: ((ai_audit_events.project_id = projects.id) AND (ai_audit_events.created_at >= '2026-09-21 00:00:00+00'::timestamp with time zone) AND (ai_audit_events.created_at < '2026-09-28 04:22:52+00'::timestamp with time zone))
Filter: (ai_audit_events.event_name = 'ai_tool_invoked'::text)
Rows Removed by Filter: 98
Buffers: shared hit=1369984
I/O Timings: read=0.000 write=0.000
-> Index Scan using duo_workflows_workflows_pkey on public.duo_workflows_workflows (cost=0.43..0.74 rows=1 width=8) (actual time=0.002..0.002 rows=0 loops=588787)
Index Cond: (duo_workflows_workflows.id = ai_audit_events.workflow_id)
Filter: (duo_workflows_workflows.agent_type IS NOT NULL)
Rows Removed by Filter: 1
Buffers: shared hit=2357494
I/O Timings: read=0.000 write=0.000
Settings: jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB'
Query ID: -2919886862585114405Plan: groups arm, no matching events, 0.5 s
Limit (cost=3029.44..69847.52 rows=1 width=22) (actual time=475.146..503.769 rows=0 loops=1)
Buffers: shared hit=1186380 read=3054
I/O Timings: read=132.790 write=0.000
-> Nested Loop (cost=3029.44..69847.52 rows=1 width=22) (actual time=475.144..503.767 rows=0 loops=1)
Buffers: shared hit=1186380 read=3054
I/O Timings: read=132.790 write=0.000
-> Gather (cost=3029.01..69843.52 rows=5 width=30) (actual time=42.733..441.193 rows=23909 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=1090783 read=3012
I/O Timings: read=130.265 write=0.000
-> Merge Join (cost=2029.01..68843.02 rows=2 width=30) (actual time=38.505..445.159 rows=7970 loops=3)
Merge Cond: (ai_audit_events.namespace_id = namespaces.id)
Buffers: shared hit=1090783 read=3012
I/O Timings: read=130.265 write=0.000
-> Parallel Index Scan using ai_audit_events_202609_namespace_id_created_at_idx on gitlab_partitions_dynamic.ai_audit_events_202609 ai_audit_events (cost=0.43..367613.20 rows=863084 width=38) (actual time=0.944..424.504 rows=159957 loops=3)
Index Cond: ((ai_audit_events.created_at >= '2026-09-21 00:00:00+00'::timestamp with time zone) AND (ai_audit_events.created_at < '2026-09-28 04:22:52+00'::timestamp with time zone))
Filter: (ai_audit_events.event_name = 'ai_tool_invoked'::text)
Rows Removed by Filter: 239817
Buffers: shared hit=1086208 read=3012
I/O Timings: read=130.265 write=0.000
-> Sort (cost=2028.57..2031.58 rows=1202 width=4) (actual time=8.999..9.106 rows=1604 loops=3)
Sort Key: namespaces.id
Sort Method: quicksort Memory: 49kB
Buffers: shared hit=4575
I/O Timings: read=0.000 write=0.000
-> Bitmap Heap Scan on public.namespaces (cost=72.92..1967.08 rows=1202 width=4) (actual time=1.097..8.685 rows=1604 loops=3)
Recheck Cond: ((namespaces.traversal_ids @> '{9970}'::integer[]) AND ((namespaces.type)::text = 'Group'::text))
Heap Blocks: exact=1478
Buffers: shared hit=4554
I/O Timings: read=0.000 write=0.000
-> Bitmap Index Scan on index_namespaces_on_traversal_ids_for_groups (cost=0.00..72.61 rows=1202 width=0) (actual time=0.846..0.846 rows=1604 loops=3)
Index Cond: (namespaces.traversal_ids @> '{9970}'::integer[])
Buffers: shared hit=120
I/O Timings: read=0.000 write=0.000
-> Index Scan using duo_workflows_workflows_pkey on public.duo_workflows_workflows (cost=0.43..0.74 rows=1 width=8) (actual time=0.002..0.002 rows=0 loops=23909)
Index Cond: (duo_workflows_workflows.id = ai_audit_events.workflow_id)
Filter: (duo_workflows_workflows.agent_type IS NOT NULL)
Rows Removed by Filter: 1
Buffers: shared hit=95597 read=42
I/O Timings: read=2.525 write=0.000
Settings: effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4'
Query ID: -487267322002178245Plan: single project, all agents, 14 ms
Limit (cost=0.43..8571.20 rows=10001 width=22) (actual time=0.065..14.084 rows=10001 loops=1)
Buffers: shared hit=23245
I/O Timings: read=0.000 write=0.000
-> Index Scan using ai_audit_events_202609_project_id_created_at_idx on gitlab_partitions_dynamic.ai_audit_events_202609 ai_audit_events (cost=0.43..211216.22 rows=246462 width=22) (actual time=0.063..13.301 rows=10001 loops=1)
Index Cond: ((ai_audit_events.project_id = 278964) AND (ai_audit_events.created_at >= '2026-09-21 00:00:00+00'::timestamp with time zone) AND (ai_audit_events.created_at < '2026-09-28 04:22:52+00'::timestamp with time zone))
Filter: (ai_audit_events.event_name = 'ai_tool_invoked'::text)
Rows Removed by Filter: 14789
Buffers: shared hit=23245
I/O Timings: read=0.000 write=0.000
Settings: effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4'
Query ID: -7032874156194148889How to validate
ee/spec/models/audit_events/ai_audit_event_spec.rb, ee/spec/services/ai/governance/postgresql_tool_calls_service_spec.rb.
Follow-ups
Agent class is resolved through the workflow today. When ai_governance_sessions lands (#630095), this moves to the session's source.