Skip to content

feat: subscription clarity for everyone (pre-ladder) - #21474

Merged
Git-on-my-level merged 18 commits into
mainfrom
lane/sub-ship-now
Oct 14, 2026
Merged

Git-on-my-level merged 18 commits into
mainfrom
lane/sub-ship-now

Conversation

@Git-on-my-level

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

Copy link
Copy Markdown
Collaborator

Subscription clarity before the price change

A legacy Plus subscriber now sees Plus with the server-resolved monthly listening allowance on web, rather than being treated as unlimited. Saved conversations and memories on mobile open usage without forcing the wrong-plan upgrade prompt; desktop shows saved-and-waiting copy instead of a padlock. Billing and entitlement rules are preserved.

  • W1: add unknown-plan/additive-field response coverage on Flutter, Swift and web. Main already contains unknown-safe decoders and canonical names; this PR retains the installed-client wire-remap floor and legacy pro alias.
  • W4: share the web allowance meter between Account billing and the plans dialog, display canonical plan names, use hours, clamp remaining usage to zero, and show unavailable instead of inventing a cap. Replace generic unlimited feature promises with neutral capability descriptions. Mobile saved-content previews share a content-sized primitive with Pending and See plans, an hourglass, hidden obscured-content semantics, and a usage destination; desktop shows Saved and waiting. Content access stays governed by the existing server.
  • W2 (§8): the web Account billing path now emits the registered funnel events with tier, interval, and actual usage_page source. Main already supplies mobile/desktop/web funnel helpers, attributed success alongside the immutable released Upgrade Succeeded, the correct locked-memory source, registry entries, and app-update event exclusion. Removing the locked-content forced paywall also removes its false paywall impression.
  • Post-merge nits: refreshed all items against origin/main. All listed production fixes and other test gaps are already landed. Add the remaining focused unresolved-checkout regression for Basic/paid × ledger off/on, proving cache effects and valid-paid unlocks happen before retry admission without a stored-plan write.

The shared inventory is omi-workspace/data/run-2026-10-12-subscription-overhaul/split-inventory.md, initially written before coding against origin/main f0d1b54.

Cutover boundary and legacy risk

The draft cutover lane owns new Stripe prices/price bindings, remap-floor retirement and alias migration, blended budget admission, unified ladder storefront rollout, Pro gates, daily MCP caps, and new budget-choice wiring. The overflow runner, Parakeet setup, summaries and schedule from #21466 remain a later merge; only the existing freemium fallback improvements below ship here. No Stripe writes, price/catalog policy edits, flag flips, deployment, prod/GCP access or historical-data changes are included.

Legacy prices retain today's entitlements. Existing held-content and bounded reset-recovery implementations are already on main and remain default-off; this PR does not activate recovery or grant access beyond the current allowance. Waiting copy describes preserved content, not a promise of immediate processing. Web zero-cap display follows the existing endpoint convention; missing/invalid allowance is explicitly unavailable. Checkout completion in a separate Stripe tab is still backend-authoritative; no client success is inferred merely from returning focus.

On-device fallback engine

This addition is isolated in clearly named freemium-engine commits. It uses the existing fallback path and UX/status strings, with no overflow UI, runner, schedule, provenance, summaries, Parakeet download or plan changes.

  • iOS 26+: SpeechAnalyzer/SpeechTranscriber on supported hardware and locales. The existing SpeechAnalyzer-first native channel now checks hardware availability and reserves its OS-managed locale asset before installation. Unsupported hardware/locale, installation failures and recognition failures use the existing on-device SFSpeech path. Older iOS keeps SFSpeech. The minimum iOS version is unchanged.
  • Kill switch: mobile-speech-analyzer-disabled, registered as a PostHog operational kill switch. Missing/false selects the new engine; true forces SFSpeech in both availability checks and transcription without a release. Reads occur per operation through the existing identity/consent-fenced analytics adapter. Offline/error/tracking-disabled reads retain the shipped default. No remote flag is created or flipped in this PR.
  • Android: existing whisper.cpp input is decoded through independent calls of at most 30 seconds, each creating/freeing its own native context. Sample rate, PCM bytes and segment offsets are preserved; a failed chunk retains the whole polling input for retry. No model or diarization changes.

Evidence is an internal pendant sample, not truth-labelled accuracy: legacy on-device SFSpeech misses most speech (approximately 84% disagreement with cloud, worst in the hand-judged comparison). SpeechAnalyzer was 44.5% disagreement and preferred in hand judgments over the stored cloud transcript. Cloud is not ground truth. No private audio, transcript text or identifiers are included. Phone battery remains unmeasured, as do real-phone performance and model-install recovery.

Validation: focused engine-selection/availability/kill-switch, Whisper chunk custody/offset/retry and existing freemium/listen tests; scoped analyzer; iOS and Android dev debug builds; native synthetic macOS say smoke using the current helper, including an unsupported-locale availability check. Focused final Flutter suite: 41 passed; scoped analyzer: no issues; full bash scripts/analyze_ratchet.sh: passed without baseline changes; ruby app/ios/test/speech_deadline_test.rb: passed. Synthetic host smoke: current native helper transcribed a say clip and reported an unsupported locale unavailable. flutter build ios --debug --no-codesign --flavor dev and flutter build apk --debug --flavor dev: both passed. No device installation or battery measurement is claimed. OMI_PR_BODY_FILE="$PWD/app/.local/pr21474-body.md" make preflight: 63 checks passed; metadata-only preflight: 5 checks passed.

Read-only merge rehearsal against #21466: AppDelegate.swift merges cleanly. Expected integration conflicts are on_device_apple_provider.dart, on_device_whisper_provider.dart and config/feature-flags.yaml: retain the ready kill-switch wiring/bounded fallback and the cutover-only batch/provenance branches; keep both registry entries. These are integration of intentionally different flag/scope contracts, not competing native implementations.

Product invariants affected

  • INV-MEM-1: the memory row copy/navigation change preserves the three tiers, canonical collection and default access policy.
  • INV-NAV-1: the locked-row title suppression in ConversationRowView/ConversationDetailHeader does not replace or reduce a destination — the badge keeps the established waiting-state copy and navigation targets are unchanged.

Validation

  • cd backend && BACKEND_UNIT_TEST_FILE_LIST=/tmp/sub-ready-backend-tests.txt bash test.sh: 735 passed across 8 file-isolated suites, including four new unresolved-checkout cases, entitlement byte-equivalence, rate card, reconciliation, storefront, held content, reset recovery, and plan-wire contracts.
  • flutter pub get, then cd app && bash test.sh <three focused paths>: 138 spine tests and 19 focused tests passed. Focused paths: subscription_plan_type_test, conversation_locked_preview_test, storefront_funnel_test. Preview checks cover light/dark, 200% text, semantics, no padlock, usage navigation, selection and long-press behavior.
  • Additional flutter test --no-pub test/widgets/waiting_memory_preview_test.dart test/widgets/knowledge_ledger_memory_item_test.dart test/widgets/conversation_locked_preview_test.dart: 21 passed, including the short-memory 200% text regression and canonical memory controls.
  • flutter test --no-pub test/visual_audit/visual_audit_smoke_test.dart: 141 passed; the waiting-preview visual scenario asserts Pending, See plans, hourglass and no padlock.
  • flutter analyze --no-pub: no errors; 95 existing infos/warnings. cd app && bash scripts/analyze_ratchet.sh: passed against the committed baseline after documented hermetic bootstrap.
  • macOS xcrun swift build -c debug --package-path Desktop: passed. xcrun swift test --package-path Desktop --filter 'Subscription(InfoDecoder|PlanNames|FunnelTelemetry)Tests': 14 passed. Both run from desktop/macos.
  • Web bun run check: typecheck and 130 files / 687 tests passed. bun run lint: passed with existing warnings; focused tests cover paid Plus, Free/server cap, over-cap clamping, unlimited/missing/invalid allowances, actual subscription decoding and Account billing funnel payloads.
  • Isolated SwiftUI render of the actual waiting badge: passed in light/dark mode and inspected below. Mobile widget layout/semantics verified at 200% text; its test runner's block fonts are not presented as visual product acceptance. No production app or existing agent-flutter session was touched.
  • OMI_PR_BODY_FILE=/tmp/sub-ready-pr-body.md make preflight: 53 selected checks passed. scripts/pr-preflight --pr-body-file /tmp/sub-ready-pr-body.md --metadata-only: 5 passed. Mandatory pre-push gates remain enabled.

Saved and waiting, light
Saved and waiting, dark

P1 held-content tap boundary

The enclosing memory row and See plans button now share one guarded handler. Full-area preview taps use its guarded action, including grouped conversation gaps; child long-press and swipe recognizers remain available. With subscription UI enabled, frosted-area and row-padding taps navigate to usage without opening the memory sheet. App Review accounts retain read-only memory access and no conversation navigation. No entitlement or engine changes are made by this fix; the two engine commits remain intact.

Regression coverage exercises off-button memory, single-conversation and grouped-conversation taps, plus both subscription visibility states. The new failure class records these reusable behavioral guards.

P1 validation: flutter pub get; focused memory/conversation/knowledge-ledger widget suite 33 passed; visual-audit smoke 141 passed; flutter analyze --no-pub no errors, 95 existing infos/warnings; analyzer ratchet passed without baseline changes. On the original production code, the four new off-button memory cases fail; on this fix they pass.

Failure-Class: new

Main synchronization: merge commit 213b459fe95e resolves only adjacent feature-flag additions, retaining the SpeechAnalyzer kill switch and all four upstream live-demand flags. Both engines commits and fix commit 83e123fc4042 remain unchanged in ancestry. Registry validation, the 33 focused widget tests, and analyzer ratchet pass on the merged tree. Initial local preflight and pre-push each passed all 63 checks.

Round-2 engine recovery fixes

  • Polling advances by consumed audio duration, including empty trailing chunks. A 65-second speech-then-silence input followed by new speech now keeps its real timeline.
  • Both native plugin entrypoints use unconditional RAII context/WAV cleanup, including validation errors and exceptions. Supported PCM is downmixed/resampled to canonical 16 kHz before independent <=30-second decodes. Invalid input/model errors pause retries; transient inference failures retain audio for retry. The pinned wrapper is patched fail-closed and idempotently from Podfile/Gradle.
  • Freemium readiness checks the actually selected engine and legacy authorization. Rollback with denied/unavailable SFSpeech pauses transcription and exposes existing localized saved-recording copy, permission/settings and retry actions. Capture and transcript ingest remain active; audio is retained in the existing WAL with phone-only custody, and both automatic/manual cloud-audio drains reject it. No new storage, overflow runner, schedule, summary, provenance or plan policy was added. During the active session retained audio retries after permission/engine recovery; after restart the WAL remains durable, without adding a new batch runner to this READY scope.
  • Locale changes release previous SpeechAnalyzer reservations; only the current locale is reserved.

Behavioral validation: 209 focused Flutter tests passed, including real polling retention/recovery, rollback readiness, composite capture continuity, physical WAL persistence and actionable widgets at 200% text. Actual pinned native wrapper ran 600 WAV rejection/inference-error/exception cases under ASan/UBSan with zero live contexts or leaked WAV handles; Swift coordinator tests execute install failure, denied/unavailable legacy fallback and recognition-error fallback. Both dev debug builds passed. Phone battery and physical-device runtime acceptance remain unmeasured.

INV-CAP-1: capture ownership and existing WAL custody are preserved across permanent local-engine failure; captured bytes are not counted as delivered or admitted to cloud STT. INV-UI-1 and INV-UI-3: recovery uses shared Omi controls and existing localized status/action strings.

Local speech permission recovery
Local speech unavailable recovery

Line-Count-Exception: app/lib/pages/settings/transcription_settings_page.dart | 1554 -> 1572 | Existing capture owner keeps recovery and durable local custody together; behavioral tests cover the transition.

Line-Count-Exception: app/lib/services/wals/local_wal_sync.dart | 2663 -> 2690 | Existing capture owner keeps recovery and durable local custody together; behavioral tests cover the transition.

Remote synchronization: retains review-fix commit e7097297d0e0; its trailing-silence regression now asserts the separate consumed clock and unchanged speech end. No PR merge or readiness change is performed.

Final local evidence: READY focused 209 tests plus 150 custody/recovery and the retained lead regression; overflow 205 focused, 60 runner/schedule/UI, 150 custody/recovery and the retained lead regression. Analyzer ratchets pass without baseline edits; iOS and Android dev debug builds pass on both branches. A fresh current-native SpeechAnalyzer synthetic say host smoke passes with unsupported-locale rejection.

Line-Count-Exception: app/lib/services/capture/capture_controller.dart | 4384 -> 4470 | Existing capture owner supplies resolved custody and recovery actions without adding another capture lifecycle; behavioral guards cover retained audio.

CI follow-up: test-only commit aligns the real visual-audit scenario with the lead’s approved View Usage action. The scenario passes locally; this corrects the single Dart-suite failure at e7097297d0e0 without changing production behavior. Both mandatory pre-push gates passed for the engine recovery publication.

Full-suite follow-up: healthy capture audit/Home fixtures now implement the nullable local recovery getter. A real Home-card regression verifies permission-denied state shows saved-audio copy and a working retry while capture stays active. The capture Home + full visual-audit suite passes on both lanes: 171 passed, 2 existing skips. Final make preflight passed 68 checks on READY and 65 checks on overflow. Overflow’s simulator compile and Android emulator acceptance passed on the engine fix; the only Dart failures were these stale fixture getters, now corrected equivalently.

Round-3 engine recovery corrections

  • Historical SD-card/flash/ring imports use their own recording metadata, never the current live STT route. Recording-time phone-only custody is persisted separately. A targeted decode/import repair clears pre-fix dev-build stamps on historical device-storage recordings without clearing genuine phone-local capture consent. Behavioral tests import both fresh and already-stamped records while local, change route, exercise refused automatic upload and successful explicit retry.
  • Polling overflow advances the source clock by discarded PCM/Opus duration without stretching speech segments. Real polling failure/overflow/recovery tests assert 10 → 11 → 13 seconds at 8 and 16 kHz.
  • Explicit custom on-device STT readiness and permission preparation use the configured language, matching the actual provider (including its null-language default). Tests cover both supported/unsupported mismatch directions and the production permission action through its native channel.
  • Shared custody field placement is aligned across READY/overflow to prevent duplicate fields during integration. Native engine code is unchanged in this round.

The overlapping different-locale reservation test nit is deferred: safely changing reservation ownership across asynchronous installs requires separate concurrency work. Existing reservation release and native fallback behavior remain unchanged. Phone battery and real-device iOS 15–25 execution remain unmeasured.

The paywall impression now requires the Plan tab and nonempty pricing choices. A component test delivers prices while Usage is selected and verifies no impression until Plan is selected.

Latest round-3 read-only integration rehearsal: shared polling/readiness/language/codec-clock code is byte-identical and native AppDelegate merges cleanly. Expected Git conflicts: Apple provider, Whisper provider, local WAL sync, WAL model, and feature-flag registry. Preserve overflow batch engines/provenance and onDeviceConversation, retaining one shared custody flag, recording-time marker and their JSON entries. The former silently duplicated WAL field problem is gone; the model now requires an explicit conflict resolution. The lead owns integration; no PR was merged.

🤖 Generated with Claude Code

@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-14T18:43:35.023911Z bd7c463 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: babe47e71d

ℹ️ 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 app/lib/pages/memories/widgets/memory_item.dart

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

ℹ️ 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 web/app/src/components/settings/SettingsPage.tsx Outdated
Comment thread app/lib/pages/conversations/widgets/conversation_list_item.dart Outdated
Comment thread web/app/src/components/settings/TranscriptionUsageMeter.tsx
Git-on-my-level and others added 2 commits October 14, 2026 17:58

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

ℹ️ 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 app/lib/services/sockets/on_device_whisper_provider.dart Outdated
Comment thread config/feature-flags.yaml Outdated
Comment thread app/lib/services/sockets/on_device_apple_provider.dart
Git-on-my-level and others added 3 commits October 14, 2026 18:57
- macOS: locked displayTitle now says "Saved and waiting" (matches the
  badge); row/header title slots stay quiet when locked so the pill is
  not duplicated beside itself
- web: Paywall Opened fires only when plan choices are actually visible
  (Basic: on See plans; paid: mount; unknown plan: never), once per mount
- web: a positive sub-hour allowance no longer rounds to "0 h left";
  values under 0.05 h render as "<0.1 h"
- mobile: locked previews label their destination honestly (View Usage)
  for conversations, locked runs and memories
- whisper: a silent/gate-rejected trailing chunk no longer rewinds the
  polling offset; the final segment covers the full consumed buffer
- guided voice: preflight's engine choice is pinned into the provider so
  transcription cannot diverge from validated availability
- flags: register mobile-speech-analyzer-disabled with the kill role so
  --apply-missing-kills can pre-arm the rollback row
@Git-on-my-level

Copy link
Copy Markdown
Collaborator Author

Addressed the outstanding review threads in e709729:

  • Locked-memory tap routing (P1): already fixed by 83e123f on the previous head (parent tap and the frosted card share the guarded openMemory action); thread resolved as addressed.
  • macOS locked title (P2): displayTitle for .locked now reads "Saved and waiting" to match the badge, and ConversationRowView/ConversationDetailHeader omit the title slot when locked so the pill is not duplicated beside itself. Plain-string callers (Spine tiles, link lists) still get honest copy.
  • Paywall impression timing (P2): Paywall Opened on the usage page now fires only when plan choices are actually visible — Basic on "See plans", paid plans on mount, unknown plan never — once per mount.
  • Locked-preview label vs destination (P2): locked conversation rows, grouped locked runs and locked memories now label the action "View Usage", matching the UsagePage destination instead of promising a plan picker.
  • Rounding positive allowance to zero (P2): TranscriptionUsageMeter renders a positive balance under 0.05 h as <0.1 h instead of 0 h left.
  • Whisper silent trailing chunk (P2): the final segment of a mixed-result buffer now covers the full consumed duration, so PurePollingSocket's offset advance no longer rewinds over unvoiced trailing audio.
  • Flag registry role (P2): mobile-speech-analyzer-disabled is registered with the kill role (registry doc regenerated), so posthog_flag_sync.py --apply-missing-kills can pre-arm the rollback row.
  • Guided-voice engine pinning (P2): preflight resolves the analyzer gate once (iOS only) and prepare() injects that exact choice into the provider, so transcription cannot switch engines away from what availability validated.

Verification: 39 Flutter tests (whisper chunking incl. new silent-tail regression, speech gate, locked previews, waiting memory preview), 9 vitest cases (impression timing + sub-hour rounding regressions), flutter analyze clean on touched Dart files, swiftc -parse clean on touched Swift files, registry checker + 35 Python fixtures green, and the full 63-check preflight gate passed against origin/main. PR body now names INV-NAV-1 alongside INV-MEM-1 for the touched invariant paths.

Note on the push: the pre-push hook crashed on this host's python3.9 before running checks (the bug #21413 fixes); the identical gate was run to green via python3.11 before pushing.

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

ℹ️ 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 +924 to +926
if (!isPaid && !showUpgradeOptions) return;
plansImpressionRecorded.current = true;
trackPaywallOpened('usage_page', subscription.plan);

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 Wait for pricing options before recording the impression

When getAvailablePlans() fails or returns no plans, a paid account reaches this effect immediately, and a Basic account reaches it after clicking “See plans,” even though the corresponding panel renders only the indefinite loader at lines 1347-1350. The ref is then permanently set, so a later successful plan load cannot record the impression when choices actually become visible. The updated effect still does not consult cachedPlans; require at least one rendered pricing option before emitting Paywall Opened to avoid counting non-visible paywalls.

Useful? React with 👍 / 👎.

expect(find.text('Upgrade to Unlimited'), findsOneWidget);
await a.shot('Conversation list with frosted locked previews');
expect(find.text('Pending'), findsOneWidget);
expect(find.text('See plans'), findsOneWidget);

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 Update the visual audit to expect the usage label

This scenario now deterministically fails: both the lone and grouped locked-conversation previews render context.l10n.viewUsage, whose English value is View Usage, so find.text('See plans') finds nothing and the scenario stops before capturing its screenshot. Update the expectation to the production label so the visual-audit journey remains runnable in the integration verification lane.

AGENTS.md reference: app/AGENTS.md:L111-L113

Useful? React with 👍 / 👎.

Git-on-my-level and others added 2 commits October 14, 2026 21:35
Track consumed audio, normalize Whisper PCM and clean native resources unconditionally. Probe the selected Apple engine, pause permanent failures with actionable recovery, and retain capture in the existing phone-only WAL.

Co-Authored-By: Codex <noreply@openai.com>
Resolve only Whisper trailing silence with the separate consumed-audio clock; preserve the lead regression without stretching speech segments.

Co-Authored-By: Codex <noreply@openai.com>
Keep the conversations waiting-preview scenario aligned with the lead’s View Usage copy. The real visual-audit scenario passes; no production behavior changes.

Co-Authored-By: Codex <noreply@openai.com>

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

ℹ️ 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 +74 to +75
appleReadiness =
await (appleReadinessReader ?? OnDeviceAppleProvider.readiness)(language.isEmpty ? 'multi' : language);

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 Pin the speech engine selected during capture readiness

When the kill-switch result changes between this readiness check and the first polling transcription—for example, identity settles or a rollout changes—the resolver can admit capture after validating SpeechAnalyzer, while the regular capture factory still constructs an unpinned OnDeviceAppleProvider at transcription_service.dart:622, causing transcribe() to read the flag again. If it now selects SFSpeech on a device with Dictation unavailable, recording enters the saved-audio failure state despite passing preflight. Although guided voice now pins its choice, the fresh evidence is that the main capture factory remains unpinned; propagate the selected engine from readiness into that provider too.

Useful? React with 👍 / 👎.

Comment on lines +260 to +261
} else {
await onRecordProfileSettingChanged();

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 Reopen the socket when retrying an active phone capture

When an active phone-mic recording was admitted with decision.blockSocket, _socket is null, so the recovery button takes this branch and dispatches RecordProfileChanged. That stage calls _resetState(), which returns immediately while staged.phoneOwnsCapture is true (capture_controller.dart:2145-2148), before rerunning the resolver or opening a transcription socket. Consequently, after the user grants speech permission or completes setup, “Try Again” cannot clear _localSetupFailure or resume transcription for the current phone recording; use the phone transcription-reconnection path rather than the record-profile reset.

Useful? React with 👍 / 👎.

Comment on lines +1582 to +1583
LocalSttCustody.selectedLocalRoute =
decision.allowanceOnDevice || decision.customSttConfig?.provider == SttProvider.onDeviceWhisper;

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 Clear local-only custody before starting batch capture

If a user enables Transcribe Later after an on-device session set this global flag to true, _openTranscriptionSocket() returns from the batch-mode branch at lines 1560-1562 before this assignment can update the route. Batch frames are then stamped onDeviceOnly by LocalWalSync.onFrameCaptured, and that persisted flag excludes them from automatic upload and makes manual syncWal return immediately. The resulting Transcribe Later recording can never perform its promised later upload, so reset or session-scope custody before the batch/no-socket early return.

Useful? React with 👍 / 👎.

Migrate healthy visual-audit and Home capture fakes to the new nullable recovery state. Exercise the actual Home card during permanent local failure and verify saved-audio copy, continued phone capture and a working retry.

Co-Authored-By: Codex <noreply@openai.com>

@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: 830b41c0a4

ℹ️ 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 +123 to +125
if (persistedCustomStt.provider == SttProvider.onDeviceWhisper && readiness != FreemiumReadiness.ready) {
return const SttModeDecision(
path: SttResolvedPath.blocked, reason: 'on_device_not_ready', allowanceOnDevice: true);

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 Preserve raw forwarding when local readiness fails

When an iOS user explicitly selects on-device speech with sendRawAudioToOmi: true and SpeechAnalyzer/SFSpeech readiness temporarily fails, this returns a fully blocked decision. _openTranscriptionSocket then abandons the socket before transcription_service.dart:691-695 can create the configured raw-audio secondary, so the user's requested upload path disappears precisely when local transcription is unavailable. Block only the local transcription leg, or preserve the secondary when raw forwarding is enabled.

AGENTS.md reference: app/AGENTS.md:L106-L110

Useful? React with 👍 / 👎.

Comment on lines +1582 to +1583
LocalSttCustody.selectedLocalRoute =
decision.allowanceOnDevice || decision.customSttConfig?.provider == SttProvider.onDeviceWhisper;

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 Keep raw-forwarding sessions eligible for WAL repair

For a healthy explicit onDeviceWhisper configuration with sendRawAudioToOmi: true, this expression still marks the route as phone-only. LocalWalSyncImpl consequently stamps its WALs onDeviceOnly and refuses automatic or manual upload, so frames missed by the live secondary during a network interruption can never use the durable repair path even though the user enabled raw-audio forwarding. Base custody on sendRawAudioToOmi rather than the provider name alone.

AGENTS.md reference: app/AGENTS.md:L106-L110

Useful? React with 👍 / 👎.

Comment on lines 315 to +320
if (message is Uint8List) {
_audioFrames.add(message);
if (localFailure != null) _requeueFrames([]);
} else if (message is List<int>) {
_audioFrames.add(Uint8List.fromList(message));
if (localFailure != null) _requeueFrames([]);

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 Retain all audio needed by local recovery

When a phone-mic local-speech failure lasts longer than the 19.2 MB polling cap (about ten minutes of 16 kHz PCM), every incoming frame calls _requeueFrames([]), which drops the oldest buffered audio. Although those bytes remain in WAL, this change also marks local-route WALs onDeviceOnly, and there is no WAL-to-local-STT replay consumer, so fixing permission or the engine during the same recording transcribes only the newest ten minutes and silently leaves the earlier conversation without a transcript. Preserve a replayable cursor into the WAL or otherwise avoid discarding audio that recovery still needs.

AGENTS.md reference: app/AGENTS.md:L106-L110

Useful? React with 👍 / 👎.

Comment thread app/test.sh
Comment on lines +67 to +68
# Exercise the pinned native resource-error paths before Dart mocks.
python3 scripts/test_whisper_cleanup.py

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 Bootstrap the package config before the native test

If .dart_tool/package_config.json is missing while the four generated files checked above still exist—for example after flutter clean—the setup branch skips flutter pub get, then this new command immediately fails because test_whisper_cleanup.py unconditionally reads that package config. bash test.sh therefore no longer bootstraps this otherwise supported local state; include the package config in the missing-input check or run the native test after dependency bootstrap.

AGENTS.md reference: app/AGENTS.md:L93-L104

Useful? React with 👍 / 👎.

Comment on lines +153 to +156
if (!await (sttProvider as RecoverableLocalSttProvider).isReadyToRetry()) return;
if (recoveryGeneration != _connectionGeneration || _status != PurePollingStatus.connected) return;
localFailure = null;
onConnected();

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 Notify capture listeners after automatic local recovery

When the 30-second recovery probe succeeds automatically, clearing localFailure and calling onConnected() only reaches _PrimarySocketListener.onConnected, which merely logs and does not notify CaptureController. If the retained buffer is silent or quality-filtered, no transcript message follows to trigger another rebuild, so the saved-audio warning and buffering state can remain visible even though local transcription has recovered. Emit a recovery state change through the composite/controller listener rather than relying on a future transcript.

AGENTS.md reference: app/AGENTS.md:L106-L110

Useful? React with 👍 / 👎.

Comment on lines +1220 to +1225
return LocalSttRecoveryNotice(
failure: LocalSttFailure(readiness.status, canProbeRecovery: true),
onRetry: () async {
setState(() {});
},
onSetup: openAppSettings);

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 Provide a usable setup path for unavailable SFSpeech

On older iOS, or when the analyzer kill switch selects SFSpeech, disabling Dictation makes the native probe return unavailable as documented in AppDelegate.swift:1244-1249. This recovery control then routes “Setup On Device” to openAppSettings, but the app-specific Settings page cannot enable the system Dictation setting, leaving the user in the same unavailable state with no actionable guidance. Route this status to Dictation instructions or another setup flow that can actually resolve the failed probe.

Useful? React with 👍 / 👎.

Use recording-time custody for live audio, repair historical imports, and probe the configured local speech language. Require visible web pricing for impressions.

Co-Authored-By: Codex <noreply@openai.com>

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

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

/// is Paused, capture recovering on its own (a dropped socket, a mic stall) is Reconnecting, a
/// terminal transcription failure or offline buffering is named as such, otherwise Listening.
CaptureDisplayState _displayState(CaptureProvider provider, {required bool capturingPhotos}) {
if (provider.localSttFailure != null) return CaptureDisplayState.recording;

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 Preserve pause priority during local STT failures

When a local speech failure remains active and the user pauses capture or an OS call takes the microphone, this early return bypasses captureInterruption and liveCaptureDisplayState, so ConversationStateAppBar continues to show the red-dot “Recording” state instead of “Paused.” Resolve pause and microphone-interruption states before mapping an otherwise active local-only capture to recording.

Useful? React with 👍 / 👎.

Comment on lines +95 to +97
let readiness = await legacyReady()
guard readiness.ready else { throw LocalSpeechUnavailable(readiness: readiness) }
return try await legacy()

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 Avoid probing SFSpeech before every audio chunk

On iOS 15–25, or whenever the rollback flag selects SFSpeech, every OnDeviceAppleProvider.transcribe call reaches this branch for each polling buffer. legacyReady runs a separate silent recognition task that can wait up to five seconds before the real recognition begins, so sustained capture pays for two recognition jobs per chunk and can accumulate audio faster than it is transcribed. Reuse the readiness established before capture, or cache it and let an actual recognition failure invalidate it.

Useful? React with 👍 / 👎.

@Git-on-my-level
Git-on-my-level merged commit 8a98b8e into main Oct 14, 2026
61 of 63 checks passed
@Git-on-my-level
Git-on-my-level deleted the lane/sub-ship-now branch October 14, 2026 19:34
apoorvdarshan added a commit to apoorvdarshan/omi that referenced this pull request Oct 17, 2026
Brings the branch up to upstream main (b0280d9). 126 conflicted paths:
49 .arb files merged structurally and app_localizations regenerated; 24
Flutter pages rebuilt on upstream's current code with the branch's native
presentation wiring re-applied on top.

Upstream behaviour carried into the native paths where it fits:
- Home/Library: the processing page and per-conversation processing cards
  are gone (BasedHardware#21850); the native library projects upstream's one pending row
  (library_pending → On the Way). The native Home pending row follows in
  the next commit.
- Integrations / task integrations: a failed status read offers "Try again"
  and refreshes, in native rows too.
- Memories: locked rows read "Pending" with View Usage / Saved and open the
  usage page, not the plans sheet (BasedHardware#21474); knowledge graph fit-to-bounds
  with the controller-backed page.
- Onboarding: completion is upstream's first-capture chooser, projected as
  native rows; Bluetooth permission row.
- Chat: evidence cards removed as on mobile main (the plan no longer emits
  an evidence part); edit receipts count as renderable and keep the Flutter
  card on native; the unused native quota plans sheet is dropped in favour
  of upstream's titled sheet.
- Plans sheet, freemium prompt, usage page keep both sides (native sheet
  chrome + upstream's paywall routing, titles and analytics sources).

Tests retargeted to the new behaviour (pending row instead of processing
page, locked-row copy, Sync details → How syncing works, SyncProvider and
an inert PendingWorkStore under Home/library tests).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6nc4Dq4LXiSGCnJBvD7ix
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