QA evaluation backfill execution family
Issue: ENG-3036Â (child of ENG-2681). Owner: engineering (Chidi). Reviewer: Ronald (independent review required before merge).
Complete CloudWatch worker logs after the earlier ownership release showed
US normal-path GoodJob starts from 2026-09-11T20:00:03Z through
20:07:19.776Z for QAEvaluationBackfill::PreviewJob, ConfirmJob,
QueueReplacementJob, BackdateJob, and ReconcileJob: two automatic runs
(run 171 for organization 19, run 172 for organization 25), request 5899294
and replacement run-agent 1512 from run 171, 11 attempts / 11 unique jobs.
EU had none. Those rows reset the zero-GoodJob clock; backfills are not
maintenance and block the milestone.
Reopened 2026-09-16. The first package shipped API2 execution behind a
default-off PostHog organization gate (qa_evaluation_backfill_api2_owner)
with the Rails GoodJob classes as the gate-off path. The exact EU Rails-worker
CloudWatch window 2026-09-15T15:06:59Z → 2026-09-16T15:06:59Z still contained
QAEvaluationBackfill::PreviewJob for backfill run 1156, organization 33,
because the gate was false for that organization. The run was cancelled, but
the Rails preview kept planning for roughly two hours and ended in a
PostgreSQL statement timeout. A default-off flag therefore does not meet the
completion target; the target is zero normal-path Rails jobs.
This correction makes API2/BullMQ the permanent normal owner of the complete
five-job family for every organization. There is no organization gate, no
environment override, and no Rails fallback on the normal path. Rails keeps its
admin/report HTTP entrypoints and model/request callbacks as compatibility
enqueuers only, and they never perform_later these classes. The five Rails
job classes remain solely as operator recovery tombstones: their
enqueue raises QAEvaluationBackfill::OperatorRecoveryTombstone::NormalPathDisabledError,
so perform_later, set(wait:).perform_later, and automatic retries all fail;
only an explicit perform_now from an operator console runs them.
Ownership boundary
| Concern | Owner after this package | Notes |
|---|---|---|
| Preview (plan) | API2 qa-evaluation-backfill worker, runPreview | Port of PreviewJob + PlanBuilder + CoveragePeriod: 5-minute SET LOCAL statement_timeout, organization-local target date and fixed-per-period coverage window, agent-partitioned rules, employee filter, completed active AI evaluations + in-flight requests of every trigger as consumed quota, ticket scope, recorded-call eligibility, scorecard/employee matching, poisoned-vs-sampling-gated skip reasons, eligible/standby order, observed in-flight request ids. Writes run-agents with public ids and moves previewing → preview_ready only via UPDATE … WHERE status = 'previewing' (a concurrent cancel wins). Failure → failed + truncated notes. auto_proceed → preview_ready → queueing under the row lock and one deterministic confirm dispatch |
| Confirm (queue) | API2 worker, runConfirm | Row lock, queueing transition, excluded agents skipped, per-conversation tenant + active-request uniqueness (partial unique index idx_qa_eval_requests_active_conversation_unique as the race tolerance via ON CONFLICT … DO NOTHING), admin_forced requests linked to the run-agent with qa_evaluation_request_* public ids, requested_count recomputed from durable rows so a partial retry never double-counts, queued status, and empty-plan auto-backdate. Every created request is enqueued on the existing API2 QA orchestration queue (process-qa-evaluation-request) — no side effect of the request lifecycle is duplicated here |
| Replacement | API2 worker, runReplacement | Triggered from the Rails terminal callback (QAEvaluationRequest#enqueue_backfill_replacement_if_needed on completed/skipped/failed/cancelled backfill requests) through the dispatcher. Locks the run-agent, stops when excluded or quota is filled against completed AI evaluations in the coverage window plus in-flight requests of every trigger, pops standby ids exactly once (persisted back), skips missing/already-requested/active conversations, stamps replaces_qa_evaluation_request_id, updates requested_count, stamps standby_exhausted_at once, then re-evaluates auto-backdate. Completed-but-net-zero evaluations still replace |
| Backdate | API2 worker, runBackdate | Only the run’s completed requests; organization-local 23:55 on the target date; one transaction updates qa_evaluations, every qa_agent_evaluations row, request completed_at/evaluation_started_at/prep_completed_at, and conversations.qa_evaluated_at; idempotent; running → completed/failed with backdated_at; count logging; exactly one reconcile dispatch |
| Reconcile | API2 worker, runReconcile | One unsettled run per organization/date (Rails partial unique index); previewing auto runs are re-dispatched to preview; observed external request ids inspected; bounded one-minute waits up to 90 attempts with a deterministic id per attempt; no speculative empty runs; follow-up auto run only for a currently fillable shortfall; otherwise exactly one post-backfill report on the existing daily-agent-evaluation-report queue with source: "backfill", sendEmail: true, queueBackfill: false |
| Request terminal callbacks, broadcasts, requester notifications, queue/sampling slot release, conversation score/cache updates, summarize/event/workload links, report delivery | Unchanged (Rails model callbacks + existing API2 QA completion) | The new worker only creates requests and hands them to the existing orchestrator |
Rails QAEvaluationBackfill::*Job | Operator recovery tombstones only | No automatic caller. include QAEvaluationBackfill::OperatorRecoveryTombstone makes every enqueue path (perform_later, set(...).perform_later, perform_all_later, retry-on) raise NormalPathDisabledError. Reachable only by an explicit perform_now in an operator console. Rails PreviewJob#perform additionally re-reads run status every 5 s from PlanBuilder checkpoints and enforces a 30-minute wall-clock budget so even a recovery run stops promptly on cancel |
Rails normal-path entrypoint inventory
Every Rails producer routes unconditionally through
QAEvaluationBackfill::Api2Dispatcher (rulebase-api/app/services/qa_evaluation_backfill/api2_dispatcher.rb).
The inventory below is exhaustive for app/, lib/, config/, and
db/data/; spec/lint/qa_evaluation_backfill_normal_path_spec.rb fails the
build if any of them (or a new one) reaches a job class directly.
| Entrypoint | Stage | Handoff |
|---|---|---|
Admin::QAEvaluationBackfillsController#create (manual admin backfill) | Preview | Api2Dispatcher.preview_later(run) |
Admin::QAEvaluationBackfillsController#confirm (row-locked preview_ready → queueing) | Confirm | confirm_later(run) |
Admin::QAEvaluationBackfillsController#backdate | Backdate | backdate_later(run) — asynchronous for every organization; the response meta.backdated_count is null (the previous gate-off branch ran BackdateJob.perform_now inline) |
Admin::QAEvaluationBackfillsController#cancel | — | Row-locked status write only; no dispatch |
Internal::DailyAgentEvaluationReportsController#backfill_preview → QAEvaluationBackfill::DailyReportPreviewQueuer (API2 report tick / scheduled) | Preview | preview_later(run) for new and stuck previewing auto runs |
SendDailyAgentEvaluationReportForOrganizationJob (report queue_backfill) → DailyReportPreviewQueuer | Preview | preview_later(run) |
QAEvaluationBackfillRun#enqueue_confirm_if_auto_proceed! | Confirm | confirm_later(run) after the locked preview_ready → queueing claim |
QAEvaluationBackfillRun#enqueue_backdate_if_auto_proceed! | Backdate | backdate_later(run) after the locked none → running claim |
QAEvaluationBackfillRun#enqueue_quota_reconciliation! | Reconcile | reconcile_later(organization, target_date) |
QAEvaluationRequest#enqueue_backfill_replacement_if_needed (terminal callback on completed/skipped/failed/cancelled backfill requests) | Replacement | replacement_later(run_agent, replaces_qa_evaluation_request_id:) |
Rails ReconcileJob#perform / BackdateJob#perform / PreviewJob#perform chains (operator recovery only) | Preview / Reconcile / Confirm | Downstream stages are dispatched to API2 (preview_later, reconcile_later(…, wait_attempt:), confirm_later), never re-enqueued on GoodJob |
| Retries | all | None on Rails: the tombstone concern makes retry_on/re-enqueue raise. BullMQ owns retries (5 attempts, exponential backoff from 30 s) |
db/data/20260827151400_backfill_david_okon_aug24_qa_evaluations.rb (shipped, immutable data migration) | Preview | Historical, already applied. Its stale PreviewJob.perform_later call is unreachable on the normal path: rerunning it now raises NormalPathDisabledError (covered by spec/db/data/…_spec.rb). Not edited per the immutable-migration rule |
The dispatcher posts POST /internal/qa_evaluation_backfills/enqueue
(bearer RAILS_INTERNAL_API_KEY) and treats only a 202 as acceptance. Any
non-2xx or transport error raises Api2Dispatcher::DeliveryError — there is
no GoodJob fallback; the calling request/callback fails loudly and is
retried by its own caller. The dispatcher never consults PostHog, Flipper, or
Organization.
Queue and ids
- Queue
qa-evaluation-backfill, worker concurrency 4, one organization + one run/run-agent per job (failure isolation at the run-agent / conversation boundary inside confirm and replacement). - Deterministic ids:
qa-eval-backfill-preview-<run>,qa-eval-backfill-confirm-<run>,qa-eval-backfill-replacement-<run_agent>-<replaced_request|manual>,qa-eval-backfill-backdate-<run>,qa-eval-backfill-reconcile-<org>-<date>-<wait_attempt>. Finished jobs with the same id are replaced (addReplacingFinishedJob); an active job wins. The report handoff reuses the existing deterministic organization report job id. - 5 attempts, exponential backoff from 30s (GoodJob parity); run/run-agent rows checkpoint progress so every kind is safe to re-drive.
- The worker executes for every organization. There is no execution-time
owner check, no
gate_offoutcome, and noQA_EVALUATION_BACKFILL_OWNERenvironment override.
Cancellation during preview
runPreview passes a checkpoint into PlanBuilder, awaited before each agent
plan and before each eligibility batch. The checkpoint enforces a 30-minute
wall-clock budget (PREVIEW_WALL_CLOCK_BUDGET_MS; each statement is still
capped by the 5-minute SET LOCAL statement_timeout) and, at most every 5 s
(PREVIEW_CANCELLATION_CHECK_INTERVAL_MS), re-reads the run row. Under READ
COMMITTED the read sees an admin cancel committed after planning started. A
run that is no longer previewing throws PreviewCancelledError, which rolls
back the planning transaction, returns
{ outcome: "skipped", reason: "cancelled_during_planning" } without marking
the run failed, and is not retried. Budget exhaustion throws
PreviewBudgetExceededError, marks the run failed with a note, and rethrows
so BullMQ records the attempt. The final locked previewing check before the
run-agent write stays as the last line of defense. The Rails tombstone
PreviewJob has the same checkpoint (CANCELLATION_CHECK_INTERVAL,
PREVIEW_WALL_CLOCK_BUDGET) for operator recovery runs.
Ownership (no feature gate)
The qa_evaluation_backfill_api2_owner flag, Organization#qa_evaluation_backfill_api2_owner?,
API2 resolveQaEvaluationBackfillOwner, POSTHOG_FEATURE_FLAGS.QA_EVALUATION_BACKFILL_API2_OWNER,
and QA_EVALUATION_BACKFILL_OWNER are removed. The flag was never created in
production and nothing in this correction creates, reads, or deletes it. If a
stale definition exists in PostHog it is inert and may be deleted at any time.
Rollout gates
- Merge only after independent review and green CI (Rails suite including
spec/lint/qa_evaluation_backfill_normal_path_spec.rb; API2pnpm typecheckand the PGlite suite). - Deploy Rails and API2 together (the dispatcher already exists on both sides; the change removes the Rails branch and the API2 gate, so ordering is not load-bearing, but do not leave one side undeployed across a daily report tick).
- Immediately after deploy: run the outcome queries below for the previous
and current target dates in both regions; expect no GoodJob rows for the
five classes with
created_at >= :deployed_atand everyauto_proceedrun reachingbackdate_status = 'completed'within 120 minutes. - Strict CloudWatch verification (required before closing ENG-3036):
after at least one full daily cycle per region, query the Rails worker log
group for the exact 24-hour window with the filter
"QAEvaluationBackfill::" ("PreviewJob" OR "ConfirmJob" OR "QueueReplacementJob" OR "BackdateJob" OR "ReconcileJob")and require zero job-start rows. Record the exact window boundaries, region, and result in the migration log. An operatorperform_nowrecovery is the only acceptable match and must be linked to an incident note.
Rollback trigger
Any of: a DeliveryError spike in Rails (Sentry), an organization/date
failing the invariant query after 120 minutes, duplicate active requests for
one conversation, a run stuck in queueing/running for > 30 minutes, a
preview skipped with cancelled_during_planning for a run that is still
previewing, or cross-tenant ids in API2 logs.
Rollback (code revert or explicit operator-only recovery)
There is no flag to flip. Two options, never combined on the same run:
- Code revert. Revert this PR (Rails and API2 together). That restores
the gated dispatcher; with the flag still absent in production the gate
evaluates false and Rails GoodJob execution resumes for every organization.
Queued BullMQ jobs then complete as
gate_off. Use this only for a systemic defect in the API2 executor. - Explicit operator-only recovery for individual stranded runs, from a
Rails console, within
ActsAsTenant.with_tenant(run.organization):- re-drive on API2 first:
previewing→QAEvaluationBackfill::Api2Dispatcher.preview_later(run);queueing→confirm_later(run);backdate_status: running→backdate_later(run); a missing report →reconcile_later(run.organization, run.target_date). - only if API2 is unavailable:
QAEvaluationBackfill::PreviewJob.perform_now(run.id)(or the matching class) — synchronous, foreground, in the console.perform_laterraises by design. Record the run id, class, operator, and reason in the migration log; that entry justifies the matching CloudWatch row.
- re-drive on API2 first:
Invariant and evidence queries (read-only)
Invariant: for each eligible organization/date, the target shortfall and planned eligible/standby conversations reconcile to terminal requests/evaluations plus explicit excluded, active-request-blocked, no-gap, or standby-exhausted reasons within 120 minutes of the run’s creation.
-- Outcome per run-agent for a coverage date. Replace :org and :date.
WITH ra AS (
SELECT ra.id AS run_agent_id, r.id AS run_id, r.status, r.backdate_status, r.backdated_at,
r.created_at, ra.employee_id, ra.target_count, ra.consumed_count_at_queue,
ra.eligible_count_at_queue, ra.requested_count, ra.excluded, ra.standby_exhausted_at,
jsonb_array_length(ra.eligible_conversation_ids) AS eligible_planned,
jsonb_array_length(ra.standby_conversation_ids) AS standby_remaining,
ra.observed_in_flight_request_ids
FROM public.qa_evaluation_backfill_runs r
JOIN public.qa_evaluation_backfill_run_agents ra ON ra.qa_evaluation_backfill_run_id = r.id
WHERE r.organization_id = :org AND r.target_date = :date
),
req AS (
SELECT q.qa_evaluation_backfill_run_agent_id AS run_agent_id,
array_agg(q.id ORDER BY q.id) AS request_ids,
array_agg(q.replaces_qa_evaluation_request_id) FILTER (WHERE q.replaces_qa_evaluation_request_id IS NOT NULL) AS replacements,
array_agg(q.qa_evaluation_id) FILTER (WHERE q.qa_evaluation_id IS NOT NULL) AS evaluation_ids,
count(*) FILTER (WHERE q.status IN ('pending','ready','processing')) AS active,
count(*) FILTER (WHERE q.status = 'completed') AS completed,
count(*) FILTER (WHERE q.status IN ('skipped','failed','cancelled')) AS non_completed,
min(q.completed_at) AS first_completed_at, max(q.completed_at) AS last_completed_at
FROM public.qa_evaluation_requests q
WHERE q.organization_id = :org AND q.qa_evaluation_backfill_run_agent_id IN (SELECT run_agent_id FROM ra)
GROUP BY 1
)
SELECT ra.*, req.request_ids, req.replacements, req.evaluation_ids,
req.active, req.completed, req.non_completed, req.last_completed_at,
GREATEST(ra.target_count - ra.consumed_count_at_queue, 0) AS shortfall_at_plan,
CASE
WHEN ra.excluded THEN 'excluded'
WHEN GREATEST(ra.target_count - ra.consumed_count_at_queue, 0) = 0
AND jsonb_array_length(ra.observed_in_flight_request_ids) > 0 THEN 'active_request_blocked'
WHEN GREATEST(ra.target_count - ra.consumed_count_at_queue, 0) = 0 THEN 'no_gap'
WHEN ra.standby_exhausted_at IS NOT NULL THEN 'standby_exhausted'
WHEN COALESCE(req.active, 0) = 0
AND COALESCE(req.completed, 0) >= GREATEST(ra.target_count - ra.consumed_count_at_queue, 0) THEN 'filled'
WHEN COALESCE(req.active, 0) > 0 THEN 'in_progress'
ELSE 'shortfall_unexplained'
END AS outcome,
ra.backdate_status = 'completed' AND ra.backdated_at IS NOT NULL AS backdated,
now() - ra.created_at > interval '120 minutes' AS past_window
FROM ra LEFT JOIN req USING (run_agent_id)
ORDER BY ra.run_id, ra.run_agent_id;Rows with outcome = 'shortfall_unexplained' or 'in_progress' and
past_window = true violate the invariant and are the rollback trigger.
-- Report handoff: one post-backfill daily report per completed auto run.
SELECT r.id AS run_id, r.organization_id, r.target_date, r.backdated_at,
(SELECT count(*) FROM public.qa_evaluation_backfill_runs x
WHERE x.organization_id = r.organization_id AND x.target_date = r.target_date
AND x.status NOT IN ('cancelled','failed') AND x.backdate_status <> 'completed') AS unsettled_runs
FROM public.qa_evaluation_backfill_runs r
WHERE r.auto_proceed AND r.backdate_status = 'completed' AND r.target_date = :date;Pair it with the daily-agent-evaluation-report BullMQ job
source = "backfill" for (organization, date) — exactly one expected.
-- Zero-GoodJob assertion after the deploy timestamp (per region).
SELECT job_class, count(*) AS executions, count(DISTINCT active_job_id) AS unique_jobs
FROM good_jobs
WHERE job_class IN ('QAEvaluationBackfill::PreviewJob','QAEvaluationBackfill::ConfirmJob',
'QAEvaluationBackfill::QueueReplacementJob','QAEvaluationBackfill::BackdateJob',
'QAEvaluationBackfill::ReconcileJob')
AND created_at >= :deployed_at
GROUP BY 1;The expected result is zero rows. Any row is either an operator
perform_now with a logged justification or a regression to fix.
-- Exact outcome check for the reopened incident shape: the most recent run per
-- organization/date, its terminal state, and whether a cancel was honored
-- (no run-agents written after cancellation). Replace :since.
SELECT r.id AS run_id, r.organization_id, r.target_date, r.status, r.backdate_status,
r.auto_proceed, r.created_at, r.updated_at,
(SELECT count(*) FROM public.qa_evaluation_backfill_run_agents ra
WHERE ra.qa_evaluation_backfill_run_id = r.id) AS run_agents,
(SELECT max(ra.created_at) FROM public.qa_evaluation_backfill_run_agents ra
WHERE ra.qa_evaluation_backfill_run_id = r.id) AS last_run_agent_at,
r.status = 'cancelled'
AND EXISTS (SELECT 1 FROM public.qa_evaluation_backfill_run_agents ra
WHERE ra.qa_evaluation_backfill_run_id = r.id
AND ra.created_at > r.updated_at) AS wrote_after_cancel,
r.status = 'previewing' AND now() - r.updated_at > interval '35 minutes' AS stuck_previewing,
r.notes
FROM public.qa_evaluation_backfill_runs r
WHERE r.created_at >= :since
ORDER BY r.organization_id, r.target_date, r.id;wrote_after_cancel = true or stuck_previewing = true fails the gate (the
preview wall-clock budget is 30 minutes).
-- Exactly-once terminal effects: no conversation with two active backfill
-- requests and no run-agent with more requests than its planned shortfall.
SELECT q.conversation_id, count(*) AS active_requests
FROM public.qa_evaluation_requests q
WHERE q.qa_evaluation_backfill_run_agent_id IS NOT NULL
AND q.status IN ('pending','ready','processing')
GROUP BY 1 HAVING count(*) > 1;
SELECT ra.id AS run_agent_id, ra.requested_count,
GREATEST(ra.target_count - ra.consumed_count_at_queue, 0) AS shortfall_at_plan
FROM public.qa_evaluation_backfill_run_agents ra
JOIN public.qa_evaluation_backfill_runs r ON r.id = ra.qa_evaluation_backfill_run_id
WHERE r.created_at >= :since
AND ra.requested_count > GREATEST(ra.target_count - ra.consumed_count_at_queue, 0);Both return zero rows when the gate passes.
Side-effect inventory
| Effect | Where | Preserved by |
|---|---|---|
Run status transitions (pending/previewing/preview_ready/queueing/queued/failed/cancelled) | qa_evaluation_backfill_runs.status | runs.ts guarded UPDATE … WHERE status = … |
Backdate transitions (none/running/completed/failed, backdated_at) | qa_evaluation_backfill_runs | backdate.ts; Rails enqueue_backdate_if_auto_proceed! still owns the none → running claim |
Run-agent plan rows, public ids, standby pool, standby_exhausted_at, requested_count | qa_evaluation_backfill_run_agents | preview.ts, replacement.ts, confirm.ts |
admin_forced requests, replaces_qa_evaluation_request_id, request public ids, stage_progress, consumed_sampling_rule_partitions | qa_evaluation_requests | runs.ts createBackfillRequest (effective_evaluation_at stays NULL as the Rails callback only fills it for ticket_closed) |
| QA orchestration per request | API2 qa-evaluation-request queue | enqueueQaEvaluationRequest |
| Terminal callbacks, notifications, slot release, conversation score/cache, workload links | Rails QAEvaluationRequest callbacks + existing API2 completion | Unchanged |
| Backdated evaluation/agent-evaluation/request/conversation timestamps | qa_evaluations, qa_agent_evaluations, qa_evaluation_requests, conversations.qa_evaluated_at | backdate.ts single transaction |
| Post-backfill daily report | daily-agent-evaluation-report queue | reconcile.ts enqueuePostBackfillReport |
| Structured logs | API2 qa-evaluation-backfill logger, Rails dispatcher | ids only, no PII |
Tests
rulebase-web/rulebase-api2/src/lib/qa-evaluation-backfill/execute.db.test.ts(PGlite): Rails-entry-shaped job data → the realexecuteQaEvaluationBackfillJobwith real SQL, PlanBuilder port, callbacks and routing; only the BullMQ/report transport edges are recorded. Fixtures (test-fixtures.ts) are anonymized, production-shaped tenants 19 (US) and 25 (EU) with prefixed ids, fixed-per-period rules, employees, completed AI evaluations, in-flight requests, scorecard/ticket-scope conditions; scenarios cover no-gap, skip → replacement, completed-net-zero replacement, standby exhaustion, active-request collision, partial retry, duplicate and concurrent delivery, cancellation before and during planning (PlanBuilder checkpoint), wall-clock budget exhaustion, retry of a failed preview, backdate, reconcile/report, an organization whose old gate was false (run 1156 shape), and the run-171-shaped end-to-end lifecycle across all five stages.routes/internal-qa-evaluation-backfills.test.ts.- Rails:
spec/lint/qa_evaluation_backfill_normal_path_spec.rb(source-level gate: no directQAEvaluationBackfill::*Jobcall, no owner-flag reference inapp/ lib/ config/ db/data/; all five classes include the tombstone; aperform_laterraises),spec/services/qa_evaluation_backfill/api2_dispatcher_spec.rb(API2 for every organization regardless of PostHog, delivery errors raise, no GoodJob rows, explicitperform_nowrecovery still works),spec/requests/admin/qa_evaluation_backfills_spec.rb(manual admin create/confirm/backdate/cancel handoffs, double-submit exactly once, old gate false),spec/jobs/qa_evaluation_backfill/preview_job_spec.rb(cancellation observed at the next checkpoint, wall-clock budget), model, request-callback, daily-report and data-migration specs asserting the API2 handoff instead of GoodJob.
Known gaps
- The Rails
PlanBuilderremains the reference; the API2 port is verified by the PGlite suite, not by a live dual-run diff. Run the canary’s first automatic run with bothPlanBuilder.new(...).call(read-only, Rails console) and the persisted API2 run-agents before expanding. - The cancellation checkpoint observes cancels at agent/batch boundaries; a single 5-minute statement that is already running finishes (or times out) before the checkpoint fires.
- A Rails
DeliveryError(API2 unreachable) fails the admin request or the model callback; the caller retries. There is intentionally no queueing fallback on Rails. - Not covered here:
TranslateConversationJoband the remaining QA lifecycle packages.
State
| Item | State |
|---|---|
| Code | First package merged (gated). Correction prepared on chidi/eng-3036-finalize-api2-backfill-owner (ENG-3036 reopened) |
| Review | Pending — Ronald requested |
| Merge / deploy | Not merged, not deployed |
| Flag | Removed from code; never created in production; nothing to flip |
| Rollout | Permanent API2 ownership for every organization on deploy; strict CloudWatch verification pending |
| Production data / queues | Untouched |