Rails runtime retirement archive
Completed October 3, 2026. Rails source and deployment infrastructure are gone. API2 no longer has Rails HTTP clients, proxy fallbacks, effect adapters, workload-owner switches, or rollback environment variables. Everything below this notice is historical rollout evidence; do not follow its rollback or remaining-work instructions in a current environment.
Rails source removal — 2026-10-02
ENG-2681 / PR #13640 removed the Rails source tree, its CI/deploy entrypoints, and local boot path. The public-schema SQL snapshot moved to API2 for fresh local/CI database bootstrap. A local-only native session seed replaced the Rails-backed Playwright setup while retaining the end-to-end CI gate. The PR merged at 2026-10-02T15:55:15Z as 1c32bf66a8b1ca3beaf88c0803ced752d201c2e9; final-head aggregate CI, Playwright smoke, edge routing, and CodeRabbit passed. GitHub shows no submitted Ronald review despite a pending request. Record this as a review exception, not approval. Release PR #13658 merged to release-prod at 18:00:51Z; its UI Workers Builds checks failed, so its overall deployment must not be called green.
At the 2026-10-02T08:29Z checkpoint below, the gateway had no event of any outcome in either region over 24 hours; seven days still included 441 historical unsuccessful forwards and no successful forwards. Historical Rails controller log groups are unavailable, so this is not a fresh controller seven-day census. The full post-cutover seven-day zero window, native QA/effect outcomes and errors, and live development callback/domain ownership are still open. The source removal and already-destroyed service resources do not prove complete irreversible retirement. Keep the remaining API2 Rails adapters until their own gates pass.
2026-10-02T20:30:00Z follow-up (24 h starts October 1 20:30Z; seven days starts September 25 20:30Z). AWS account 879381255132 reports all three Rails ECS clusters INACTIVE with zero services/tasks: US fc-rulebase-api-prod-xv109mm, EU fc-rulebase-api-prod-eu-u56f166n, development fc-rulebase-api-dev-xs8w05rj. US/EU API2 web services are PRIMARY/COMPLETED at 1/1/0 on task definitions 1931/1948; both report running SHA 52194cf674eb586f5e1dc3ea0f5836827d6f9a68, which contains #13640. Canonical US/EU and api2-dev /v1/identity return 401; the legacy api-dev Slack callback returns 530. Live Slack app redirect and development domain consumers remain unverified.
API2 web logs reach 20:29:59.331Z US and 20:30:00.571Z EU (freshness queries 9b5b767c-a281-4b83-89c8-bb7d305803fe, a531dd9f-c4eb-4ba5-93be-7a3edc1ac462). legacy_rails_gateway has zero events of any outcome in either exact 24-hour window (US 65051f24-1d7d-4ded-b467-29e71157c909, EU 7aa6a7c6-9375-45b6-b9af-e930451a185e). The exact seven-day forwarded method/path/status queries have zero successful Rails forwards and 437 historical unsuccessful forwards: US 214 (212 status 503, 2 status 404), EU 223 (208 status 503, 15 status 404); query IDs US 0e300b2d-e4be-4413-9c04-d3ef11bf9a6c, EU d7735628-261e-4f56-9dc5-2a2ce1981809. /admin*, /clients/MyCRL, and /rails/actions are scanner-shaped; the two historical US GET /agent/agent_performance_reports 503s remain unattributed. API2-local rejected/blocked/retired 404s and normalized 308s are separate. Historical Rails controller log groups are unavailable, so no fresh controller method/path census or full post-cutover seven-day zero is credited. Natural native QA/effect outcome parity and development callback/domain ownership also remain open. Do not delete the remaining Rails adapters or claim complete retirement.
Source audit at 2026-10-03T02:31Z. The deployed Rails clusters are gone, but API2 still has reachable Rails error paths: QA sampling recovery dispatches its Rails branch when ownership resolves to rails; evaluation-check execution and terminal failure marking retain Rails rollback branches; expired-evaluation cleanup calls Rails when its native sweep defers rows; call-recording attachment defaults to Rails when its native owner flag is off or unavailable; and provider-sync effects call Rails if its native effect keys are incomplete. These are conditional code paths, not measured live calls. Before removing them, establish native outcomes and error parity, inspect the actual flag/worker configuration, and complete the post-cutover observation window. The source audit does not fill the missing controller logs or certify a zero-volume window.
2026-10-02T14:30:00Z follow-up (24 h starts October 1 14:30Z; seven days starts September 25 14:30Z). AWS account 879381255132 still reports the US, EU, and development Rails ECS clusters INACTIVE, with zero active services and running tasks. API2 web logs are fresh through 14:30:00.999Z US and 14:29:59.792Z EU (CloudWatch freshness queries 53d60b7b-f7e4-4ab6-8e4a-16ac2eec9842, 57d28d73-a1a8-4a0a-af08-b539322a85e1). API2 legacy_rails_gateway has zero events of any outcome in either 24-hour region (US query 00cc977a-7fcc-4fb2-999c-7fbfc246f692; EU 46917520-f2c5-4840-a98b-dedb32bfc69b). The seven-day window has zero successful Rails forwards and 441 historical unsuccessful forwards: 216 US and 225 EU (method/path/status queries 662f4c3b-d914-42a3-90dc-be839bd9f068, e1f1a456-a85c-4c89-9fd6-78a5abc85fb4). Most target /admin*; /clients/MyCRL and /rails/actions are scanner-shaped, while two historical US GET /agent/agent_performance_reports 503s still lack caller attribution. API2-local rejected/blocked/retired 404s and normalized 308s are separate from forwarded traffic. Historical Rails controller log groups are unavailable, so this checkpoint cannot prove a fresh controller method/path census. PR #13640 passed aggregate CI, Playwright smoke, edge routing, and CodeRabbit on its prior head, but Ronald has not submitted a review; this plan update requires new-head checks before any merge. The full post-cutover seven-day zero window, native QA/effect outcome parity, and live development callback/domain ownership remain open.
Shutdown checkpoint — 2026-10-01
ENG-3587 / PR #13576 removed the three Rails web service definitions from flightcontrol.cue (development, US production, and EU production). It also set LEGACY_RAILS_FALLBACK_ENABLED to false in both production API2 environments; development was already false. The CUE export comparison showed no other service changes. Final-head Flightcontrol, internal-docs, migration-freeze, aggregate CI, and CodeRabbit checks passed. The PR merged at 2026-10-01T02:04:33Z as fc58f3e48c2effbaf94985dd41a8c2923ed45ad7 and is included in release-prod. GitHub still reports REVIEW_REQUIRED with no submitted Ronald approval; record this as a review exception, not retroactive approval.
The exact production census ending 2026-10-01T02:29:01Z found zero Rails /internal/* and /conversation_images/* controller calls in each region over 24 hours. Seven days still contain 79 historical internal calls (74 US, 5 EU), all before the production scale-to-zero cutover, and zero image calls. Separate historical GET /up heartbeats in the seven-day window ending 02:29:55Z total 141,393 US and 141,652 EU; they are not live dependencies. API2’s gateway recorded zero successful Rails forwards in either window. Its 24-hour forwarded 503s total 377 (US 190, EU 187), dominated by /admin* scanner paths with two each of /clients/MyCRL and /rails/actions per region. Seven-day unsuccessful forwards total 464 (US 227, EU 237), including historical 404s. Rejected/blocked 404s and normalized 308s are separate from forwards. The full seven-day post-cutover zero window and native QA/effect outcome parity remain open.
At 2026-10-01T02:09Z, fresh ECS reads showed US and EU production Rails desired/running/pending 0/0/0, with staging at 1/1/0. Staging’s ECS autoscaling target was changed from min/max 1/1 to 0/0, then its desired count was set to zero. A 02:27Z three-region read confirmed all Rails web services ACTIVE with desired/running/pending 0/0/0, completed rollouts, and autoscaling min/max 0/0. The Flightcontrol service records and AWS resources remain in place for rollback; Flightcontrol’s development config page recognizes the Rails service is no longer in flightcontrol.cue. Canonical US/EU and development API2 /v1/identity probes returned the expected unauthenticated 401 after the staging scale change. The repository’s Slack manifest still references https://api-dev.rulebase.co/integrations/auth/slack/callback; at 02:27Z that URL returned 503, while development API2 /v1/identity returned 401. API2 owns /connections/oauth/slack/callback, a different path. Resolve the development callback and domain ownership before deleting the retained service or its domain.
Recheck native outcomes, errors, and the exact regional 24-hour and seven-day windows before widening any retirement claim. Preserve a reversible rollback path until those gates pass. Destroying a Flightcontrol service would delete its underlying AWS resources and is a separate step from this operational scale-to-zero shutdown.
08:26Z follow-up. PR #13578 , which recorded the scale-to-zero evidence above, merged at 2026-10-01T07:32:53Z as da2fae8566f0184b3399b5708cc0bafb6bdb3352. It had passing required checks but no submitted Ronald review; this is another review exception, not approval. release-prod cdc192ba15c7da9c5042344b2f509b184fc2ce4a contains both #13576 and #13578. A branch head is not proof of the running Flightcontrol or ECS revision.
At approximately 08:26Z, unauthenticated HTTP probes returned 503 from api-dev.rulebase.co on both /v1/identity and /integrations/auth/slack/callback, while api2-dev.rulebase.co/v1/identity returned the expected 401 and /connections/oauth/slack/callback returned 400 without OAuth parameters. The checked-in Slack manifest lists the dead Rails development callback and does not list the API2 development callback; the live Slack app configuration has not been verified. The current flightcontrol.cue assigns api2-dev.rulebase.co to the development API2 service and also configures api-dev.rulebase.co in API2’s API2_CORS_ORIGIN, RULEBASE_API_BASE_URL, and RAILS_INTERNAL_BASE_URL for both web and worker. API2’s native image-caption parser uses RULEBASE_API_BASE_URL to recognize historical Rails image URLs; native provider sync mints new image URLs on RULEBASE_API2_BASE_URL. The development UI’s live API base URL and runtime consumers of the old api-dev origin remain unverified. Migrate or remove the Slack redirect in the live app, verify the development UI and other api-dev consumers, and settle domain ownership before destroying the retained staging service.
AWS SSO token refresh failed on this follow-up, so ECS state, Flightcontrol deployment state, CloudWatch volumes, and native outcome/error parity could not be refreshed. Retain the 02:27Z ECS verification and 02:29Z census above as the latest exact evidence rather than extending their windows by inference.
14:33:20Z follow-up (fixed cutoff 2026-10-01; 24 h begins September 30 14:33:20Z, seven days begins September 24 14:33:20Z). PR #13585 merged externally at 08:39:35Z with passing required checks but no submitted Ronald approval, a third recorded review exception. CodeRabbit correctly identified the api-dev API2 configuration references corrected above. AWS STS again succeeded in account 879381255132. ECS shows all three Rails services ACTIVE and stable at desired/running/pending 0/0/0: US fc-rulebase-api-prod-xv109mm, EU fc-rulebase-api-prod-eu-u56f166n, and development fc-rulebase-api-dev-xs8w05rj. No service or domain was destroyed. The US and EU API2 web services are stable at 1/1 on task definitions 1915 and 1932; both task definitions report FC_GIT_COMMIT_SHA=27ace5eb96e283b9262b1636b042cb31e1fcdf03, which contains #13576 and #13578. The newer release-prod branch head 8dbf2ff1e0626578e1f7bdee6d06cbe03d9a2700 is not yet the verified running web revision. Canonical US/EU and development API2 /v1/identity returned 401; api-dev.rulebase.co/v1/identity still returned 503.
CloudWatch Rails process_action.action_controller method/path/status queries found zero /internal/* and /conversation_images/* calls in each region over 24 h (US query 064322b4-8a9d-4e5c-b4aa-df5923b323ca; EU 73cad777-8d35-4c4d-a06a-1930797057f0). Seven days contain 72 historical US internal calls, zero EU internal calls, and zero image calls in either region (US 7b4c818a-8005-4944-bfd1-33c0a1b56554; EU abce4e35-4763-4f31-9874-dcf8996f2284). The US 72 are POST evaluation-check run 200 ×45, transcription context GET 200 ×4 and write POST 200 ×4, reconciliation POST 200 ×5, QA sampling recovery POST 200 ×3, report email POST 200 ×3 and preview POST 200 ×2, evaluation results POST 200 ×2, Slack notification POST 201 ×2, Unit21 sync POST 200 ×1, and direct-upload process POST 500 ×1. Latest US internal call was POST /internal/slack_notifications 201 at September 26 13:00:24Z (query 63b9b8e1-0de5-44c4-8bac-31e8b7b79dc8). Separate seven-day empty GET /up 200 heartbeats were 115,369 US and 115,628 EU (queries a289166b-7d69-4944-b3de-44956c681a08, 9f627723-52d8-47cb-8fe0-520e92d4c103).
API2 legacy_rails_gateway method/path/status queries show zero successful Rails forwards in either window. In 24 h, US had no forwarded events; EU had one scanner-shaped POST /admin 503 (path queries US 9f79181b-249e-44eb-b314-185bb19417a9, EU 97f1643c-d8cb-4265-a52e-856f5c02abdc). The other gateway events were API2-local rejected/blocked/retired 404s: US 654 total (647 rejected, 3 blocked, 4 retired), EU 1,059 total (1,056 rejected, 2 retired, 1 forwarded). Seven days include 218 US unsuccessful forwards (212 503, 6 404) and 235 EU (211 503, 24 404), dominated by admin/old-auth scanner probes, with an unresolved historical US GET /agent/agent_performance_reports 503 ×2; there are no successful forwards (summary queries US 414d4703-bbe2-4d11-908e-335b424f730d, EU 4ca97b2f-d62f-4d9b-ab2f-6b3edc95b089; path queries US 65fe274b-dcb5-4c01-856b-c2cb83718866, EU 28946e59-0302-4b57-a5dd-fa065100c326). Rejected/blocked 404s and normalized 308s are separate. The complete post-cutover seven-day zero window, development callback/domain ownership, and native QA/effect outcome parity remain open; retain the reversible zero-replica services.
20:30:00Z follow-up (fixed cutoff 2026-10-01; 24 h begins September 30 20:30:00Z, seven days begins September 24 20:30:00Z). AWS STS succeeded in account 879381255132. ECS again shows all three Rails services ACTIVE, PRIMARY/COMPLETED, desired/running/pending 0/0/0 (US fc-rulebase-api-prod-xv109mm, EU fc-rulebase-api-prod-eu-u56f166n, development fc-rulebase-api-dev-xs8w05rj). No Flightcontrol service, domain, or AWS resource was destroyed. Canonical US/EU and development API2 /v1/identity returned 401; the old development Slack callback at api-dev.rulebase.co/integrations/auth/slack/callback returned 503. The live Slack app redirect and development UI/API base URL remain unverified.
Exact Rails controller method/path/status queries found zero /internal/* and /conversation_images/* calls in either region over 24 h (US dbcc8c56-ce3e-41ce-88b5-c13607225ab3, EU 239d7611-7736-49b3-883e-1895d9c92cd1). Seven days contain 67 historical US internal calls, zero EU internal and zero image calls in either region (US 08dfece1-bbdb-4cd7-a3fb-54752b916880, EU b4e3e832-79a1-4a7a-b119-5722536875e3). US method/status totals are POST 200 ×60, GET 200 ×4, POST 201 ×2, POST 500 ×1; these are pre-scale residue, including evaluation-check runs, transcription, reports, Slack, Unit21, and one failed direct-upload process. Separate seven-day GET /up 200 heartbeats are 102,500 US and 102,757 EU (US 79308d62-c7ff-414c-b2d7-b14f841936ce, EU fdb0343c-493d-4863-a392-ddf550b31c9a); they are historical empty health checks, not live application dependencies.
The API2 gateway has zero successful Rails forwards in either window. The 24 h contains zero forwarded events in both regions; all 314 US events and 227 EU events were local 404 responses (US rejected 311, retired 2, blocked 1; EU rejected 227). Seven days contain 216 US unsuccessful forwards (212 503, 4 404) and 225 EU (211 503, 14 404), dominated by historical /admin* scanner probes (exact summary queries US 297d7d92-cc75-4a47-a15a-b4ab7d6caa33, EU 02d5b8dc-9676-456e-8d7a-00b1743807ae; path queries US b6d1fb47-b2c6-40d2-bf0d-4ee3840dd476, EU bc877208-09d6-468d-92c7-7f2932ed9152). The two historical US GET /agent/agent_performance_reports 503s still need caller attribution; no successful live dependency was observed. Rejected/blocked/retired 404s and normalized 308s are not successful Rails forwards. PR #13590 has green required checks but no submitted Ronald review at this checkpoint and remains open. The full seven-day post-cutover zero window, native outcome/error parity, and development callback/domain ownership still block irreversible retirement; keep the zero-replica services available for rollback.
2026-10-02T02:30:00Z follow-up (24 h starts October 1 02:30Z; seven days starts September 25 02:30Z). PR #13590 merged externally at October 1 20:49:07Z as 1f94deae615c7f4c643c4bd8e9d66dddee22f4a4 with green checks and no submitted Ronald approval; this is a review exception, not approval. Flightcontrol subsequently destroyed the three Rails ECS clusters through its CloudFormation role at October 2 01:45:37Z (US), 01:45:54Z (EU), and 01:45:52Z (development). ECS now reports all three clusters INACTIVE with zero services, superseding the reversible zero-replica posture above. This resource deletion was not performed by this automation. The US/EU canonical API hosts and api2-dev.rulebase.co/v1/identity still return 401; api-dev.rulebase.co/v1/identity returns 530. The checked-in development Slack callback and API2 api-dev URL configuration described above have not been reconciled with the live Slack app, development UI, or domain owner. Treat that dev-host failure as an unresolved operational risk.
The API2 web services remain ACTIVE, PRIMARY/COMPLETED at desired/running/pending 1/1/0: US task definition 1922 and EU 1939 both report FC_GIT_COMMIT_SHA=2ec23e3294dbad401ee1cb092a32b92e4c34ac4e; development task definition 2335 reports be46e98d6c3457259dec0394d03118036afb663a. These are running-task-definition attestations, distinct from branch heads.
API2 gateway logs are fresh through approximately 02:31Z. Fixed-cutoff CloudWatch queries found zero legacy_rails_gateway events of any outcome in either region over 24 h (US 21b07d1f-e484-4d41-9254-9b85bdace5b1, EU b7c8bf5e-1ce2-4595-a366-125ab02de3ca). Seven days contain zero successful Rails forwards; 216 US and 225 EU historical forwards were all unsuccessful (US GET 503 ×155, POST 503 ×57, GET 404 ×4; EU GET 503 ×176, POST 503 ×35, GET 404 ×12, POST 404 ×2). The seven-day forwarded paths are dominated by /admin* probes, with /clients/MyCRL and /rails/actions also present (summary queries US a561e77c-d002-455f-b831-d42eaf5408d2, EU fa71f9d0-31de-4f85-bb93-aba8eca63d7f; path queries US fa290f1d-524f-4518-bb3f-f70307400abb, EU 57b154af-5a18-461d-9f09-c96a20452f9f). Rejected, blocked, and retired API2-local 404s and normalized 308s are separate from forwards. The historical Rails controller log groups are no longer listed in CloudWatch; the latest exact controller census remains the October 1 20:30Z observation above. Do not infer a new controller seven-day zero window from the missing log groups. The checked-in Slack manifest now names the existing API2 development callback instead of the dead Rails callback, but this does not establish or change the live Slack app configuration. Native QA/effect outcome and error parity, the full post-cutover seven-day window, and dev-host ownership remain open; do not claim complete irreversible retirement.
2026-10-02T08:29:00Z follow-up (24 h starts October 1 08:29Z; seven days starts September 25 08:29Z). PR #13624 , which recorded Flightcontrol deletion and corrected the checked-in development Slack callback, merged externally at 02:47:46Z as 3bf7c1c6eee199601ad8f363528f7a7889a513bd. Its final-head CI workflow succeeded, but GitHub lists zero submitted Ronald reviews; this is another review exception, not approval. Fresh AWS ECS reads show the US, EU, and development Rails clusters remain INACTIVE with zero services. Canonical US/EU and api2-dev /v1/identity return 401; api-dev.rulebase.co/v1/identity returns 530, while API2’s development Slack callback returns 400 without OAuth parameters. The live Slack app redirect and development host consumers remain unverified.
The US/EU API2 web log groups contain events through 08:29Z. Fixed-cutoff legacy_rails_gateway queries show zero events of any outcome in either region over 24 h (US abece527-24a6-48ad-955f-4fa9e7183ab1, EU 221b2829-5346-427b-972c-c60c7e897fc0). Seven days still contain zero successful forwards and the same 441 historical unsuccessful forwards: US 216 (GET 503 ×155, POST 503 ×57, GET 404 ×4) and EU 225 (GET 503 ×176, POST 503 ×35, GET 404 ×12, POST 404 ×2); summary queries US c72acf41-d410-436e-a639-35406c4c110d, EU bc653688-89bb-4265-847f-91717afc9cac. API2-local rejected, blocked, and retired 404s and normalized 308s are separate. This is gateway evidence, not a fresh Rails controller method/path census: those historical controller log groups are still unavailable. Native QA/effect outcomes and errors, live development callback/domain ownership, and a complete seven-day post-cutover zero window remain open.
The public API belongs to API2. The remaining migration goal is narrower: nobody should use the Rails application runtime.
This plan is the source of truth for that work. It organizes the remaining dependencies by who calls Rails, measures them from production traffic, and turns the largest coherent families into migration packages.
Current boundary
The migration has crossed the public ownership boundary:
api.rulebase.coandeu.api.rulebase.coterminate on API2.- API2 owns the documented
/v1public contract and its OpenAPI document. - Deprecated public paths are API2-owned compatibility aliases with successor links and sunset metadata.
- Rails sits behind regional
legacy-rails.*origins as an implementation dependency. Public documentation and API responses do not expose those origins.
This makes Rails an internal legacy service from a product and API-contract perspective. It does not make Rails unused. API2 still forwards some public requests to Rails and calls Rails controllers for behavior that API2 does not yet own.
The retirement program therefore has two active workstreams:
- Stop forwarding public calls to Rails.
- Stop calling Rails for effects.
Residual GoodJob, Active Storage, and Rails operational work are shutdown gates for those workstreams. They are not a separate queue of migration work.
What recently changed
| Change | Result | Retirement consequence |
|---|---|---|
| #12326 made API2 the canonical HTTP gateway | Native API2 routes win; unmatched HTTP and /cable can reach Rails through a controlled compatibility layer | Every remaining public Rails dependency is observable at one API2 boundary |
| #12339 recorded the regional cutover | The gateway and legacy origins are the deployed topology in both regions | The old public-host migration is no longer the organizing goal |
#12344 and #12348 unified public routes and documentation on canonical /v1 | API2’s OpenAPI document is the sole public reference; legacy public paths are explicit API2 compatibility aliases | Public ownership is complete after the release is live, even where an API2 handler still proxies implementation to Rails |
| #12328 moved GoodJob into Rails web processes | Dedicated Rails worker services are gone | Rails web cannot stop until residual jobs and their observability are resolved |
| #12305 , #12306 , and #12275 removed major Zendesk, QA-backfill, and transcription ownership from Rails | Normal application work continues to move to API2/BullMQ | GoodJob is now a final shutdown gate rather than the best way to prioritize the migration |
#13143 Rails-web-dark cutover merged (c3cc5473d, 2026-09-26) | Transcription, QA result import, daily reports and Slack notifications have no Rails destination; source gate blocks the retired routes | Explicit API2→Rails effects are closed in code; the published 24 h window ending 2026-09-26T16:18:57Z (six US calls) is pre-cutover and a fresh post-deploy window is still required |
#13164 / #13167 set both production Rails services to minInstances: 0 / maxInstances: 0 and made flightcontrol.cue a production watch path | Rails is retained only as a zero-replica rollback target (restore 3…10 to bring it back) | Every remaining API2→Rails fallback can only fail on the configured topology; reconciliation of the hold in Flightcontrol is unverified from this session |
ENG-3488 removes the conversation-upload Rails proxy fallback and its PostHog gate (#13196 merged 2026-09-27T09:56Z as df9b31c2ab, no submitted Ronald review — recorded exception) | API2 answers 401/503 itself on the five upload contracts; proxy.ts deleted, source-gated | The last public contract with a recorded successful Rails forward has no Rails destination in code; post-deploy window still required |
ENG-3495 removes the last three route-level Rails fallbacks (public media, Zendesk sidebar, Outreach webhook); #13221 merged 2026-09-28T15:42:33Z as 9486119ea087daebbe399efeeeee3548831086f5 with no submitted Ronald review | Only the wildcard gateway in app.ts can reach Rails from API2; paused/misconfigured routes end with 404/503/500 | No explicit API2→Rails destination remains in code; post-deploy native outcomes still require proof |
The canonical public API changes entered release-prod in
#12352 , and the follow-up
release #12358 deployed as
7dd7eef. On September 17, 2026, both regional API2 services were steady on
their current task definitions, canonical /v1/identity returned the native
API2 authorization response in both regions, and the canonical OpenAPI document
and documentation redirect were live. The public-boundary deployment gate is
therefore complete.
The same day, attachment purge #12353 ,
image-caption context/writeback #12355 ,
and generic tool telemetry plus the first two native tools
#12356 reached both regions
in release-prod revision 9311bcca. Both API2 and Rails services completed
their rollouts by approximately 2026-09-17T13:04:40Z. The first nine-minute
post-deploy slice contained zero Rails calls for those three families. This is
an initial smoke signal, not the complete production window required to close
their retirement gates.
QA sampling recovery #12369
merged into #12373 at
2026-09-17T13:26Z. Keep qa_sampling_recovery_native off: the merged native
transcription claim still uses the plain organization counter instead of Rails’
member-aware counter and :members set. A same-hour rollback or redelivery can
therefore charge one conversation twice. A follow-up correction and focused
counter-compatibility proof are required before any production canary. That
correction shipped in #12423
and is live on cb829218; the September 18 rollout below supersedes the
“keep off” guidance.
Native public media #12374
reached both regions through #12397 .
The regional API2 deployments completed at approximately 2026-09-17T16:40Z.
In the first post-deploy slice, API2 served six US conversation-image requests
successfully, and neither region forwarded a conversation-image or Active
Storage request to Rails. This is enough to confirm that the route is active;
the comparable outcome distribution and complete zero-forwarding window remain
the retirement gate.
The production census ending around 2026-09-17T17:11Z extends structured
gateway coverage to approximately 13 hours. It contains 15,535 events: 8,335
scanner, unknown-path, or Rails 404 events and 7,200 meaningful events. The
meaningful tail is now led by Active Storage and conversation images (3,599),
Action Cable (1,917), conversation uploads and preflights (850), Unit21 (332),
Zendesk (422), Slack (2), and 78 other events. This remains a partial window and
must not be extrapolated into a daily rate.
Three clean-current-main implementation packages are active from this census:
ENG-3217
corrects the shared member-aware QA throttle while the production gate remains
off; ENG-3218
moves public conversation-upload ingress and status ownership into API2; and
ENG-3219
starts the Cable retirement with native SSE for ConversationChannel. None is
merged or deployed.
Evidence as of 2026-09-18 (trailing windows ending approximately 10:16Z)
Historical: superseded by the 13:12:11Z section below where the two disagree.
Deployment state: the evidence windows below were measured while both regions
ran release-prod cb829218 on every API2 process (US web 2/2 and worker 5/5;
EU web 1/1 and worker 5/5). At 10:17:31Z release-prod advanced through
#12477 to
1d29cccdf5c02a8c3ede5498b045007f43f78e75, which contains release d2188798
(call-recording attachment
#12455 , conversation
transcription state #12456 ,
native transcribe_calls
#12463 ) plus
upload-driven sync completion
#12469 and native QA
related-conversation sync
#12470 (both merged
10:17:19Z). That deployment is in progress: at 10:19:26Z US web/worker
still ran cb829218 while EU web/worker had advanced to the intermediate
release 9d657672131dde460b927cb6be867bf6304632f8, all steady. Attribute no
traffic change to any of those packages until the running task SHAs show
1d29cccd in both regions and the rollout has completed.
The API2 worker services are the Flightcontrol logical services
rulebase-api2-worker-prod and rulebase-api2-worker-prod-eu; their physical
ECS names contain -pr- and must not be read as missing production workers.
Effects boundary, fixed trailing 24 hours: 47,230 API2-to-Rails calls.
6,962 are pre-cutover residue from already-shipped families (attachment purge
3,000; image captions 1,279; generic tools 2,683), every one last seen before
2026-09-17T12:54Z. The remaining 40,268 meaningful live calls are:
| Family | 24-hour calls |
|---|---|
| QA sampling recovery | 26,620 |
| Transcription reconciliation | 5,076 |
QA stage execution (execute_stage) | 2,792 |
| Call-recording attachment | 2,745 |
| Conversation upload processing | 963 |
| Provider-sync completion | 941 |
| Transcription context/write | 759 |
| Unit21 sync | 144 |
| Counter cache | 129 |
| Expiry | 48 |
| Field values | 24 |
| Imports | 21 |
| Reports | 5 |
| Bulk create | 1 |
Effects boundary, fixed trailing seven days: 417,996 calls, of which 199,570 are shipped-family pre-cutover residue (purge 115,637; captions 37,745; tools 46,188) and 218,426 remain.
Gateway boundary, fixed trailing 24 hours: 18,934 events: 527 blocked at
API2, 12,817 forwarded and answered 404 by Rails (scanner/unknown paths), and
5,589 meaningful as an upper bound. The current clear live tail is 2,583 =
Action Cable 1,415 + conversation uploads 944 + Outreach 224.
The seven-day gateway query still has only about 30 hours of structured
coverage: 24,205 events, 644 blocked, 14,559 Rails 404, 9,000 meaningful;
clear live tail 3,926 = Cable 2,486 + uploads 1,216 + Outreach 224. Do not
present this as complete seven-day coverage.
QA sampling recovery rollout: qa_sampling_recovery_native was created at
09:36Z as a bounded kuda-pilot canary, expanded at 09:53Z to
kuda-pilot, rho-tnkw, qonto, at 10:12Z to easypay and nala, and at
10:16Z advanced to those five exact 100% canaries plus a second 25%
organization-level rollout group. Every completed natural cycle (Kuda 09:45
and 10:01, Rho 10:00, Qonto 10:10, Easypay 10:15, NALA 10:15) ran natively
with zero candidate or transcription failures; the per-cycle counters are in
the rollout runbook .
PostHog last_called_at stayed at 09:45Z throughout and is advisory only;
the structured worker result is the source of truth.
QA sampling recovery rollback (10:36Z): the first 25% tick at 10:30Z
(US 11 native organizations, EU 5; completed results seen so far 1,204
candidates, 10 requested, zero candidate/transcription failures) did not
pass. At 10:32:58Z EU BullMQ emitted a terminal qa-sampling-recovery
failure for Qonto (organization 33), job qa_sampling_recovery_33_1988585
(the 10:15 bucket): job stalled more than allowable limit, attemptsMade 1
of maxAttempts 5, terminalFailure: true, correlating with the EU worker
replacements during the 1d29cccd rollout. At 10:36:16Z the coordinator
rolled PostHog flag version 5 back to four exact 100% canaries only
(kuda-pilot, rho-tnkw, easypay, nala); Qonto and the 25% cohort are
back on Rails. Re-enabling Qonto or any percentage rollout is gated on a
durability review of the stalled bucket (partial persisted admissions, audit
entries, requests, and Redis throttle consumption) and natural
scheduler-cycle resumption evidence after the hardening change is deployed;
the automation is not authorized to replay customer jobs, so no manual replay
of the stalled bucket is part of the gate; see the incident section of the
runbook.
Evidence as of 2026-09-18T13:12:11Z (epoch 1789737131)
Historical: superseded by the 14:06:18Z section below where the two disagree.
This section supersedes the 10:16Z windows above wherever the two disagree.
Deployment and QA-cycle facts were verified at 13:04:35Z; the effects and
gateway windows below were re-measured with cutoff 13:12:11Z.
Deployment state: release-prod is
b96da600faff9d1a29051252b8a4c08250047675 (merged 12:34:22Z; Flightcontrol
succeeded 12:40:56Z; new task definitions and images registered 12:37Z to
12:38Z). Running tasks: US web fc-rulebase-api2-prod-5whj12su task
definition 1634, 1/1; EU web fc-rulebase-api2-prod-eu-9rfp06k task
definition 1652, 2/2; US worker fc-rulebase-api2-worker-pr-zy7q0 task
definition 1624, 5/5; EU worker fc-rulebase-api2-worker-pr-m9780 task
definition 1641, 5/5. The worker names contain -pr- because of
Flightcontrol’s physical naming; they are the production logical services
rulebase-api2-worker-prod and rulebase-api2-worker-prod-eu, not preview
workers. Both web regions acquired the migration advisory lock and completed
startup with no migration failure and no package-specific first-slice error.
Pre-existing provider/Intercom warnings continued, deployment churn briefly
produced Redis connection/stream errors, and one still-Rails-owned
conversation-upload process (conversation-upload-process-648660) returned
504 at 13:02Z.
QA sampling recovery: qa_sampling_recovery_native remains exactly
kuda-pilot, rho-tnkw, easypay, nala after the 10:36Z rollback; Qonto
and percentage targeting remain off. Every four-canary cycle completed after
the b96da600 deployment was clean with zero candidate or transcription
failures: US 12:45 Rho 13 candidates / 9 requested / 4 skipped and Kuda
1,903 / 0 / 100 skipped / 1,802 throttled / 1 ignored / 73 counters lowered;
US 13:00 Rho 4 / 1 / 3 and Kuda 1,994 / 53 / 99 skipped / 1,841 throttled /
1 ignored / 73 counters lowered; EU Easypay 155 / 1 / 30 skipped / 124
throttled / 15 counters lowered, then 161 / 2 / 35 / 124 / 15; NALA cycles had
zero candidates. Qonto is back on Rails and completed its 5,000-candidate
cycles there. Do not widen the flag before
#12482 (deployment-
interruption-safe sweeps, ENG-3249 )
is reviewed and deployed and the normal scheduler’s natural post-deploy cycles
show clean resumption; production proof comes from natural traffic only, with
no manual customer-job replay.
Other native flags (decision: no widening):
conversation_upload_intake_native is Kuda-only with no native traffic and
PostHog last_called_at null. conversation_transcription_state_native is
Kuda-only: 23 natural Kuda context loads in the inspected slice; the 10 after
b96da600 were all terminal already_transcribed, plus one completed
conversation; still no native persistence-write proof.
call_recording_attachment_native is Kuda-only with no native attachment
events, while Rails telemetry showed 14 US provider_sync and 189 EU
aircall_import attachment calls.
Effects boundary, fixed trailing 24 hours ending 13:12:11Z: 37,749
API2-to-Rails calls (US 12,884; EU 24,865). The already-cut-over attachment purge, generic tools,
and image-caption families are zero in this window.
| Family | 24-hour calls |
|---|---|
| QA sampling recovery | 25,128 |
| Transcription reconciliation | 4,991 |
| Call-recording attachment | 2,671 |
QA stage execution (execute_stage) | 2,172 |
| Direct upload processing | 1,038 |
| Provider-sync completion | 743 |
| Transcription context/write | 634 |
| Unit21 sync | 144 |
| Counter cache | 129 |
| Expired evaluations | 48 |
| Remaining tiny families | 51 |
Effects boundary, fixed trailing seven days ending 13:12:11Z: 412,091
calls (US 157,345; EU 254,746). Shipped-family pre-cutover residue is 195,626
(attachment purge 113,411; tools 45,438; image captions 36,777). Leading
remainder: QA sampling recovery 154,276; call recording 15,679; QA stage
13,533; transcription context/write 8,793; reconciliation 7,476; upload
processing 6,833; provider-sync completion 6,441.
Gateway boundary, fixed trailing 24 hours ending 13:12:11Z: 16,709
events (US 14,035; EU 2,674): 531 API2-blocked, 12,253 forwarded Rails
404/scanner, 3,925 non-404 as an upper bound. Meaningful live
dependencies: Action Cable 1,164 (US last 09:34Z; EU still live through
13:06Z), conversation uploads 1,016, Outreach 224. Older residue: Zendesk
727 (last 2026-09-17T23:45Z), Unit21 338 (last 2026-09-17T21:59Z), public
media 427 (last 2026-09-17T14:46Z), and 29 miscellaneous non-404 events.
Gateway coverage caveat: structured gateway telemetry is only about 33
hours old, so the nominal seven-day query is exact over the available coverage
and is not a full seven days: 25,809 events; 715 blocked; 15,682 Rails
404; 9,412 non-404. Known families: Cable 2,559; uploads 1,540; Zendesk
914; Unit21 529; Outreach 224; media 3,599; 47 other.
The current live public-route dependencies are still Action Cable, conversation uploads, and Outreach. #12474 owns Outreach ingress plus the explicit allowlist, #12483 owns native XCALLY/Outreach-Kaia direct upload processing, and #12482 owns QA sweep interruption safety; all three are green and await Ronald/human review.
Evidence as of 2026-09-18T14:06:18Z (residual windows) and 14:09:47Z (flag state)
Historical: superseded by the 15:05:47Z section below where the two disagree.
This section supersedes the 13:12:11Z windows above wherever the two disagree.
QA interruption safety deployed:
#12482
(ENG-3249 ) was human-merged and
is deployed in the current cmu70 API2 worker build: US worker task
definition 1628 and EU worker task definition 1645, both steady 5/5 (physical
service names still carry -pr-; they are the production logical workers).
Clean natural 14:00Z cycles on the hardened build (structured worker results; every native cycle had zero candidate and zero transcription failures): US org 48 0 candidates; US org 38 1 candidate / 1 requested / 0 skipped; US org 19 4 / 1 / 3; Kuda US org 25 2,406 candidates / 4 requested / 116 skipped / 2,286 throttled / 75 counters lowered; EU org 25 0 candidates; EU org 41 178 candidates / 0 requested / 51 skipped / 127 throttled / 15 counters lowered.
Flag state: at 14:09:47Z qa_sampling_recovery_native was expanded to
exact slugs kuda-pilot, rho-tnkw, easypay, nala, qonto only.
Percentage targeting stays off; there is no manual replay of the stalled
qa_sampling_recovery_33_1988585 bucket, and none is authorized. Rollback
remains removing qonto from the exact list.
Qonto’s first natural hardened cycle (clean): at 14:22:23.484Z job
qa_sampling_recovery_33_1988601 ran with owner=api2: 5,000 candidates, 9
requested, 130 skipped, 4,860 throttled, 1 ignored, 244 counters lowered,
loadMs 2,641, sweepMs 440,679, 0 candidate failures, 0 transcription
failures, stoppedEarly null. This is the natural scheduler evidence the
rollback gate required. The five exact slugs remain enabled; percentage
targeting stays off.
Effects boundary, fixed trailing 24 hours ending 14:06:18Z: 36,952
API2-to-Rails calls (US 12,198; EU 24,754), down 797 from the 13:12:11Z
window.
| Family | 24-hour calls |
|---|---|
| QA sampling recovery | 24,674 |
| Transcription reconciliation | 4,965 |
| Call-recording attachment | 2,627 |
QA stage execution (execute_stage) | 1,995 |
| Direct upload processing | 1,069 |
| Provider-sync completion | 636 |
| Transcription context/write | 614 |
Effects boundary, fixed trailing seven days ending 14:06:18Z: 409,658
calls, of which 193,829 is shipped-family pre-cutover residue (attachment
purge 112,671; tools 44,812; image-caption context/write 36,346).
Gateway boundary, fixed trailing 24 hours ending 14:06:18Z: 15,687
events: 530 API2-blocked, 11,537 forwarded Rails 404/scanner, 3,620
non-404 as an upper bound. Meaningful live dependencies: Action Cable
1,077, conversation uploads 1,049, Outreach 221.
Gateway coverage caveat: the nominal seven-day query covers only the
structured gateway telemetry available since 2026-09-17 and is not a full
seven days: 26,004 events; 734 blocked; 15,690 Rails 404; 9,580 non-404.
Evidence as of 2026-09-18T15:05:47Z (effects census) and 15:09:20Z (flag state)
Historical: superseded by the 16:06:07Z section below where the two disagree.
This section supersedes the 14:06:18Z windows above wherever the two disagree. Gateway windows were not re-measured; the 14:06:18Z gateway figures stand.
Deployment state: release-prod 677ba560 is steady with zero pending
and all deployments complete. US web task definition 1638 (2/2), EU web 1656
(1/1); the logical production workers (physical -pr- names) run task
definitions 1628 (US) and 1645 (EU), both 5/5. All four task definitions run
commit 677ba560.
Repeated natural exact-canary cycles after the first Qonto proof stayed clean (candidates / requested / skipped / throttled / ignored / counters lowered; zero candidate and transcription failures in every cycle):
| Organization | Cycle (UTC) | Result |
|---|---|---|
| Qonto (org 33) | 14:37:45Z | 5,000 / 8 / 128 / 4,863 / 1 / 245 |
| Qonto (org 33) | 14:51:46Z | 5,000 / 11 / 127 / 4,862 / 0 / 246 |
| Kuda (US org 25) | 14:32:20Z | 2,554 / 2 / 110 / 2,442 / 0 / 75 |
| Kuda (US org 25) | 14:47:17Z | 2,630 / 3 / 99 / 2,528 / 0 / 75 |
| Kuda (US org 25) | 15:03:24Z | 2,704 / 0 / 113 / 2,589 / 2 / 75 |
| Easypay (org 41) | three cycles | 169 / 1 / 38 / 130 / 0 / 15; 200 / 2 / 68 / 130 / 0 / 15; 181 / 1 / 49 / 131 / 0 / 15 |
| Rho, NALA | small cycles | clean |
Flag state: at 15:09:20.750879Z, after those repeated natural cycles,
qa_sampling_recovery_native v8 kept the five exact slugs (kuda-pilot,
rho-tnkw, easypay, nala, qonto) at 100% and added a separate 5%
organization-group rollout (server-only, active). This is the only canary
change. The next natural cycles and API2 errors must be verified before any
further widening; any regression is an immediate rollback to the five exact
slugs. No manual customer-job replay.
Other native flags (still exact Kuda-only, decision: no widening):
post-cmu70 through about 15:10Z, conversation_upload_intake_native saw
zero native upload events, conversation_transcription_state_native saw zero
API2-owned transcription state transitions, and
call_recording_attachment_native saw zero native call-recording attachments;
all call-recording traffic stayed Rails-owned (US 11 provider_sync, EU 197
aircall_import). No API2 level>=50 errors from 14:25Z through about
15:10Z.
Effects boundary, fixed trailing 24 hours ending 15:05:47Z: 35,204
API2-to-Rails calls (US 11,513; EU 23,691), down 1,748 from the 14:06:18Z
window.
| Family | 24-hour calls |
|---|---|
| QA sampling recovery | 23,283 |
| Transcription reconciliation | 4,938 |
| Call-recording attachment | 2,581 |
QA stage execution (execute_stage) | 1,856 |
| Direct upload processing | 1,031 |
| Transcription context/write | 598 |
| Provider-sync completion | 545 |
Effects boundary, fixed trailing seven days ending 15:05:47Z: 406,808
calls (US 154,855; EU 251,953), of which 192,210 is shipped-family pre-cutover
residue (attachment purge 111,889; tools 44,606; image captions 35,715).
Evidence as of 2026-09-18T16:06:07Z (effects census) and 17:09Z (deployment, flag state and cycles)
This section supersedes the 15:05:47Z windows above wherever the two disagree, and the 14:06:18Z gateway figures for the public track.
Gateway boundary, fixed trailing 24 hours ending 16:06:07Z (public track;
package-level detail in #12512 ):
13,930 events (US 11,693; EU 2,237): 528 API2-blocked, 10,529
forwarded/rejected Rails 404 scanner/stale, 2,871 non-404/Action Cable
upper bound, 2 aborts. Meaningful live dependencies: conversation uploads 989,
Action Cable 825, Outreach rolling residue 210 with zero forwards after
15:28Z; older residue Zendesk about 582, Unit21 239. Three signed Outreach
deliveries at 15:43Z returned malformed_event 500 — recorded as
parity/error evidence, not as accepted natural deliveries; no accepted natural
Outreach delivery yet, so Outreach ownership holds.
Gateway coverage caveat: the nominal seven-day query still covers only the
structured telemetry available since 2026-09-17 (partial coverage): 27,332
events; 780 blocked; 16,650 Rails 404; 9,898 non-404/Cable; 4 aborts.
Direct upload processing: #12483
is deployed in 623cf7a7; api2_conversation_upload_native_processing is
confirmed absent in PostHog, so the path is fail-closed (off) and every direct
upload still calls Rails; no package-specific first-slice error. Flag creation
and the first canary are held pending Ronald’s review and a natural-traffic
plan.
Deployment state: release-prod c47e1113 (#12520, containing only
#12517 on top of 623cf7a7) is live; Flightcontrol reports #12520 Success in
US and EU, API2 web 1/1 and the logical API2 workers (physical names
fc-rulebase-api2-worker-pr-zy7q0 and fc-rulebase-api2-worker-pr-m9780, not
preview services) 5/5 in both regions. The EU worker deployment took 9m54s and
is service revision rev:1651. Runtime logs at 17:09Z show active natural
Qonto (EU org 33) processing across multiple worker hosts, including
recovery-33 native effects applied and provider sync completion, with no
error/fatal rows in the visible first slice.
Verification boundary for c47e1113 (state precisely, do not assume):
PostHog has zero application logs and the AWS SSO session expired, so for this
revision the exact task-definition commit SHAs, migration startup, the full
first-slice level>=50 error query, and a fresh residual recount after the
16:06:07Z cutoff are unverified. The fixed 24 h / 7 d census ending
16:06:07Z below remains the current measured baseline until a new exact
count is taken.
Preceding revisions: 623cf7a7 (#12516) completed in EU at 16:35:56Z and US
at 16:36:16Z with zero pending, zero failed tasks, every image and
FC_GIT_COMMIT_SHA matching, both web tasks acquiring the migration advisory
lock with no migration failure, web TD 1643/1661 and workers TD 1633/1650;
before it cf0374d (completed 16:10Z; web TD 1641/1659, workers TD
1631/1648) and c7d66aaa, the running revision during the 15:36Z Qonto stop
below.
Organization attribution correction (read-only production DB mapping): EU
org 41 is nala, an exact canary, not Spendesk. EU Spendesk is org 39 (slug
spendesk) and stayed Rails-owned. The four EU org-41 cycles below are
therefore Nala exact-canary cycles, not 5% group cycles; earlier text
attributing them to Spendesk is superseded, and earlier sections’ references
to “EU org 41” (including rows labelled Easypay (org 41)) describe the same
Nala organization.
Nala (EU org 41) natural cycles were clean (candidates / requested / skipped
/ throttled / counters lowered; zero candidate failures, zero transcription
failures, zero unprocessed): 15:16Z 278 / 0 / 147 / 131 / 15; 15:30Z 166 / 0
/ 33 / 133 / 15; 15:45Z 178 / 0 / 45 / 133 / 15; 16:00Z 294 / 0 / 52 / 242 /
15.
Qonto (org 33) stopped safely during the c7d66aaa rollout: the 15:36Z
cycle ended with 911 unprocessed and zero failures (the #12482 cooperative
stop), then the normal scheduler resumed with clean 5,000-candidate cycles at
15:51Z (15 requested / 90 skipped / 4,895 throttled) and 16:07Z (8 / 68 /
4,924), zero failures and zero unprocessed. No manual replay. This is the
first production observation of the deployment-interruption path behaving as
designed.
Kuda (US org 25) 16:07Z: 5,000 candidates / 6 requested / 117 skipped / 4,876 throttled / 1 ignored / 75 counters lowered, zero failures.
Flag state: at 16:11:54Z qa_sampling_recovery_native v9 advanced the
organization group from 5% to 10%, preserving the exact slugs kuda-pilot,
rho-tnkw, easypay, nala, qonto and server-only group evaluation. At
16:12:41Z conversation_transcription_state_native v2 added qonto as a
second exact canary next to kuda-pilot, because Kuda produced no native
persistence writes while Qonto had 18 Rails loaded and 18 Rails
transcribed calls since 15:09Z. Hold further widening of both flags until
the natural outcomes of these changes are verified; any regression is an
immediate rollback to the previous flag version. No manual customer-job
replay.
First native transcription-state persistence proof (Qonto, EU org 33):
after v2, one native job completed end-to-end: 16:23:45Z owner api2, phase
context, outcome loaded for call detail
conversation_part_call_detail_mxPBJ8kOGrmtJ8kQbljy9Z4a; 16:24:16Z phase
persist, outcome transcribed. Read-only row reconciliation: org_id 33,
transcribed_at 16:24:16.644Z, language eng, duration_ms 151,340,
attempt_count 0, failed_at nil, 7 transcript-message rows. CreditUsage
count is 0 because Qonto has zero active transcription credits, which matches
both native and Rails semantics (both skip usage when there is no active
credit). No native-path error. Hold at Qonto + Kuda only: the package runbook
requires a complete 24-hour distribution/reconciliation window before further
expansion.
First natural 10% cycle (16:15Z) was clean. The 10% group newly selected
only zero-candidate API2 organizations: US 26 intercom-demo, 40
synctera-demo, 44 novo-quick-test-l5h6, 45 vestwell-demo; EU 26
intercom-demo. Exact canaries: US Rho 8 candidates (3 requested / 5 skipped),
Easypay 2 skipped; EU Nala 178 (45 skipped / 133 throttled / 15 counters
lowered). All cycles had zero candidate/transcription failures, zero
unprocessed, stoppedEarly null. Hold at 10%: the new percentage cohort
contributed zero candidates, so it is not yet evidence for widening.
16:30Z cycle under v9 and the 623cf7a7 rollout. US was clean: Kuda 3,055
candidates / 0 requested / 113 skipped / 2,941 throttled / 1 ignored / 78
counters lowered; Rho 9 / 6 / 3; Easypay 3 skipped; percentage-selected
organizations again had zero candidates. EU Nala 170 / 0 / 36 / 134 / 15 was
clean. Qonto (EU org 33) was interrupted by the rollout and stopped safely: 5,000
candidates, 12 requested, 108 skipped, 2,956 throttled, 245 counters lowered,
1,924 unprocessed, stoppedEarly shutdown, zero candidate/transcription
failures. This is the second production cooperative stop. Hold QA at 10%; the
next natural Qonto scheduler cycle must resume cleanly (no manual replay).
Flag state at 17:09Z (no widening): qa_sampling_recovery_native is still
v9 — exact kuda-pilot, rho-tnkw, easypay, nala, qonto at 100% plus
the 10% organization group; PostHog last called 16:45:01Z (advisory). The
17:09Z worker logs show Qonto processing has resumed naturally under
c47e1113, but a clean resumed Qonto QA-cycle summary (structured worker
result with zero failures and zero unprocessed) and a non-zero
percentage-cohort outcome are both still missing, so the group stays at 10%.
Effects boundary, fixed trailing 24 hours ending 16:06:07Z: 33,475
API2-to-Rails calls (US 10,705; EU 22,770), down 1,729 from the 15:05:47Z
window.
| Family | 24-hour calls |
|---|---|
| QA sampling recovery | 21,809 |
| Transcription reconciliation | 4,911 |
| Call-recording attachment | 2,564 |
QA stage execution (execute_stage) | 1,713 |
| Direct upload processing | 1,011 |
| Transcription context/write | 614 |
| Provider-sync completion | 481 |
Effects boundary, fixed trailing seven days ending 16:06:07Z: 403,815
calls (US 153,570; EU 250,245), of which 190,537 is shipped-family pre-cutover
residue (attachment purge 111,057; tools 44,248; image captions 35,232),
leaving 213,278.
Read-only verification attempt at 2026-09-18T19:12Z (access boundary; no new census)
This attempt refreshed only what read-only PostHog (production project
63353) and Sentry could answer. It does not supersede the 16:06:07Z
effects/gateway census or the 17:09Z deployment observation; those remain the
current measured baselines.
Access boundary (exact): no AWS/Flightcontrol credentials were available
(aws sts get-caller-identity → Unable to locate credentials), and PostHog
logs-count over the trailing 24 h and 7 d returned 0. Therefore the following
remain unverified after 17:09Z: the task-definition commit SHAs and
desired/running counts in both regions (last verified c47e1113, web 1/1,
logical -pr- production workers 5/5, EU worker rev:1651), migration startup,
the first-slice level>=50 error query, every natural
qa_sampling_recovery_native cycle summary after 17:09Z (including the
resumed Qonto cycle and any percentage-cohort candidate outcome), and any
effects residual recount after 16:06:07Z. Post-17:09Z gateway traffic is
likewise unverified; the public runbook update in
#12512 (head
71063f926) records the same boundary and the 15:28Z→cutoff split (27 US
upload forwards, 11 EU Cable forwards, 0 Outreach forwards; the partial 7 d
27,332 is US 21,327 / EU 6,005). Outreach remains held: the three signed US
deliveries at 15:43Z returned malformed_event 500 with Rails parity and no
accepted delivery exists, none after 16:10Z.
Exact flag definitions (PostHog production, read at ~19:05Z):
| Flag | Version | Targeting | updated_at | last_called_at (advisory — see the flag-evaluation evidence gate; not proof of non-execution) |
|---|---|---|---|---|
qa_sampling_recovery_native (id 280844) | 9 | exact kuda-pilot, rho-tnkw, easypay, nala, qonto at 100% + separate organization group 10%; active, server-only | 16:11:54.586Z | 18:45:01.246Z |
conversation_transcription_state_native (id 280861) | 2 | exact kuda-pilot, qonto at 100%; active, server-only | 16:12:41.583Z | 18:28:50.299Z |
conversation_upload_intake_native (id 280845) | 1 | exact kuda-pilot; active | 09:36:04.177Z | null (never called) |
call_recording_attachment_native (id 280862) | 1 | exact kuda-pilot; active | 09:49:43.224Z | 17:48:38.965Z |
api2_conversation_upload_native_processing | — | absent (does not exist in project 63353); #12483 path fail-closed/off | — | — |
No flag was changed during this verification.
Advisory PostHog event signals (not outcome reconciliation): PostHog holds
no QA cycle summaries or package outcome events, so these do not satisfy the
QA gates below. $feature_flag_called rows for qa_sampling_recovery_native
from 17:30:00Z to 19:00:00Z show the scheduler still evaluating the flag
each 15-minute bucket: true for the five exact slugs and for the
percentage-selected zero-candidate organizations novo-quick-test-l5h6,
synctera-demo, vestwell-demo and intercom-demo (17:30Z, 18:30Z,
18:45Z); qonto evaluated true at 17:45:00.386Z. These prove flag
evaluation only, not that the cycles completed cleanly.
call.transcribed events split cleanly by emitting library: API2
(posthog-node) emitted 26 for org:qonto from 16:24:16.788Z (the
documented first native persist) through 19:13:04.971Z, 14 of them after
17:20Z, while Rails (posthog-ruby) last emitted a Qonto call.transcribed
at 15:55:02.747Z — none after qonto was added to the transcription-state
flag at 16:12:41Z. rho-tnkw (not a transcription-state canary) stayed
Rails-emitted (24 events, last 19:14:16Z). No call.transcription_failed
event exists in the project. Kuda produced no API2-emitted call.transcribed;
conversation_transcription_state_native evaluated true three times for
kuda-pilot/qonto and false once for rho-tnkw (18:08:49Z);
call_recording_attachment_native evaluated false once for rho-tnkw
(17:48:38Z, the advisory timestamp above) and conversation_upload_intake_native
was never evaluated. Row-level reconciliation of these Qonto native persists
(transcript rows, credit usage, attempt counts) remains unverified without
database access.
Sentry (API2 and Rails, unresolved, seen after 17:20Z): no new API2 issue
matches the QA sampling, QA stage, transcription, upload or call-recording
paths. One Rails issue is relevant to upload intake:
RULEBASE-API-5WT
Api2ConversationUploadEnqueuer::AmbiguousDeliveryError: Net::ReadTimeout in
ConversationUploadsController#create (Rails → API2 enqueue), 2 occurrences
(09:52:53Z, 17:55:40Z), 0 users. This is the still-Rails-owned upload path
calling API2, not a native-intake regression; track it under the upload-intake
package.
Triage decision (internal track, ENG-3218
full decision and query; ENG-3248
summary): RULEBASE-API-5WT does not block the Kuda intake or
direct-processing canaries and needs no code PR. It is the existing Rails →
API2 enqueue timeout on the still-Rails-owned ingress: both occurrences were
3-second read timeouts, the client still received 201, and the durable path
recorded an uncertain ledger row for the API2 reconciler under the
deterministic job id. The first event predates c47e1113. Native intake
removes this hop entirely. Residual verification gap: without production DB
access, conversation-upload/process/649026 could not be confirmed to have
left the uncertain state; if it remains owner=rails, upgrade this to a
blocker. Other unresolved API2 items (RULEBASE-API2-82 dashboard-widget
statistics query, RULEBASE-API2-DJ provider-sync N+1 HTTP, RULEBASE-API2-F1
Redis subscriber-mode on channel stream) predate or are unrelated to the QA
track.
Decisions (19:12Z): hold qa_sampling_recovery_native at v9 — a clean
resumed Qonto structured cycle summary and a non-zero percentage-cohort outcome
are still unverified; hold conversation_transcription_state_native at Qonto +
Kuda pending the 24-hour reconciliation window and row-level verification;
conversation_upload_intake_native and call_recording_attachment_native stay
Kuda-only with no native traffic observed; direct upload processing stays
fail-closed pending Ronald’s review and a bounded single-organization Kuda
canary (no PR, no flag change). No job was replayed, no production data was
written, no flag was changed, nothing was deployed or merged.
Read-only verification attempt at 2026-09-18T20:59Z (new release, still no structured QA outcomes)
Deployment (GitHub deployment status only): release-prod is now
e45afa48c1c887549fa2b374da8a7c6002a9fbff (#12521, from main d198f5f7),
superseding c47e1113. Flightcontrol’s GitHub deployment status reports
success in EU at 20:51:10Z and US at 20:51:38Z. AWS SSO remains expired,
so task-definition commit SHAs, desired/running counts, migration startup logs
and the first-slice error query for e45afa48 are unverified; the
16:06:07Z effects/gateway census and the 17:09Z c47e1113 worker
observation remain the last measured baselines.
PostHog flags (project 63353), definitions unchanged, advisory
last_called_at only: qa_sampling_recovery_native v9 (five exact slugs +
10% organization group) 20:15:01Z; conversation_transcription_state_native
v2 Kuda + Qonto 18:28:50Z; conversation_upload_intake_native v1 Kuda never
called; call_recording_attachment_native v1 Kuda 17:48:38Z;
api2_conversation_upload_native_processing still absent (fail-closed).
Structured QA outcome evidence (independent read-only attempt): PostHog
carries no qa_sampling_recovery_native cycle-summary event of any kind since
16:00Z (the only QA-adjacent event is four conversation.agent_evaluated),
so the 20:00Z/20:15Z/20:30Z/20:45Z/21:00Z natural cycles cannot be
reconciled from PostHog. $feature_flag_called rows 19:00Z-21:00Z show
the scheduler evaluating true for kuda-pilot, rho-tnkw, easypay,
nala, qonto and for the same zero-candidate percentage cohort seen at
16:15Z (intercom-demo, novo-quick-test-l5h6, synctera-demo,
vestwell-demo); no new organization entered the 10% cohort. This is flag
evaluation, not candidate/requested/failure counts. Both widening gates
therefore remain unmet: no structured clean resumed-Qonto summary and no
non-zero percentage-cohort outcome. No widening was recommended or made.
Advisory transcription signal: call.transcribed since 16:00Z is 38
posthog-node (API2) events for org:qonto through 20:43:08Z and 29
posthog-ruby (Rails) events for org:rho-tnkw through 20:52:30Z; the
Qonto events do not carry the flag property, so they are advisory only and the
DB row-reconciliation gate for conversation_transcription_state_native stays.
Sentry, e45afa48 first slice:
RULEBASE-API2-F1 (Action
Cable/SSE Redis subscriber-mode error) recurred at 20:52:22Z, immediately
after the EU/US deployment success timestamps; the public-track session owns
its triage. No QA sampling, QA stage, transcription, upload or call-recording
issue appeared.
Decisions (20:59Z): all 19:12Z holds stand; no flag change, replay,
deploy or merge.
Authoritative CloudWatch cycle evidence, 21:00Z natural cycle (post-e45afa48)
CloudWatch worker logs (authoritative, structured cycle summaries) show all five
exact-slug QA canaries running owner=api2 at the 21:00Z natural cycle, the
first after the e45afa48 task replacement, with zero candidate/transcription
failures and unprocessed=0:
| Organization | Candidates | Requested | Skipped | Throttled | Counters lowered | Failures | unprocessed |
|---|---|---|---|---|---|---|---|
| Kuda (US org 25) | 3,477 | 0 | 69 | 3,406 | 79 | 0 / 0 | 0 |
Rho (US org 19, rho-tnkw) | 9 | 3 | 6 | 0 | 0 | 0 / 0 | 0 |
| Nala (EU org 41) | 180 | 1 | 34 | 145 | 15 | 0 / 0 | 0 |
Qonto (EU org 33, completed 21:07Z) | 5,000 | 13 | 119 | 4,867 | 241 | 0 / 0 | 0 |
Easypay (US org 38, easypay) | 0 | 0 | 0 | 0 | 0 | 0 / 0 | 0 |
Qonto’s first attempt, begun before the task replacement, stopped
cooperatively at 20:50Z with unprocessed=948 and stoppedEarly=shutdown;
the post-deploy retry completed cleanly at 21:07Z. This is deployment
interruption followed by natural recovery, not a canary failure, and it is the
structured clean resumed-Qonto evidence the first widening gate asked for.
Easypay’s zero-candidate cycle is API2-evaluated but adds no workload proof;
its earlier non-zero cycles remain the useful evidence. Org IDs here follow the
21:18Z read-only production Rails query
Organization.where(id: [19, 38]).pluck(:id, :slug) →
[[19, "rho-tnkw"], [38, "easypay"]].
The completed Qonto summary at 21:07:08Z also carried ignored=1 and
stoppedEarly=null.
Aborted flag advance (21:11:25Z → 21:16:22Z): the operator advanced
qa_sampling_recovery_native (PostHog id 280844) from 10% to 25% (v10) at
21:11:25Z, citing a clean non-zero 20:15:06Z cycle for US org 38 (3
candidates / 1 requested / 2 skipped / 0 failures) as random-cohort proof.
A read-only production Rails query at 21:18Z
(Organization.where(id: [19, 38]).pluck(:id, :slug) →
[[19, "rho-tnkw"], [38, "easypay"]], no data write) confirmed org 38 is
Easypay and org 19 is Rho — both exact canaries — so that cycle was exact-canary
evidence and the widening gate was not met. The flag was rolled back to v11 — five exact
slugs at 100% + 10% group at 2026-09-18T21:16:22.308984Z (PostHog read
21:17Z). The 25% exposure lasted 4 m 57 s, covering only the 21:15Z cycle.
US organizations selected around that interval: 16 rho-demo-2, 21
baserule, 22 frame-finance, 25 kuda-pilot, 26 intercom-demo, 38
easypay, 40 synctera-demo, 44 novo-quick-test-l5h6, 45 vestwell-demo,
48 nala, 50 alioune-sow. Every non-exact completion visible at the check
had zero candidates and zero failures; no regression was observed. Record this
as an aborted advance caused by incorrect regional-ID attribution, not an
outcome regression. Current state: v11 / 10%. No further widening until a
meaningful non-zero random-cohort cycle proves outcomes. No replay, deploy or
merge.
Exact residual census at 2026-09-18T21:05:00Z / 21:06:56Z
Two fixed censuses were taken minutes apart; both are exact over their own window and are recorded as reported.
Cutoff 21:05:00Z: Rails /internal effects 24,963 / 24 h (US 7,225; EU
17,738) and 390,842 / 7 d (US 148,376; EU 242,466). QA sampling alone:
14,745 / 24 h (US 1,555; EU 13,190) and 144,877 / 7 d (US 35,620; EU 109,257).
Cutoff 21:06:56Z: 24,905 / 24 h (US 7,223; EU 17,682), down 8,570 (25.6%)
from the 16:06:07Z 33,475. Families: QA sampling 14,694 (down 7,115 / 32.6%);
transcription reconciliation 4,675; call recording 2,462; QA stage 1,050;
direct upload processing 984; provider-sync completion 277; transcription
context + write 422; Unit21 144. Fixed 7 d: 390,754 (US 148,373; EU
242,381). Families with zero current-24 h calls (pre-cutover residue only)
total 184,129 — attachment purge 107,676; tools 42,646; image-caption context +
update 33,755; small tail 52 — leaving 206,625 meaningful live/recent calls.
e45afa48 is live in both regions; public Action Cable has a separate blocker
tracked in #12526 . Full
Rails shutdown remains blocked.
Release boundary 06236881 and the 21:15Z partial cycle
origin/release-prod advanced to 06236881964c45ac32c9c8bc079ddcd484959182
(merge #12525; only the devalue 5.9.2 CVE bump since e45afa48). All four
API2 services are COMPLETED on that SHA: web US/EU 1/1, logical production
workers (physical fc-rulebase-api2-worker-pr-zy7q0 and -m9780) 5/5.
First post-deploy QA cycle at 21:15Z (snapshot taken before the heavy jobs
finished): Rho org 19 completed 5 candidates / 3 requested / 2 skipped with
zero candidate/transcription failures and unprocessed=0; Easypay org 38 and every
percentage-cohort evaluation seen so far were zero-candidate / zero-failure;
Kuda and Qonto had not yet finished.
The percentage cohort therefore still has no meaningful non-zero workload
proof. This was the only cycle under the aborted v10 25% window; the flag is
v11 (five exact slugs + 10%) since 21:16:22Z and holds there until a clean
natural percentage-cohort cycle with real candidates.
2026-09-19 evidence: eae24156 healthy, 17:47:25Z census, transcription canary past its 24 h gate
Runtime. Through the 17:47:25Z census the API2 task SHA was
eae24156f80b5dc372a333e340a25f975ac01291 in both regions: web US/EU 1/1,
logical production workers (physical fc-rulebase-api2-worker-pr-zy7q0 /
-m9780) 5/5, pending 0, COMPLETED; migration advisory locks acquired EU
13:59:03Z, US 14:00:24Z. Intermediate release-prod UI/docs commits did
not change the API2 task SHA. release-prod
2d02ce188799b8488bb2bfeb9ab26007867f2efc (#12541, human-merged) is now live
and COMPLETED in both regions: US web TD 1650 1/1, US worker TD 1640 5/5; EU
web TD 1668 1/1, EU worker TD 1657 5/5; all tasks on 2d02ce18, pending 0,
failedTasks 0; migration locks acquired EU 17:58:13.243Z, US
17:59:21.806Z. No QA or transcription package error in the first slice.
release-prod 279ae48c then completed in both regions: US web TD 1651 1/1,
worker TD 1641 5/5; EU web TD 1669 1/1, worker TD 1658 5/5; all tasks on the
exact commit, pending/failed 0, migration locks acquired and listeners up. All
holds below, including the Rho transcription expansion, are unchanged by these
deploys. Later: the API2 runtime commit on all four services is 4d5b1044;
release-prod 610aebdb is docs-only and did not change the runtime.
release-prod 5e3c514 is then fully deployed: US web TD 1653 1/1, US logical
worker (physical -pr-zy7q0) TD 1643 5/5, EU web TD 1671 1/1, EU logical
worker (physical -pr-m9780) TD 1660 5/5, all completed, zero pending; no
worker level>=50 or package errors in the inspected 20-minute slice, and
none through 19:10Z. Rails
remains desired/running 3/3 in both regions — do not scale Rails down while
the QA ladder is back at 10% and canaries are still being widened.
Fixed residual census ending 2026-09-19T17:47:25Z. Rails /internal
effects 6,997 / 24 h (US 3,950; EU 3,047), down 17,908 (71.9%) from the
2026-09-18T21:06:56Z 24,905. Families: transcription reconciliation 3,921;
call recording 1,834; direct upload 826; Unit21 144; counter cache 129;
transcription context/write 64; expiry 48; QA stage 20; tiny tail 11; QA
sampling and provider completion both zero. Seven-day effects 344,118
(US 136,068; EU 208,050), of which 288,199 is major zero-current-24 h residue
(attachment purge 93,754; tools 37,778; captions 27,651; QA sampling 123,685;
provider completion 5,331), leaving a 55,919 live/recent upper bound.
Refreshed at cutoff 2026-09-19T18:09:40Z: effects 6,917 / 24 h (US
3,917; EU 3,000), down 72.2% from 24,905, led by transcription reconciliation
3,930, call-recording attachment 1,779 and direct upload 801; seven-day
343,395, of which 287,545 is pre-cutover / now-zero residue, leaving a
55,850 meaningful-live upper bound.
Refreshed again at cutoff 2026-09-19T18:44:39Z: effects 6,860 / 24 h (US
3,938; EU 2,922), down 57 from 6,917. Combined methods: transcription
reconciliation 3,912; call-recording create 1,708; direct upload process 762
retry1 +mark_failed1; Unit21 144; QAexecute_stage95; counter cache 128; transcription context 25 + write 25; expiry 48; field values 4; report actions 6; Slack 1. Fixed seven-day 341,577 (US 135,048; EU 206,529); major now-zero / pre-cutover residue 285,878 (attachment purge 93,129; QA sampling 122,885; tools 37,263; captions 27,331; provider completion 5,270), leaving a 55,699 live/recent upper bound before tiny now-zero tails (55,657 including them).
Refreshed at cutoff 2026-09-19T19:11:23Z: effects 6,755 / 24 h (US
3,898; EU 2,857), down 105 — reconciliation 3,912 (empty heartbeats);
recording 1,639 (all pre-cutover); direct upload 734; Unit21 144; QA stage 95;
counter cache 128; expiry 48; transcription context/write 44; tiny tail 11.
Seven-day 340,652 (US 134,791; EU 205,861), dominated by
pre-cutover/now-zero residue. Rails stays 3/3 because uploads and Cable
persist.
Refreshed at cutoff 2026-09-19T19:24:32Z: effects 6,718 / 24 h (US
3,898; EU 2,820) — reconciliation 3,906 (empty heartbeats); call recordings
1,603; direct uploads 741 (process + retry + mark_failed); Unit21 144; counters
128; QA stage 95; transcription context 21 + write 21; expiry 48; fields 4;
reports 6; Slack 1. Seven-day 340,305; major now-zero/pre-cutover residue
284,645 (QA sampling 122,074; purge 93,129; tools 37,126; captions 27,074;
provider completion 5,242), leaving 55,660 live/recent before tiny now-zero
tails. At this cutoff PostHog read qa_sampling_recovery_native v16 (five
exact + 50%, 19:22:47Z) and call_recording_attachment_native v4 (three
exact + 10%, 19:23:51Z); because these were concurrent changes, no further
widening of either flag until the first complete post-v16 19:30Z QA cycle
and the first non-exact percentage recording outcome are both clean — document
rollback on any regression. Runtime remains 5e3c514 in both regions;
release-prod 8ec16 (ENG-3270) deployment has just started.
Refreshed at cutoff 2026-09-19T19:54:08Z: effects 6,610 / 24 h (US
3,871; EU 2,739), down 108 from 6,718 — reconciliation 3,882 (empty
heartbeats); call recording 1,525; direct uploads 741; Unit21 144; counters
128; QA stage 95 (including one 500); transcription context 18 + write 18;
expiry 48; tiny tail 10. Seven-day 338,760; major now-zero/pre-cutover
residue 283,068 (QA sampling 121,370; purge 92,507; tools 37,074; captions
26,875; provider completion 5,242), leaving 55,692 live/recent before tiny
tails. Release 8ec16b8d remains exact/COMPLETED on all four services (US web
TD 1654 1/1, worker TD 1644 5/5; EU web TD 1672 1/1, worker TD 1661 5/5; zero
pending/failed). call_recording_attachment_native remains v4 (exact Kuda +
Qonto + Rho + 10%): every visible outcome since v4 is attached (Qonto through
19:51Z, Rho at 19:31Z) but there is still no non-exact percentage outcome —
do not widen.
Refreshed at cutoff 2026-09-19T20:58:21Z: effects 6,262 / 24 h, down
456 (6.8%) from 6,718 — reconciliation 3,741 (empty heartbeats); call
recording creates 1,347; direct uploads 741; Unit21 144; counters 128; QA
stage 82; transcription context/write 22; expiry 48. Seven-day 336,636;
major now-zero/pre-cutover residue 280,965, leaving 55,671 live/recent upper
bound. qa_sampling_recovery_native has been at 100% (v21) since
20:08:04.328711Z; post-rollout native cycles US 108, EU 51. Qonto job
..._33_1988722 stopped only for shutdown during a deploy (5,000
candidates, 1,372 unprocessed, zero candidate/transcription failures) and the
next natural cycle ..._33_1988723 completed at 20:52:08Z with 5,000
candidates / 4 requested / 732 skipped / 4,264 throttled, zero failures, zero
unprocessed, no early stop. Decision: keep QA at 100%; the deploy shutdown
is a known interruption class (see the 19:30Z bucket), not a regression, and
the natural retry closed it without replay. The reversible zero-window clock
for Internal::QASamplingRecoveriesController and
Internal::ConversationTranscriptionReconciliationsController continues to run
from 20:08:04.328711Z; retirement of those controllers is still a later,
explicit decision.
Refreshed at cutoff 2026-09-19T21:10:29Z: Rails /internal effects
6,202 / 24 h (US 3,697; EU 2,505) and 336,317 / 7 d (US 133,669; EU
202,648). 24 h: reconciliation 3,700; call recording 1,332; direct upload 741;
Unit21 144; counters 128; QA execute_stage 78; expiry 48; transcription
context 22 + persist 22; tiny tail 11. Last-seen timestamps show the large
families are now historical: reconciliation 20:00:03Z (before the QA v21
100% write at 20:08Z); call recording 18:41:47Z; direct upload
19:18:11Z; execute_stage 18:20:59Z; Rails transcription context/write
17:10:30Z. The 21:00Z natural all-organization QA cycle was clean: Kuda
1,706 candidates / 0 requested / 77 skipped / 1,627 throttled / 2 ignored;
Qonto 5,000 / 11 / 97 / 4,892; Nala 237 / 21 / 55 / 161; Rho 4 / 1 / 3; zero
candidate/transcription failures, zero unprocessed, no early stops. The
21:30Z all-organization cycle was also clean: Rho 3 / 3; Kuda 1,728 / 5 /
191 / 1,530 / 2 ignored; Nala 223 / 7 / 54 / 162; Qonto 5,000 / 6 / 85 /
4,909; zero failures, zero unprocessed. Later effects cutoff: 6,079 / 24 h
and 334,979 / 7 d. Keep QA at 100%; the 24 h Rails-zero clock for the
reconciliation controller runs from its last call at 20:00:03Z.
Public gateway at cutoff 21:10:29Z (24 h): 6,784 = US 4,795
(rejected 404 3,550 + blocked 404 495 + forwarded 404 22 + upload forwards
728) + EU 1,989 (rejected 404 1,868 + blocked 404 7 + forwarded 404 11 +
WebSocket forwards 101 + WebSocket upstream errors 2). Classification: local
blocked/scanner 5,920; Rails-forwarded 404 33; meaningful live 831 (728
uploads + 101 Cable forwards + 2 Cable upstream errors). Rails stays 3/3 for
the gateway because uploads and Cable still persist.
Bounded backfill_run_agent fallback after the census (US only): the
API2 worker emitted 25 qa_stage_rails_fallback events 21:17:19Z–21:20:45Z,
all organization_id=25 (Kuda), reason=backfill_run_agent — 21
check_eligibility + 4 start_evaluation per the exact Logs Insights
re-query (an earlier read reported 20 / 5) — each matched by a successful
Rails Internal::QAEvaluationRequestStagesController#execute_stage; EU zero.
This is the documented ENG-3237 fallback family (admin backfill-run requests
carry qa_evaluation_backfill_run_agent_id; eligibility-stage.ts /
evaluation-creation-stage.ts hand them to Rails because skip/fail paths
enqueue the backfill replacement, auto-backdate and admin_forced sampling
bypass live in QAEvaluationRequest). It is distinct from QA sampling
recovery (still 100%, clean) and bounded by the run’s standby pool. Decision
at evidence cutoff 21:20:45Z (last of the 25 events): no hot-fix; port the
family natively. Implemented in #12573
(ENG-3280, in review, not merged): admin_forced requests are native in both
stages regardless of qa_evaluation_backfill_run_agent_id (Rails
sampling_required? / should_evaluate? already bypass sampling for them),
and the native terminal-effect outbox dispatches the deterministic
qa-eval-backfill-replacement-<runAgentId>-<requestId> job after commit
(replacement re-evaluates auto-backdate). backfill_run_agent remains in
native dispatch only for scheduled/ticket_closed rows carrying a run-agent
id, a shape nothing produces; the other fallback reasons are unchanged.
Rollback boundary: code revert — Rails keeps serving execute_stage, no flag
was added. Until #12573 is deployed the Kuda family continues to fall back to
Rails 1:1; this is not an irreversible retirement claim for QA stages.
Public gateway at cutoff 18:45:18Z (summary; package detail in the
gateway runbook): 7,623 / 24 h = 6,730 local blocked/rejected + 40
forwarded 404 + 853 meaningful (755 uploads, 96 Cable forwards, 2 Cable
upstream errors). Available structured seven-day 36,570 = 10,072 local +
15,761 forwarded 404 + 10,737 meaningful — not a full seven-day window.
QA sampling (qa_sampling_recovery_native, v11: five exact slugs + 10%).
Exact cohorts stayed clean after eae24156. Kuda post-deploy 14:15Z cycle:
1,547 candidates / 66 requested / 99 skipped / 1,382 throttled / 37 counters
lowered, zero candidate/transcription failures, unprocessed=0; later cycles
also clean (17:32:55Z: 2,078 / 2 / 97 / 1,978, 38 counters, zero
failures/unprocessed). Qonto completed repeated 5,000-candidate cycles after
14:30Z with zero failures and unprocessed=0; Nala, Rho and Easypay clean.
The 18:00Z cycles were also clean: Kuda 2,171 / 84 requested / 127 skipped /
1,437 throttled / 39 counters; Qonto 5,000 / 9 / 118 / 1,203 / 256; Nala 235 /
0 / 58 / 177 / 15; Rho 1 candidate skipped no_agent_response; all zero
candidate/transcription failures. Random-cohort orgs were again zero-candidate,
so the percentage-cohort workload gate is still unproven: hold at five exact
slugs + 10%; do not widen or replay.
Transcription state (conversation_transcription_state_native). The Qonto
canary cleared its >24 h distribution/reconciliation gate. Since Qonto
enablement (2026-09-18T16:12:41Z) structured logs contain exactly 196
API2 phase=context outcome=loaded and 196 phase=persist outcome=transcribed, with no retry, permanent, not_found or
already_transcribed outcomes. Read-only EU DB reconciliation over the same
window: exactly 196 Qonto rows, zero failures, zero duplicate credit usages,
max attempt_count 0. Three zero-message / missing-duration rows match the
prior Rails window shape (5/165); all 196 have no credit_usage, matching
Qonto’s 0/165 Rails usage baseline (no active transcription credit). At about
17:59Z the operator expanded the flag progressively to exact kuda-pilot +
qonto + rho-tnkw (no percentage); Rho had 30 Rails transcriptions in the
preceding 24 h and zero failures. The first natural Rho native
call.transcribed occurred at 18:10:12Z; read-only DB reconciliation of the
post-canary window: one row, transcribed=1, failed=0, zero_message=0,
27 transcript messages, credited=0, max attempt_count 0, against a
prior-24 h Rails baseline of 27/27 transcribed, 0 failed, 3 zero-message,
1,032 messages. US CloudWatch from the 18:10:12Z Rho boundary through
19:53Z shows conversation_transcription_state owner=api2 for rho-tnkw:
7 context loads, 5 persist|transcribed (fifth at 19:31:24.072Z) and, for
the two 19:45:22.687Z / 19:45:23.202Z contexts, jobs that returned
already_transcribed within ~1 ms with no persist transition — zero failure /
retry / permanent / not_found telemetry. Advisory PostHog read-only
reconciliation (about 19:55Z): exactly 5 API2-emitted (posthog-node)
Rho call.transcribed events between 18:10:12.394Z and 19:31:24.311Z with
5 distinct call_detail_ids (A4o0BGnb…, BEWKkrew…, YD3a2kez…,
9AGXvjyb…, qAPynjmw…), no duplicates, zero Rails-emitted Rho events.
Read-only production DB reconciliation (no writes) confirmed the five
completed API2-owned Rho rows are clean: all transcribed_at set,
transcription_failed_at / error null, attempt_count 0; transcript message
counts 27, 16, 18, 28 and 0 — the zero-message row (qAPynjmw1vZCoayRbrWB6N5J)
has transcribed_at 19:31:24.042Z and a clean failure state. The
“already_transcribed” reading for the two 19:45Z calls was false. They
decode to internal call details 138307 and 138306 whose stored public_ids
are legacy short ids (wrkukptg880r / rrnqqpybfj5a); at 19:58:46Z both had
transcribed_at null, zero transcript messages, transcription_failed_at /
error null, attempt_count 0 and no worker error telemetry — i.e. they were
never transcribed. Cause (code, not data): loadNativeTranscriptionContext
returns call.id = row.public_id when present, while the queued job carries the
prefixed conversation_part_call_detail_… id (Rails’ prefix_id, which is also
what the Rails context endpoint returns); run.ts
find(candidate.id === data.callDetailId) therefore misses on legacy-public_id
rows and silently returns already_transcribed, so the completion job runs
and the row is left untranscribed with no failure signal. The flag was rolled
back at 2026-09-19T20:10:58.059946Z from v3 (exact Kuda + Qonto + Rho) to
v4: exact kuda-pilot + qonto only; no replay or data write; the two Rho
rows are left untouched pending a human recovery decision. Fix (id-form
compatible lookup + explicit missing-call telemetry, regression tests for both
id forms) is owned in a separate focused PR under
ENG-3271 .
Decision: hold at exact Kuda + Qonto; do not re-add Rho or widen until the
fix is deployed and the two rows reach a terminal clean outcome.
Other holds. conversation_upload_intake_native exact Kuda: still no
native traffic. call_recording_attachment_native: exact Kuda still has no
native outcome; the Qonto canary produced 37 exact native intents from the
18:19Z boundary, all attached — max attempts 0, errors 0, 37 blobs /
attachments / distinct call details. The flag was advanced at
18:43:12.702316Z to v3, exact kuda-pilot + qonto + rho-tnkw only
(no percentage). Since v3, US Rho (org 19) produced its first native result at
18:59:35.125Z: outcome=attached, caller=provider_sync,
transcriptionQueued=1, no error — one intent attached, one detail / blob /
attachment, zero retry / error / orphan / duplicate; EU Qonto (org 33) produced
38 additional attached outcomes from 18:43:31Z through 19:11:08Z with
zero error outcomes in the queried slice, and is 40/40 attached post-boundary.
The initial decision was to hold at exact Kuda + Qonto + Rho for more natural
Rho volume; after a re-read showing every verified 24 h cohort covered and
clean (Rho 1/1, Qonto 40/40 post-boundary, Kuda prior clean; zero Rails
recording creates after 18:43:12Z), the flag was advanced at
19:23:51.290614Z to v4: the same three exact 100% canaries plus a separate
10% organization rollout. Current state: v4. Next gate: hold v4 pending a
natural percentage-cohort outcome plus continued API2 reconciliation and
Rails-zero evidence; immediate rollback to exact-only on any regression. The
v4 slice since 19:23:51Z has 15/15 attached log outcomes (Rho 1, Qonto
14), zero non-attached outcomes, and still no non-exact percentage-cohort
outcome. Full Rails shutdown remains blocked.
Rho downstream outcome, 21:21Z–21:55Z (read-only): call detail 554172
attached natively at 21:21:10Z with transcriptionQueued=true, but no
conversation_transcription_state telemetry ever ran for it. After 30
one-minute polls the call-summary job completed at 21:51:10Z with
status=skipped, reason=transcript_missing_at_deadline; QA request 5985771
remained pending / waiting-for-transcription through at least 21:55Z. The
shared transcription queue showed burst processing only through 21:22Z and
carries the same 400/hour ceiling as Rails, so this is not yet attributable to
the restored transcription-state handler (Rho was removed from
conversation_transcription_state_native at 20:10:58Z, v4 exact Kuda +
Qonto). Decision: no rollback, no replay, no data write; both Rho canaries
(recording v4 slice and transcription) are held and widening is blocked
until the queue-vs-handler cause is identified and 554172 / 5985771 reach a
terminal outcome by human decision.
Rho 554172 queue evidence and transcription rollback to v6, 22:08:07Z
(read-only): BullMQ job
conversation_transcription_call_932d35dea92b5f31_conversation_part_call_detail_r4dBk5Lz0B2hLRl4bep6xZRv
for call detail 554172 was in state waiting with attemptsMade=0,
attemptsStarted=0, enqueued 21:21:10Z. The shared call-transcription queue
had active 0, waiting 1,049, failed 1, completed 1,000, and the shared
400/hour limiter TTL was 566,961 ms. Backlog composition: 1,049 waiting = Kuda
1,044 + Rho 5; run sources 831 kuda-tx-yesterday, 197 kuda-tx-backfill, 20
live-hash, 1 provider-legacy-zendesk; oldest 19:51:52Z, newest
21:54:03Z. This is a queue backlog / global-limiter capacity failure, not
a recurrence of the identifier lookup bug fixed in
#12560 : the job was
never attempted, so no handler ran. Its call-summary deadline expired and QA
request 5985771 remained waiting. The coordinator rolled
conversation_transcription_state_native from v5 exact Kuda + Qonto + Rho
back to v6 exact Kuda + Qonto at 2026-09-19T22:08:07Z. Decisions: no
replay, no queue or customer-row mutation; a human recovery decision was
required for 554172 / 5985771 at the time (resolved naturally by 22:21:42Z,
see below); the 1,049-job backlog behind the global
limiter is a production blocker requiring a reviewed focused fix.
#12572 / ENG-3279
(retry throttle-skipped calls) was reviewed on the QA track and extended so
the retry sweep deduped candidates before its 100 cap and did not claim a
new-runId duplicate while a call’s attach-time job was still
waiting/active/delayed; its author closed it unmerged at 22:56:24Z in
favour of a one-off enqueue script for the missed Kuda calls. The dedup
findings are preserved on ENG-3279 and apply to that script too: exact-job-id
dedup means a fresh runId for a still-waiting call (554172 included) adds a
second job to the same limited backlog, and the 1,049 waiting jobs are already
enqueued — the gap is limiter capacity, not missing enqueues. Live-vs-bulk
precedence is the canonical fairness package
#12577 / ENG-3283; the
PGlite halfvec fixture fix
#12570 is still open and
not worked around. Nothing merged; human review required.
Rho 554172 drained naturally, ~23:20Z read-only check. The waiting job
above was picked up once the limiter window turned: processedOn 22:19:58Z,
finishedOn 22:20:08Z, attemptsMade/attemptsStarted 1, state completed.
DB call detail 554172: transcribed_at 2026-09-19T22:20:08.161Z, 105
transcript messages, transcription_failed_at/error null, attempt_count 0.
QA request 5985771 then completed normally at 22:21:42.701Z with no skip or
error. No human recovery action remains for these two records; no replay
or write occurred. This does not resolve the two earlier Rho rows
138306/138307 (legacy public_id, already_transcribed mis-match, fixed
forward by #12560) — those stay on the human recovery list. Decision:
conversation_transcription_state_native stays at v6 exact Kuda + Qonto
(Rho off) pending a reviewed and deployed fairness package (#12577 / ENG-3283)
and a fresh clean Rho canary. Since 22:13:05Z natural QA summaries are 180 US
- 85 EU API2-owned jobs with candidate failures 0, transcription failures 0,
unprocessed 0,
stoppedEarly0; QA stays at 100%. The one-off Kuda repair Ronald proposed when closing #12572 is human-owned and is not run or facilitated from this track.
2026-09-20 post-release state (release-prod 0a6fd1e775faabc7ea0cbc8df404ab3841fb9e69).
The release is healthy on US/EU API2 web and both logical worker services.
Merged and deployed in it: #12569 (this plan’s QA 100% evidence), #12570
(ENG-3277 idempotent halfvec fixture — the shard-1 blocker), #12573
(ENG-3280 native backfill_run_agent stages) and #12577 (ENG-3283 live-over-
bulk transcription precedence). qa_sampling_recovery_native is unchanged at
v21 / all organizations; no flag change and no replay. Fresh Rails 24h
read-only census, split into residue vs live dependency:
Internal::QAEvaluationRequestStagesController#execute_stage: 102 US, 0 EU, last21:20:46Z— that last call is the tail of the 25-call Kudabackfill_run_agentburst (21:17:25Z–21:20:46Z) recorded above, and that family is now native as of #12573 in0a6fd1e7. No stage call has been observed since. Classification: pre-0a6fd1e7residue; the gate is zeroexecute_stagecalls over the next natural backfill runs.Internal::QASamplingRecoveriesController/Internal::ConversationTranscriptionReconciliationsController: 2,171 US + 1,002 EU, last20:00:03Z, before the20:08:04Zv21 cutover; all empty-heartbeat polls. Classification: pre-cutover residue, already ageing out of the 24h window (Rails-zero clock still runs from20:00:03Z).- Live dependencies (unchanged, tracked in their own sections): direct-upload proxy, call recording attach, Unit21, counters, expiry.
Decision: hold QA at 100%; keep the zero-window reversible (no controller
removal until the 24h window is clean of both families and the next natural
backfill run produces zero execute_stage calls). The post-release natural
QA cycles must be read against the same gate — candidateFailures=0,
transcriptionFailures=0, unprocessed=0, stoppedEarly=null — before the
release is treated as steady for this track.
New Rho transcription blocker (2026-09-20 00:19Z): duplicate concurrent
jobs. Call detail 554173 / conversation_PzJ7xaQR38vU8wNxWY3AwL48 ran
four simultaneous conversation_transcription_call jobs with four different
runIds at 00:19Z. The BullMQ job id is
conversation_transcription_call_<runId>_<callDetailId>, so distinct runIds
defeat the job-id dedup by construction. runId is derived as
sha256(<callDetailPrefixId>:<updated_at µs>)[0,16] at every enqueue site
that computes it (recording attach, the QA stage hand-off, the retry write
path, which also bumps updated_at before deriving), and the reconciliation
sweep mints a random 8-byte token per claim; Rails-originated enqueues through
/internal/conversation_transcriptions/calls/enqueue pass their own runId.
Any two of those sites (or the same site observing a fresh updated_at)
therefore enqueue independent jobs for the same call. Which four sites fired
here is not established from this track (no worker log access) — treat the
per-site attribution as open. This is exactly the new-runId duplicate risk
called out on ENG-3279 when #12572 was closed; the call-detail-keyed admission
check from that PR (already_queued when any waiting/active/delayed/
prioritized job exists for the call, regardless of runId) was not merged and
is not on 0a6fd1e7. Decision: conversation_transcription_state_native
stays at v6 exact Kuda + Qonto with Rho off; Rho does not re-enter the
canary until (a) worker logs attribute the four enqueues, (b) a reviewed
call-keyed dedup lands across all enqueue sites (reopen the ENG-3279 scope or
a successor, human decision), and (c) a fresh clean Rho canary follows. No
replay, no write, no flag change from this track.
Reviewed call-keyed dedup for the 554173 blocker above — ENG-3286 /
#12589 (gate (b)). Call detail
554173 (conversation_PzJ7xaQR38vU8wNxWY3AwL48 /
conversation_part_call_detail_DGmPVlYzWn9ivZkxOg0de26J) was processed by
four distinct runIds on the shared conversation-transcription-call queue at
the same time: 12f2c6de4d598e48,
provider-legacy-zendesk-38efa12e-0fc9-460c-8504-3a8648732b50,
d4ca1de97276f6da, and
provider-zendesk_reconciliation_ticket_19_3_285644_01a0bc1c-e8a7-73f6-b7c1-3ecaea393ddd.
Rails logged four context loads and four persisted/transcribed outcomes.
Read-only DB: one clean final row, 25 transcript messages, no failure/error,
attempt_count 0, credit_usage_count 0, transcribed_at 00:19:26.343Z.
This is redundant provider execution and cost, not duplicate stored messages;
no replay or write occurred. Cause: the job id is deliberately per run
(conversation_transcription_call_${runId}_${callDetailId}, because completed
ids are retained 24 h and a stable id would swallow legitimate
re-transcription), and the only cross-run guard was the
transcript_available || transcription_failed read in run.ts, which four
concurrent workers all passed before any of them persisted. Decision / fix:
ENG-3286 — a bounded Redis claim per stable call (org slug + callDetailId,
SET NX PX, 15-min TTL above the 10-min provider timeout, compare-and-delete
release) acquired in runConversationTranscriptionCallJob before the context
load and provider call for both owners; a job that finds the claim held throws
a distinct error so the unchanged attempts: 3 / 60 s exponential retry
re-runs it, at which point the ordinary already-transcribed check returns
already_transcribed. Redis-claim errors fail open (provider parity over
dedup). Single queue, the 400/hour aggregate limiter, live/bulk lanes, job ids,
retry shape, and the existing backlog are unchanged; no DB lock spans the
claim. Real-Redis/BullMQ concurrency coverage reproduces the four run ids
above. Reducing the number of upstream enqueue sources is out of scope here.
Transcription widening stays held (v6 exact Kuda + Qonto) pending review and
deploy of this fix plus a fresh Rho canary; #12571 Unit21 stays separate and
review-gated; nothing merged.
Direct-upload processing: canary re-enabled for exact Kuda, no post-fix
proof yet (authoritative PostHog readback 2026-09-20). Flag 281904
api2_conversation_upload_native_processing is ACTIVE at v3, exact Kuda
only, updated 2026-09-19T20:54:19.918961Z (a human re-enable after the
parser fix #12555; this automation made no flag change). Its last_called_at
is still 19:16:11.440Z, i.e. no natural direct upload has evaluated the
flag since it was re-enabled, so there is no post-fix outcome — positive or
negative — to cite. Decision: hold at exact Kuda, no widening, no replay,
until a natural Kuda batch completes cleanly under v3. The paragraph below is
the pre-20:54Z history and is kept for the record. Earlier text describing
this flag as “not active” / “disabled” was stale as of 20:54:19Z.
api2_conversation_upload_native_processing (PostHog flag id 281904) was
created for exact Kuda at 18:19:08Z. The first natural upload, 649840,
failed terminally at 18:20:30Z with Invalid recorded_at, and the flag was
disabled immediately (v2). Read-only evidence: the existing Kuda payload
format 2026-09-19 19:20:22 (space separator) is accepted by the Rails parser
but rejected by the API2 parser. Upload 649840 then recovered on its own
through the normal scheduler after the flag rollback (read-only evidence):
completed at 18:33:01Z, delivery 11387 completed, sync delivery 11388
completed, conversation 6890594, exactly one recording attachment — no human
replay and no production write. Decisions: flag stays disabled pending review
and deploy of the parser fix
#12555 and a fresh
clean natural Kuda batch afterwards. (Superseded at 20:54:19Z by the v3
re-enable recorded above; the hold on widening stands.)
Expired-evaluation sweep: native port prepared (2026-09-20). Fresh
read-only evidence: Internal::ExpiredEvaluationsController#process_all is
exactly 48 successful calls / 24h (US 24, EU 24), one per hour per region;
post-release since 23:45Z exactly 4 calls (US 2, EU 2), latest
2026-09-20T01:05:00.180Z US / 01:05:00.309Z EU; at the fixed
01:18:53Z cutoff still 48/24h (24 US + 24 EU), both regions last
01:05Z. Rails internal effects at that cutoff: 5,007/24h (US 3,234 + EU
1,773) and 328,830/7d (US 132,073 + EU 196,757). 24h by family: 3,009
reconciliation + 794 recording + 741 direct upload + 144 Unit21 + 129 counter
cache + 101 QA stage + 48 expiry + 32 transcription context/write + 9 tiny
tail (field values 4 + report actions 4 + Slack 1) = 5,007; EU split
reconciliation 950 + recording 743 + counters 56 + expiry 24 = 1,773. (Same
cutoff, for the record: reconciliation 3,009 last
20:00:03Z/20:00:01Z and zero QA sampling — residue; execute_stage 101 US
last 21:20:46Z — residue; QA v21 active at 100%, last_called_at
00:45:01Z. #12571 merged 01:19:47Z and dedup #12589 merged 01:20:05Z;
release-prod 35c5dce7 contains both but is not yet proven live, so Rho
transcription stays off at v6 exact Kuda + Qonto until it is, and a bounded
fresh natural canary is explicitly advanced by a human.) Later read-only
refresh: release-prod 017b86ef fully deployed/COMPLETED on US/EU API2
web 1/1 and workers 5/5. Rails /internal 4,749/24h and 327,254/7d;
the post-release window 01:48:40Z → ~02:23Z holds exactly 10 calls:
Unit21 4 US, expiry 1 US + 1 EU, field-value upsert 4 US. 24h composition:
reconciliation 2,845 (last 20:00Z, empty-heartbeat residue), direct upload
741, Unit21 144, QA stage 101, counter cache 129, recording 696, expiry 48,
transcription context/write 32, field values 8, reports 4, Slack 1. QA flag
280844 active v21 (five exact canaries + 100% group) since 20:08:04Z,
last_called_at 01:45:02Z. Deletion claims for every family stay gated on
the 24h/7d windows, not on this post-release slice. This is not residue: API2
already owns the hourly :05 BullMQ scheduler
(mark-expired-evaluations-failed-hourly) and the Flipper kill switch
pause_api2_expired_evaluation_cleanup, but runExpiredEvaluationCleanup
still delegated the whole sweep to Rails via processExpiredEvaluationsInRails,
so every hourly tick is one live Rails round-trip. Decision: port both markers
natively in one focused package (Linear ENG-3289, PR below; review-required,
unmerged, no deploy, no flag change, no data mutation):
src/lib/expired-evaluations/native.tsreproducesExpiredEvaluationsFailureMarker(snapshotsrunning/pendingwithcreated_at < now() - 1 hour;processing_status = 'failed'; conversationevaluation_state = 'failed'only when the snapshot is still the conversation’s most recent) andExpiredQAEvaluationRequestsFailureMarker(stalepending/readybyCOALESCE(scheduled_for, created_at), staleprocessingbyCOALESCE(evaluation_started_at, updated_at); source-AI pending and ghost-completed agent evaluations failed first, parent score recomputed; thenfailNativeRequestwith the Rails timeout messageQA evaluation request timed out after 1 hour, which writes the terminal state,completed_at,api2_terminal_failure {retryable:false}and the durableqa_request_failure_effectsoutbox row whose post-commit delivery carries sampling/queue-slot release,qa_evaluated_atclearing, ActionCable broadcast, requester mail and backfill replacement). Batching is by primary key infind_each-sized (1,000) pages; every row fails in its own short transaction; per-row errors are logged and isolated;candidates/failedcounts match the Rails shape. Therequest_completed/failureworkflow event is emitted through the API2 PostHog workflow-event path (Sentry reporting is the documented difference: API2 logs a structured warning per timed-out request instead).src/lib/expired-evaluations/sweep-lock.tstakes the same advisory lock asExpiredEvaluationSweepLock(expired_evaluation_failure_sweep, keyed[crc32(name) & 0x7fffffff, 0]exactly likewith_advisory_lock), as a session-levelpg_try_advisory_lockon a dedicated connection with no open transaction, released infinally. Contention throwsExpiredEvaluationSweepContentionError, and the BullMQ scheduler now retries 5 attempts, fixed 10 minutes to mirror the Rails jobs’retry_on ContentionError, wait: 10.minutes, attempts: 5.- Rollout is per organization on a new PostHog flag
expired_evaluation_cleanup_native(off/fail-closed = Rails). Each hourly tick runs the native pass under the shared lock for owned organizations, then — only if stale rows for non-owned organizations exist — releases the lock and calls the Rails controller (which takes the same lock), so Rails stays a live, exercised fallback with no overlap. Rollback layers: unset the PostHog flag (native pass becomes a no-op, Rails handles everything on the same tick); or enablepause_api2_expired_evaluation_cleanupand re-enable the two retained Rails GoodJob crons.
Gates before any retirement claim for this family: the PR is human-reviewed
and deployed; the flag is widened organization by organization with each
natural :05 tick showing native snapshots/requests counts and
deferredToRails = 0 for owned organizations; process_all drops from 48/24h
toward zero as ownership widens, with no lock-contention retries exhausted;
then a reversible zero-window (Rails controller and crons kept, flag at all
organizations) before the Rails markers/controller are removed.
Cross-track state at 2026-09-20T04:06Z (release f990fc4999). The
release was fully live at 03:44:40Z–03:44:42Z (US and EU web 1/1,
physical workers 5/5). It carries the Unit21 accelerator, which has no
natural webhook proof yet. The 04:05Z natural expired-evaluation sweep was
clean in both regions — US 4/4 and EU 4/4 snapshot candidates handled
natively, zero deferredToRails, zero Rails calls, zero errors, and an exact
eight-row reconciliation against the Rails markers — so the expired-evaluation
PostHog flag 282005 was advanced by this authorized automation at
04:06:20.993Z to v2: exact Easypay + Rho + NALA plus a 50%
organization group; next gate is the 05:05Z tick. Counter-cache: the Flipper
api2_counter_cache_native_repairs row is absent in both regions (native off,
Rails still repairing) and the region-scoped PostHog gate is the focused
#12615 /
ENG-3299 . Legacy image edge
package #12613 (head
401c2d9e0, fully green after the strict drift guards: apply is idempotent
only against the exact managed origin/behavior; rollback refuses to delete
anything it does not own) was open and unapplied at that point. Update
(human merges, not automation): Chidi merged #12613 at 03:56:15Z
(a00a3bf436) and #12615 at 03:56:26Z (8f84f67350), both with no recorded
GitHub review despite Ronald being requested. Both are in release-prod
39f11065e: US already completed the intermediate 8cfa8d65dd containing
both (web TD1669 1/1, worker TD1659 5/5); EU completed 39f11065e at
04:14:36Z (web TD1687 1/1, physical worker TD1676 5/5, one deployment each,
rollout COMPLETED, zero pending). Activation status stays explicit:
the CloudFront image behavior is unapplied in both regions and the
counter-cache PostHog flag is not created, so both runtime paths remain on
Rails; the required Ronald/human activation review is still outstanding before
either cutover. The 03:30:00Z gateway and image census above is unchanged.
The only automation mutation in this slice was the bounded PostHog flag 282005
v2 advance; no scheduler, CloudFront, data or deploy change.
QA ladder state. qa_sampling_recovery_native took the first ladder step
at 2026-09-19T18:19:06Z: PostHog version 12, five exact slugs retained,
organization group 10% → 50%. The first natural bucket at 50% (18:30Z)
did not meet the complete-cycle gate: Kuda (org 25) completed at
18:35:02Z with 2,155 candidates / 77 requested / 304 skipped / 1,772
throttled / 2 ignored / 55 counters, zero candidate/transcription failures,
unprocessed=0; Rho, Easypay, Nala and the percentage-selected organizations
were clean (percentage-selected again zero-candidate); but Qonto (org 33)
completed at 18:40:25Z with 5,000 candidates / 20 requested / 78 skipped /
3,634 throttled, zero failures, stoppedEarly=budget and 1,268
unprocessed. Per the ladder the flag was immediately restored to the
last-known-good five exact slugs + 10% at 18:42:15.585896Z (PostHog
version 13), with no replay. The budget-stopped run was Qonto job
qa_sampling_recovery_33_1988714. The next natural cycle under v13 completed
cleanly: Qonto job qa_sampling_recovery_33_1988715 finished at 18:52:58Z
processing the full 5,000 candidates = 14 requested + 813 skipped + 4,173
throttled, 0 failures, no stoppedEarly, 255 counters lowered; Kuda job
qa_sampling_recovery_25_1988715 finished at 18:47:13Z with 1,936
candidates / 41 requested / 219 skipped / 1,674 throttled / 2 ignored, 0
failures. The second post-rollback cycle was also clean and complete: Qonto
job qa_sampling_recovery_33_1988716 finished at 19:06:55Z with the full
5,000 = 12 requested + 120 skipped + 4,868 throttled, 0 ignored, 0
candidate/transcription failures, no stoppedEarly, 254 counters; Kuda job
qa_sampling_recovery_25_1988716 finished at 19:02:29Z with 1,810 / 39 /
153 / 1,617 / 1 ignored, 0 failures. On two consecutive complete Qonto cycles,
PostHog flag 280844 was advanced conservatively at 19:07:38.696511Z to
version 14: same five exact slugs + 25% group (not a direct retry of
50%), server-only active. The 19:15Z bucket under v14 passed: Qonto job
qa_sampling_recovery_33_1988717 completed at 19:22:01.699Z with the full
5,000 = 5 requested + 86 skipped + 4,909 throttled, 0 ignored, 0
candidate/transcription failures, no stoppedEarly, 255 counters; Kuda job
qa_sampling_recovery_25_1988717 completed at 19:18:22.061Z with 1,756 / 32
/ 161 / 1,562 / 1 ignored, 60 counters, 0 failures, 0 unprocessed; Easypay
228 / 0 / 50 / 178 / 0 ignored, 15 counters, 0 failures, 0 unprocessed; all
additional percentage-selected tenants completed with zero candidates and zero
failures. The flag was then advanced to the same five exact slugs + 50%,
server-only active — reported as version 15 at 19:22:39.140626Z and
version 16 at 19:22:47Z (two writes eight seconds apart, same
definition). The first 50% 19:30Z bucket was interrupted by a deployment,
not a load failure: Qonto job qa_sampling_recovery_33_1988718 ended at
19:34:02.454Z with 5,000 candidates, 5 requested + 93 skipped + 2,776
throttled = 2,874 processed, 0 candidate/transcription failures,
stoppedEarly=shutdown, 254 counters; native-run logged reason=shutdown,
processed=2,874, unprocessed=2,126 at 19:33:59.918Z. Kuda job
qa_sampling_recovery_25_1988718 had already completed clean at
19:33:03.858Z: 1,732 candidates, 18 requested, 126 skipped, 1,587 throttled,
1 ignored, 61 counters, zero failures/unprocessed; Easypay was clean (237 / 0
/ 59 / 178, 15 counters, zero failures/unprocessed). Cause: #12557 and release sync #12558
merged at 19:26:32Z / 19:26:44Z, and the release-prod 8ec16b8d
(8ec16b8d858d3047745e265e8829749b779baf73) rollout
replaced the EU worker fleet mid-sweep (EU worker TD 1661 began 19:31:56Z,
all five new tasks started 19:32:17Z, old tasks stopped 19:33:34Z /
19:33:46Z / 19:33:57Z). Per the ladder the flag was rolled back
immediately to the known-good five exact slugs + 25%, server-only active,
exact targeting preserved — reported as version 17 at 19:34:47.873628Z
and version 18 at 19:36:30.489390Z (same definition). No replay. State
after rollback: v18, five exact slugs + 25%. Services at that time: EU web TD 1672 completed 1/1; EU worker TD 1661
5/5 IN_PROGRESS; US web TD 1654 1/1 and worker TD 1644 5/5 IN_PROGRESS; zero
pending/failed. Verified at 19:36:57Z: 8ec16b8d is exact and COMPLETED on
all four services — US web TD 1654 1/1 (19:36:20Z), US logical worker
(physical fc-rulebase-api2-worker-pr-zy7q0) TD 1644 5/5 (19:36:14Z), EU
web TD 1672 1/1 (19:34:59Z), EU logical worker (physical
fc-rulebase-api2-worker-pr-m9780) TD 1661 5/5 (19:35:42Z); all pending 0,
failedTasks 0; no level>=50 first-slice errors. Treat this as a deployment
interruption, not a package failure, but the explicit complete-cycle rollback
gate stands: hold five exact slugs + 25% until a complete natural post-deploy
cycle has candidateFailures=0, transcriptionFailures=0, unprocessed=0,
stoppedEarly=null; do not widen during a rollout. That gate passed: the full
19:45Z post-deploy cycle (jobs ..._1988719) under five exact slugs + 25%
completed clean — Kuda 1,709 candidates / 25 requested / 139 skipped / 1,544
throttled / 1 ignored / 61 counters; Rho 7 / 5 / 2; EU cohort org 41 (Nala)
221 / 0 / 43 / 178 / 15 counters; Qonto 5,000 / 7 / 761 / 4,232 / 254
counters; across all jobs zero candidate failures, zero transcription
failures, zero unprocessed, no early stop. The flag was advanced at
19:52:38.132439Z to version 19 and reported again at
19:53:40.142567Z as version 20 — both the same five exact slugs +
50%, server-only active. The first complete natural 50% cycle (20:00Z,
jobs ..._1988720) completed clean: Kuda 4,648 candidates / 16 requested /
157 skipped / 4,468 throttled / 6 ignored / 61 counters; Nala (org 41) 324 /
0 / 37 / 287 / 15 counters; Qonto 5,000 / 4 / 106 / 4,890 / 254 counters;
and newly percentage-selected org 38 processed one candidate and requested one
evaluation — the first non-exact percentage-cohort proof. Across all jobs:
zero candidate failures, zero transcription failures, zero unprocessed,
stoppedEarly=null. The flag was then advanced at 20:08:04.328711Z to
all organizations (PostHog response version 21; likely a
same-definition concurrent v20 write), server-only active. The first complete
natural 100% cycle (20:15Z, jobs ..._1988721) completed clean at
20:23:02.661Z: Rho (org 19) 1 candidate / 1 requested; Nala (org 41) 214 /
0 / 35 / 179 / 15 counters; Kuda 1,693 / 7 / 105 / 1,580 / 1 ignored / 61
counters; Qonto 5,000 / 6 / 128 / 4,864 / 2 ignored / 253 counters. Across
all jobs: zero candidate failures, zero transcription failures, zero
unprocessed, stoppedEarly=null. A read-only Rails structured controller
census (structured event time, not ingestion time) from the 20:08:04.328711Z
100% cutover through 20:24Z found exactly zero
Internal::QASamplingRecoveriesController and zero
Internal::ConversationTranscriptionReconciliationsController calls in both
regions. Current state: v21, 100%, held; the reversible zero-window clock for
these two controllers starts at 20:08:04.328711Z. This is not an
irreversible retirement claim — the flag remains a one-write rollback to five
exact slugs + 50% on any regression. No replay. The separate
conversation_transcription_state_native v4 Rho rollback (see the
transcription section) is unaffected.
Weekend fast-track decision (2026-09-19)
Evidence. In the 24 h before this decision every Rails-owned organization
in both regions completed its QA sampling cycles with zero candidates, zero
reconciled transcriptions and zero transcription handoffs. The 3,921 / day
transcription-reconciliation residual in the 17:47:25Z census is empty
15-minute heartbeat traffic (each sweep finds nothing to reconcile), not
meaningful work. The remaining Rails QA/transcription work is therefore
already effectively idle; what is left is the operational cutover of the
flags, not new proof of native correctness.
Recommended cutover ladder (operational, reversible). For
qa_sampling_recovery_native and conversation_transcription_state_native,
keep the exact canaries and advance the organization-group rollout
10% → 50% → 100%. Between steps:
- wait for one complete natural 15-minute scheduler cycle at the new percentage (no manual replay);
- explicitly check every completed cycle summary for
candidateFailures=0,transcriptionFailures=0,unprocessed=0andstoppedEarlynull (orshutdownonly across a deploy boundary), and for transcriptionphase=persist outcome=transcribedwith noretry/permanent/not_found/already_transcribedoutcomes; - on any regression, roll the flag back to the previous percentage immediately (PostHog flag edit, no deploy) and record the version, time and reason in the runbook ledger before re-attempting.
Weekend success criteria. Zero meaningful Rails production traffic on the QA/evaluation and transcription paths (heartbeat sweeps that find no work do not count), with Rails scaled to zero behind the flag rollback: the Rails services stay deployable and the flags can be returned to Rails within one cycle. This is a runtime state, not a deletion.
Still gated. Irreversible work — deleting Rails adapters, controllers, jobs or the API2 fallback branches — remains gated on the complete 7-day zero-call window, the effects-boundary evidence and human review described in Rails shutdown gate. Nothing in this decision widens a flag by itself; each step is an operator action recorded in the ledgers.
The same gateway cutover caused an MCP outage before
#12391 restored it. The MCP
feature router registered ALL /mcp, but the composed gateway did not recognize
that registration as ownership of POST /mcp, so the wildcard fallback sent the
request to Rails and returned Rails’ 404. Route-level tests had not exercised
the composed application with the fallback enabled. Public-cutover work must
therefore prove ownership at the final application boundary, not only inside a
feature router.
Direct-upload canary regression at 2026-09-19T18:20:30Z (Kuda, rolled back)
api2_conversation_upload_native_processing (id 281904) was created as exact
kuda-pilot at 18:19:08Z. The first natural Kuda job, conversation_upload
649840 (source xcally, direct, metadata recorded_at
"2026-09-19 19:20:22"), failed terminally at 18:20:30Z with
Invalid recorded_at; the flag was disabled at v2 immediately, so every
later direct upload went back to Rails. Read-only row state: upload failed;
delivery owner=api2, state=failed, attempts=1, replay_count=0,
last_error=Invalid recorded_at.
Root cause: parseLocalDateTimeInZone in
rulebase-web/rulebase-api2/src/lib/conversation-uploads/native-processing/xcally.ts
accepted only YYYY-MM-DDTHH:MM[:SS] (optional T, optional trailing Z),
while Rails (ConversationUpload::Processors::Xcally#recorded_at,
ActiveSupport::TimeZone#parse after stripping Z) accepts the
space-separated local time XCALLY actually sends. The fix makes the native
parser accept T or whitespace as the separator (and surrounding whitespace),
with the same organization-local-time semantics, covered by a unit test and by
the production-shaped PGlite XCALLY test using the space-separated form.
No human replay or write was made. Upload 649840 nevertheless recovered
automatically: the normal 18:30Z conversation-upload tick saw it as
retryable and enqueued it after the flag rollback. The API2 worker recorded
Rails 504s at 18:31:02Z (retry_direct) and 18:32:05Z
(process_direct); Rails logged retry_direct 500, process_direct 500,
then process_direct 200 at 18:33:01Z. Read-only state afterwards: upload
completed at 18:33:01Z; process delivery 11387 owner=api2,
state=completed, attempts=1, replay_count=0, last_error=null,
failed_at=18:20:30Z, completed_at=18:33:01Z; sync-completion delivery
11388 completed; conversation 6890594 is the only conversation for the
upload, with one part, one call detail, and one recording attachment. A
read-only US census at 18:56Z for unique_id 1789841918.162363 confirms
no duplicates: conversations=1, parts=1, call_details=1,
recording_attachments=1, upload_links=1 (delivery row last updated
18:33:01.149Z, last_error=NULL). The earlier customer-recovery decision
is therefore closed without any human replay or production write.
That recovery exposes a contract question rather than a data problem: a
terminal native (owner=api2) failure was picked up by the periodic retry
tick and handed to Rails once the flag was off. Whether this cross-owner retry
needs a durable guard is tracked separately from the parser fix (see
ENG-3267 ); the parser PR stays
scoped to the whitespace separator.
The Kuda canary was re-enabled by a human at 2026-09-19T20:54:19.918961Z
(flag 281904 v3, exact Kuda only) after the reviewed fix reached
release-prod. As of the 2026-09-20 PostHog readback last_called_at is
unchanged at 19:16:11.440Z, so no natural Kuda batch has yet exercised the
fixed parser; the bounded single-organization gate (one clean natural batch,
no widening, no replay) is still open. This automation did not change the
flag.
Counter-cache repair goes native (2026-09-20, ENG-3288 )
Read-only census: the rolling 24 h ending 2026-09-20 holds exactly 129
successful Internal::CounterCachesController#fix calls (US 73, EU 56); the
post-release window since 2026-09-19T23:45Z holds 17 (US 10, last
00:06:48.966Z; EU 7, last 00:00:29.298Z). The earlier 21:10Z / 21:53Z
censuses reported the same family at 128. The fixed cutoff
2026-09-20T01:18:53Z confirmed it unchanged at 129 (US 73 / EU 56) inside a
24 h Rails-effects total of 5,007 (US 3,234 + EU 1,773; 7 d 328,830 = US
132,073 + EU 196,757), alongside Unit21 144 (last 01:10Z, now merged as
#12571) and expiry 48 (last 01:05Z); the rest was pre-cutover residue. Full
24 h breakdown: 3,009 reconciliation + 794 recording + 741 direct upload +
144 Unit21 + 129 counter cache + 101 QA stage + 48 expiry + 32 transcription
context/write + 9 tiny tail (field values 4, report actions 4, Slack 1).
After Unit21 this is the largest verified live non-QA Rails effect. Reviewed
release-prod 35c5dce7 contains #12571 and #12589 but was not yet proven
live at that cutoff.
Mechanism: API2 already owns the 3-hourly counter-caches-tick scheduler,
the fix_counter_caches session-level advisory lock, per-step resume
(completedSteps) and the pause_api2_counter_cache_repairs Flipper kill
switch, but runCounterCacheFix still posts each of the seven
COUNTER_CACHE_STEPS to Rails via fixCounterCacheStep. Rails runs
counter_culture_fix_counts for six models and
EvaluationSamplingRuleCounter.fix_agent_counter_caches! for the seventh,
all under ActsAsTenant.without_tenant.
Native replacement (rulebase-api2/src/lib/counter-caches/native-steps.ts
and sampling-rule-counters.ts), behind the deployment-scoped PostHog gate
api2_counter_cache_native_repairs (owner platform engineering, created
2026-09-20, review/remove by 2026-12-19;
ENG-3299 ). API2 evaluates the
gate with the deployment region (AWS_REGION, falling back to
RULEBASE_REGION) as distinct id api2-deployment:<region> and a region
person property, cached per process for 20 seconds;
unknown, disabled, or unreadable flags keep every step on Rails. The
pause_api2_counter_cache_repairs Flipper switch remains the independent
rollback lever.
Gate blocker (read-only, 2026-09-20T03:38Z): no flipper_features row for
api2_counter_cache_native_repairs in US or EU, so native repairs were disabled
in both regions; the key moves to PostHog (ENG-3299 )
and is removed from Flipper use so no duplicate key exists. A per-organization
canary was rejected because the sweep is global: org-filtered native steps
would run alongside a Rails sweep that still covers every organization,
doubling work without removing a single Rails call until 100%. No
Flipper/production mutation by Devin.
Counter-cache regional rollout
Create the PostHog flag api2_counter_cache_native_repairs with a release
condition for one deployment region (region = eu-central-1 or
region = us-east-1) at 100%, with no percentage cohort. Wait for one
three-hour tick; verify counter cache fix completed with zero
Internal::CounterCachesController#fix calls in that region and unchanged
counter parity. Then add the second region and repeat the same verification.
Rollback by disabling the PostHog flag (the next run returns to Rails) or by
enabling the Flipper pause_api2_counter_cache_repairs switch.
- Six
counter_culturesteps: one autocommitUPDATE target SET col = cnt FROM (SELECT id, COUNT(child.id) … GROUP BY id) WHERE col IS DISTINCT FROM cntper counter (gem 3.11.2 reconciler semantics): discarded children excluded where the child model includesDiscard(employees, sheet rows), unresolved-only formemberships.assigned_conversations_count, andGroupEmployee’sgroups.employees_count(see the defect note below). Noupdated_attouch, matching Rails. evaluation_sampling_rule_counters: agent counters scanned in Rails’updated_at DESC, id DESCorder in batches of 1,000; onlyfixed_per_period+partition_by = agentrules; successful AI evaluations (active completed or pending) fromticket_closed/scheduledrequests in the counter’s organization-local day/week/month period, with brand/group filters on the conversation and the team filter onqa_agent_evaluations.team_id; result clamped torule.count. One deviation: the write isUPDATE … WHERE consumed_count = <loaded value>instead of Rails’ absoluteupdate!, so a concurrent consume/release is left for the next sweep instead of being clobbered.- No transaction, row lock, or external work spans statements; the advisory lock remains session-level on its own connection.
Validation: PGlite suites against the exact structure.sql tables cover
parity, idempotency, discard/status filters, organization-timezone period
bounds, caps, >1,000-counter batching, the optimistic guard, and two-tenant
sweeps; run-loop tests cover gate off, gate on (zero Rails calls), gate read
failure (Rails), and native-step failure with completedSteps resume.
Newly discovered defect: group_employees clobbers direct counts (2026-09-21, #12679 )
GroupEmployee declares counter_culture :group and
counter_culture %i[group parent_group], both on groups.employees_count,
so the incremental callbacks count each membership toward its group and
that group’s parent. The reconciler (counter_culture_fix_counts, gem 3.11.2
Reconciler#update_count_for_batch) iterates the two definitions in order and
writes column = count for each: the parent-group pass runs last and sets
every leaf group to 0 and every parent to its child-group count only. The
native step shipped in #12595 reproduced those two ordered writes exactly, so
the defect exists on both sides of the boundary; Rails has had it for as long
as the repair cron has existed, and API2 would inherit it on activation.
#12679 (found by Devin Review on an earlier revision, no recorded human review
on #12615) replaces the two CounterCacheUpdates with one aggregate write per
group, COUNT(ch.id) over memberships whose group_id is the group itself or
one of its immediate children, matching the incremental definition rather than
the Rails reconciler. native-steps.db.test.ts asserts a parent with 1 direct
- 2 child memberships ends at 3 and leaf groups with direct members keep their
count. Consequences for rollout: on the first native
group_employeestick,groups.employees_countwill increase for leaf groups and parents with direct members relative to the Rails-repaired value — that is the correction, not a parity regression, and the per-steprowscomparison in the regional runbook should expect a one-time non-zero delta on this step.
Rollout: follow the regional PostHog runbook above after review and deploy,
compare the next tick’s per-step rows against the pre-cutover Rails cadence,
then the other region; disabling the flag is an immediate Rails rollback.
Expiry (~48/day) is a separate later package.
Exact residual census at 2026-09-19T21:10:29Z and the Unit21 sync decision
Rails /internal effects: 6,202 / 24 h (US 3,697, EU 2,505) and
336,317 / 7 d (US 133,669, EU 202,648). The large 24 h families are
residue or idle rather than live: transcription reconciliation 3,700 (last
seen 20:00:03Z, empty heartbeats; QA sampling became 100% at 20:08Z),
call-recording attachment 1,332 (last 18:41:47Z), direct upload 741
(last 19:18:11Z), QA execute_stage 78 (last 18:20:59Z), transcription
context/write 22 + 22 (last 17:10Z). The largest unquestionably live
families were Unit21 sync 144 / 24 h (still firing through 21:10Z),
counter-cache fix 128 / 24 h (through 21:05Z) and expired evaluations
48 / 24 h (through 21:05Z). Post-cutoff confirmation: Rails received two
more successful sync_unit21 calls in US at 21:20:00Z and 21:30:00Z (none
in EU). Recount around 21:40Z: 6,079 / 24 h (US 3,670, EU 2,409) and
334,979 / 7 d (US 133,365, EU 201,614); still-live recurring families
unchanged (Unit21 144 / day through 21:30Z, counter-cache 128, expiry
48); no Kuda upload after 649843; Rho call detail 554172 attached
natively at 21:21:10Z with transcriptionQueued=true, but no
conversation-transcription telemetry ever ran; after 30 one-minute polls the
call-summary job completed at 21:51:10Z as status=skipped,
reason=transcript_missing_at_deadline, and QA request 5985771 stayed
pending/waiting-for-transcription through at least 21:55Z. The shared
transcription queue showed burst processing only through 21:22Z and carries
the same 400/hour ceiling as Rails, so this is not yet attributable to the
restored transcription-state handler; no rollback, replay or write was made,
but both Rho recording and transcription canaries are held and widening is
blocked pending attribution (outside this package). Recount at
2026-09-19T21:53:42Z: 6,006 / 24 h (US 3,639, EU 2,367) and 334,624
/ available 7 d (US 133,251, EU 201,373): empty reconciliation heartbeats
3,583, recording pre-cutover residue 1,230, direct-upload residue 741
total (739 process_direct = 729 × 200 + 10 × 500, 1 retry_direct 500,
1 mark_failed 200), Unit21 144 (live through 21:50Z, four
successful post-21:10:29Z calls), QA stage 103, counter-cache 128,
expiry 48, transcription context/persist 20, tail 9. Holds unchanged:
direct processing exact Kuda with zero Kuda uploads since 20:54:19Z; Rho
transcription restore with zero telemetry since 21:00:53Z; recording v4
since 19:23:51Z exact-only (Rho 5 attached + 1 already-attached, Qonto 96
attached, zero percentage outcomes). Counter-cache (128 / day) is the next
candidate package once this one is stable.
At 2026-09-19T22:08:07Z conversation_transcription_state_native was rolled
back from v5 (exact Kuda + Qonto + Rho) to v6 (exact Kuda + Qonto). Read-only
queue evidence for Rho call detail 554172 (job
conversation_transcription_call_932d35dea92b5f31_conversation_part_call_detail_r4dBk5Lz0B2hLRl4bep6xZRv):
state waiting, attemptsMade 0, attemptsStarted 0, enqueued 21:21:10Z;
the shared transcription queue had active 0, waiting 1,049, failed 1,
completed 1,000, and the shared 400/hour limiter TTL was 566,961 ms. Its
call-summary deadline expired and QA request 5985771 remained waiting. The
customer work was not replayed or mutated; recovery of 554172 / 5985771
is a human decision. The 1,049-job backlog behind the global limiter is
treated as a production blocker requiring a separate reviewed, tested fix
(outside this package). That fix is ENG-3283 / #12577: one queue, the
unchanged aggregate 400/hour limiter, every new call job classified live
(unprioritized) or bulk (BullMQ priority: 10, so live leads once the
pre-existing unprioritized backlog drains), existing Redis jobs untouched.
Read-only follow-up at ~23:20Z: the Rho job for 554172 completed
naturally at 22:20:08Z and QA request 5985771 completed cleanly at
22:21:42Z, with no replay or write. Queue reads during the drain: waiting
406 / active 20 / failed 1 / completed retained 1,000, then waiting
309, all 309 kuda-tx-yesterday jobs enqueued 21:53:57Z–21:54:03Z.
Rails internal effects were 5,588 / 24 h (US 3,463, EU 2,125, down
418 from 6,006) and 332,199 / available 7 d (US 132,661, EU
199,538), still dominated by pre-cutover residue; live Rails calls after
22:13:05Z were US 7 Unit21, 5 transcription context + 5 writes, 1
expiry, EU 1 expiry, zero 5xx. Decision: #12577 remains necessary to
prevent recurrence but is not a recovery prerequisite for 554172;
transcription widening stays held until the fairness fix is reviewed and
deployed and a fresh Rho canary is observed.
Unit21 was selected. Semantic inspection of
Internal::OrganizationDataSourcesController#sync_unit21 /
#sync_unit21_status, OrganizationDataSourceSyncRun and
OrganizationDataSource::Sync::Unit21 shows a substantive, portable effect,
not a no-op: API2 already owns scheduling (listUnit21SyncCandidates, one
BullMQ job per source, pause_api2_unit21_sync Flipper switch) and only
delegates the provider work to Rails, which claims a durable run
(source-row lock, 30-minute lease, duplicate/in_progress/abandoned),
then lists Unit21 cases and alerts (POST /v1/cases/list,
POST /v1/alerts/list, page size 50, offset 1 advancing by response_count,
created_after and dispositioned_after passes from the per-source
incremental cursor minus a 5-minute buffer, 24-hour fallback), fetches missing
rules (GET /v1/rules/:id), upserts compliance_cases, compliance_alerts,
compliance_rules, compliance_events, compliance_narratives,
case_rules, alert_rules, compliance_alert_cases with redacted content,
and advances last_synced_at plus the two sync_data cursors.
The native package (ENG-3278 )
ports that whole flow into rulebase-web/rulebase-api2/src/lib/unit21-sync/native/
behind the organization-targeted PostHog flag api2_unit21_sync_native
(owner platform, created 2026-09-19, review/remove by 2026-12-18, safe state
off). runUnit21SourceSyncJob keeps the pause check, then resolves the source
by exact (id, organization_id) and the flag; any lookup or flag failure, a
missing source, or flag off keeps the existing Rails call, so Rails remains the
rollback path with no code change. Native ownership reproduces the Rails claim
(short transaction locking only the source row, run_key duplicate detection,
30-minute lease, stale-run abandonment, one retry on unique violation) and
finishes the run as synced, skipped (missing_credentials, credentials
decrypted from organization_data_sources.data in Rails order api_key,
token, u21_key) or failed (error truncated to 500 characters). All
provider and redaction calls happen outside database transactions and locks;
persistence transactions per case/alert only run DB statements. Bounds: 30 s
per provider request, 200 pages per list pass (non-retryable), BullMQ
attempts: 3 unchanged; retryable provider (5xx/network/timeout) and
redaction failures re-throw for retry, everything else is UnrecoverableError.
Intentional deviations from Rails, all fail-safe: (1) redaction failure fails
closed and retries the attempt instead of persisting raw text; (2) the
200-page cap; (3) non-numeric action_time/created_at strings parse to
null rather than Rails’ "abc".to_i epoch-0 1970-01-01. Verification:
pnpm vitest run src/lib/unit21-sync (6 files, 44 tests incl. PGlite
ownership, tenant isolation, pagination 50+3 at offsets 1/51, persistence and
replay idempotency, empty-note redaction alignment, rule_id links, flag
on/off/failure routing) and pnpm typecheck pass. No production write,
replay, flag or provider change was made; the flag is not created. Expected
removal when rolled out: about 144 Rails calls / day.
Native canary rollback at 2026-09-20T02:30Z (run-key collision, ENG-3293 )
#12571 shipped in reviewed release 017b86ef (API2 US/EU web 1/1, logical
workers 5/5). PostHog api2_unit21_sync_native (ID 282001) was created at
2026-09-20T02:24:44Z for exact rho-tnkw only. The 02:30Z natural Rho tick
selected engine=native and logged done, but it was a no-op: the worker’s
durable key unit21-sync-source-53:1 collided with the already synced
organization_data_source_sync_runs row id 1 from 2026-09-15T01:00:01Z, so
the claim returned duplicate and no provider call, persistence or cursor
advance happened (organization_data_sources 53 kept last_synced_at
2026-09-20T01:51:41Z; both native cursors stayed at
2026-09-15T01:00:01/02Z). The coordinator disabled the flag immediately
(active=false, v2); the 02:40:00Z natural tick selected engine=rails and
logged synced/done but added no run row (count still 1): the route selected
Rails, yet it was a duplicate no-op and did not restore sync. Production
mutations were limited to creating the exact-Rho PostHog canary and immediately
disabling it on regression; no scheduler, Flipper, data, replay or provider
change was made.
Root cause (not native-specific): since #12136 the worker derived the key as
${job.id}:${job.attemptsMade + 1} while the source job uses the deterministic
id unit21-sync-source-<ods id> and addReplacingFinishedJob, so every
scheduled replacement is attempt 1 with the identical key. Rails’
OrganizationDataSourceSyncRun.claim returns :duplicate for the same key, so
the Rails-engine synced replies (including 02:40Z) were duplicate no-ops as
well. Read-only production evidence (coordinator, 2026-09-20 ~02:45Z) confirms
an active stale-sync incident for Rho source 53: compliance_cases 971 rows,
max synced_at 2026-09-14T20:20:00.705Z (max updated_at 20:20:00.708Z, max
provider_created_at 20:12:42Z); compliance_alerts 25,606 rows, max
synced_at 2026-09-15T00:50:12.766Z (max updated_at 00:50:12.767Z, max
provider_created_at 00:37:45Z); native cursors still Sep 15; exactly one
durable run row. Unit21 data for this source has not advanced since #12136
shipped. The documented operational rollback (Flipper pause_api2_unit21_sync
plus re-enabling the Rails sync_unit21 GoodJob cron) was deliberately not
exercised: production scheduler/Flipper mutation is unauthorized and direct
prod writes are prohibited; recovery is the reviewed/deployed fix, whose first
tick claims a fresh run and syncs incrementally from the Sep 15 cursors. The fix
(unit21SourceSyncRunKey, ${job.id}:${job.timestamp}:${attempt}, BullMQ’s
creation timestamp being fixed per job instance and new per replacement) gives
each scheduled replacement a distinct durable run for both engines while
retries keep the per-attempt shape from #12136. Re-enable gate: fix reviewed
and deployed, then one exact-Rho natural tick that adds a new run row and
advances both cursors.
Recovery proof (read-only, 2026-09-20): #12606 was human-merged and release
sync #12607 deployed b2f5094c fully across US/EU API2 web and workers (all
desired, zero failed, exact SHA, no level>=50 first-slice logs). The 03:00Z
tick with flag 282001 still off ran engine=rails under run key
unit21-sync-source-53:1789873200232:1, created durable run id 2, hit the
expected 504 then reconciled, and reached status synced at 03:02:10Z;
compliance cases rose 971→984 and alerts 25,606→25,656. The operator then
enabled flag 282001 v3 for exact Rho only. The 03:10Z tick ran engine=native,
created run id 3 under unit21-sync-source-53:1789873800107:1, synced in
0.85s with no errors, and advanced both Unit21 cursors plus last_synced_at to
03:10:00.507Z. The stale-sync incident is resolved; the flag stays exact-Rho
with no widening. Devin made no production mutation.
Webhook-triggered sync (ENG-3294)
Accepted Unit21 webhooks now enqueue a debounced source sync, coalescing bursts
onto the deterministic per-source job and requesting an after-active successor
when a delivery arrives after the active job fetched its data. Enqueue failures
still return the provider-facing 200 and are surfaced through
syncEnqueue=failed for alerting; the ten-minute scheduled tick remains the
correctness backstop. PostHog flag 282001 is active v3 exact Rho with recovery
proven (03:10Z run id 3); the 03:20Z exact-Rho native tick created distinct
durable run id 4 under key unit21-sync-source-53:1789874400080:1, synced in
0.75s with no error, and advanced last_synced_at plus both cursors to
03:20:00.372Z. Rho is the sole non-demo Unit21 candidate, so exact-Rho is the
complete meaningful production cohort; targeting remains unchanged with no
percentage widening, and no production mutation was made by Devin. Live API2
remains b2f5094c in US/EU web 1/1 and workers 5/5, with zero pending or
failed instances. Flag 282001 v3 exact Rho stayed active; the second consecutive
natural native tick at 03:20Z completed in about 0.75s with no error. Since the
02:59:06Z deploy boundary, the only Unit21 Rails calls were the 03:00 recovery
run (one sync_unit21 200 plus 15 status polls), with zero Unit21 Rails calls
after flag enable at 03:02:52Z. Hold exact Rho with no widening; no production
mutation was made by Devin. This package changes no flag or engine-selection
behavior.
Deployment (2026-09-20): #12608
merged at ba712b982 and shipped through the human release sync
#12612 as release-prod
f990fc4999c47ccbffb60501b0b56d6b41c1f128; Flightcontrol marked the US and EU
deployments successful at 03:44:40–42Z (API2 web TD1667/TD1685 1/1, workers
TD1657/TD1674 5/5, exact FC_GIT_COMMIT_SHA=f990fc4999, all rollouts
completed, zero pending). No natural native_unit21_webhook event has been
observed yet, so the accelerator is not proven: the first accepted Rho
webhook must show syncEnqueue followed by an immediate durable native run
before any claim. Flag 282001 stays exact Rho; no merge, deploy trigger, or
production mutation by Devin.
QA sampling 7 d Rails-zero gate check at 2026-09-21T20:10Z (not yet reachable)
Verdict: the 7 d zero-Rails-QA-sampling window cannot be complete yet.
The last Rails Internal::ConversationTranscriptionReconciliationsController#create
call recorded above is 2026-09-19T20:00:03Z (before the v21 100% write at
20:08:04Z), so the earliest instant a clean 7 d window can close is
2026-09-26T20:00:03Z. At this check about 48 h of the window have
elapsed. Do not remove the Rails QA-sampling or reconciliation path before
that date, and only after a fresh CloudWatch census confirms zero calls to
both /internal/conversation_transcription_reconciliations and
/internal/qa_sampling_recoveries in both regions across the full window.
What this session could and could not verify. No AWS credentials were
available, so the CloudWatch census was not refreshed here; the
20:00:03Z last-call time is carried from the earlier authoritative census.
As read-only corroboration, Sentry rulebase-api production http.server
transactions (trailing 14 d) show the last sampled
Internal::ConversationTranscriptionReconciliationsController#create at
2026-09-18T21:15Z and no sampled transaction since. Sentry traces are
sampled and already missed the 2026-09-19 tail that CloudWatch recorded, so
this is consistent with zero but is not gate evidence; the gate remains
CloudWatch-only.
Correction of a false-positive dispatch. A brief measured 2,671 US /
1,234 EU reconciliation calls over a 72 h window and dispatched a native port
of the endpoint as “the largest remaining /internal workload”. That window
straddled the cutover and counted decaying pre-20:08Z volume; the package
was already native and at zero. The port was stood down before any PR was
opened; nothing merged. Note for the removal step: the rails ownership mode
in rulebase-web/rulebase-api2/src/lib/qa-sampling-recovery/rails-run.ts
still calls both Rails endpoints over HTTP, so a rollback of
qa_sampling_recovery_native would resume Rails traffic and restart the 7 d
clock. Removing the Rails path therefore also means removing that fallback
mode, not only the Rails controllers.
QA/evaluation residual census at 2026-09-22T23:23Z and the churn-enum poison-row root cause
Fixed 24 h Rails /internal QA/evaluation residuals (CloudWatch, operator
census ending 2026-09-22T23:23Z). Expired-evaluation process_all 24 US
/ 0 EU; transcription reconciliation 3 US / 1 EU; daily report
backfill_preview/email 4 US / 1 EU; evaluation-check mark_failed
2 US (one HTTP 422); QA execute_stage 0. API2 release 05911a9b
was live in both regions; Rails 3/3.
Classification (read-only; PostHog project 63353 flag inventory, Sentry
rulebase-api errors, source on origin/main 3f6908c1).
- Expired evaluations, 24 US = exactly one call per hourly tick.
runExpiredEvaluationCleanuponly calls Rails when the native sweep deferred stale rows for an organization outsideexpired_evaluation_cleanup_native(100% foreasypay,rho-tnkw,nala; 50% general). Rails-sideQA evaluation request failed (...)events fromInternal::ExpiredEvaluationsController#process_allstop at2026-09-19T22:05Z(Sentry errors are unsampled), so the hourly Rails call has failed zero QA requests for 72 h (RULEBASE-API-5S4last seen22:05:00Z,RULEBASE-API-5WMlast seen21:05:00Z, both 2026-09-19). Inference, not yet confirmed by a row-level read: the native sweep is re-deferring the same stale snapshot rows each tick and Rails’ExpiredEvaluationsFailureMarkerrescues (Rails.logger.error, no Sentry event) the sameRecordInvalidon them. Classified as poison-row residue, not live work, with the root cause below; a CloudWatch read of the hourlyprocessed expired evaluationslog line (native.deferredToRails,rails.snapshots) confirms or refutes it. - Evaluation-check
mark_failed, 2 US, one 422. The 422 is RULEBASE-API-5TJ :ActiveRecord::RecordInvalid: Churn risk level is not included in the listraised byConversationCxRiskSnapshot#mark_as_failed!→update!(processing_status: :failed)forPOST /internal/evaluation_check_runs/7040287/mark_failedat2026-09-21T23:50:07Z(6 occurrences since2026-09-07). The nativenative_evaluation_check_run_mark_failedflag does not exist in the PostHog project, so this family is 100% Rails by fail-closed default; the non-422 call is real fallback work, the 422 is the same poison row. - Root cause (this package, ENG-3373 ):
the native customer-issue check
(
rulebase-web/rulebase-api2/src/lib/evaluation-checks/customer-issue.ts) wroteconversation_cx_risk_snapshots.churn_risk_levelwith the conversation-risk enum (none 1, low 2, medium 3, high 4) instead of the Rails snapshot enum{ low: 0, medium: 1, high: 2 }(validate: true). Every native customer-issue write therefore stored either the wrong level (low→ 2 =high,none→ 1 =medium) or an out-of-range value (medium→ 3,high→ 4). Completed snapshots are healed becauseconversation-cx-risk-completion.tsrewrites the column correctly, but a snapshot that stalls after the customer-issue check keeps the invalid value and then every Railsupdate!on it raises: Railsmark_failedreturns 422, and the Rails expired-evaluation marker rescues and logs the sameRecordInvalideach hour, so the row never leaves the stale scope. The fix writes the Rails enum (blank score →low, matchingEvaluationCheck::Checks::CustomerIssue#calculate_churn_risk_level); thebodies.db.test.tsexpectation that had codified3for a0.6score was corrected to1. No data repair is performed by the PR. - Transcription reconciliation, 3 US / 1 EU.
qa_sampling_recovery_nativeis 100% for all organizations;rails-run.tscalls the reconciliation endpoint once per organization tick whenever the owner resolves torails(PostHog unavailable/timeout or missing slug fails closed). These are fail-closed fallback residue, but they are Rails calls inside the 7 d window: the gate above is reset. The earliest clean close is now 7 d after the last of these calls, i.e. no earlier than2026-09-28T23:23Zand the exact instant must be re-read from CloudWatch. Do not remove the Rails path orrails-run.tsbefore then. - Daily report preview/email, 4 US / 1 EU.
api2_daily_agent_evaluation_report_deliverydoes not exist in the PostHog project (onlyapi2_daily_agent_evaluation_report_collectdoes), so backfill preview and email delivery are 100% Rails by fail-closed default. These are live fallback work, not residue; the native modules are complete but unrolled. execute_stage0. Retired; no action.
Operational canaries that are now safe (operator action, not in the PR).
expired_evaluation_cleanup_native→ 100% in US (EU already shows 0 Rails calls). The native sweep fails stale snapshots with raw SQL, so it drains the poisoned rows Rails cannot; expected effect is Railsprocess_alldropping from 24/day to 0 within one hourly tick. Rollback: unset the 100% condition (fail-closed returns to Rails) or enablepause_api2_expired_evaluation_cleanup; rollback criteria are anyrunExpiredEvaluationCleanupJoberror,snapshots.failed/requests.faileddiverging fromcandidatesfor owned organizations across two ticks, or any new Sentryrulebase-api2error underexpired-evaluations.- After this PR ships, a read-only census
SELECT id, organization_id, processing_status, churn_risk_level FROM conversation_cx_risk_snapshots WHERE churn_risk_level NOT IN (0,1,2)bounds the remaining poison rows; a guarded repair mapping3→1, 4→2, 2→0 (if customer_churn_risk_score < 0.4), 1→0 (if score IS NULL)is a separate, explicitly approved package. - Creating
api2_daily_agent_evaluation_report_deliveryin PostHog (off, then org-targeted) is the prerequisite for retiring the daily-report family; until then it is 100% Rails by design. Thenative_evaluation_check_run_mark_failedprerequisite is superseded: the ENG-3400 package below bakes the native transition in behind a Flipper kill switch instead.
Remaining-internal-effects census at 2026-09-22T23:23Z and the native mark_failed package (ENG-3372 )
The coordinator’s fresh 24 h census (ending 2026-09-22T23:23Z, API2
release-prod 05911a9b in both regions, Rails 3/3) left these non-QA
API2-to-Rails internal effects: counter-cache fix 9 US / 7 EU,
conversation-upload mark_failed 6 US, conversation-field-value upsert 4
US, Slack notification create 1 US, Unit21 sync 1 US. Read-only
classification (PostHog $feature_flag_called, Sentry, code on origin/main
3f6908c1a; no AWS/CloudWatch or database access in this session):
| Residual | Classification | Evidence |
|---|---|---|
Counter-cache fix (9 US / 7 EU) | Activation-transition residue, not a code gap | api2_counter_cache_native_repairs returned null (absent/off) in both regions through 2026-09-22T00:00Z and true from 02:00Z onward in both api2-deployment:us-east-1 and api2-deployment:eu-central-1; the runner falls back to Rails per step while the gate reads null/false or errors, so the 16 calls are the pre-02:00Z ticks inside the window plus any gate-read misses. Needs one clean 3 h tick per region with zero Internal::CounterCachesController calls before the Rails path is removed; no code change in this package. |
Conversation-upload mark_failed (6 US) | Meaningful live work — now owned natively for natively processed uploads (this package) | Sentry RULEBASE-API2-FH has exactly 6 production events, first 2026-09-22T15:46:29Z, last 18:04:41Z, US, job=ConversationUploads::ProcessDirectUploadJob, failure_stage=conversation_direct_upload, culprit reportTerminalFailure(lib.conversation-uploads:process). Each was a native XCALLY processing failure (Failed query: SELECT id FROM public.conversation_parts WHERE organization_id = $1 AND ((organization_data_source_id = $2 AND external_id = $3) OR (conversation_id = $4 AND external_id = $5)) LIMIT 1, org 25, ODS 28, conversations 7183787/7183761) whose terminal marking was then handed back to Rails Internal::ConversationUploadsController#mark_failed — the only Rails call left on the native processing path. The Drizzle wrapper hid the underlying Postgres error and SQLSTATE, so the failure was classified terminal by message text alone. |
Conversation-field-value upsert (4 US) | Flag/configuration hold | upsertOwnedConversationFieldValue falls back to Rails when api2_conversation_field_value_native_upsert is disabled or unreadable; PostHog has no flag by that key (nor conversation_field_value_upsert_native), so the native path in #12722 has never evaluated. Activation is a flag creation (owner, review date, exact-organization first) — outside this package and explicitly not performed. |
Unit21 sync (1 US) | Canary targeting (intentional fallback) pending confirmation | api2_unit21_sync_native is at 100% only for group rho-tnkw; one call from an organization outside the canary is the designed Rails fallback. Confirm the organization from the CloudWatch line before widening. |
Slack notification create (1 US) | Unclassified — too small to package | Callers are agent-slack-tools, agent-alert-conversation-tools and tool-catalog/builtin-handlers via sendSlackNotification; a single call carries no attribution without the request log. Leave on Rails until a native Slack sender package exists. |
Package chosen: native terminal failure marking for natively processed
direct uploads (this is the only residual whose native owner already runs
the attempt and whose remaining Rails call is a pure persistence step Rails
itself performs with update!). Code
(rulebase-web/rulebase-api2/src/lib/conversation-uploads/process.ts,
native-processing/run.ts):
markNativeDirectUploadFailedperforms the exactConversationUpload#mark_direct_upload_failed!write natively —status='failed',metadata.last_processing_error,metadata.last_processing_error_at(ISO8601),updated_at— scoped byidandorganization_id, returning whether the row still exists. The periodic retry (isRetryableIncomplete, and Rails’ own retry scope) keys offstatus='failed'plus the retry counters, so natively marked rows are picked up exactly like Rails-marked ones.failUploadcalls it only when the native processor ran the attempt (nativeEnabledfor that upload); Rails-processed uploads still go throughmarkConversationUploadFailed→mark_failedunchanged. A missing row on the native path drops the job like the Rails 404 path. The Rails endpoint, client adapter and controller are untouched (rollback = the existing organization flagapi2_conversation_upload_native_processingoff, which routes both processing and marking back to Rails immediately).- Sentry annotation parity: Rails’
report_to_sentry: falsebranch only annotated the enclosing API2 event; API2’sreportTerminalFailurenow receivessource,organization_slugandconversation_upload_idfrom the native upload row so tags and fingerprint match the Rails-path events. classifyNativeProcessingErrorunwraps Drizzle’scauseso retryability uses the Postgres SQLSTATE (08xxx,53xxx,40001,40P01,57014,57P0x,55P03,25P02retry; constraint classes remain terminal) and the persisted/reported message names the real error (DatabaseError:<sqlstate>error class). The sixRULEBASE-API2-FHevents would have carried their SQLSTATE and message instead ofFailed query.
Not in this package: the root cause of the conversation_parts lookup
failure itself (needs the SQLSTATE the fix above will now surface, or a
database read), the field-value flag creation, the Unit21 widening, the
counter-cache Rails-path removal, and the Slack sender.
Gates and rollback for this package. Deploy with no flag change. After
one full US day: Internal::ConversationUploadsController#mark_failed count
for organizations enabled on api2_conversation_upload_native_processing
must be 0, conversation_uploads rows failed natively must carry both
last_processing_error* keys, and the periodic retry must still pick them up
(periodic_retry_count increments). Roll back (disable the organization on
api2_conversation_upload_native_processing, no deploy) if any natively
processed failure lands with status <> 'failed' and no delivery-ledger
failed row, if RULEBASE-API2-FH-class terminal events per day exceed the
pre-package 6, or if a DatabaseError:* classified retryable retries past
the BullMQ attempt budget for more than 3 uploads in a day. Separately and
only as an operational canary (not code): a single-region counter-cache
parity check — one 3 h tick, zero Internal::CounterCachesController calls
in that region, native fixed/unchanged counters matching the Rails audit —
is the next gate for removing the counter-cache Rails path; rollback for that
canary is pause_api2_counter_cache_repairs on (Flipper) or the PostHog flag
off, threshold any counter drift on the audited tables.
Remaining-internal-effects census at 2026-09-23T01:20:02Z and the Kuda expired-evaluation cutover (ENG-3376 )
Census (exact trailing 24 h ending 2026-09-23T01:20:02Z, supplied by the
operator, not re-derived here). Rails /internal effects US 43 + EU 2 =
45. US: expired-evaluation sweep 24, upload mark_failed 6, field-value
upsert 4, daily-report actions 4, reconciliation 3, Unit21 1, Slack 1. EU:
reconciliation 1, report 1. Counter-cache repair and evaluation-check
mark_failed have rolled out of the window. #12821 (native upload
mark_failed) deployed in both regions at 00:56Z; the first post-deploy
slice 00:57Z–01:20Z holds exactly one US expiry sweep and zero other
internal calls. QA enum fix #12822 remains open (Ronald review pending) and is
not part of this package.
Classification of the residual families.
- Expired-evaluation sweep (24 US, 53% of the total) — flag-targeting hold,
not live Rails-only work. Flag 282005
expired_evaluation_cleanup_nativehad sat at v2 (exacteasypay,rho-tnkw,nala+ 50% organization group) since2026-09-20T04:06:20.993Zwith no further ladder step. PostHog$feature_flag_calledfor the flag over the trailing 48 h (read-only HogQL,2026-09-23T01:2xZ): the onlyfalseevaluations arekuda-pilot, one per hourly tick where Kuda had stale rows (15 deduped rows from2026-09-21T22Zthrough2026-09-23T01Z), plus a singletrueforrho-tnkwat2026-09-22T01Z; no other organization evaluated the flag at all. BecauseownsOrganizationis only consulted for organizations that actually have stale rows, every remaining USprocess_allcall is a Kuda deferral (Kuda is outside the 50% bucket), and EU had zero sweep calls in the window. Outcome side:backend_workflow_stageqa_evaluation/request_completed/failureshows zero Rails- or API2-recorded Kuda request timeouts since2026-09-19(Rails recorded 209 + 724 Kuda timeouts on 09-18/09-19, before the flag existed) and zero API2reason=timeoutevents anywhere in 7 days, so the hourly Kuda deferrals are staleconversation_cx_risk_snapshotsrows and/or a row the Rails marker rescues without transitioning — not a stream of fresh request failures. Row identity is unverified without a production read. - Upload
mark_failed(6 US) — owned by #12821; its 24 h clock starts00:56Z. - Field-value upsert (4 US) — unchanged flag/configuration hold
(
api2_conversation_field_value_native_upsertstill not created); smaller than the sweep and needs a human flag-creation decision, so not taken here. - Reconciliation (3 US + 1 EU) and reports (4 US + 1 EU) — sub-daily tails with no new outcome census in this slice; left as is.
- Unit21 (1 US) — intentional fallback outside the exact canary. Slack (1 US) — unattributed, too small to package.
Bounded activation (this automation, reversible, no data write). Flag
282005 advanced to v3 at 2026-09-23T01:32:23.151665Z: exact easypay,
rho-tnkw, nala, kuda-pilot at 100% plus the unchanged 50%
organization group; server-only, active. Basis: the native snapshot marker
was reconciled row-for-row on the 2026-09-20T04:05Z tick (US 4/4, EU 4/4),
the native request marker shares failNativeRequest with the live native QA
paths and is covered by the PGlite parity suite, and Kuda is the single
organization whose deferral produces the whole remaining Rails sweep volume.
Rollback: one PostHog write back to the v2 definition (remove
kuda-pilot from the exact list); the same tick’s stale Kuda rows then defer
to Rails again with no replay needed. Second layer: Flipper
pause_api2_expired_evaluation_cleanup on plus re-enabling the two retained
Rails GoodJob crons.
Focused code package (#12826 , review-required, unmerged).
runNativeExpiredEvaluationSweep now returns deferredOrganizations: [{ slug, snapshots, requests }] (sorted by slug) alongside the aggregate
deferredToRails, and the per-row error logs carry organizationId. The
hourly processed expired evaluations and QA evaluation requests log line
therefore names exactly which organizations forced the Rails round-trip, so
the next census reads attribution from the job log instead of inferring it
from deduplicated $feature_flag_called rows. No change to selection scopes,
row transitions, lock handling or the Rails fallback decision.
Gates before any retirement claim. Next natural :05Z ticks: Kuda handled
natively (native.snapshots/native.requests > 0 when stale rows exist),
deferredToRails = {0, 0}, deferredOrganizations = [] once the PR is
deployed, zero per-row Error marking expired … logs, and zero
Internal::ExpiredEvaluationsController#process_all calls in US. Then the
24 h zero window from 01:32:23Z and the 7-day zero window in both regions
before the Rails markers, controller and crons are removed. Widening the 50%
organization group to all organizations is a separate, human-approved ladder
step; this slice made no other flag, scheduler, deploy or data change.
Churn-enum poison rows: Rails failure-marking hardening and guarded repair task (ENG-3377 , follow-up to ENG-3373 / #12822)
State at 2026-09-23T01:50Z (operator-supplied, not re-derived). #12822
merged at 2026-09-23T01:49:31Z; release-prod d6607a6 was not yet
deployed at that time. The operator later reported the d6607a6 API2
deployment live by ~02:08Z with one clean expiry cycle (see census below);
that is the only post-deploy observation recorded here. #12825 and #12826 merged without a recorded Ronald review; nothing
here depends on them and neither was flagged, deployed or bypassed.
Evidence (read-only).
-
Sentry
RULEBASE-API-5TJ(rulebase-api, US/Ashburn):ActiveRecord::RecordInvalid: Validation failed: Churn risk level is not included in the list, culpritInternal::EvaluationCheckRunsController#mark_failed, 6 occurrences, first seen2026-09-07T02:35:44Z, last seen2026-09-21T23:50:07Z. The latest event (ead16a3476634197be8847c235162098) isPOST /internal/evaluation_check_runs/7040287/mark_failed; an earlier one at2026-09-21T21:07:45Zis run7038988. The stack ends inConversationCxRiskSnapshot#mark_as_failed!→update!, i.e. the check itself was already flipped tofailedand only the snapshot transition rejected. These are the API2→Railsmark_failed422s counted in the2026-09-22T23:23Zcensus above. -
Hourly
ExpiredEvaluationsFailureMarker(process_all) hits the sameupdate!, rescues per row and logsError marking expired evaluation as failedwith the snapshot prefix id, so a poisonedpending/runningrow stays in the stale scope and is re-attempted every tick until a native sweep (failSnapshot, raw SQL, enum-agnostic) owns the organization. -
Row-level US/EU census (operator, read-only via ECS Exec,
2026-09-23 ~02:08Z, after thed6607a6API2 deployment; no writes or replay).conversation_cx_risk_snapshots WHERE churn_risk_level NOT IN (0,1,2):- US: 6 rows, all raw level
3— org 38:1676207,1676266,1676326; org 19:1676121,1800561; org 25:1800129. - EU: 4 rows, all raw level
3— org 41:516999,517000,517001,517002. - No raw level
4row in either region. Under the pre-#12822 writer3wasmediumin the conversation-risk enum, so every poison row is a stalledmedium-scored snapshot; the repair task’s score-based recomputation is expected to yield1(medium) for each, to be confirmed per row by the dry run. processing_status,customer_churn_risk_scoreand the check correlation (query 2 below) were not part of the pasted census; run them before the repair decision and record them in ENG-3377.- Same operator read,
02:05Znatural expiry cycle on the deployed release: US one candidate / one native failure, EU zero candidates,deferredToRails = 0,deferredOrganizations = [],rails = nullin both regions. One clean cycle — not a 24 h or 7 d zero-window claim.
Census/correlation SQL for the follow-up reads (
bin/aws/ecs-shell.sh api2-prod-{us,eu}, see.agents/skills/rulebase-prod-debug/references/prod-notes.md):-- 1. bound the poison set SELECT s.id, o.slug, s.processing_status, s.churn_risk_level, s.customer_churn_risk_score, s.created_at, s.updated_at FROM conversation_cx_risk_snapshots s JOIN organizations o ON o.id = s.organization_id WHERE s.churn_risk_level NOT IN (0, 1, 2) ORDER BY s.created_at; -- 2. correlate with stuck checks / the Sentry runs SELECT c.id, c.type, c.status, c.updated_at, c.conversation_cx_risk_snapshot_id FROM evaluation_checks c JOIN conversation_cx_risk_snapshots s ON s.id = c.conversation_cx_risk_snapshot_id WHERE s.churn_risk_level NOT IN (0, 1, 2) OR c.id IN (7040287, 7038988) ORDER BY c.id;Expected shape from the code path: rows with
churn_risk_level IN (3, 4)whoseprocessing_statusisrunning/pending/failed(nevercompleted, because completion rewrites the level), all created before the #12822 deploy. - US: 6 rows, all raw level
Focused code package (review-required, no data write, no flag).
ConversationCxRiskSnapshot#mark_as_failed!writes the terminal status withupdate_columns(processing_status: :failed, updated_at:)instead ofupdate!. The transition no longer validates unrelated columns, so bothEvaluationCheck#mark_as_failed!(themark_failed422 family) andExpiredEvaluationsFailureMarker(the hourly retry loop) drain poisoned rows exactly like the native sweep does. The conversationevaluation_stateupdate is unchanged.Maintenance::RepairInvalidSnapshotChurnRiskLevelsTask(maintenance_tasks UI):organization_slug(required),max_snapshots(default 500, fails instead of truncating),apply(default false = log-only). Collection ischurn_risk_level NOT IN (0,1,2)for that organization;processtakes a row lock, re-checks the stored value, recomputes the level from the row’s owncustomer_churn_risk_scorewithConversationCxRiskSnapshot.churn_risk_level_for_score(≥0.7 → high,≥0.4 → medium, else/NULL →low) and writes onlychurn_risk_level. It never touchesprocessing_status, checks, requests or conversations. Running it in production is a separate, explicitly approved step per organization, dry-run first.- Specs: poisoned-row
mark_as_failed!on snapshot and check; task collection scoping/bounding, dry-run no-op, score-based repair, idempotence.
Rollback. Revert the PR (single Rails deploy). The model change is
behaviour-preserving for valid rows; reverting only reintroduces the 422s.
The task is inert unless an operator runs it with apply=true; a mistaken
run is reversible per row from the task log (stored_level → repaired_level
is logged for every row).
Gates.
- Outcome (first deploy tick):
Internal::EvaluationCheckRunsController#mark_failedreturns 200 for the runs Sentry listed (or they are already terminal);RULEBASE-API-5TJreceives no new event; the next:05ZRailsprocess_alltick logs zeroError marking expired evaluation as failed. - 24 h: zero new
RULEBASE-API-5TJevents in US and EU; census query 1 returns nopending/runningrows (remaining poison rows, if any, arefailedand inert); no newsnapshot.faileddivergence in the native sweep. - 7 d: census query 1 returns zero rows in both regions after the approved
per-organization repair, zero
mark_failed422s, and the Railsmark_failedfamily can then be retired on the ENG-3372 schedule. Until then the Rails endpoint stays.
Read-only ECS correlation at ~2026-09-23T02:24Z (coordinator-supplied;
no repair run, no data write). All ten churn_risk_level = 3 snapshots
are already processing_status = failed, so the invalid enum values are
historical failed-row data quality, not currently stuck work, and are
not a reason to run the repair task urgently. Any repair stays a separate,
guarded operator decision.
| Region | Snapshot ID | Organization | Score | Last update |
|---|---|---|---|---|
| US | 1676121 | Rho | .50 | — |
| US | 1676207, 1676266 | Easypay | .60 | — |
| US | 1676326 | Easypay | .40 | — |
| US | 1800129 | Kuda | .50 | 2026-09-23T02:05:00.372Z by the natural native expiry tick |
| US | 1800561 | Rho | .60 | 2026-09-22T21:24:24Z |
| EU | 516999 | NALA | .50 | 2026-09-20T04:05Z |
| EU | 517000, 517001 | NALA | .60 | 2026-09-20T04:05Z |
| EU | 517002 | NALA | .40 | 2026-09-20T04:05Z |
Each snapshot has three evaluation_checks: 28 completed and two failed
complaint checks — check 7038988 (Kuda snapshot 1800129) and 7040287 (Rho
snapshot 1800561). No pending/running poison row exists in either region,
which satisfies the 24 h gate’s “remaining poison rows are failed and
inert” condition for this read.
Native evaluation-check body ran model/embedding calls inside the DB transaction and advisory lock: two-phase fix (ENG-3414 , follow-up to ENG-3400 / #12891)
Finding (static, confirmed on origin/main 801d6b7ac against the root
AGENTS.md “Database lock safety” rule).
rulebase-web/rulebase-api2/src/lib/evaluation-checks/index.ts
tryRunNativeEvaluationCheck opened a single db.transaction, took
pg_try_advisory_xact_lock on the Rails execution_lock_key twin, selected
the check FOR UPDATE, set status = 'running', and then invoked
bodies[checkType](tx, target) on that same transaction client. Every body
awaits provider work on that client: customer-issue generateText and OpenAI
embed, complaint detection and follow-up generateText, compliance
generateText. The transaction, the row lock, and the transaction-scoped
advisory lock were therefore held for the full duration of the model calls.
Rails does not do this: EvaluationCheck#run_once wraps run in a
session-level with_advisory_lock(execution_lock_key, timeout_seconds: 0) on
a dedicated connection with no transaction open.
Production evidence (read-only, Sentry only).
RULEBASE-API2-FA:
Error: Failed query: rollback, culprit
Object.tryRunNativeEvaluationCheck [as runNativeCheck], inner cause Client has encountered a connection error and is not queryable, worker
evaluation-check-run, attempts_made 5 / max_attempts 5, 25 occurrences
from 2026-09-21T13:34:55Z to 2026-09-23T13:39:27Z, release dd914087,
us-east-1. The final event matches the 13:39Z Rails mark_failed call in
the fixed 19:30Z census (ENG-3400). Attribution of the other fourteen
mark_failed calls to this issue is inference, not per-event evidence. No
CloudWatch read was attempted: both operator AWS SSO profiles expired at
2026-09-24T01:35Z, so no fresh traffic-zero claim is made here.
Coordinator census (exact, read-only, trailing 24h to
2026-09-24T07:40:00Z). US Rails /internal 19 successful calls, of which
EvaluationCheckRuns#mark_failed 15 (latest at 2026-09-23T13:39Z, the same
final event as the 19:30Z audit; no newer mark_failed call); EU Rails
/internal 0. Both regions: zero successful API2-gateway forwards to Rails
and zero successful direct /conversation_images/:signed_id GETs. These
numbers were supplied by the coordinator’s read-only query; this track did not
re-run CloudWatch. #12891 (ENG-3400) and #12912 (ENG-3414) were open,
CI-green, labeled codex/codex-automation, Ronald requested, with no
submitted Ronald approval; neither merged nor deployed.
Package (this section’s PR). Two-phase execution, keeping Rails serialization semantics:
withRailsCompatibleExecutionLocktakes a session-levelpg_try_advisory_lockon a dedicated pool connection with the same[crc32("evaluation-check-run-<id>") & 0x7fffffff, 0]key Rails andmark_as_failed_onceuse, and releases it infinally(same shape asexpired-evaluations/sweep-lock.ts). Contention returnsalready-runningwithout touching the row. The transaction-scopedacquireRailsCompatibleExecutionLockstays for the terminal-failure path. If the acquisition query itself throws (the Sentry failure mode is a non-queryable client), the connection is released with the error sopg-pooldestroys it instead of handing it out again.- Claim phase: a short transaction re-reads the check
FOR UPDATE, returns the terminal/not-applicable short-circuits unchanged, setsstatus = 'running', and commits.loadTarget,loadStatus,isApplicableandterminalPostCommitResultare SQL reads only (audited 2026-09-24); no provider, queue, Redis or HTTP work runs inside the claim. - Body phase: the body receives the autocommit
dbclient, so no transaction, row lock, or transaction-scoped advisory lock is open whilegenerateText/embedrun. Each body’s write phase opens its own short transaction (customer-issue: issue insert + snapshot update + completion; complaint: create, thenrouteCreatedin a separate transaction because the routing pass takes apg_advisory_xact_lock(a no-op on an autocommit client), or follow-up link; compliance: results + completion), still tenant-scoped byorganization_id. Known pre-existing debt, out of scope here:conversation-routing/notify.tsposts to Slack inside that routing transaction (shared with every other routing caller). - Post-commit effects (completion and customer-issue assignment enqueues) run after the lock is released, as before.
Behavior change to review. A body exception no longer rolls status back
from running to pending; the check stays running (Rails parity) until
the terminal-failure job (fail-evaluation-check-run, ENG-3400 path) applies
failed. BullMQ retries of the same job take the session lock again and
re-run the body from a fresh claim.
Tests (src/lib/evaluation-checks/index.db.test.ts, PGlite). The body
observes txid_current_if_assigned() IS NULL, status = 'running', and the
advisory lock held in pg_locks; the lock is released after success and after
a body throw; a held lock yields already-running with the row untouched and
the pooled connection released; a failing acquisition query rejects the run,
leaves the row pending, and releases the connection with that error
(fault-injection); the pre-existing flag-off, not-applicable and
completion-enqueue cases are unchanged. Body DB tests in bodies.db.test.ts
run unchanged on the transactional client.
Gates. Land after Ronald review only; no flag, provider, deploy, replay or
production data change in the PR. Post-deploy read-only check: Sentry
RULEBASE-API2-FA receives no new events across one full US business day
and the evaluation-check-run exhaustion count returns to zero; the ENG-3400
mark_failed census then measures real terminal failures only. Rollback is
revert of the carrying commit; there is no flag.
Native evaluation-check execution owned by default; PostHog fail-closed Rails run fallback retired (ENG-3453 , follow-up to ENG-3400 / ENG-3414 / ENG-3435)
Dependency. On the deployed code evaluation-checks/index.ts returned
legacy — i.e. evaluation-check-run.ts posted to Rails
POST /internal/evaluation_check_runs/:id/run (EvaluationCheck#run_once,
body executed by Rails) — whenever (a) the per-type PostHog flag
native_complaint_evaluation_check / native_customer_issue_evaluation_check
/ native_compliance_evaluation_check read anything other than true,
including a PostHog error or null response (fail-closed), (b) the check row
was missing for the organization, or (c) the type was outside the three native
bodies. Per rollouts all three flags have been 100% since the
gate passed on 2026-09-08 22:01Z, so (a) is the same latent shape as the QA
sampling posthog_unavailable fallback fixed in ENG-3435 / #13003: a transient
PostHog failure under a 100% flag silently selects Rails. No run call
appeared in the 2026-09-22T23:23Z, 2026-09-24T07:40Z or 2026-09-25T02:15Z
censuses already recorded here; this is a latent-dependency removal, not a
live residual, and no new production evidence is claimed for it.
Decision and package. Bake the winning behavior in: native execution is
the production default and the PostHog read is removed. Rails runs the check
only while Flipper pause_native_evaluation_check_run is enabled or
unreadable (logged ownerReason=paused|flipper_unavailable, fail-closed
like pause_native_evaluation_check_run_mark_failed; owner Platform, created
2026-09-25, review by 2026-12-24, removed with the Rails endpoint). A missing
check now returns missing natively — the Rails endpoint answered missing
too — with no Rails call. A type Rails never creates (qa/dispute/sla;
ConversationCxRiskSnapshot creates exactly complaint/compliance/
customer_issue) throws so the job fails the way Rails’ check_class
constant lookup would, instead of round-tripping to Rails. rails-client.ts #runRailsEvaluationCheck and Internal::EvaluationCheckRunsController#run
stay as the rollback path. The three PostHog flags are left for operator
archival after deploy; no flag was created or changed.
Tests (index.db.test.ts, PGlite). Flipper switch read (off / boolean
gate on / unreadable → "unavailable"); paused and unavailable both yield
legacy with the row untouched; default run is native with one switch read
and no PostHog; missing id and wrong organization yield missing without a
body call; sla type rejects with the row left pending. Lock, fault
injection, not-applicable and completion cases unchanged.
Rollback / gates. Rollback is enabling the Flipper switch (no deploy).
Zero clock for evaluation_check_runs#run starts when a SHA containing this
package is COMPLETED on all four API2 services; require exact regional 24 h
then 7 d censuses with zero run calls, accounting for the remaining Rails
selectors (switch on, Flipper unreadable). Land only after Ronald review and
green required checks; codex + codex-automation labels.
QA/evaluation Rails internal-call closure matrix (2026-09-25 source audit, ENG-3465 )
Source audit of every remaining API2→Rails call in the QA/evaluation family on
main at the #13098 merge. Production evidence is only what is already
recorded above (2026-09-22T23:23Z, 2026-09-24T07:40Z / 15:40Z / 19:40Z,
2026-09-25T02:15Z censuses); no new counts and no post-rollout zero window are
claimed. #13098 is merged but no census taken after a SHA containing it was
COMPLETED on all four services is recorded here, so its deployed behavior is
unverified.
| Rails route | Owner on main | Rails still selected when | Class | Last recorded evidence | Gate / rollback |
|---|---|---|---|---|---|
evaluation_check_runs#run | API2 native (#13098) | Flipper pause_native_evaluation_check_run on or unreadable | rollback | 0 in every itemized census (latent) | deployed-SHA 24 h / 7 d zero; rollback = enable switch |
evaluation_check_runs#mark_failed | API2 native (#12891) | Flipper pause_native_evaluation_check_run_mark_failed on or unreadable | rollback | 15 US at 19:30Z (pre-package); 0 at 02:15Z | same |
conversation_transcription_reconciliations, qa_sampling_recoveries | API2 native (#13003) | Flipper pause_qa_sampling_recovery_native on/unreadable, or QA_SAMPLING_RECOVERY_OWNER=rails | rollback + env override | 8 US at 02:15Z, all before the 02:21Z rollout; 02:30/02:45 cycles native_default | 24 h / 7 d zero from 02:21Z; rollback = enable switch |
evaluations#results (full Rails writer) | PostHog native_conversation_qa_result_import, fail-closed → this package makes API2 native | PostHog false/error/null; job without organizationSlug | latent fail-closed + old-job | 0 in every itemized census (23:23Z, 19:30Z, 15:40Z, 19:40Z, 02:15Z); 07:40Z US 19 not fully itemized | this package; then deployed-SHA 24 h / 7 d zero; rollback = enable pause_native_conversation_qa_result_import |
evaluations#result_effects | PostHog native_conversation_qa_completion, fail-closed → this package makes API2 native for new runs | persisted marker completion_owner='rails' (kept), PostHog false/error/null (removed) | durable compatibility | 0 in every itemized census; 07:40Z not fully itemized | replay of existing Rails-owned markers only; remove with route once they drain |
qa_evaluation_requests#execute_stage | API2 native preparation | RAILS_STAGE_FALLBACK_REASONS (easypay_historical_backfill, backfill_run_agent, unsupported_trigger, no_sampling_plan, no_eligible_agents_without_sampling) → all five retired in the ENG-3471 package below (no Rails selector remains) | unsupported-trigger compatibility | 0 at 23:23Z and 02:15Z | ENG-3471 package: native skip or explicit terminal failure per reason |
qa_evaluation_requests#finalize_failure | API2 native terminal effects | native context (request/agent rows) cannot be loaded → never after the ENG-3471 package | missing-row compatibility | none recorded | ENG-3471 package: imported requests native, other misses retry without Rails |
qa_evaluation_requests#bulk_create / tag_bulk_import | API2 native (#12978) | never (client removed) | retired | 4 EU at 15:40Z pre-package | natural EU import outcome still unproven |
qa_evaluation_requests#send_bulk_recap | API2 native (#12991) | never (client removed) | retired | 1 EU at 15:40Z pre-package | one natural recap claim + Resend message |
conversation_imports#ensure (import rows) | API2 native (#13020, merged red/unreviewed) | never from imports; Jira legacy webhook still calls it | retired for imports | none recorded | Jira webhook accounted separately |
expired_evaluations#process_all | per-org PostHog expired_evaluation_cleanup_native + Flipper pause | organization not natively flagged | intentional per-org rollout | 24 US at 23:23Z, 6 at 19:30Z | ENG-3373 poison repair, then canary widen |
Result/finalization package (this section). conversation-qa-result-import-finalization.ts
returned legacy (Rails results) when native_conversation_qa_result_import
read anything but true, including a PostHog error or null, and recorded
completion_owner='rails' (Rails result_effects) when
native_conversation_qa_completion did the same. Both flags have been 100% in
both regions since 2026-09-08 22:01Z (rollouts), so this is the
same latent fail-closed shape as ENG-3435 / ENG-3453. qa-evaluation-result-finalization.ts
also still posted slug-less jobs to Rails: every producer has carried
organizationSlug since #11219 (2026-09-06) and finalization jobs stop after 8
attempts with 30 s exponential backoff, so no slug-less job can remain queued.
Decision: native import and completion are the production default; Rails takes
a newly accepted run only while Flipper pause_native_conversation_qa_result_import
is enabled or unreadable (ownerReason=paused|flipper_unavailable; Platform,
created 2026-09-25, review by 2026-12-24, removed with the Rails routes).
Existing completion_owner='rails' markers keep replaying to Rails
result_effects; a slug-less payload fails the job instead of guessing
ownership. postEvaluationResults / postEvaluationResultEffects and the
Rails controller stay as rollback. The two PostHog flags are left for operator
archival after deploy; no flag was created or changed.
Tests. conversation-qa-result-import-finalization.db.test.ts (PGlite):
switch read off/on/unreadable; paused and unavailable yield legacy with the
result claim untouched; default run imports, completes natively with one
switch read, and replays durably; a persisted Rails marker replays as
native-replay-rails-completion; terminal failure/skip, cross-tenant and
conflicting runs, and rollback on import failure unchanged.
qa-evaluation-result-finalization.test.ts: slug-less payload rejects before
either writer; legacy still reaches the Rails writer after hash verification.
Remaining gates before the QA family can lose its Rails routes. (1) A SHA
containing #13098 and this package COMPLETED on all four API2 services, then
exact regional 24 h and 7 d censuses with zero calls on every row above, with
the pause switches off and Flipper readable throughout. (2) One natural EU
import → requests created natively → one recap delivery claim + Resend
message. (3) execute_stage and finalize_failure compatibility branches
resolved by their own reviewed packages. (4) Expired-evaluation cleanup
poison-row repair and canary (ENG-3373). (5) public.* ownership G2–G7 remain
independent shutdown blockers.
QA execute_stage / finalize_failure selector audit (ENG-3471 , child of ENG-3347 )
Status of the previous package. #13120 (ENG-3465) was merged to main
(092227761) and synced to release-prod by a human merge with no submitted
Ronald review. No census taken after a SHA containing it was COMPLETED on
all four API2 services is recorded in this session, so its deployed behavior
and any post-deploy zero window remain unverified. Nothing in this section
adds production counts; the evidence column below repeats only what is already
recorded above.
Re-scope after Rails-web-dark (#13143). main now contains
#13143 (ENG-3347, Rails
residual web paths API2-only) and the requester reports Rails configured at
0..0 tasks. That configuration and its deployment are unverified in this
session (no ECS or census read is available here). With no Rails web target,
any surviving API2 → Rails selector is a hard failure rather than a fallback,
so this package no longer keeps any execute_stage reason on Rails: the
first revision’s “keep on Rails” rows below were dropped, the direct
execute_stage executor and rails-fallback.ts are deleted, and
preparation-effects.ts leaves the RAILS_INTERNAL_BASE_URL source-gate
allowlist. The web-dark gate retired-rails-web-routes.source-gate.test.ts
gains the /execute_stage and /finalize_failure fragments and asserts the
QA stage modules never reference the Rails origin, so neither selector can be
restored without editing the gate.
Source audit of execute_stage on main. Rails
Internal::QAEvaluationRequestStagesController#execute_stage accepted a call
only while the live api2_stage_lease token and stage matched, then ran
StageProcessor#execute_selected_stage! with transcription_owner: :api2, evaluation_owner: :api2 inside request.with_lock. API2 selected it through
five RAILS_STAGE_FALLBACK_REASONS; the revised decision per reason:
| Reason | Selected when | Class | Decision |
|---|---|---|---|
easypay_historical_backfill | user_requested row carrying stage_progress.easypay_jira_2026_backfill | historical one-off (Rails script, 2026 EasyPay Jira backfill) | fail explicitly (RetiredQaRequestShapeError, terminal retired_request_shape); nothing creates these rows anymore |
backfill_run_agent | scheduled / ticket_closed row attached to a qa_evaluation_backfill_run_agent | malformed under the current backfill runner, which creates admin_forced rows | fail explicitly with the trigger named; admin_forced backfill rows are native |
unsupported_trigger | trigger_type outside Rails’ closed enum | malformed-row | fail explicitly — Rails validated the enum and would have raised as well |
no_sampling_plan | ticket rules exist but both native resolvers return mode: "none" | impossible under Rails’ strategy enum | fail explicitly — invariant violation |
no_eligible_agents_without_sampling | zero live eligible agents and Rails had not already skipped | over-broad | native skip no_agents_sampled with Rails’ no_agents_sampled_message_for message (sampling-capacity text when a plan exists, no-coverage text otherwise), with or without ticket rules |
Retired shapes are classified retired_request_shape by
classifyNonRetryableQAEvaluationRequestError, so the job discards instead of
burning retries and failNativeRequest records the terminal failure
(api2_terminal_failure.retryable = false) the same way as
provider_invalid_json.
Source audit of finalize_failure. Rails NativeFailureEffects.deliver!
finalized failed agent rows, cleared qa_evaluated_at, released queue and
sampling slots, mailed the requester (never for imports), enqueued backfill
replacement/auto-backdate and broadcast. API2 failure-effects.ts already
delivered every ordinary failed/skipped effect natively (same slot release,
timestamp clearing, email, backfill dispatch, broadcast) and posted to Rails
only when its native context query returned no row, i.e. when the request had
evaluation_request_import_id set, was not yet failed/skipped, or its
conversation/organization row was missing. Decision: imported requests are
delivered natively with the requester email suppressed (Rails parity); a
nonterminal or missing-row effect stays in the outbox and retries on the
existing 5-minute schedule without a Rails call (Rails raised
ArgumentError/404 for the same rows). The finalizeThroughRails client and
the failure-effects.ts entry in the RAILS_INTERNAL_BASE_URL source gate
are removed. Native no_agents_sampled skips write the same failure-effect
row, so Rails mark_skipped! parity (slot release, workflow broadcast,
requester notification, backfill replacement) comes from that outbox. Known
pre-existing gap, unchanged by this package and out of its scope: neither owner
path recomputed QAAgentEvaluation#finalize_failure! side effects for
failed_agent_ids on natively delivered effects, because API2 never routed
those rows to Rails.
Tests. eligibility-stage.db.test.ts / evaluation-creation-stage.db.test.ts:
unsupported trigger, EasyPay historical and scheduled backfill-run-agent rows
reject with RetiredQaRequestShapeError before any agent lookup; no live
agents skips natively as no_agents_sampled with one failure-effect row both
with and without ticket rules. preparation-effects.test.ts: retired shapes
propagate with no fetch and no qa_stage_rails_fallback telemetry;
workflow telemetry records outcome: "failure",
error_class: "RetiredQaRequestShapeError". errors.test.ts: retired shape
→ retired_request_shape. failure-effects.db.test.ts: imported skip
delivers natively with broadcast and no email; a nonterminal request leaves
the effect pending with no broadcast, email or Rails call. Source gates:
preparation-effects.ts removed from the RAILS_INTERNAL_BASE_URL
allowlist; /execute_stage and /finalize_failure added to the retired
web-route fragments.
Rollback. Code revert only (no flag: with Rails web-dark there is no
Rails target to fall back to, and the removed branches were error or parity
paths, not traffic). Rails execute_stage / finalize_failure routes are
deletable with the QA family once gate (3) passes.
Gates. (1) Ronald review and green required checks on the package PR; no
merge before both. (2) A SHA containing #13098, #13120, #13143 and this
package COMPLETED on all four API2 services, recorded with its completion
time — unverified. (3) From that time, exact regional 24 h then 7 d
censuses (or, with Rails at 0..0, zero API2 RailsInternalApiError /
connection failures for the QA family) with execute_stage = 0 and
finalize_failure = 0 — unverified; the earliest 7-day Rails-zero gate
the requester quoted is 2026-09-26T20:00:03Z and is not claimed here. (4) Any
retired_request_shape terminal failure in production identifies a row this
audit believed impossible and reopens the decision for that reason.
QA result-import terminal effects audit (ENG-3494 , child of ENG-3347 )
2026-10-01 status correction. #13220 merged 2026-09-28T20:53:46Z as 06c02fa1371d08077f5bf5edbbb762dcc4c58404; its final-head CI passed, but GitHub still reports REVIEW_REQUIRED and zero submitted reviews. This is a review exception, not Ronald approval. Deployment and natural QA failure-effects parity were not reverified in this correction, so the outcome gate below remains open.
Status of the previous package. #13132
(ENG-3471) was merged to main as 7c39ee573 at 2026-09-27T09:56:02Z by
chidiwilliams with zero submitted GitHub reviews (verified against the
GitHub API from this session). That merge is recorded as an external merge,
not as Ronald approval; gate (1) of the ENG-3471 section was not met before
merge. Gates (2) and (3) — a SHA containing #13098/#13120/#13132 COMPLETED
on all four API2 services, and the regional 24 h / 7 d zero windows — remain
unverified: this session has no ECS, CloudWatch or production DB read,
and no census after the 2026-09-25T02:15Z one is recorded here. origin/main
at audit time was 6087ec7dc.
Natural EU import → recap outcome. Still unproven. The only
read-only evidence recorded in this plan is the requester’s 02:15Z
investigation: zero new api2.evaluation_request_imports and zero new
qa_evaluation_request_import_recap:% delivery claims since the
2026-09-24T17:45:18Z rollout. Sentry (the only observability source available
to this session) shows absence of import/recap failures, which is not a
positive outcome. No import was replayed or created to manufacture proof.
failed_agent_ids side-effect audit (source, main). Rails
Internal::EvaluationsController#results ended a native result run through
import_api2_results! → QAAgentEvaluation#mark_as_failed! →
finalize_failure! (per-agent release_sampling_slot_after_failure,
progress broadcast, QAEvaluation#calculate_overall_score!), which with no
completed agents called fail_unscoreable_evaluation_request! →
QAEvaluationRequest#mark_failed!; an API2 skip_reason went through
request.mark_skipped!. Those request transitions release consumed
sampling partitions and fixed-ticket queue slots, clear qa_evaluated_at,
broadcast evaluation_request:stage failed, mail the requester and dispatch
backfill replacement. Since #13120 made native result import unconditional,
API2 qa-evaluation-request/completion.ts is the only owner of that ending,
and its markEvaluationFailed / markRequestSkipped wrote only the
processing_status = 'failed' and status = 'failed' | 'skipped' rows —
no qa_request_failure_effects row, so none of the effects above ran for
result runs ending in errorMessage, skipReason or a missing parent score.
Every other native terminal path (failNativeRequest, eligibility /
transcription / creation-stage skips) already writes that outbox row and
failure-effects.ts delivers it. This is the concrete form of the
“pre-existing gap” noted in the ENG-3471 section; the per-agent slot release
Rails performed in finalize_failure! is covered by the request-level
release of consumed_sampling_rule_partitions in prepareNativeTerminalState
(same counters, same partitions), so no separate per-agent step is added.
Package. markEvaluationFailed and markRequestSkipped insert the
outbox row (carrying the agent IDs failed in that call, or [] for skips)
inside the same short import transaction, only when the request actually
transitions out of a non-terminal status; enqueueNativeQaCompletionEffects
calls deliverQaFailureEffect after the transaction commits, logging and
leaving a failed delivery to the existing 5-minute outbox sweep. No Rails
call, no flag, no network work inside the transaction.
Tests. completion.db.test.ts: model failure records one effect with
failed_agent_ids = ["101"] and the error message; explicit skip records one
effect with [] and the reason; an already-terminal request records no
effect. completion.effects.test.ts: post-commit delivery is invoked once
with the request key, a delivery failure is logged and does not block billing
or summary enqueues, and evaluations without a request skip delivery.
Gates. (1) Ronald review and green required checks on the package PR; no
merge before both. (2) A SHA containing the package COMPLETED on all four
API2 services — unverified. (3) One natural production result run ending
failed or skipped whose qa_request_failure_effects row is delivered
(delivered_at set) with the matching broadcast — unverified; no count
or outcome is assumed here.
Safety gates for every retirement package
Every implementation package must satisfy these gates before rollout:
- Flag-evaluation evidence: never use PostHog
last_called_atto decide whether a gated native path has run. It does not distinguish atrueevaluation from afalseornullone, so an older flag-off evaluation can read as the latest activity and hide real native traffic. Query$feature_flag_calledfiltered to the flag key and$feature_flag_response = true, withtimestampafter the enable or re-enable moment, then reconcile the rows those evaluations produced. A canary is “unexercised” only when that query returns nothing. - Final-boundary routing: test the exact method/path matrix through the
composed API2 application with the Rails fallback enabled. Assert that the
request reaches the intended native handler and that the fallback is not
invoked. Include
OPTIONS,HEAD, streaming, and WebSocket behavior where the contract uses them. - External canary: after deployment, exercise the public hostname in both regions and verify native structured telemetry before counting the route as migrated. A healthy service or feature-router unit test is insufficient.
- Mixed-version compatibility: prove that old and new tasks can overlap during a rolling deployment without route collisions, duplicate effects, or incompatible payloads. New behavior remains default-off when a mixed fleet is unsafe.
- Replay-safe ownership: use stable idempotency identities and distinguish definitive rejection from ambiguous delivery. Retries, timeouts after commit, rollback to Rails, and same-hour redelivery must not duplicate writes, object deletion, credit usage, or throttle consumption.
- Shared primitive parity: when Rails and API2 overlap, preserve the exact shared Redis keys, members, expiries, counters, storage service, signed-ID purpose, and regional host trust. Similar behavior is not sufficient; the QA member-counter defect is the current example.
- Short database critical sections: never hold a database transaction, row lock, or transaction-scoped advisory lock across model, provider, storage, Redis, queue, or HTTP work. Re-read and validate state in a short transaction before applying an externally computed result.
- Bounded work: cap discovery queries, fan-out, concurrency, retries, and per-tenant work. A queue move does not protect PostgreSQL from unbounded work or missing backpressure.
- Complete resource lifecycle: test database rows and external objects, including shared blobs, variants, foreign keys, orphan cleanup, and partial failure. Success-path row assertions alone do not establish parity.
- Tenant and origin isolation: prove cross-tenant negatives and use an exact regional host allowlist. Do not replace an exact trust boundary with a domain suffix or other broad match.
- Outcome reconciliation: compare accepted, rejected, not-found, duplicate, retry, and failure distributions before expanding a flag. Zero legacy calls without equivalent business outcomes is a regression, not completion.
Workstream 1: stop forwarding public calls to Rails
The canonical API2 gateway forwards a request only when API2 has no native route for its method and path. Explicit API2 proxy routes, such as conversation uploads, also count here when their implementation still calls the legacy Rails origin.
Track forwarded traffic in four groups:
- Unknown and scanner traffic: unmatched paths that Rails also answers
with
404. Reject these at API2. They are not compatibility contracts. - Compatibility HTTP: live provider callbacks, browser paths, redirects, uploads, and other public contracts still implemented by Rails.
- Explicit API2 proxies: API2-owned public routes whose handler deliberately invokes Rails, including the current conversation-upload implementation.
- Action Cable:
/cableWebSocket traffic bridged through API2 to Rails.
Initial production baseline
The first post-cutover sample covered about 70 overnight minutes. It is useful for classification, not daily forecasting.
| Traffic | US | EU | Total |
|---|---|---|---|
| Forwarded HTTP | 248 | 71 | 319 |
Rails 404 responses | 204 | 29 | 233 |
Non-404 Rails responses | 44 | 42 | 86 |
| Cable connection attempts | 13 | 24 | 37 |
About 73% of forwarded HTTP in that sample was unknown-path or scanner noise. The meaningful tail was concentrated in:
- Zendesk sidebar requests;
- Active Storage redirects;
- Unit21 webhooks;
- conversation-upload requests and preflights;
- Slack callbacks and interactions;
- a small monitoring and legacy-route tail.
Replace this sample with complete rolling 24-hour and seven-day regional windows. Record method, normalized path family, status, caller class, request count, unique tenant count when available, and the owning migration package. Do not extrapolate a short overnight sample into a daily rate.
Current post-cutover observation
The most recent fixed trailing-24-hour gateway census (ending approximately
2026-09-18T10:16Z) is in Evidence as of 2026-09-18:
18,934 events with a clear live tail of 2,583 (Cable 1,415, uploads 944,
Outreach 224). Structured coverage is still only about 30 hours, so no
seven-day gateway figure is complete yet.
Structured gateway telemetry became available only after the September 17
cutover. The query ending at approximately 2026-09-17T17:11Z begins at
04:11:53Z in the US and 04:20:35Z in the EU, so it covers approximately 13
hours rather than a complete rolling 24-hour or seven-day window.
| Traffic class | US | EU | Total |
|---|---|---|---|
Unknown, scanner, blocked, or Rails 404 | 7,796 | 539 | 8,335 |
| Meaningful forwarded or proxied traffic | 4,053 | 3,147 | 7,200 |
| Explicit conversation-upload proxy and preflights | 848 | 2 | 850 |
| Action Cable connection attempts | 1,470 | 447 | 1,917 |
| All structured gateway events | 11,849 | 3,686 | 15,535 |
The meaningful compatibility tail contains 2,352 Active Storage redirects,
1,247 conversation-image requests, 850 conversation-upload requests or
preflights, 422 Zendesk sidebar requests, 332 Unit21 requests, two Slack
requests, 78 other requests, and 1,917 Action Cable attempts. After the media
deployment completed at approximately 16:40Z, neither region forwarded a
media request and the US served six conversation images natively. These counts
establish the current path-family shape but must not be converted into a daily
rate. The next evidence gate remains the first complete rolling 24-hour and
seven-day regional census.
Execution sequence
- Classify every non-
404path family as live, rollback-only, dormant, or dead. - Reject unknown and scanner paths at API2 instead of forwarding them.
- Replace the wildcard fallback with an explicit compatibility allowlist.
- Give every allowlisted family a native API2 owner or an approved retirement decision.
- Replace the five active first-party Action Cable channel families with
API2-owned server-sent events (SSE), then remove the
/cablebridge. - Hold a complete production window with zero meaningful forwarded calls in both regions before removing the fallback and legacy public origins.
The wildcard fallback is removable only when a denied path cannot break a documented API, provider callback, browser workflow, media URL, or supported integration.
The first complete rolling 24-hour census (ending 2026-09-18T09:30Z)
counted 18,888 gateway events: 526 blocked in API2, 12,066 forwarded requests
Rails answered 404, and a meaningful tail of about 2,836 (Action Cable 1,689,
conversation uploads 923, Outreach 224). Steps 2 and 3 are implemented as an
explicit compatibility allowlist: the gateway forwards only paths whose first
segment is a Rails route-family prefix
(legacy-rails-gateway/rails-public-route-prefixes.json, kept equal to the
Rails route table by spec/lint/api2_gateway_compatibility_prefixes_spec.rb)
and answers everything else with Rails’ 404 body and outcome=rejected.
LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS=forward is the rollback gate; remove it
once a complete regional window shows no rejected event for a documented
path.
Retired prefix: /knowledge_base_documents (step 4, first native-owned retirement)
Evidence. A 24 h legacy_rails_gateway query found exactly four forwarded
US GET /knowledge_base_documents/:id requests, all 200, all simultaneous at
2026-09-19T21:27:29Z; EU had zero. Read-only Rails mapping shows all four are
Kuda (org 25) manual knowledge documents; process_action confirms
KnowledgeBaseDocumentsController#show, 6 queries per request, 36–53 ms. The
same IDs appear in Kuda QA/evaluation context and the burst followed the Kuda
backfill fallback, so this is meaningful QA-linked traffic, not scanner residue.
Caller: no code on main emits the path. The UI moved to the native API2 route
GET /knowledge-library/documents/:id in #11781/#11940 (Sep 10/13) and the
endpoint is session-authenticated, so the caller is a stale pre-#11940 UI
bundle in an open Kuda tab — the same stale-client pattern as /cable.
Decision. API2 owns the boundary natively, so the legacy prefix is retired
rather than kept as a compatibility route: /knowledge_base_documents is
removed from rails-public-route-prefixes.json and listed in
retired-rails-route-prefixes.json. The gateway answers every method under
it (including HEAD, OPTIONS, nested paths, query strings and //-prefixed
variants) with Rails’ JSON 404 locally and outcome=retired, never
contacting Rails. /import_knowledge_base_documents_tasks is a distinct
prefix and kept forwarding until batch 2 retired it. The Rails drift spec now checks that every retired
prefix is still a Rails route and not already blocked, and that the allowlist
equals the route table minus blocked minus retired. Expected effect: stale
Kuda tabs get 404 until refreshed (accepted, as for /cable); current UI is
unaffected.
Rollback. Revert the retiring PR (restores the prefix and forwarding), or
set LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS=forward on the API2 web service for
an immediate hold — under forward the retired check is skipped and the path
forwards again. The adapter and this config gate are retained. Post-cutover
proof: outcome=retired for /knowledge_base_documents should decay to zero
within the UI forced-refresh window; any retired event from a current UI
build (or from a non-browser client) is a rollback trigger.
Coordinator census at 2026-09-23T01:20:02Z and the public.* ownership probe package (ENG-3378 , child of ENG-3345 )
Coordinator census ending 2026-09-23T01:20:02Z (API2 release-prod
3492e8a5 deployed steady in both regions at 00:56Z, Rails 3/3 in both).
The two windows are kept separate on purpose; the 7 d figures are dominated by
pre-cutover residue and do not establish a post-3492e8a5 zero, which
needs its own clean window after 00:56Z:
| Window | Signal | US | EU | Total | Reading |
|---|---|---|---|---|---|
| 24 h | Rails internal calls | 43 | 2 | 45 | Residual internal effects; classification in the 2026-09-22T23:23Z section still applies |
| 24 h | Gateway forwarded successes | POST /conversations/upload 201 ×1 | GET /conversations/ 200 ×23, GET /conversation_fields 200 ×1 | 25 | EU /conversations/ is the empty-segment path #12820 redirects once deployed |
| 24 h | Direct Rails-origin image successes | 0 | 0 | 0 | First zero-image 24 h window (ENG-3282/3297) |
| 7 d | Rails internal calls | 60,142 | 96,244 | 156,386 | Mostly pre-cutover residue; not a post-deploy measure |
| 7 d | Direct Rails-origin image successes | 2,594 | 2,221 | 4,815 | Pre-cutover; contrast with the 24 h zero above |
This session had no AWS, CloudWatch or database access (aws sts get-caller-identity: no credentials), so the census is requester-supplied
and no production probe of any kind was run.
Package chosen (Workstream 3): a read-only, TTY-free public.* live-diff
probe plus the runbook gates, because it is the only step toward the
zero-statement proof that needs no production DDL, no flag, and no deploy.
What was done, all local against a structure.sql (05527e7c6) + baseline +
409 Drizzle migrations database and its _push clone:
- Reproduced the Phase 2 TTY
drizzle-kit push --strictpreview: 259 statements,publicstill exactly the 4 knownidx_conversations_*nav_datedrop/recreate statements; aborted at the apply prompt. - Root-caused the churn in
drizzle-kit 0.31.10source:sqlindex columns are always serializedasc: true, nulls: "last"while introspection readspg_index.indoption, soCOALESCE(...) DESCcan never round-trip and there is no declaration-side fix in this version. Options (upgrade,tablesFilterexclusion, or rewriting the indexes with production DDL) are recorded in the runbook and left for a human decision. - Added
pnpm db:probe-public-ownership(scripts/public-schema-diff-probe.ts+src/db/public-schema-probe.ts):drizzle-kit pull --schemaFilters publicinto a temp dir,generateMigrationagainst the declaration, classification intoknown-expression-index-churn/strict-only-index-churn/public-drift/api2-drift/agent_api-drift, exit0only on public zero. It shares the loopback-and-*_pushguard withdrizzle.probe.config.tsand never calls an apply function. Local result:publicPushStatements: 4,public-drift: 0,api2-drift: 0,agent_api-drift: 0,strict-only-index-churn: 48(24 pairs that differ only in introspectedopclass/WHEREtext — tool artefacts, documented). 12 unit tests cover the guard, the push-equivalence squash and the classifier. - The strict pass exposed two real declaration bugs push-mode hides:
drizzle-kit pulltruncates expressions at an embedded'', soindex_pg_search_documents_on_contentandindex_pg_search_documents_on_content_unaccent_tsvwere declared asCOALESCE(content, '. Restored frompg_get_indexdef. Consequence recorded in the runbook: a push-mode zero is necessary but not sufficient.
Decisions. No production DDL, data write, flag change, deploy, replay or
regional repair was performed or proposed for automation; regional index
reconciliation and the ten invalid indexes remain a human-owned repair PR.
drizzle.config.ts is unchanged (["api2", "agent_api"]). The runbook now
carries operator gates G1–G7 (regional reconciliation → per-region
regeneration → two-region probe exit 0 with the strict list read →
expression-index strategy → non-declarable owners → baseline/ordering → Rails
migration-freeze CI) and the rollback statement (nothing runtime to roll back
while gates are open; api2 image redeploy if schemaFilter is ever widened).
Next gates. G1 repair PR (human); then restore a schema-only dump of each
region into a local *_push database and run the probe per region, expecting
exit 0 in both; only then Phases 3–5.
Coordinator read-only audit at 2026-09-23T02:20:00Z (epoch 1790130000)
#12831 was
human-merged at 02:19:54Z under user review authorization, without a
submitted Ronald review (not Ronald approval); API2 release sync
#12833 merged at
02:20:07Z. The probe, runbook gates and declaration fixes above are on
main; no deploy of them had been observed by the cutoff. The
coordinator then supplied a second read-only CloudWatch audit with a fixed
cutoff of 2026-09-23T02:20:00Z. This session still had no AWS, CloudWatch
or database access, so the figures are requester-supplied and no
production probe or DDL was run. The schema scope of this package is
unchanged: G1–G7 stay blocked on the human-owned repair PR.
Deployment attribution at the cutoff. API2 release-prod advanced to
89d1b45a at 02:11:23Z via sync #12832
for the merged QA PR #12830 ,
but as of ~02:22Z ECS US and EU web/worker services all still ran
d6607a6d (every deployment COMPLETED); Rails 3/3 in both regions. All
“post-deploy” reads below are therefore attributed to d6607a6d, not
89d1b45a. #12830 was
merged by a human under user review authorization without a submitted Ronald
review; this is recorded as such and not as Ronald approval.
Trailing 24 h ending 02:20:00Z (mixed pre/post-d6607a6d; not a
post-deploy zero measure):
| Signal | US | EU | Total | Method/path classification |
|---|---|---|---|---|
Rails internal /internal/* calls | 42 | 2 | 44 | US: expiry 23, upload mark_failed 6, field upsert 4, reconciliation 3, reports 4, Unit21 1, Slack 1. EU: reconciliation 1, report 1 |
| API2 gateway events (all outcomes) | 1,212 | 4,172 | 5,384 | Includes the rejected, removed and forwarded events below |
| Forwarded successes (meaningful public) | POST /conversations/upload 201 ×1 | GET /conversations/ 200 ×23, GET /conversation_fields 200 ×1 | 25 | Same set as the 01:20:02Z census; EU /conversations/ is the empty-segment path #12820 redirects |
Forwarded GET 404 | 8 | 386 | 394 | US: scanner noise; EU: stale empty-id paths |
/cable removed (websocket_bridge_removed) | 1 | 1,612 | 1,613 | Bridge removal working as designed |
Direct Rails-origin image GET successes | 0 | 0 | 0 | Second consecutive zero-image 24 h window |
Exact trailing 7 d ending 02:20:00Z (almost entirely pre-cutover
residue; does not establish any post-deploy zero):
| Signal | US | EU | Total |
|---|---|---|---|
| Rails internal calls | 59,545 | 94,811 | 154,356 |
| API2 gateway events | 39,749 | 25,600 | 65,349 |
First fully post-d6607a6d slice, 02:04:56Z → 02:20:00Z (~15 min).
Non-health Rails controller calls US and EU: zero (/up health checks were
intentionally excluded from the query). Gateway: US zero events; EU 15
websocket_bridge_removed and 10 rejected 404, zero forwarded. The natural
02:05Z expiry tick ran US 1 candidate / 1 failed and EU 0, both with
deferredToRails = 0 and rails = null (no Rails deferral). No natural Kuda
external_api_key_auth or conversation_upload_native_intake event occurred
through the cutoff, so those native paths have no post-deploy natural sample
yet. A 15-minute clean slice is evidence of direction only; the 24 h and 7 d
zero gates remain open.
Gate position after this audit. Workstream 1/2 24 h and 7 d zero windows: open. Workstream 3 schema gates G1–G7: blocked on the human-owned G1 repair PR; no production probe, DDL, data write, flag change, deploy or replay was performed. Rollback position unchanged from the section above.
Field-upsert attribution and bounded activation (ENG-3379 ; coordinator-performed, recorded here — not performed by this package).
The four US “field upsert” calls in the 24 h table were
PUT /internal/conversations/7054000/field_values ×2 and
PUT /internal/conversations/7158696/field_values ×2; read-only ECS pg at
~02:34Z resolved both conversations to organization_id = 19, slug
rho-tnkw. PostHog project 63353 already has api2_conversation_relationships
(flag 273643) active at 100%. The coordinator created the server-only flag
api2_conversation_field_value_native_upsert (id 285250, v1) at
2026-09-23T02:35:13.538613Z with a single condition — exact $group_key
rho-tnkw at 100%, no other group or percentage. Owner Platform Engineering;
review/remove by 2026-12-21. Rollback: disable the flag or remove the
condition (≤ 20 s SDK cache). No synthetic write, replay or data mutation
was performed. Hold at exact Rho until a natural field-upsert produces an
outcome with DB row parity and error evidence; roll the flag back immediately
on any regression. The 24 h/7 d zero gates are unaffected by this activation.
Later deployment addendum (read-only ECS at ~02:29Z; distinct from the
fixed 02:20:00Z window, which stays attributed to d6607a6d). API2
b905bc20 (FC_GIT_COMMIT_SHA=b905bc20cd3b7eec68c0d888205bce9b3f57a693
on all four new task definitions) finished rolling out in both regions:
| Region | Service | Task definition | Tasks | Rollout completed |
|---|---|---|---|---|
| US | web | TD1726 | 1/1 | 2026-09-23T02:27:42Z |
| US | worker | TD1716 | 5/5 | 2026-09-23T02:27:42Z |
| EU | web | TD1743 | 1/1 | 2026-09-23T02:28:13.920Z |
| EU | worker | TD1732 | 5/5, pending 0 | 2026-09-23T02:28:57.382Z |
Rails remains 3/3 per region. No post-b905bc20 traffic outcome is
claimed; the next census must open a fresh window starting no earlier than
02:28:57Z and attribute it to b905bc20.
Early smoke slice only (read-only CloudWatch, 02:28:58Z → 02:31:07Z,
129 s; cannot close any 24 h / 7 d gate). Non-health Rails controller calls
US and EU: zero. Gateway US: two rejected GET 404 (/, /favicon.ico).
Gateway EU: three websocket_bridge_removed (/cable) and two rejected
GET 404 (/wp-json/wp/v2/posts, /wp-login.php). Zero forwarded in both
regions.
Coordinator read-only audit at 2026-09-23T03:20:00Z (epoch 1790133600)
Operator-supplied CloudWatch/ECS evidence; no production probe, DDL, data write, flag change, deploy, replay or runtime-code change accompanies this record. Historical windows above are preserved unchanged.
Trailing 24 h ending 03:20:00Z (mixed pre/post-cutover; not a
post-deploy zero measure):
| Signal | US | EU | Total | Method/path classification |
|---|---|---|---|---|
Rails internal /internal/* calls | 41 | 2 | 43 | US: expired evaluations 22, direct-upload mark_failed 6, field upsert 4 (two each for conversations 7054000 and 7158696, both Rho), reconciliation 3, daily reports 4, Unit21 1, Slack 1. EU: reconciliation 1, report 1 |
| API2 gateway events (all outcomes) | 1,216 | 4,171 | 5,387 | Includes the rejected, removed and forwarded events below |
| Forwarded successes (meaningful public) | POST /conversations/upload 201 ×1 | GET /conversations/ 200 ×23, GET /conversation_fields 200 ×1 | 25 | Same set as the 02:20:00Z census |
Forwarded GET 404 | 8 | 386 | 394 | US: scanner noise; EU: 384 stale empty-id paths + 2 scanner |
/cable removed (websocket_bridge_removed) | 1 | 1,611 | 1,612 | Bridge removal working as designed |
Direct Rails-origin image GET successes | 0 | 0 | 0 | Third consecutive zero-image 24 h window |
Exact trailing 7 d ending 03:20:00Z (mostly pre-cutover residue;
not a zero-window claim):
| Signal | US | EU | Total |
|---|---|---|---|
| Rails internal calls | 59,130 | 93,419 | 152,549 |
| API2 gateway events | 39,759 | 25,705 | 65,464 |
Direct Rails-origin image GET successes | 2,594 | 2,220 | 4,814 |
Post-canary slice, 02:35:13Z → 03:20:00Z (~45 min, on b905bc20).
Non-health Rails controller calls US and EU: zero. Gateway: US 5 events, all
rejected scanner/unknown GET 404; EU 77 events — 43
websocket_bridge_removed (GET /cable, removed at the gateway and never
reaching Rails) and 34 rejected scanner/unknown GET 404. Zero forwarded
events in both regions, so none of this gateway activity is live Rails
traffic. The natural 03:05Z
expiry ticks ran in both regions with candidates = 0,
deferredToRails = 0, rails = null — the absence of candidates means no
outcome transition occurred, so this tick is not a native-path sample.
Rho field-upsert canary (ENG-3379 ).
Flag 285250 v1 (api2_conversation_field_value_native_upsert, exact
rho-tnkw 100%) has been active since 02:35:13.538613Z; no native Rho
upsert and no matching error had been observed by 03:20Z. HOLD — no
widening until a natural outcome with DB row parity is observed.
Gate position after this audit. Workstream 1/2 24 h and 7 d zero windows: open. Workstream 3 schema gates G1–G7: blocked on the human-owned G1 repair PR. Rollback position unchanged.
Coordinator read-only audit at 2026-09-23T04:20:00Z
Operator-supplied CloudWatch/ECS evidence; no production probe, DDL, data write, replay, deploy or runtime-code change accompanies this record (the flag activation below was coordinator-performed and is recorded, not performed, here). Historical windows above are preserved unchanged.
Trailing 24 h ending 04:20:00Z (mixed pre/post-cutover; not a
post-deploy zero measure):
| Signal | US | EU | Total | Method/path classification |
|---|---|---|---|---|
Rails internal /internal/* calls | 40 | 1 | 41 | US: expiry 21, direct-upload mark_failed 6, field upsert 4, reports 4, reconciliation 3, Unit21 1, Slack 1. EU: reconciliation 1 |
| API2 gateway events (all outcomes) | 1,244 | 4,195 | 5,439 | Includes the rejected, removed and forwarded events below |
| Forwarded successes (meaningful public) | POST /conversations/upload 201 ×1 | GET /conversations/ 200 ×23, GET /conversation_fields 200 ×1 | 25 | Unchanged from the 02:20Z and 03:20Z censuses |
Forwarded GET 404 | 8 | 386 | 394 | US: scanner noise; EU: 384 stale empty-id paths + 2 scanner |
/cable removed (websocket_bridge_removed) | 1 | 1,611 | 1,612 | Bridge removal working as designed |
Direct Rails-origin image GET successes | 0 | 0 | 0 | Fourth consecutive zero-image 24 h window |
Exact trailing 7 d ending 04:20:00Z (pre-cutover dominated; not a
zero-window claim): Rails internal US 58,892 / EU 92,052 = 150,944; API2
gateway US 39,792 / EU 25,845 = 65,637; direct Rails-origin conversation-image
GET successes US 2,594 / EU 2,220 = 4,814 (all pre-cutover; 24 h zero).
Post-canary slice, 02:35:13Z → 04:20:00Z (~105 min, on b905bc20).
Non-health Rails controller calls: US exactly one —
POST /internal/daily_agent_evaluation_reports/combined_email 200 at
04:00Z, the natural daily-report tick; EU zero. Gateway forwards zero in
both regions; US 16 rejected GET 404, EU 44 websocket_bridge_removed
(/cable, never reaching Rails) + 50 rejected GET 404. The natural
04:00Z report tick ran US 38 organizations (2 emailed, no failures) and EU
16 organizations (0 emailed, no failures); US report rows were rho-tnkw 3
and kuda-pilot 1. The natural 04:05Z expiry ticks completed in both
regions with candidates = 0, failed = 0, deferredToRails
snapshots/requests = 0, rails = null — no positive candidate transition,
so not a native-path outcome sample.
Daily-report delivery canary (coordinator-performed). PostHog
server-only flag api2_daily_agent_evaluation_report_delivery (id 285273,
v1) was activated at 04:24:03Z with exact rho-tnkw only; no
post-activation natural outcome yet. Scope, per internal code review: the
daily report cron enqueues per-organization jobs with sendEmail: false, so
the natural 20:00Z/04:00Z ticks can only prove native backfill_preview
(queued backfill preview plan with source = native, zero Rails
backfill_preview for organization 19). Native per-organization email
status = sent is unreachable from the cron and is not a hold criterion;
it needs a later natural backfill reconcile. The Rails
combined_email call remains expected on every tick while the Kuda
contributor stays off — this flag does not eliminate the next
combined_email, and all other report contributors keep their preview/email
on Rails. HOLD — no widening until a natural tick shows the native Rho
backfill_preview outcome above; roll back on any regression. The Rho field-upsert flag 285250 (exact
Rho) remains held with no natural positive outcome yet.
Related. The EU empty-id GET /conversations//... prefetch has a
UI-only fix in #12838
(ENG-3381 ; green, awaiting
Ronald review, not merged); its plan edit lives in a separate ENG-3381
subsection of this document, not in this one.
Gate position after this audit. Workstream 1/2 24 h and 7 d zero windows:
open (the 04:00Z US combined-email call is live Rails traffic and resets
any post-canary zero slice). Workstream 3 schema gates G1–G7: blocked on the
human-owned G1 repair PR. Rollback position unchanged.
Status note, 2026-09-23 (after 05:21Z)
The 04:20:00Z census above remains the latest verified exact 24 h / 7 d
census: the coordinator’s AWS SSO session expired at 05:21Z, so no later
CloudWatch/ECS read has been taken. Docs-only
#12834 (this plan) and
UI-only #12838
(ENG-3381 ) both have Ronald
review requested; #12838’s final head was CI-green, and #12834’s prior head
432805067 was CI-green while the head carrying this note has checks pending
at the time of writing. Neither has a submitted Ronald review and neither
is merged. Rho flags 285250 (field-value native upsert) and 285273
(daily-report delivery) remain held at exact rho-tnkw pending natural
positive outcomes. G1 schema repair remains blocked and human-owned. No
counts above are revised by this note.
Status note, 2026-09-23 06:23Z
AWS SSO token refresh still fails for both configured coordinator profiles,
so 04:20:00Z remains the latest verified exact cutoff: 24 h Rails internal
41, successful Rails gateway forwards 25, direct image GET successes 0;
7 d Rails internal 150,944, gateway 65,637, direct image GET successes
4,814 (pre-cutover); post-canary slice one US combined_email Rails call and
zero forwarded requests in both regions. Elapsed time since 04:20Z is
not a measured window and must not be read as a zero window. Open
focused PRs, CI-green on their latest heads (for #12834, the head preceding
this note; the head carrying it has checks pending at the time of writing),
labeled codex +
codex-automation, Ronald requested, no submitted Ronald approval on
any, none merged: #12834
(this plan, ENG-3378), #12838
(ENG-3381 empty-id prefetch), #12839
(ENG-3382 QA fallback
attribution) and #12840
(ENG-3383 object-field prefix
retirement; its plan edit is the separate “Legacy object-field prefixes
retired” section, not this one). Rho flags 285250/285273 still held; G1
schema repair still blocked and human-owned (G1–G7 unchanged).
Coordinator read-only audit at 2026-09-23T07:25:00Z
Fixed CloudWatch cutoff 07:25:00Z (epoch 1790148300); AWS SSO recovered, so
this supersedes 04:20:00Z as the latest verified exact census. Read-only;
no production DDL, data write, flag change, replay or deploy.
Trailing 24 h ending 07:25:00Z (mixed pre/post-cutover; not a
post-deploy zero measure):
| Signal | US | EU | Total | Method/path classification |
|---|---|---|---|---|
Rails internal /internal/* calls | 37 | 1 | 38 | US POST: expiry 18, direct-upload mark_failed 6, transcription reconciliation 3, report backfill_preview 2 + combined_email 2, Unit21 1, Slack 1; US PUT field-value upsert 4. EU POST reconciliation 1 |
| API2 gateway events (all outcomes) | 20,731 | 28,648 | 49,379 | Includes the rejected, removed and forwarded events below |
| Forwarded successes (meaningful public) | POST /conversations/upload 201 ×1 | GET /conversations/ 200 ×23, GET /conversation_fields 200 ×1 | 25 | All pre-cutover; unchanged since the 02:20Z census |
Forwarded GET 404 | 299 | 704 | 1,003 | Scanner noise; EU also includes stale empty-id paths |
/cable removed (websocket_bridge_removed) | 1 | 1,588 | 1,589 | Bridge removal working as designed |
Direct Rails-origin image GET successes | 0 | 0 | 0 | Fifth consecutive zero-image 24 h window |
Exact trailing 7 d ending 07:25:00Z (pre-cutover dominated; not a
zero-window claim): Rails internal US 57,795 / EU 87,117 = 144,912; API2
gateway US 59,304 / EU 50,722 = 110,026; direct Rails-origin
conversation-image GET 200 US 2,498 / EU 2,200 = 4,698.
Strictly post-04:20:00Z slice, 04:20:00Z → 07:25:00Z (~185 min).
Rails /internal/* calls zero in both regions; successful forwards zero in
both regions; forwarded GET 404 US 291 / EU 318, scanner only. This is a
~3 h measured slice on top of the earlier post-canary slice (which contained
the one US 04:00Z combined_email call) and does not constitute a 24 h
or 7 d zero window; the Workstream 1/2 zero gates remain open.
Merges and release state.
#12840
(ENG-3383 , object-field prefix
retirement) was human-merged by chidiwilliams at 06:40:54Z as 84199d09
under user review authorization, without a submitted Ronald review; its
plan edit is the “Legacy object-field prefixes retired” section below.
Release syncs: #12841
merged 06:41:08Z (release-prod 1c5886a5, includes #12840) and
#12843 merged
07:24:29Z (release-prod tip c463d408). Read-only ECS, observed in
successive reads from ~07:3xZ: US API2 web TD1727 2/2 COMPLETED on
1c5886a5; EU API2 web TD1745 for c463d408 went IN_PROGRESS (desired 0 /
running 0, TD1743 b905bc20 1/1 still active) → running 1/1 → the final
read COMPLETED, desired/running 1/1, pending 0. Rails US 3/3
(TD2912) and EU 3/3 (TD2008), both COMPLETED. So #12840 is now deployed on
API2 web in both regions (worker services not read in this pass). The
07:25:00Z traffic cutoff predates EU completion, and a 06:45Z–07:25Z
retired-prefix query returned 0 requests in both regions — no natural
retired-prefix request has been observed post-#12840. Docs-only
#12834 (the
02:20Z–06:23Z plan updates above) was human-merged by chidiwilliams at
07:30:36Z as a1291ec7, and UI-only
#12838 (ENG-3381) at
07:28:53Z as 3ac4f850, both under user review authorization, without a
submitted Ronald review; this 07:25Z audit therefore lands via a fresh
docs-only follow-up PR on the same ENG-3378 scope.
#12839 (ENG-3382)
remains open, CI-green on its prior head, labeled codex +
codex-automation, Ronald requested, no submitted Ronald approval, not
merged.
Decisions. Rho flags 285250 (field-value native upsert) and 285273
(daily-report delivery) stay held at exact rho-tnkw: the 24 h US PUT
field-value upsert ×4 and report backfill_preview ×2 above are all
pre-04:20Z Rails calls, and no natural positive native outcome has been
observed for either flag. G1 schema repair remains blocked and human-owned;
G1–G7 unchanged. No counts in earlier sections are revised by this audit.
Coordinator read-only audit at 2026-09-23T13:30:00Z
Fixed CloudWatch cutoff 13:30:00Z (epoch 1790170200), complete read-only
regional queries; supersedes 07:25:00Z as the latest verified exact
census. Read-only; no production DDL, data write, flag change, replay or
deploy.
Trailing 24 h ending 13:30:00Z (mixed pre/post-cutover; not a
post-deploy zero measure):
| Signal | US | EU | Total | Method/path classification |
|---|---|---|---|---|
Rails internal /internal/* calls | 44 | 1 | 45 | US: evaluation-check-run mark_failed 13, expiry 12, direct-upload mark_failed 6, PUT field-value upsert 4, transcription reconciliation 3, report backfill_preview 2 + combined_email 2, Unit21 1, Slack 1. EU: reconciliation 1 |
| API2 gateway events (all outcomes) | 20,601 | 28,508 | 49,109 | Includes the rejected, removed and forwarded events below |
| Forwarded successes (meaningful public) | POST /conversations/upload 201 ×1 | GET /conversations/ 200 ×22 | 23 | All old pre-cutover forwards (the 07:25Z count of 25 has aged out of the window) |
Forwarded GET 404 | 297 | 672 | 969 | Scanner plus stale empty-id paths |
/cable removed (websocket_bridge_removed) | — | 1,437 | 1,437 | Bridge removal working as designed |
Direct Rails-origin image GET successes | 0 | 0 | 0 | Sixth consecutive zero-image 24 h window |
Exact trailing 7 d ending 13:30:00Z (pre-cutover dominated; not a
zero-window claim): Rails internal US 50,375 / EU 72,598 = 122,973; API2
gateway US 59,414 / EU 51,524 = 110,938; direct Rails-origin
conversation-image GET 200 US 2,200 / EU 1,829 = 4,029.
Post-04:15:00Z slice, 04:15Z → 13:30Z. Rails /internal/*: US 14
(the 13 evaluation-check-run mark_failed calls and the 1 Slack call all fall
in this slice), EU 0. Successful gateway forwards zero in both regions. The
US mark_failed calls are live Rails effects, not residue, so the
Workstream 1/2 24 h and 7 d zero gates remain open; no elapsed time is a zero
window.
Release attribution. release-prod branch advanced to 2fd3b757 at
13:33:50Z; the running ECS SHA per service had not been read at this
cutoff, so no deployment claim is made for 2fd3b757 (last verified ECS
state is the ~07:3xZ read above: US API2 web 1c5886a5, EU API2 web
c463d408).
#12839 ,
#12845 (this audit’s
docs-only PR) and
#12847 remain open,
CI-green, with no submitted Ronald approval; none merged. (Superseded at
19:30Z below: #12845 was human-merged at 19:27Z, and #12847 is CI-red.)
Decisions. Rho flags 285250 and 285273 stay held at exact rho-tnkw
(no natural positive native outcome observed; the 24 h PUT upsert ×4 and
backfill_preview ×2 remain Rails calls). No scale cut. G1 schema repair
remains blocked and human-owned; G1–G7 unchanged. No counts in earlier
sections are revised by this audit.
Coordinator read-only audit at 2026-09-23T19:30:00Z
Fixed CloudWatch cutoff 19:30:00Z, complete read-only regional queries;
supersedes 13:30:00Z as the latest verified exact census. Read-only; no
production DDL, data write, flag change, replay or deploy.
Evaluation-check-run mark_failed timing. Reconciling every US
/internal path row in this window gives 15 calls: 1 at 08:07Z, 6 at
09:07–09:08Z, 8 at 13:07–13:39Z. The 13:30Z audit above counted a
different 24 h window and is left as written.
Trailing 24 h ending 19:30:00Z (mixed pre/post-cutover; not a
post-deploy zero measure):
| Signal | US | EU | Total | Method/path classification |
|---|---|---|---|---|
Rails internal /internal/* calls | 26 | 1 | 27 | US: evaluation-check-run mark_failed 15 (owned by the QA track), expiry 6 (all before 01:05Z, pre Kuda v3), report backfill_preview 2 + combined_email 2 (all before 04:01Z, pre flag 285273), Slack POST /internal/slack_notifications 1 (13:00:44Z, Rho). EU: transcription reconciliation 1 at 2026-09-22T23:15Z |
Direct-upload mark_failed, PUT field-value upsert, Unit21 and the US
reconciliation calls have all aged out of the 24 h window with no
recurrence; the only US Rails internal effects since 04:15Z are the
evaluation-check mark_failed family and the single Rho Slack call.
Exact trailing 7 d ending 19:30:00Z (pre-cutover dominated; not a
zero-window claim): Rails internal US 40,689 / EU 59,984 = 100,673; API2
gateway US 59,627 / EU 52,239 = 111,866; successful direct Rails-origin
conversation-image GET US 1,798 / EU 1,152 = 2,950.
Slack family classification (read-only, code on main). The
13:00:44Z call is createSlackTools →
resolveNativeSlackDelivery returning null because
api2_slack_notification_native does not exist in PostHog project
63353 (found:false at 13:4xZ), so the agent slack-send-<channel> tool
fell through to sendSlackNotification (Rails) by fail-closed design. The
native path (#12719 ,
ENG-3352 ) is deployed:
sendNativeSlackNotification posts chat.postMessage with the
OrganizationDataSource token and relays Slack refusals as { ok:false, error } exactly as Internal::SlackNotificationsController#create does;
53/53 focused tests (agent-slack-notifications, slack-client,
agent-slack-tools) pass on main. The gate is read with
organizationSlug: organizations.rulebaseId, which Rails resolves via
Organization.friendly.find, i.e. the slug — so an exact rho-tnkw
$group_key condition matches the one natural caller. Bounded Slack
canary activated (operator, user-authorized):
api2_slack_notification_native flag 286616
v1 created 2026-09-23T19:44:52.277399Z in PostHog project 63353, active,
server-only, exact organization $group_key=rho-tnkw 100%, no other cohort
or percentage; owner Platform Engineering, review/remove by 2026-12-22.
Held pending the next natural Rho agent Slack send: success = native
chat.postMessage ok:true with a ts, zero Rho
POST /internal/slack_notifications and
POST /internal/conversations/:id/slack_notifications, no Sentry
exception; no widening, replay or manual send. Rollback = disable the flag
(≤20 s SDK cache). The 13:00:44Z Rails call above predates the flag.
Evaluation-check mark_failed (15/24 h, the largest live family) is owned
by the QA track; its gate native_evaluation_check_run_mark_failed is
likewise absent in PostHog.
Release and deployment attribution. release-prod branch is 290c0ca4
at 19:27Z from the human-merged docs-only
#12845 (Chidi
approval). ECS new task definitions are rolling: API2 web and worker
IN_PROGRESS in both regions at the cutoff, Rails 3/3 in each region.
No traffic in this audit is attributed to 290c0ca4.
#12839 (5342b77)
is CI-green with REVIEW_REQUIRED and no submitted Ronald review; it stays
open. #12847 was
red on API2 test shard 4 / aggregate at this cutoff and had no Ronald
review; its 13:30Z census text is superseded by this section (see the
01:35Z update below for its current state). Rho flags 285250 and 285273
are held at exact rho-tnkw with no natural positive native outcome yet
(the 20:00Z report tick is the first gate for 285273). No scale cut. The
24 h and 7 d zero gates for every Workstream 1/2 family remain open.
Evaluation-check-run mark_failed attribution at the 19:30:00Z audit (ENG-3400 )
Owned by the QA track; complements the coordinator 19:30:00Z census above (same fixed cutoff, same 15-call figure). Read-only; no production write, flag change, replay or deploy.
Attribution of the 15 mark_failed calls. They are not residue. The only
caller of POST /internal/evaluation_check_runs/:id/mark_failed is API2’s
fail-evaluation-check-run BullMQ job (failEvaluationCheckRunTerminally in
rulebase-web/rulebase-api2/src/lib/evaluation-check-run.ts), enqueued when an
evaluation-check-run job exhausts its 5 attempts. The native port
tryMarkNativeEvaluationCheckFailed existed but was gated on PostHog
native_evaluation_check_run_mark_failed, which was never created (23:23Z
PostHog inventory above), so every terminal failure fell back to Rails by
design. The exhaustions themselves are API2-side: Sentry
RULEBASE-API2-FA
(Error: Failed query: rollback caused by Client has encountered a connection error and is not queryable, culprit
tryRunNativeEvaluationCheck, 25 production events, first
2026-09-21T13:34:55Z, last 2026-09-23T13:39:27Z with attempts_made 5 / max_attempts 5, job evaluation-check-run-7063997-…, US worker booted
13:13:43Z) lines up exactly with the last call of the 13:07–13:39Z
cluster. Per-call attribution of the 08:07Z and 09:07–09:08Z calls to
the same issue is inference from the shared job path, not a verified
per-event match. The connection loss occurs while the native check body
holds the execution advisory lock and transaction across the model call,
which violates the database lock-safety rule; that is a separate follow-up
and is not changed by this package.
Decision (this PR). Bake the native transition in: the PostHog opt-in
flag is removed from code and tryMarkNativeEvaluationCheckFailed owns
mark_failed unconditionally, with a Flipper kill switch
pause_native_evaluation_check_run_mark_failed (safe state “on” → Flipper per
AGENTS.md; owner Platform, created 2026-09-23, removed with the Rails
endpoint, review by 2026-12-22) that hands the transition back to Rails while
that endpoint still exists. An unreadable switch defers to Rails. Rails
parity is unchanged: with_advisory_lock(timeout: 0), terminal statuses
returned as-is, already_running re-thrown so the failure job retries, check
- snapshot + (most-recent-snapshot) conversation failed in one transaction. No flag, deploy, replay or data change happens in the PR; the deploy that carries it is the operational canary.
Gates and rollback. After the carrying deploy, one full US and EU day
must show Internal::EvaluationCheckRunsController#mark_failed at 0 in
Rails logs while fail-evaluation-check-run completions continue and no new
rulebase-api2 Sentry issue appears under evaluation-checks/mark-failed.
Rollback is enabling pause_native_evaluation_check_run_mark_failed in
Flipper (both regions; the fallback is immediate, no deploy) or reverting the
PR. Rollback criteria: any fail-evaluation-check-run job error, an
already_running deferral that survives the job’s retry budget, or any
snapshot/conversation left running/in_progress after its check reached
failed. The remaining internal families (expiry 6, reports 4, Slack 1, EU
reconciliation 1) are unchanged by this package and keep their own gates.
Coordinator status update at 2026-09-24T01:35Z (no new traffic evidence)
Both coordinator AWS SSO profiles are expired, so no CloudWatch read was
possible; the latest exact traffic cutoff remains 2026-09-23T19:30:00Z
above and no zero-window credit is taken for the elapsed 19:30Z →
01:35Z interval. Rails 3/3 per region is the last verified ECS state; the
290c0ca4 API2 rollout (four API2 services) was last verified completed at
19:45Z.
Release branch vs. running SHA. The release-prod branch advanced to
e66de8cb via #12909
(sync main into release-prod, merged 2026-09-23T23:50:24Z). This is
branch state only from GitHub: the ECS running SHA per service is
unverified because both AWS SSO profiles fail token refresh, so no
deployment or traffic attribution is made for e66de8cb.
PR state.
#12847 is now
CI-green at c5e53fbf (the shard-4 failure noted above is resolved) but
still has no Ronald review; it stays open.
#12891 (QA track,
ENG-3400 : native
evaluation-check mark_failed behind a Flipper kill switch) is CI-green
and unapproved.
#12839 (5342b77)
is CI-green with no Ronald review. This audit’s docs-only
#12890 was CI-green
(required ci passing) at prior head 05fc322e; checks on the head
carrying this section were still pending when it was written. Ronald is
absent. Nothing merged.
Decisions. The exact-Rho Slack canary (flag 286616) stays held with
no natural Rho send observed (no telemetry read); Rho flags 285250 and
285273 unchanged and held; no widening, replay, manual send, data write,
deploy or scale cut. The evaluation-check mark_failed family remains the
largest live Rails effect (15 in the 19:30Z 24 h window) and is owned by
the QA track via #12891. All Workstream 1/2 24 h and 7 d zero gates remain
open.
Coordinator read-only audit at 2026-09-24T07:40:00Z
AWS read-only access recovered; fixed CloudWatch cutoff 07:40:00Z,
complete regional queries. Supersedes 19:30:00Z as the latest verified
exact census. Read-only; no DDL, data write, flag change, replay or deploy.
Trailing 24 h ending 07:40:00Z (mixed; not a post-deploy zero measure):
| Signal | US | EU | Total | Classification |
|---|---|---|---|---|
Rails internal /internal/* calls (all POST) | 19 | 0 | 19 | US: POST /internal/evaluation_check_runs/:id/mark_failed 15 (QA track, #12891; latest 2026-09-23T13:39:28.372Z), POST /internal/daily_agent_evaluation_reports/combined_email 2 (latest 2026-09-24T04:00:30.219Z) + POST /internal/daily_agent_evaluation_reports/backfill_preview 1 (latest 2026-09-23T20:00:15.272Z), POST /internal/slack_notifications 1 (2026-09-23T13:00:44.953Z). EU: none |
legacy_rails_gateway events (all structured gateway events, mostly locally rejected/removed) | 1,124 | 2,260 | 3,384 | Only 10 + 2 were forwarded to Rails (next row), all 404; successful Rails forwards zero in both regions |
| Forwarded Rails 404 | 10 | 2 | 12 | US: /admin/.env ×2, plus /admin/.git/config, /admin/index.php, bare /admin, /admin%2F.env, /admin/images.php, /admin/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php ×1 each (scanner-shaped), and /agent/auth, /agent/identity ×1 each (source attribution pending — not classified as scanner). EU: /admin/login.asp, /admin/index.html ×1 each |
| EU removed-prefix Cable upgrades | – | 703 | 703 | Stale/blocked at the gateway, not a live bridge |
| Direct successful Rails conversation-image GET | 0 | 0 | 0 |
Exact trailing 7 d ending 07:40:00Z (historical, pre-cutover
dominated; not a zero-window claim): Rails internal US 27,953 / EU
43,346 = 71,299; gateway events US 59,470 / EU 52,383 = 111,853; direct
successful image GET US 1,627 / EU 964 = 2,591.
Deployment. API2 running SHA afabdfcd (release sync
#12927 , commit
2026-09-24T06:19:44Z) on all four regional web/worker services; rollout
completion is gated by US API2 web at 07:25:58Z (US worker 06:28:35Z,
EU web 06:27:39Z, EU worker 06:27:40Z), so this census has only ~14
minutes after the complete afabdfcd rollout and takes no post-release
24 h gate credit. Rails 3/3 per region. The release-prod branch
(e29ef0df) is ahead of the running SHA — branch state and running SHA
are not equated.
Report family (still live, code on main). The two combined_email
calls are the 04:00Z/20:00Z cron ticks: combinedEmailGoesNative
requires every contributing report to carry native_delivery:true, so
with flag 285273 exact-Rho the cross-org combined email stays on Rails by
design until every reportable organization with rows is gated on. The
single backfill_preview call is one per-org job whose
nativeDeliveryEnabled gate returned false (a non-Rho org with rows, or an
unavailable PostHog read — the gate does not distinguish these; see the
attribution package below). Rho flags 285250 (field-value upsert) and
285273 (report delivery) remain held: a positive natural native outcome is
not yet verified — this is a Rails-only census and no API2 native log
was queried, so it proves neither presence nor absence of a native Rho
outcome.
Slack family. The single Rails Slack call is the Rho
POST /internal/slack_notifications at 2026-09-23T13:00:44.953Z,
conclusively before flag 286616’s activation at 19:44:52Z; there is
no post-flag Rails Slack call in the fixed 24 h. Flag 286616 stays
exact-Rho, held, pending a natural Rho send with ok:true + ts
(native outcome likewise not yet verified).
Decisions. No widening, replay, manual send, data write, deploy or
merge. Next focused packages: (a) attribute which organizations keep the
combined email on Rails and why a per-org gate returned false, from the
report tick/org job logs; (b) operator-owned widening of 285273 only after
Rho native backfill_preview evidence; (c) Slack canary outcome from the
next natural Rho send; (d) source attribution of the two /agent/*
forwarded 404s (public track). All internal-effects and gateway 24 h and
7 d post-cutover zero gates remain open; no credit taken.
Coordinator status update at 2026-09-24T13:37Z (no new traffic evidence)
Evidence access boundary. The coordinator’s two configured AWS SSO
profiles (default and AdministratorAccess-879381255132) both returned
Token has expired and refresh failed on sts get-caller-identity, and
the automation (Devin) box has no AWS credentials at all, so no newer
CloudWatch census was taken. 07:40:00Z remains the latest exact cutoff: US 19
internal calls / 24 h (mark_failed 15, combined_email 2,
backfill_preview 1, Slack 1), EU 0; 7 d 71,299 (historical). The
04:00Z/20:00Z report ticks and any natural Rho field-upsert, report or
Slack outcome that occurred between 07:40Z and 13:37Z are
unobserved, not absent; no zero-window credit accrues from the gap.
Deployment uncertainty. afabdfcd (all four API2 services, complete
07:25:58Z) is the last verified running SHA. Coordinator
git ls-remote origin refs/heads/release-prod at 13:38Z returned
c3a17f2d66d309c8eb6b3707e28c90b3a2d579eb — branch state only; whether
ECS has rolled past afabdfcd is unknown without AWS/ECS reads. No
deployment claim is made from the branch.
PR state. This docs PR (#12890 ,
head 143e8339) and the report attribution package
(#12932 , head
2db22176, ENG-3419 ) are
open and green but REVIEW_REQUIRED with no Ronald review; neither is
merged, so combinedEmail.railsGatedOrganizations attribution is not
yet deployed — the next ticks’ combined-email path still does not name
the gated-off organizations.
#12839 (ownerReason)
is in the same state.
Decisions. Rho flags 285250, 285273 and 286616 stay exact-Rho and held; no widening, replay, manual send, data write, deploy or merge. Next gate: a fresh regional 24 h / 7 d census with per-family method/path and the pre-cutover-vs-live split, once AWS read access returns; until then all 24 h / 7 d post-cutover zero gates remain open with no credit.
Coordinator audit at 2026-09-24T15:40Z and the exact-Rho Slack canary rollback (ENG-3428 )
Exact 24 h Rails /internal census (CloudWatch, both regions, cutoff 15:40:00Z).
| Region | Method / path | Status | Count | Latest | Classification |
|---|---|---|---|---|---|
| US | POST /internal/daily_agent_evaluation_reports/combined_email | 200 | 2 | 2026-09-24T04:00:30Z | Live: cron combined email stays Rails while any contributing org lacks native_delivery (flag 285273 exact Rho) |
| US | POST /internal/daily_agent_evaluation_reports/backfill_preview | 200 | 1 | 2026-09-23T20:00:15Z | Live: non-Rho preview, Rails by fail-closed design |
| US | POST /internal/slack_notifications | 201 | 1 | 2026-09-24T13:00:34.969Z | Live: Rho fallback after native owner miss (below) |
| EU | QA bulk_create / recap | — | 5 | — | QA track (ENG-3400 ); not a report/Slack residual |
US total 4, EU total 5. Seven-day internal US 16,229 + EU 26,915 = 43,144,
historical / pre-cutover dominated. mark_failed (both the upload and
evaluation-check-run families) has rolled out of the 24 h window for the
first time. No /internal call in the 15:34–15:40Z slice, but the API2
a6b5071 rollout completed only ~15:33Z, so that slice is a ~7 min
post-rollout observation, not 24 h credit.
Slack canary regression. Flag 286616 (api2_slack_notification_native,
exact rho-tnkw) was evaluated true at 13:00:01Z. API2 then logged two
warnings Active Rails Slack data source 11 not found and
resolveNativeSlackDelivery returned null, so the notification went to
Rails (201 at 13:00:34.969Z). Fail-closed behaviour held: no send was
lost and no double-send is possible on this path. The coordinator disabled
the flag at ~15:47Z (inactive, version 2). Native { ok: true, ts } has
never been observed for Rho; the canary produced negative evidence only.
Root cause boundary (code, read-only). API2
loadSlackAccessTokenForRailsDataSource selects organization_data_sources
with id = 11 AND organization_id = 19 AND data_sources.key = 'slack' AND discarded_at IS NULL. Rails Internal::SlackNotificationsController#create
resolves OrganizationDataSource.find_by_prefix_id! under the Rho tenant —
(id, organization_id) only, with no kept scope and no data-source-key
check — and #slack_client posts using data['token'] regardless. Because
Rails located and used ODS 11 for Rho, the API2 miss is one of two
production-data states: ODS 11 is soft-discarded (Rails still posts through
discarded connections) or its data_sources.key is not slack. Prefix
decoding (connection) and org resolution are correct. Distinguishing the two
needs a read-only operator query
(SELECT id, organization_id, data_source_id, discarded_at FROM organization_data_sources WHERE id = 11); the automation has no production
DB or AWS access and performed no production read or write.
Package. #12976
(ENG-3428 ) keeps the strict
native lookup (loosening it would replicate Rails’ post-through-discarded
behaviour rather than parity with the intended one) and makes the miss
attributable: SlackDataSourceNotFoundError carries
reason ∈ {missing, other_organization, discarded, wrong_data_source} and the
actual key, logged as missReason / dataSourceKey on the existing
falling back to Rails warning. No routing change; 11/11 focused tests;
user-merged 17:08:54Z without a submitted Ronald review (see 17:13Z).
Decisions. 286616 stays disabled; re-enable only after (a) the
operator query identifies the ODS 11 state and, if the row is discarded or
non-Slack, a human decides whether Rails’ behaviour or API2’s is the intended
one, and (b) a natural Rho send then yields native ok:true + ts, zero Rho
Rails Slack calls and no Sentry error. 285250 and 285273 remain exact-Rho
and held with no natural positive outcome verified. No widening, replay,
manual send, data write, deploy or merge. All 24 h / 7 d post-cutover zero
gates remain open; the Slack family’s window restarts from any future
re-enable, not from 15:47Z.
Coordinator status update at 2026-09-24T17:13–17:17Z (merges, G1 completion, deployment boundary)
Merges. #12976
(ENG-3428 ) was user-merged at
17:08:54Z without a submitted Ronald review. It is attribution-only
(miss-reason on the fail-closed Slack fallback); it changes no routing and
does not by itself justify re-enabling 286616, which remains disabled.
This docs PR (#12977 )
is still open.
Deployment boundary. At 17:13Z ECS API2 was still running 28cd9ee8
and Rails its prior task definitions. By ~17:17Z all US/EU Rails and API2
web+worker services had new tasks serving e504aad3 (release sync
#12985 , commit
17:08:42Z), rollout IN_PROGRESS draining old tasks. e504aad3 predates
the #12976 merge (17:08:54Z), so the Slack miss-reason attribution is not
deployed by this rollout; a Rho Slack attempt would still log the
unclassified not found warning. Later syncs
#12987 /
#12988 carry the merge
commits but had no observed ECS rollout. Branch state is not runtime state.
G1 regional index reconciliation. Coordinator read-only preflight before
the migration: US 2,398 public indexes, 4 invalid, 3 _cc leftovers, all
six composites, unsourced index absent; EU 2,395, 6 invalid, 4 _cc
leftovers, only the compliance composite, unsourced index present. The Rails
G1 migration (schema_migrations marker 20260924155417) then completed in
both regions: 2,395 public indexes each, identical name+definition md5
8d73d977a51edc8baaf2f21fff92606c, zero invalid indexes, zero leftovers.
G1 is therefore satisfied; G2–G7 (per-region regeneration, two-region probe
exit 0, expression-index strategy, non-declarable owners, baseline/ordering,
migration-freeze CI) remain open. Figures are coordinator-supplied; the
automation ran no production query or DDL.
Traffic. 15:40–17:10Z: zero Rails /internal and zero direct
conversation-image calls in both regions. This is a 90-minute slice with
scheduled work (20:00Z / 04:00Z report ticks, QA recaps) still ahead of
it, so it is an observation only — no 24 h / 7 d credit.
Next. Combined-email/report and Slack are pursued as one grouped
internal-effects package: report side needs the 20:00Z tick after
railsGatedOrganizations is live to name the Rails-gated organizations;
Slack side needs the operator ODS 11 read (discarded vs non-Slack key) and a
human parity decision before any exact-Rho re-enable. Held flags unchanged.
Coordinator read-only investigation at 2026-09-25T02:15Z: 17 Rails /internal calls, ODS 11 resolved, Kuda named (ENG-3379 , ENG-3428 , ENG-3419 )
Coordinator-supplied evidence (CloudWatch plus read-only US/EU production DB
reads; the automation ran no production query). Exact trailing 24 h ending
2026-09-25T02:15Z: 17 Rails /internal calls, all preceding the
315a8404 API2 rollout that completed in both regions at ~02:21Z, so all
17 are historical relative to the running SHA.
| Region | Family (all POST) | Count / status | Classification |
|---|---|---|---|
| US | conversation_transcription_reconciliations | 200 ×5 | 18:45Z cluster attributed by #13003 (ENG-3435 ) to PostHog unknown_error/null reads under the old fail-closed owner gate; 23:30Z API2 worker logs recorded owner=rails ownerReason=posthog_unavailable for org 49 (0 candidates) and Kuda org 25 (48 candidates, 3 batches) |
| US | qa_sampling_recoveries | 200 ×3 | same 23:30Z posthog_unavailable cluster, nonempty customer work |
| US | daily report combined_email | 200 ×2 (04:00Z, 20:00Z) | 20:00Z worker log: source=rails status=sent railsGatedOrganizations=[kuda-pilot] — the Rails-gated organization is Kuda, outside exact-Rho flag 285273 |
| US | daily report backfill_preview | 200 ×1 (20:00Z) | organizationSlug=kuda-pilot source=rails "queued backfill preview plan"; rows collected in API2, delivery not native |
| US | slack_notifications (Rho) | 201 ×1 (13:00Z) | native attempt missed the active-source predicate and fell back; flag 286616 disabled since ~15:47Z |
| EU | QA evaluation_requests/bulk_create | 200 ×3, 404 ×1 | all predate the 17:45:18Z rollout carrying #12978 / #12991 ; the 404 was a Rails org/membership lookup failure (logs do not distinguish which) |
| EU | QA send_bulk_recap | 200 ×1 | same pre-rollout window |
Post-rollout slice. 02:21–02:46:20Z zero Rails /internal and zero
direct image calls in both regions — ~25 minutes, observation only, no
24 h / 7 d credit. Rails stays 3/3 per region.
QA sampling (#13003 deployed). The running SHA removes PostHog from the
production owner decision and defaults to API2 (ownerReason=native_default)
with Flipper pause_qa_sampling_recovery_native as rollback. 02:30Z cycle:
US 39 API2-owned org runs / 57 candidates, EU 16 / 145; 02:45Z: US 39/57,
EU 16/143; zero candidate, transcription, or unprocessed failures either
region. Remaining Rails selectors: Flipper read unavailable, pause switch on,
or an environment override of rails — each must be accounted for before
any scale-to-zero claim.
Daily reports. Rho rows were collected natively (1 row at 04:00Z, 0
at 20:00Z) but no native Rho preview/email delivery claim has been
found since Sep 23; collection is not delivery proof. Combined email remains
Rails whenever any contributing report lacks native_delivery:true, and the
gate fails closed to Rails on a PostHog error. Do not widen 285273 to Kuda
on this evidence. Order: natural Rho preview/email success with ledger
proof → bounded Kuda canary (preview run + email outcome, zero Rails calls,
no regression) → reviewed removal of the PostHog fail-closed Rails dependency
→ re-observe 04:00Z/20:00Z ticks.
Slack root cause — resolved read-only. Rho Rails
organization_data_sources.id=11 is a Slack source discarded
2026-06-30; Rho’s only other Rails Slack source (id 55) is also
discarded. Rails’ notification controller looks the source up without an
active-source condition and still posts; API2’s native resolver requires
discarded_at IS NULL and therefore rejects it — the discarded branch of
the #12976 classifier (and of the #12998
flag-off readiness probe, still open). API2-side: two Slack connections with
legacy rulebase_id are active but hold no encrypted API2 token; a third
modern active connection has an encrypted token and external account but no
legacy rulebase_id (last_used_at=2026-09-25T01:01:07Z, no
last_error_at, channel sync present — readiness context, not a
notification outcome). Re-enabling 286616 now would repeat the miss.
Required before any re-enable: a human parity decision (bind the agent Slack
tool to the modern API2 connection vs intentionally honour the legacy
discarded-source semantics), a reviewed implementation with focused tests for
discarded/stale mappings and the delivery response, deploy, then one natural
Rho chat.postMessage ok:true with ts, zero Rails fallback, no error.
Slack parity decision (2026-09-25, Chidi): bind to the modern API2
connection. The native path must not mirror Rails’ discarded-source
semantics. Implementation #13023
(ENG-3428 ; codex /
codex-automation, Ronald requested, open — no merge before Ronald
approval and green required checks): after the 286616 flag gate, prefix
decode and Rails-org resolution, resolveNativeSlackDelivery looks up the
organization’s connections row with provider='slack',
auth_type='oauth2', status='active' and a nonempty encrypted token;
exactly one match → decrypt and post with it (tokenSource=api2_connection);
zero or ≥2 matches → the existing strict Rails ODS token path
(tokenSource=rails_data_source, which for Rho still misses on the discarded
row and falls back to Rails); any lookup/decrypt error → null (Rails,
fail-closed). Legacy legacy_rails_passthrough rows are excluded by auth type.
Focused tests: owner unit tests (preference, ambiguity, lookup failure,
flag-off, no token in logs) plus a pglite predicate test; typecheck clean;
required checks green at head e60afb0c6 after merging main (the earlier
shard-5 red was main’s scorecard_version_id drift, fixed by
#13024 ). Tokens are
never logged. No flag, data, deploy or provider change was made.
Slack gate (ordered). (1) Ronald approval + green required checks →
human merge of #13023; (2) release-prod sync and verified ECS running SHA
containing #13023 on all four API2 services (branch state is not runtime
state); (3) operator re-enables 286616 exact rho-tnkw only (automation does
not touch the flag); (4) one natural Rho agent Slack send logging
tokenSource=api2_connection, chat.postMessage ok:true with nonempty
ts, zero Rails POST /internal/slack_notifications for Rho, no Sentry
error; (5) 24 h then 7 d zero-Rails-Slack windows counted from the re-enable,
not from the merge; wider rollout is a separate human step. Rollback at any
step = disable 286616 (Rails path unchanged). Interim: flag stays disabled;
#12998 (flag-off
readiness probe) remains open and would log missReason=discarded for ODS 11
once deployed.
Report-delivery gate (ordered). (1) natural Rho 04:00Z/20:00Z tick
with native preview and email delivery proven in the delivery ledger
(collection alone does not count; none found since Sep 23); (2) bounded
exact-Kuda 285273 canary — operator action — requiring one preview run + one
email outcome, railsGatedOrganizations=[], zero Rails combined_email /
backfill_preview calls, no regression; (3) reviewed removal of the PostHog
fail-closed Rails dependency; (4) re-observe both daily ticks for 24 h / 7 d.
Rollback = flag back to exact Rho (Rails combined email resumes). Do not
widen 285273 on the current evidence.
Report-delivery package for gate step (3) — prepared, unmerged.
#13097
(ENG-3419 ; both codex labels,
Ronald requested, no submitted review) moves the delivery-owner decision out
of PostHog into daily-agent-evaluation-report/delivery-owner.ts, the same
shape as the QA sampling owner in #13003: env_override →
non_production → Flipper kill switch
pause_daily_agent_evaluation_report_native_delivery (paused /
flipper_unavailable → Rails) → native_default (API2). The boolean gate
pauses every organization; an Organization;<id> actor holds one tenant
(e.g. Kuda) on Rails while the rest run natively, so rollback no longer
depends on PostHog. The PostHog collection gate in rollout-gate.ts is
unchanged; railsGatedOrganizations / source attribution on the combined
email is unchanged and each org now also logs owner + ownerReason. Rails
endpoints and client functions are retained. Merge precondition: an
absent Flipper feature means native for every reportable org, so merging
cuts Kuda’s delivery over — it must follow gate steps (1)–(2) above, or an
operator must enable the pause gate (boolean or exact-Kuda actor) in Flipper
before the deploy so runtime behaviour is unchanged at rollout and orgs are
un-paused deliberately. Evidence basis is only the 02:15Z investigation
already recorded here; no fresh production outcome is claimed and no flag,
data, deploy or provider change was made. Once shipped, 285273 is dead for
delivery (still read for collection) and is removed with the Rails endpoints
after the 24 h / 7 d zero-Rails windows.
EU import/recap. Deployed import code calls native
bulkCreateUserRequestedQaEvaluationRequests and sendBulkRecapEmail; the
former Rails client functions are removed. Read-only US+EU counts since
17:45:18Z: zero new api2.evaluation_request_imports and zero
qa_evaluation_request_import_recap:% delivery claims — no natural native
import→recap outcome yet. Latent dependency in the running SHA:
evaluation-request-imports.ts still calls ensureImportedConversation
(Rails POST /internal/conversation_imports/ensure, which may import or sync
provider data) when a CSV row’s conversation is absent or unsynced; it
produced no call in this window but blocks claiming the import journey is
API2-owned. Required: reviewed native ownership (or proven safe removal) of
the ensure branch, tests for absent/unsynced conversations and invalid
membership, a natural EU import creating requests plus native recap
claim/provider outcome, zero Rails import-family calls through full windows.
No customer job replay.
Decision. Rails 3/3 per region; Kuda report delivery and Rho Slack
stay off (Slack re-enable only after #13023 is reviewed, merged and verified
running); 24 h / 7 d post-cutover zero windows remain open; public.*
G2–G7 is an independent shutdown blocker.
Coordinator census at 2026-09-24T15:40Z and the native import bulk-create package (ENG-3429 )
Evidence. Coordinator CloudWatch, fixed cutoff 2026-09-24T15:40Z,
trailing 24 h: Rails /internal US 4, EU 5. Every EU call is
Internal::QAEvaluationRequestsController: bulk_create 200 ×3 (latest
13:10:22.595Z), bulk_create 404 ×1 (09:45:58.263Z), send_bulk_recap
200 ×1 (10:06:26.513Z). API2 a6b5071 completed on all four services at
about 15:33Z, so all five calls precede the running SHA; no post-rollout
zero window is claimed from this cutoff.
Classification: meaningful live work, not residue. Static trace on
origin/main 57e04fd27:
- API2
evaluation-request-imports.tssubmitEvaluationRequestRows(CSV and agent-selection evaluation-request imports) posts every submit-ready row to Railsbulk_createand every already-queued row totag_bulk_import;syncEvaluationRequestImportRowspostssend_bulk_recaponce no tracked row is open. - Rails
bulk_createrunsQAEvaluationRequest::UserRequestedCreator(conversationwith_lock, supersede scheduled requests,save!,process_later→ API2 orchestration) and answers per conversation withcreated,already_queued(tagging the active request with the import),not_foundorfailed. The whole-batch 404 fires only whenOrganization.friendly.findorMembership.find_by_prefix_id!misses, so the09:45Z404 is one import whose organization/membership lookup failed in Rails; per-conversation misses are 200 responses withnot_foundrows. send_bulk_recaprunsBulkImportRecapNotifier→QAEvaluationRequestMailer.bulk_completed.deliver_later: a real customer email through GoodJob.
API2 already owned the single-conversation twin
(createUserRequestedQaEvaluationRequest behind
POST /conversations/:id/qa_evaluation_requests); the bulk import path was
the last reason evaluation-request imports still went through Rails.
Package. qa-evaluation-request/bulk-user-requested.ts:
bulkCreateUserRequestedQaEvaluationRequests reproduces the controller’s
per-row contract on the native creator (tenant-scoped conversation lookup →
not_found; membership of another organization → failed; missing
membership → throws, the 404 twin; QaEvaluationRequestConflictError →
already_queued plus import tagging), and tagQaEvaluationRequestsForImport
reproduces BulkImportRequestTagger (only untagged rows of the tenant).
submitEvaluationRequestRows uses both; the Rails client functions
bulkCreateQaEvaluationRequests and tagBulkImportQaEvaluationRequests are
deleted. Lock safety is unchanged from the creator: SQL-only transaction,
BullMQ enqueue and Action Cable broadcast after commit. Covered by
bulk-user-requested.db.test.ts (created / not_found across tenants /
already_queued tagging / cross-tenant membership / missing membership /
tagger scoping).
Out of scope. send_bulk_recap stayed on Rails in this package; the
native port is ENG-3431 below. The
Rails bulk_create / tag_bulk_import routes stay deployed as rollback
until a complete post-deploy window shows zero calls.
Gates and rollback. Canary is the carrying deploy. Watch
bulk-user-requested-qa-evaluation-requests warn logs, imports failing with
Membership not found (the former Rails 404), and import-created
qa_evaluation_requests rows carrying stage_progress.processing_owner=api2
with api2_enqueue.outcome=accepted. Rollback is a revert of the PR; the
Rails endpoints remain. No flag, deploy, replay or production write in this
package. Next evidence: a regional 24 h census after the carrying deploy with
QAEvaluationRequestsController split by action; expected shape is
bulk_create 0, tag_bulk_import 0, send_bulk_recap ≥ 0.
Native bulk_completed recap email (ENG-3431 )
Rollout state (user-reported, 2026-09-24). #12978 (ENG-3429) was
merged at 17:09:27Z without a submitted Ronald review and is carried by
release-prod syncs #12987/#12988. ECS was serving 28cd9ee8 at 17:13Z and
e504aad3 (sync #12985, 17:08:42Z, includes G1 #12980) at ~17:17Z with
old tasks still draining; both SHAs predate the #12978 merge, so #12978 is
not deployed and no native import outcome or bulk_create-zero window may
be claimed until a SHA that contains it is confirmed running on all four
API2 services. The 15:40Z census above therefore remains pre-rollout
evidence for both packages.
Evidence. The single remaining QA-family residual is the EU
send_bulk_recap 200 at 2026-09-24T10:06:26.513Z. It is live customer
work, not residue: maybeSendBulkRecapNotification fires once every tracked
row of a submitted import is terminal and Rails then emails the requesting
member (QAEvaluationRequestMailer#bulk_completed, deliver_later).
Decision. Port the mailer natively
(rulebase-api2/src/lib/qa-evaluation-request/bulk-recap-email.ts +
mailer-chrome.ts, a literal port of _chrome.html.erb). Eligibility
follows BulkImportRecapNotifier#call (membership account with an email,
≥ 1 request tagged with the import in the same organization, all of them
completed|skipped|failed|cancelled); counts, created_at order, the
10-ticket cap, (overall_score * 100).round, conversation and /home/tasks
links, both subject lines and the %b %d, %Y at %l:%M %p %Z footer in the
organization timezone match the Rails templates. Transport is Resend via
sendAgentEmail from Rulebase <notifications@rulebase.co>. At-most-once
delivery uses the existing public.outbound_delivery_claims table with key
qa_evaluation_request_import_recap:<import_id>, also passed as the Resend
idempotency key; the claim is marked delivered only after Resend accepts,
then recapNotificationSentAt is set. A Resend refusal releases the claim
and throws so the BullMQ sync retries (parity with deliver_later); a lost
answer keeps the claim and later attempts report uncertain without
resending. No transaction or lock spans the provider call.
sendBulkEvaluationRecapNotification is removed from rails-client.ts,
leaving zero API2→Rails calls in the QA evaluation-request family. Covered by
bulk-recap-email.db.test.ts (send once + counts/copy/links, open request
blocks, no requests / cross-tenant membership, refusal retry, lost answer,
10-ticket cap and import scoping, timestamp format) with outbound HTTP
guarded.
Gates and rollback. Canary is the carrying deploy; the Rails
send_bulk_recap route stays deployed as rollback (revert the PR, no flag).
Evidence required before the route is deleted: (1) a SHA containing this
package confirmed on all four API2 services, (2) one natural import
completion producing exactly one outbound_delivery_claims row with the
recap key and delivered_at set plus one Resend message to the requester,
(3) a regional 24 h and then 7 d census with QAEvaluationRequestsController
split by action reading bulk_create 0, tag_bulk_import 0,
send_bulk_recap 0. Roll back on any qa-evaluation-request-bulk-recap
uncertain warning without a matching Resend message, or on an import
completing without a recap claim. No deploy, flag, replay or production
write in this package.
Rollout evidence at 2026-09-24T17:45–17:55Z (gate 1 met, gates 2–3
open). #12991 was
human-merged with no submitted Ronald review (main 8e8823d36). The
coordinator read at 17:45Z shows all four API2 web/worker services
COMPLETED on a62a5f66, which contains #12978 and #12991; the last EU
worker completed at 17:45:18Z. That is the earliest instant any
post-release zero clock for bulk_create / tag_bulk_import /
send_bulk_recap may start. The coordinator’s “zero Rails /internal
calls since 17:20Z in both regions” slice is in-rollout (e504aad3
and a62a5f66 tasks overlapping) and earns no credit; the 24 h and 7 d
windows run from 17:45:18Z.
Read-only checks from the automation (Sentry only; no AWS, database or
Resend access from this session): no new rulebase-api2 issue in the 3 h
to 17:55Z touches the import or recap path, and no bulk recap email not sent, failed to resolve import or qa-evaluation-request-bulk-recap
log line exists after 17:45:18Z. That is absence-of-failure only; gate 2
(one natural import completing with one delivered
qa_evaluation_request_import_recap:<import_id> claim and one Resend
message) needs an operator outbound_delivery_claims / Resend read and is
unproven. Pre-rollout note for the same family: import
01a0d315-c148-7a58-92bd-c0787dcf6fd4 logged failed to resolve agent selection import three times at 13:12:28Z, 13:15:59Z, 13:19:50Z
(Sentry traces 67f25a88…, 017eb79f…, e7840cdc…), on the SHA that still
posted to Rails bulk_create; whether it reached a terminal state or a recap
is unknown and is a coordinator read, not a gate for this package. No
replay, synthetic import or production write was performed.
Coordinator census fixed at 2026-09-24T19:40:00Z: post-release Rails
reconciliation call, zero clock reset (ENG-3435 ,
#13003 ). Trailing
24 h: US Rails POST /internal/conversation_transcription_reconciliations
HTTP 200 ×3 at 18:45:06Z, EU 0. That is after a62a5f66 completed at
17:45:18Z, so the post-cutover Rails-zero clock for the QA sampling family
(/internal/conversation_transcription_reconciliations and
/internal/qa_sampling_recoveries) is reset; no window starting at
17:45:18Z may be credited. The send_bulk_recap / bulk_create /
tag_bulk_import family recorded no call in the same census, but the
natural import-to-recap outcome (gate 2 above) remains unproven.
Attribution (read-only PostHog $feature_flag_called rows, no CloudWatch
access from the automation): the three sweeps for aircall-demo
(18:45:06.103Z), test-aircall (.105Z) and rho (.107Z) evaluated
qa_sampling_recovery_native with $feature_flag_error = unknown_error and
a null response (posthog-node 5.37.0); the eight other organizations in the
same tick evaluated true (flag v21, “Matched condition set”). A
non-boolean SDK result becomes posthog_unavailable in
readOrganizationPosthogFlag, resolveQASamplingRecoveryOwnership maps it
to owner: rails, and rails-run.ts calls the reconciliation endpoint once
per Rails-owned sweep regardless of candidates. The 2026-09-22 census shows
the same signature: exactly 4 unknown_error evaluations (17:00:05Z …
23:15:03Z) for the 3 US / 1 EU reconciliation calls recorded then. These
are meaningful live dependencies: each call runs the Rails reconciliation
and enqueues its transcriptions, triggered by transient PostHog errors under a
flag that has been 100% since 2026-09-19. Per-request CloudWatch↔PostHog
pairing is by timestamp coincidence within one second and is marked as
inference.
Decision: #13003 bakes native ownership into code (root AGENTS.md: finished
rollout → remove the flag and fallback branch; Flipper for gates whose safe
state is “on”). owner.ts stops reading PostHog; production owner is api2
(native_default) unless Flipper pause_qa_sampling_recovery_native is
enabled (paused) or unreadable (flipper_unavailable). rails-run.ts and
both Rails controllers stay as the rollback path; the PostHog flag is left
for the operator to archive after the deploy. Gates: canary is the carrying
deploy; rollback is enabling the Flipper switch (no deploy) on native sweep
errors or an admission drop across two ticks; the reconciliation /
qa-sampling zero clock restarts when a SHA containing #13003 is COMPLETED
on all four API2 services, and deletion still needs the exact regional 24 h
and 7 d zero census. No merge before Ronald review; no deploy, flag change,
replay or production write.
Retired prefixes, batch 2 (step 4, 2026-09-21, ENG-3344 )
Evidence. Operator 24 h legacy_rails_gateway census ending 2026-09-21:
1,131 forwarded US requests, prefixes conversations 1,076 (essentially all
POST /conversations/upload), admin 25, pages 12, agent 9, clients
1; everything but the uploads was scanner probing (/admin/.env,
/agent/vicidial.php, /pages/index.astro.mjs.map). A clean 44 h window
after the 2026-09-20 gateway deploy, measured on Rails’ own request logs
(so it also covers direct API2 fetches), shows Rails served only
conversations (2,002 US), conversation_images (758 US / 275 EU),
internal (232 US / 108 EU, blocked at the gateway) and integrations
(10 US). Static classification of the remaining 155 allowlisted prefixes
against the composed API2 router and every first-party client is in the
gateway prefix retirement runbook.
The 120 h window was not used: it straddles the reject cutover and the
MCP/Zendesk/webhook native cutovers and is contaminated by pre-cutover
behaviour. CloudWatch was not reachable from the authoring session; the
figures above were supplied by the operator.
Decision. 85 prefixes move from rails-public-route-prefixes.json to
retired-rails-route-prefixes.json (159 → 74): 25 whose every Rails
method/path has a method-specific API2 handler that does not proxy Rails
(each of the 48 routes gets an exact PUBLIC_ROUTE_OWNERSHIP_CONTRACTS
entry, so app.route-ownership.test.ts fails if a handler is removed or
downgraded to ALL), and 60 with no Rails-served traffic and no URL built
by any first-party client. /conversations, /conversation_images,
/rails and /integrations are left to their own packages. /admin and
/agent stay because Rails still serves authenticated routes with live
callers under them despite scanner-only census traffic; /zendesk-app stays
because the native sidebar handler still proxies Rails. Retiring a prefix
changes only the gateway’s handling of inbound public requests; API2’s direct
RAILS_INTERNAL_BASE_URL fetches are unaffected and are audited separately
before the Rails shutdown gate.
Rollback. As for /knowledge_base_documents:
LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS=forward on the API2 web service is the
immediate hold; reverting the PR is the permanent rollback. Post-cutover
proof: outcome=retired events in both regions should be scanner paths
only; any retired event for a path Rails previously answered with a
non-404 is a rollback trigger.
Empty-segment /conversations paths redirected to API2 (step 4, 2026-09-23, ENG-3371 )
Evidence. Operator 24 h legacy_rails_gateway census ending
2026-09-22T23:23Z, after the Cable removal and the #12686 prefix retirement
(API2 release-prod 05911a9b): 25 forwarded requests Rails answered
non-404 — EU GET /conversations/ 23, EU GET /conversation_fields 1,
US POST /conversations/upload 1 — and 394 forwarded 404s, mostly
empty-id /conversations//... paths plus scanner probes. Rails’ own logs
additionally show three US ConversationImagesController#show 200 direct
reads (EU: the synthetic not-a-signed-id probes only). CloudWatch was not
reachable from the authoring session; the figures were supplied by the
operator.
Classification.
| Traffic | Class | Owner |
|---|---|---|
EU GET /conversations/ 23 (200) and the empty-id /conversations//thread, //navigation, //applicable_scorecards 404s | Live first-party defect: API2-native clients emit an empty id (openapi-fetch substitutes "" verbatim into /conversations/{id}). Hono matches strictly, so no native handler claims the path, the wildcard forwards it, and Rails’ router squeezes // and drops the trailing slash, answering conversations#index or conversations#show with a bogus id | This package |
EU GET /conversation_fields 1 | Stale client; the UI has called native /conversation-fields since the object-field reads cutover | Stayed on the allowlist here; retired with /customer_fields and /employee_fields in the object-field batch |
US POST /conversations/upload 1 | Supported, covered by the upload-intake parity package | #12682 gates (digest backfill, Kuda response=true); digest gate now closed in code by ENG-3375 , see below |
US direct ConversationImages 200 3 | Legacy Rails-origin media URLs | #12685 telemetry; #12613 edge behavior, unapplied pending Ronald |
Remaining forwarded 404s | Scanners under live prefixes (/admin/.env, …) | Already rejected/forwarded per batch 2 |
Decision. The gateway now applies Rails’ path normalization before
giving up ownership: when the request path differs from its normalized form
(repeated slashes squeezed, trailing slash dropped) and a method-specific
API2 handler owns the normalized path, the gateway answers 308 to that
path (query preserved) and logs outcome=normalized with path and
normalizedPath. 308 keeps method and body, so the client’s retry is
served by the same route Rails would have resolved, but by API2 and behind
API2 auth. Preflights for a normalizable native path stay in API2. Paths
whose normalized form Rails still owns (/conversation_fields/ at the time;
/locations/ is the contract example since /conversation_fields was
retired) keep forwarding, so no compatibility contract changes. UNOWNED_PUBLIC_REQUESTS
gains normalized contracts for the four census shapes, checked against the
composed router. No flag, no data effect, no operator step.
Rollback. Revert the PR. Triggers: any outcome=normalized event whose
normalizedPath Rails answered non-404 in the pre-deploy window but API2
answers with a 5xx, or a first-party client surfacing redirect failures.
Post-deploy gate. One 24 h window in both regions with
outcome=forwarded for GET /conversations/ and /conversations//* at
zero, every normalized event carrying an expected normalizedPath; then
locate the UI call sites that emit the empty id (request ids are on the
event) and fix them at source.
Operator audit at 2026-09-23T03:20Z and the empty-id source fix (ENG-3381 )
Evidence (operator-supplied, trailing 24 h ending 03:20Z). Gateway
events US 1,216 / EU 4,171. Successful Rails forwards 25, all inside the
window overlapping the #12820 deploy at 00:56Z: US POST /conversations/upload 201 ×1, EU GET /conversations/ 200 ×23, EU GET /conversation_fields 200 ×1. EU forwarded 404s: 384 empty-id
/conversations//* plus 2 scanner probes. Removed /cable traffic US 1 /
EU 1,611. From 02:35Z through this 03:20Z cutoff, no non-health Rails
controller call in either region (see the 04:20Z refresh below for the
first later call). These are the same 25 successes recorded at 01:20:02Z and
02:20Z ageing through the window, not new forwards; the first fully
post-deploy slices remain at zero meaningful forwards. The Kuda native
upload canary is still unproven — no natural ciphertext_fallback,
response=true or outcome=created event has been observed — and absence
of forwards is not evidence for it.
04:20Z operator refresh (trailing 24 h ending 04:20Z; same shapes, one
hour later). Gateway events US 1,244 / EU 4,195. The 25 successful
forwards are unchanged and all pre-cutover residue. Forwarded 404s: US 8
scanner; EU 384 empty-id plus 2 scanner. Removed /cable US 1 / EU 1,611.
Exact 7 d gateway US 39,792 / EU 25,845, pre-cutover dominated, not a
post-deploy zero. Fully post-canary slice 02:35:13Z → 04:20Z: zero
outcome=forwarded in both regions (US 16 rejected 404; EU 44 Cable
removed + 50 rejected 404). Non-health Rails controller calls in the
slice: one US POST report combined_email 200 at 04:00Z, EU zero —
a direct Rails call on the report path, not a gateway forward, so it does
not alter the zero-forwarded reading. 04:05Z expiry ticks in both
regions: candidates=0, deferredToRails=0, rails=null. Direct Rails
image successful GETs: 24 h zero in both regions; exact 7 d US 2,594 /
EU 2,220 = 4,814, all pre-cutover. Kuda native intake still unproven; flag
not widened. The ENG-3378 coordinator
audits at the same cutoffs live in
#12834 and are the
authority for internal-effects, schema-gate and Rho report-delivery
(flag 285273) status; this subsection records only the empty-id / public
gateway view.
Source. The audited request set (GET /conversations/{id},
/{id}/thread, /{id}/navigation, /{id}/applicable_scorecards) is the
fan-out of prefetchConversation() / prefetchCriticalConversation() in
rulebase-ui/app/features/conversations/components/prefetch-conversations.ts,
called from list-row, drilldown-row and thread-item hover handlers and from
evaluation prev/next navigation. Neither function guarded conversationId,
and openapi-fetch substitutes "" verbatim, yielding /conversations/
and /conversations//…. The specific caller passing an empty id cannot be
pinned from source alone (ids are typed string, several are nullable at
runtime); the fix therefore sits on the shared prefetch entry points.
Change. Both prefetch functions return a resolved promise without
issuing a request for an empty or whitespace id, with focused vitest
coverage. UI-only; no API2, flag, data, or schema change. The #12820 308 normalized backstop stays in place.
Gate. One complete 24 h window per region after the UI deploy with
legacy_rails_gateway outcome=normalized for /conversations/ and
/conversations//* at zero and outcome=forwarded for the same shapes at
zero. Rollback: revert (no data effect).
Bounded operator activation still outstanding (not performed here). The
#12613 CloudFront /conversation_images/* behavior remains the only
remaining route for the three direct US media reads; its activation and
rollback criteria are unchanged in the
direct media section.
Legacy API-key digest gate closed in API2 (2026-09-23, ENG-3375 )
Evidence. Exact 24 h legacy_rails_gateway census ending
2026-09-23T01:20:02Z (supplied by the operator; CloudWatch not reachable
from the authoring session). Forwards Rails answered non-404: US
POST /conversations/upload 201 ×1, EU GET /conversations/ ×23, EU
GET /conversation_fields ×1 — every one timestamped before or overlapping
the #12820 deployment at
00:56Z, so they are pre-cutover residue of the ENG-3371 normalization
package, not post-deploy behaviour. First fully post-deploy slice
00:57Z–01:20Z: zero meaningful forwards; four US scanner 404s only.
The 7 d window was not refreshed at this cutoff; the last exact 7 d figures
remain those recorded above and still contain pre-cutover upload 201s
(~700–900/day), which must be kept separate from the 24 h count.
Residual chosen. With Cable deleted, normalization deployed and the
media edge change awaiting Ronald, the only supported public family still
forwarding is POST /conversations/upload, and its native owner has been
blocked since 2026-09-18 for one reason: nativeDecision() authenticates
before evaluating conversation_upload_intake_native, API2 resolved keys by
key_digest only, and Kuda’s key has no digest. Rails authenticates that key
via ApiKey.active.find_by(key: token) — an exact-equality match on the
encrypts :key, deterministic: true ciphertext.
Decision. authenticateExternalApiContext performs the same lookup
Rails does, in the same order: digest first; on a miss, and only when API2
has ENCRYPTION_DETERMINISTIC_KEY + ENCRYPTION_KEY_DERIVATION_SALT (the
same secrets API2 already uses to mint keys in api-key-writes), compare
the deterministic ciphertext of the presented token to api_keys.key with
active = true. No decryption, no plaintext at rest, no widening of who can
authenticate: the accepted set is exactly Rails’ accepted set. A ciphertext
match writes key_digest/key_prefix via COALESCE (never overwrites) and
logs event=external_api_key_auth outcome=ciphertext_fallback api_key_id=…,
which is the lazy, per-key form of Maintenance::BackfillApiKeyDigestsTask.
Without the env the code path is inert and behaviour is byte-identical to
today. pglite coverage: ciphertext auth + backfill then digest-only
re-auth, COALESCE non-overwrite, env-absent 401 with no write, inactive /
wrong-token / wrong-key-material 401s. No flag, schema, provider,
production-data or replay change by automation; the maintenance task stays
documented as the bulk alternative.
Canary gates (natural Kuda traffic only). (1) exactly one
ciphertext_fallback for Kuda’s api_key_id, then none; (2) first natural
$feature_flag_called response=true for kuda-pilot after that timestamp;
(3) conversation_upload_native_intake outcome=created ≈ Kuda’s daily
volume, Kuda unauthenticated_fallback → 0, no
invalid/storage_failed/insert_failed, handoff=accepted ≥ 99.9 %;
(4) 1:1 upload/blob/attachment/delivery/conversation reconciliation over one
24 h window; then Kuda outcome=forwarded path=/conversations/upload at zero
for a complete window before the proxy-removal PR.
Rollback. Flag to 0 % (proxy resumes within 20 s; Rails accepts the key
either way). Auth fallback: revert the PR, or unset the env on API2 to make
it inert. A backfilled digest needs no rollback — it is the value the Rails
task would have written. Triggers: any ciphertext_fallback for an
api_key_id that does not authenticate on Rails in the same window, any
rise in public 401s for previously-201 clients, or native intake outcomes
failing gate 3.
Legacy object-field prefixes retired (step 4, 2026-09-23, ENG-3383 )
Evidence (no fresh telemetry). The coordinator’s AWS SSO expired at
05:21Z, so this package makes no new production claim; it works from the
exact censuses already recorded above. Across the operator-fixed 24 h windows
ending 01:20Z, 03:20Z and 04:20Z, the only forwarded request under
/conversation_fields, /customer_fields or /employee_fields was one EU
GET /conversation_fields 200, timestamped before the 00:56Z #12820
cutover and already classified as stale-client traffic in the ENG-3371
table; the fully post-canary slice 02:35:13Z → 04:20Z had zero
outcome=forwarded in both regions. Static verification on main
(7861d24f4): no file in rulebase-ui, rulebase-admin, rulebase-beam-web,
rulebase-zendesk-app or api2 builds any of the three legacy URLs (the
0/0/0/0/1 api2 count in the 2026-09-21 classification was doc comments in
routes/object-field-reads.ts, routes/object-field-writes.ts and
lib/employee-payload.ts), no RAILS_INTERNAL_BASE_URL caller touches them,
and every first-party read/write already goes to the API2-native
/conversation-fields, /customer-fields and /employee-fields routes
(object-field-reads.ts, object-field-writes.ts). /company_fields was
retired in batch 2 for the same reason.
Decision. Same shape as /knowledge_base_documents: API2 owns the
boundary natively under a different path, so the three legacy prefixes move
from rails-public-route-prefixes.json to retired-rails-route-prefixes.json
(allowlist 74 → 71, retired 86 → 89) and are answered with Rails’ JSON 404
and outcome=retired without contacting Rails. Rails’ show route has no
first-party caller and no successor. PUBLIC_ROUTE_OWNERSHIP_CONTRACTS gains
the twelve native successor routes (object-fields family, anonymous 401),
and UNOWNED_PUBLIC_REQUESTS replaces the ENG-3371 forwarded contract for
/conversation_fields/ with retired entries for the legacy shapes (the
retired check runs before normalization). Classification detail is in the
prefix retirement runbook.
No flag, no data effect, no operator step; Rails routes unchanged.
Rollback. Revert the PR, or LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS=forward
on the API2 web service for an immediate hold. Trigger: any outcome=retired
event under these prefixes from a current UI build or a non-browser client.
Post-deploy gate. One 24 h window per region with outcome=retired for
the three prefixes at zero or stale-tab residue only, and no forwarded
event for them (the gateway no longer can). Kuda native intake remains
unproven and its flag unchanged; this package does not touch upload, media
or report paths.
Merge record. #12840
was merged by Chidi at 2026-09-23T06:40:54Z as 84199d09 with no submitted
Ronald review (human merge; not a retroactive approval). ECS at read time:
US API2 web TD1727 running release-prod 1c5886a (sync #12841, includes
#12840) 2/2; EU TD1745 c463d40 still IN_PROGRESS with 0 running behind
active TD1743 b905bc2; Rails 3/3 in both regions. The post-deploy gate is
therefore not credited: it needs EU rollout plus natural outcome=retired
telemetry per region, not the merge. #12838
(ENG-3381) was likewise merged by Chidi at 07:28:53Z as 3ac4f8509 with no
submitted Ronald review.
Scanner-shaped paths under live prefixes rejected locally (2026-09-23, ENG-3385 )
Evidence (operator-fixed CloudWatch cutoff 07:25Z; no query by this
session). 24 h gateway events US 20,731 / EU 28,648. Successful Rails
forwards 25, unchanged and all pre-cutover (US POST /conversations/upload
201 ×1; EU GET /conversations/ 200 ×23, GET /conversation_fields 200
×1). Forwarded 404s US 299 / EU 704 (EU = stale empty-id residue plus
scanners); removed Cable US 1 / EU 1,588. The slice 04:20Z → 07:25Z has
zero successful forwards in both regions and only scanner 404s (US 291 /
EU 318). Seven-day gateway US 59,304 / EU 50,722, pre-cutover dominated. These
windows are distinct from the 01:20Z/03:20Z/04:20Z censuses above and
scanner traffic is not counted as a meaningful residual anywhere in this
plan; the remaining meaningful post-cutover forward count is zero, the Kuda
native-intake canary is still unproven, and nothing here infers otherwise.
Source classification. The post-cutover forwards are dotfile and
PHP-style probes under live prefixes (/admin/.env, /admin/config.php,
/agent/vicidial.php). /admin (68 Rails routes, rulebase-admin
Admin::BaseController, 38 admin-app callers) and /agent
(Agent::BaseController) are live Rails families and stay on the allowlist;
retiring the prefixes is not justified by scanner traffic. The source proof
has two strengths. Dot-leading segments are unroutable: Rails’ default
dynamic-segment pattern [^\/.?]+ never matches a segment beginning with
., routes.rb declares no custom segment constraints and no literal dotted
path, and the only glob route (*filename under /rails/active_storage) is
excluded — every such forward is a guaranteed Rails 404. PHP/ASP/JSP/CGI
suffixes are traffic-proven, not routing-proven: none is a registered
Rails MIME extension (config/initializers/mime_types.rb is absent; the
suffix set is disjoint from json/csv/xml/html/pdf/zip/… by
focused test), routes.rb declares no such path and no first-party client
emits one; where the suffix lands on an :id(.:format) position (e.g.
/accounts/index.asp) Rails would route it, authenticate and fail the
lookup, so no legitimate request is lost, but the response is not
structurally guaranteed to be 404. Both forward classes spend a round trip
and the internal credential for nothing.
Decision. isScannerShapedPath() in
legacy-rails-gateway/compatibility-allowlist.ts: when the unknown-path
policy is reject, a path under an allowlisted prefix whose any segment starts
with . or whose last segment ends in .php|.phtml|.asp|.aspx|.jsp|.jspx|.cgi
is answered with Rails’ JSON 404 and outcome=rejected without contacting
Rails; /rails/* is exempt. Allowlist and retired lists are unchanged (71/89).
UNOWNED_PUBLIC_REQUESTS flips /admin/.env and /agent/vicidial.php from
forwarded to rejected and adds forwarded contracts for real routes under
the same prefixes (/admin/feedback_statistics, /agent/statistics) plus an
Active Storage dotfile that must still forward. No flag, no data effect, no
Rails change.
Rollback. Revert the PR, or LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS=forward
(which already bypasses this check together with the allowlist). Trigger: a
rejected event for a path a first-party client or Rails log shows as a real
route.
Post-deploy gate. One 24 h window per region in which every gateway
forwarded event resolves to a Rails route (no 404 forwards), and
rejected absorbs the dotfile/PHP probes. This changes the shape of the
scanner column only; the meaningful-forward gate, the Kuda gate and the
object-field gate above are unaffected.
Refresh (operator-fixed CloudWatch cutoff 2026-09-23T13:30:00Z, epoch
1790170200; read-only regional queries, none by this session; distinct from
the 07:25Z census). 24 h gateway US 20,601 / EU 28,508 = 49,109. Successful
Rails forwards 23, all old pre-cutover (US POST /conversations/upload 201
×1; EU GET /conversations/ 200 ×22 — the /conversation_fields success
has aged out of the window). Forwarded 404 US 297 / EU 672 = 969 (scanner
plus stale empty-id); EU removed Cable 1,437; direct Rails image successful
GET 0. Rails /internal 24 h US 44 / EU 1 = 45 (US: evaluation
check_runs mark_failed 13, expiry 12, upload mark_failed 6, field PUT 4,
reconciliation 3, reports backfill 2 + combined 2, Unit21 1, Slack 1; EU
reconciliation 1); all 13 evaluation mark_failed and the Slack call fall in
the 04:15Z → 13:30Z slice, EU 0 there. Post-04:15Z: 14 US Rails internal
calls, zero EU, and zero successful gateway forwards in both regions.
Seven-day: Rails internal US 50,375 / EU 72,598 = 122,973; gateway US 59,414 /
EU 51,524 = 110,938; direct image GET 200 US 2,200 / EU 1,829 = 4,029,
pre-cutover dominated. The 24 h and 7 d zero gates therefore remain open
(pre-cutover residue still inside both windows). Rollout: release-prod moved
to 2fd3b757 at 13:33:50Z; the running ECS SHA was not verified, so
neither the #12838 nor the #12840 deployed gate is credited. At that cutoff
#12839, #12845 and #12847 were open with green checks and no Ronald approval
(state superseded below); Rho flags held, no scale cut. Correction from the
19:30Z per-row reconciliation: the 24 h evaluation check_runs mark_failed
count is 15, not 13 (1 at 08:07Z, 6 at 09:07–09:08Z, 8 at
13:07–13:39Z).
Refresh (operator-fixed cutoff 2026-09-23T19:30Z; none queried by this
session; distinct from 07:25Z/13:30Z). 24 h gateway US 20,732 / EU
27,784. Zero successful Rails forwards observed in both regions — an
observation only: the window starts 2026-09-22T19:30Z, before the
#12838 UI source fix merged and deployed and before the post-cutover rollout
completed, so it is not an all-post-cutover 24 h window and does not meet the
24 h gate. Forwarded 404 618 (US 298 across 91 distinct paths / EU 320
across 89), all status 404 and probe-like, but not all match the
#12847 predicate: e.g.
POST /clients/MyCRL and POST /rails/actions (2 each per region) have no
dotfile segment or PHP/ASP/JSP/CGI suffix and will keep forwarding. Only the
dotfile/PHP-like subset moves to rejected after deploy; the remainder stays
in the separate 24 h forwarded-404 observation gate, which this package does
not close. EU removed Cable 1,431 (7 d gateway US 59,627 / EU 52,239,
pre-cutover dominated). The canonical 19:30Z figures — Rails /internal
24 h/7 d breakdown (US mark_failed 15), direct image GET 7 d, Slack
classification and the exact-Rho Slack canary (flag 286616), release
290c0ca4 attribution — live in the coordinator audit section
“Coordinator read-only audit at 2026-09-23T19:30:00Z”
(#12890 ) and are not
restated here. Both the 24 h post-cutover zero gate and the 7 d zero gate
remain open; deployed-SHA verification, the Kuda canary and the
object-field/empty-id post-deploy gates remain uncredited and no Rho flag or
scale change follows from this package. #12847 was red at 5e9790d6c on
API2 shard 4 (instruct-tool-ui-coverage missing the
create_agent_monitoring_monitor label, introduced on main by #12846 and
fixed on main since); it was re-merged with main for green checks and
still has no Ronald review. Both operator AWS SSO profiles expired at
2026-09-24T01:35Z; the next exact census is the operator-supplied 07:40Z
read below.
Refresh — exact fixed CloudWatch cutoff 2026-09-24T07:40:00Z, scanner
scope only. 24 h gateway US 1,124 / EU 2,260 events; zero successful Rails
forwards in both regions (observation only, no 24 h post-cutover or 7 d
zero-window credit). Forwarded Rails 404 US 10 / EU 2 = 12, classified per
path against the #12847 predicate as source-shipped (not deployed):
| Region | Method/path (×n) | Predicate | Rails routing |
|---|---|---|---|
| US | /admin/.env ×2, /admin/.git/config, /admin%2F.env (decodes to /admin/.env) | rejected (dotfile) | unroutable ([^\/.?]+) |
| US | /admin/index.php, /admin/images.php, /admin/vendor/phpunit/…/eval-stdin.php | rejected (.php) | no route/format; traffic-proven |
| EU | /admin/login.asp | rejected (.asp) | no route/format; traffic-proven |
| US | /admin | not rejected | no bare /admin route → Rails 404 |
| EU | /admin/index.html | not rejected (.html is a Rails format) | no admin/index route → Rails 404 |
| US | /agent/auth, /agent/identity ×1 each | not rejected | namespace :agent declares only conversations#index, statistics#index, agent_performance_reports; no first-party client (rulebase-web, rulebase-admin, beam, zendesk) references either path → Rails 404, caller unattributed |
So 8 of 12 match the predicate; 4 keep forwarding after deploy and stay in
the separate forwarded-404 observation gate. /agent/auth and
/agent/identity are not counted as scanners: they are unroutable in Rails
by source, but the external caller is unknown and needs attribution (user
agent / source IP) before dismissal. EU websocket_bridge_removed 703 are
stale/blocked upgrade attempts, not supported live Cable. Direct Rails
successful ConversationImages GET 0 / 24 h both regions. 7 d gateway US
59,470 / EU 52,383 = 111,853 and successful direct images US 1,627 / EU 964 =
2,591, pre-cutover dominated. API2 running SHA afabdfcd complete across the
four web/worker services; Rails 3/3 each; #12847 is not in that SHA, so no
outcome=rejected telemetry exists yet and no deployed gate is credited.
Refresh — exact fixed CloudWatch cutoff 2026-09-24T15:40:00Z, operator
read (session has no AWS access). 24 h gateway by outcome: US rejected
2,220 / forwarded Rails 404 16 / retired 12 / blocked 8; EU rejected
1,610 / forwarded Rails 404 2 / retired 2 / websocket_bridge_removed
224. Zero successful Rails forwards in both regions — observation only, no 24 h
post-cutover or 7 d zero-window credit. #12847 merged to main as
af5b2a4f7 at 2026-09-24T15:17Z (human merge, no submitted Ronald review —
no retroactive approval implied). The latest API2 rollout a6b5071 completed
~15:33Z; whether that SHA includes af5b2a4f7 is unverified from this
session, so the rejected counts above are attributed to the pre-existing
unknown-path (non-allowlisted) rejection, and the scanner branch is credited
no deployed telemetry. Post-a6b5071 slice 15:34Z–15:40Z: US no
gateway events, EU four rejected 404 only. Exact 24 h forwarded 404
method/paths (all GET), classified against the predicate:
| Region | Path (×n) | Predicate |
|---|---|---|
| US | /admin/.env ×6, /admin%2F.env ×5 (decodes to /admin/.env), /admin/.git/config ×1 | rejected (dotfile) |
| US | /admin/index.php ×1, /admin/images.php ×1, /admin/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php ×1 | rejected (.php, traffic-proven) |
| US | /admin ×1 | not rejected → Rails 404 |
| EU | /admin%2F.env ×1, /admin/.env ×1 | rejected (dotfile) |
17 of 18 match the predicate; bare /admin stays forwarded. No /agent/*
forwards in this window, so /agent/auth / /agent/identity remain
caller-unattributed from the 07:40Z window only. All 18 are treated as
pre-#12847-deployment residue ageing through the 24 h window; the deployed
gate needs a full 24 h per region with outcome=rejected observed for these
shapes and forwarded 404s limited to non-predicate paths. Direct Rails
successful ConversationImages GET 0 / 24 h; 1,506 / 7 d, pre-cutover.
Seven resources prefixes retired; wildcard-middleware ownership shadow fixed (step 4, 2026-09-25, ENG-3463 )
Evidence (no fresh telemetry). This package makes no new production
claim; the latest censuses remain the 07:40Z, 15:40Z and 17:45Z
windows above (zero successful Rails forwards in both regions, every
forwarded 404 under /admin or /agent, none of these prefixes present).
The trigger was code evidence: an exact-match ownership check against
bin/rails routes showed routes/org-directory-mutations.ts guarding
/teams/*, /partners/*, /memberships/* and /invitations/* with a
method-scoped wildcard middleware, which the gateway’s matchedRoutes
predicate counts as native ownership. Unported Rails routes under those
shapes — POST /invitations (InvitationsController#create) and
PUT /partners/:id — were being answered 404 locally with no
legacy_rails_gateway event, so no census could have surfaced them. This is
a live Rails dependency the gateway was hiding, not scanner cleanup.
Change. Middleware attached per route; native PUT aliases for the
Rails resources update verb on /assignment_queues, /compliance_agents,
/controls, /memberships, /partners, /teams, /work_schedules and
PATCH /roles/:id; the seven prefixes with full exact-method parity moved
to the retired list (allowlist 71 → 64). /memberships is retired with two
action-less Rails routes (POST /memberships, POST /memberships/:id/deactivate)
whose Rails answer is already 404. /partners and /invitations stay
live. UNOWNED_PUBLIC_REQUESTS pins POST /invitations and
POST /partners/123/nope as forwarded. Details and the step 2 addendum in
the prefix-retirement package.
No Rails, flag, data, schema or deploy change. Gate: one 24 h window per
region post-deploy with outcome=retired for the seven prefixes at zero or
stale-tab residue; outcome=forwarded for POST /invitations appearing is
expected and becomes the next port candidate.
Six compliance-program resources prefixes retired on PUT alias parity (step 4, 2026-09-26, ENG-3469 )
Evidence (no fresh telemetry; source audit only). This package makes no
new production claim. No AWS credentials were available to the authoring
session, so the latest censuses remain the recorded 2026-09-24 windows above
and the last recorded gateway window with zero successful Rails forwards;
none of them lists these six prefixes as forwarded, and no new zero window is
asserted. The trigger was an exact-match ownership audit on main
53c53de5d (ENG-3463 step 2 addendum: bin/rails routes against the
composed api2 router with wildcard registrations excluded). For
/agent_test_columns, /compliance_criterion_groups, /program_risk_factors,
/program_sampling_rules, /program_test_questions and
/test_risk_scenarios the only Rails method/path pair without an exact
api2 handler was PUT /:id, the Rails resources alias of update; every
first-party caller (rulebase-ui, admin, beam, core, api2
RAILS_INTERNAL_BASE_URL clients) uses rulebaseApi2.PATCH/GET/POST/DELETE
and none sends PUT or targets Rails for these prefixes (0 callers each).
Classification: no meaningful live traffic can reach Rails under these
prefixes except an unknown-client PUT, which is now answered natively.
Change. Native PUT /:id alias registered beside PATCH /:id with the
same route config and handler (the ENG-3463 pattern); six prefixes moved to
the retired list (allowlist 64 → 58, retired 96 → 102); six session-owned
ownership families (23 exact method/path pairs, anonymous 401) and retired
pins for never-routed paths under each prefix. Details in the
prefix-retirement package.
No Rails, flag, data, provider, job, scale, CloudFront or deploy change;
no operator activation performed. Gate: Ronald review and green required
checks, then one clean 24 h window per region post-deploy with
outcome=retired under the six prefixes at zero (non-zero = stale tab or
unknown client to trace before the next batch). Rollback: temporary
LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS=forward; permanent revert of the PR.
Remaining public-route gaps after this batch (same audit). Live
prefixes with unported Rails routes: /invitations (POST /invitations
create, GET /invitations/download_template,
GET /invitations/import/bulk/download_template, plus action-less
PATCH/PUT/DELETE /invitations/:id), /partners
(GET /partners/:partner_id/employees), /conversations (23 of 66 pairs),
/accounts (7 of 30), /admin (61 of 68), /agent (4 of 4). POST /invitations remains the next port candidate. Direct
/conversation_images/:signed_id Rails-origin media is unchanged: the
ENG-3297 CloudFront behavior
is still unapplied in both regions pending human/Ronald activation review.
/cable bridge removed; conversation-upload proxy removal gates
/cable WebSocket bridge — deleted (2026-09-20). The bridge is no longer
a gated compatibility path awaiting a seven-day zero window. Gate 1 was already
met: #12426 (6970f3248,
2026-09-17) removed Action Cable from rulebase-ui, so no shipped build opens
/cable, and every first-party channel family has a deployed SSE owner. The
017b86ef cutoff (boundary 2026-09-20T01:48:40Z) then drove
websocket_forwarded to zero in both regions; the only residue was EU
websocket_bridge_disabled events from stale tabs.
Chidi decided not to hold the adapter for the remaining window: “we should
completely delete cable window. it’s fine, let clients fail and refresh and
switch over.” Accordingly legacy-rails-gateway/cable.ts, its test, the
LEGACY_RAILS_CABLE_BRIDGE_ENABLED environment gate and the http-proxy
dependency are deleted. legacy-rails-gateway/websocket-upgrades.ts replaces
them with a handler that answers every WebSocket upgrade with 404 and logs
/cable attempts as outcome=websocket_bridge_removed, so the census keeps
its stale-tab evidence without any Rails coupling.
Consequences, accepted. Stale tabs stop receiving live updates and get a
404 on upgrade until refreshed. Rollback is no longer config-only: restoring
the bridge means reverting the deleting PR and redeploying. Rolling the UI back
to a pre-#12426 build is therefore no longer supported, because that build
would re-create Cable demand with no upstream. This closes the /cable line of
the Rails shutdown gate — the bridge has no supported
consumer and is removed — and removes the last WebSocket dependency on Rails.
Legacy GET /knowledge_base_documents/:id (observed 2026-09-19T21:27:29Z).
The 24 h gateway window also holds exactly four forwarded US
GET /knowledge_base_documents/:id requests, all 200, all in the same
second, EU zero. Rails maps them to Kuda (org 25) manual knowledge documents
served by KnowledgeBaseDocumentsController#show (6 queries, 36–53 ms); the
same IDs appear in Kuda QA/evaluation context and the burst followed Kuda
backfill-fallback activity, so this is classified as meaningful QA-linked
traffic, not scanner residue, until disproven. Caller: no first-party code
on main emits that path — the UI’s Knowledge Library boundary moved to API2
GET /knowledge-library/documents/:id in #11781 (f626438b2, 2026-09-10)
and #11940 (c92f3bd2b, 2026-09-13), Rails builds no
knowledge_base_document_url, and the endpoint needs a session (200s rule
out scanners). The strongest explanation is the same stale-client pattern as
/cable: a pre-#11940 rulebase-ui bundle still open in a Kuda tab, loading a
cited document plus its neighbours. API2 already owns the native route, so no
new code is required for ownership; the legacy path is served only through
the compatibility allowlist (/knowledge_base_documents prefix in
rails-public-route-prefixes.json). Retirement package, when the evidence
gate passes (zero forwarded /knowledge_base_documents for 24 h on a release
newer than the stale build, or an explicit human decision as for Cable): drop
the prefix from the allowlist so API2 answers 404 locally, reversible by
re-adding the prefix. Not done in this PR; until then these 200s count as
live and keep Rails at 3/3 alongside uploads and Cable.
Direct Rails GET /conversation_images/:id (observed 2026-09-19T21:12:53Z
to 21:29:20Z). Outside the gateway census, US Rails process_action shows
11 successful ConversationImagesController#show requests in the window from
the 21:10:29Z cutoff through 21:34:51Z (EU zero), all on signed
/conversation_images/:id paths — mostly an Anti-Scam Email Footer.png
signature image plus a few image/IMG uploads. The API2
legacy_rails_gateway stream has zero /conversation_images events over
24 h while native_public_media independently served 85 US + 42 EU
conversation images in the same 24 h, so these 11 did not pass through the
API2 gateway and are not evidence that the native route failed: they reach
Rails by a separate direct/legacy-origin path. Candidate emitters in code:
Conversation::Sync::Base/Jira rewrite inline img[src] to
conversation_image_url(signed_id) on the Rails default_url_options host
(RulebaseConfig#api_base_url), so stored conversation HTML carries absolute
Rails-origin image URLs that any consumer (browser rendering an email footer,
Jira/Zendesk preview, API2 caption fetch via defaultImageOrigins) loads
directly; ImageUrlConverter rewrites such URLs to Active Storage blob URLs
only where it is applied. The controller is unauthenticated (signed blob ID
only), so 200s do not identify the caller. Classification: meaningful
direct Rails dependency until the origin host/consumer is identified; it is
outside the gateway retirement gates and does not change the 3/3 hold on its
own, but it must be attributed (host, referer family, consumer) before Rails
public ingress can be closed. Any code for this (e.g. emitting API2 media URLs
from sync, or a redirect) is a separately scoped follow-up, not part of the
Cable or knowledge-document packages.
Full-window census (both regions, fixed at 2026-09-19 ~21:45Z). The
11-call slice above understated the dependency. Direct Rails
ConversationImagesController#show served 194 successful requests in the
trailing 24 h (US 184, EU 10) and 7,221 successful requests over the full
trailing 7 days (US 3,633, EU 3,588), plus one EU 404 in the 7-day window;
latest US 21:43:17Z, latest EU 15:02:10Z. All bypass legacy_rails_gateway
telemetry. In the same 24 h API2 native_public_media independently served
98 US + 42 EU conversation images and one EU Active Storage redirect, so the
native route is healthy while legacy-host image URLs remain live. Reading:
treat 194/day as a meaningful live direct dependency (not stale-tab
residue — volume is steady in both regions) and the 7,221/7d as pre-cutover
plus live residual from absolute Rails-origin URLs embedded in stored
conversation HTML. Rails stays 3/3. Next step is exact attribution of the
old-origin emitter (host, referer family, consumer); any fix is its own
focused package after that, not mixed into #12564 or #12574.
Emitter attributed (read-only source trace). The live emitter is API2
itself, not only stale rows: provider-conversation-sync/embedded-images.ts
conversationImageUrl() builds every newly imported provider-conversation
image URL from RAILS_INTERNAL_BASE_URL, even though API2 already owns
GET /conversation_images/:signed_id natively (native_public_media) and the
repo has the regional getRulebaseApi2BaseUrl() helper (RULEBASE_API2_BASE_URL
is set per region in Flightcontrol). Every new import therefore mints a
Rails-origin URL, which explains steady direct Rails hits alongside healthy
native traffic. Decision: a separate focused package switches only newly
minted URLs to the regional public API2 origin (signed IDs and the
/conversation_images/ replay guard unchanged; API2 caption
defaultImageOrigins accepts the API2 host), with production-shaped US/EU
and replay tests. Existing stored rows are not rewritten, so the old-URL
tail keeps Rails at 3/3 until it ages out or a later, separately evidenced
package handles it. No deploy from this track.
Status at 2026-09-19T21:53:42Z. Gateway 24 h 6,786 = 5,918 locally
rejected/blocked + 33 Rails-forwarded 404 + 835 meaningful (728 US upload
201s, 101 EU /cable forwards, 2 old Cable upstream errors, 4 US
GET /knowledge_base_documents reads). Available structured 7 d 36,752 (not a
full seven days): 25,988 local/scanner + 4 client aborts + 10,760 meaningful.
Cable live through 21:49:04Z, so the cutoff is not deployable and Rails
cannot scale to zero. Direct (non-gateway) Rails calls since 21:10:29Z: 48 US,
0 EU = 25 QA-stage + 15 ConversationImagesController#show (through
21:43:17Z) + 4 knowledge-document reads + 4 Unit21 calls (through 21:50Z).
#12570 fully green awaiting human review; #12564 review-required with only the
known shard-1 halfvec failure; #12574 owns the knowledge-document prefix
retirement separately.
Status at about 2026-09-19T23:20Z. Rolling 24 h selected gateway events
6,429 = 5,593 blocked/rejected/forwarded-404 scanner traffic + 836
meaningful/residual: US 728 upload forwards (201, latest 19:15:23Z) + 4
GET /knowledge_base_documents forwards (200, latest 21:27:29Z) + EU 102
/cable forwards (latest 22:51:34Z) + 2 old Cable upstream errors (latest
02:04Z). No US Cable forward in 24 h; EU Cable remains live, so the
post-cutover zero clock has not started. Available structured 7 d selected
events 36,803 (US 27,736; EU 9,067), still not a full seven-day window. #12564,
#12574 and #12575 remain human-review blockers, unmerged; the transcription
priority package #12576 / ENG-3284 was closed as superseded by ENG-3283 /
#12577 and is no longer tracked here. No deploy, flag, data or replay change.
Status after release-prod 0a6fd1e775faabc7ea0cbc8df404ab3841fb9e69
(healthy; #12570, #12574 and #12575 human-merged and deployed). Rolling 24 h
gateway census 6,503 = 5,663 rejected/blocked/forwarded-404 scanner traffic
plus 840 meaningful (728 US conversation uploads, 4 knowledge-document calls
(the 21:27:29Z burst, still inside the rolling window), 106 EU /cable
websocket forwards, 2 old EU websocket upstream errors). Available structured
7 d 36,933, pre-cutover-heavy and not a post-cutover zero window. EU Cable
remains live after the deploy: websocket_forwarded at 23:45:02Z,
23:45:55Z and 2026-09-20T00:13:31Z, with Rails registering the connection
and streaming ConversationChannel at 00:13:31Z, so the cutoff in this PR is
still undeployed and its zero clock has not started. Two direct Rails
ConversationImagesController#show reads at 00:17Z are the expected tail of
already-stored Rails-origin URLs (new URLs mint on the api2 origin since
#12575); no backfill is planned. #12564 is green and review-gated. No deploy,
flag, data or replay change.
Fixed cutoff 2026-09-20T01:18:53Z (#12571 human-merged 01:19:47Z, #12589
01:20:05Z; release-prod 35c5dce7 includes both but is not yet proven
live). Gateway 24 h 6,272 = US 4,194 + EU 2,078: scanner/local 5,434 (4,910
rejected + 499 blocked + 25 forwarded 404) plus 838 meaningful or recent =
728 US POST /conversations/upload 201 (last 19:15:23Z, pre-cutover
residue), 4 US GET /knowledge_base_documents (last 21:27:29Z,
pre-retirement residue), 105 EU /cable websocket forwards (last 01:16:02Z)
and 1 EU Cable upstream error (last 02:04Z). Structured nominal 7 d 37,092
(US 27,907; EU 9,185; coverage began Sep 17 so not a full seven days) =
15,764 forwarded 404, 9,064 rejected, 1,484 blocked, 2,753 Cable forwards +
2 errors, 2,691 upload 201s, and older Zendesk/Unit21/media/Outreach residue.
Cable is the only meaningful gateway path still live at the cutoff; the cutoff
in this PR remains undeployed, its zero clock has not started, the adapter stays
reversible, and Rails stays 3/3. #12564 fully green, labels present, Chidi and
Ronald requested, unmerged. No deploy, flag, data or replay change.
Rails internal effects at the same 01:18:53Z cutoff (query-result sums,
corrected): 5,007 / 24 h (US 3,234 + EU 1,773) and 328,830 / 7 d (US
132,073 + EU 196,757). 24 h by family: reconciliation 3,009; call recording
794; direct upload 741; Unit21 144; counter cache 129; QA execute_stage 101;
expiry 48; transcription context/write 32; tiny tail 9 (field values 4, report
actions 4, Slack 1). EU 1,773 = reconciliation 950 + recording 743 + counters
56 + expiry 24. These are internal Rails-owned effects, separate from the public
gateway census above, and do not change the Cable gate.
Post-cutoff release 017b86ef fully deployed (release boundary
2026-09-20T01:48:40Z; census through 02:23:02Z). Trailing 24 h gateway
total 6,359 = US 4,187 + EU 2,172. Meaningful/recent old tail: 728 US
POST /conversations/upload forwards (last 19:15:23Z), 4 US GET /knowledge_base_documents 200 forwards (last 21:27:29Z) and 102 EU
/cable websocket forwards (last 01:28:02Z, before the boundary). Since
01:48:40Z there are zero forwarded or websocket_forwarded events in
either region; EU shows 39 websocket_bridge_disabled /cable events through
02:23:02Z (expected stale tabs, rejected locally), US has no
non-blocked/non-rejected gateway events. The Cable 24 h / 7 d zero clocks run
from 01:48:40Z; the adapter stays reversible and Rails stays 3/3. No deploy,
flag, data, replay or CloudFront change from automation.
Fixed cutoff 2026-09-20T03:30:00Z (first census with a >90-minute
post-cutoff window). Trailing 24 h gateway total 5,736 = US 3,431 + EU
2,305. Meaningful pre-cutover residue: 728 US POST /conversations/upload
201 (last 19:15:23Z), 4 US knowledge-document GET 200 (last
21:27:29Z) and 97 EU websocket_forwarded (last 01:28:02Z). Since the
Cable cutoff at 01:48:40Z there are zero meaningful forwarded or
websocket_forwarded events; the only post-cutoff forward is one US scanner
GET /admin/config.php 404 at 03:16:27Z, and EU logged 122
websocket_bridge_disabled stale-tab attempts through 03:29Z. Available
structured 7 d is 37,406 = US 27,967 + EU 9,439 (not a full post-cutover
window). The gateway is therefore quiet; the largest verified live public
dependency is now outside gateway telemetry — direct Rails
ConversationImagesController#show on the legacy-rails.* origins, see the
section below (194 successful reads in the same fixed 24 h).
Conversation-upload proxy (lib/conversation-uploads/proxy.ts). The
native intake and status routes exist behind the kuda-pilot
conversation_upload_intake_native canary created on 2026-09-18T09:36Z; use
that canary’s evidence rather than creating another. Remove the proxy only
when:
- the canary shows multipart, presign, status, and idempotency parity with the Rails responses for the pilot’s full request mix, with no durable handoff losses in reconciliation;
- the native path is default-on for every tenant in both regions and a
complete 24-hour window shows zero
legacy_rails_gatewayoutcome=forwardedevents for/conversations/upload*and/v1/conversation_uploads*other than stale-client retries; - a mixed-version rollback (API2 native on, Rails route retained) has been exercised once so the Rails route can still absorb traffic if the native flag is turned off.
Both removals ship as separate PRs that delete code only after their window completes; this package records the gates so they are not deleted blindly.
Superseded (2026-09-27). Gates 1–3 above assumed a live Rails upstream to compare against and to absorb rolled-back traffic. With Rails configured at zero replicas in both regions (#13164 ) neither the parity comparison nor the mixed-version rollback can be exercised, and the fallback itself can only fail. The proxy was therefore removed under ENG-3488 with a code-revert rollback and a post-deploy gate expressed on API2’s own terminal outcomes; see the ENG-3488 section.
Conversation-upload Rails proxy fallback and PostHog gate removed (2026-09-27, ENG-3488 )
Evidence available to the authoring session (no fresh production census; CloudWatch/PostHog event queries were not reachable).
- Source audit on
origin/mainc6c6557bd: fourproxyLegacyRailsRequestcall sites remain in API2 —lib/conversation-uploads/proxy.ts,routes/public-media.ts,routes/zendesk-app-sidebar.ts,routes/outreach-webhooks.ts. The upload proxy was the only one on a contract that produced a successful public Rails forward in any recorded census (USPOST /conversations/upload201×1 at2026-09-2301:20Z; ≈700–900/day before the canary). - PostHog flag
conversation_upload_intake_native(id280845, read2026-09-26): active, v1, exactkuda-pilotat 100 %,last_called_at2026-09-26T16:13:31Z, created2026-09-18T09:36:04Z, review by2026-12-16. A freshlast_called_atis advisory only (see the intake runbook); it does not prove native volume, and noconversation_upload_native_intakelog query was possible from here. - Rails capacity:
flightcontrol.cueonmainsets both production Rails services tominInstances: 0/maxInstances: 0(#13164) and #13167 made the file a production watch path. Whether the zero-replica hold has reconciled in Flightcontrol, and whether any upload forward has since producedupstream_error/timeout, is unverified from this session. - #13143 (Rails-web-dark)
merged as
c3cc5473d; its 24 h window ending2026-09-26T16:18:57Zis the published figure (six US internal requests, EU zero), not re-queried here.
Change. nativeDecision() in lib/conversation-uploads/public-intake-shared.ts
no longer evaluates PostHog and never proxies. It returns the terminal
response API2 owes the caller: 401 {"error":"Unauthorized"} (the Rails
ApiKeyAuth body) with outcome=unauthorized when the bearer token resolves
by neither digest nor deterministic ciphertext; 503 {"error":"Service Unavailable"}
with outcome=storage_unconfigured when the regional storage resolver has no
credentials. lib/conversation-uploads/proxy.ts is deleted and removed from
the RAILS_INTERNAL_BASE_URL source-gate allowlist; CONVERSATION_UPLOAD_INTAKE_NATIVE
is removed from posthog.ts. The five public upload contracts keep their
exact-route ownership pins and deprecated-alias headers. No Rails route,
flag, provider, data or deploy change; the Rails ConversationUploadsController
retirement is a later package.
Tests. public-intake.db.test.ts (PGlite composed app): 503/401
terminals, per-request conversation_upload_native_intake outcomes, bearer
token absent from every log line, proxyLegacyRailsRequest never called.
rails-internal-base-url.source-gate.test.ts fails if the module or
allowlist entry returns; app.route-ownership.test.ts unchanged (232 tests
across the three files green locally; pnpm typecheck clean).
Rollback. Revert the PR and redeploy API2. A revert only restores the
attempt to forward; a working Rails destination additionally requires
minInstances: 3 / maxInstances: 10 on rulebase-api-prod and
rulebase-api-prod-eu (#13164’s stated rollback) and a deploy. The PostHog
flag is not touched by the package; the operator archives it after the
revert window.
Gate (post-deploy; unverified until a census is run). One complete 24 h
window per region on the deployed release with
conversation_upload_native_intake outcome=unauthorized and
outcome=storage_unconfigured both zero for every organization that produced
Rails upload 201s in the last live census (Kuda), outcome=created at
Kuda’s daily volume with handoff=accepted ≥ 99.9 %, and the 1:1
upload/blob/attachment/delivery/conversation reconciliation from the intake
runbook §2c clean. Rollback trigger: any unauthorized for a known
customer key (means ENCRYPTION_DETERMINISTIC_KEY/ENCRYPTION_KEY_DERIVATION_SALT
are missing from the API2 task, or the key is undecryptable and must be
rotated with the customer), or storage_unconfigured above zero.
Merge record (review exception). #13196 was merged externally at
2026-09-27T09:56:19Z as df9b31c2ab by Chidi with no submitted Ronald
review (review had been requested). The package is on main; whether it
is in the running API2 release, and the post-deploy gate above, are
unverified from this session. The exception is also recorded on ENG-3488.
Not changed by this package. routes/public-media.ts,
routes/zendesk-app-sidebar.ts and routes/outreach-webhooks.ts kept their
explicit Rails fallbacks at the time; they are retired by the
ENG-3495 section
below. Public Rails-only routes still forwarded by the allowlist
(POST /invitations, invitation template downloads,
GET /partners/:id/employees, parts of /conversations, /accounts,
/admin, /agent) remain the next gateway candidates once a census can
rank them.
Last three route-level Rails fallbacks removed (2026-09-27, ENG-3495 )
2026-10-01 status correction. #13221 merged 2026-09-28T15:42:33Z as 9486119ea087daebbe399efeeeee3548831086f5; its final-head edge-routing and aggregate CI passed, but GitHub still reports REVIEW_REQUIRED and zero submitted reviews. This is a review exception, not Ronald approval. The historical authoring-session and ledger entries below retain their original timestamps; post-deploy native outcome/error parity remains unverified here.
Evidence available to the authoring session (source audit of
origin/main 6087ec7dcf; no CloudWatch, PostHog event or Flightcontrol
access, so no production window in this section is fresh).
-
After #13196, exactly three
proxyLegacyRailsRequestcall sites remained outside the wildcard gateway (app.ts), each guarded byLEGACY_RAILS_FALLBACK_ENABLED:Route Fallback trigger Terminal response without fallback routes/public-media.ts(/conversation_images/:signed_id, Active Storage redirect)Flipper pause_api2_public_media_routeson, or the blob’s service has no API2 storage clientempty 404routes/zendesk-app-sidebar.tsFlipper pause_api2_zendesk_sidebaron, orRULEBASE_BEAM_BASE_URL/ZENDESK_MARKETPLACE_APP_IDmissing503 Service Unavailableroutes/outreach-webhooks.tsOUTREACH_WEBHOOK_SECRETunset500 {"status":500,"error":"Internal Server Error"} -
With both production Rails services at
minInstances: 0/maxInstances: 0onmain(#13164), each branch is a guaranteed transport failure (upstream_error/timeout), not a rollback path: pausing the media route would return a gateway error instead of Rails bytes. Whether the zero-replica hold has reconciled in Flightcontrol is unverified here. -
Their published activation prerequisites are already met:
OUTREACH_WEBHOOK_SECRETprovisioned in Infisicalprod/production-euat2026-09-18T15:24Zwith unsigned canaries403in both regions (section above);RULEBASE_BEAM_BASE_URL=https://beam.rulebase.coin both environments (ENG-3290 note in the sidebar runbook). Both are published facts from earlier sessions, not re-queried now. -
ENG-3297 CloudFront
/conversation_images/*behavior: not changed by this session, but edge probes run from the authoring box at2026-09-27T15:43Zshow it is now applied in both regions (by someone else):GET https://legacy-rails.rulebase.co/conversation_images/invalidand the EU equivalent return404with API2 response headers (x-request-id,access-control-allow-origin: https://app.rulebase.co/https://eu.app.rulebase.co,access-control-expose-headers: Content-Disposition,Deprecation,Sunset,Link), while every other path on the same hosts (/up,/zendesk-app/sidebar) returns503fromserver: awselb/2.0(an ALB with no healthy targets, consistent with Rails at0..0having reconciled). This is inferred from response headers; the CloudFront distribution config was not read, no signed-id200path was probed, and no Rails-origin request count exists to close the direct-media gate. The repo-leveledge-routingcheck failed on its/up still Railsassertion for the same reason (Rails answers nothing);check.shnow also accepts a 5xx from the Rails ALB (server: awselb/2.0) on/upas “still on the Rails origin” and fails on anything else, so a repoint of more than the image path is still caught while Rails is at0..0. Theoutcome=loop/508guard the README listed as a post-apply check no longer exists because there is no fallback to loop. -
#13130 (ENG-3469) was refreshed against
main(b979211b52, clean merge, no conflict) and the previously skipped Rails prefix lintspec/lint/api2_gateway_compatibility_prefixes_spec.rbwas run locally against a throwawaypgvector/pgvector:pg15test database:2 examples, 0 failures. #13130 stays open with Ronald review as the merge gate.
Change. All three routers drop their fallback / fallbackEnabled
dependencies and end locally with the terminal response in the table. Media
logs legacy_rails_media_fallback with outcome=paused_not_found,
forwardedToRails:false (event name kept for query continuity; it now means
“left the native path and terminated locally”); the gateway-hop 508 loop
response is removed. Zendesk outcomes paused_fallback /
misconfigured_fallback are deleted; paused_unavailable /
misconfigured_unavailable remain. Outreach outcome
forwarded_secret_unconfigured is deleted; misconfigured remains. The
three modules leave the RAILS_INTERNAL_BASE_URL source-gate allowlist, so
app.ts (wildcard gateway, still governed by the prefix allowlist and
LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS) is the only gateway-fallback entry
left. The Flipper pause flags stay as incident switches and are now
fail-closed. No Rails route, flag, env, provider, CloudFront, data or deploy
change.
Tests. public-media.test.ts, public-media.gateway.test.ts,
zendesk-app-sidebar.test.ts, zendesk-app-sidebar.gateway.test.ts,
rails-internal-base-url.source-gate.test.ts, app.route-ownership.test.ts
(247 tests) and outreach-webhooks.db.test.ts (13, PGlite) green locally;
pnpm typecheck clean.
Rollback. Revert the PR and redeploy API2. As with ENG-3488, the revert
only restores the attempt to forward; a working Rails destination also
needs 3..10 on both Rails services (#13164) and a deploy.
Gate (post-deploy; unverified until a census is run). One complete 24 h
window per region on the deployed SHA with legacy_rails_media_fallback reason=paused at zero, native_zendesk_sidebar outcome in (paused_unavailable, misconfigured_unavailable) at zero,
native_outreach_webhook outcome=misconfigured at zero, and
legacy_rails_gateway outcome=forwarded at zero for /webhooks/outreach,
/zendesk-app/sidebar and /conversation_images/*. Rollback trigger:
any misconfigured* outcome above zero for a real tenant (an env var was
lost on the API2 task; fix the task, the revert would not help). Direct
Rails-origin /conversation_images reads are unaffected by this package and
closed only when the ENG-3297 apply is confirmed from the CloudFront config
(not just the probes above) and a fixed 24 h window shows zero Rails
ConversationImagesController#show requests.
Direct /conversation_images/:signed_id reads on the Rails origin
This dependency does not appear in legacy_rails_gateway telemetry: the
requests hit the Rails origin directly, so the gateway census undercounts
Rails’ live public surface. Read-only census (trailing windows ending
2026-09-19T21:43Z):
| Window | Rails ConversationImagesController#show 200 | API2 native_public_media conversation images |
|---|---|---|
| 24 h | 194 (US 184, EU 10); latest US 21:43:17Z, EU 15:02:10Z | 98 US + 42 EU + 1 EU Active Storage redirect |
| full trailing 7 d | 7,221 (US 3,633, EU 3,588) + 1 EU 404 | — |
Classification: the native route is healthy, so this is not a native-media failure. 194/day is a meaningful live dependency; 7,221/7 d is pre-cutover volume plus the live residual of absolute Rails-origin URLs that are embedded in stored conversation HTML.
Emitter. Read-only source tracing found the live emitter is API2 itself, not
only stale rows: provider-conversation-sync/embedded-images.ts
conversationImageUrl() built every newly imported provider-conversation
image URL from RAILS_INTERNAL_BASE_URL, even though API2 already owns
GET /conversation_images/:signed_id natively (routes/public-media.ts) and
the regional getRulebaseApi2BaseUrl() helper exists (RULEBASE_API2_BASE_URL
is set per region in Flightcontrol: https://api2.rulebase.co /
https://eu.api2.rulebase.co). Every new import therefore minted a Rails-origin
URL, which explains steady direct Rails hits alongside healthy native traffic.
Package (ENG-3282). Newly minted URLs use the regional public API2 origin;
the Rails ActiveStorage signed id and the /conversation_images/ replay guard
are unchanged, so /conversation_images/:signed_id verifies identically on
API2 and a replay of a rewritten part still downloads nothing and writes no
second blob. API2 image-caption defaultImageOrigins() accepts the API2 host
alongside the Rails origins. Stored rows are not rewritten and no backfill
runs: URLs already persisted on the Rails origin keep resolving through Rails
(and are still skipped by the replay guard), so the old-URL tail keeps Rails
at 3/3 until it ages out or a later, separately evidenced package handles it.
Post-deploy gate: the direct Rails ConversationImagesController#show rate
should fall from the ~194/day mint-driven baseline toward only old-URL reads;
a flat rate means another emitter exists. Rollback is a revert (new rows
would resume minting Rails-origin URLs; API2-origin rows already written stay
valid because API2 owns the route). No deploy, provider, or production-data
change is part of the package.
Post-deploy census (fixed 24 h ending 2026-09-20T03:30:00Z; #12575
deployed in 0a6fd1e7). Direct Rails ConversationImagesController#show
served 194 successful reads (US 177, latest 03:18:00Z; EU 17, latest
01:18:29Z); available 7 d is 7,228 successful (US 3,633 + EU 3,595) plus one
EU 404. The rate is flat against the pre-fix 194/day, so the emitter fix did
not move the live number: the reads are the stored Rails-origin URLs, and
at 194/day they are now the largest live Rails public dependency (larger than
counter cache 129/day or expiry 48/day) and the one blocker to scaling Rails
web to zero over a weekend.
Exact path. These URLs were minted by API2 from RAILS_INTERNAL_BASE_URL,
which in production has been https://legacy-rails.rulebase.co /
https://eu.legacy-rails.rulebase.co since #12326 (merged 2026-09-17)
repointed it from the canonical api.* hosts (rows minted before that carry
api.* and already resolve natively on API2). A read is
therefore browser → Rails CloudFront distribution (alias legacy-rails.*)
→ Rails ALB → ConversationImagesController#show (find_signed +
send_data), never touching the API2 gateway. The two origins are
distinguishable on the wire: Rails replies carry x-runtime and set session
cookies; the API2 native handler does neither. The decision in ENG-3282 to
leave stored rows untouched stands (no backfill or row rewrite); the
reversible owner has to sit at the edge: a path-scoped ordered behavior
/conversation_images/* on each Rails distribution whose origin is the
regional API2 ALB, so the same signed id verifies on the already-deployed
native handler (routes/public-media.ts) with method, status and header
behavior preserved and regional routing unchanged. Rollback is deleting that
one behavior. Flightcontrol’s CUE schema cannot express a per-path origin,
so — as with the Zendesk sidebar exception — it ships as a guarded operator
runbook plus an external probe; API2 additionally refuses to fall back to
Rails for a /conversation_images/* request that already carries the gateway
hop header, so pausing the native media route while the edge behavior exists
cannot loop. Tracked as the separate focused package
ENG-3297 /
#12613 : head 401c2d9e0
is fully green with fake-aws fixture tests proving apply is a no-op only
against the exact managed origin/behavior and that rollback refuses foreign or
drifted state. Chidi human-merged it at 03:56:15Z as a00a3bf436 with no
recorded GitHub review (Ronald requested); it ships in release-prod
39f11065e. Activation status: the CloudFront /conversation_images/*
behavior is unapplied in both regions — merging only lands the runbook,
probes and API2 loop guard; a human/Ronald activation review is still required
before APPLY=1 is run. No CloudFront, data or deploy change from automation.
Gate test (requester-supplied, clean 44 h window ending approximately
2026-09-21T19:00Z; not independently re-queried — no CloudWatch access from
the session). Rails served 758 US (724 200 + 34 404) and 275 EU
/conversation_images requests while API2 served 704 US + 773 EU natively
with near-zero fallbacks. Roughly 563/day is above the 194/day baseline,
not below it. Re-reading the gate: the “flat means another emitter” rule
assumed reads track mints. They do not — a stored URL is read every time the
conversation is opened (agent view, QA review, evaluation drill-down; Rails
send_data replies Cache-Control: private, must-revalidate, so every render
revalidates against the origin), and review
lags import by days. The Rails-origin cohort is bounded by source: URLs were
minted from RAILS_INTERNAL_BASE_URL only between #12326 (a9c91b566,
2026-09-17T03:21Z) and the 0a6fd1e7 deploy of #12575 (merged
2026-09-19T23:30Z); rows before that carry api.* (Cloudflare → API2
native) and rows after carry api2.*. A rising read rate over a still-fresh
2.8-day cohort is therefore consistent with the old-URL tail and is not
by itself evidence of a second emitter. Read-only source tracing of main
found no second emitter: the only /conversation_images/ minter in API2 is
embedded-images.ts conversationImageUrl() (now getRulebaseApi2BaseUrl(),
with RULEBASE_API2_BASE_URL set on both api2 web and worker services in both
regions); the Rails minters (Conversation::Sync::Base#import_and_update_src,
Conversation::Sync::Jira) use routes.default_url_options[:host] =
RULEBASE_API_BASE_URL, which is https://eu.api.rulebase.co in the EU CUE
and is not set in flightcontrol.cue for rulebase-api-prod (US) — its
value lives outside the repo and should be confirmed as https://api.rulebase.co
(a legacy-rails.* value there would be a live emitter). ImageUrlConverter
rewrites /conversation_images/ to rails_blob_url on the same host, and
the API2 QA-feedback renderer (qa-feedback-generation/render/parts.ts)
presigns R2 directly.
The decisive discriminator is the blob id, not the rate: the signed id is
base64(JSON) -- signature with plaintext
{"_rails":{"data":<blob_id>,"pur":"blob_id"}}, so every Rails-served path
decodes to a blob id without any secret. Revised gate: SELECT id, created_at FROM active_storage_blobs for the decoded ids — every id must have
created_at inside the #12326→0a6fd1e7 cohort window. Any Rails-served
blob created after the 0a6fd1e7 deploy proves a second emitter; none
proves the tail, which only the ENG-3297 edge behavior retires. Logs
Insights on the Rails groups: filter path like /^\/conversation_images\// | parse path "/conversation_images/*--" as b64 | stats count(*) by b64,
then decode b64 offline. #12685
removes the offline step: Rails ConversationImagesController and every
Active Storage controller log event=legacy_rails_media_request with
blob_id, service_name, host, via_api2_gateway, referer_host and
user_agent, so blob age, edge host and reader are one query; API2 logs
event=legacy_rails_media_fallback with reason and serviceName on every
remaining fallback, and serves email_assets / conversation_uploads blobs
natively instead of falling back for non-cloudflare services.
Outreach webhook cutover: OUTREACH_WEBHOOK_SECRET provisioning
#12474 is deployed in
API2 build cmu70 (US web task definition 1638, EU 1656). The first natural
slice after deploy (2026-09-18T13:54Z) proved the allowlist is active: US
recorded 35 forwarded 201 conversation uploads, 7 blocked 404, and 3
forwarded 400; EU recorded 6 Cable forwards and 1 rejected 404, with no
gateway or Outreach errors. The three US 400s were POST /webhooks/outreach
and native_outreach_webhook logged outcome=forwarded_secret_unconfigured status=0 for each: OUTREACH_WEBHOOK_SECRET was not configured for API2 in
either region, so Outreach ingress stayed Rails-owned. Deployment alone does
not change ownership; the route only validates natively once the secret is
present.
Configuration is complete as of 2026-09-18T15:24Z. The existing
non-empty SecureString from SSM /rulebase/prod/OUTREACH_WEBHOOK_SECRET was
copied into Infisical project 848b62cf-7704-49b0-9f39-70ae0b4c3f19
environments prod and production-eu (both keys existed but were empty;
both now match the SSM value exactly; staging was intentionally left
unchanged). The API2 web services were force-cycled with no code or task
definition change: US ecs-svc/4785599704546838455 on TD 1638 complete at
2/2, EU ecs-svc/2513181737475495035 on TD 1656 complete at 1/1, zero
pending, migration locks acquired cleanly. Unsigned POST /webhooks/outreach
canaries at 15:28Z returned 403 in every attempt (US 4/4, EU 2/2) and
native_outreach_webhook recorded outcome=forbidden_signature status=403 in
both regions; no forwarded_secret_unconfigured remains in the canary slice.
No provider change was made and no signed or manual delivery was sent. The
next gate is natural signed traffic: step 3 below.
API2 does not read application secrets from flightcontrol.cue. Each API2
service starts through infisical run --projectId=$INFISICAL_PROJECT_ID --env=$INFISICAL_ENV (project 848b62cf-7704-49b0-9f39-70ae0b4c3f19;
INFISICAL_ENV is prod for api2.rulebase.co, production-eu for
eu.api2.rulebase.co, and staging for the dev deployment), so the reviewed
configuration path is the same one used for ZENDESK_WEBHOOK_BEARER_TOKEN
(see rollouts):
- (Done
15:24Z.) A human with Infisical and AWS access provisionsOUTREACH_WEBHOOK_SECRETinto the API2 Infisicalprodandproduction-euenvironments (andstagingfor parity) with the value of the Parameter Store key/rulebase/prod/OUTREACH_WEBHOOK_SECRET, which is what Rails’OutreachConfig#webhook_secretreads in both regions (the EU Rails service references the same/rulebase/prod/...key inflightcontrol.cue). Noflightcontrol.cuechange and no Outreach provider change is required.infisical runinjects secrets at process start, so after provisioning cycle the API2 web service tasks in the FlightcontrolProductionandProduction EUenvironments (a no-code redeploy is sufficient) and confirm the new task definition revisions before verifying. - (Done
15:28Z.) Verify per region with the unsigned canary from the ownership contract:POST /webhooks/outreachreturns403andnative_outreach_webhooklogsoutcome=forbidden_signature(notforwarded_secret_unconfigured). - (Open.) Require real signed deliveries to log
outcome=acceptedwithaction=created|existing, then reconcileconversationsand provider-sync enqueues against the Rails baseline for a complete 24-hour window with zerolegacy_rails_gateway outcome=forwardedfor/webhooks/outreach. Do not send synthetic signed deliveries and do not change flags, ownership manifests, or the Outreach provider registration before that window closes.
Rollback is unsetting the secret in Infisical and cycling the task: the route returns to explicit Rails forwarding without a code change.
Workstream 2: stop calling Rails for effects
API2 still calls authenticated Rails /internal/* controllers for behavior it
does not own natively. “Effect” is intentionally broad: it includes writes,
callbacks, context construction, model-bound reads, file lifecycle operations,
notifications, bookkeeping, and orchestration.
Measure this boundary at the API2 Rails-client call site and at the Rails controller. Group calls by semantic family, not only by URL. A single generic controller can hide many unrelated tools; several endpoints can implement one workflow.
Initial production baseline
The first complete classification found 432,931 API2-to-Rails calls over seven days, averaging about 61,850 calls per day. Approximately 22 Rails controllers and 30 actions were live. Four families accounted for 375,443 calls, or 86.7% of the total:
| Priority | Family | Seven-day calls | Share | Current next step |
|---|---|---|---|---|
| 1 | Attachment purge | 127,942 | 29.6% | Build the native attachment/blob purge primitive and durable object-deletion outbox |
| 2 | Image-caption context and writeback | 43,029 | 9.9% | Move context loading, signed image access, stale-write protection, and persistence into API2 |
| 3 | Generic Rails tool executor | 48,651 | 11.2% | Instrument tool_id, region, outcome, and duration; then migrate the hottest live tools |
| 4 | QA sampling recovery | 155,821 | 36.0% | Reuse API2’s native eligibility and request pipeline, then port the missing admission and reconciliation behavior |
This is the preferred implementation order, not descending-volume order. It front-loads bounded primitives, gathers the missing tool distribution before porting generic behavior, and leaves the largest semantic migration until its native building blocks are ready.
Current production baseline
The fixed trailing windows ending approximately 2026-09-18T10:16Z are
summarized in Evidence as of 2026-09-18:
47,230 calls in 24 hours, of which 40,268 are meaningful live calls, and
417,996 over seven days, of which 218,426 remain after removing shipped-family
residue. The earlier census below is retained for the per-family trend.
The exact rolling windows ending at approximately 2026-09-17T17:08Z show
that the effects boundary remains stable while the deployed families begin to
roll out of the shorter window. The seven-day total is 433,186 calls:
165,197 in the US and 267,989 in the EU. The rolling 24-hour total is 68,563
calls: 30,013 in the US and 38,550 in the EU.
| Family | 24-hour calls | Seven-day calls | Change from preceding census |
|---|---|---|---|
| Attachment purge | 20,000 | 126,929 | -3,259 (-2.50%) |
| Image-caption context and writeback | 4,998 | 41,176 | -1,672 (-3.90%) |
| Generic Rails tool executor | 8,169 | 49,760 | -1,443 (-2.82%) |
| QA sampling recovery | 26,819 | 159,327 | +1,008 (+0.64%) |
These four families account for 377,192 calls, or 87.1% of the current
seven-day total. None has materially redistributed since the prior baseline.
From 2026-09-17T13:08Z through this census, both regions recorded zero legacy
attachment-purge and zero legacy image-caption calls. That four-hour signal is
longer than the initial smoke slice but does not replace a complete production
window or business-outcome reconciliation.
Package 1: attachment purge
API2 already tombstones the business attachment row, then makes one Rails call per attachment to purge Active Storage state.
The native package must:
- detach the
active_storage_attachmentsrow transactionally; - delete the blob row only when it is orphaned;
- remove tracked Active Storage variant records and enqueue deletion of their generated variant objects before deleting the source blob;
- persist the object key and storage service to a durable purge outbox inside the transaction;
- delete the R2 object after commit and retry failed outbox deliveries;
- preserve shared blobs;
- serve retention and manual attachment deletion through the same primitive.
Completion requires no growing purge outbox, no attachment metadata for tombstoned rows, preserved shared blobs, and zero Rails purge calls for a complete production window.
Package 2: image-caption context and writeback
The model call and BullMQ lifecycle already run in API2. Rails still builds the caption context and persists the result.
The native package must:
- resolve and tenant-check the organization, conversation, and part;
- preserve the revision or run ID used for stale-write rejection;
- select up to five supported embedded or attached images within size limits;
- generate direct signed R2 URLs instead of Rails blob redirects;
- build prompt context from the preceding and following five parts;
- persist conditionally as
completed,already_captioned, orstale; - replace both the ordinary caption worker and QA caption-stage calls.
Completion requires comparable captioned, no-image, and stale ratios; continuing caption population; no unredacted output or tenant mismatch; and zero Rails caption context or update calls.
Package 3: generic Rails tool executor
/internal/tools/call combines multiple tool IDs behind one controller. Total
request volume is not enough to choose an implementation order.
The first package must record tool_id, region, outcome, and duration at the
API2 Rails-client boundary without logging tool arguments. Observe at least one
representative business day, then:
- port live read tools to existing API2 query, search, and analytics engines;
- port AI tools to API2’s AI runtime;
- make live mutators typed and idempotent;
- delete unused dispatcher IDs instead of porting dead code;
- replace the generic Rails fallback with an exhaustive native registry.
Completion requires a native handler or a proved-dead disposition for every
observed tool ID, contract parity for returned citations and errors, idempotent
mutators, and zero /internal/tools/call traffic.
Package 4: QA sampling recovery
API2 owns the schedule and candidate query but still sends selected candidates to Rails in batches. Rails applies credits, eligibility, sampling, audit, counter, transcription, and request-creation behavior.
Do not translate the Rails job line by line. Reuse API2’s native eligibility, scorecard, agent, percentage-sampling, fixed-period-sampling, and evaluation creation implementations. Port only the missing behavior:
- evaluation-credit admission;
- customer-response and untranscribed-call cases;
- work-item completeness;
- skipped-request audit records;
- sampling-counter reconciliation;
- outstanding-transcription reconciliation and handoff;
- deterministic enqueueing and one active request per conversation.
Roll out per organization with decision and skip-reason parity metrics. Completion requires stable ticket-closed request creation, comparable skip distributions, no sampling-counter drift, no duplicate or overdue active requests, successful transcription handoffs, and zero Rails sampling or reconciliation calls.
Direct RAILS_INTERNAL_BASE_URL call sites outside the Rails client (2026-09-21)
lib/rails-client.ts is not the only API2 code that reaches the Rails origin.
A source census of RAILS_INTERNAL_BASE_URL (rulebase-web/rulebase-api2/src,
non-test files) found 18 references. They bypass createLegacyRailsGateway, so
they emit no legacy_rails_gateway event and are invisible on the gateway
dashboards; only the Rails request logs see them. Reconciled against this
ledger:
| Call site | Rails endpoint(s) | Classification | Ledger row |
|---|---|---|---|
lib/env.ts, lib/rails-client.ts | — | Declaration / centralised client (audited separately) | n/a |
app.ts | wildcard fallback | Gateway fallback | Forwarding: classify gateway traffic |
routes/public-media.ts | /conversation_images/:signed_id, /rails/active_storage/* | Gateway fallback (native first; proxyLegacyRailsRequest only when paused) | Forwarding: Active Storage blobs and conversation images |
routes/outreach-webhooks.ts | POST /webhooks/outreach | Gateway fallback while OUTREACH_WEBHOOK_SECRET unset | Forwarding: Outreach webhook ingress |
routes/zendesk-app-sidebar.ts | Zendesk sidebar routes | Gateway fallback | Forwarding: Zendesk marketplace sidebar |
lib/conversation-uploads/proxy.ts | POST /conversations/upload | Explicit proxy (sibling-owned) | Forwarding: Conversation uploads |
lib/qa-sampling-recovery/rails.ts | POST /internal/conversation_transcription_reconciliations, sampling admission | Direct caller (sibling-owned) | Effects: QA sampling recovery |
lib/qa-evaluation-request/preparation-effects.ts | POST /internal/qa_evaluation_requests/:id/execute_stage | Direct caller, native-first fallback | Effects: QA stage execution |
lib/conversation-transcription/rails.ts | /internal/conversation_transcriptions/:id/* | Direct caller, owner-gated (conversation_transcription_state_native) | Normal work: Conversation transcription state |
lib/sales-call-compliance/rails.ts | /internal/sales_call_compliance_analyses/:id/{context,start,fail,transcriptions/:call,results} | Direct caller (BullMQ job lib/sales-call-compliance/run.ts, final-attempt failure from worker.ts) | Retired 2026-09-27 (ENG-3493 ): adapter deleted, job and worker use lib/sales-call-compliance/native.ts; see the Workstream 3 section |
lib/qa-evaluation-request/failure-effects.ts | POST /internal/qa_evaluation_requests/:id/finalize_failure | Direct caller, native-first fallback (failure-effect outbox delivery) | none — added below |
lib/knowledge-base/document-images.ts | POST /internal/knowledge_base_document_images | Direct caller (Zendesk/Scribe knowledge-base sync) | none — added below (documented only in the knowledge-base-sync package page) |
lib/evaluation-contest-notifications/post-create.ts | POST /internal/evaluation_contests/:id/notifications | Direct caller, dormant rollback drain (legacy_delegated) | none — added below (runbook exists) |
lib/scorecard-chat-context.ts | GET /scorecards/:id (public Rails route, forwarded browser cookie) | Direct caller (Instruct chat opened from a scorecard page) | none — added below |
lib/run-scheduled-export.ts | POST /internal/scheduled_exports/{run,preview} | Dead code: no non-test importer since #66dce4908 | none — deleted in its package below |
lib/conversation-part-image-caption/native.ts, lib/intercom/webhook-forwarding.ts, lib/rulebase-app-url.ts | — | URL/origin comparison only; no HTTP request to Rails | not a family |
routes/slack-interactions.ts forwards unrecognised Slack payloads to Rails
through the Rails client (sibling-owned); it no longer references
RAILS_INTERNAL_BASE_URL directly and is tracked in the requester census as
POST /integrations/slack/interactions (10 US / 0 EU in the 72 h after
cutover).
The list is now frozen by
rulebase-web/rulebase-api2/src/rails-internal-base-url.source-gate.test.ts,
which fails when a source module references RAILS_INTERNAL_BASE_URL without
an allowlist entry, and when an allowlist entry outlives its reference.
Volume caveat: this session had no CloudWatch access. The requester supplied a 72 h Rails path census starting after the 2026-09-16 cutover; none of the five newly added families’ paths appear in it, which is consistent with zero or sub-threshold volume but is unverified by this session.
Workstream 3: stop Rails owning the public.* schema
Schema-migration ownership moved from Rails to API2 on 2026-09-26. API2 runs
all future public, api2, and agent_api changes from scripts/migrate.ts
before web start; Rails no longer runs db:prepare in Flightcontrol. This is
distinct from the per-table write-ownership work in the
Rails table ownership map: that map decides which
service writes a table; this workstream decides which tool changes its shape.
The failure mode is total: drizzle-kit treats any table in a filtered schema
that is not declared as a DROP TABLE, and any undeclared column as a
DROP COLUMN. src/db/rails-schema.ts is a deliberately partial mirror, so
widening schemaFilter to public without a complete declaration destroys both
regional databases on the next push. The package is therefore gated on a
complete introspected declaration diffing to zero statements against a live
copy of each regional database, not on db:generate (which only diffs against
drizzle/meta snapshots and never reads a database).
Package runbook: rulebase-web/rulebase-api2/docs/runbooks/public-schema-ownership-transfer.md.
Ownership transfer is blocked as of 2026-09-21 by findings the package surfaced rather than caused:
- US and EU
publicschemas differ in indexes (2,398 vs 2,395; six US-only and three EU-only) and carry ten invalid (indisvalid = false) indexes fromREINDEX CONCURRENTLYleftovers and a partially failedopenai_file_idconcurrent build. No single declaration is correct for both regions until a human-owned repair PR reconciles them with production DDL. rulebase-api/db/structure.sqllags production (4,772 vs 5,086 table columns, 26 vs 28 views, 161 vs 163 functions), so the declaration must be regenerated from schema-only dumps of each region after the repair.- Drizzle cannot round-trip two
public.conversationsexpression indexes (COALESCE(...) DESC), and cannot declare triggers, functions or extensions at all; those need an explicit non-Rails owner.
Update 2026-09-23: the expression-index churn is a drizzle-kit 0.31.10
limitation with no declaration-side fix; a read-only, TTY-free probe
(pnpm db:probe-public-ownership) now reproduces the public-only live diff and
is the per-region gate, and the runbook carries operator gates G1–G7 plus
rollback. See the dated section above.
Update 2026-09-24: G1 repair DDL exists as Rails migration
20260924155417_reconcile_public_indexes_for_ownership_transfer
(#12980 , ENG-3345):
drops _ccnew*/_ccold* REINDEX CONCURRENTLY leftovers, REINDEX INDEX CONCURRENTLY every invalid public.* index, adds any missing
*_on_org_and_openai_file_id composite through an explicit index_exists?
check (no if_not_exists), drops the EU-only unsourced
idx_qa_eval_criterion_results_on_openai_file_id, and raises if any invalid
index remains; down is irreversible. Verified only locally
(structure.sql load + seeded EU-shaped drift → 0 invalid / 0 leftovers /
2,417 indexes; no-op on a converged DB). Merged by a human at
2026-09-24T17:08:29Z with no submitted Ronald review (opened as a draft
with a human-preflight note; no retroactive approval implied) and carried in
release-prod sync #12987
/ #12988 . Rails ECS was
still on the prior task definitions 3/3 in both regions at 17:13Z, so no
migration or deployment is credited. Coordinator read-only production
preflight (API2 ECS shell, operator read — this session has no production
access) matches the expected drift and therefore the migration’s inputs: US
2,398 public indexes, invalid 4 (good_jobs _ccnew1/_ccnew/_ccold plus
teams.zendesk_brand_id), leftovers 3, all six composites present, unsourced
index absent; EU 2,395 indexes, invalid 6 (compliance composite,
deprecated_dashboards, four good_jobs _cc*), leftovers 4, only the
compliance composite present, unsourced index present. Expected post-migrate
state is identical index sets and indisvalid = false = 0 in both regions;
G1 closes only when the same read-only census shows that after the automatic
db:prepare run, and the US teams.zendesk_brand_id / EU
deprecated_dashboards rebuilds are the ones to watch for lock_timeout
aborts (a failed statement leaves the version unrecorded; re-run is safe).
G1 held at ~17:17Z: coordinator read-only census after the automatic
db:prepare run on the e504aad3 rollout (old tasks still draining) shows US
and EU both at 2,395 public indexes with an identical sorted
name+indexdef set (md5 8d73d977a51edc8baaf2f21fff92606c), 0 invalid, 0
_cc* leftovers, and version 20260924155417 recorded in both. This closes
G1 only: Rails still owns public.* migrations, drizzle.config.ts is
unchanged, and G2–G7 remain open. G2 (per-region schema-only dump →
declaration regeneration → two-region probe) is next; the census/regeneration
procedure is in the package runbook.
G2/G3 first run, 2026-09-24 regional dumps (G3 not credited): operator
supplied schema-only public/api2/agent_api dumps for US and EU (no data,
no owners). Local *_push restores match the G1 census (2,395 indexes, 0
invalid, 0 _cc*). pnpm db:regenerate-public-schema pulled 447 tables /
1,948 indexes per region, content-identical modulo order, 23 hand-edit audit
findings (22 known + callRecordingUploadIntents, an api2 table added after
the 09-21 pull; the audit now derives that set from schema.ts).
pnpm db:probe-public-ownership exited 1 in both regions with a
byte-identical list: public-drift 71, api2 0, agent_api 0, plus 4
known and 72 push-equivalent index churn statements. Root cause is not the
Drizzle declaration but rulebase-api/db/structure.sql, which it mirrors:
structure.sql declares snapshots, snapshot_items and 24 columns
(scorecards.active_snapshot_id, conversations.employee_ids_for_evaluation,
six *.scorecard_version_id, organizations.knowledge_gaps_enabled, …) that
exist in neither region; none has a source migration, and
knowledge_gaps_enabled’s removal (20260410092213) is recorded in
structure.sql’s own version list while the column is still declared, so the
file was dumped from a development database with phantom objects. Production
is the correct side. Operator-classification findings: live-only
public.databasechangelog (Liquibase, unreferenced in the repo), live-only
qa_agent_evaluation_scorecard_criterion_check_results.comment (both
regions, no migration), US-only agent_api.conversation_thread_memberships
view (no source), and zendesk_webhooks.fk_rails_13818777d6 validated in EU
but NOT VALID in US/structure.sql. G2 therefore cannot replace the
declaration yet and G3 stays open; the next step is a Rails-side
structure.sql regeneration from a migrated-from-scratch database as its own
reviewed PR, then re-run G2/G3 with the same dumps. No production schema
writes were made; the probe’s refusal to report zero is the intended
behaviour.
structure.sql regeneration, 2026-09-25 (local only): replaying the 1,857
migration files onto an empty database is not possible. bin/rails db:migrate on an empty database only appears to work because Rails loads
structure.sql first; a forced replay
(ActiveRecord::Base.connection.pool.migration_context.migrate) fails at
20240609082833_add_scraped_to_website_review_page.rb with
uninitialized constant WebsiteReviewPage (model since deleted), and 75 of
the 1,932 recorded versions have no migration file at all. The file was
therefore regenerated with Rails’ own db:schema:dump (--clean --if-exists,
dump_schemas = :all) pointed at the local restore of the EU 2026-09-24
schema-only dump, with the drizzle schema and the existing 1,932 versions
carried over and the out-of-band public.databasechangelog dropped locally
first. Result: db:schema:load into a fresh database succeeds, 447 public
tables / 26 views, 2,395 public indexes (= G1 census), 0 pending migrations;
the 2 phantom tables, 24 phantom columns and the unsourced
immutable_unaccent / index_conversations_on_qa_evaluated_at objects are
gone, qa_agent_evaluation_scorecard_criterion_check_results.comment is
declared, and fk_rails_13818777d6 is validated (EU side; US is still NOT VALID). The diff also carries api2.* catch-up (19 Drizzle-created tables
added since the previous dump, 4 work_item_resource_* tables that no longer
exist in EU) because Rails dumps every schema. This is a local artefact and a
reviewed Rails PR; it is not production DDL and does not credit G2/G3, which
re-run against the same regional dumps once it merges.
G2/G3 second run, 2026-09-25, after #13021 (G2 done, G3 mechanically
met, not yet credited): #13021
(regenerated structure.sql, no migration) and
#13024 (two phantom
scorecard_version_id declarations removed from rails-schema.ts, which had
turned API2 CI red once pglite loaded the truthful structure.sql) were
human-merged by Chidi (no Ronald review). pnpm db:regenerate-public-schema
re-run against the same local rulebase_{us,eu}_push restores of the
2026-09-24 dumps: raw pulls byte-identical to the first run, 23 audit
findings, all re-applied by hand plus two new any annotations
(evaluationContests, qaAgentEvaluations) and one column-name pin
(databasechangelog.md5sum, which snake_case casing would otherwise emit
as md5_sum). src/db/public-schema.generated.ts replaced: 474 → 473
declarations (snapshots/snapshotItems gone, databasechangelog added),
38 tables changed content, the rest re-ordered only. pnpm db:probe-public-ownership now exits 2 in both regions with identical
output: public-drift 0 (tableSetDrift empty), known-expression-index-churn
4, strict-only-index-churn 38 (19 push-equivalent pairs: expression indexes
and the six schema.ts-owned public tables’ indexes, which differ only in
quoting), api2 0, agent_api 0. That is the runbook’s defined “known churn
only” state. Residuals that this public-filtered probe does not see and
that still need an owner decision before G3 is credited: US-only
agent_api.conversation_thread_memberships view, US NOT VALID
fk_rails_13818777d6 (declaration assumes validated, matching EU), and
public.databasechangelog (now declared so the probe is zero, but it is a
Liquibase artefact API2 must never drop or migrate). No production schema or
data writes; local restores only.
G3 residual decisions (2026-09-25). #13029 merged by human (Chidi, no
submitted Ronald review). agent_api.conversation_thread_memberships is a
read-only view over public.conversations (conversation_id,
conversation_thread_id, tenant-scoped by app.current_organization_id);
nothing in the repo defines or reads it and scripts/schema-compatibility
treated it as a US-only exception. Decision (Chidi): keep it in both
regions — Drizzle migration 0426_agent-api-conversation-thread-memberships
creates the exact US definition where absent (EU), is a no-op where present
(US, oid unchanged) and grants SELECT to agent_query_reader; verified
on fresh local EU and US restores with identical pg_get_viewdef. The US
NOT VALID
fk_rails_13818777d6 gets Rails migration
20260925044504_validate_zendesk_webhooks_organization_foreign_key.rb
(validate_foreign_key, SHARE UPDATE EXCLUSIVE, no rewrite, no row changes;
fails and leaves the constraint unchanged if US has orphan rows), verified on
the local US restore (convalidated f → t, re-run no-op) and opened for
Ronald review; it is credited only on operator-confirmed convalidated = true
in US after deploy. databasechangelog is excluded at the G4 baseline.
Ownership transfer, 2026-09-26 (G1–G7 closed). The final read-only live
census shows US and EU converged: 2,395 public indexes, 0 invalid indexes,
0 _ccnew/_ccold leftovers, the agent_api.conversation_thread_memberships
view present in both, and fk_rails_13818777d6 validated. Canonical
fingerprints are identical across regions (5,086 columns, 2,395 indexes,
1,548 constraints, 28 views, 159 functions). API2 now owns future public.*
changes as reviewed custom SQL migrations. public deliberately remains out
of drizzle.config.ts’s schemaFilter, so neither drizzle-kit push nor its
lossy expression-index model can propose implicit public DDL. Migration
0427_public_schema_ownership_baseline.sql is a no-DDL handoff marker that
refuses to apply unless the reconciled live invariants hold. The same change
removes all three Rails db:prepare pre-deploy commands and adds CI that
rejects every addition or edit under rulebase-api/db/migrate/. Liquibase’s
databasechangelog remains externally owned and untouched; Rails
schema_migrations remains historical metadata, not an active migration
ledger. API2-only PGlite test databases have no public.conversations and
skip these production guards, matching the existing cross-schema migration
convention.
The schema-ownership shutdown gate is closed once this package is deployed and
the 0427 ledger row is verified in both regions. Future public.* changes
must be generated as custom API2 Drizzle migrations and reviewed as raw SQL.
Rollback should restore Rails db:prepare as a roll-forward config change
while retaining 0427. An older API2 image rejects the unknown marker hash;
if that image must also be restored, first take the API2 migration advisory
lock and delete the exact no-DDL 0427 row (hash
dd9c748692089b50ac7bcdc947732e7268f8097820c81e7b7efd511963c94475,
created_at = 1790448038726) in both regions, require one returned row per
region, then deploy the older image and verify its migrator starts cleanly.
Rails-web-dark cutover audit (2026-09-26)
The exact 24-hour window ending 2026-09-26T16:18:57Z contained six Rails
internal requests in US and none in EU: one QA result finalization, one
transcription context read plus one transcription write, one daily-report
backfill preview plus one combined email, and one Slack notification. All six
returned success, which proved that API2’s retained fail-closed/rollback
branches were still live request paths rather than dead compatibility code.
The rails-web-dark cutover removes Rails as a destination for those families:
- conversation transcription always loads and persists native state; a failed native translation enqueue retries in BullMQ instead of invoking the Rails translation fallback;
- QA result import and completion are unconditionally native, including replay
of historical
completion_owner = 'rails'markers; - daily report preview, per-organization email, and combined email are always delivered by API2; the incident switch pauses work instead of delegating it;
- agent Slack tools and conversation alerts prefer the active API2 OAuth connection and may read a legacy credential directly from Postgres, but a credential miss disables/fails the tool and never calls Rails web.
The obsolete PostHog/Flipper owner selectors, Rails HTTP helpers, and owner
environment overrides are removed. retired-rails-web-routes.source-gate.test.ts
prevents the retired URL fragments from returning to the shared Rails client.
Deployment and fresh 24-hour/7-day zero-call windows remain required before the
shutdown gate receives production credit.
Rails-at-zero residual fallback removal (2026-09-27, ENG-3489 )
Rails production services have been held at minInstances = maxInstances = 0
since #13164
(0ca2801c8, 2026-09-26T20:19Z). From that point every remaining
API2-to-Rails fallback branch is not a rollback path but a guaranteed
connection failure, so the audit on origin/main c6c6557bd reclassified
the three non-QA residual families from the 2026-09-22T23:23Z census that
still routed to Rails in code:
| Family | Rails branch on main before this package | Why it is a rollback gap, not a rollback path |
|---|---|---|
| Conversation-field-value upsert | upsertOwnedConversationFieldValue called PUT /internal/.../conversation_field_values when api2_conversation_field_value_native_upsert was off/unreadable or the native engine deferred (attachments, relationships disabled) | Flag 285250 is exact rho-tnkw only, so every non-Rho agent write and every deferral hit a zero-replica Rails and surfaced a transport error to the agent |
| Counter-cache repair | runCounterCacheRepairs fell back to Internal::CounterCachesController#fix per step when api2_counter_cache_native_repairs read null/false/threw | Flag 283894 is 100 % in both deployment groups; the only remaining Rails path is the PostHog-outage branch, which now fails the tick instead of repairing counters |
| Unit21 source sync | runUnit21SourceSyncJob routed non-canary organizations and native errors to sync_unit21 / sync_unit21_status, with Rails-side reconciliation of ambiguous outcomes | Flag 282001 is exact rho-tnkw; Rho is the only non-demo Unit21 source, so the Rails branch had no live consumer and its polling loop could only time out against zero replicas |
The package makes all three native-only and fail-closed: field-value writes
raise ConversationFieldValueUpsertError (DEFERRED_UPSERT_ERRORS) for
attachments / relationships-off / missing Rails organization id instead of
delegating; counter-cache repairs always run fixCounterCacheStepNatively
(Flipper pause_api2_counter_cache_repairs remains the incident switch);
Unit21 always runs runNativeUnit21SourceSyncJob (Flipper
pause_api2_unit21_sync remains the incident switch). The three PostHog
selector constants, the Rails client functions
(upsertConversationFieldValue, fixCounterCacheStep,
syncUnit21OrganizationDataSource,
getUnit21OrganizationDataSourceSyncStatus) and their tests are deleted.
Rails controllers are untouched; rollback is a revert of this PR plus a Rails
scale-up, not a flag.
Slack (#13023 ).
Superseded: #13143
shipped agent-slack-notifications/api2-connection.ts and tokenSource
attribution on main, so #13023 is merge-conflicted duplicate work and should
be closed against ENG-3428 rather than rebased. The one residual is a stale
comment in agent-alert-conversation-tools.ts referencing the removed
api2_slack_notification_native gate; flag 286616 stays disabled and is now
dead configuration eligible for deletion once the Rails-web-dark 7-day window
is credited.
Unverified production windows. This session had no CloudWatch, Rails
request-log or database access. PostHog flag definitions were read at
~2026-09-27T02:30Z; Sentry rulebase-api2 unresolved issues at
~03:35Z showed no Rails-transport error for these three families, but
Sentry is not a request census. A Sentry log aggregation for
falling back to Rails organization_data_sources.data.token for Intercom sync
returned 9,515 rows, but the tool applied a 14-day range instead of the
requested 36 h, so it is not window evidence. Post-deploy gates:
one natural counter-cache tick per region with all seven steps rows
reported natively, one natural Rho Unit21 run with cursor advance, and a
non-Rho agent field-value write producing a native updated result; then the
usual 24 h / 7 d zero-Rails windows, which can only be credited by an operator
with request-log access.
Review-process exception: #13197 merged without a submitted Ronald review (2026-09-27)
#13197 (ENG-3489,
Rails-at-zero fallback removal) landed on main as bf219623ae through an
external merge before Ronald submitted a review. This is recorded as an
exception to the review gate this plan requires; it is not retroactive
approval and must not be cited as review evidence for the field-value,
counter-cache or Unit21 native-only behavior. The post-deploy gates listed in
the previous section remain uncredited.
Slack #13023 reconciliation. Verified directly on main 6087ec7dcf:
agent-slack-notifications/api2-connection.ts and owner.ts from #13143
(c3cc5473df) implement the organization-scoped provider = 'slack',
auth_type = 'oauth2', status = 'active', nonempty-token, exactly-one
candidate lookup with legacy data-source fallback and tokenSource
attribution that #13023 proposed. #13023 was closed as superseded only after
that verification; ENG-3428 carries the evidence and no cherry-pick is needed.
Native sales-call compliance analysis (2026-09-27, ENG-3493 )
Classification: meaningful live runtime work that hard-fails at Rails
0/0. conversation-sync-processing.ts enqueues the sales-call-compliance
BullMQ job for every synced sales_call conversation, and until this package
the job (lib/sales-call-compliance/run.ts) and the worker’s final-attempt
handler (worker.ts) called five Rails endpoints under
/internal/sales_call_compliance_analyses/:id/ — context, start,
transcriptions/:call, results, fail. With Rails at zero replicas every
sales-call evaluation therefore failed on its first Rails call and, after
retries were exhausted, the fail callback also failed, leaving the
conversation evaluation_state at whatever the sync left it. This is not
dead code and not a flag hold: US api2_sales_call_compliance_pipeline is
enabled globally and EU has no flag.
Package. lib/sales-call-compliance/native.ts replaces the adapter:
| Rails endpoint | Native replacement | Parity notes |
|---|---|---|
context | loadSalesCallComplianceContext — loadNativeTranscriptionContext (calls, signed recording URLs, transcript availability), loadComplianceCriteria, loadComplianceTranscriptMessages, buildConversationTranscriptXml | Tenant-checked by organization slug; interaction_type = 'sales_call' required; source version is COALESCE(last_synced_at, created_at) normalized to milliseconds as Rails’ iso8601(3) |
start | startSalesCallComplianceAnalysis | Sets evaluation_state/transcription_state = 'in_progress' only when the stored source version still equals the queued one (stale otherwise, job throws for retry) |
transcriptions/:call | transcribeAndPersistSalesCall → persistNativeCallTranscription | Same native writer the transcription pipeline uses (short transaction, duplicate-safe, usage tracking, post-commit hooks) |
results | persistSalesCallComplianceResults | One short transaction after the model call: advisory lock + FOR UPDATE, api2_result_run_id duplicate check, source-version re-check, deactivate prior snapshots, insert completed conversation_cx_risk_snapshots + evaluation_checks (compliance), persistComplianceCriterionResults, conversation update (compliance_risk_level, review_status = 'compliance_review' on high risk, evaluation_state = 'completed', evaluated timestamps), recalculateConversationRiskLevel, evaluation credit usage, runConversationRoutingPass with the reviewed event; PostHog conversation_event.processed + conversation.reviewed after commit |
fail | failSalesCallComplianceAnalysis | Skips when a newer sync changed the source version or the run already has a snapshot; otherwise evaluation_state = 'failed' and a Sentry message |
Not ported: @conversation.notify (high-risk complaint notification for the
latest un-notified complaint). The compliance path creates no complaints and
the conversation-sync pipeline already owns complaint notification; treat a
regression report here as a follow-up rather than a Rails dependency. The
source-gate entry for lib/sales-call-compliance/rails.ts is removed and the
adapter deleted; no Rails, flag, provider or deployment change. Rollback is a
revert of the PR plus a deliberate Rails scale-up — a flag flip alone
cannot restore the old path while Rails is 0/0.
Evidence and unverified windows. Verified by code inspection on main
6087ec7dcf (Remove the unified_coaching flag (#13195)). No CloudWatch,
Rails request-log or production database access; the count of sales-call
evaluations that failed since 0ca2801c89 (Rails 0/0) is unverified.
Sentry rulebase-api2 searches found no grouped issue matching
sales_call_compliance in the searched period, but Sentry is not a request
census and the previously noted 14-day-vs-36-hour range defect applies.
Post-deploy gates: one natural sales-call sync per region producing a
completed conversation_cx_risk_snapshots row with api2_result_run_id
set, a matching evaluation_checks compliance row, evaluation_state = 'completed', and a reviewed routing pass; a forced-failure canary showing
evaluation_state = 'failed' with no Rails call; then the usual 24 h / 7 d
windows with zero /internal/sales_call_compliance_analyses requests, which
only an operator with request-log access can credit.
Evaluation-contest notification ownership audit (2026-09-28, ENG-3518 )
Classification: meaningful live runtime work that silently fails at Rails
0/0, gated by a flag that was never widened past the canaries.
lib/evaluation-contest-notifications/owner.ts resolved the run owner from
PostHog evaluation_contest_notifications_api2_owner. Read-only PostHog
evidence (flag id 275344, read 2026-09-28, UTC morning): active, exact-match on
group keys kuda-pilot and nala only, last_called_at 2026-09-28T08:53Z.
Every contest submitted by any other organization therefore persisted
owner = rails, wrote both channel rows as legacy_delegated, and called
POST /internal/evaluation_contests/:id/notifications. With Rails at 0/0 that
fetch throws a connection error, which is not a RailsDrainRejectedError,
so the evaluation_contest_rails_drain:<contest> claim was never released;
the BullMQ retry then saw the claim as uncertain, flipped both rows to
uncertain with last_error = 'rails drain outcome unknown', and completed
the run. Net effect: no reviewer email or Slack DM for non-canary
organizations, with no error left in Sentry. This is not dormant rollback
residue; the plan-ledger classification “dormant rollback drain” was wrong
for the post-0ca2801c89 period.
Package. api2 is the only owner: owner.ts, the PostHog flag constant,
the EVALUATION_CONTEST_NOTIFICATIONS_OWNER override, the Rails drain
(defaultDrainViaRails, drainThroughRails, RailsDrainRejectedError, the
drainViaRails test seam) and the source-gate entry for post-create.ts are
removed. settleOwner handles runs the old code persisted as rails and
that are still pending/failed: if no drain claim carries delivered_at
the run is taken over (owner = api2, legacy_delegated rows → pending,
per-channel jobs enqueued); if the drain was marked delivered the run
completes as rails untouched, so a Rails-sent contest is never re-sent.
uncertain rows are never re-queued (outbound-delivery.ts contract).
Focused pglite cases cover both branches. Rollback is a revert plus a
deliberate Rails scale-up; the flag no longer changes behavior. Not in this
package: Rails-side deletion of Internal::EvaluationContestNotificationsController
and EvaluationContest#deliver_legacy_reviewer_notifications! (follow-up once
deployed in both regions), deleting PostHog flag 275344 (operator step), and
any production data write.
Evidence and unverified windows. Verified by code inspection on main
8f279b9d7b and the PostHog flag definition above. No CloudWatch, Rails
request-log or production database access: the number of runs completed
with uncertain rows since 0ca2801c89, per region, is unverified, and
so is the count of POST /internal/evaluation_contests/:id/notifications
attempts. Operator read-only queries after deploy (both regions, api2 DB):
SELECT owner, status, count(*) FROM api2.evaluation_contest_notification_runs GROUP BY 1, 2 and SELECT status, last_error, count(*) FROM api2.evaluation_contest_notifications WHERE status IN ('uncertain', 'legacy_delegated') GROUP BY 1, 2. Runs already completed with
uncertain rows from the drain are a manual re-send decision (check Rails
delivery logs first); this session performed no replay. Post-deploy gate: one
natural contest per region from a non-canary organization producing
owner = api2 and two delivered/skipped rows, then the runbook
invariant with ok_legacy_delegated appearing only for pre-ENG-3518 runs.
Package ledger
Update this table whenever a package, pull request, deployment, or production window changes state. Volume is evidence-window-specific; include the window when replacing a value.
| Workstream | Package | Current evidence | State | Issue | Devin | PR | Deployment | Next gate |
|---|---|---|---|---|---|---|---|---|
| Forwarding | Classify gateway traffic and replace wildcard fallback | 18,888 events in the first complete 24 h ending 2026-09-18T09:30Z; 12,066 forwarded Rails 404, 526 blocked, about 2,836 meaningful | Explicit Rails route-prefix allowlist with API2 rejected outcome implemented; LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS=forward rollback gate retained | ENG-3246 | session | TBD | Not started | Deploy, regional canaries (/.env rejected, /trmnl/data forwarded), then a complete window with zero rejected events for documented paths |
| Forwarding | Retire dead compatibility prefixes, batch 2 | Clean 44 h Rails request-log window after the 2026-09-20 deploy: Rails served only conversations 2,002 US, conversation_images 758 US / 275 EU, internal 232 US / 108 EU, integrations 10 US; operator 24 h gateway census ending 2026-09-21: 1,131 forwards, all non-upload traffic scanner probes | 85 prefixes retired (25 natively owned with 48 exact ownership contracts, 60 dead), allowlist 159 → 74; PR ready for review, CI green | ENG-3344 | session | #12686 | Not started | Merge and deploy, then one complete regional window where every outcome=retired event is a scanner path; then classify the 70 remaining non-protected prefixes with the runbook |
| Forwarding | Retire legacy object-field prefixes (/conversation_fields, /customer_fields, /employee_fields) | Recorded operator censuses ending 2026-09-23 01:20Z/03:20Z/04:20Z: one pre-cutover EU GET /conversation_fields 200, nothing under the other two; post-canary slice 02:35:13Z → 04:20Z zero forwarded in both regions; no fresh telemetry (SSO expired 05:21Z) | Three prefixes retired (allowlist 74 → 71), object-fields ownership family (12 native routes), retired contracts for the legacy shapes; static caller search clean; focused tests green | ENG-3383 | session | #12840 | Merged by human 2026-09-23T06:40:54Z (84199d09, no submitted Ronald review); live US only (TD1727 1c5886a), EU TD1745 in progress; gate not credited | EU rollout; one 24 h window per region with natural outcome=retired for the three prefixes at zero or stale-tab residue only |
| Forwarding | Reject scanner-shaped paths under live prefixes locally | Operator-fixed 07:25Z census: post-04:20Z forwards are scanner 404s only (US 291 / EU 318, e.g. /admin/.env, /agent/vicidial.php); zero successful post-cutover forwards; 25 pre-cutover successes unchanged | isScannerShapedPath() rejects dotfile / PHP-ASP-JSP-CGI shapes under allowlisted prefixes (/rails exempt); allowlist unchanged; ownership contracts flipped; focused tests green | ENG-3385 | session | #12847 | Merged (af5b2a4f7, human merge at 2026-09-24T15:17Z, no submitted Ronald review); deployment to running API2 (a6b5071) unverified, no natural outcome=rejected telemetry credited; latest exact gateway cutoff 2026-09-24T15:40Z: 18 forwarded 404s, 17 predicate-matching (pre-deploy residue), 1 bare /admin | Verify the running SHA includes af5b2a4f7; then one 24 h window per region with outcome=rejected for the shapes and forwarded 404s limited to non-predicate paths |
| Forwarding | Retire seven resources prefixes; fix wildcard-middleware ownership shadow | Code evidence on main after #13066: method-scoped wildcard middleware in org-directory-mutations.ts counted as native ownership, shadowing POST /invitations and PUT /partners/:id (no gateway event emitted); recorded 2026-09-24 07:40Z/15:40Z/17:45Z censuses show none of the seven prefixes forwarded; no fresh telemetry | Per-route middleware; native PUT update aliases (6 families + /partners) and PATCH /roles/:id; seven prefixes retired (allowlist 71 → 64); seven ownership families (61 contracts); forwarded pins for POST /invitations, POST /partners/123/nope; focused tests + Rails prefix lint green | ENG-3463 | session | #13118 | Merged to main (53c53de5d); deployment and post-deploy window not verified from this session | Verify the running SHA includes 53c53de5d; then one 24 h window per region with outcome=retired under the seven prefixes at zero/stale-tab residue; POST /invitations forwarded expected to appear |
| Forwarding | Retire six compliance-program resources prefixes on PUT alias parity | Source audit on main 53c53de5d: only PUT /:id lacked an exact api2 handler under /agent_test_columns, /compliance_criterion_groups, /program_risk_factors, /program_sampling_rules, /program_test_questions, /test_risk_scenarios; 0 first-party PUT/Rails callers each; recorded censuses show none forwarded; no fresh telemetry | Native PUT aliases (six families); six prefixes retired (allowlist 64 → 58); six ownership families (23 contracts); retired pins; focused tests + typecheck green | ENG-3469 | session | #13130 | Open; not merged; no production change | Ronald review; green required checks; post-deploy one 24 h window per region with outcome=retired under the six prefixes at zero |
| Forwarding | Outreach webhook ingress | 224 POST /webhooks/outreach forwards in the 24 h ending 2026-09-18T09:30Z | Native route implemented behind OUTREACH_WEBHOOK_SECRET; conversation create is ON CONFLICT DO NOTHING + re-read on the Rails unique identity so concurrent deliveries stay idempotent; explicit Rails fallback while unset | ENG-3246 | session | #12474 | Not started | Set the secret per region, unsigned canary 403, conversations/provider-queue reconciliation against the Rails baseline, then a complete zero-forwarding window |
| Forwarding | Final-boundary public route ownership contract | MCP outage proved feature-router tests did not protect the composed gateway boundary | Safety implementation dispatched from current main | ENG-3184 | session | — | Not started | Focused PR proving exact native method/path ownership and no fallback invocation for critical public routes |
| Forwarding | Redirect empty-segment /conversations paths to API2 | 24 h census ending 2026-09-22T23:23Z: EU GET /conversations/ 23 forwarded 200, empty-id /conversations//... the bulk of 394 forwarded 404s | Gateway 308s to the normalized path when API2 owns it (outcome=normalized); four composed-router normalized ownership contracts; 77 + 118 focused tests green | ENG-3371 | session | #12820 | Not deployed | Merge and deploy; one 24 h window per region with zero forwarded GET /conversations/ and /conversations//*; fix the empty-id UI callers at source |
| Forwarding | Direct Rails-origin /conversation_images/:signed_id reads | 758 US (724 200 + 34 404) + 275 EU in a requester-supplied clean 44 h window ending approximately 2026-09-21T19:00Z (approximately 563/day vs the 194/day baseline); API2 native 704 US + 773 EU, near-zero fallbacks; about 18/region of the 404s are the six-hourly not-a-signed-id edge probe (GET+HEAD, 31/region per 72 h) | Emitter fix #12575 deployed (0a6fd1e7); edge behavior #12613 merged but unapplied in both regions; rate gate inconclusive (reads do not track mints), source tracing found no second emitter; blob-id gate defined above | ENG-3282 / ENG-3297 | session | #12685 (draft: telemetry + native non-cloudflare services) | Not deployed | Run the blob-id/created_at check on the Rails-served ids (or the legacy_rails_media_request query once #12685 lands); confirm US RULEBASE_API_BASE_URL is https://api.rulebase.co; zero post-0a6fd1e7 blob ids clears ENG-3297 activation as the sole remaining owner |
| Forwarding | Active Storage blobs and conversation images | 3,599 requests in the partial approximately 13-hour window; first 31 post-deploy minutes served six US requests natively and forwarded zero in both regions | Merged and deployed in both regions; initial routing proof is clean | ENG-3204 | session | #12374 | release-prod #12397 | Compare success/not-found outcomes and complete a zero-forwarding production window |
| Forwarding | Unit21 webhook ingress | 473 US POST /webhooks/unit21/:connection (all 200), 0 EU in the rolling 24 hours ending 2026-09-17T20:10Z | Native route implemented; PR open, Rails route and fallback retained | ENG-3224 | session | #12431 | Not started | Contract review, regional canaries, connection_logs/checkpoint reconciliation, then a complete zero-forwarding window |
| Forwarding | Zendesk marketplace sidebar | US 677 200 + 2 400, EU 2 400 in the rolling 24 h ending 2026-09-17T20:10Z | Native route live; RULEBASE_BEAM_BASE_URL=https://beam.rulebase.co in both Infisical environments (ENG-3290); 2026-09-27 (ENG-3495 ): route-level Rails fallback removed, paused/misconfigured → 503; PR open, not merged; see runbook | ENG-3225 | session | TBD | Not started | Set RULEBASE_BEAM_BASE_URL per region, deploy, then a complete zero-forwarding window with served tracking the Rails 200 baseline |
| Normal work | Conversation transcription state | 64 context/write and 3,921 reconciliation Rails calls in the fixed 24 h ending 2026-09-19T17:47:25Z (614 / 4,911 at 2026-09-18T16:06:07Z) | Deployed in release-prod b96da600; conversation_transcription_state_native was Kuda-only (23 natural context loads in the inspected slice, the 10 after b96da600 all terminal already_transcribed plus one completed conversation, no native persistence-write proof); v2 at 16:12:41Z added qonto as a second exact canary (18 Rails loaded + 18 Rails transcribed since 15:09Z); first native persistence proof at 16:23:45Z/16:24:16Z (Qonto call detail conversation_part_call_detail_mxPBJ8kOGrmtJ8kQbljy9Z4a, loaded then transcribed, row reconciled: transcribed_at 16:24:16.644Z, eng, 151,340 ms, 7 transcript messages, 0 CreditUsage because Qonto has no active transcription credit, matching Rails); hold at Qonto + Kuda until a complete 24-hour distribution/reconciliation window; advisory PostHog signal at 19:12Z: 26 API2-emitted (posthog-node) Qonto call.transcribed events from 16:24:16.788Z to 19:13:04.971Z and no Rails-emitted Qonto call.transcribed after 15:55:02Z, but row-level reconciliation of those persists is unverified without database access; see runbook; Qonto cleared the >24 h gate on 2026-09-19: since 16:12:41Z exactly 196 API2 loaded and 196 transcribed, no retry/permanent/not_found/already_transcribed outcomes, read-only EU DB 196 rows, zero failures, zero duplicate credit usages, max attempts 0, three zero-message/missing-duration rows matching the Rails 5/165 shape, 0 credit_usage matching the 0/165 baseline; flag expanded at about 17:59Z to exact kuda-pilot + qonto + rho-tnkw (no percentage; Rho had 30 Rails transcriptions / 24 h, zero failures) | ENG-3235 | session | #12456 | release-prod 279ae48c live in both regions (web TD 1651/1669 1/1, workers TD 1641/1658 5/5), preceded by 2d02ce18 (#12541) and eae24156 | Hold at the three exact canaries pending Rho’s natural native outcome and row reconciliation; then widen per organization with reconciliation, then a complete zero-call window |
| Forwarding | Conversation uploads | 989 gateway proxy requests or preflights and 1,011 upload-processing effects in the fixed 24 h ending 2026-09-18T16:06:07Z | 2026-09-27 (ENG-3488 ): Rails proxy fallback and PostHog gate removed from nativeDecision(); intake answers 401/503 itself; proxy.ts deleted; PR open, Ronald requested, not merged, not deployed. Previously: explicit proxy live; upload-driven sync completion #12469 deployed in b96da600; conversation_upload_intake_native (id 280845) Kuda-only with no native traffic; 2026-09-21 root cause: nativeDecision() authenticates by key_digest before evaluating the flag and Kuda’s key has no digest (#12633), so every Kuda upload is proxied to Rails without the flag being evaluated — the three lifetime $feature_flag_called events (2026-09-19T18:16Z, kuda true / rho false / qonto false) are a manual probe, not traffic; focused ingress parity PR #12682 closes the Rails response/persistence divergences (400 model-error bodies, presign empty-object and xCally cases, seconds-precision timestamps, NULL metadata, xCally connection log before validation, encrypted organization_data_sources.data) with pglite coverage, adds an unauthenticated_fallback warn, and ships the intake runbook ; native XCALLY/Outreach-Kaia direct upload processing #12483 is the separate processing package (flag 281904, complete — see its row); 2026-09-23 (exact 24 h ending 01:20:02Z): 1 US POST /conversations/upload 201, pre/overlapping the #12820 deploy at 00:56Z; post-deploy slice 00:57Z–01:20Z zero meaningful forwards — the digest gate is closed in code by ENG-3375 (API2 mirrors Rails’ deterministic-ciphertext equality lookup with lazy COALESCE digest backfill, inert without the encryption env; see the digest-gate section) | ENG-3218 , ENG-3242 , ENG-3375 | session , ingress parity session , digest-gate session | #12344 , #12469 , #12483 , #12682 , #12825 | Proxy and #12469 live since b96da600; #12483 live in 623cf7a7; #12682 merged; #12825 open, Ronald requested, not merged | Ronald review + green checks on the ENG-3488 PR; deploy; then one 24 h window per region with conversation_upload_native_intake outcome=unauthorized/storage_unconfigured at zero and created ≈ Kuda volume (unverified until run). Superseded gate: merge and deploy #12825 (the Rails Maintenance::BackfillApiKeyDigestsTask becomes an optional bulk alternative); then confirm one Kuda ciphertext_fallback, then the first natural $feature_flag_called response=true for kuda-pilot after the backfill timestamp plus conversation_upload_native_intake outcome=created in api2 logs, reconcile Kuda’s native uploads 1:1 against blobs/attachments/deliveries/conversations for one 24 h window, and require Kuda legacy_rails_gateway outcome=forwarded path=/conversations/upload at zero before the proxy is removed |
| Forwarding | Outreach webhook ingress and explicit compatibility allowlist | 210 Outreach rolling residue in the fixed 24 h ending 2026-09-18T16:06:07Z, zero forwards after 15:28Z; three signed deliveries at 15:43Z returned malformed_event 500 (parity/error evidence, no accepted natural delivery) | Secret provisioned both regions 2026-09-18T15:24Z, unsigned canary 403; 2026-09-27 (ENG-3495 ): forwarded_secret_unconfigured fallback removed, unset secret → 500 misconfigured; PR open, not merged | ENG-3246 | — | #12474 | Not started | CI, review, deploy, then a complete zero-forwarding window for Outreach and no allowlist regression |
| Forwarding | Action Cable to SSE | 825 connection attempts in the fixed 24 h ending 2026-09-18T16:06:07Z (1,077 in the 14:06:18Z window; 1,415 in the 10:16Z window; 1,917 in the earlier partial 13-hour window) | Temporary bridge live; first ConversationChannel SSE package active | ENG-3219 | session | #12326 | Current bridge deployed; SSE package not deployed | Focused PR proving tenant isolation, shared Redis-stream compatibility, bounded cleanup, and every first-party ConversationChannel consumer |
| Effects | Attachment purge | 20,000 / 24 hours; 126,929 / seven days; zero legacy calls from 13:08Z through the approximately 17:08Z census | Merged and deployed; four-hour zero-call signal is clean | ENG-3192 | session | #12353 | release-prod 9311bcca | Reconcile purge outcomes and outbox health, then complete production window with zero legacy calls |
| Effects | Image-caption context/writeback | 4,998 / 24 hours; 41,176 / seven days; zero legacy calls from 13:08Z through the approximately 17:08Z census | Merged and deployed; four-hour zero-call signal is clean | ENG-3193 | session | #12355 | release-prod 9311bcca | Reconcile caption outcome ratios and continuing population, then complete production window with zero legacy calls |
| Effects | Generic tool executor | 8,169 / 24 hours; 49,760 / seven days | Telemetry and first two native tools deployed; only approximately four post-deploy hours are available | ENG-3194 | session | #12356 | release-prod 9311bcca | Observe one representative business day of tool_id telemetry before ranking the remaining tools |
| Effects | QA sampling recovery | 0 / fixed 24 h ending 2026-09-19T17:47:25Z (14,694 at 2026-09-18T21:06:56Z; 123,685 of the 7 d total is zero-current-24 h residue); last Rails reconciliation call 2026-09-19T20:00:03Z, so the 7 d Rails-zero window cannot close before 2026-09-26T20:00:03Z (checked 2026-09-21T20:10Z: CloudWatch not refreshed — no AWS access — Sentry sampled traces show nothing since 2026-09-18T21:15Z, corroboration only) | qa_sampling_recovery_native at 100% (v21, all organizations) since 2026-09-19T20:08:04Z; all-organization natural cycles clean (20:52Z Qonto 5,000 / 4 / 732 / 4,264; 21:00Z Kuda 1,706 / 0 / 77 / 1,627, Qonto 5,000 / 11 / 97 / 4,892, Nala 237 / 21 / 55 / 161, Rho 4 / 1 / 3; 21:30Z Rho, Kuda, Nala, Qonto clean) with zero candidate/transcription failures, zero unprocessed, no early stops; deploy-time stoppedEarly shutdown stops are a known interruption class closed by the next natural cycle without replay; last Rails reconciliation call 2026-09-19T20:00:03Z, no Rails QA sampling or reconciliation call since; Interruption-safe sweeps (#12482) live; earlier canary/percentage-cohort history (v8-v11 five exact slugs + 5-10% group, v10 25% abort, 2026-09-18/19 exact-cohort cycles) is in the dated sections above and the rollout runbook | ENG-3217 | correction session ; 7 d gate check | Native package #12369 ; correction #12423 ; runbook #12471 ; interruption safety #12482 (ENG-3249 , merged) | release-prod 279ae48c (US web TD 1651 1/1, worker TD 1641 5/5; EU web TD 1669 1/1, worker TD 1658 5/5; pending/failed 0; migration locks acquired, listeners up), preceded by 2d02ce18 (#12541; locks EU 17:58:13Z / US 17:59:21Z) and eae24156 (locks EU 13:59:03Z / US 14:00:24Z), 06236881 (#12525), e45afa48 (#12521) and c47e1113 (#12520) | Keep at 100%; immediate rollback to exact slugs only on regression; no manual replay; complete zero-Rails-QA-sampling 7 d window (earliest close 2026-09-26T20:00:03Z, both regions, both /internal/conversation_transcription_reconciliations and /internal/qa_sampling_recoveries, CloudWatch census) before removing the Rails path, including the rails ownership fallback in qa-sampling-recovery/rails-run.ts; then bake the flag in and remove it |
| Effects | Call-recording attachment | 2,564 Rails creates / fixed 24 h ending 2026-09-18T16:06:07Z | Deployed in b96da600; call_recording_attachment_native Kuda-only with no native attachment events (Rails: 14 US provider_sync, 189 EU aircall_import); at 19:12Z still v1 exact Kuda, last_called_at 17:48:38Z was a single false evaluation for rho-tnkw (advisory); no widening; 2026-09-19: still exact Kuda with no native outcome; 1,834 Rails call-recording calls in the fixed 24 h ending 17:47:25Z; 2026-09-19: Qonto canary 37/37 native intents attached from 18:19Z (attempts 0, errors 0); flag v3 exact Kuda + Qonto + Rho at 18:43:12.702316Z; first Rho native attached at 18:59:35.125Z (provider_sync, transcriptionQueued=1), Qonto +38 attached 18:43:31Z–19:11:08Z, zero errors; v4 at 19:23:51.290614Z = three exact 100% + separate 10% org rollout (gate: all verified 24 h cohorts clean, zero Rails creates after 18:43:12Z); hold v4 pending natural percentage-cohort outcome, rollback to exact-only on regression; 1,708 Rails creates in the fixed 24 h ending 18:44:39Z | ENG-3236 | session | #12455 | release-prod b96da600 live in both regions since 12:40:56Z | EU canary, regional reconciliation, then complete zero-Rails 24-hour and seven-day windows |
| Effects | QA stage execution (execute_stage) | 1,713 / fixed 24 h ending 2026-09-18T16:06:07Z | Native transcribe_calls plus qa_stage_rails_fallback telemetry (#12463) and native related-conversation sync (#12470) are both live on b96da600; five sampling/backfill fallback reasons remain; see runbook | ENG-3237 , ENG-3244 | session , session | #12463 , #12470 | release-prod b96da600 live in both regions since 12:40:56Z | One complete regional window on b96da600 where execute_stage volume equals the summed remaining fallback reasons and no transcribe_calls or related-sync fallback appears |
| Effects | Upload-driven provider sync completion (ProviderConversationSyncsController#complete) | approximately 921 Rails complete calls / 24 hours in the fixed two-region window ending 2026-09-18, all from XCALLY upload hand-offs (xcally-upload-* sync runs) | Upload-driven completion job now claims the same native steps as provider-sync runs and skips the Rails call when every completion key is native; deployed in release-prod b96da600; 481 Rails completion calls in the fixed 24 h ending 2026-09-18T16:06:07Z; Rails endpoint retained as the PROVIDER_SYNC_NATIVE_EFFECTS rollback path | ENG-3242 | session | #12469 | release-prod b96da600 live in both regions since 12:40:56Z | One complete regional window on b96da600 with zero xcally-upload-* complete requests and unchanged completion-job failure rate |
| Effects | Direct upload processing (ConversationUploadsController#process_direct / retry_direct) | 1,011 Rails direct-upload processing calls / fixed 24 h ending 2026-09-18T16:06:07Z (XCALLY and Outreach/Kaia direct uploads) | Native API2 XCALLY and Outreach/Kaia processors (metadata parsing, agent/customer/team attribution, find-or-create identities, GSM/MP4 to WAV, recording attachment, duration) behind organization-targeted PostHog flag api2_conversation_upload_native_processing; deployed in 623cf7a7, flag confirmed absent in PostHog so the path is fail-closed and still calls Rails; no package-specific first-slice error; 2026-09-19: 826 Rails direct-upload calls in the fixed 24 h ending 17:47:25Z; flag 281904 created exact Kuda 18:19:08Z, first natural upload 649840 failed terminally Invalid recorded_at at 18:20:30Z (Kuda 2026-09-19 19:20:22 space-separated format accepted by Rails, rejected by API2 parser), flag disabled v2 immediately; 649840 recovered automatically via the normal scheduler after rollback (completed 18:33:01Z, deliveries 11387/11388 completed, conversation 6890594, one recording attachment; no replay/write); flag re-enabled by a human at v3 exact Kuda 20:54:19.918961Z after #12555 ; correction (2026-09-20) — this package is complete, not pending. The 19:16:11.440Z last_called_at readback was a null-response evaluation from before the 20:54:19.918961Z re-enable and was wrongly read as the latest activity. Read-only production reconciliation on api2-prod-us shows 609 Kuda direct XCALLY uploads between 2026-09-20T07:04:13.763Z and 13:48:43.269Z, every one status=completed. Delivery ledger: 609 process + 609 sync_completion rows, all owner=api2, state=completed, max attempts 1 / 0, replay_count 0, zero rows with last_error. Duplicate check: all 609 uploads are exactly one conversation and one attached file. No $exception mentioning upload, XCALLY or recorded_at in the trailing 48 h. Crucially, kuda-pilot is the entire direct-upload population — no other organization produced a upload_type='direct' row in the window — so exact-Kuda already is 100% of meaningful traffic and there is nothing to widen. The remaining Rails process_direct / retry_direct volume in the census is pre-cutover residue. Only $feature_flag_called cache misses reach PostHog (the 20 s POSTHOG_FLAGS_CACHE_TTL_MS in isOrganizationPosthogFlagEnabled), which is why three evaluations correspond to 609 processed uploads — another reason evaluation counts are not a volume signal. No replay or write was made; 2026-09-23: the 6 US mark_failed calls in the 24 h census ending 2026-09-22T23:23Z were terminal markings of native XCALLY failures (Sentry RULEBASE-API2-FH, 6 events 15:46Z–18:04Z) — the last Rails call on the native processing path; native terminal marking plus SQLSTATE-aware retry classification ships in the package described under “Remaining-internal-effects census at 2026-09-22T23:23Z” (no flag change; rollback is the same organization flag) | ENG-3248 | session | #12483 | release-prod 623cf7a7 (flag absent, off; re-confirmed absent in PostHog at 19:12Z; Rails-side RULEBASE-API-5WT AmbiguousDeliveryError on the Rails-owned enqueue path, 2 occurrences, latest 17:55:40Z; triaged non-blocking per ENG-3218 — 3 s read timeouts, client 201, uncertain ledger row left for the reconciler; conversation-upload/process/649026 unconfirmed without DB access, blocker if still owner=rails) | Ronald review plus a bounded single-organization Kuda canary before creating the flag; then enable per organization and require one complete regional window with zero process_direct/retry_direct requests for enabled organizations and unchanged upload failure rate |
| Effects | Unit21 source sync (OrganizationDataSourcesController#sync_unit21 / #sync_unit21_status) | 144 Rails Unit21 sync calls / fixed 24 h ending 2026-09-19T21:10:29Z, still firing through 21:10Z (largest unquestionably live family; overall effects 6,202 / 24 h, 336,317 / 7 d) | Native API2 Unit21 sync (durable run claim, incremental case/alert/rule listing, compliance persistence with redaction, cursor advance) behind organization-targeted PostHog flag api2_unit21_sync_native; Rails call retained as the fail-closed path for flag off, flag/source lookup failure or missing source; #12571 merged/deployed (017b86ef); flag 282001 exact rho-tnkw canary at 02:30Z hit a runKey collision (no new run, cursors unchanged) and was disabled ~02:32Z; the 02:40Z engine=rails tick hit the same collision and returned the stale duplicate (cases max synced_at 2026-09-14T20:20Z, alerts 2026-09-15T00:50Z) → active stale-sync incident needing the two-step operational rollback (Flipper pause + Rails GoodJob cron) or the key fix, human decision pending; 2026-09-27 (ENG-3489): Rails branch and reconciliation removed, native-only behind Flipper pause (see Rails-at-zero section); #13197 merged externally as bf219623ae without a submitted Ronald review — recorded exception, not approval | ENG-3278 | session | #12571 | Blocked — active stale-sync incident (0 %, human rollback/fix decision pending) | Reviewed idempotency-key fix deployed, then a fresh natural exact-organization canary with organization_data_source_sync_runs / compliance_* row reconciliation against the Rails baseline, then a complete zero-Rails 24-hour window |
| Effects | Direct RAILS_INTERNAL_BASE_URL call-site guard | 18 source references outside tests at 2026-09-21 (see reconciliation) | Source-gate test with an explicit per-file allowlist; new call sites and stale allowlist entries both fail CI | — | session | TBD | Not started | Merge; then shrink the allowlist as each family below retires |
| Effects | Scorecard chat context (GET /scorecards/:id, public route with forwarded browser cookie) | Not present in the requester-provided 72 h post-cutover Rails path census (unverified by this session) | Ported: lib/scorecard-chat-context.ts now builds the payload from findScorecard / buildScorecardFullPayload scoped to the session organization; no flag (read-only, same payload builder the native /scorecards/:id route already serves) | — | session | TBD | Not started | Zero GET /scorecards/:prefix_id requests with Cookie from the API2 origin in one complete regional window; Instruct scorecard-page chat error rate unchanged |
| Effects | Knowledge-base document images (POST /internal/knowledge_base_document_images) | Not present in the requester-provided 72 h post-cutover Rails path census (unverified by this session); fires once per Zendesk/Scribe document sync that contains images | Ported: lib/knowledge-base/document-image-store.ts downloads allowed-host images, deduplicates by MD5 within the organization’s KnowledgeBaseDocument attachments, writes ActiveStorage blob/attachment rows on the shared cloudflare service and mints rails_blob_url-shaped signed URLs; no flag — Rails controller and ImageStore retained untouched | — | session | TBD | Not started | One complete regional window with zero knowledge_base_document_images requests, unchanged knowledge-base sync failure rate, and image <figure> rehosting still present in newly synced Zendesk/Scribe documents |
| Effects | Scheduled export run/preview (POST /internal/scheduled_exports/{run,preview}) | No API2 importer since 66dce4908; not present in the requester census | lib/run-scheduled-export.ts deleted as dead code; Rails routes untouched | — | session | TBD | Not started | Confirm zero Rails scheduled_exports requests in one window, then retire the Rails routes under the normal-work workstream |
| Effects | Sales-call compliance analysis (/internal/sales_call_compliance_analyses/:id/*) | Code evidence on main 6087ec7dcf: every synced sales_call enqueues the job and all five Rails calls hard-fail at Rails 0/0; failed-evaluation count since 0ca2801c89 unverified (no request-log/DB access); US api2_sales_call_compliance_pipeline enabled globally, EU has no flag | Native lib/sales-call-compliance/native.ts (context, start, transcription persistence, idempotent results with reviewed routing, terminal fail); rails.ts and its source-gate entry deleted; focused tests green | ENG-3493 | session | PR pending | Open; not merged; no production change | Ronald review; green required checks; post-deploy one natural sales-call completion per region (snapshot + compliance check + reviewed routing) and one forced-failure canary; then 24 h / 7 d zero-request windows credited by an operator |
| Effects | QA request failure finalisation (POST /internal/qa_evaluation_requests/:id/finalize_failure) | Not present in the requester-provided 72 h post-cutover Rails path census (unverified by this session) | Packaged (ENG-3471): failure-effects.ts no longer calls Rails; imported requests deliver natively without requester email, nonterminal/missing-row effects retry in the outbox | ENG-3471 | this session | see ENG-3471 section | #13132 merged externally 2026-09-27T09:56:02Z with no submitted Ronald review (not approval); deployment unverified | Deployed-SHA 24 h / 7 d census finalize_failure = 0; delete route with the QA stage family |
| Effects | Evaluation-contest reviewer notifications (POST /internal/evaluation_contests/:id/notifications) | Live path for every non-canary organization (flag 275344 targets kuda-pilot, nala only); attempt counts since Rails 0/0 unverified | Silent hard failure at Rails 0/0 (drain claim left uncertain, run completed without delivery); see ENG-3518 analysis and runbook | ENG-3518 | Session | pending | Not deployed; native-only (owner = rails branch and drain removed; open rails runs taken over unless the drain was marked delivered) | Deploy both regions; one non-canary contest per region with owner = api2 and delivered/skipped rows; operator re-send decision for pre-ENG-3518 uncertain rows; then delete Rails controller/model method and flag 275344 |
| Effects | Counter-cache repair (Internal::CounterCachesController#fix) | 129 successful calls / rolling 24 h ending 2026-09-20 (US 73, EU 56); 17 since 2026-09-19T23:45Z | All seven steps implemented natively (#12595 , merged) behind Flipper api2_counter_cache_native_repairs; 2026-09-20T03:38Z read-only check: no flipper_features row in US or EU, native disabled in both regions; ENG-3299 moves the gate to deployment-region-scoped PostHog (unknown/off/unreachable = Rails), Flipper pause switch retained; #12615 human-merged 03:56:26Z (8f84f67350, no recorded review) into release-prod 39f11065e, PostHog flag not created; 2026-09-21: group_employees step found to zero leaf-group employees_count (two ordered writes to one column, same as the Rails reconciler) — single aggregate write + leaf-group pglite regression in #12679 , which also carries the rails-client.ts / direct RAILS_INTERNAL_BASE_URL reachability audit and removes three unreachable API2 wrappers (completeRailsConversationCxRiskSnapshot, createJiraIssueSuggestion, run-scheduled-export.ts); 2026-09-27 (ENG-3489): PostHog selector and Rails fallback removed, native-only behind Flipper pause | ENG-3288 , ENG-3299 | session | #12595 , #12615 , #12679 | #12595 deployed; #12615 deployed (US 8cfa8d65dd, EU 39f11065e COMPLETED 04:14:36Z), gate off in both regions | Merge #12679 before activation; human activation review; create the flag for one region at 100%, wait one three-hour tick, compare per-step rows and counter parity with Rails (expect a one-time group_employees delta, see defect note), then add the other region |
| Effects | Expired-evaluation sweep (Internal::ExpiredEvaluationsController#process_all) | 48 / 24 h (US 24, EU 24) at 2026-09-20T01:18:53Z before the native port; 24 US + 0 EU / exact 24 h ending 2026-09-23T01:20:02Z, every one a kuda-pilot deferral (sole false $feature_flag_called evaluator in the trailing 48 h); zero Kuda qa_evaluation.request_completed/failure events since 2026-09-19 | Native markers + shared sweep lock deployed (#12601 ); flag 282005 expired_evaluation_cleanup_native v2 exact Easypay/Rho/NALA + 50% group since 2026-09-20T04:06Z (first natural tick US 4/4, EU 4/4, zero deferrals); v3 2026-09-23T01:32:23.151665Z adds exact kuda-pilot (bounded activation, no data write); per-organization deferredOrganizations attribution in the sweep result (#12826 , human-merged 01:49:59Z under user review authorization, no submitted Ronald review) deployed in b905bc20 by 02:28:57Z; natural 02:05Z tick US 1 candidate/1 failed, EU 0, and 03:05Z tick US/EU candidates = 0, both with deferredToRails = 0, rails = null (03:05Z had no candidates, so it is not a native-path outcome sample) | ENG-3289 , ENG-3376 | session | #12826 | Native path live in both regions; flag v3 live 01:32:23Z; #12826 attribution deployed on b905bc20 (all four services, 02:28:57Z) | A natural tick with Kuda candidates handled natively (deferredToRails = {0,0}, zero US process_all); rollback = flag back to v2 (or Flipper pause_api2_expired_evaluation_cleanup + Rails crons); then 24 h and 7 d zero windows in both regions before removing Rails markers/controller/crons; widening the 50% group is a separate human step |
| Effects | Agent Slack notifications on legacy passthrough connections (POST /internal/slack_notifications, POST /internal/conversations/:id/slack_notifications) | 1 US 201 at 2026-09-23T13:00:44Z (Rho) in the exact 24 h ending 19:30:00Z; 0 EU; the only Slack call in every census since 01:20Z | Native chat.postMessage path (#12719 ) deployed behind api2_slack_notification_native, which was absent in PostHog until 19:44:52Z (100% Rails by fail-closed design before then); 53/53 focused tests on main; QA-sampling ownerReason attribution pattern in #12839 is the reference for adding fallback-reason logging here if the canary needs it; 2026-09-27: #13023 superseded by merged #13143 (api2-connection.ts on main), recommend closing | ENG-3352 , ENG-3379 | session | #12719 merged | Deployed both regions; flag 286616 v1 exact rho-tnkw active 2026-09-23T19:44:52Z → disabled ~2026-09-24T15:47Z (v2) after the 13:00:01Z evaluation missed (Active Rails Slack data source 11 not found) and fell back to Rails 201; #12976 (ENG-3428 ) adds miss-reason attribution, user-merged 17:08:54Z, not in the e504aad3 rollout serving at ~17:17Z | Held disabled: coordinator read-only DB read (2026-09-25T02:15Z) found ODS 11 discarded 2026-06-30 (ODS 55 also discarded), so re-enabling would repeat the miss; parity decision taken 2026-09-25 (Chidi): bind to the organization’s active oauth2 API2 Slack connection, implemented in #13023 (open, required checks green at e60afb0c6, Ronald review pending, no merge before approval); then human merge → verified ECS running SHA → operator re-enable exact Rho → natural native ok:true + ts with tokenSource=api2_connection, zero Rho Rails Slack calls, no Sentry error; rollback = disable flag; 24 h / 7 d windows restart from re-enable |
| Schema | public.* migration ownership Rails → API2 Drizzle (Phases 1–2: introspection + live-DB diff) | Live drizzle-kit push --strict diff against a throwaway structure.sql + baseline + migrate clone, 2026-09-21: 257 statements — public 4 (two COALESCE(...) DESC expression indexes dropped/recreated, no table/column/constraint/view statements), api2 253 (pre-existing schema.ts drift incl. 4 DROP TABLE), agent_api 0. Read-only regional census the same day: US 2,398 vs EU 2,395 indexes, 10 invalid indexes, structure.sql behind production | Blocked; draft for human review, no auto-merge. G1 repair migration #12980 human-merged 2026-09-24T17:08:29Z (no Ronald review); G1 held at 17:17Z (US/EU 2,395 identical, 0 invalid, 0 leftovers, version recorded); G2/G3 first run on 2026-09-24 regional dumps: both regions public-drift 71, identical, traced to structure.sql phantom objects (2 tables, 24 columns) not present in production — G3 not credited; structure.sql regenerated from the EU dump in #13021 (human-merged, no Ronald review), G2/G3 second run 2026-09-25: declaration regenerated, both regions public-drift 0 / push-visible statements = the 4 known expression-index churn only (probe exit 2); G3 pending owner decisions on the US-only agent_api view, US NOT VALID FK and databasechangelog; G4–G7 open | ENG-3345 | session | #12694 | Not deployed; zero schema changes, drizzle.config.ts unchanged | Human-owned regional index reconciliation and invalid-index rebuild PR; regenerate the declaration from per-region schema-only dumps and re-run the live diff to zero before Phases 3–5 |
| Schema | public.* ownership: read-only TTY-free live-diff probe, expression-index root cause, operator gates G1–G7 | Local 2026-09-23 (structure.sql 05527e7c6, 409 migrations): pnpm db:probe-public-ownership exit 2 — publicPushStatements 4 (known idx_conversations_*nav_date churn), public-drift 0, api2/agent_api 0, strict-only-index-churn 48 (introspection artefacts); TTY push --strict 259 total / public 4, aborted; two truncated pg_search_documents index expressions restored in the declaration; no production access, coordinator census 2026-09-23T01:20:02Z recorded in the dated section; coordinator audit at 2026-09-23T02:20:00Z (24 h internal US 42 / EU 2, forwarded successes 25, image 0; ECS still d6607a6d while release-prod 89d1b45a) and 2026-09-23T03:20:00Z (24 h internal US 41 / EU 2, forwarded successes 25, image 0; post-canary 02:35:13Z→03:20Z zero non-health Rails calls, zero forwarded) and 2026-09-23T04:20:00Z (24 h internal US 40 / EU 1; one natural US 04:00Z combined-email Rails call in the post-canary slice) recorded as sub-sections via docs-only #12834 | Merged (#12831 ); blocked on G1 (human repair) | ENG-3378 | session | #12831 | Probe code/declaration present on API2 b905bc20 in all four US/EU web+worker services (rollout complete 02:28:57Z); runtime config unchanged (drizzle.config.ts still api2/agent_api); no production probe/DDL; G1 blocked | G1 repair PR, then per-region schema-only dump → local *_push restore → probe exit 0 in US and EU with the strict list read; then Phases 3–5 |
How the automation drives this plan
The Rails retirement automation uses this document as its backlog and durable status board. On every material run it must:
- Read this plan before choosing work.
- Refresh the two boundary measurements with exact 24-hour and seven-day regional evidence.
- Reconcile merged and deployed revisions before attributing a traffic change.
- Update the package ledger with issue, Devin session, PR, review, deployment, residual volume, and next gate.
- Use available implementation capacity on the highest-impact ready package. Do not fill all slots with source cleanup while a runtime dependency is ready to move.
- Add a newly discovered family to this document before starting its package.
- Report only material movement: a package dispatched, a PR reviewable or shipped, a material volume change, a new dominant family, a regression, or a completed retirement gate.
Code-source counts remain useful deletion evidence. They do not outrank live runtime dependencies when choosing the next package.
Rails shutdown gate
Rails application services can stop only when all of these are true in both regions:
- the canonical
/v1public contract and documentation are deployed; - meaningful gateway forwarding is zero;
- explicit API2-to-Rails calls are zero;
- the
/cablebridge has no supported consumer and is removed; - Active Storage URLs, analysis, and purge no longer require Rails web;
- no normal application GoodJob work remains;
- residual operational work has a non-Rails owner or is retired;
- business-outcome reconciliation is clean;
- correctness-heavy transfers received independent review;
- complete production observation windows show no hidden caller or fallback.
Database tables and Rails models may remain after this gate if API2 accesses the data directly and no Rails process is required. Their ownership migration and deletion continue under the Rails table ownership map.