| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
perf(ios): pace capture healthy-state watchdog polls Co-authored-by: Cursor <cursoragent@cursor.com> | 15 天前 | |
fix(app): resume phone mic after other-app audio on iPadOS (#4706) (#11146) * fix(app): resume phone mic after other-app audio on iPadOS (#4706) Heal silent mic death when Stage Manager / YouTube stops capture without an AVAudioSession interruption: rebuild a dead engine on foreground, always probe resume when an interruption ends, and trip the Dart stall path after suspended timers so the UI no longer stays stuck on Listening. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(ci): retrigger checks after hermetic scope timeout (#4706) Detect Hermetic Backend Scope hit the 2m job timeout (cancelled); Merge Gate fail-closed on SCOPE_RESULT=cancelled. App-only PR — no backend scope change. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(app): soft-rearm phone-mic stall on foreground (#4706) Address cubic review: foreground must not race native rebuild or false- restart healthy sessions. Soft-rearm the stall clock instead of escalating immediately; add widget coverage for non-call interrupted → Paused; reformat Dart for CI. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(app): stub probeStallAfterForeground in mic test doubles (#4706) Dart Analyze CI failed: FakeMic / _ContendedMic missing the new IMicRecorderService method. Also tighten #4706 widget coverage to assert Paused text plus orange status-dot and play affordance. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> | 2 个月前 | |
fix(ios): regain healthy timer leeway after the fast foreground beat The fast foreground beat arms first=1s/repeating=4s with 50ms leeway (one leeway serves the whole DispatchSourceTimer schedule), but recorded the armed interval as 4s, so the tick handler's desired==armed check never retargeted and every later healthy beat kept the 50 ms coalescing tolerance for the rest of the session. Track the pending fast first fire explicitly, retarget to the armed cadence after it emits (deferring to the desired-cadence branch when the state changed while the beat was pending), and clear the flag on cancel. Invariant: none triggered (liveness deadlines unchanged; coalescing tolerance only). | 14 天前 | |
SCA-486: isolated mobile sessions, local-dev auth, capture replay, and seeded journeys (#14213) * mobile sessions: freeze session-evidence-v1 receipt contract Freeze the minimal versioned session/evidence schema shared by the mobile development-foundation consumers (C2 journeys, C3 capture replay, C4 verification, C5 devices) and its executable validator. - contracts/session/session-evidence-v1.schema.json: closed v1 object binding source SHA + dirty digest, built artifact identity, loopback-only endpoints, fixture/runner versions, real timestamps, status/blocked reason and exact execution counts. No credential-shaped field exists anywhere. - dev_harness/session_evidence.py: builder + validator enforcing the cross-field semantics a JSON Schema cannot express: ready/running require an artifact whose git_sha matches the source (a stale build cannot be reported ready), production-family profiles are rejected as session targets, counts must account exactly, zero-execution receipts are only valid pre-run, and credential-shaped keys are refused at any depth. - Egress guards: validate_local_http_url/validate_local_host_port reject non-loopback or non-plain-HTTP endpoints (api.omi.me / api.omiapi.com by name) before any request is attempted. Evidence: scripts/dev-harness/run-tests.sh (test_session_evidence.py, 20 contract tests incl. stale-artifact, egress, credential and accounting rejections). * mobile sessions: deterministic synthetic auth fixture v1 Seed a synthetic Auth-emulator user through the local-dev custom-token endpoint contract from open PR #11784 (feat/app+backend local-development sign-in without OAuth) — reuse, not a competing endpoint. The PR stays with its owner; scripts/dev-harness/MOBILE_SESSIONS.md records the integration plan and provenance. - fixtures/mobile/v1.json: one deterministic user (omi-fixture-v1-user-1@local.test); RFC-reserved domain so fixture identities can never collide with a real account. No real Google/Apple user, provider key, or copied token involved. - dev_harness/mobile_fixtures.py: fail-closed seeding client — the backend URL is validated as loopback plain-HTTP before any request, production hosts are denied by name, and the persisted receipt records identity and outcome only: token_minted/token_retained, never the token itself. Evidence: test_mobile_fixtures.py (16 tests) — determinism, reserved-domain enforcement, pre-request egress refusal, 404/unreachable/wrong-uid fail-closed paths, credential-free receipts. * mobile sessions: structured doctor for the session lanes Every readiness failure classifies exactly one of ready / agent-remediable (with the exact resumption command) / operator-action-needed (privileged install, license, host capacity), per lane (backend, android, ios). - Flutter version is read from the mobile CI pin in .github/workflows/mobile-app-checks.yml — never 'latest'; inconsistent pins refuse rather than guess. - Backend lane: python3.11 (venv must be 3.11, ambient 3.14 must not select the runtime), JDK 21 for the firebase emulators, firebase-tools, redis/typesense via native binary or a responding docker daemon. - Android lane: ANDROID_HOME + adb + emulator engine + system image, each with the exact sdkmanager remedy and a capacity-gated download note. - iOS lane: Xcode + simctl runtime; missing runtime is an operator action. - Capacity: <12GiB free on the shared Data/scratch container is an operator gate for emulator/build lanes (agents never free space themselves); contract/unit lanes skip it via --skip-capacity. - Egress: an ambient production OMI_LOCAL_API_BASE_URL override is reported as a blocking misconfiguration. Evidence: test_mobile_doctor.py (13 tests) over an injected runner — lane filtering, ready/degraded/blocked classification, pin parsing, capacity and operator-gate behavior; live run on m1-mac-studio via 'make mobile-session ARGS="doctor --platform android --platform ios"' reports backend+ios ready, android emulator engine agent-remediable. * mobile sessions: isolated session lifecycle CLI behind one entrypoint 'make mobile-session ARGS="…"' (scripts/dev-harness/mobile-session.sh) owns a uniquely-leased local mobile session: doctor / acquire / start / seed / reset / status / evidence / stop / recover / release. A session is an existing dev-harness instance + port offset + device lease + seed receipt + evidence receipt — the harness lifecycle is reused in-process under OMI_LOCAL_INSTANCE/OMI_HARNESS_PORT_OFFSET, not duplicated. Ownership is fail-closed: - leases are created atomically (O_EXCL) with owner host/user/pid and a harness-standard sentinel; a live foreign owner or another local user's session is never touched; cross-host takeover is an operator decision; recover bumps the generation for same-host/same-user takeovers. - ports come from a claimed offset registry; a foreign process occupying a port is refused (never killed) and the allocator skips that offset; release frees the claim only when it belongs to the session. - start gates device attach on doctor readiness (precise blocked reason, not a crash); ios-simulator devices are created/booted/deleted session-owned via simctl. - seed/reset/stop/release are idempotent; reset only touches the session's own harness instance (sentinel-validated underneath). - evidence emits session-evidence-v1 receipts; ready/running refuse without a bound artifact and refuse when the source moved since acquire. app/setup.sh (separate commit): OMI_IOS_DEVICE_ID pins non-interactive device selection; OMI_DEVICE_SUFFIX overrides hostname identity. Evidence: test_mobile_session.py (19 tests) + wrapper tests — exclusivity, disjoint ports, dead-owner/live-foreign/different-user/cross-host refusals, foreign-port refusal with a real live listener, idempotent release, artifact binding, stale-source refusal, harness env handoff. Live CLI run: acquire/list/evidence/seed-fail-closed/stop/release with exit codes 0/2 on m1-mac-studio. * app/setup.sh: non-interactive device pin and per-session device suffix - OMI_IOS_DEVICE_ID: when set, select_ios_device uses exactly that device id, failing precisely (with the available device list) when absent, instead of enumerating and prompting — the mobile-session harness, CI and nested agents cannot answer an interactive prompt, and the current no-TTY path errors out whenever more than one iOS destination exists. - OMI_DEVICE_SUFFIX: let a session harness (or a second checkout on one host) inject a unique device-identity suffix instead of the hostname, which collides across concurrent sessions on the same machine. Unset behavior is unchanged. Verified by sourcing the function with a stubbed flutter devices --machine: pinned-present emits the id; pinned-absent fails with the list; unpinned multi-device no-TTY keeps the existing enumeration failure. * mobile sessions: apply repo python formatter to the new modules black 26.5.1, --line-length 120 --skip-string-normalization via scripts/backend-python-format; behavior unchanged, dev-harness lane re-run green (214 passed; 1 pre-existing environmental failure — the host's global git worktree guard blocks pytest-tmp linked worktrees). * test: place linked-worktree pytest fixtures under OMI_WORKTREES The managed git wrapper correctly refuses worktrees in /private/tmp. Keep that guard and put the fixture where task worktrees are allowed. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: treat session-evidence-v1 as proposed until consumers review it C1 shipped the schema; freeze it only after C2/C3/C4 agree, not from a single worker declaration. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: reuse PR 11784 local-dev custom-token auth on current main Copy the reviewed emulator-gated sign-in path onto this integration branch so synthetic seed talks to real local services. Leave the original PR open and unmerged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: boot isolated sessions on real CoreSimulator IDs and offline STT Use the installed iPhone 17 Pro / iOS 26.5 identifiers, pin PROVIDER_MODE=offline, and drop soniox from the offline STT chain so the local backend can start without a paid key. Co-authored-by: Cursor <cursoragent@cursor.com> * test: isolate provider-secret fixtures from ambient PROVIDER_MODE A previous offline session left PROVIDER_MODE in the shell and made the secret-injection tests read ambient offline instead of the fixture file. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): injectable capture seams for deterministic recovery replay CaptureController and the phone WAL resolved clock, timers, connectivity, auth, mic, socket and upload policy through global singletons, so the capture -> WAL -> recovery path could not be replayed deterministically. Add narrow constructor seams (capture_seams.dart) with production-identical defaults: CaptureScheduling, CaptureAuthBoundary, CaptureConnectivityBoundary, plus wal/phoneMic/clock/scheduler injection on CaptureController; clock/periodic/job-status injection on LocalWalSyncImpl threaded through WalSyncs/WalService; periodic-timer injection on the NativeMicRecorderService watchdogs. The in-progress-conversation loader seam now covers the socket-connect path too, and streamRecording honors the microphone permission requester like the batch path already did. No behavior change with default construction; every seam is optional. Evidence: bash app/test.sh (1990 passed, 5 pre-existing skips); analyze ratchet green. * test(app): deterministic capture-recovery replay schedules (SCA-489/C3) Replay the REAL production capture pipeline (CaptureController, NativeMicRecorderService, TranscriptSegmentSocketService, WalService, RecordingTransferCoordinator) against controlled external I/O: virtual clock, manual bounded scheduler, scripted transport/upload boundary, fake native host. Restart evidence destroys and reconstructs the object graph from real temp files (torn wals.json -> backup recovery, missing audio -> terminal corruption, process kill -> disk reload and re-upload). Six schedules with invariant oracles: network loss/reconnect mid-capture (exact frame identity in the stored WAL, single upload), stale native events after stop/new session (session-identity gate, no double teardown), interruption/resumption (live + batch, bounded stall escalation), partial/torn persistence plus reconstruction, failed upload with bounded backoff and persisted/enqueued/server-acknowledged distinctions, and ownership transition (signed-out reconnect cancellation, bounded 4001 token refresh). Also publishes the C2/C4 adapter (capture_scenario.dart: catalog + result contract) and the C5 native-event vector schema (phone-mic-native-events/v1) mirroring the Pigeon PhoneMicFlutterApi contract without touching Pigeon. Falsification evidence: removing the NativeMicRecorderService session gate flips the stale-idle schedule to failure (record->stop); removing the finalizeCurrentSession unsynced-retention guard drops the WAL and fails the network-loss schedule. Evidence: flutter test test/unit/capture_recovery_replay_scenarios_test.dart (17 passed); bash app/test.sh full suite green. * chore(app): allowlist SCA-489 replay contract libs in the dead-code ratchet The scenario catalog/result contract and the native-event vector schema are library-only by design until the C2/C4 and C5 lanes import them; the ratchet demands an explicit allowlist entry with a reason for exactly this case. * test: pin conversation-window capture session id across sequential phone-mic lives activeCaptureSessionId is WAL/conversation-scoped so a late ConversationEvent can still stamp WALs. C2 must use activeRecordingId as the live recording identity. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: wait for unawaited capture-upload retries before asserting and teardown CI failed the bounded-backoff replay because cooldown wakes are unawaited and settle used wall-clock sleeps that missed the drain under load, then deleted the temp WAL dir mid-write. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): typed debug semantic controls for the local journey lane Extends the existing debug Marionette surface (same debug VM-service transport, no new server/framework) with product-semantic controls: versioned capabilities (semantic-controls/v1), privacy-safe state (route, principal, capture lifecycle with activeRecordingId as the authoritative recording identity), bounded wait_ready, production-path navigation, and named journey faults. Fail-closed eligibility: kDebugMode AND local_dev profile AND OMI_DEV_CONTROLS=1 dart-define. Ineligible builds (including production-flavor debug) install nothing and the HTTP fault chokepoint is a pure pass-through — pinned by semantic_controls_guard_test.dart. Narrow seams added for the hermetic journey lane: - AuthService.installLocalHarnessTokenGateway (debug+local_dev gated Firebase token I/O boundary; isSignedIn routes through the gateway) - PlatformManager.initializeForLocalHarness (header fields only) - CrashlyticsManager report paths tolerate a missing Firebase app the same way main.dart's zone handler already does, so host-lane errors surface instead of being masked by [core/no-app] Verified: flutter test test/unit/semantic_controls_guard_test.dart (10 passed); auth regression suites (34 passed); C3 capture replay (17 passed); dead-code ratchet at baseline. * test(app): five strict seeded acceptance journeys with negative fault variants Canonical executable definitions (one per behavior) under app/integration_test/journeys/, runnable hermetically (flutter-tester + loopback fixture backend) or on a simulator via run_journeys.sh: j1 seeded conversation detail — real provider fetch + real detail page, exact synthetic identity; negative: wrong-owner session refused. j2 chat send -> distinct assistant reply — real input/send-button keys (omi.chat.input / omi.chat.send), request observed server-side, server-minted ai-role reply distinct from the prompt, rendered; negatives: suppress-send, suppress-assistant-reply, wrong-owner-session. j3 memory create/edit surviving reload — production provider path, server-minted id required after reload; negative: drop-memory-save. j4 expired session — transient failure re-mints via the real custom-token endpoint; terminal failure emits expiry and blocks requests; negative: production-family profiles never silently re-mint. j5 capture interruption/reconnect — C3 capture-scenario/v1 adapter: real temp files, process reconstruction, drain exactly once; negative: fail-capture-recovery. Each negative arms exactly one named fault and must fail with the invariant named. Evidence receipts follow session-evidence-v1 accounting; zero-execution runs never pass. Verified: bash integration_test/journeys/run_journeys.sh (5/5 pass); repeated deterministic vertical: bash integration_test/journeys/run_journeys.sh --filter j2 --runs 5. * fix: stamp journey evidence finished_at at write time Receipts were recording construction time as the end timestamp, so duration could not be distinguished from start. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(app): clear new analyzer-ratchet regressions in the integrated journeys The integrated checkpoint (b217eb1a9d) fails app/scripts/analyze_ratchet.sh with 7 new occurrences: 3 unused imports plus a bogus 'show WalStatus' in j5, an unused-looking nested import that actually provides SingleChildWidget in hermetic_boot, a missing const in j4, and two depend_on_referenced_packages for test-only platform interfaces. Declares path_provider_platform_interface and nested as direct dev dependencies (same pattern as the existing web_socket_channel dev deps) and removes the dead imports. Mechanical lint repairs only — j4/j5 hermetic journeys re-run green after the change. * feat: unified mobile verify lanes, mechanical journey selection, and CI/contributor path (SCA-490/C4) One canonical verification entrypoint over the proven lanes: make mobile-verify select|doctor|fast|smoke|physical (scripts/dev-harness/mobile-verify.sh -> dev_harness.mobile_verify). It never adds a second runner: journeys delegate to the C2 canonical runner and session infrastructure to the C1 session CLI. Selection is mechanical and fail-closed: journeys are glob-discovered (runner --list contract test), changed paths map through a per-seam rule table, unknown app/lib impact falls back to the full suite, and an empty selection is drift (exit 65), never PASS(0) — run_journeys.sh now fails closed the same way. Receipts are validated against session-evidence-v1 accounting (honest counts, zero-execution never passes) and every lane writes a verify-receipt.json binding source SHA + dirty digest, runner versions, outcomes, and the exact rerun command. smoke is fail-closed (exit 2 + remedy, never CI), physical is a separately reported admission lane. CI runs the same command in a new journeys-hermetic job in the existing mobile-app-checks.yml when has_app_journeys fires (journey definitions and support, C3 replay world, dev controls, non-generated app/lib Dart, evidence contract, or this entrypoint) — synthetic fixtures only, fork-safe, receipts uploaded on pass and failure. Selection is resolved by the shared pre_push_ci_prediction.py and deliberately stays out of the bounded pre-push gate. Docs reconciled around the real command: app README, app AGENTS (within the lean budget), and the e2e SKILL now point here instead of diverging on setup/auth. * chore(app): stop tracking Flutter's iOS ephemeral tree app/ios/Flutter/ephemeral/** is regenerated by flutter on every pub get and self-describes as 'Generated file. Do not edit.' It was committed by accident in a formatting sweep (dec329a84a) and has been stale ever since: the tracked SwiftPM Package.swift lists pods (in_app_review, pasteboard) that no pub dependency provides, so any flutter run rewrites it, dirties every worktree, and fails the diff-hygiene push gate on regenerated trailing whitespace. Untrack the four files and ignore the tree, mirroring the existing **/macos/Flutter/ephemeral/ rules. Xcode resolves the local package after flutter regenerates it during setup; nothing consumes a committed copy. * feat: native lifecycle seams, vector replay, and leased device qualification (SCA-491/C5) - PhoneMicController (iOS + Android) now consumes narrow, injectable environment/ports seams: event sink, engine, permission, session config, interruption source, batch pipeline, main loop. Production behavior is unchanged; all live wiring lives in PhoneMicHostApiImpl.swift (iOS) and PhoneMicControllerPorts.production (Android). - Canonical phone-mic-native-events/v1 vector fixtures (8 schedules incl. session adoption) shared by Dart guard, iOS ruby harness, Android JVM harness; Pigeon contract types extracted at iOS test time (drift-guarded). - iOS: ios/test/phone_mic_lifecycle_replay_test.rb replays all vectors through the production controller+emitter with fakes for OS I/O only. - Android: PhoneMicLifecycleReplayTest (JVM, virtual main loop + manual audio queue) replays the same vectors through the production controller. - device_lease.py: exclusive physical-device leases with qualification registry (personal-device refusal), bounded acquisition, live-lease never-stolen, stale-owner recovery with generation bump, safe release. - device_runner.py + 'mobile-session device' CLI: readiness doctor with exact operator steps, and a runner consuming C1 session manifests (install/adb-reverse/untethered launch/permission cycle/device-run evidence v1). All hermetically tested with fake devices (25 tests). - PHYSICAL_DEVICES.md: m1-mac-studio read-only inventory, operator runbook, and the external physical-test handoff template. Physical acceptance stays pending user-run evidence by design. * docs: point mobile-verify physical at the C5 device handoff C4's physical lane stays fail-closed (exit 2). After C5 landed, the admission document should name the real runner and PHYSICAL_DEVICES.md instead of implying the software path is still missing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: expect four Flutter pins in mobile-app-checks C4 added journeys-hermetic as a fourth Flutter job on the same repository toolchain pin. The workflow-contract count of 3 was stale. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: raise Desktop Swift PR-lane suite budget to 3000s Run 35134593036 measured 2778s against 2700s on a cache-hit PR lane. The overrun was one 1500s batch ceiling plus isolation, not a slow desktop suite; this mobile PR has no desktop sources. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> | 1 个月前 | |
app(ios): native PhoneMic capture module (AVAudioEngine + Pigeon) Self-contained Swift module replacing flutter_sound capture for iOS conversation recording. AVAudioEngine input tap at the hardware format, AVAudioConverter to PCM16/16kHz/mono, and native self-healing recovery: AVAudioSession interruption observer (resumes only when the OS grants shouldResume), route-change rebuilds (AirPods in/out), engine config-change and media-services-reset handling, CXCallObserver for the declined-call case where .ended never arrives, and a 3s resume ticker. Session policy: playAndRecord + mixWithOthers + BT options — recording no longer pauses other apps' audio; permission denial throws instead of recording silence. Replaces AudioInterruptionManager.swift and the audioSession 'reactivate' method — interruption recovery now lives in the native module instead of a Dart-driven stop/sleep/reactivate/restart cycle. Verified: flutter build ios --debug --flavor dev --no-codesign (OK); full behavior exercised in the follow-up Dart integration commit. | 3 个月前 | |
fix(ios): keep watchdog margin for a missed beat and preserve recovery's fast beat Address review feedback on the paced batch heartbeat: - Healthy cadence 5s -> 4s (P2 r4259674208): strictly under half the unchanged 10s Dart batch-stall deadline. One lost or coalesced beat now lands the next at ~8.5s (2x4s + 500ms leeway) instead of exactly at the >=10s deadline, so a transient missed callback no longer restarts a healthy batch capture. - Preserve the pending fast first fire when recovery reaches .running (P1 r4259674193): applyProgressCadence previously rescheduled the existing timer to now+5s, so a quick interruption/rebuild recovery could stretch the beat gap past 10s and false-restart a recovered session. It now keeps the ~1s first fire (the shape the foreground path already used) and only switches the repeating cadence. - Align the Pigeon cadence doc across Dart/Swift/Kotlin outputs (P2 r4259674216): onBatchProgress is iOS 4s healthy / 1s fault, Android 1Hz. Verified: flutter test test/unit/native_mic_recorder_service_test.dart (29 passed) and ruby app/ios/test/phone_mic_lifecycle_replay_test.rb (14 assertions), which now also pins the 4s cadence and the preserved fast beat on resume. | 15 天前 | |
SCA-486: isolated mobile sessions, local-dev auth, capture replay, and seeded journeys (#14213) * mobile sessions: freeze session-evidence-v1 receipt contract Freeze the minimal versioned session/evidence schema shared by the mobile development-foundation consumers (C2 journeys, C3 capture replay, C4 verification, C5 devices) and its executable validator. - contracts/session/session-evidence-v1.schema.json: closed v1 object binding source SHA + dirty digest, built artifact identity, loopback-only endpoints, fixture/runner versions, real timestamps, status/blocked reason and exact execution counts. No credential-shaped field exists anywhere. - dev_harness/session_evidence.py: builder + validator enforcing the cross-field semantics a JSON Schema cannot express: ready/running require an artifact whose git_sha matches the source (a stale build cannot be reported ready), production-family profiles are rejected as session targets, counts must account exactly, zero-execution receipts are only valid pre-run, and credential-shaped keys are refused at any depth. - Egress guards: validate_local_http_url/validate_local_host_port reject non-loopback or non-plain-HTTP endpoints (api.omi.me / api.omiapi.com by name) before any request is attempted. Evidence: scripts/dev-harness/run-tests.sh (test_session_evidence.py, 20 contract tests incl. stale-artifact, egress, credential and accounting rejections). * mobile sessions: deterministic synthetic auth fixture v1 Seed a synthetic Auth-emulator user through the local-dev custom-token endpoint contract from open PR #11784 (feat/app+backend local-development sign-in without OAuth) — reuse, not a competing endpoint. The PR stays with its owner; scripts/dev-harness/MOBILE_SESSIONS.md records the integration plan and provenance. - fixtures/mobile/v1.json: one deterministic user (omi-fixture-v1-user-1@local.test); RFC-reserved domain so fixture identities can never collide with a real account. No real Google/Apple user, provider key, or copied token involved. - dev_harness/mobile_fixtures.py: fail-closed seeding client — the backend URL is validated as loopback plain-HTTP before any request, production hosts are denied by name, and the persisted receipt records identity and outcome only: token_minted/token_retained, never the token itself. Evidence: test_mobile_fixtures.py (16 tests) — determinism, reserved-domain enforcement, pre-request egress refusal, 404/unreachable/wrong-uid fail-closed paths, credential-free receipts. * mobile sessions: structured doctor for the session lanes Every readiness failure classifies exactly one of ready / agent-remediable (with the exact resumption command) / operator-action-needed (privileged install, license, host capacity), per lane (backend, android, ios). - Flutter version is read from the mobile CI pin in .github/workflows/mobile-app-checks.yml — never 'latest'; inconsistent pins refuse rather than guess. - Backend lane: python3.11 (venv must be 3.11, ambient 3.14 must not select the runtime), JDK 21 for the firebase emulators, firebase-tools, redis/typesense via native binary or a responding docker daemon. - Android lane: ANDROID_HOME + adb + emulator engine + system image, each with the exact sdkmanager remedy and a capacity-gated download note. - iOS lane: Xcode + simctl runtime; missing runtime is an operator action. - Capacity: <12GiB free on the shared Data/scratch container is an operator gate for emulator/build lanes (agents never free space themselves); contract/unit lanes skip it via --skip-capacity. - Egress: an ambient production OMI_LOCAL_API_BASE_URL override is reported as a blocking misconfiguration. Evidence: test_mobile_doctor.py (13 tests) over an injected runner — lane filtering, ready/degraded/blocked classification, pin parsing, capacity and operator-gate behavior; live run on m1-mac-studio via 'make mobile-session ARGS="doctor --platform android --platform ios"' reports backend+ios ready, android emulator engine agent-remediable. * mobile sessions: isolated session lifecycle CLI behind one entrypoint 'make mobile-session ARGS="…"' (scripts/dev-harness/mobile-session.sh) owns a uniquely-leased local mobile session: doctor / acquire / start / seed / reset / status / evidence / stop / recover / release. A session is an existing dev-harness instance + port offset + device lease + seed receipt + evidence receipt — the harness lifecycle is reused in-process under OMI_LOCAL_INSTANCE/OMI_HARNESS_PORT_OFFSET, not duplicated. Ownership is fail-closed: - leases are created atomically (O_EXCL) with owner host/user/pid and a harness-standard sentinel; a live foreign owner or another local user's session is never touched; cross-host takeover is an operator decision; recover bumps the generation for same-host/same-user takeovers. - ports come from a claimed offset registry; a foreign process occupying a port is refused (never killed) and the allocator skips that offset; release frees the claim only when it belongs to the session. - start gates device attach on doctor readiness (precise blocked reason, not a crash); ios-simulator devices are created/booted/deleted session-owned via simctl. - seed/reset/stop/release are idempotent; reset only touches the session's own harness instance (sentinel-validated underneath). - evidence emits session-evidence-v1 receipts; ready/running refuse without a bound artifact and refuse when the source moved since acquire. app/setup.sh (separate commit): OMI_IOS_DEVICE_ID pins non-interactive device selection; OMI_DEVICE_SUFFIX overrides hostname identity. Evidence: test_mobile_session.py (19 tests) + wrapper tests — exclusivity, disjoint ports, dead-owner/live-foreign/different-user/cross-host refusals, foreign-port refusal with a real live listener, idempotent release, artifact binding, stale-source refusal, harness env handoff. Live CLI run: acquire/list/evidence/seed-fail-closed/stop/release with exit codes 0/2 on m1-mac-studio. * app/setup.sh: non-interactive device pin and per-session device suffix - OMI_IOS_DEVICE_ID: when set, select_ios_device uses exactly that device id, failing precisely (with the available device list) when absent, instead of enumerating and prompting — the mobile-session harness, CI and nested agents cannot answer an interactive prompt, and the current no-TTY path errors out whenever more than one iOS destination exists. - OMI_DEVICE_SUFFIX: let a session harness (or a second checkout on one host) inject a unique device-identity suffix instead of the hostname, which collides across concurrent sessions on the same machine. Unset behavior is unchanged. Verified by sourcing the function with a stubbed flutter devices --machine: pinned-present emits the id; pinned-absent fails with the list; unpinned multi-device no-TTY keeps the existing enumeration failure. * mobile sessions: apply repo python formatter to the new modules black 26.5.1, --line-length 120 --skip-string-normalization via scripts/backend-python-format; behavior unchanged, dev-harness lane re-run green (214 passed; 1 pre-existing environmental failure — the host's global git worktree guard blocks pytest-tmp linked worktrees). * test: place linked-worktree pytest fixtures under OMI_WORKTREES The managed git wrapper correctly refuses worktrees in /private/tmp. Keep that guard and put the fixture where task worktrees are allowed. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: treat session-evidence-v1 as proposed until consumers review it C1 shipped the schema; freeze it only after C2/C3/C4 agree, not from a single worker declaration. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: reuse PR 11784 local-dev custom-token auth on current main Copy the reviewed emulator-gated sign-in path onto this integration branch so synthetic seed talks to real local services. Leave the original PR open and unmerged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: boot isolated sessions on real CoreSimulator IDs and offline STT Use the installed iPhone 17 Pro / iOS 26.5 identifiers, pin PROVIDER_MODE=offline, and drop soniox from the offline STT chain so the local backend can start without a paid key. Co-authored-by: Cursor <cursoragent@cursor.com> * test: isolate provider-secret fixtures from ambient PROVIDER_MODE A previous offline session left PROVIDER_MODE in the shell and made the secret-injection tests read ambient offline instead of the fixture file. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): injectable capture seams for deterministic recovery replay CaptureController and the phone WAL resolved clock, timers, connectivity, auth, mic, socket and upload policy through global singletons, so the capture -> WAL -> recovery path could not be replayed deterministically. Add narrow constructor seams (capture_seams.dart) with production-identical defaults: CaptureScheduling, CaptureAuthBoundary, CaptureConnectivityBoundary, plus wal/phoneMic/clock/scheduler injection on CaptureController; clock/periodic/job-status injection on LocalWalSyncImpl threaded through WalSyncs/WalService; periodic-timer injection on the NativeMicRecorderService watchdogs. The in-progress-conversation loader seam now covers the socket-connect path too, and streamRecording honors the microphone permission requester like the batch path already did. No behavior change with default construction; every seam is optional. Evidence: bash app/test.sh (1990 passed, 5 pre-existing skips); analyze ratchet green. * test(app): deterministic capture-recovery replay schedules (SCA-489/C3) Replay the REAL production capture pipeline (CaptureController, NativeMicRecorderService, TranscriptSegmentSocketService, WalService, RecordingTransferCoordinator) against controlled external I/O: virtual clock, manual bounded scheduler, scripted transport/upload boundary, fake native host. Restart evidence destroys and reconstructs the object graph from real temp files (torn wals.json -> backup recovery, missing audio -> terminal corruption, process kill -> disk reload and re-upload). Six schedules with invariant oracles: network loss/reconnect mid-capture (exact frame identity in the stored WAL, single upload), stale native events after stop/new session (session-identity gate, no double teardown), interruption/resumption (live + batch, bounded stall escalation), partial/torn persistence plus reconstruction, failed upload with bounded backoff and persisted/enqueued/server-acknowledged distinctions, and ownership transition (signed-out reconnect cancellation, bounded 4001 token refresh). Also publishes the C2/C4 adapter (capture_scenario.dart: catalog + result contract) and the C5 native-event vector schema (phone-mic-native-events/v1) mirroring the Pigeon PhoneMicFlutterApi contract without touching Pigeon. Falsification evidence: removing the NativeMicRecorderService session gate flips the stale-idle schedule to failure (record->stop); removing the finalizeCurrentSession unsynced-retention guard drops the WAL and fails the network-loss schedule. Evidence: flutter test test/unit/capture_recovery_replay_scenarios_test.dart (17 passed); bash app/test.sh full suite green. * chore(app): allowlist SCA-489 replay contract libs in the dead-code ratchet The scenario catalog/result contract and the native-event vector schema are library-only by design until the C2/C4 and C5 lanes import them; the ratchet demands an explicit allowlist entry with a reason for exactly this case. * test: pin conversation-window capture session id across sequential phone-mic lives activeCaptureSessionId is WAL/conversation-scoped so a late ConversationEvent can still stamp WALs. C2 must use activeRecordingId as the live recording identity. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: wait for unawaited capture-upload retries before asserting and teardown CI failed the bounded-backoff replay because cooldown wakes are unawaited and settle used wall-clock sleeps that missed the drain under load, then deleted the temp WAL dir mid-write. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): typed debug semantic controls for the local journey lane Extends the existing debug Marionette surface (same debug VM-service transport, no new server/framework) with product-semantic controls: versioned capabilities (semantic-controls/v1), privacy-safe state (route, principal, capture lifecycle with activeRecordingId as the authoritative recording identity), bounded wait_ready, production-path navigation, and named journey faults. Fail-closed eligibility: kDebugMode AND local_dev profile AND OMI_DEV_CONTROLS=1 dart-define. Ineligible builds (including production-flavor debug) install nothing and the HTTP fault chokepoint is a pure pass-through — pinned by semantic_controls_guard_test.dart. Narrow seams added for the hermetic journey lane: - AuthService.installLocalHarnessTokenGateway (debug+local_dev gated Firebase token I/O boundary; isSignedIn routes through the gateway) - PlatformManager.initializeForLocalHarness (header fields only) - CrashlyticsManager report paths tolerate a missing Firebase app the same way main.dart's zone handler already does, so host-lane errors surface instead of being masked by [core/no-app] Verified: flutter test test/unit/semantic_controls_guard_test.dart (10 passed); auth regression suites (34 passed); C3 capture replay (17 passed); dead-code ratchet at baseline. * test(app): five strict seeded acceptance journeys with negative fault variants Canonical executable definitions (one per behavior) under app/integration_test/journeys/, runnable hermetically (flutter-tester + loopback fixture backend) or on a simulator via run_journeys.sh: j1 seeded conversation detail — real provider fetch + real detail page, exact synthetic identity; negative: wrong-owner session refused. j2 chat send -> distinct assistant reply — real input/send-button keys (omi.chat.input / omi.chat.send), request observed server-side, server-minted ai-role reply distinct from the prompt, rendered; negatives: suppress-send, suppress-assistant-reply, wrong-owner-session. j3 memory create/edit surviving reload — production provider path, server-minted id required after reload; negative: drop-memory-save. j4 expired session — transient failure re-mints via the real custom-token endpoint; terminal failure emits expiry and blocks requests; negative: production-family profiles never silently re-mint. j5 capture interruption/reconnect — C3 capture-scenario/v1 adapter: real temp files, process reconstruction, drain exactly once; negative: fail-capture-recovery. Each negative arms exactly one named fault and must fail with the invariant named. Evidence receipts follow session-evidence-v1 accounting; zero-execution runs never pass. Verified: bash integration_test/journeys/run_journeys.sh (5/5 pass); repeated deterministic vertical: bash integration_test/journeys/run_journeys.sh --filter j2 --runs 5. * fix: stamp journey evidence finished_at at write time Receipts were recording construction time as the end timestamp, so duration could not be distinguished from start. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(app): clear new analyzer-ratchet regressions in the integrated journeys The integrated checkpoint (b217eb1a9d) fails app/scripts/analyze_ratchet.sh with 7 new occurrences: 3 unused imports plus a bogus 'show WalStatus' in j5, an unused-looking nested import that actually provides SingleChildWidget in hermetic_boot, a missing const in j4, and two depend_on_referenced_packages for test-only platform interfaces. Declares path_provider_platform_interface and nested as direct dev dependencies (same pattern as the existing web_socket_channel dev deps) and removes the dead imports. Mechanical lint repairs only — j4/j5 hermetic journeys re-run green after the change. * feat: unified mobile verify lanes, mechanical journey selection, and CI/contributor path (SCA-490/C4) One canonical verification entrypoint over the proven lanes: make mobile-verify select|doctor|fast|smoke|physical (scripts/dev-harness/mobile-verify.sh -> dev_harness.mobile_verify). It never adds a second runner: journeys delegate to the C2 canonical runner and session infrastructure to the C1 session CLI. Selection is mechanical and fail-closed: journeys are glob-discovered (runner --list contract test), changed paths map through a per-seam rule table, unknown app/lib impact falls back to the full suite, and an empty selection is drift (exit 65), never PASS(0) — run_journeys.sh now fails closed the same way. Receipts are validated against session-evidence-v1 accounting (honest counts, zero-execution never passes) and every lane writes a verify-receipt.json binding source SHA + dirty digest, runner versions, outcomes, and the exact rerun command. smoke is fail-closed (exit 2 + remedy, never CI), physical is a separately reported admission lane. CI runs the same command in a new journeys-hermetic job in the existing mobile-app-checks.yml when has_app_journeys fires (journey definitions and support, C3 replay world, dev controls, non-generated app/lib Dart, evidence contract, or this entrypoint) — synthetic fixtures only, fork-safe, receipts uploaded on pass and failure. Selection is resolved by the shared pre_push_ci_prediction.py and deliberately stays out of the bounded pre-push gate. Docs reconciled around the real command: app README, app AGENTS (within the lean budget), and the e2e SKILL now point here instead of diverging on setup/auth. * chore(app): stop tracking Flutter's iOS ephemeral tree app/ios/Flutter/ephemeral/** is regenerated by flutter on every pub get and self-describes as 'Generated file. Do not edit.' It was committed by accident in a formatting sweep (dec329a84a) and has been stale ever since: the tracked SwiftPM Package.swift lists pods (in_app_review, pasteboard) that no pub dependency provides, so any flutter run rewrites it, dirties every worktree, and fails the diff-hygiene push gate on regenerated trailing whitespace. Untrack the four files and ignore the tree, mirroring the existing **/macos/Flutter/ephemeral/ rules. Xcode resolves the local package after flutter regenerates it during setup; nothing consumes a committed copy. * feat: native lifecycle seams, vector replay, and leased device qualification (SCA-491/C5) - PhoneMicController (iOS + Android) now consumes narrow, injectable environment/ports seams: event sink, engine, permission, session config, interruption source, batch pipeline, main loop. Production behavior is unchanged; all live wiring lives in PhoneMicHostApiImpl.swift (iOS) and PhoneMicControllerPorts.production (Android). - Canonical phone-mic-native-events/v1 vector fixtures (8 schedules incl. session adoption) shared by Dart guard, iOS ruby harness, Android JVM harness; Pigeon contract types extracted at iOS test time (drift-guarded). - iOS: ios/test/phone_mic_lifecycle_replay_test.rb replays all vectors through the production controller+emitter with fakes for OS I/O only. - Android: PhoneMicLifecycleReplayTest (JVM, virtual main loop + manual audio queue) replays the same vectors through the production controller. - device_lease.py: exclusive physical-device leases with qualification registry (personal-device refusal), bounded acquisition, live-lease never-stolen, stale-owner recovery with generation bump, safe release. - device_runner.py + 'mobile-session device' CLI: readiness doctor with exact operator steps, and a runner consuming C1 session manifests (install/adb-reverse/untethered launch/permission cycle/device-run evidence v1). All hermetically tested with fake devices (25 tests). - PHYSICAL_DEVICES.md: m1-mac-studio read-only inventory, operator runbook, and the external physical-test handoff template. Physical acceptance stays pending user-run evidence by design. * docs: point mobile-verify physical at the C5 device handoff C4's physical lane stays fail-closed (exit 2). After C5 landed, the admission document should name the real runner and PHYSICAL_DEVICES.md instead of implying the software path is still missing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: expect four Flutter pins in mobile-app-checks C4 added journeys-hermetic as a fourth Flutter job on the same repository toolchain pin. The workflow-contract count of 3 was stale. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: raise Desktop Swift PR-lane suite budget to 3000s Run 35134593036 measured 2778s against 2700s on a cache-hit PR lane. The overrun was one 1500s batch ceiling plus isolation, not a slow desktop suite; this mobile PR has no desktop sources. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> | 1 个月前 | |
app(ios): native PhoneMic capture module (AVAudioEngine + Pigeon) Self-contained Swift module replacing flutter_sound capture for iOS conversation recording. AVAudioEngine input tap at the hardware format, AVAudioConverter to PCM16/16kHz/mono, and native self-healing recovery: AVAudioSession interruption observer (resumes only when the OS grants shouldResume), route-change rebuilds (AirPods in/out), engine config-change and media-services-reset handling, CXCallObserver for the declined-call case where .ended never arrives, and a 3s resume ticker. Session policy: playAndRecord + mixWithOthers + BT options — recording no longer pauses other apps' audio; permission denial throws instead of recording silence. Replaces AudioInterruptionManager.swift and the audioSession 'reactivate' method — interruption recovery now lives in the native module instead of a Dart-driven stop/sleep/reactivate/restart cycle. Verified: flutter build ios --debug --flavor dev --no-codesign (OK); full behavior exercised in the follow-up Dart integration commit. | 3 个月前 | |
app(ios): native opus batch capture mode for the PhoneMic module Adds a batch (Transcribe Later) sink to the phone-mic capture module: converted PCM16 chunks are opus-encoded natively (PhoneMicOpusEncoder, libopus via the already-linked OpusKit.xcframework; 20ms/320-sample frames @32kbps; a non-variadic C shim wraps opus_encoder_ctl, which Swift cannot call) and written as WAL-compatible length-prefixed .bin files (PhoneMicBatchAudioWriter over BaseBatchAudioWriter: audio_omibatchphone[auto]_opus_fs320_16000_1_fs320_<ts>.bin, 32MB/900s rotation, lazy 30s gap finalize, batchMuted/batchCutRequested honored, storage-full edge latched). Pigeon contract: start() -> start(mode: stream|batch); onBatchProgress (1Hz liveness signal, frames-written derived); DEBUG-only debugEncodeWavToBin round-trip hook (removed before merge). Key orderings: the writer shares the module's audioQueue so audioQueue.sync{} genuinely drains writes, and batch stop finalizes the file (fsync + atomic promote) before the Pigeon stop() resolves; one encoder per session survives rebuilds (backend decodes each file with a fresh decoder — same property as Limitless pendant files) and its partial frame is discarded on every engine teardown so interruption boundaries never splice audio. Stream mode is behavior-identical. Verified: flutter test test/unit/native_mic_recorder_service_test.dart test/unit/mic_arbiter_test.dart (13 passed); flutter build ios --debug --flavor dev --no-codesign; plutil -lint pbxproj OK. Encoder round-trip against backend decode_opus_file_to_wav deferred to device testing (the DEBUG hook exists for it). | 3 个月前 | |
app(ios): native opus batch capture mode for the PhoneMic module Adds a batch (Transcribe Later) sink to the phone-mic capture module: converted PCM16 chunks are opus-encoded natively (PhoneMicOpusEncoder, libopus via the already-linked OpusKit.xcframework; 20ms/320-sample frames @32kbps; a non-variadic C shim wraps opus_encoder_ctl, which Swift cannot call) and written as WAL-compatible length-prefixed .bin files (PhoneMicBatchAudioWriter over BaseBatchAudioWriter: audio_omibatchphone[auto]_opus_fs320_16000_1_fs320_<ts>.bin, 32MB/900s rotation, lazy 30s gap finalize, batchMuted/batchCutRequested honored, storage-full edge latched). Pigeon contract: start() -> start(mode: stream|batch); onBatchProgress (1Hz liveness signal, frames-written derived); DEBUG-only debugEncodeWavToBin round-trip hook (removed before merge). Key orderings: the writer shares the module's audioQueue so audioQueue.sync{} genuinely drains writes, and batch stop finalizes the file (fsync + atomic promote) before the Pigeon stop() resolves; one encoder per session survives rebuilds (backend decodes each file with a fresh decoder — same property as Limitless pendant files) and its partial frame is discarded on every engine teardown so interruption boundaries never splice audio. Stream mode is behavior-identical. Verified: flutter test test/unit/native_mic_recorder_service_test.dart test/unit/mic_arbiter_test.dart (13 passed); flutter build ios --debug --flavor dev --no-codesign; plutil -lint pbxproj OK. Encoder round-trip against backend decode_opus_file_to_wav deferred to device testing (the DEBUG hook exists for it). | 3 个月前 | |
app(ios): native opus batch capture mode for the PhoneMic module Adds a batch (Transcribe Later) sink to the phone-mic capture module: converted PCM16 chunks are opus-encoded natively (PhoneMicOpusEncoder, libopus via the already-linked OpusKit.xcframework; 20ms/320-sample frames @32kbps; a non-variadic C shim wraps opus_encoder_ctl, which Swift cannot call) and written as WAL-compatible length-prefixed .bin files (PhoneMicBatchAudioWriter over BaseBatchAudioWriter: audio_omibatchphone[auto]_opus_fs320_16000_1_fs320_<ts>.bin, 32MB/900s rotation, lazy 30s gap finalize, batchMuted/batchCutRequested honored, storage-full edge latched). Pigeon contract: start() -> start(mode: stream|batch); onBatchProgress (1Hz liveness signal, frames-written derived); DEBUG-only debugEncodeWavToBin round-trip hook (removed before merge). Key orderings: the writer shares the module's audioQueue so audioQueue.sync{} genuinely drains writes, and batch stop finalizes the file (fsync + atomic promote) before the Pigeon stop() resolves; one encoder per session survives rebuilds (backend decodes each file with a fresh decoder — same property as Limitless pendant files) and its partial frame is discarded on every engine teardown so interruption boundaries never splice audio. Stream mode is behavior-identical. Verified: flutter test test/unit/native_mic_recorder_service_test.dart test/unit/mic_arbiter_test.dart (13 passed); flutter build ios --debug --flavor dev --no-codesign; plutil -lint pbxproj OK. Encoder round-trip against backend decode_opus_file_to_wav deferred to device testing (the DEBUG hook exists for it). | 3 个月前 | |
app(ios): native PhoneMic capture module (AVAudioEngine + Pigeon) Self-contained Swift module replacing flutter_sound capture for iOS conversation recording. AVAudioEngine input tap at the hardware format, AVAudioConverter to PCM16/16kHz/mono, and native self-healing recovery: AVAudioSession interruption observer (resumes only when the OS grants shouldResume), route-change rebuilds (AirPods in/out), engine config-change and media-services-reset handling, CXCallObserver for the declined-call case where .ended never arrives, and a 3s resume ticker. Session policy: playAndRecord + mixWithOthers + BT options — recording no longer pauses other apps' audio; permission denial throws instead of recording silence. Replaces AudioInterruptionManager.swift and the audioSession 'reactivate' method — interruption recovery now lives in the native module instead of a Dart-driven stop/sleep/reactivate/restart cycle. Verified: flutter build ios --debug --flavor dev --no-codesign (OK); full behavior exercised in the follow-up Dart integration commit. | 3 个月前 | |
fix(ios): keep watchdog margin for a missed beat and preserve recovery's fast beat Address review feedback on the paced batch heartbeat: - Healthy cadence 5s -> 4s (P2 r4259674208): strictly under half the unchanged 10s Dart batch-stall deadline. One lost or coalesced beat now lands the next at ~8.5s (2x4s + 500ms leeway) instead of exactly at the >=10s deadline, so a transient missed callback no longer restarts a healthy batch capture. - Preserve the pending fast first fire when recovery reaches .running (P1 r4259674193): applyProgressCadence previously rescheduled the existing timer to now+5s, so a quick interruption/rebuild recovery could stretch the beat gap past 10s and false-restart a recovered session. It now keeps the ~1s first fire (the shape the foreground path already used) and only switches the repeating cadence. - Align the Pigeon cadence doc across Dart/Swift/Kotlin outputs (P2 r4259674216): onBatchProgress is iOS 4s healthy / 1s fault, Android 1Hz. Verified: flutter test test/unit/native_mic_recorder_service_test.dart (29 passed) and ruby app/ios/test/phone_mic_lifecycle_replay_test.rb (14 assertions), which now also pins the 4s cadence and the preserved fast beat on resume. | 15 天前 | |
app(ios): restore defaultToSpeaker on the phone-mic audio session The rewrite dropped .defaultToSpeaker on the assumption playback would configure its own session. It doesn't: the capture session stays active after a recording (shared sessions are never deactivated) and AudioPlayerUtils plays through whatever session state exists — so playAndRecord without defaultToSpeaker routed post-recording playback to the earpiece receiver, heard as near-silent audio. Restores the exact routing the pre-rewrite flutter_sound path had (configureForBluetooth always included defaultToSpeaker). Input capture is unaffected. Verified: flutter build ios --debug --flavor dev --no-codesign; user reproduced the earpiece-volume symptom on device, root-caused to this option. | 3 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 15 天前 | ||
| 2 个月前 | ||
| 14 天前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 15 天前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 15 天前 | ||
| 3 个月前 |