Loading
Add ai_governance_sessions table
What does this MR do and why?
Adds ai_governance_sessions, one row per AI session. It covers GitLab Duo sessions (Chat and agent flows) and external agent sessions such as Claude Code, MCP and glab.
Today an external agent session can only be stored by creating a dummy duo_workflows_workflows row, because duo_workflow_session_artifacts.workflow_id is NOT NULL with a foreign key. That pollutes the workflows table. ai_governance_sessions removes the need for it.
Nothing consumes the table yet.
Design points:
namespace_idisNOT NULLand is the sharding key. Project-scoped rows store the project namespace id, so a group hierarchy lookup is a singlenamespace_id IN (...). This copiesduo_workflow_session_artifactsrather thanduo_workflows_workflows, which uses a XOR check constraint and so needs a join for group queries.namespace_idhas a loose foreign key (config/gitlab_loose_foreign_keys.yml,on_delete: async_delete): when a namespace is deleted, its sessions are deleted asynchronously, since the session data belongs to the namespace.project_id,user_idandworkflow_idhave no foreign key at all, so sessions are kept and these ids can dangle when the project, user or workflow is deleted — audit data should outlive the entity it describes. Onlyproject_id,user_idandworkflow_idare listed inspec/db/schema_spec.rb'signored_fk_columns_map;sharding_key_spec.rbdoesn't need a change, since the loose FK onnamespace_idalready satisfies its check.db/docs/data_retention/ai_governance_sessions.ymldeclaresindefinite_retention: there's no time-based purge, and namespace deletion is the only path that removes sessions.namespace_idanduser_idare still validated withpresence: trueat the model level, matching theirNOT NULLcolumns, even though the associations are alloptional: trueso a session stays readable and writable once its target is gone.- A session is either Duo-sourced (
workflow_id, a back-pointer to a realAi::DuoWorkflows::Workflow) or externally-sourced (external_xid, an external agent's own identifier).ExactlyOnePresentValidatorenforces exactly one of the two, never both, never neither. - The keyset index is named explicitly. The generated name would be
index_ai_governance_sessions_on_namespace_id_and_session_started_at_and_id, 74 characters, past the 63-character Postgres identifier limit. - Includes a dev fixture (
ee/db/fixtures/development/99_ai_governance_sessions.rb) seeding one Duo-sourced and one external-agent row, sodb:migrate:multi-version-upgradehas a row to exercise for this new table.
References
- Closes https://gitlab.com/gitlab-org/gitlab/-/work_items/629762
- Epic: https://gitlab.com/groups/gitlab-org/-/epics/21540
- Supersedes the proposal in https://gitlab.com/gitlab-org/gitlab/-/work_items/628968
Database
Click to expand Migration command output
### up
bundle exec rake db:migrate:up_all VERSION=20260918110508
main: == [advisory_lock_connection] object_id: 164860, pg_backend_pid: 36506
main: == 20260918110508 CreateAiGovernanceSessions: migrating =======================
main: -- create_table(:ai_governance_sessions, {:if_not_exists=>true})
main: -- quote_column_name(:external_xid)
main: -> 0.0000s
main: -- quote_column_name(:agent_type)
main: -> 0.0000s
main: -- quote_column_name(:model_used)
main: -> 0.0000s
main: -- quote_column_name(:flow_type)
main: -> 0.0000s
main: -> 0.0911s
main: == 20260918110508 CreateAiGovernanceSessions: migrated (0.0975s) ==============
main: == [advisory_lock_connection] object_id: 164860, pg_backend_pid: 36506
ci: == [advisory_lock_connection] object_id: 164860, pg_backend_pid: 36507
ci: == 20260918110508 CreateAiGovernanceSessions: migrating =======================
ci: -- create_table(:ai_governance_sessions, {:if_not_exists=>true})
ci: -- quote_column_name(:external_xid)
ci: -> 0.0000s
ci: -- quote_column_name(:agent_type)
ci: -> 0.0000s
ci: -- quote_column_name(:model_used)
ci: -> 0.0000s
ci: -- quote_column_name(:flow_type)
ci: -> 0.0000s
ci: -> 0.0230s
I, [2026-09-21T10:54:42.130732 #35637] INFO -- : Database: 'ci', Table: 'ai_governance_sessions': Lock Writes
ci: == 20260918110508 CreateAiGovernanceSessions: migrated (0.0372s) ==============
ci: == [advisory_lock_connection] object_id: 164860, pg_backend_pid: 36507
sec: == [advisory_lock_connection] object_id: 164860, pg_backend_pid: 36509
sec: == 20260918110508 CreateAiGovernanceSessions: migrating =======================
sec: -- create_table(:ai_governance_sessions, {:if_not_exists=>true})
sec: -- quote_column_name(:external_xid)
sec: -> 0.0000s
sec: -- quote_column_name(:agent_type)
sec: -> 0.0000s
sec: -- quote_column_name(:model_used)
sec: -> 0.0000s
sec: -- quote_column_name(:flow_type)
sec: -> 0.0000s
sec: -> 0.0229s
I, [2026-09-21T10:54:42.303934 #35637] INFO -- : Database: 'sec', Table: 'ai_governance_sessions': Lock Writes
sec: == 20260918110508 CreateAiGovernanceSessions: migrated (0.0366s) ==============
sec: == [advisory_lock_connection] object_id: 164860, pg_backend_pid: 36509
### down
bundle exec rake db:migrate:down_all VERSION=20260918110508
main: == [advisory_lock_connection] object_id: 164840, pg_backend_pid: 34643
main: == 20260918110508 CreateAiGovernanceSessions: reverting =======================
main: -- drop_table(:ai_governance_sessions)
main: -> 0.0417s
main: == 20260918110508 CreateAiGovernanceSessions: reverted (0.0478s) ==============
main: == [advisory_lock_connection] object_id: 164840, pg_backend_pid: 34643
ci: == [advisory_lock_connection] object_id: 164840, pg_backend_pid: 34644
ci: == 20260918110508 CreateAiGovernanceSessions: reverting =======================
ci: -- drop_table(:ai_governance_sessions)
ci: -> 0.0035s
ci: == 20260918110508 CreateAiGovernanceSessions: reverted (0.0129s) ==============
ci: == [advisory_lock_connection] object_id: 164840, pg_backend_pid: 34644
sec: == [advisory_lock_connection] object_id: 164840, pg_backend_pid: 34646
sec: == 20260918110508 CreateAiGovernanceSessions: reverting =======================
sec: -- drop_table(:ai_governance_sessions)
sec: -> 0.0037s
sec: == 20260918110508 CreateAiGovernanceSessions: reverted (0.0147s) ==============
sec: == [advisory_lock_connection] object_id: 164840, pg_backend_pid: 34646
How to set up and validate locally
bundle exec rails db:migratebundle exec rspec ee/spec/models/ai/governance/session_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.
Edited by Hitesh Raghuvanshi