Skip to content

[SUPERSEDED by revert #21484 / RE-LAND BASE] fix(android): close retention-probe audio-loss windows and make the escalated backoff reachable - #21475

Closed
Git-on-my-level wants to merge 5 commits into
mainfrom
lane/p1-21404-audit
Closed

Git-on-my-level wants to merge 5 commits into
mainfrom
lane/p1-21404-audit

Conversation

@Git-on-my-level

@Git-on-my-level Git-on-my-level commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes the review debt of #21404: three of the four P1 threads left unanswered at merge were verified REAL against current origin/main by an independent line-level trace, and a second independent model audit (gpt-6-astra, unseeded) concurred on all four verdicts. The fourth P1 was REFUTED as filed, and this PR still hardens the exact boundary it pointed at. No audio-loss path ships in the 1.0.55x builds, and the escalated reconnect schedule this PR series advertised becomes actually reachable.

  1. Subscribe before probing (P1, real). fireDeviceReady ran the retention probe before the audio subscription. readCharacteristic enqueues synchronously into the serial GATT command queue while subscribeCharacteristic defers through the main looper, so the probe read could own the queue first; the firmware drops dequeued frames while connected-but-unsubscribed, and a lost read callback (the queue has no timeout for reads) blocked the CCCD write indefinitely. The subscription now runs first and the probe read is posted behind it on the same looper, so it can never precede the CCCD write.

  2. Require a valid RTC before certifying retention (P1, real). The firmware offline writer drops every request while its clock is unsynced (sd_card.c process_write_data_req), but retentionProbeSucceeds discarded the status payload's rtc-valid field, so a mounted-but-timeless device (rebooted while Flutter absent, no persisted epoch) was certified as retaining audio it was actually throwing away. RTC validity is now required; RTC alone still does not license escalation.

  3. Preserve a completed proof across disconnected retries; invalidate on each new GATT connection (P1, real). RetentionProbeLifecycle.interrupt() erased completed verification at every disconnect and retry attempt, always BEFORE handleRetryLogic read it, so delayForFailure always saw retentionVerified=false and the advertised 6/12/30/60s schedule was unreachable (flat 3s forever). The real hazard ran the other way: the schedule was dead, and the naive fix (keep proof forever) would strand offline audio behind long gaps on devices whose storage later broke. Now interrupt() is cancel() (retires an in-flight probe, keeps completed proof), and the proof resets in onGattConnected before anything on the new link can consume stale verification.

  4. Stale proof on a new GATT (P1, refuted as filed, hardened anyway). The disconnect path already retired the in-flight probe before retry selection, so the filed window (consume stale proof on a pre-ready disconnect) could not occur as described. The connect-time invalidation in (3) re-establishes the same guarantee structurally: any proof describes exactly the GATT link it was proven on.

Product invariants

  • INV-CAP-1 (pendant-audio-retention): every change narrows when the phone may stretch reconnect gaps (RTC gate, per-link proof) and removes two ways the audio subscription could be delayed behind optional probe work. Regression tests pin subscribe-before-probe, the deferred probe enqueue, cancel-vs-reset semantics, and connect-time invalidation.

Failure class

Failure-Class: none

Test plan

  • BleReconnectBackoffTest: RTC-less payloads with nonzero storage stay flat; escalation requires rtc=1; overflow cases keep rtc=1
  • RetentionProbeLifecycleTest: completed proof survives cancel(); fresh connection resets it; late/replaced-GATT callbacks still rejected
  • RetentionCaptureOrderingContractTest (new): reads the real service source and pins fireDeviceReady ordering, the deferred probe enqueue, cancel-vs-reset, and connect-time invalidation
  • Android JVM unit suite (CI android-unit-tests)
  • Full CI green on this exact head, then independent review before merge

View guided diff Turn on auto-fix

…scalated backoff reachable

Review of #21404 left three verified P1s against the merged design (independent
astra audit concurred; the fourth, stale-proof-consumption on new GATT, was
refuted as stated but is hardened here anyway):

- Subscribe before probing: fireDeviceReady now requests the audio CCCD write
  before the optional retention probe, and the probe read is posted through the
  same looper as subscribeCharacteristic's deferred enqueue, so the probe can
  never own the serial GATT queue ahead of capture (firmware drops dequeued
  frames while connected-but-unsubscribed, and a lost read callback has no
  queue timeout).
- Require a valid RTC before certifying retention: retentionProbeSucceeds now
  demands the status payload's rtc-valid field; the firmware offline writer
  drops every request while its clock is unsynced, so mounted-but-timeless
  storage cannot license longer reconnect gaps.
- Preserve a completed proof across disconnected retries and invalidate it on
  each new GATT connection: RetentionProbeLifecycle.interrupt() becomes
  cancel() (retires an in-flight probe without erasing completed proof), so
  delayForFailure can actually reach the advertised 6/12/30/60s schedule; the
  proof resets in onGattConnected before anything on the new link consumes it,
  closing the rebooted-into-SD-failure window (the refuted finding 3).

Tests: RTC pins in BleReconnectBackoffTest, proof-lifecycle pins in
RetentionProbeLifecycleTest, and a new source-reading contract test pinning
subscribe-before-probe, the deferred probe enqueue, cancel-vs-reset semantics,
and connect-time invalidation.

Invariant: INV-CAP-1 (product/invariants/pendant-audio-retention.md).
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T11:40:41.658319Z d0da81a 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.

@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: bdfc905216

ℹ️ 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".

// posts its enqueue behind this subscription's own post, so the CCCD
// write owns the serial queue first and a slow or lost probe read can
// never delay it.
ensureBackgroundAudioSubscription(addr, services)

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 Handle connections without persisted audio configuration

On a first connection, or whenever nativeBleStreamConfig is absent or does not match this address, this call returns without posting a CCCD write, but probeRetentionRuntime is still posted before Flutter's onDeviceReady notification. Flutter cannot create its audio subscription until that notification, so the storage read still reaches the serial GATT queue first; because ordinary reads have no queue timeout, a missing callback can wedge the queue and leave the connected pendant unsubscribed indefinitely. Defer the probe until either the native subscription was actually queued or Flutter has had an opportunity to queue its audio subscription.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Comment on lines +64 to +65
val prefix = body.substringBefore(match.range.first)
prefix.substringAfterLast("mainHandler.post", "").isNotEmpty()

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 Make the ordering test inspect executable code

This assertion treats any preceding text containing mainHandler.post as proof that the read is inside a posted block, but the production method's explanatory comment already contains that exact text. Moving readCharacteristic back to a direct call while retaining the comment therefore leaves both this prefix check and the earlier body.contains("mainHandler.post") check green, so the new contract test does not catch the queue-order regression it was added to prevent. Strip comments or inspect the Kotlin syntax/block structure instead.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

…e after clock sync

Addresses the fresh review round on this PR (P1 x2, P2 x1):

- fireDeviceReady now tracks whether the native audio subscription was queued.
  With no native target and Flutter alive, the retention probe is posted behind
  the onDeviceReady notification, so Dart's own subscription (queued from that
  callback) reaches the serial GATT queue before the probe read. With neither
  native target nor Flutter, no subscription can ever arrive, so the probe
  cannot delay one.
- The probe failure shape 'mounted storage with RTC still unsynced' schedules
  ONE bounded re-probe 20s after the first (Dart performs performSyncTime after
  onDeviceReady). Budget is per-link, reset on each new GATT connection.
- The third finding (invalidate at attempt start) is rejected: it contradicts
  the original review's own prescription and re-breaks reachability of the
  escalated schedule (the finding this PR fixes); the proof boundary stays
  'new GATT connection', as prescribed.

Tests: contract pins for the Flutter-path deferral and the re-probe budget;
backoff pins for storageStatusHasDataWithoutClock.

@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: 4daad82767

ℹ️ 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 on lines +511 to +512
val managed = managedDevices[address.uppercase()] ?: return@postDelayed
probeRetentionRuntime(address)

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 Bind delayed re-probes to their originating GATT

When link A schedules this callback and disconnects before 20 seconds, a replacement link B for the same address may be ready when it fires; the looked-up managed value is unused, so probeRetentionRuntime() simply captures B's current GATT instead of rejecting the replaced link as the comment claims. If B has already verified retention, starting this stale probe clears runtimeVerified, and a read failure, timeout, or disconnect during it loses B's valid proof and returns retries to flat cadence. Capture the originating GATT or a connection generation when scheduling and ignore the callback after replacement.

Useful? React with 👍 / 👎.

…in self-healing

Round-3 review: a stale re-probe callback could fire on a successor GATT and
clear its fresh proof (probeRetentionRuntime captures the current GATT), and a
single fixed-delay re-probe could expire before Dart's connect-time GATT work
finishes, wasting the per-link budget. Re-probes now carry the originating
GATT and attempt number: a replaced link rejects the callback outright, and
the chain re-arms only while the probe keeps seeing mounted-storage-without-
clock, up to 3 cycles per link (budget reset on each new GATT connection).
The RTC gate itself is unchanged.

@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: 5288ac400a

ℹ️ 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".

Git-on-my-level added a commit that referenced this pull request Oct 14, 2026
…ange retry loops (#21404)" (#21484)

## Summary

Reverts merge b7e38cf (#21404, "exponential BLE reconnect backoff for
out-of-range retry loops") ahead of the 1.0.559 train, per the
review-debt ruling of 2026-10-14.

Why revert rather than fix forward: #21475 (the fix-forward for #21404's
four unanswered P1s) resolved the three verifiable findings, but a
focused second-model arbitration of the remaining design disagreement
(invalidate retention proof per new GATT connection vs. per attempt)
ruled the conservative design audio-loss-critical, not
battery-efficiency-only: with a stale proof surviving failed attempts, a
device that rebooted into broken storage and then returns to range
mid-gap is reached later (6-60s schedule instead of 3s), extending the
connected-but-unsubscribed window over audio the broken device is
discarding — exactly the class INV-CAP-1's own history rejected ("a
trade that silently deletes user recordings is not acceptable at any
battery saving"). The conservative design eliminates the escalation
feature entirely, so the honest disposition is revert; the fixes and the
gate design (per-link proof, RTC-validity gate, subscribe-before-probe)
are preserved in #21475 for a re-land only after the escalation gate
gets a retention guarantee that covers the disconnected interval.

## Product invariants

- INV-CAP-1 (pendant-audio-retention): the revert removes the battery
policy whose gate could not prove retention across the disconnected
interval. Reconnect pacing returns to the pre-#21404 steady cadence.

## Failure class

Failure-Class: none

## Test plan

- [x] `git revert -m 1 b7e38cf` — clean (it reverses the original
merge wholesale, including its tests and changelog fragment)
- [x] No remaining references to BleReconnectBackoff /
RetentionProbeLifecycle in main or test sources
- [ ] Full CI green on this exact head, then merge to clear the train


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/BasedHardware/omi/pull/21484?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<a
href="https://www.cubic.dev/action/auto-fix/pr/BasedHardware/omi/21484?returnTo=https%3A%2F%2Fgithub.com%2FBasedHardware%2Fomi%2Fpull%2F21484&source=description"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img
alt="Turn on auto-fix"
src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
@Git-on-my-level

Copy link
Copy Markdown
Collaborator Author

Re-land of the #21404 battery backoff, gated on a redesigned escalation license

Blocked on a design decision (David): the 10-14 arbitration (gpt-6-astra, adversarial, audio-loss-only standard) ruled that escalating reconnect gaps on a per-connection retention proof is audio-loss-critical — a device that reboots into broken storage while out of range is reached later (6–60s vs 3s), extending the deaf window over audio it is discarding (transport.c:1224–1251). INV-CAP-1's own history ("a trade that silently deletes user recordings is not acceptable at any battery saving") therefore forces one of:

What survives from #21475 regardless (engineering record, all tested)

  • F1 fix: audio subscription strictly before the retention probe, including the queue-ordering (same-looper deferral) and the no-native-config/Flutter-path deferral.
  • F2 fix: rtc-validity required in retentionProbeSucceeds + GATT-bound self-healing re-probe chain after clock sync.
  • F3 refutation pin (disconnect path retires the probe before retry selection).
  • F4 fix: completed proof survives disconnected retries, invalidated at new-GATT-connect.

Evidence

@Git-on-my-level Git-on-my-level changed the title fix(android): close retention-probe audio-loss windows and make the escalated backoff reachable [SUPERSEDED by revert #21484 / RE-LAND BASE] fix(android): close retention-probe audio-loss windows and make the escalated backoff reachable Oct 14, 2026
@Git-on-my-level

Git-on-my-level commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Closed wont-fix per owner ruling (Option A) — 2026-10-14

Decision (David, 2026-10-14T12:23Z, ANX ask ev_857a8db7, response event a001dcf8): "A: keep the revert, close the re-land as wont-fix."

Why: the 2026-10-14 independent arbitration (gpt-6-astra via codex, exec exec-debris-lunar-actor-small-neutral-genius, recorded 10:59Z) ruled the escalated backoff finding "audio-loss-critical (P1)": while the pendant is unreachable, the loss interval is common to both schedules — but under the escalated schedule a pendant that becomes reachable during a longer wait gets reconnected later (6s vs 3s in the concrete counterexample), and audio sampled in that gap survives under the flat cadence but is discarded under escalation (disconnected audio only persists to storage when is_sd_on(); otherwise the dequeued audio has no destination). The arbitration is code-supported with line citations (transport.c 1224–1247; OmiBleForegroundService.kt 620/780/818–856) and explicitly notes escalation's 6/12/30/60s delays were unreachable in the fixed flow anyway. INV-CAP-1 "covers reconnect windows and forbids battery policy changing whether audio survives." The revert (#21484, f6bf27e6, merged green) stands; the flat 3s retry is kept for everyone; INV-CAP-1 stays as-is. Full arbitration text persisted (kanban task t_bf8e5f98).

Correction of the first version of this comment: the arbitration was performed by gpt-6-astra (codex), not grok-4.7-high; grok-4.7-high was the separate fixed-code re-audit (exec-aim-quiz-hub-library-liar-note). No structural "confidence score" exists in the arbitration record.

What this PR remains: the preserved RE-LAND BASE at head d0da81a7fb92. All four verified fixes with tests stay reviewable here for any future re-land decision:

  • F1 retention-probe subscribe-ordering (incl. queue + Flutter path) — RetentionCaptureOrderingContractTest
  • F2 RTC gate + GATT-bound bounded re-probe chain — BleReconnectBackoffTest
  • F3 refutation pin (retire-before-retry, cancelled-probe-cannot-verify)
  • F4 proof-lifecycle split across reconnects — RetentionProbeLifecycleTest

If firmware ever ships the precondition (advertised disconnected-interval retention flag + writer-health word incl. sd_write_blocked in the storage-status payload), a re-land would be a fresh decision — rebase this branch over main, fresh CI, fresh independent review, merge gate at the exact head. This PR will not be reopened for that; file a new one and use this branch as the base.

Battery work continues elsewhere (INV-CAP-1 unchanged, no backoff in 1.0.559).

@Git-on-my-level Git-on-my-level added the wontfix This will not be worked on label Oct 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hey @Git-on-my-level 👋

Thank you so much for taking the time to contribute to Omi! We truly appreciate you putting in the effort to submit this pull request.

After careful review, we've decided not to merge this particular PR. Please don't take this personally — we genuinely try to merge as many contributions as possible, but sometimes we have to make tough calls based on:

  • Project standards — Ensuring consistency across the codebase
  • User needs — Making sure changes align with what our users need
  • Code best practices — Maintaining code quality and maintainability
  • Project direction — Keeping aligned with our product principles and locked invariants

Before your next PR, please skim:

  • PRODUCT.md — product north star
  • Product invariants — locked rules (shared chat, memory tiers, agent control plane, integrations, brand)

If this was declined for direction or taste, maintainers should cite an invariant ID or open a proposed one — ask if that citation is missing.

Your contribution is still valuable to us, and we'd love to see you contribute again in the future! If you'd like feedback on how to improve this PR or want to discuss alternative approaches, please don't hesitate to reach out.

Thank you for being part of the Omi community!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

wontfix This will not be worked on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant