Skip to content

Route pairing connect kickoff rejections to the failure screen - #3351

Merged
PhilippeFerreiraDeSousa merged 4 commits into
devfrom
pairing-kickoff-failure-routing
Jul 7, 2026
Merged

Route pairing connect kickoff rejections to the failure screen#3351
PhilippeFerreiraDeSousa merged 4 commits into
devfrom
pairing-kickoff-failure-routing

Conversation

@PhilippeFerreiraDeSousa

@PhilippeFerreiraDeSousa PhilippeFerreiraDeSousa commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Stacked on #3346 (pairing-two-phase-identity) — builds on its pairing screens; merges into that branch.

Problem

A pairing connect KICKOFF rejection — e.g. Bluetooth powered off between scan and tap, or a native-bridge error — was only console.error'd while navigation proceeded to /pairing/loading. The loading screen routes onward only when the glasses reach fullyBooted or a pair_failure event arrives; a kickoff throw emits neither (the waitForReady result is discarded, only the 35s diagnostic report fires), so the user sat on the spinner until manually cancelling.

Fix

Catch the kickoff rejection at each fire-and-forget call site and replace() the top route (loading by then) with /pairing/failure — the same navigation shape as loading.tsx's handlePairFailure:

  • pairing/scan.tsxstartPairing() direct path (toolkit.pairing.pair in the 2s setTimeout)
  • pairing/scan-controller.tsx — same shape as scan.tsx (found while scoping; identical two-line fix)
  • pairing/btclassic.tsxhandleSuccess(), both branches: toolkit.glasses.connect(device, {saveAsDefault: false}) and the paired-context connectDefault() (failure deviceModel comes from the default_wearable setting there)

New errors:pairingCouldNotStart key ("Couldn't start connecting to your {{glassesModel}}…") matches failure.tsx's translate(error, {glassesModel}) rendering; other locales spread ...en so only en.ts changes. Existing console.error logs are kept. Kickoff promises resolve as soon as the native attempt starts (BT-off throws synchronously via requirePoweredOn), so there is no happy-path latency cost.

This also resolves the cubic P2 on #3346 (unawaited connect in btclassic.tsx handleSuccess leaving the user to the 35s timeout with no error).

Tests

  • New src/__tests__/app/pairing/btclassic.test.tsx: happy-path kickoff does not route to failure; device-branch rejection and paired-context connectDefault rejection both route to /pairing/failure with the right deviceModel.
  • scan.test.tsx: new case — pair kickoff rejection after the 2s delay replaces loading with the failure route.
  • jest.setup.js: added the missing otherBtConnected/onOtherBtConnected members to the island pairing mock (btclassic.tsx subscribes to them; no btclassic suite existed before).
  • loading.test.tsx unchanged (loading.tsx untouched).

Evidence: cd mobile && bun run compile clean; bun run test 412/412 passing, 53 suites (the "worker process has failed to exit gracefully" teardown warning reproduces on the base branch too). Prettier-clean on touched files.


Note

Medium Risk
Touches core pairing navigation and delayed async callbacks; guards limit wrong-screen redirects, but regressions could still affect scan, BT Classic, and loading flows.

Overview
When toolkit.pairing.pair or toolkit.glasses.connect / connectDefault reject at kickoff (e.g. Bluetooth off), the app no longer only logs the error while /pairing/loading spins indefinitely. A shared routePairingKickoffFailure helper **replace**s to /pairing/failure with errors:pairingCouldNotStart, calls abandonAttempt like loading.tsx, and no-ops if the user already left loading.

Wired from scan.tsx, scan-controller.tsx, and btclassic.tsx (including connectDefault failures using the pairing identity model). English copy adds the new error string; Jest gains otherBtConnected pairing mocks, btclassic tests, and scan cases for failure routing and suppressed stale rejections.

Reviewed by Cursor Bugbot for commit 752278e. Bugbot is set up for automated code reviews on this repo. Configure here.

A pair()/connect() kickoff rejection (Bluetooth powered off between scan
and tap, a native-bridge error) was only console.error'd while navigation
proceeded to /pairing/loading, which routes onward only on fullyBooted or
a pair_failure event — a kickoff throw emits neither, so the user sat on
the spinner until manually cancelling.

Catch the rejection at the fire-and-forget kickoff sites (scan,
scan-controller, btclassic including the paired-context connectDefault
branch) and replace the loading screen with /pairing/failure using the
new errors:pairingCouldNotStart message. Kickoff promises resolve as soon
as the native attempt starts, so the happy path is unchanged.
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📋 PR Review Helper

📱 Mobile App Build

Ready to test! (commit 752278e)

📥 Download APK

🕶️ ASG Client Build

Waiting for build...


🔀 Test Locally

gh pr checkout 3351

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2a1c077. Configure here.

Comment thread mobile/src/app/pairing/scan.tsx Outdated
Comment thread mobile/src/app/pairing/scan.tsx Outdated

@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: 2a1c077f74

ℹ️ 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 mobile/src/app/pairing/scan.tsx Outdated
// A kickoff rejection (e.g. Bluetooth powered off) emits no
// pair_failure event, so the loading screen pushed below would spin
// forever — replace it with the failure screen.
replace("/pairing/failure", {error: "errors:pairingCouldNotStart", deviceModel: device.model})

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 Guard delayed pairing failure after cancellation

When the user backs out of /pairing/loading during the fixed 2s delay, or before the native promise rejects, this timer/catch still runs because it is not cancelled on navigation. With Bluetooth off, the new unconditional replace can replace whatever screen the user returned to with /pairing/failure, so a manual cancel can be undone by a stale async callback; gate this on still being on the matching loading route or clear/ignore the delayed callback on cancellation. The same delayed-catch pattern was added in scan-controller.tsx.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4685fc9 — took the gate approach: the shared routePairingKickoffFailure() helper replaces only while /pairing/loading is still the top route, so a manual cancel can no longer be undone by the stale callback. Same guard applied in scan-controller.tsx and btclassic.tsx.

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 7 files

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mobile/src/app/pairing/scan.tsx">

<violation number="1" location="mobile/src/app/pairing/scan.tsx:143">
P2: The setTimeout in startPairing is never cleared on unmount, so the replace("/pairing/failure") inside the catch can fire after the user has already left the screen or started a different pairing flow, causing an unexpected route change with stale device params. Consider storing the timeout ID in a ref and clearing it in a useEffect cleanup or at the start of a new pairing attempt.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread mobile/src/app/pairing/scan.tsx Outdated
// A kickoff rejection (e.g. Bluetooth powered off) emits no
// pair_failure event, so the loading screen pushed below would spin
// forever — replace it with the failure screen.
replace("/pairing/failure", {error: "errors:pairingCouldNotStart", deviceModel: device.model})

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.

P2: The setTimeout in startPairing is never cleared on unmount, so the replace("/pairing/failure") inside the catch can fire after the user has already left the screen or started a different pairing flow, causing an unexpected route change with stale device params. Consider storing the timeout ID in a ref and clearing it in a useEffect cleanup or at the start of a new pairing attempt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mobile/src/app/pairing/scan.tsx, line 143:

<comment>The setTimeout in startPairing is never cleared on unmount, so the replace("/pairing/failure") inside the catch can fire after the user has already left the screen or started a different pairing flow, causing an unexpected route change with stale device params. Consider storing the timeout ID in a ref and clearing it in a useEffect cleanup or at the start of a new pairing attempt.</comment>

<file context>
@@ -137,6 +137,10 @@ export default function SelectGlassesBluetoothScreen() {
+          // A kickoff rejection (e.g. Bluetooth powered off) emits no
+          // pair_failure event, so the loading screen pushed below would spin
+          // forever — replace it with the failure screen.
+          replace("/pairing/failure", {error: "errors:pairingCouldNotStart", deviceModel: device.model})
         })
       }, 2000)
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4685fc9 — rather than tracking timeout IDs across screens, the catch now routes through routePairingKickoffFailure(), which no-ops unless /pairing/loading is still the top route; a late rejection after leaving the flow only logs.

Comment thread mobile/src/app/pairing/scan.tsx Outdated
Comment thread mobile/src/app/pairing/btclassic.tsx Outdated
@PhilippeFerreiraDeSousa
PhilippeFerreiraDeSousa changed the base branch from pairing-two-phase-identity to integration/toolkit-boundary July 6, 2026 23:40
@PhilippeFerreiraDeSousa
PhilippeFerreiraDeSousa changed the base branch from integration/toolkit-boundary to dev July 7, 2026 01:18
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploying mentra-live-ota-site with  Cloudflare Pages  Cloudflare Pages

Latest commit: c65fd1b
Status: ✅  Deploy successful!
Preview URL: https://aa5aad19.mentra-live-ota-site.pages.dev
Branch Preview URL: https://pairing-kickoff-failure-rout.mentra-live-ota-site.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mentra-store-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: c65fd1b
Status: ✅  Deploy successful!
Preview URL: https://1784c298.augmentos-appstore-2.pages.dev
Branch Preview URL: https://pairing-kickoff-failure-rout.augmentos-appstore-2.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying dev-augmentos-console with  Cloudflare Pages  Cloudflare Pages

Latest commit: c65fd1b
Status: ✅  Deploy successful!
Preview URL: https://a8e789cd.dev-augmentos-console.pages.dev
Branch Preview URL: https://pairing-kickoff-failure-rout.dev-augmentos-console.pages.dev

View logs

…ttempt

Address the bot-review findings on the kickoff-failure routing:

- The delayed pair()/connect() rejection could land after the user backed
  out of /pairing/loading, replacing whatever screen they returned to with
  the failure screen. Route through a shared routePairingKickoffFailure()
  helper that fires only while /pairing/loading is still the top route.
- Match loading.tsx's handlePairFailure by clearing the failed attempt
  (abandonAttempt) before surfacing the failure screen.
- Read default_wearable at rejection time in btclassic's connectDefault
  branch instead of capturing a render-time value.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying prod-augmentos-account with  Cloudflare Pages  Cloudflare Pages

Latest commit: c65fd1b
Status: ✅  Deploy successful!
Preview URL: https://b0d173bc.augmentos-e84.pages.dev
Branch Preview URL: https://pairing-kickoff-failure-rout.augmentos-e84.pages.dev

View logs

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

PR Agent Orchestrator State

{
  "cycle": 2,
  "fixRound": 0,
  "totalReviewerRuns": 2,
  "consecutiveNoNewReviews": 2,
  "openFindings": [],
  "resolvedFindings": [],
  "nitFindings": [],
  "phase": "discovery",
  "status": "in_progress",
  "lastPair": [],
  "stagnationFixRounds": 0,
  "lastOpenCount": 0,
  "fingerprintReopenCounts": {},
  "mutedFingerprints": []
}

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Agent Review — cycle 2

✅ No blocking findings · 0 blocking · 0 nits
Reviewers this cycle: none

No model reviews ran this cycle.

Updated automatically by the PR Agent Orchestrator each review cycle. Nits do not block merge.

PR #3352 reified the identity lifecycle behind toolkit.pairing.identity();
host readers go through the projection, not raw settings keys. The
btclassic connectDefault kickoff-failure copy was reading default_wearable
raw, which also skips the projection's legacy-junk handling. Read the
identity snapshot at rejection time instead and name its model (paired or
pending) in the failure copy.
@PhilippeFerreiraDeSousa
PhilippeFerreiraDeSousa merged commit 2a1c4b4 into dev Jul 7, 2026
25 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