Skip to content

SCA-1808: Offer yearly switches in mobile retention with a UID holdout - #25158

Open
Git-on-my-level wants to merge 17 commits into
mainfrom
agent/m1-studio-codex-sol/17581fef0939
Open

Git-on-my-level wants to merge 17 commits into
mainfrom
agent/m1-studio-codex-sol/17581fef0939

Conversation

@Git-on-my-level

@Git-on-my-level Git-on-my-level commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

SCA-1808 offers the existing-price yearly switch before the mobile cancellation discount and once on Home for engaged monthly subscribers. Both placements share durable 50/50 UID experiment enrollment; only a newly enrolled treatment UID can display either offer. Controls survive retries, devices and flag toggles. Engagement v1 is eight completed, visible, non-discarded saved captures in 14 days, not eight distinct active days.

Existing invoice preview and price/amount/proration-pinned switch APIs remain the only billing path. Cancellation stays reachable while admission loads, after refusal and after payment failure. Both existing discount events are registered; yearly observations have closed placement/experiment properties. Account wipe explicitly erases the existing top-level UID experiment row and propagates failures for retry. No payer ledger, identity key, exporter or retention policy is introduced.

Hold before merge: David’s exact render/copy approval and UI-author approval remain required. The owner authorized retaining upstream cf2cb6b0ce for source preparation only: above 130% text size, yearly preview values stack below labels. The candidate packet includes normal/150% comparisons; normal previews are byte-identical before/after, and yearly/cancellation English copy is unchanged. No approval is inferred from tests or the owner’s source-readiness decision. Prices, plans, ceilings, production flags and rollout are unchanged.

Admission stays dark until an approval reference and separately authorized rollout are configured. After those gates, YEARLY_SWITCH_RETENTION_ENABLED defaults on; explicit false stops both new placements while preserving controls, cancellation and the existing settings switch. No production access, merge, deployment or activation is part of this PR.

UID assignment is not payer clustering. Linked accounts and duplicate customers may contaminate arms. Missing/ambiguous billing linkage is unknown/excluded with coverage reported; overall Stripe totals cannot prove arm-specific effects. Revenue basis, refunds/currency handling, UID denominator, missingness and uncertainty are specified in backend/docs/experiments/EXP-006-yearly-switch-retention.md. Small cells and recoverable complements are suppressed. Measured lift is unknown; direct churn credit is 0pp.

The verifier fails closed: local receipts, authority booleans, signatures and plausible URLs cannot establish provenance. An owner-authorized adapter must authenticate the producer and independently verify referenced facts. Exact release/approval, assignment/non-overlap, protected take-up, 30/60-day paid ends and realized net revenue by enrolled UID remain live acceptance requirements.

Required read-only live/maturity command:

python3 backend/scripts/verify_yearly_switch_retention.py --read-only --require-live --require-mature-holdout

Current result: WAITING, exit 2 for authorized serving, approval and mature arm-linked aggregate evidence. Source correctness and renders do not establish LIVE acceptance.

The owner-authorized second-look correction fixes exactly three findings:

  • discussion_r4560109077: yearly confirmation rechecks the parent busy state after its asynchronous enabled read. A regression delays that read, starts real Cancel Subscription, and verifies no yearly preview/sheet opens until cancellation finishes; a later attempt works.
  • discussion_r4555538010: the UID assignment erase and experiment ID live in database/yearly_switch_retention.py. Account wipe imports the primitive at module scope. Delete target/order, propagated failure and retry semantics stay unchanged; account-purge failure/retry is exercised through the real wipe helper.
  • discussion_r4560109069: the retention route and admission helper use module-level imports. Firestore clients are still constructed only at call time, with a default-client call-boundary regression.

Current main 4f13c9db8ec089b7bb22c21a2eb8b485548073fc is merged without rebase/force-push to include the landed upstream guardrail repairs. No guardrail is weakened. The Firestore runtime witness patches the new module-level references while retaining its existing signature/profile, call-count and network-blocking assertions. The People purge fingerprint is re-audited after the import move; People subtree and generation-head erasure are unchanged.

Focused validation: 165 backend tests (billing, admission, verifier, erasure, strict People generation and inventory); eight widget tests; clean targeted Flutter analysis; 31 owned loopback emulator checks, stopped afterward. Render/copy and repository-check receipts are recorded on SCA-1808. The existing candidate packet remains subject to David’s exact approval; synthetic prices are not live prices. No device/debugger/deployed-client acceptance is claimed.

The non-admitted mounted-Home correction remains intact: later genuine Home presentations may retry; rebuilds and focus-only changes do not. Existing assignment owns stable controls and once-only/cross-placement admission.

Return through in_review is routing under the latest owner RULING, not another full review round or author verdict. The independent second-look findings above are implementation dispositions, not an independent SHIP claim. David’s exact final render/copy approval and exact-head UI-author approval remain mandatory. Live/maturity acceptance remains WAITING; no activation or churn lift is authorized.

Candidate packet: .agent-artifacts/ui-evidence/yearly-retention-20261115/PACKET.md; original renders and exact copy remain in .agent-artifacts/ui-evidence/yearly-retention/. The preserved upstream accessibility layout stacks preview values above 130% text size.

Product invariants: INV-UI-1, INV-UI-3, INV-DATA-1.

Failure-Class: none

Why this is a shared primitive: both placements share UID experiment admission and the existing preview/charge path; account wipe and the admission deletion fence share the database-owned erase. No new billing or identity authority.

Current integration head: 26bdd57bcfafc3883d7373299452fc78b1cdde6e.

Existing immutable render packet head: 32d7201844647eadd12425eb9249dca97031c38f.

Cancellation placement

Engaged placement

Yearly preview at 150% text

Integration repair on November 15: merged current main (27ff8bb57f37567fb526b2956208c788d95972b8) into the same branch without rebase or force-push. Preserved both feature-flag lists and re-audited the combined account-erasure inventory. The inherited shard 8/8 timeout is addressed by upstream c2e2e50972, which keeps the test assertions and its five-second cleanup deadline. Regenerated the Swift and four TypeScript clients' method-count comments (550 → 552); no executable code changed in those follow-ups.

Bounded checks on m1: 389 backend tests pass, including the entire previously failing outbox file, billing, UID admission, retryable account erasure and strict People generation. Seventeen widget tests and eight render checks pass; targeted Flutter analysis is clean. All six fresh placement/preview/failure rasters are byte-identical to the prior packet, and all 129 yearly/cancellation/discount/retention English keys match. Original visible packet remains unchanged. These tests ran on the integration commit; the final follow-ups change only generated method-count comments.

Shared preflight: all 70 checks passed at dd66f42ac111971f5cd0c80184a1a052fb69fdd7 (399.68s). The final follow-up changes only four TypeScript method-count comments; Swift and TypeScript generator checks pass at the final head. Five final-head metadata checks and the bounded push gate pass, including backend typechecking (zero errors), selected backend units, OpenAPI/generated-output checks and a cold desktop Swift debug build. Shared preflight was reused via its documented hatch; remaining hook checks were retained. Receipts are attached on SCA-1808. Hosted CI is running and was not awaited. Required read-only live/maturity command remains WAITING, exit 2. David's exact render/copy and exact-head UI-author approval holds remain; no third full review, production access, merge, deployment, flag activation or churn-lift claim is authorized.

@mintlify

mintlify Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
omi 🟢 Ready View Preview Nov 15, 2026, 7:20 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T05:48:58.416990Z dd41b13 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

@kodjima33 UI changes involving Git-on-my-level require your approval. Review the current diff, then comment /approve-ui 84bf580661be632f8fcbb571c7cbdfa072f65893. Each new head needs a fresh approval. To withdraw it, comment /reject-ui <full head SHA>. Non-UI work and unrelated access are unaffected.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 84bf580661

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/database/user_data_deletion.py Outdated
Comment on lines +82 to +84
from utils.yearly_switch_retention import erase_assignment

erase_assignment(uid, client)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move assignment erasure into the database layer

Every account wipe now imports utils.yearly_switch_retention from the database layer, reversing the dependency hierarchy explicitly required by backend/AGENTS.md. This also couples the shared erasure primitive to the retention business-logic module and its experiment/telemetry imports just to delete one document. Move the assignment-deletion primitive into database/ and import it at module scope from both callers, preserving the existing retry-on-failure behavior.

AGENTS.md reference: backend/AGENTS.md:L168-L174

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

@kodjima33 UI changes involving Git-on-my-level require your approval. Review the current diff, then comment /approve-ui dd41b1354cb9f38f0dcdb2e182bf960023dd8a1f. Each new head needs a fresh approval. To withdraw it, comment /reject-ui <full head SHA>. Non-UI work and unrelated access are unaffected.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd41b1354c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/routers/yearly_switch.py Outdated
Comment on lines +55 to +56
from database.account_deletion_policy import account_deletion_blocks_access
from database.conversations import get_conversations_count

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move retention dependencies to module-level imports

The new admission handler imports its deletion policy, conversation counter, and fallback recorder inside request/error branches; utils/yearly_switch_retention.py::admit likewise imports the Firestore accessor inside the function. These additions violate the explicit module-level-import requirement in backend/AGENTS.md, leaving dependencies hidden until those branches execute. Move these imports to module scope while continuing to call get_firestore_client() lazily. This is separate from the previously flagged database-to-utils dependency reversal.

AGENTS.md reference: backend/AGENTS.md:L168-L174

Useful? React with 👍 / 👎.

Comment on lines +130 to +132
if (!await _checkEnabled() || !mounted || _candidate != candidate) return;
final switched = await showYearlySwitchSheet(context,
surface: widget.surface, load: widget.loadPreview, apply: widget.apply, readEnabled: widget.readEnabled);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck cancellation busy state after the flag request

If a reader taps the yearly row and then Cancel Subscription or the discount button while _checkEnabled() is pending, the parent sets busy=true, but this continuation still opens the yearly confirmation because it only checked widget.busy before the await. With a slow cancellation or discount request, this exposes another billing action while the first is running; the yearly sheet calls the payment API directly and does not honor UsageProvider's payment-loading guard. Recheck widget.busy after the asynchronous flag read and before opening the sheet.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

@kodjima33 UI changes involving Git-on-my-level require your approval. Review the current diff, then comment /approve-ui 32d7201844647eadd12425eb9249dca97031c38f. Each new head needs a fresh approval. To withdraw it, comment /reject-ui <full head SHA>. Non-UI work and unrelated access are unaffected.

@github-actions

Copy link
Copy Markdown
Contributor

@kodjima33 UI changes involving Git-on-my-level require your approval. Review the current diff, then comment /approve-ui 8bb80d6565f6a2cdcfaa1b933f6c7d58710d9f19. Each new head needs a fresh approval. To withdraw it, comment /reject-ui <full head SHA>. Non-UI work and unrelated access are unaffected.

@github-actions

Copy link
Copy Markdown
Contributor

@kodjima33 UI changes involving Git-on-my-level require your approval. Review the current diff, then comment /approve-ui 8a54fdc4539073bb5eb53270bb0e1703397f6642. Each new head needs a fresh approval. To withdraw it, comment /reject-ui <full head SHA>. Non-UI work and unrelated access are unaffected.

@github-actions

Copy link
Copy Markdown
Contributor

@kodjima33 UI changes involving Git-on-my-level require your approval. Review the current diff, then comment /approve-ui c7d6cd6859e497dd20d1603e8439420775b9b51f. Each new head needs a fresh approval. To withdraw it, comment /reject-ui <full head SHA>. Non-UI work and unrelated access are unaffected.

@github-actions

Copy link
Copy Markdown
Contributor

@kodjima33 UI changes involving Git-on-my-level require your approval. Review the current diff, then comment /approve-ui 940087324fd0a60c921d32a72b048e8773a55693. Each new head needs a fresh approval. To withdraw it, comment /reject-ui <full head SHA>. Non-UI work and unrelated access are unaffected.

@github-actions

Copy link
Copy Markdown
Contributor

@kodjima33 UI changes involving Git-on-my-level require your approval. Review the current diff, then comment /approve-ui 26bdd57bcfafc3883d7373299452fc78b1cdde6e. Each new head needs a fresh approval. To withdraw it, comment /reject-ui <full head SHA>. Non-UI work and unrelated access are unaffected.

This branch was successfully deployed

1 active deployment
staging - docs — 26bdd57b Deployed Nov 15, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant