Skip to content

fix(ota): re-land the background-apply rollback fix stranded by #2960/#2963 - #2986

Merged
innolope-dev merged 1 commit into
devfrom
fix/ota-background-apply-rollback-relanded
Sep 4, 2026
Merged

fix(ota): re-land the background-apply rollback fix stranded by #2960/#2963#2986
innolope-dev merged 1 commit into
devfrom
fix/ota-background-apply-rollback-relanded

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Re-lands 1d67a3cc, which was merged into a branch that had been merged away eight seconds earlier and so never reached dev.

What happened

19:53:28Z #2960 merged fix/ota-android-restart-deadlockdev
19:53:36Z #2963 merged fix/ota-background-apply-rollbackfix/ota-android-restart-deadlock

The child landed on a branch that no longer had anywhere to go. Both PRs read as merged, the branch still holds the commit, and nothing anywhere says the fix is missing — dev has simply never had it. Same shape as the loss #2941 recovered from, eight seconds instead of a stale head.

Found by auditing every PR merged in the last 14 days for commits absent from dev by patch-id. Two other gaps turned up and are not in this PR: #2945 is still open with the TASK-22193 review-sheet work, and a docs-only commit sits on api-ts fix/card-geo-prohibited (#1450).

What it fixes

Unchanged from the original — cherry-picked with no conflicts, author preserved:

Capgo consumes a next()-staged bundle only from installNext(), which runs from appMovedToBackground(). The reload therefore lands in a process the OS is about to freeze. Freezing stops threads but not the clock, so on resume every overdue setTimeout fires at once — webpack's 120 s chunkLoadTimeout included — and every in-flight import() rejects with a ChunkLoadError for a local file that was never missing. notifyAppReady sat behind two of those imports plus React hydration, so it was never called and the plugin rolled the bundle back.

Live in Sentry as PEANUT-UI-SVT (booted 13:46:04, no breadcrumb for 23m46s, then four ChunkLoadErrors and the rollback inside 40 ms — one of them the chunk that calls notifyAppReady) and PEANUT-UI-SVE (the milder shape, the call landing 6 ms past the deadline).

Net effect on dev today: an OTA bundle that applies while the app is backgrounded can roll itself back on Android. src/utils/native-app-ready.ts and its test do not exist on dev at all.

Verified

  • Cherry-pick applied clean: 12 files, +457/-15
  • pnpm typecheck, prettier --check ., ds-lint ratchet unchanged
  • Full jest suite: 465 suites / 5887 tests green
  • next build clean

Capgo consumes a next()-staged bundle only from installNext(), which runs
from appMovedToBackground(). The reload therefore lands in a process the OS
is about to freeze. Freezing stops threads but not the clock, so on resume
every overdue setTimeout fires at once — webpack's 120 s chunkLoadTimeout
included — and every in-flight import() rejects with a ChunkLoadError for a
local file that was never missing. notifyAppReady sat behind two of those
imports plus React hydration, so it was never called and the plugin rolled
the bundle back.

PEANUT-UI-SVT is the whole chain in one trace: booted at 13:46:04, no
breadcrumb for 23m46s, then four ChunkLoadErrors and the rollback inside
40 ms — one of them the chunk that calls notifyAppReady. PEANUT-UI-SVE is
the milder shape, where the call landed 6 ms past the deadline.

- notifyAppReady now runs from a raw inline script in the root layout,
  straight off the bridge stub Capacitor injects at document start. No
  chunk, no hydration, no React. The React path still calls it as a
  fallback for binaries whose bridge does not expose the stub.
- That trades away Capgo's rollback net, so a boot-incomplete counter
  replaces it. It is the better net: Capgo could not tell a bundle whose JS
  is broken from one the OS froze and rolled back on both, whereas a frozen
  boot resumes and clears the counter.
- A bundle staged by an earlier launch is now applied in the foreground
  while the splash still covers the reload, instead of being left to the
  background apply. Once per bundle, and only on binaries that can restart
  in place; outside that window the background apply remains the fallback.
- import() retries once on a chunk-load failure, since the second attempt
  runs with the process scheduled again.

Also makes the failure legible. Capgo's own logs reach Sentry only through
bridge.eval("console.error(...)") into a WebView the rollback is about to
tear down, and the [capgo] prefix filter dropped whatever survived — the
entire population read as one event in 90 days. The two rollback strings
are now actionable, and getFailedUpdate() is reported at launch from the
plugin's own SharedPreferences record, which outlives the reload.
@innolope-dev innolope-dev self-assigned this Sep 4, 2026
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 4, 2026 7:12pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 6823fd81-a0b0-4686-abde-b11a3ac58e7b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@innolope-dev
innolope-dev marked this pull request as ready for review September 4, 2026 19:07
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7163.66 → 7166.57 (+2.91)
Findings: +1 net (+21 new, -20 resolved)

🆕 New findings (21)

  • critical complexity — src/utils/capgo-updater.ts — CC 87, MI 60.09, SLOC 393
  • high complexity — src/context/OtaUpdateContext.tsx — CC 41, MI 66.09, SLOC 175
  • high complexity — src/app/layout.tsx — CC 11, MI 40.68, SLOC 106
  • medium high-mdd — src/context/OtaUpdateContext.tsx:46 — OtaUpdateProvider: MDD 38.5 (uses across many lines from declarations)
  • medium high-mdd — src/context/OtaUpdateContext.tsx:102 — : MDD 25.2 (uses across many lines from declarations)
  • medium complexity — src/hooks/useSplashGate.ts — CC 21, MI 68.58, SLOC 73
  • medium complexity — src/utils/chunk-error-recovery.ts — CC 14, MI 58.44, SLOC 49
  • medium react-effect-fetches — src/context/OtaUpdateContext.tsx:55 — useEffect for async data fetching — use TanStack Query / server components
  • low high-dlt — src/context/OtaUpdateContext.tsx:46 — OtaUpdateProvider: DLT 28 (calls 28 distinct functions — high context load)
  • low high-mdd — src/app/layout.tsx:155 — RootLayout: MDD 19.5 (uses across many lines from declarations)
  • low high-mdd — src/context/OtaUpdateContext.tsx:55 — : MDD 17.0 (uses across many lines from declarations)
  • low high-mdd — src/utils/capgo-updater.ts:515 — leaveBetaOtaChannel: MDD 16.6 (uses across many lines from declarations)
  • low high-mdd — src/utils/capgo-updater.ts:517 — : MDD 16.6 (uses across many lines from declarations)
  • low high-dlt — src/context/OtaUpdateContext.tsx:55 — : DLT 15 (calls 15 distinct functions — high context load)
  • low high-dlt — src/utils/capgo-updater.ts:99 — checkAndStageUpdate: DLT 15 (calls 15 distinct functions — high context load)
  • low high-mdd — src/utils/capgo-updater.ts:219 — applyStagedBundle: MDD 14.6 (uses across many lines from declarations)
  • low high-mdd — src/utils/capgo-updater.ts:23 — initCapgoUpdater: MDD 14.1 (uses across many lines from declarations)
  • low high-mdd — src/utils/capgo-updater.ts:99 — checkAndStageUpdate: MDD 11.4 (uses across many lines from declarations)
  • low missing-return-type — src/app/layout.tsx:155 — RootLayout: exported fn missing return type annotation
  • low missing-return-type — src/context/OtaUpdateContext.tsx:46 — OtaUpdateProvider: exported fn missing return type annotation

…and 1 more.

✅ Resolved (20)

  • src/utils/capgo-updater.ts — CC 76, MI 60.68, SLOC 347
  • src/context/OtaUpdateContext.tsx — CC 33, MI 65.3, SLOC 152
  • src/app/layout.tsx — CC 11, MI 40.79, SLOC 105
  • src/context/OtaUpdateContext.tsx:43 — OtaUpdateProvider: MDD 39.0 (uses across many lines from declarations)
  • src/context/OtaUpdateContext.tsx:87 — : MDD 25.2 (uses across many lines from declarations)
  • src/context/OtaUpdateContext.tsx:52 — : MDD 20.6 (uses across many lines from declarations)
  • src/hooks/useSplashGate.ts — CC 20, MI 67.97, SLOC 71
  • src/utils/chunk-error-recovery.ts — CC 12, MI 57.68, SLOC 39
  • src/context/OtaUpdateContext.tsx:52 — useEffect with empty deps + setState — derived state anti-pattern
  • src/context/OtaUpdateContext.tsx:43 — OtaUpdateProvider: DLT 24 (calls 24 distinct functions — high context load)
  • src/app/layout.tsx:154 — RootLayout: MDD 19.5 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:439 — leaveBetaOtaChannel: MDD 16.6 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:441 — : MDD 16.6 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:98 — checkAndStageUpdate: DLT 15 (calls 15 distinct functions — high context load)
  • src/utils/capgo-updater.ts:218 — applyStagedBundle: MDD 14.6 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:23 — initCapgoUpdater: MDD 13.9 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:98 — checkAndStageUpdate: MDD 11.4 (uses across many lines from declarations)
  • src/app/layout.tsx:154 — RootLayout: exported fn missing return type annotation
  • src/context/OtaUpdateContext.tsx:43 — OtaUpdateProvider: exported fn missing return type annotation
  • src/context/OtaUpdateContext.tsx:174 — useOtaUpdate: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/native-app-ready.ts 0.0 2.4 +2.4
src/utils/capgo-updater.ts 9.0 9.6 +0.6

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 5893 ran, 0 failed, 0 skipped, 1.9m

📊 Coverage (unit)

metric %
statements 74.7%
branches 60.0%
functions 68.5%
lines 75.6%
⏱ 10 slowest test cases
time test
🐢 9.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Network failure keeps loading while retries remain, then shows the generic error
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_RECENT_REFUND fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › User KYC not approved fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_SOURCE_OVER_MONTHLY_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › routes the KYC rejection on its wire code, and does not retry it
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_VOLUME_NEAR_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_USER_NOT_PROVISIONED fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › a refused idempotency key tells the user to scan again, not to contact support
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Going offline blames the connection, and reconnecting clears it for the recovered scan
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Scan that recovers on the retry lands on the payment screen, not an error
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🖼 Visual diff — 7 screens moved

9 of 66 shots changed · 57 identical · baseline b0e6d5d → head 9902287

worst % screen widths
3.67% avatar-picker 320, 430
0.07% badges 320
0.07% home 320
0.03% empty-accounts 320, 430
0.03% limits 430
0.03% settings-language 430
0.03% unverified 430

job summary · before/after/diff images — artifact

Fixture screenshots, no backend. Advisory — this check never blocks a merge. Posted from the default branch by ds-shots-comment.yml; the report it renders is untrusted data.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Chip review — changes requested

The background-apply mitigation is well tested on current native builds, but its replacement rollback path can strand older Android binaries on a broken OTA bundle.

Findings

  • BLOCKING · src/utils/native-app-ready.ts:59 · Preserve rollback on pre-8.46 Android binaries
    On an Android binary with capacitor-updater < 8.46.0, a broken OTA can still execute this inline HTML but fail before React mounts. Line 64 marks that bundle successful, disabling Capgo's native rollback; after three such launches, this line calls reset(), whose old Android implementation runs _reload() on the shared plugin handler and blocks that handler waiting for notifyAppReady. The reloaded page's notify call cannot run, so reset times out and restores the broken bundle. These binaries are explicitly supported by canRestartInPlace(), so version-gate the early-ready path to preserve native rollback there (or provide a recovery that does not use the blocked handler), and cover the old-binary failed-boot case.

Checked clean

  • Confirmed the detached worktree head, supplied base SHA, and merge base exactly match the requested review target.
  • Reviewed all 12 changed files across OTA readiness, staged launch apply, chunk retry, splash sequencing, Sentry filtering, and their tests.
  • Exact-head aggregate, unit, typecheck, lint, format, analysis, native-export, design-system lint, provenance, and Vercel-comment checks passed; screenshot and preview jobs were still running when checked.
  • Verified Capacitor injects native plugin stubs before document head scripts, and compared the pinned 8.51.14 Android updater source with 8.45.9: current reset/set/reload calls are threaded, while 8.45.9 reset/set/reload block the shared handler around app-ready waiting.
  • The new Sentry carve-outs keep rollback failures while continuing to suppress transient updater noise.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: 9902287ad11e · Context: repo, provider · Took 17m

Comment thread src/utils/native-app-ready.ts
@innolope-dev
innolope-dev merged commit c03ad99 into dev Sep 4, 2026
27 of 28 checks passed
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