Repository navigation
feat: subscription clarity for everyone (pre-ladder) - #21474
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
💡 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".
- 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
|
Addressed the outstanding review threads in e709729:
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), 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. |
There was a problem hiding this comment.
💡 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".
| if (!isPaid && !showUpgradeOptions) return; | ||
| plansImpressionRecorded.current = true; | ||
| trackPaywallOpened('usage_page', subscription.plan); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 👍 / 👎.
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>
There was a problem hiding this comment.
💡 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".
| appleReadiness = | ||
| await (appleReadinessReader ?? OnDeviceAppleProvider.readiness)(language.isEmpty ? 'multi' : language); |
There was a problem hiding this comment.
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 👍 / 👎.
| } else { | ||
| await onRecordProfileSettingChanged(); |
There was a problem hiding this comment.
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 👍 / 👎.
| LocalSttCustody.selectedLocalRoute = | ||
| decision.allowanceOnDevice || decision.customSttConfig?.provider == SttProvider.onDeviceWhisper; |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| if (persistedCustomStt.provider == SttProvider.onDeviceWhisper && readiness != FreemiumReadiness.ready) { | ||
| return const SttModeDecision( | ||
| path: SttResolvedPath.blocked, reason: 'on_device_not_ready', allowanceOnDevice: true); |
There was a problem hiding this comment.
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 👍 / 👎.
| LocalSttCustody.selectedLocalRoute = | ||
| decision.allowanceOnDevice || decision.customSttConfig?.provider == SttProvider.onDeviceWhisper; |
There was a problem hiding this comment.
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 👍 / 👎.
| 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([]); |
There was a problem hiding this comment.
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 👍 / 👎.
| # Exercise the pinned native resource-error paths before Dart mocks. | ||
| python3 scripts/test_whisper_cleanup.py |
There was a problem hiding this comment.
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 👍 / 👎.
| if (!await (sttProvider as RecoverableLocalSttProvider).isReadyToRetry()) return; | ||
| if (recoveryGeneration != _connectionGeneration || _status != PurePollingStatus.connected) return; | ||
| localFailure = null; | ||
| onConnected(); |
There was a problem hiding this comment.
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 👍 / 👎.
| return LocalSttRecoveryNotice( | ||
| failure: LocalSttFailure(readiness.status, canProbeRecovery: true), | ||
| onRetry: () async { | ||
| setState(() {}); | ||
| }, | ||
| onSetup: openAppSettings); |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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; |
There was a problem hiding this comment.
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 👍 / 👎.
| let readiness = await legacyReady() | ||
| guard readiness.ready else { throw LocalSpeechUnavailable(readiness: readiness) } | ||
| return try await legacy() |
There was a problem hiding this comment.
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 👍 / 👎.
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
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.
proalias.usage_pagesource. Main already supplies mobile/desktop/web funnel helpers, attributed success alongside the immutable releasedUpgrade 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.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.
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.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
saysmoke using the current helper, including an unsupported-locale availability check. Focused final Flutter suite: 41 passed; scoped analyzer: no issues; fullbash scripts/analyze_ratchet.sh: passed without baseline changes;ruby app/ios/test/speech_deadline_test.rb: passed. Synthetic host smoke: current native helper transcribed asayclip and reported an unsupported locale unavailable.flutter build ios --debug --no-codesign --flavor devandflutter 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.swiftmerges cleanly. Expected integration conflicts areon_device_apple_provider.dart,on_device_whisper_provider.dartandconfig/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
ConversationRowView/ConversationDetailHeaderdoes 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, thencd 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.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.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.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.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.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-pubno 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
213b459fe95eresolves only adjacent feature-flag additions, retaining the SpeechAnalyzer kill switch and all four upstream live-demand flags. Both engines commits and fix commit83e123fc4042remain 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
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.
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
sayhost 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
e7097297d0e0without 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 preflightpassed 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
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