Prevent drift in Foundational Items and lazy loading
The problem
Foundational flows (developer/v1, etc.) declare a set of triggers: (e.g. [:assign, :mention]). For each project where a flow is enabled, an Ai::FlowTrigger row must exist per declared event type so that note/assign/reviewer events actually fire the flow. Today that mapping is materialized eagerly by a sync pipeline.
The pain point is drift. The triggers: array on a foundational flow is mutable across deploys (e.g. !228817 (merged) added mention to developer/v1), but already-synced projects have no mechanism to pick up the new trigger — they silently drift. There is also no observable signal of the gap: flows appear enabled but never fire on the events they should.
A re-sync across already-configured projects would close the drift, but the cascade is sensitive to namespace size — past performance issues at large scale have been resolved, but large namespaces remain the most expensive customers to re-sync, so we want to avoid making "rewrite every project on every trigger-set change" the standard remediation path.
Current solution
Triggers are created by Ai::Catalog::Flows::SyncFoundationalFlowsService, fanned out from CascadeSyncFoundationalFlowsWorker (group-level) and SyncProjectFoundationalFlowsWorker (project-level), and invoked on project creation, on settings change via Ai::CascadeDuoSettingsService, and on explicit worker enqueue. This pipeline is functional; the recent perf work has stabilized it for the namespace sizes we have today.
MR !231433 (merged) added a single, narrow lazy-load path in EE::Notes::PostProcessService#process_ai_flow_triggers: when a note mentions a service account, the post-processor checks whether the foundational flow declares a mention trigger and creates the row inline if it's missing. This patched the drift for the mention event on developer/v1 without forcing a re-sync over every affected project.
The other user-driven trigger paths (assign, assign_reviewer, pipeline_hooks) have no equivalent and rely entirely on the eager sync.
Future problems
The drift class is recurring, not one-off. Every time a foundational flow's triggers: changes, every already-synced project re-enters the same broken state. A blanket cascade fixes it but is disproportionately expensive on large namespaces, and that cost grows as customers grow — we don't want to rely on a full rewrite as the standard fix.
The same is true for the trigger types we haven't lazy-loaded. As soon as a flow declares a new assign (or any future event), projects already synced will silently fail to fire on it until something invalidates their rows. The fix in !231433 (merged) only covers the one event we noticed.
Advised solution
Generalize the lazy-load pattern from MR !231433 (merged) across all user-driven trigger entry points (assign on issue/MR services, assign_reviewer on reviewer-request services). The "SA must already be a project member to be mentioned/assigned" invariant holds on each, so the work is bounded and only happens on projects that are actively being used — no disruption to large namespaces, no full re-sync needed when a trigger set changes. Extract the helpers from EE::Notes::PostProcessService into a single service so the four firing paths share one implementation, and switch to create_or_find_by (with a unique index covering project_id, user_id, event_types) to handle concurrent firings safely.
Skip pipeline_hooks — it's supported_events, not triggers, and doesn't share the same invariant.
Treat the eager cascade as the way new and reconfigured projects get set up, and the lazy-load as the safety net that keeps already-synced projects correct when the trigger definitions evolve — without making large-namespace re-syncs a routine operation.