Add Anthropic Compliance transcript walk state table
What does this MR do and why?
Adds ai_compliance_anthropic_sessions, which holds transcript walk state for each Claude Code session the Anthropic Compliance API ingests: the newest message id read (message_watermark) and when the current walk started (walk_started_at).
Each row belongs to exactly one ai_governance_sessions row through a unique ai_governance_session_id. The transcript job looks rows up by that id, and rows are deleted with their governance session, integration, or namespace. The model fills namespace_id from the integration and checks that it matches both the integration's and the governance session's namespace.
Session deduplication is handled on ai_governance_sessions, so there is no unique index on session_xid. The column is kept so a stalled walk can be diagnosed without a join.
Nothing writes to the table yet. The transcript worker (https://gitlab.com/gitlab-org/gitlab/-/work_items/623178) will create a row the first time it walks a session and update it on later walks. The session creation service (https://gitlab.com/gitlab-org/gitlab/-/work_items/623175) only creates the ai_governance_sessions row. Pruning is https://gitlab.com/gitlab-org/gitlab/-/work_items/628488.
Migration layout
The table is created with its three indexes and no foreign keys. Each of the three foreign keys (namespaces, ai_compliance_anthropic_integrations, ai_governance_sessions) is then added in its own migration, so no single migration locks more than one table. All three cascade on delete.
References
- Closes https://gitlab.com/gitlab-org/gitlab/-/work_items/628485
- Design doc: gitlab-com/content-sites/handbook!20750 (merged)
How to set up and validate locally
bundle exec rails db:migratebundle exec rspec ee/spec/models/ai/compliance/anthropic/session_spec.rb
New table: ai_compliance_anthropic_sessions
Answers to the database review guide for new tables. Volume is based on the design document estimate, which is not measured yet. Real volume is being measured in https://gitlab.com/gitlab-org/gitlab/-/work_items/623183.
Assumptions
- One row per Claude Code session the transcript job walks. The row is created on the first walk.
- One large customer (1,000 developers, 10 sessions per developer per day) = 10,000 new rows a day.
- The cron runs every 6 hours. Each session is walked about 2 times a day.
- Rows are kept for 30 days from creation. Pruning is tracked in https://gitlab.com/gitlab-org/gitlab/-/work_items/628488. Until it ships (
enforcing: false), rows only leave when their governance session, integration, or namespace is deleted. - About 260 bytes per row including the four indexes (three plus the primary key). (Guessed: 4 bigints, 3 timestamps, two ~32-character text ids, tuple header.)
- Number of large customers using it: guessed at 3 after 3 months, 10 after 6 months, 25 after 12 months.
Expected growth
| 3 months | 6 months | 12 months | |
|---|---|---|---|
| Large customers (guessed) | 3 | 10 | 25 |
| Rows with 30-day pruning (customers × 10,000 × 30) | 0.9 M | 3 M | 7.5 M |
| Size with pruning (rows × 260 B) | ~0.2 GB | ~0.8 GB | ~2 GB |
| Rows if pruning has not shipped (customers × 10,000 × days live) | 2.7 M | 18 M | 91 M |
With pruning, the table stays under the small size class. Without pruning, it would reach about 24 GB at 12 months, so the pruning follow-up needs to ship before adoption grows.
Reads and writes per hour (one large customer)
| Operation | Per day | Per hour (average) | How |
|---|---|---|---|
| Insert | 10,000 | ~420 | First walk of a session |
| Update | ~40,000 | ~1,700 | 2 walks × 2 updates (set walk_started_at at the start; set message_watermark and clear walk_started_at at the end) |
Read by ai_governance_session_id |
~20,000 to 40,000 | ~830 to 1,700 | Start and end of each walk |
Multiply by the number of customers. At 25 customers this is roughly 10,000 inserts, 42,000 updates and up to 42,000 reads an hour. Load comes in bursts after each 6-hour cron run, so the peak is several times the hourly average. (The burst factor is a guess; transcript workers use concurrency_limit.)
Update pattern
- Insert once, then update on every walk. Only
walk_started_at,message_watermarkandupdated_atchange. None of them is indexed, so these updates can be HOT updates (updates that don't touch indexes). - Rows are deleted by cascade from
ai_governance_sessions,ai_compliance_anthropic_integrationsandnamespaces, and later by 30-day pruning.
Availability risk
- No web request reads this table. Only the Anthropic Compliance transcript Sidekiq worker uses it (added in a follow-up MR, behind a feature flag). The AI Governance page reads
ai_governance_sessionsand ClickHouse. - If the table is unavailable, transcript jobs fail and Sidekiq retries them. The next 6-hour run walks the sessions again. Nothing is lost while the outage is shorter than the 12-hour list window.
- If a row is lost, the next walk has no watermark and reads the whole transcript again. Events are deduplicated by
cloud_event_id, so no duplicate rows are stored, but the whole session is sent to the customer's audit event stream (SIEM) a second time.
Migration output
Up
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 45068
main: == 20261008154500 CreateAiComplianceAnthropicSessions: migrating ==============
main: -- create_table(:ai_compliance_anthropic_sessions)
main: -- quote_column_name(:session_xid)
main: -> 0.0000s
main: -- quote_column_name(:message_watermark)
main: -> 0.0000s
main: -> 0.0556s
main: == 20261008154500 CreateAiComplianceAnthropicSessions: migrated (0.0661s) =====
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 45068
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 45246
main: == 20261008154504 AddFkAiComplianceAnthropicSessionsToNamespaces: migrating ===
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- execute("LOCK TABLE namespaces, ai_compliance_anthropic_sessions IN SHARE ROW EXCLUSIVE MODE")
main: -> 0.0004s
main: -- execute("ALTER TABLE ai_compliance_anthropic_sessions ADD CONSTRAINT fk_bcc0d20c6c FOREIGN KEY (namespace_id) REFERENCES namespaces (id) ON DELETE CASCADE NOT VALID;")
main: -> 0.0039s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- execute("ALTER TABLE ai_compliance_anthropic_sessions VALIDATE CONSTRAINT fk_bcc0d20c6c;")
main: -> 0.0158s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20261008154504 AddFkAiComplianceAnthropicSessionsToNamespaces: migrated (0.0949s)
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 45246
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 45465
main: == 20261008154508 AddFkAiComplianceAnthropicSessionsToIntegrations: migrating =
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- execute("LOCK TABLE ai_compliance_anthropic_integrations, ai_compliance_anthropic_sessions IN SHARE ROW EXCLUSIVE MODE")
main: -> 0.0003s
main: -- execute("ALTER TABLE ai_compliance_anthropic_sessions ADD CONSTRAINT fk_2b8b35da36 FOREIGN KEY (anthropic_integration_id) REFERENCES ai_compliance_anthropic_integrations (id) ON DELETE CASCADE NOT VALID;")
main: -> 0.0015s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0002s
main: -- execute("ALTER TABLE ai_compliance_anthropic_sessions VALIDATE CONSTRAINT fk_2b8b35da36;")
main: -> 0.0021s
main: -- execute("RESET statement_timeout")
main: -> 0.0002s
main: == 20261008154508 AddFkAiComplianceAnthropicSessionsToIntegrations: migrated (0.0712s)
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 45465
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 45786
main: == 20261008154512 AddFkAiComplianceAnthropicSessionsToGovernanceSessions: migrating
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- execute("LOCK TABLE ai_governance_sessions, ai_compliance_anthropic_sessions IN SHARE ROW EXCLUSIVE MODE")
main: -> 0.0004s
main: -- execute("ALTER TABLE ai_compliance_anthropic_sessions ADD CONSTRAINT fk_a6ee74a7af FOREIGN KEY (ai_governance_session_id) REFERENCES ai_governance_sessions (id) ON DELETE CASCADE NOT VALID;")
main: -> 0.0016s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- execute("ALTER TABLE ai_compliance_anthropic_sessions VALIDATE CONSTRAINT fk_a6ee74a7af;")
main: -> 0.0040s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20261008154512 AddFkAiComplianceAnthropicSessionsToGovernanceSessions: migrated (0.0729s)
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 45786Down
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 43997
main: == 20261008154512 AddFkAiComplianceAnthropicSessionsToGovernanceSessions: reverting
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- remove_foreign_key(:ai_compliance_anthropic_sessions, {:column=>:ai_governance_session_id})
main: -> 0.0049s
main: == 20261008154512 AddFkAiComplianceAnthropicSessionsToGovernanceSessions: reverted (0.0978s)
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 43997
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 44234
main: == 20261008154508 AddFkAiComplianceAnthropicSessionsToIntegrations: reverting =
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- remove_foreign_key(:ai_compliance_anthropic_sessions, {:column=>:anthropic_integration_id})
main: -> 0.0022s
main: == 20261008154508 AddFkAiComplianceAnthropicSessionsToIntegrations: reverted (0.0687s)
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 44234
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 44436
main: == 20261008154504 AddFkAiComplianceAnthropicSessionsToNamespaces: reverting ===
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- remove_foreign_key(:ai_compliance_anthropic_sessions, {:column=>:namespace_id})
main: -> 0.0038s
main: == 20261008154504 AddFkAiComplianceAnthropicSessionsToNamespaces: reverted (0.0766s)
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 44436
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 44655
main: == 20261008154500 CreateAiComplianceAnthropicSessions: reverting ==============
main: -- drop_table(:ai_compliance_anthropic_sessions)
main: -> 0.0389s
main: == 20261008154500 CreateAiComplianceAnthropicSessions: reverted (0.0488s) =====
main: == [advisory_lock_connection] object_id: 166040, pg_backend_pid: 44655MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.