Limit legacy session artifact auditEvents to one session per request

What changes for users

  • An API call that asks for auditEvents on several session artifacts in one request gets the first one and an error for the rest.
  • The dashboard and the Audit events tab are not affected: their lists ask only for auditEventsCount, and the drawer asks for one session.
  • This is the same rule the sessions API field already has (Add auditEvents to AiGovernanceSession (!258505 - merged)).

DuoWorkflowSessionArtifact.auditEvents (the legacy session artifacts field, an experiment since 19.1) now resolves for one session per request, and its ClickHouse lookup is bounded by the session's start time. This matches what AiGovernanceSession.auditEvents already does.

Why

The agent artifacts pages list sessions and open one session's audit events in a drawer. On AiGovernanceSession, auditEvents already has Gitlab::Graphql::Limit::FieldCallCount with a limit of 1, and it passes the session start time (less a one-day clock skew) to the finder.

The older field had neither. A request could nest auditEvents under a full page of 100 artifacts, which runs a workflow lookup plus one audit events query per artifact. Each query had no start time, so ClickHouse could not skip any created_at partition. The work grew with the page size, on servers other features share.

The change

  • DuoWorkflowSessionArtifact.auditEvents gets extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1, and its description says it resolves for one session per request. A request that nests it under several artifacts gets the events of the first and a GraphQL error for the rest, as on the sessions API field.
  • SessionArtifactType#audit_events passes created_after (workflow creation time minus Ai::Governance::Session::AUDIT_EVENTS_CLOCK_SKEW) to AuditEvents::AiAuditEvents::Finder. The finder forwards it to the ClickHouse and PostgreSQL finders, which already accept it. Events cannot predate their workflow by more than the skew, so none are lost, and ClickHouse prunes partitions before that time.
  • auditEventsCount on the list runs one count per page, from the earliest session start on the page minus the one-day clock skew, so it is an upper bound. It can be higher than the drawer only when an event is more than a day older than its own session, as on AiGovernanceSession.
  • The frontend is unaffected. The two list queries (get_agent_artifacts.query.graphql, get_project_agent_artifacts.query.graphql) select only auditEventsCount. The two drawer queries (get_group_session_audit_events.query.graphql, get_project_session_audit_events.query.graphql) select auditEvents under duoWorkflowSessionArtifacts(workflowId: ...), one session at a time.
  • GraphQL reference and introspection are regenerated. No migration, no UI change.
  • Specs: a request spec for two artifacts in one query (first resolves, second errors), one for the created_after bound reaching the finder, and the existing workflowId drawer path. A finder spec covers the forwarded keyword. A type spec covers the limit.
Queries, before and after

Both queries already run today for the drawer; this MR adds the created_at >= bound. The same predicate reached both finders in Add auditEvents to AiGovernanceSession (!258505 - merged) (the auditEvents field on AiGovernanceSession), where it was reviewed for the sessions API.

PostgreSQL (AuditEvents::AiAuditEvents::PostgresqlFinder), before:

SELECT "ai_audit_events".* FROM "ai_audit_events"
WHERE "ai_audit_events"."workflow_id" = 12345
ORDER BY "ai_audit_events"."created_at" DESC, "ai_audit_events"."id" DESC LIMIT 21

After:

SELECT "ai_audit_events".* FROM "ai_audit_events"
WHERE "ai_audit_events"."workflow_id" = 12345 AND "ai_audit_events"."created_at" >= '2026-09-30 12:00:00'
ORDER BY "ai_audit_events"."created_at" DESC, "ai_audit_events"."id" DESC LIMIT 21

The table is partitioned by range on created_at and indexed on (workflow_id, created_at DESC, id DESC), so the bound prunes partitions and the index serves the order. On GitLab.com the table has no rows yet: a Database Lab run of the after query on 2026-10-29 scanned the eight monthly partitions from 2026-09 onwards and returned 0 rows (6 shared buffers, 0.06 ms), skipping every partition before the bound; with empty partitions the planner picks sequential scans, so the index choice cannot be shown until data exists. The same bound on the same index was reviewed in Add auditEvents to AiGovernanceSession (!258505 - merged).

ClickHouse (AuditEvents::AiAuditEvents::ClickHouseFinder), inner query before:

SELECT id, workflow_id, any(ai_governance_session_id) AS ai_governance_session_id, created_at, traversal_path,
       any(event_name) AS event_name, any(author_id) AS author_id, any(ip_address) AS ip_address, any(details) AS details
FROM ai_audit_events
WHERE workflow_id = 12345
GROUP BY traversal_path, workflow_id, created_at, id

After:

SELECT id, workflow_id, any(ai_governance_session_id) AS ai_governance_session_id, created_at, traversal_path,
       any(event_name) AS event_name, any(author_id) AS author_id, any(ip_address) AS ip_address, any(details) AS details
FROM ai_audit_events
WHERE workflow_id = 12345 AND created_at >= '2026-09-30 12:00:00'
GROUP BY traversal_path, workflow_id, created_at, id

The outer query (ORDER BY created_at DESC, id DESC LIMIT 21) is unchanged. The table is partitioned by toYYYYMM(created_at), so the bound skips every partition before the session's month. The details column leaves this query in Load AI audit event details after the ClickHous... (!265815).

Decisions

  • A per-request limit of 1, over batching lookups across sessions. It is the precedent on the sessions API field, the UI only asks for one session, and batching N sessions into one query would widen the time range the way a pooled lookup does.
  • Limit 1, over a higher number. The drawer is the only consumer, and the sessions API field already uses 1, so API callers see one rule.

Out of scope

  • DuoWorkflowWorkflow.auditEvents (under duoWorkflowWorkflows lists) has the same gap. It belongs to the Duo Agent Platform types, so it is a follow-up for that team or a later MR on this issue.
  • The audit events details lookup in Load AI audit event details after the ClickHous... (!265815) stays keyed by row kind. Nothing here changes it.

Related to #635324.

Edited by Dheeraj Joshi

Merge request reports

Loading
Loading