| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
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> | 2 小时前 | |
refactor(capture): split external provider actions | 2 个月前 | |
fix(app): persist Apple Health connect state across restarts (#11587) * fix(app): persist Apple Health connect state across restarts Settings → Integrations showed Connect after process death even when HealthKit was authorized and the backend already had apple_health connected. loadFromBackend only fetched Calendar and Gmail, so the in-memory Health flag never came back. Reload every IntegrationApp, hydrate IntegrationProvider at signed-in startup, wait for that load before chat auto-sync, and clear every tracked pref on logout. Failure-Class: none * fix(app): drop in-flight integration reload after logout loadFromBackend wrote fetched flags and hasLoaded after persist without re-checking session generation, so a mid-loop clearUserData could be overwritten with the previous users connect state. Failure-Class: none | 1 个月前 | |
Maps: keyless static previews + native-map handoff; fix Unknown location labels (#12841) * feat(backend): authed static-map proxy route with Redis-cached renders GET /v1/static-map renders dark-styled Google Static Maps images server-side (the only Maps key stays server-restricted) and caches the bytes in Redis keyed by the quantized pin set + size, so repeat renders of the same place across users/sessions cost one upstream call per distinct pin set per week. utils/static_map.py is the single provider seam for app map previews; swapping providers touches that module only. - pins parsed/bounded/de-duplicated/sorted, capped at 50; ~11m quantization makes users at the same place share one cached image - one pin centers at street zoom; several pins use provider auto-fit - failures return 502 (never cached) - the app renders its offline pin-dot canvas; auth prevents an open proxy on the project key - rate policy static_map:get (240/h per uid) stops hot loops; cached hits are one Redis read Tests: tests/routers/test_static_map.py (auth 401, 400 malformed pins, 502 upstream failure, 200 + private cache headers, cache hit/miss/order- insensitivity/no-cache-on-failure/missing key tolerances). Verified: .venv/bin/python -m pytest tests/routers/test_static_map.py -q -> 14 passed; scan_async_blockers clean. * fix(backend): enrich sync-path geolocation at the pipeline coordinator Failure-Class: FC-sync-geolocation-missing-address | new | none Offline-synced conversations were created with raw coordinates and no address: REST create, developer API, integration ingest, and live finalization all run resolve_geolocation, but the sync path shipped raw coords straight through. Recaps built from synced conversations showed 'Unknown' timeline rows. _run_full_pipeline_background_async - the one coordinator both the inline and Cloud Tasks dispatch branches call - now enriches the job's geolocation once, before any segment is processed and before the concurrency gate (no slot held during the geocode call). The resolver keeps the caller's exact coordinates and returns its input unchanged on any geocode miss/error, so a failure never drops the location. Tests: tests/unit/test_sync_geolocation_enrichment.py drives the real coordinator with a fake geocoder - one geocode per job (not per segment), enriched value reaches every segment, raw geolocation survives a geocode failure, None passthrough. Fixture stub lists in test_sync_v2 / test_sync_transcription_prefs gained the utils.conversations.location stub with an identity passthrough (the real resolver's miss behavior). Verified: pytest tests/unit/test_sync_geolocation_enrichment.py test_sync_v2.py test_sync_transcription_prefs.py -> all passed. * fix(backend): fill empty daily-summary pin addresses at read time Failure-Class: FC-sync-geolocation-missing-address | new | none generate_comprehensive_daily_summary copied c.geolocation.address verbatim, so pins from conversations created before write-time enrichment (the sync path) rendered as 'Unknown' recap timeline rows. The pins loop now fills an empty address through the shared ~100m- rounded geocode cache - the same entries write-time enrichment writes, so an already-enriched day costs no extra upstream call. All three callers are sync contexts, so the sync geocoder is used. A geocode miss or error leaves the address empty and keeps the pin (the app labels it 'Unknown'); regenerating a summary retroactively fixes history with no migration. Tests: tests/unit/test_daily_summary_location_address_fill.py (filled from geocoder with exact coords preserved, present address skips the geocoder, miss and exception both keep the pin address-less). Verified: pytest tests/unit/test_daily_summary_location_address_fill.py test_daily_summary_zero_coordinate_locations.py -> all passed. * feat(app): OmiMapPreview - one static-map widget with an offline pin-dot canvas Every map preview now funnels through a single widget backed by the authed backend static-map proxy (GET /v1/static-map): URL built by buildOmiStaticMapUrl (pins quantized to 4 decimals to match the server's cache quantization, deduped, capped at 50), image fetched with the session's Authorization header, and a deterministic dark canvas with one white dot per pin while loading, offline, or on any failure - never an error state. Product direction: previews in-app, tapping opens the native map app via MapsUtil.launchMap. The conversation detail geolocation card migrates from the client-keyed getMapImageUrl URL to this widget; getMapImageUrl is removed (maps_util keeps launchMap + place URL), taking the last direct client-side Google Static Maps call with it. Verified: flutter test test/widgets/omi_map_preview_test.dart test/widgets/daily_summary_card_test.dart (widget+URL builder tests); flutter analyze clean on changed files; analyze_ratchet.sh passed. * feat(app): replace CARTO tile maps with static previews; drop flutter_map CARTO began enforcing API keys on basemaps.cartocdn.com (keyless requests now return watermarked tiles), and Omi has no CARTO key. All three flutter_map surfaces now render OmiMapPreview instead - zero remaining tile traffic, no keyed tile provider in the app: - conversation map page: static preview of every ~100m cluster anchor (tap opens the native map app) above a grouped 'conversations at this place' list; single-conversation places open the conversation directly, multi-conversation places keep the cluster bottom sheet; grouping logic and cluster-row keys unchanged - daily summary card: 96px preview strip of the day's pins in the recap carousel (the highest-frequency map surface) - now one cached server-side render per distinct pin set instead of dozens of tiles per card - daily summary detail 'Your Day's Journey': 200px preview; image tap still opens the day's first stop (Apple Maps cannot take waypoints via map_launcher), per-stop timeline rows unchanged flutter_map is removed from pubspec (latlong2 stays - journey grouping uses it). The client-embedded Env.googleMapsApiKey goes with it: all static maps come from the server-restricted key behind the proxy now. envied outputs regenerated; test EnvFields stubs updated to match. Verified: bash test.sh / flutter test -> 1758 passed, 5 skipped, 0 failed; scripts/analyze_ratchet.sh passed (9 lint counts improved). * test(app): update map-surface tests for static previews - daily summary card: assert the preview strip, proxy URL pins/quantize/ dedupe, offline canvas fallback, and no-map-for-invalid-coordinates (was: tile provider request counting) - daily summary detail page: drop the TileProvider seam (widget no longer takes one) - conversation map groups: group-card key + cluster sheet rows replace marker keys; journey/env stubs gain the EnvFields change Verified: flutter test on all four files -> passed. * test(backend): stub utils.conversations.location in the cloud-tasks loader pipeline.py now imports async_resolve_geolocation at module scope; the cloud-tasks stub loader replaces its parent packages with MagicMocks, so the submodule import fails in file isolation (the earlier green run leaned on a module cached by another test file). Adds the explicit stub with an identity passthrough - the real resolver's miss behavior. Verified: BACKEND_UNIT_TEST_FILE_LIST=<this file> bash test.sh -> 91 passed. * chore(app): drop GOOGLE_MAPS_API_KEY from the env template The client no longer embeds a Maps key - all static-map renders go through the server-restricted key behind GET /v1/static-map. * test(backend): provide redis r on the usage-tracking stub set external_integrations now imports utils.conversations.location (daily- summary address fill), which imports the redis client symbol; the usage-tracking stub environment replaces database.redis_db with an empty module, so the import fails in file isolation. Give the stub an r. Verified: BACKEND_UNIT_TEST_FILE_LIST=<this file> bash test.sh -> 21 passed. * docs(backend): note the static-map provider URL budget with current limits * chore(backend): declare the static-map route policy in the manifest * fix(backend): drop the redundant isinstance on the typed pins parameter * fix(app): hold the preview canvas until the auth header resolves The first build fired the authed proxy request before getAuthHeader() resolved; CachedNetworkImage keys its cache by URL, so the later header-arriving setState never re-fetched - a permanent 401 fallback canvas. The widget now renders the pin-dot canvas (which already doubles as the error/offline path) until the header resolves, and only then mounts the network image with the Authorization header attached. The header resolver is injectable (authHeaderProvider) so tests control when it completes; an explicit imageUrl (test seam) skips the gate. Tests: new regression case asserts no CachedNetworkImage while auth is unresolved and the authed image after the completer fires; the card tests repoint URL assertions at the preview widget's pins (URL building has its own unit tests). Verified: flutter analyze clean; flutter test (full) -> 1759 passed. * fix(app): restore the conversation_map_marker automation key on group rows The per-place tappable key predates the static preview; keep it stable for automation (the PR brief promises preserved keys). Verified: flutter test test/unit/conversation_map_groups_test.dart passed. * fix(backend): normalize static-map dimensions, dedup render stampedes, count canvas fallbacks Review findings on the static-map proxy: - Dimension handling: one proportional scale factor (min(1, 640/w, 640/h)) computed once in fetch_static_map replaces the independent per-axis clamps - aspect is preserved and every request that differs only by scale normalizes onto the SAME cache entry (cache key and provider URL both use the effective size). - Stampede dedup: after a miss, a per-key render lock (r.set nx, 30s TTL) elects one renderer; concurrent misses poll the cache (0.25s interval, 15s budget) for the holder's result. Lock-held-timeout and lock-unavailable (Redis broken) both fail OPEN to an unlocked render - a lost lock never becomes a 502, and a broken Redis never pays the 15s wait budget. - record_fallback (component=static_map, provider_static_map -> client_pin_canvas, outcome=degraded) fires before the 502 so the degrade is counted in the shared telemetry. Tests: proportional normalization + shared cache entry, oversized fetch normalizes (size=640x150), concurrent misses render once, waiter polls a foreign lock holder to its hit, wait-timeout renders unlocked, 422s for out-of-bounds width/height via TestClient (Query contract), fallback telemetry kwargs. Verified: pytest tests/routers/test_static_map.py -> 21 passed; pyright (scripts/typecheck.sh) 0 errors, no findings in changed files. * fix(backend): cap daily-summary geocode attempts; boost-exempt static_map:get - The read-time address fill now bounds geocode ATTEMPTS at 10 per summary generation: cache hits are cheap but attempts are the deterministic wall-clock bound (10 x the geocoder's 10s worst case stays inside the job budget). Pins past the cap keep an empty address and the app's 'Unknown' fallback. Test: 11 empty-address pins -> 10 filled, 11th untouched, 10 geocoder calls. - static_map:get joins the boost-exempt rate policies: under prod's RATE_LIMIT_BOOST the 240/h hot-loop cap would otherwise resolve to 24k/h and stop protecting the billable provider calls. Verified: pytest test_daily_summary_location_address_fill.py -> 5 passed; utils.rate_limit_config import shows static_map:get in BOOST_EXEMPT_POLICIES. * test(backend): self-contained enrichment fakes; model the resolver short-circuit - test_sync_geolocation_enrichment no longer imports the transcription-prefs fixture module: sharing it loaded real heavyweight modules (GCS/Firestore protos, anthropic/jiter) inside the stub window, and stub_modules' teardown evicted them so later files broke on re-import (duplicate proto registration, jiter NameError) when run in one process. The fakes are now minimal and self-contained - every heavy leaf is stubbed, light enum modules (stt.outcomes, sync.lanes, sync.telemetry) stay real. - The None-geolocation test now models the real resolver's short-circuit (falsy input returns immediately, attempts recorded only for truthy input) and asserts: resolver called exactly once with None AND zero geocode attempts. Docstring states what is actually proven. - One-line caveat at the cloud-tasks loader injection site: new submodule imports must be added to heavy_deps explicitly (MagicMock parents do not resolve submodules). Migrating that hand-rolled loader to stub_modules would cascade across its 37 call sites - deliberately left as-is. Verified: pytest on the combined focused group (static_map, summary fill, sync enrichment, cloud tasks, sync v2, transcription prefs, geocode resolve) -> 372 passed in one process. | 10 天前 | |
fix(app): reduce and diagnose iOS background energy (#11892) * fix(app): reduce and diagnose iOS background energy Stop the iOS-only foreground-task engine and unused app-refresh request, gate reconnects and UI samplers on active work, limit diagnostic RSSI polling, and throttle native battery-history writes. Add one background-session resource event with Dart and native offline-writer counters plus BLE failure context. Failure-Class: none * style(app): dart-format changed files for CI formatting check Run dart format --line-length 120 over the PR's changed non-generated Dart files (home page diagnostics, capture provider, capture controller, capture provider test) to satisfy the repo-checks Formatting gate. Failure-Class: none * fix(desktop): project screenshot embeddings in backfill query * chore(desktop): record internal backfill test fix * fix(ios): preserve BLE battery throttle across relaunches * test(ios): cover rehydrated battery throttle --------- Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> | 27 天前 | |
Enable memory currency, evidence weighting, and history across Beta (#13953) * feat(desktop-windows): add beta memory temporal reads and use feedback * fix(hooks): verify web formatters against pinned Bun lockfiles * feat(memory): add beta desktop and web history controls * feat(memory): enable beta belief evidence and temporal reads * feat(memory): guard Windows history use controls * feat(memory): stabilize beta client pagination and feedback * feat(memory): bound beta empty-query retrieval * feat(memory): add Flutter beta currency history and use controls * feat(memory): classify the belief automation pause setting * feat(memory): register owner use feedback route policy * feat(memory): require belief settings on both ingestion hosts * feat(memory): preserve filtered ledger metadata without rebuilding keys * fix(checks): distinguish canonical metadata from legacy memory mechanisms * test(memory): make chat fixtures exercise bounded cursor reads * Align desktop Beta deployment and generated schema contracts * Update isolated memory router and evidence fixtures * Recognize archive exclusion structurally in default-read guard * Keep Windows memory capability state compatible with React compiler * test(memory): pin belief automation pause literal in dev pusher contract The dev pusher runtime contract already registers MEMORY_BELIEF_AUTOMATION_PAUSED='false' (and the prod counterpart), and the rendered deployment binds it consistently; the pinned literal snapshot in test_verify_pusher_config_references.py was not updated. Also grandfather the render+deepcopy-heavy literal-policy test in the duration allowlist next to its siblings from the same file. * fix(memory): route owner use feedback through the customer data plane POST /v3/memories/{id}/use is registered on desktop-backend, whose memory items live in the configured customer data plane (OMI_FIRESTORE_DATA_PLANE_ PROJECT is mandatory there). The route handed the compute Firestore client to the canonical mutation adapter, so valid feedback could report missing memories or mutate the wrong project. Follow the jit_ledger_snapshot / jit_rollout precedent and resolve get_data_plane_firestore_client() at request time; pin the seam with a route contract test. * fix(memory): sign ledger-history continuation before the 501-row sentinel read_ledger_history_page set next_start_after to the sentinel row itself: the next keyset page started strictly after it, so the row at the sentinel position was permanently skipped whenever it was an eligible history row. Stop the scan at the sentinel and keep the continuation key on the last row considered for emission, so the next page rescans the sentinel row. Covers search_ledger_history_page, which pages over the same provider. * fix(memory): harden belief backfill resume and evidence eligibility Four correctness fixes from PR review triage: - _default_item_reader passed a bare string id as the Firestore __name__ cursor; every real resume failed at query build. Build the DocumentReference like every sibling scanner (review_queue et al). - Checkpoint completed[] entries were terminal without comparing the stored revision: an owner-edited unknown/skipped row was never reclassified. Invalidate stale entries so reruns reclassify them. - A dry-run --checkpoint preview cached classifications but advanced the cursor past the page, so the later --apply run stranded the cache forever. Record each cached entry's page-start read cursor and have an apply run re-read the earliest cached-but-unapplied page; dry-run cursor advance (past unreadable rows) is unchanged. - eligible_record admitted pending/blocked processing states into belief corroboration; require processing_state == processed, mirroring the canonical read fence. * fix(memory): close review-verified Beta correctness gaps in backend Six fixes from PR review triage, each with a focused test: - Offset fallback for temporal views post-filtered one already-paged released window, dropping history rows and underfilling pages; pass view/as_of into MemoryService.read so admission precedes slicing. - The use-feedback route leaked 500s for MemoryFirestoreApplyError and CanonicalMemoryIntakePausedError (not RuntimeError subclasses); map intake pause to 503 and apply-store fences to 409 like jit_rollout. - The retrieval tool's temporal loop dropped page.truncated when a budget-truncated page had no continuation cursor and reported a partial scan as complete; propagate truncation and stop. - A non-string arguments.decision from the model raised TypeError in set membership and discarded the whole observation batch; drop it like any invalid decision instead. - The daily-sweep add path omitted candidate arguments from its LedgerWrite, losing proposed decisions and scoped qualifiers; pass them through like the amend path. - Canonical conversation capture dropped candidate predicate/arguments; carry them onto the persisted Memory. * fix(web): coalesce use-feedback refresh and stop dropped view changes Four review-verified fixes in the memories Beta surfaces: - The canonical refresh is now single-flight: a use-feedback commit fires the cache-invalidation listener synchronously (before its caller resumes), which used to steal the fetch lock and make refresh(true) report failure after a successful server commit. Concurrent callers now join the in-flight refresh, and the invalidation effect reuses refresh() instead of a duplicate loader. - A view/category change that arrived while any fetch was in flight was consumed by prevQueryRef and then dropped by the fetchingRef guard, permanently keeping the previous view's rows. The query is consumed only when the fetch starts, re-armed via fetchIdleTick when a fetch goes idle, and responses are fenced by query as well as scope so a stale page can never land on a new view. - The memories prefetcher bails out after each await when the owner scope changed mid-flight, so one owner's response can no longer be cached under the other's IndexedDB scope. - The prefetch effect depends on the stable scopeKey primitive instead of the per-render scope object, so a rerender inside the two-second delay no longer cancels the owner's prefetch permanently. * fix(app): deleted-row guard, single-flight history load-more, banner semantics, l10n - A deleted legacy/cached row without an assessment timestamp returned isUsefulNow=true and could surface in the useful-now collection; gate the unassessed branch on !deleted. - Two rapid taps on Show-more both fetched the same offset page and each advanced _ledgerHistoryOffset, skipping the next history page; add the single-flight guard mirroring _inFlightLoad. - The history banner's unconditional excludeSemantics hid the new load-more button from screen readers; exclude only when there is no action so the button stays reachable (label merges into the banner). - Translate memoryHistory in all 48 non-English ARBs (Arabic السجل matching memoryHistoryPartial terminology) and the generated localizations. * fix(windows): honest truncation, view-switch state sync, sheet and memo fixes - The bulk pager treated a budget-truncated cursor page (no continuation) as normal completion, silently exporting/purging a partial set; detect X-Omi-List-Truncated on the break paths and fail loudly. Offset-mode recovery by rows-received still resolves. - hydrateFromDisk skipped the loaded reset when the cache list was already null, so the revalidation effect never fetched the new view; reset whenever the view differs and sync hook state on view change so stale rows drop immediately (and stay cleared on fetch failure). - The open detail sheet kept the stale suppression label after a successful allow/suppress; patch detailMemory like onEdit. - The inline onUseAction adapter broke MemoryCard memo on every parent render; pass a useCallback-stable latest-ref adapter. - Enter/Space on the feedback buttons bubbled to the card's keydown handler and opened the sheet on top of the action; stop keydown propagation on the feedback group. * fix(macos): view-consistent auto-refresh cursor; drop dead history API - refreshMemoriesIfNeeded fetched the released projection and committed its cursor, which loadMore then paginated as the selected temporal view (pages skip/mix). Route the refresh through the device-scope-aware funnel so it fetches the selected view and commits its own cursor, restoring device-scope handling and the 400 retry it bypassed. Test seam via memoriesPageFetch; regression tests assert the refresh request carries view=history and commits that response's cursor. - Remove the never-called getMemoryHistoryPage: the History surface is served by getMemoriesPage(view:), already covered by URL contract tests. * chore(app): restore Flutter ephemeral Package.swift from main The app follow-up accidentally committed flutter pub get output, which stripped plugin packages and left trailing whitespace that fails diff-hygiene. * fix(web): avoid useMemories inFlight TDZ for typecheck Assign the refresh promise through a holder so the finally clause can drop the single-flight ref without using the binding before initialization. * fix(macos): keep memory sync UserDefaults key off the inline-literal lint The auto-refresh test latched the default-scope sync flag with an inline forKey string; route it through a local binding like production. * fix(macos): wait for the memory auto-refresh seam without sleeping The new view-cursor test polled with Task.sleep, which tripped the desktop test-quality wall-clock ratchet. Yield until the fetch seam fires instead. * fix(app): restore memoryHistory English ARB template The main merge dropped the History filter key from app_en.arb, so flutter gen-l10n deleted the getter across every locale at push time. * fix(macos): make memoriesPageFetch optional for test injection The auto-refresh seam was a required closure, so `if let` would not compile. Keep production on the shared client and let tests replace it. * fix(ci): close memory-use, STT, l10n, and client lint holes on Beta Mock the data-plane Firestore client in memory-use HTTP tests. Give pusher capability fixtures a dummy SONIOX_API_KEY. Add Allow use and Dont use localization keys. Sync Windows memory view state during render; unwrap the macOS test user id. * fix(windows): sync memory view from state, not a render-time ref The previous setState-in-effect replacement still failed eslint (Cannot access refs during render). Adjust local snapshot from state during render when the requested view changes. * fix(windows): do not leak useful-now rows into the history view mock The pager issues a follow-up offset request. mockImplementationOnce let the previous useful-now fixture merge into the history result. | 1 天前 | |
fix(app): fail closed on account cutover control refresh Keep mobile cutover enforcement from leaking prior-owner state, booting HomePage under a block, or dropping generation on 503/malformed/failed refreshes. Scope X-Account-Generation to Omi API mutations, refresh before WAL recovery, and quiet quarantined offline drains. Co-authored-by: Cursor <cursoragent@cursor.com> | 1 个月前 | |
feat(app): add plan-authoritative SttModeResolver for S17 (#12904) * feat(app): add plan-authoritative SttModeResolver for S17 Basic sessions must open on-device before any billed socket once the flag is on. Read S16's transcription_allowance; do not re-derive it. Co-authored-by: Cursor <cursoragent@cursor.com> #11681 stays open and idle — this branch is from current main. * style(app): const-construct S17 decisions for the analyzer ratchet Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> | 10 天前 | |
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> | 2 小时前 | |
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> | 2 小时前 | |
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> | 2 小时前 | |
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> | 2 小时前 | |
Speaker identification: measured threshold + margin, live clip pooling, SpeechBrain retirement; carries #12531 without the onboarding-step removal (#12935) * fix: unblock speech-profile redo and STT pre-flight for already-onboarded accounts Rebased onto origin/main as a single commit. Keep both main's open_provider_selection_circuit and this PR's is_stt_available helpers, then regenerate OpenAPI clients from the rebased backend. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): fade transcript words in as they arrive on the speech-profile screens Add FadeInWordsText: a centered word Wrap where only the words appended since the previous render animate from transparent to opaque with a short stagger, existing words stay put, and a rewritten transcript re-reveals from the start. Both the onboarding speech-profile step and the Settings redo page adopt it in the next commit so the live transcript reads the same whether the words come from the server or the on-device fallback. Verification: flutter test test/widgets/fade_in_words_text_test.dart (4 passed); observed on an iPhone 16 Pro via hot reload while dictating. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1zmRTRh3DV8NsM3QUrrAB * feat(app): fall back to on-device speech recognition when server STT is unavailable The speech-profile question flow (onboarding step and Settings redo) needs a transcript only to drive the questions and progress; the voice print itself is computed server-side from the WAV uploaded at finalize(). So when the backend's streaming STT is down, transcribe on the phone instead of dead-ending: - SpeechProfileProvider gains a local-STT mode. It is entered up front when the stt-availability pre-flight fails, or mid-session after the existing three 1011 closes with no captured speech (previously STT_UNAVAILABLE). The socket becomes the existing CompositeTranscriptionSocket: an on-device polling primary (Apple speech on iOS, downloaded Whisper on Android) forwarding suggested_transcript frames to the backend listen socket in custom_stt mode, which the OnboardingHandler already consumes like server STT output. No backend change; a receiver regression test pins that seam. - iOS on-device recognition hardening (AppDelegate.swift): resolve the app's bare language code to an installed on-device locale (a recognizer built from "en" failed every request with kAFAssistantErrorDomain 1101); reply exactly once per clip on final result, error, or a 20 s timeout, keeping partial results; and expose onDeviceAvailable, which probes a silent clip so a phone with Siri and Dictation disabled (kLSRErrorDomain 201) is reported as "no local STT" instead of entering the fallback blind. - PurePollingSocket bounds each transcribe() with a 30 s timeout. A provider that never answered left the processing flag set forever and silently stopped transcription for the rest of the session; now the audio is requeued and the next tick retries. This also protects the main app's on-device mode. - When neither server nor on-device STT is available, the pre-flight dialog now says to check the connection or turn on Dictation. - Speech-profile UI: subtler mic-level glow, and the live transcript uses the new fade-in words widget. Verification: - flutter test (full suite): 1731 passed, 5 skipped; scripts/analyze_ratchet.sh passed - new tests: speech_profile_provider_test (5 fallback cases), pure_polling_test (hung-provider timeout), fade_in_words_text_test (4), backend test_onboarding_question_start (suggested_transcript reaches the transcript queue only in custom-STT mode) - live on iPhone 16 Pro against the local dev harness with the STT primary forced unavailable: session connects with custom_stt+onboarding flags, Apple on-device recognition returns the spoken answer (~180 ms per clip) and it is forwarded to the backend; with Dictation disabled the probe reports unavailable and the dialog appears. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1zmRTRh3DV8NsM3QUrrAB * feat(app): use SpeechAnalyzer for on-device speech on iOS 26 SFSpeechRecognizer's on-device mode fails with kLSRErrorDomain 201 whenever Siri and Dictation are turned off in Settings, which is what produced the "turn on Dictation" pre-flight dialog in the speech-profile fallback. iOS 26's SpeechAnalyzer/SpeechTranscriber has no such dependency: the language model is an asset the app installs itself through AssetInventory. - transcribe: on iOS 26 run the clip through SpeechAnalyzer (preset .transcription, analyzeSequence(from:) + finalizeAndFinish), falling back to the SFSpeechRecognizer path only if the analyzer throws. - onDeviceAvailable: report true when a supported locale's model is installed or installs within 8 s; a longer download keeps going in the background and the first transcribe() waits for it. Concurrent callers share one download. - SFSpeechRecognizer remains the path for iOS 15-18. Verified on an iPhone (iOS 26.6.1) with Dictation off: the speech-profile redo enters local-STT mode and transcribes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * feat(app): speech profile talks through three topics and completes on a word target Speech-profile recording (onboarding step and Settings redo) no longer walks one question at a time with a percentage bar. Instead: - A compact white-outlined card headed "Answer with your voice:" lists three topics (where you live, what you do for work, your long-term goal), and a thin bar under it fills as the user speaks. Reaching SpeechProfileProvider.targetWordCount (60 spoken words) finalizes the recording; the backend's onboarding_complete event no longer does, so "bar full" and "done" are the same moment. Omi's own question segments are excluded from the count. - The live transcript is bottom-anchored in a box exactly three lines tall above the card, so whole lines scroll off the top and nothing overlaps. - The Play button on the Settings page plays the saved profile audio in place (just_audio) and turns into Stop, instead of opening the samples page. Redo stops playback first. - Backend ONBOARDING_QUESTIONS is the same three topics, and OnboardingHandler keeps the transcript across questions so one stretch of speech can satisfy several of them. - Removed the unused percentage progress-bar widget and the "Skip this question" button; onboarding keeps "Skip for now". Tests: speech_profile_provider_test (word target fills, finalizes once, ignores Omi segments and the backend completion event); backend test_onboarding_talk_about_flow (one transcript answers every topic; the transcript is kept when it stops answering). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * feat(dev-harness): advertise local-storage links on OMI_DEV_HOST A phone built against OMI_DEV_HOST could reach the backend but not the files it links to: OMI_LOCAL_STORAGE_BASE_URL was always http://127.0.0.1:<port>/_local/storage, so playing the saved speech profile from a device failed. The harness now derives a dev_advertise_host from OMI_DEV_HOST (loopback stays the default) and uses it only for that base URL; every other service address still binds and talks over loopback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): keep the last three whole transcript lines instead of a clipped scroll The speech-profile transcript was a bottom-scrolled ListView clipped to a three-line box, so a sliver of the line above always showed at the top edge and read as cut-off text. FadeInWordsText now takes visibleLines: it replays the Wrap line breaking with measured word widths and builds only the words on the last N lines, so earlier lines drop off whole, nothing is clipped or scrolled, and words keep their reveal state while on screen. Both screens use visibleLines: 3 inside a fixed three-line, bottom-anchored area, moved a little further above the topics card. Test: fade_in_words_text_test covers short text showing everything, earlier lines dropping once the text exceeds three lines, and the shown words matching the line-break replay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): measure transcript lines with the effective text style; 40-word target FadeInWordsText replayed the Wrap line breaking with the caller's raw style, but each word's Text inherits the ambient DefaultTextStyle (font family, weight) under that style, so the replay undercounted lines and the real layout could reach four lines and draw over the topics card. Measure with the same merged style, and clip the fixed three-line area on both screens as a safety net so a stray line can never overlap the card. Also lower SpeechProfileProvider.targetWordCount from 60 to 40 so the recording finishes sooner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): keep the "<Name>'s Speech Profile" title on one line The title wrapped onto two lines for longer names; it now scales down to fit a single line instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): never clip the speech-profile transcript; raise it above the card The three-line transcript area was a fixed-height clipped box, so whenever the rendered lines ran taller than the fontSize*height estimate (text scaling, font metrics) the top line was cut off. FadeInWordsText already guarantees at most three lines, so the area now only has a three-line minimum height (scaled with the text scaler) and grows to its content instead of clipping. Both screens also keep more space between the transcript and the topics card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * feat(app): finish the speech profile after three sentences; no page-load spinner The recording now completes once the user has spoken three sentences (SpeechProfileProvider.targetSentenceCount, counted on ./!/? boundaries followed by a space or the end of the text, so "3.5" is not one) instead of a word count, and the bar under the topics card fills per sentence. The progress-bar widget is renamed SpeechProgressBar to match. The Settings speech-profile page no longer swaps its Play/Redo or Get Started buttons for a spinner while the page initialises or the STT pre-flight runs; the buttons stay put and startRecording() ignores taps until the check finishes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): play the saved speech profile on the loudspeaker The app's audio session is normally configured for recording, so tapping Play on the Settings speech-profile page routed the WAV to the quiet earpiece. Before playing, configure a playback-category session (default mode, media usage on Android) and play at full volume, so the profile comes out of the main speaker like any other media. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): start every speech-profile recording with an empty transcript Tapping Redo showed the previous recording's words (and counted them toward the sentence target) because nothing cleared the provider's transcript before a new session; only close() did, on leaving the page. initialise() now calls a new resetTranscript() first, which forgets the segments, text, progress, completion and upload flags without touching the audio storage it recreates right after. resetSegments() reuses it. Test: a completed session's transcript is gone after resetTranscript and the fresh session counts sentences from zero and can finalize again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): finish the speech profile after a pause, keeping the last sentence on screen Reaching the third sentence finalized immediately, which stopped the mic mid-utterance (the recognizers punctuate each clip, so a pause can read as a sentence end) and swapped the transcript for a spinner at once. Now: - After the target is reached the provider waits completionGrace (2 s) without new speech before finalizing, restarting the wait on every new segment, and finalizes at completionCap (8 s past the target) at the latest. Once fired it does not re-arm; resetTranscript() clears it. - Both screens keep the last three transcript lines visible through the upload and the All done state, so the final sentence lingers instead of vanishing. Tests: grace/cap timing under fakeAsync, no double finalize, and the reset test now elapses the grace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(backend): give every speech-profile recording its own conversation Tapping Redo within two minutes of the previous attempt showed last time's words as soon as the user spoke again. The new listen socket attached to the still-open in-progress conversation from the previous attempt (same source, inside conversation_creation_timeout), so combine_segments() merged the first new segment into that conversation's last segment and the merged segment, old text included, was what the client received. LiveConversationController.prepare() now always creates a fresh in-progress conversation for onboarding_mode sessions (the onboarding step and the Settings redo both set it) instead of consulting the in-progress pointer. Ordinary listen sessions are unchanged. Test: test_listen_speech_profile_fresh_conversation.py. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): cross-fade the speech-profile recording UI into a plain All done button Keeping the transcript on screen through the upload made it pop back in on its own above the spinner and the All done button. The Settings page now cross-fades (450 ms) from the recording UI (transcript, topics card, bar) to nothing while uploading and then to the All done button, which is the same black capsule with a plain white border as the other buttons instead of the gradient box. The onboarding step likewise no longer shows the transcript in its upload and All done states. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * chore(app): remove the speech-samples page and dead progress-state code The Play button now plays the saved profile in place, so the samples page and its provider became unreachable (CI dead-code ratchet). Also drop the scroll controllers and SCROLL_DOWN signal the old clipped transcript used, and the word-count progress-message state (SpeechProfileProgressState, percentageCompleted, questionProgress) nothing reads any more. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): hold the finished speech-profile recording on screen before All done After the third sentence the final words disappeared as soon as the upload began. Both screens now keep the finished recording (last words, topics card, full bar) on screen through the upload and for a further 1.5 s (allDoneHold) after the profile is saved, then cross-fade into the All done button. Onboarding's upload spinner row and its now-unused loading-text helper are gone; Skip for now hides once recording ends. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): fade the finished speech-profile recording out as one block Parts of the finished recording could change on their own before the cross-fade (the transcript and the mic disclaimer are built from live provider state that finalize() and its callbacks touch), so they did not disappear together. Both screens now snapshot the recording view (last words, no-device flag) the moment recording ends and build from that until a new recording starts, and the onboarding step's All done switch is now the same AnimatedSwitcher cross-fade as the Settings page, so the words, the topics card, the bar and the disclaimer fade out at the same time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): keep the speech-profile bar full until the finished recording fades finalize() clears the provider's text once the profile is saved, and the bar derived its value from that text, so it dropped back to zero before the cross-fade. The frozen recording view now pins the bar at full from the moment recording ends. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * feat(app): drop the speech-profile and memory-graph steps from first-run onboarding Onboarding now goes from Permissions straight to the completion screen. The speech profile is recorded from Settings instead, and the memory-graph preview (with its background graph prebuild) is gone. The two step widgets are deleted; their page indices stay as placeholders like the other retired steps so the existing page constants keep working. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): ease the mic glow shut as the finished speech profile fades The white glow behind the device graphic vanished the instant the upload began. It now stays through the upload and hold and eases down to nothing over the same 450 ms in which the recording view fades into All done. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): hold the mic glow at its last size until it eases out The glow followed the live mic level, which drops to zero the instant the microphone stops after the recording ends, so it snapped down to its resting size before the ease-out. The frozen recording view now also captures the last mic level, so the glow holds still and then eases shut with the fade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * Revert "feat(app): drop the speech-profile and memory-graph steps from first-run onboarding" This reverts commit 4ebcb3d2c773747051dfeb2d519fe88aeb1faea0. * feat(backend): tune speaker verification from measured enrollments and retire the SpeechBrain matcher Speaker identification rejected most of the owner's own audio. The verification threshold (0.45 cosine distance) was copied from a clean-studio VoxCeleb figure; an offline bench over real enrollments in the speech-profiles bucket (229 users with a current profile plus an older one, 16 with extra recordings, 60 taught persons, 400 impostors; wespeaker-voxceleb-resnet34-LM, the diarizer's /v2/embedding model) puts same-user cross-session distance at a median of 0.40-0.53 and other users at 0.93. At 0.45 the owner was rejected 37-71% of the time at a 0.0% false-accept rate; the equal-error threshold is ~0.78. Same-session audio matched at either value, which is why the old constant looked fine in demos. - New utils/stt/speaker_match.py owns the policy (numpy only, shared by the live socket and the sync pipeline): threshold 0.65, plus a 0.10 margin over the runner-up so the owner is not guessed as a taught household member. - Live sessions pool up to three recent clips per diarized speaker and decide on the centroid once 5 s of clip audio has accumulated, instead of letting the first 2 s clip that lands under the threshold stick for the session. - Both surfaces log one structured speaker_id_decision line (best, runner-up, evidence, accepted) so the prod distribution can be checked against the bench from a day of logs. - The bench scripts live in backend/scripts/speaker_id_bench for reruns; user audio never leaves the machine running them. - Retire the dead SpeechBrain speaker-identification path: modal/speech_profile_modal, utils/stt/speech_profile (zero production callers), the /v1/speaker-identification route, HOSTED_SPEECH_PROFILE_API_URL in every chart/env, the speechbrain dependency, the shared-package COPY lines in the modal image, the dev-harness and e2e fakes, and the unused ListenLimits.speaker_id_target_audio field. Drop the now-unused is_same_speaker/find_best_match/bytes helpers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(backend): keep speaker_match real in sync test isolation, allowlist its import cost utils/sync/pipeline.py now imports utils.stt.speaker_match, but the hand-maintained heavy_deps mock list in test_sync_cloud_tasks.py and test_sync_v2.py didn't know about it, so `from utils.stt.speaker_match import select_speaker_match` raised ModuleNotFoundError: 'utils.stt' is not a package once utils.stt was replaced with a MagicMock. Real-import speaker_match (pure, dependency-free, like utils.stt.outcomes) instead of stubbing it, since a MagicMock decision object would also break the %.3f log formatting on decision.best_distance/runner_up_distance. Also allowlist test_speaker_match.py::test_short_clips_are_pooled_before_a_live_decision in the fast-unit duration guard: it's the first test in the file to import routers.listen.speakers, so it amortizes that module's FastAPI router-graph import cost, same structural pattern already documented for other files in the allowlist. * fix(speaker-id): preserve distinct evidence and household ambiguity Serialize live matches per speaker, subtract previously embedded audio, invalidate late session results, and keep all enrolled candidates in sync margin comparisons before enforcing unique assignment. Validation: 396 selected backend tests passed; Python typecheck has zero errors. Five live regression cases and two sync cases failed before the fixes. Changed sync expectations follow PR #12935's measured household-confusion margin. Failure-Class: new * fix(speaker-id): include owners in household benchmark cohorts Include available owner profiles even outside legacy, additional, and impostor cohorts. Distinguish offline benchmark evidence from deployed accuracy. Validation: synthetic manifest regression passes for owners outside other cohorts and people without an owner profile. No private audio or threshold retuning. Failure-Class: new * fix(speech-profile): bound native recognition and discard stale work Use one native completion owner so availability deadlines do not wait for shared model downloads and recognition cleanup precedes timeout completion. Serialize legacy recognition callbacks on the main queue. Propagate native failures to retain audio for retry; remove the polling Future timeout that allowed overlapping work. Scope fallback availability and polling results to their recording session. Validation: full Flutter suite 1836 passed, 5 skipped; analyzer ratchet passed. Native deadline behavioral tests pass and are registered in the existing manifest. Native speech code typechecks for iOS 15 deployment with Flutter boundary stubs; no full iPhone build or live enrollment claim. Preflight passed 53 selected checks. Failure-Class: new * fix(l10n): translate speech-profile flow in every supported locale Translate the eight speech-profile keys across all 48 non-English ARBs and fill two inherited missing keys exposed by generation. Use device-neutral speech recognition guidance and regenerate localization output from source catalogs. Validation: flutter gen-l10n reports zero untranslated messages; owner-name placeholders and complete catalog coverage verified. Full Flutter suite passed. Failure-Class: new * fix(speaker-id): require persisted speech profile before the redo admission bypass cubic P1: the client-supplied speech_profile_redo flag alone proved nothing; any authenticated client could send it to skip the completed-account onboarding-provenance admission gate. The runtime now confirms the redo from durable state (an actually stored speech_profile.wav) before taking the bypass, and an unprovable claim falls through to the provenance admission, failing closed when the check errors. Adds a regression test asserting a redo claim without a persisted profile is judged by the gate. * fix(listen): gate the onboarding fresh-conversation path on server admission cubic P2: onboarding=enabled is a client hint, yet prepare() took the fresh-conversation shortcut on the raw flag even when _bootstrap refused to admit the session — a client could dodge the existing-conversation lookup with a query parameter. The path now requires the runtime's onboarding_admitted (also true for the authorized Settings redo); an unadmitted claim keeps an ordinary session's behavior. Adds a regression test for the unadmitted path. * fix(onboarding): queue segments that arrive during AI answer checks cubic P2: is_checking_answer stayed set across up to three awaited LLM calls in _check_answer, and on_segments_received dropped everything spoken in that window, so answers covering later topics could be lost. Segments received while a check is in flight are now queued and replayed when it finishes, re-entering the normal accumulate-and-timer flow. Adds a regression test. * fix(speaker-id-bench): report the production threshold and true impostor rates cubic P2 x2: score.py evaluated the retired 0.45 operating point while the README and shipped policy (SPEAKER_MATCH_THRESHOLD) sit at 0.65, making its false-reject/false-accept and live-decision numbers misleading; and cohort-C impostor distances included the current user's own owner profile when that user was also sampled as an impostor, folding owner-vs-own-person confusion into the random-impostor sweep. score.py now pins T to the production 0.65 and formats every label from it; sweep.py excludes each cohort-C user's own profile from their impostor pool (the confusion keeps its dedicated diagnostic). Owner profiles for cohort C were already added to the cohort inputs by 8126713612. * fix(speech-profile): close startup, playback, and socket adoption races cubic review follow-ups still present after 1d7a2fd917: - page.dart: _isCheckingAvailability is now held until the entire startup path exits (dialogs, codec lookup, stopDeviceRecording, initialise), not just the availability round-trip, so a second tap cannot race socket and microphone init; context/mounted are rechecked after the language dialog and before initialise. - page.dart: profile playback deactivates the activated audio session on every teardown path (stop, natural completion, failure after activation, disposal) instead of leaving media routing active. - speech_profile_provider: a socket created while the session was closed or reset is discarded instead of adopted, which previously leaked a live backend session stop() never saw. - transcription_service: the speech-profile on-device fallback forwards raw audio per config.sendRawAudioToOmi, matching the conversation composite, instead of hardcoding every frame onto the Omi socket; suggested transcripts still flow and keep the backend session clock alive. Pinned by a factory test. * docs(app): keep AGENTS.md within its lean-budget ratchet after the main merge The merge combined this PR's on-device speech pointer with main's profile-build-mode and batch-contract lines, pushing app/AGENTS.md past its agents-md-lean budget (11747 > 11500 bytes). Tightens wording without dropping any fact: the batch-writer guarantee detail lives in the manifest reason and the ruby test itself; the other compressions are same-fact rewording. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Nathan Cheng <nathanjcx@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Nathan <nathan@Nathans-MacBook-Air.local> | 8 天前 | |
Add app_globals.dart: extract globalNavigatorKey from MyApp Breaks the transitive dependency on main.dart so that tests and providers no longer pull in codegen-dependent env files. Fixes #5935 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 5 个月前 | |
merge: integrate current main into web parity Resolve the manifest and brand-check conflicts while retaining both branches’ active CI coverage. Tighten the hex token boundary so the merged brand test rejects embedded literals. Failure-Class: none | 28 天前 | |
chore(app): drop unreachable switch defaults Removes both unreachable_switch_default diagnostics. Each switch exhaustively covers its enum, so the default clause was dead: - flavors.dart F.title: Environment has exactly prod/dev, both cased. Exhaustiveness is now compiler-enforced — adding a new Environment value becomes a compile error here instead of silently falling into the old default. - image_utils.dart rotateImage: ImageOrientation fully cased; dropped the default label from the orientation0 case (same body kept). Verification: - dart analyze: UNREACHABLE_SWITCH_DEFAULT -> 0 (was 2), no ERROR-severity diagnostics, no other rule changed. - app/scripts/analyze_ratchet.sh: only unreachable_switch_default improved (2->0), no rule regressed. - --update-baseline: git diff shows only that key dropped. - bash app/test.sh: 763 passed, 0 failed. | 2 个月前 | |
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> | 2 小时前 | |
fix(desktop): resolve CI format/ratchet drift swift-format 602.0.0 reformats CanonicalMemoryAtlasView.swift (nearestNode signature wrapping) and RewindDatabase.swift (paren elision), raising their line counts to 4409 and 3547 respectively; update the product-file-line-count baselines to match. dart format --line-length 120 (Dart 3.12.2 from Flutter 3.44.8, matching CI) reformats 73 changed app/ files to the package-resolved formatter output. The prior format pass used Dart 3.12.0 without resolved analysis_options, which produced different wrapping. Verification: - swift-format-wrapper.sh lint-scope: clean - xcrun swiftc -parse on both Swift files: exit 0 - dart format --set-exit-if-changed --output=none (3.44.8): 0 changed - check_product_file_line_count_ratchet.py: OK | 1 个月前 | |
fix(app): reduce and diagnose iOS background energy (#11892) * fix(app): reduce and diagnose iOS background energy Stop the iOS-only foreground-task engine and unused app-refresh request, gate reconnects and UI samplers on active work, limit diagnostic RSSI polling, and throttle native battery-history writes. Add one background-session resource event with Dart and native offline-writer counters plus BLE failure context. Failure-Class: none * style(app): dart-format changed files for CI formatting check Run dart format --line-length 120 over the PR's changed non-generated Dart files (home page diagnostics, capture provider, capture controller, capture provider test) to satisfy the repo-checks Formatting gate. Failure-Class: none * fix(desktop): project screenshot embeddings in backfill query * chore(desktop): record internal backfill test fix * fix(ios): preserve BLE battery throttle across relaunches * test(ios): cover rehydrated battery throttle --------- Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> | 27 天前 | |
merge: integrate current main into web parity Resolve the manifest and brand-check conflicts while retaining both branches’ active CI coverage. Tighten the hex token boundary so the merged brand test rejects embedded literals. Failure-Class: none | 28 天前 | |
fix(app): four iOS startup fixes — CGNAT dev endpoints, and hangs that presented as a blank splash (#11652) * fix(app): accept Tailscale's CGNAT range as a local-dev API endpoint Env._isLocalDevelopmentApi allowed loopback and RFC 1918 only. Tailscale assigns addresses from 100.64.0.0/10 (RFC 6598 shared address space), which is neither, so validateApplicationStartupRouting() threw before any network call: StateError: Profile local_dev requires a loopback or private-network API endpoint; use mobile_beta for https://api.omiapi.com/. main() wraps _init() in runZonedGuarded whose handler only calls debugPrint, so runApp() was never reached and the app sat on the launch storyboard forever with no diagnostic. It presented as a network hang and cost about a day: five connectivity hypotheses were investigated and disproven before the guard was found, because the failure never gets as far as opening a socket. This is not an exotic configuration. The local dev harness binds loopback only by design (scripts/dev-harness/dev_harness/safety.py:194 is a fail-closed guard, and it holds real provider API keys), so a physical device cannot reach it on 127.x and cannot reach it over the LAN either. A tailnet address is in practice the only route from real hardware to a developer's harness. Measured on iPhone 17 Pro / iOS 27.0, one variable at a time: OMI_API_BASE_URL | reachable | result --------------------------------|------------------|------------- loopback default | no | sign-in screen http://100.105.2.5:8000/ | yes, responds | splash hang http://100.105.2.5:9999/ | nothing listening| splash hang The dead-port run is the discriminator: no responding server is required, which is what pointed at a validation guard rather than a connection. Bounded to the real /10 — second octet 64..127. Tests cover both directions, including 100.63.x and 100.128.x staying rejected so this cannot silently become "any 100.x host". Verification: flutter test test/unit -> 755 passed (the new case fails without the fix, with exactly the StateError quoted above). INV-DATA-1: this widens the local_dev branch only. Production-family routing is unchanged — production keeps its own exact-match branch, and the invariant's MUST NOT clauses all concern production-family endpoints. Failure-Class: none Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ * fix(app): show why startup failed instead of a blank splash screen _init() runs inside runZonedGuarded, whose handler logs with debugPrint and nothing else. Any throw before runApp() therefore left the launch storyboard on screen indefinitely: no UI, no crash, no message — and debugPrint is invisible in profile and release builds, which is where this happens on real hardware. The failure mode is worse than the failures it hides. The preceding commit fixes a startup guard that threw a precise, actionable StateError naming the exact misconfiguration; presenting that as a frozen splash screen is what made it expensive to diagnose. Five connectivity hypotheses were investigated on device before anyone read the guard, because the app looked hung rather than rejected. _init() failures are now caught explicitly, recorded to Crashlytics when Firebase got far enough to initialise, and rendered by StartupFailureApp. StartupFailureApp is deliberately dependency-free: no providers, no services, no theme lookups, no localisation. Everything it could depend on is precisely what may have just failed to initialise, so it renders from nothing but Flutter. The message is selectable because the situation it exists for is a device with no debugger attached; the stack trace is included in debug and profile builds only. Behaviour is unchanged on the success path — runApp(const MyApp()) still runs exactly as before, and the zone handler is untouched for post-startup errors. Verification: flutter test test/unit -> 755 passed, plus 3 new widget tests covering the rendered message, rendering with no ambient scope at all, and selectability. INV-DATA-1: app/lib/main.dart is a path glob of this invariant. No routing behaviour changes — this only alters what is displayed when startup validation has already failed and the app would otherwise show nothing. Failure-Class: none Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ * fix(app): bound the startup auth refresh so a stall cannot hold the first frame _init() awaited AuthService.getIdToken() with no timeout, before runApp(). Any stall there leaves the launch storyboard on screen indefinitely: no crash, no UI, no diagnostic, and nothing a user or developer can act on. This is not hypothetical. Measured on iPhone 17 Pro / iOS 27.0 against a Firebase Auth emulator on a non-loopback host, FirebaseAuth's forced token refresh never returned at all — not an error, just silence. File-backed probes recorded entry to _refreshIdTokenWithRetries with every guard passing (uids matched, session not expired, generation current) and then no further line, ever. Worse, the app could not be launched again until it was deleted, because the cached session made every subsequent start hang on this same call. Note the startup error screen added earlier does not help here: it catches throws, and this is a hang. A timeout is treated as "not authenticated" rather than as an error, because that is what the surrounding code already does — getIdToken() returns null on every failure branch and startup already continues to the sign-in screen. So this only makes a hang behave like the failure it effectively is, and never blocks startup where the previous code would have proceeded. Ten seconds is long enough not to sign out a user on a slow network. resolveStartupAuth lives in its own library, free of Firebase and generated-env imports, so it is testable without running codegen — the same reason startup_routing.dart is separate. Importing main.dart in a test fails on the envied-generated prod env. Implemented as a race rather than Future.timeout(onTimeout:). onTimeout must return the *concrete* future's type, so a callback producing Future<String> rather than Future<String?> makes `() => null` throw a TypeError at runtime, turning a hang-guard into a new startup crash. A test caught exactly that during development and now pins it. Verification: flutter test test/unit -> 762 passed, including 4 new. The guard test supplies a future that never completes and asserts startup still resolves. Failure-Class: none Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ * fix(app): bound the forced token refresh so a stall cannot freeze every request backend/http/shared.dart:73 refreshes the id token on the way into *every* authenticated request. _refreshIdTokenOnce awaited _tokenGateway.forceRefresh() with no ceiling, so a refresh that stalls rather than fails takes the whole app offline: no error, no snackbar, no timeout, and nothing for a user or a developer to act on. Observed end to end on iPhone 17 Pro / iOS 27.0 against a Firebase Auth emulator on a non-loopback host. The app signed in, ran onboarding normally on its cached token — a clean sequence of 200s in the backend log — and then went permanently silent the moment a refresh was first required. Tapping Confirm on the language step did nothing at all: no PATCH arrived and neither the success nor the failure snackbar appeared, because the await never returned. Backend traffic stopped dead and never resumed. Each attempt is now bounded and a stall is reported as a transient failure, which the surrounding retry loop and every existing caller already handle — getIdToken() returns null on four failure branches today and callers cope with that. So this only makes a hang behave like the failure it effectively is; it never fails a refresh that the old code would have completed. Eight seconds per attempt is far above a healthy refresh and still bounded. 'refresh_timeout' is a distinct failure class rather than reusing 'transient', so a stalled refresh stays distinguishable in telemetry from one that genuinely failed. They have very different causes and very different fixes. The timeout is injectable via AuthService.forTesting, matching the existing refreshDelay seam, so the tests do not wait eight seconds. This supersedes the narrower startup-only bound in the preceding commit as the general fix; that one is kept because it also covers a stall in anything else _init() awaits before the first frame. Verification: flutter test test/unit -> 766 passed, including 4 new. The guard test supplies a refresh that never completes and asserts refreshIdToken resolves, that the result is classified refresh_timeout, that the retry loop still runs, and that a slow-but-successful refresh inside the budget is unaffected. Not a fix for the underlying stall itself: why FirebaseAuth's forced refresh never returns against the Auth emulator on a non-loopback host is still open. This makes that failure survivable rather than fatal. Failure-Class: none Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ * fix(app): let the dev build reach a CGNAT dev host over plain HTTP The dev Info.plist declared NSAllowsLocalNetworking, which permits plain HTTP only to .local, unqualified hostnames and link-local addresses. A Tailscale address (100.64.0.0/10) is none of those, so every NSURLSession request to a tailnet-hosted dev harness was refused by ATS before it left the device, with NSURLErrorDomain code=-1022. Only Firebase Auth was affected, which is what made this hard to see: it alone reaches the network through GTMSessionFetcher on NSURLSession. Every other call in the app goes through the Dart http package on dart:io sockets, which bypass ATS entirely — so the app talked to the same host on :8000 all day while token refresh failed against :9099. Swap the key for NSAllowsArbitraryLoads rather than adding it. The two cannot coexist: declaring NSAllowsArbitraryLoads alongside NSAllowsLocalNetworking makes the former a documented no-op on any OS that understands the newer key, which silently leaves CGNAT hosts blocked and makes the obvious fix look like it does nothing. Dev-only plist; release builds are unaffected. Verified on iPhone 17 Pro / iOS 27.0 against the local dev harness: a native NSURLSession probe to the Auth emulator goes from code=-1022 to status=200, network-request-failed drops to zero, and sign-in plus onboarding now complete end to end (PATCH /v1/users/onboarding 200 OK). Refs #11730 * fix(app): stop the iOS dev Info.plist generator from reverting the CGNAT/ATS fix generate_ios_dev_info_plist.sh regenerated Info-Dev.plist from scratch on every `setup.sh ios` run and unconditionally added NSAllowsLocalNetworking, silently discarding the NSAllowsArbitraryLoads fix #11652 hand-edited into the checked-in file. NSAllowsLocalNetworking only covers .local/unqualified/ link-local hosts, never a Tailscale CGNAT address (100.64.0.0/10), which is what the documented physical-device setup uses — so every setup.sh ios run silently re-broke device connectivity to the local-dev harness. Reproduced live on a physical iPhone over the real Tailscale path: every NSURLSession call failed with CFNetwork Code=-1022 (the ATS-block signature from #11730) until the generator's output was corrected. Failure-Class: none Fixes #11782. * fix(app): drop the incorrect @visibleForTesting on resolveStartupAuth main.dart calls resolveStartupAuth directly to gate the first frame, so it was never test-only — the annotation was wrong, not the call site. CI's analyzer ratchet caught the resulting invalid_use_of_visible_for_testing_member violation (baseline 0, 1 found) after this branch was pushed. Failure-Class: none --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 29 天前 | |
fix(app): survive [core/duplicate-app] instead of failing to start (#12128) Symptom: the app does not start at all. Instead of the first frame the user gets StartupFailureApp with `[core/duplicate-app] A Firebase App named "[DEFAULT]" already exists` and a stack coming out of `_init`. Why the old guard never worked. `if (Firebase.apps.isEmpty)` guarantees nothing: `Firebase.apps` is empty until the Dart side calls `initializeApp`, even when a native `[DEFAULT]` app is already running — on Android FirebaseInitProvider creates it from google-services.json before any Dart code runs, and on macOS the native SDK does the same. The real check lives INSIDE `initializeApp`: firebase_core pulls in the native apps and throws `duplicate-app` when our apiKey / databaseURL / storageBucket disagree with the native ones (firebase_core_platform_interface, method_channel_firebase.dart). The exception escaped `_init` before `runApp`, so the whole app died — while a perfectly usable native Firebase app for the same project sat right next to it. A configuration mismatch is worth complaining loudly about; it is not a reason to refuse to start. What changed: - `ensureFirebaseApp()` in the new `startup_firebase.dart` is the single initialization point: adopt the app that already exists, otherwise `initializeApp`, and `duplicate-app` is no longer fatal — the existing app is adopted and run through the same `Env.validateFirebaseProject`, so a genuinely foreign project is still caught and still fails startup. - The FCM background handler goes through the same path. It lives in a SEPARATE Flutter engine and called `Firebase.initializeApp()` with no arguments, so it could bring `[DEFAULT]` up from the platform resources with parameters that differ from the ones the UI engine uses. Both engines now resolve the parameters identically. - The logic is extracted into its own library, generic over the app type, so it is unit-testable: `FirebaseApp` has no public constructor and `main.dart` cannot be imported by a test because it depends on the generated `firebase_options_prod.dart`. - `env_test.dart`'s static wiring tripwire named the old `else` branch verbatim (`Env.validateFirebaseProject(projectId: Firebase.app().options.projectId);`), a statement that no longer exists. It now pins the same guarantee where the guarantee moved to — main.dart feeding the resolved app's `options.projectId` into `Env.validateFirebaseProject` — while the branch behaviour it could only approximate is asserted for real in the new test. Failure-Class: none | 23 天前 | |
fix: make mobile emulator and beta Firebase profiles explicit (#11273) * fix: make mobile emulator and beta Firebase profiles explicit Failure-Class: none * fix: harden mobile environment setup Disable local Crashlytics mapping uploads and address review findings around iOS beta OAuth, plist generation, setup defaults, and profile-isolated Firebase validation.\n\nFailure-Class: none | 1 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 2 小时前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 10 天前 | ||
| 27 天前 | ||
| 1 天前 | ||
| 1 个月前 | ||
| 10 天前 | ||
| 2 小时前 | ||
| 2 小时前 | ||
| 2 小时前 | ||
| 2 小时前 | ||
| 8 天前 | ||
| 5 个月前 | ||
| 28 天前 | ||
| 2 个月前 | ||
| 2 小时前 | ||
| 1 个月前 | ||
| 27 天前 | ||
| 28 天前 | ||
| 29 天前 | ||
| 23 天前 | ||
| 1 个月前 |