feat(desktop-windows): hydrate default chat thread from backend on mount - #9
Open
formed2forge wants to merge 89 commits into
Open
feat(desktop-windows): hydrate default chat thread from backend on mount#9formed2forge wants to merge 89 commits into
formed2forge wants to merge 89 commits into
Conversation
Remove unused imports, drop a duplicate MCP scope key and dead deploy helper, share the duplicated Levenshtein helper, and narrow bare excepts around datetime parsing. Update webhook/app-integration tests that stubbed the removed imports. Verification: BACKEND_UNIT_TEST_FILE_LIST covering test_memory_ingestion_text, test_verify_output_grounding, test_mcp_data_endpoints, test_async_app_integrations, test_async_webhooks — all passed. Co-authored-by: Max Carter 祁明思 <max@tsc.hk>
Delete unused memory/conversation UI remnants (including unused_element ignores and commented-out blocks) and share identical quick-edit, duration, and status pill helpers across sibling pages. Behavior preserved; Flutter SDK unavailable in this cloud VM so app tests were not executed here. Co-authored-by: Max Carter 祁明思 <max@tsc.hk>
Replace eval() with json.loads for Firebase service-account JSON, unify cm-builds under x-auth-token with HTTP status checks, and point onboarding sync tooling at desktop/macos/Desktop instead of the removed desktop/Desktop layout. Co-authored-by: Max Carter 祁明思 <max@tsc.hk>
Co-authored-by: Max Carter 祁明思 <max@tsc.hk>
Avoid growing the memory_ingestion package past its grandfathered source-file count by placing the shared Levenshtein helper in the existing ids module instead of adding a new text.py file. Co-authored-by: Max Carter 祁明思 <max@tsc.hk>
Delete the new text.py module so memory_ingestion stays within its grandfathered source-file count, and point pipeline/verify_output at ids. Co-authored-by: Max Carter 祁明思 <max@tsc.hk>
- scripts/install_onboarding_figma_sync.sh: point launchd WatchPaths at the real Resources bundle (desktop/macos/Desktop/Sources/Resources). - scripts/run_onboarding_figma_sync.sh: remove redundant -path clause for OnboardingLoadingAnimation.swift (already matched by -name 'Onboarding*.swift'). - app/lib/pages/memories/widgets/memory_edit_sheet.dart: showMemoryQuickEditSheet now accepts and forwards an optional onDelete callback instead of hardcoding a no-op. - app/lib/pages/phone_calls/active_call_banner.dart: ActiveCallTopBar uses the shared formatPhoneCallDuration helper, completing the duration-format dedup. - backend/tests/unit/test_async_app_integrations.py: test_no_threading_used inspects _async_trigger_realtime_audio_bytes bytecode (not the wrapper) and removes the brittle hasattr(app_integrations, 'threading') check.
Re-run dart format after flutter pub get so the pinned language version applies; 6 changed files now match the repo formatter. pubspec.lock picks up the same transitive meta/test_api bumps CI's pub get resolves. Failure-Class: none
Failure-Class: none
Rewrite the store-link helper as sequential ifs so Frontend Lint prettier/prettier passes after the hygiene indent change.
Hygiene cleanup dropped webhook_url_from_setting from utils.webhooks, so first-time setup treated raw Redis values as URLs and audio-bytes sends raised NameError. Parse stored settings through the helper again so ',5' and whitespace-only values stay disabled, and audio delivery can extract the endpoint. Failure-Class: none
Reproduce the revoked process-wide owner state, then establish and restore the test owner through RuntimeOwnerAuthorityTestFixture so suite order cannot turn authenticated capture into anonymous capture. Verification: RewindCaptureExclusionGenerationTests 9/9 passed; deterministic contamination recovery passed 50/50 runs; the repaired test passed inside two 5,769-test process runs (each full run retained one unrelated baseline failure). Failure-Class: FC-hand-listed-test-isolation-membership
formed2forge
force-pushed
the
feat/windows-default-thread-backend-hydration
branch
2 times, most recently
from
August 26, 2026 15:06
ec9a177 to
e85de0c
Compare
BasedHardware#11303) <!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## What changed and why High-confidence, behavior-preserving hygiene cleanup: remove proven dead code/unused imports, share identical helpers, narrow bare excepts around datetime parsing, and fix broken script paths. Net ~500 lines removed; no intentional product behavior change. ## Product invariants affected - INV-DATA-1 (merge from main brought app/lib/main.dart into diff; no intentional behavior change) - INV-MEM-4 (merge from main brought memory_ingestion files into diff; no intentional behavior change) - INV-MEM-1 (path match on memory ingestion / memories UI files; no intentional behavior change) ## How it was verified Focused backend unit tests via `bash test.sh` (file list below), script syntax checks, product line-count baseline ratchet, and `scripts/pr-preflight --pr-body-file` (27 checks passed). App Flutter SDK is unavailable on this Linux cloud VM; Dart brace balance was checked manually and pre-push used the documented Flutter/Dart skip hatches. ``` BACKEND_UNIT_TEST_FILE_LIST=... bash test.sh # test_memory_ingestion_edit_distance: 2 passed # test_verify_output_grounding: 26 passed # test_async_app_integrations: 18 passed # test_mcp_data_endpoints: 67 passed # test_async_webhooks: 22 passed scripts/pr-preflight --pr-body-file /tmp/pr-body.md # PR preflight passed: 27 checks ``` ## Tests - Added `backend/tests/unit/test_memory_ingestion_edit_distance.py` proving pipeline and verify_output share `ids.edit_distance` - Updated `test_async_webhooks` / `test_async_app_integrations` fixtures for removed unused imports - No Flutter tests run here (SDK absent); CI remains the app authority ## Failure class (fixes) Failure-Class: none ## Scoped cleanups - Backend: unused imports, duplicate MCP scope key, shared Levenshtein helper, bare-except narrowing, dead `_inject_config_map` - App: dead memory/conversation UI remnants; shared quick-edit / duration / status-pill helpers - Scripts: `eval` → `json.loads`, unified `cm-builds`, onboarding paths → `desktop/macos/Desktop` - CI: downward product file line-count baseline ratchet for shrunk backend files <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-6f28e0a4-53fb-41b4-8046-c6e2dbebd2bd?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-6f28e0a4-53fb-41b4-8046-c6e2dbebd2bd&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
Unused import introduced by BYOK refactor (BasedHardware#11454). Causes pyright error in CI.\n\nFailure-Class: none
…asedHardware#12274) The manual development backend deploy ran its post-promotion smoke against https://api.omi.dev, which has never resolved: the omi.dev zone exists but the api record is NXDOMAIN, so smoke_what_matters_now.py could not reach anything and failed with "could not reach the deployed backend". That failed the deploy after traffic had already shifted and triggered the traffic restore, rolling the promotion back. Development's real public API host is api.omiapi.com, which matches the rest of the development domain family (parakeet.omiapi.com, nllb.omiapi.com, pusher.omiapi.com in backend/deploy/runtime_env/dev.overlay.yaml) exactly as production uses the omi.me family. It serves /ready 200 today. Making api.omi.dev real was rejected: the codebase already uses it as the canonical fake hostname in mobile production-routing tests, so giving it a live record would undermine those fixtures. This defect was introduced by 933fdf7 on the same day as the probe-signer defect fixed in BasedHardware#12264, and stayed invisible because the probe failed first. Run 33008079463 is the first development deploy to reach this step. The step is guarded to the manual development lane; the production smoke keeps its own api.omi.me path untouched. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…asedHardware#11454's contract (BasedHardware#12277) * chore(desktop): unbreak the local Swift build and formatter gate on Xcode 26 Two pre-existing blockers on `main` that fail the pre-push gate for any desktop PR when the local toolchain is newer than the pinned Xcode 16.4: - `AppState+Permissions.swift` has swift-format drift, and `desktop-swift-format-lint` runs `lint-scope` over every first-party Swift file. Formatter output only. - `AppState+Transcription.swift:848` captures `alertPresenter` implicitly in an escaping closure. Swift 6.2 (Xcode 26.x) rejects this; Xcode 16.4 accepts it. Writing `self.` is what the diagnostic asks for and is the same strong capture the implicit form already produced, so behavior is unchanged on both toolchains. Failure-Class: none * chore(desktop): dodge the Xcode 16.4 SILGen segfault on alertPresenter Every desktop CI lane (Static & Test Contracts, Release Compile, Build & Tests aggregate) has been red since main's d49f978 landed `var alertPresenter: any DesktopAlertPresenting = AppKitSheetAlertPresenter()`: the pinned Xcode 16.4 toolchain segfaults (signal 11) in silgen emitStoredPropertyInitialization while lowering that existential-erasure default initializer. Reproduced on main itself (d49f978, fb67ca9, 50cf064 all failed; d06e220 passed only because its desktop jobs were path-filter skipped) and on unrelated PRs (BasedHardware#12269, BasedHardware#12272), so this is not specific to this branch. Move the initializer from the stored-property default position into init(). Identical semantics on both toolchains - AppState is @mainactor with a single designated init, and the alert tests overwrite the presenter immediately after construction. Xcode 26.6 parses, swift-format lint passes, changelog gate passes. Failure-Class: none * test(desktop): enroll BYOK fingerprints so paywall/agent tests match BasedHardware#11454's contract BasedHardware#11454 replaced the old "all keys present in UserDefaults" check for isByokActive with a stricter one: the selected provider's *current* key must match a fingerprint already persisted via APIKeyService.persistEnrolledFingerprints (set by activateBYOK reconciliation after BYOKValidator confirms the key). Seven tests across BYOKPaywallTests and AgentRuntimeProcessTests still set up state the old way — raw UserDefaults keys, no enrollment — and started failing the moment CI could actually reach them (BasedHardware#12276): main's own contract job caught this on BasedHardware#11454 before merge, but a compiler crash landed 8 minutes earlier (tracked separately in BasedHardware#12275) blocked every real desktop Swift test run afterward, so it went unnoticed. This is a test-only fix that transcribes BasedHardware#11454's already-stated and already-tested enrollment contract into the tests that never learned about it; no Sources change. - BYOKPaywallTests: add `enroll(_:)`, calling `persistEnrolledFingerprints` with the SHA-256 fingerprint of the provider's current key, exactly as `activateBYOK` reconciliation would after successful validation. Five tests were asserting on raw key presence: testByokActiveRequiresSelectedLLMKey (needs a *second* enrollment after setAllBYOKKeys() rewrites openrouter's key and invalidates the first fingerprint), testBuildHeadersAttachSelectedLLMByokKey, testBuildHeadersSuppressesOnlyInvalidByokHeader, testPaywallFlagSuppressedWhenByokActive, and testRemovingDeepgramKeyLeavesSelectedLLMByokActive (both of the last two now select the provider explicitly rather than relying on legacy first-match inference, since every provider's key is set and the test must enroll the same provider it selects). - AgentRuntimeProcessTests: enroll the selected provider in testUsableByokEnvironmentIncludesAllKeysWhenAllProvidersAreUsable and testUsableByokEnvironmentSuppressesAllKeysWhenOneProviderIsKnownBad — usableBYOKEnvironment() gates on isByokActive before the CredentialHealthManager suppression these tests exercise. Both existing `defer` blocks now also save/restore the enrollment map. Not touched: testBuildHeadersCanExplicitlyExcludeByokKeys and testLowLevelTransportDefaultsToExcludingByokKeys currently pass vacuously — neither enrolls a provider, so isByokActive is false and headers come back nil regardless of includeBYOK. Flagging for whoever owns BYOK rather than fixing here, since giving them real coverage means deciding what "excluded despite being active" should assert, and that's a product call, not a mechanical transcription of BasedHardware#11454. No assertions were weakened or removed — every fix completes test setup to match the stated contract. testPaywallFlagSuppressedWhenByokActive is the only coverage that an enrolled BYOK user is never paywalled; it was made to pass by enrolling correctly, not by loosening what it checks. Fixes: BasedHardware#12276 Failure-Class: none --------- Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com>
Auto-generated: consolidates unreleased changelog fragments into v0.12.223 and regenerates CHANGELOG.json.
…re#12267) ## Why The 2026-08 macOS churn cohort analysis (`omi-knowledge-base/projects/macos-churn-analysis/evidence/2026-08-26-macos-churn-cohort-analysis.md`) listed `chat_agent_error` as "well-powered but cannot explain itself". Measured against PostHog (project 302298, macOS, JSON extraction rather than the map subscript — see below), August 2026 `chat_agent_error`: | property | populated | | --- | --- | | `error_class` / `surface` / `harness` | 1177 / 1197 | | `error_code` | 909 / 1197 | | `root_cause` | 178 / 1197 | `error_class` is fine — schema v2 fixed that. The remaining hole is that only one of the ~15 `telemetryAttempt.fail(...)` call sites (`ChatProvider.swift:5446`) passes a `ChatQueryErrorDetail`. Every other terminal — timeout, tool stall, session setup, bridge unavailable, attachment upload, concurrent request — reached PostHog with no `error_code` at all, and `root_cause` was hardcoded for exactly one error class (`.authentication`). A typed failure existed at the catch boundary and was collapsed to a bare class name by the time it was recorded. ## What changed `ChatQueryErrorClass` now classifies itself, so the fix lands at the one place that builds the payload instead of at 15 call sites: - `rootCause` maps every class to a bounded `ChatQueryRootCause` (subsystem attribution: provider, agent runtime, bridge process, local session, network, device resources, ...). `.authentication` keeps the already-published `provider_claude` value so existing PostHog breakdowns stay valid. - `fallbackErrorCode(watchdogFired:)` gives every class a bounded code when no `ChatQueryErrorDetail` is available. It separates `watchdog_timeout` from `bridge_timeout`, which have different owners and were previously indistinguishable. - A supplied `ChatQueryErrorDetail` still wins; the fallback only fills the gap. No raw exception text, prompt, path, or message enters the payload — the values are enum raw values, per the analytics integrity contract in `desktop/macos/AGENTS.md`. ## Proof `ChatQueryTelemetryTests`: - `testEveryFailureClassCarriesABoundedCodeAndRootCause` iterates all 16 classes and fails if any emits an empty/absent code or an out-of- vocabulary root cause. - `testRootCauseAndTimeoutCodesStayActionable` pins the compatibility value for auth and the watchdog/bridge timeout split. - `testErrorDetailCodeOverridesTheClassFallback` proves the fallback cannot shadow real detail. - `testAnalyticsPayloadUsesTypedAllowlist` (existing) still pins the exact emitted key set. 37 tests pass locally. Failure-Class: FC-typed-failure-collapsed-to-generic
Hovering the notch opens the agent menu; when the pointer leaves, the
collapse resize lands while the menu's collapse spring is still shrinking
the SwiftUI content. The hosting view forwards the content's min size as a
window constraint, and auto layout grows the panel right back - from its
pinned bottom-left origin. That pushed the top-anchored island chrome up to
240pt above the screen edge, where nothing ever brought it back: the island
'disappeared' until a Push-to-Talk press happened to resize the window.
Two guards, both mechanical. windowDidResize re-anchors any notch-mode
resize whose top edge left the screen top (auto layout growth bypasses every
programmatic resize path, so the anchor is enforced at the notification, not
at call sites) - user-resizable and mid-drag windows are never fought. And
the collapse re-asserts the idle island frame once after the spring's visual
tail, so the panel returns to size instead of keeping a stale menu-height
frame.
Fixes the hover-then-vanish report on Omi macOS Beta 0.12.226.
Verification: reproduced deterministically on a dev bundle via a cursor-free
bridge seam driving the same pointer entry point the tracking view calls -
every hover cycle left the window at {{816,1263},{430,307}} (chrome 240pt
offscreen). With the fix, 20 timing patterns including rapid x15 and
re-enter-mid-collapse all settle back to the exact idle frame
{{828,1263},{392,67}}, menu open and idle states captured. 5 new geometry
tests cover the re-anchor policy, including the reproduced bug frame.
Auto-generated: consolidates unreleased changelog fragments into v0.12.227 and regenerates CHANGELOG.json.
Cross-review caught a collision: a visible remote prompt plus the third question left both bars stacked in the shared overlay. The built-in rating ask now SUSPENDS any current remote prompt (cleared without a resolution) via builtInAskChanged() on every ask state change, and evaluate() re-offers it once the ask resolves. Also registers GET /v2/desktop/prompts in the route policy manifest (baseline check was failing CI).
…rdware#12311) ## Summary On the macOS Beta, hovering over the notch island and then moving the cursor away sometimes made the island vanish entirely — it stayed gone until a Push-to-Talk press happened to bring it back. **Root cause, reproduced deterministically.** Hover opens the agent menu; on pointer exit the collapse resize (416×67, logged) lands while the menu's collapse spring is still shrinking the SwiftUI content. The hosting view forwards the content's min size as a window constraint (`sizingOptions = [.minSize, .maxSize]`), so auto layout immediately grows the panel back to menu height — **from its pinned bottom-left origin**. The island chrome is top-anchored inside the window, so it ends up to 240pt *above* the screen edge: invisible. Nothing re-anchors it; PTT "fixed" it only because its resize recomputes the frame. Reproduced on a dev bundle with a cursor-free bridge seam driving the same `updateNotchPointer` entry the tracking view calls — every hover cycle ended at `{{816,1263},{430,307}}` (top edge 1570 on a 1330-high screen). **Fix, two mechanical guards:** 1. `windowDidResize` re-anchors any notch-mode resize whose top edge left the screen top (`FloatingControlBarGeometry.notchTopReanchoredFrame`). The buggy growth comes from auto layout, which bypasses every programmatic resize path — so the invariant is enforced at the notification, not at call sites. Resizable (user-sized conversation) and mid-drag windows are never fought; an epsilon prevents setFrame churn on AppKit rounding. 2. The menu collapse re-asserts the idle island frame once after the spring's visual tail (0.45s), so the panel returns to size instead of keeping a stale menu-height frame. A re-hover cancels the re-assert. Also adds the `notch_hover` bridge action (non-prod) used to reproduce and verify without a cursor. ## Verification - **Before:** every hover cycle across 20 timing patterns left the window at `{{816,1263},{430,307}}` — chrome 240pt offscreen (the user-reported disappearance, on demand). - **After:** the same 20 patterns — five enter-hold durations × four exit delays, rapid ×15, and re-enter-mid-collapse ×10 — all settle back to the exact idle frame `{{828,1263},{392,67}}` with `notchRevealProgress=1`, alpha 1. Menu-open and post-hover idle states captured from the running bundle (`.cross-review-verify.png`). - `swift test --filter NotchTopReanchorTests` — 5 passed, including a case built from the reproduced bug frame. - `swift build -c debug` — Build complete. ## Product invariants affected - **INV-CHAT-1** — unchanged. The touched files carry chat-adjacent surfaces, but this diff only re-anchors the notch window frame and re-asserts its collapse size; no chat journaling or routing changes. Failure-Class: none Line-Count-Exception: desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift | 4764 -> 4791 | one cursor-free notch_hover QA action, following the file's established registry pattern Line-Count-Exception: desktop/macos/Desktop/Sources/FloatingControlBar/FloatingControlBarWindow.swift | 5309 -> 5384 | the top-edge re-anchor and collapse re-assert live in the notch panel's owner beside the resize paths they guard; extracting the window is out of scope <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/BasedHardware/omi/pull/12311?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
…t cycle Users who already asked 3+ questions before this build now see the rating ask on their next launch instead of after three MORE questions: a one-shot seed reads the real chat history (GET v2/desktop/messages, sender=human) with launch-timing retries; fetch failure leaves the marker unset so the next launch retries. Verified live on a dev bundle: reset -> seed -> bar armed with zero new questions (capture in PR). Also breaks the RatingPromptManager/RemotePromptEngine singleton init cycle (refresh() during static-let init synchronously reached back into .shared — the deferred MainActor hop removes the startup deadlock the reviewer flagged; the exclusivity walk passes live: remote prompt suspends on the 3rd question and restores unresolved after the ask resolves). Failure-Class: none
…n ledger The engine kept an in-memory counter that reset every launch, so an 'after Nth question' prompt demanded N fresh questions per session and ignored the history seed. questionCount is now a read of the SAME persisted key the rating prompt and history seed write — one ledger, relaunch-proof. Regression tests (RemotePromptLedgerTests, 2): a persisted count of 3 arms a question_count prompt on the relaunch-time fetch with zero session questions; a count of 2 stays hidden. Auth check made injectable so the tests exercise the real fetch path. Failure-Class: none
…asedHardware#12309) ## Why Every in-app ask (rating bar, surveys, announcements) previously needed a desktop release and days of channel rollout. This adds the remote prompt engine: prompts are authored on **admin.omi.me → Prompts** and reach every desktop client within one poll (~5 minutes) — created, targeted, killed, and measured with zero releases. ## What - **Backend** — `GET /v2/desktop/prompts` (new `routers/desktop_prompts.py`): serves active `desktop_prompts` Firestore docs with stable per-(user,prompt) rollout buckets, channel and min-build audience filters. Read-only over admin-authored docs; standard user auth. - **Desktop** — `RemotePromptEngine`: polls at launch + every 5 min, renders `stars` / `nps` / `choice` / `banner` natively above the composer, one prompt at a time, once per user (persisted resolution). Question-count triggers ride the same accepted-question seam as the built-in rating ask, which keeps right of way. Answers/dismissals land in PostHog (`Desktop Prompt Shown/Answered/Dismissed`). A prompt deactivated on admin disappears from live clients on the next poll — the no-release kill path. - **Admin** — `/dashboard/prompts` page (sidebar: Prompts): create (born inactive), activate/deactivate, rollout %, delete, with live per-prompt tallies (shown / answered / dismissed, stars-NPS averages, per-option counts) from PostHog. - Bridge actions `remote_prompts_state/refresh/reset`, `remote_prompt_answer/dismiss` drive the same engine methods the visible controls call; e2e flow `remote-prompts.yaml`. ## Verification - Units: backend 7/7 (`test_desktop_prompts.py` — audience, rollout stability, spec validation), `RemotePromptPolicyTests` 5/5 (trigger thresholds, once-only, unknown-kind ignored, deterministic ordering), admin vitest 198/198 (incl. `normalizePrompt` contract), `tsc` clean. - **Live end-to-end on a dev bundle** (`omi-rating-qa`, real seeded account): created a stars prompt through the real admin POST route (prod Firestore) → real-auth backend route served it → running app fetched it (`spec_count 1`) → hidden until the 3rd accepted question, and the built-in rating ask correctly took right of way first → after resolving it the remote bar rendered ("How useful was Omi today?" with stars, window capture) → bridge answer(4) resolved it via the same method the star buttons call → refresh does NOT re-show → **deactivating on admin removed it from the live client on the next fetch** → deleted (no test residue). - UNVERIFIED (narrow): the Prompts page UI in a browser (admin login wall blocks headless); its API routes were exercised for real above. Will screenshot on prod admin after deploy. Failure-Class: none Line-Count-Exception: desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift | 4764 -> 4765 | one registration line for the remote-prompt action group (actions live in their own extension file) Line-Count-Exception: desktop/macos/Desktop/Sources/MainWindow/DesktopHomeView.swift | 1643 -> 1650 | remote-prompt overlay slot, engine start, and history-seed hook; the features live in RemotePrompts.swift / RatingPrompt.swift Product invariants affected: INV-NAV-1 — DesktopHomeView changed only by adding a bottom overlay slot and an engine-start hook for remote prompts; no navigation destination, route owner, or shell chrome changed. ## Round 3 - **Instant ask for existing users** (Nik): the question counter seeds one-shot from real chat history — users with 3+ lifetime questions see the rating bar on next launch, zero new questions needed. Live-verified: reset → seed → bar armed (capture). - **Init-cycle fix** (review): `refresh()` inside `RatingPromptManager`'s own `static let` init reached `RemotePromptEngine` which read `.shared` back — now a deferred MainActor hop; the pre-fix trap reproduced in a test process, post-fix the live app runs clean. - **Slot exclusivity live-verified** (review): remote banner current → 3rd question arms rating bar and remote prompt suspends (`current_id` empty) → resolving the ask restores it unresolved. Captures attached in-repo for review.
…ions Cross-review caught the BasedHardware#9821 account-switch-bleed class: every prompt key (question count, submitted rating, dismissed, history-seed marker, remote prompt resolutions) was device-global, so one account's answer suppressed prompts for every later account on the Mac. All state now lives under owner-scoped ScopedDefaultsKeys; the first scoped access migrates the pre-scoping global keys to the signed-in account (single-account Macs keep their state, nobody gets re-prompted) and removes them. The remote engine's question count is now literally RatingPromptManager.questionCount — one owner-scoped ledger accessor. PromptStateAccountScopingTests (3): rating submission does not suppress the next account and survives switching back; remote resolutions are per-account; legacy global state migrates to the first account only. Full prompt suites 23/23. Failure-Class: none
…tions) (BasedHardware#12312) ## Why Cross-review caught the BasedHardware#9821 account-switch-bleed class in the new prompt surfaces: all rating/remote-prompt state was device-global, so one account's answer or dismissal suppressed prompts for every later account on the same Mac. ## What Every key (question count, submitted rating, dismissed, history-seed marker, per-prompt remote resolutions) is now owner-scoped via ScopedDefaultsKey. First scoped access migrates the pre-scoping global keys to the currently signed-in account (single-account Macs keep their state — nobody who already answered gets re-prompted) and deletes the globals. The remote engine's question count is now literally `RatingPromptManager.questionCount` — one owner-scoped ledger. ## Verification `PromptStateAccountScopingTests` (3): submit on account A → account B starts clean, asks 3 questions, sees the prompt; switching back preserves A's submission; remote prompt dismissed by A is still offered to B; legacy global state migrates to the first account only, second account starts clean. All prompt suites 23/23. swift build clean. Failure-Class: none
Auto-generated: consolidates unreleased changelog fragments into v0.12.228 and regenerates CHANGELOG.json.
…Prometheus (BasedHardware#12100) Implemented-By: codex gpt-5.6-sol Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ng under T5838 AAD (BasedHardware#12101) Implemented-By: codex gpt-5.6-sol Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
142a0fe limited the chat identity mark to the newest assistant message for a quieter timeline, which left every older answer unattributed — by request, the mark follows the sender again, not recency. The last-assistant-message helper had no other caller and is retired with its tests. Verification: exercised in the running dev bundle — two chat turns (echo, foxtrot) each render the Omi mark beside the reply; 13 ChatWorkingIndicatorTests pass; screenshot in .cross-review-verify.png.
Cross-review follow-up: ownerProvider changes keyed the STORAGE per account, but cached isVisible/current stayed until the next question or flag reload. Both managers now expose ownerDidChange(); DesktopHomeView's owner-keyed task (the established transition seam) calls it on every switch, and RatingPromptManager also observes .userDidSignOut for the sign-out leg. The rating manager clears thank-you and recomputes for the new owner's keys; the remote engine drops the slot (no resolution written), re-evaluates, and refetches. Tests: two new PromptStateAccountScopingTests drive ownerDidChange with a VISIBLE prompt — armed rating bar disappears for account B and returns for A; a remote banner survives the switch unresolved and B's dismissal never sticks to A. Prompt suites 25/25. Failure-Class: none
…Hardware#12314) ## Why Follow-up to BasedHardware#12312 (per-account prompt state): storage was owner-scoped, but the cached `isVisible`/`current` published state persisted across an account switch until the next question or flag reload. ## What `RatingPromptManager.ownerDidChange()` (clears thank-you, recomputes from the new owner's keys) and `RemotePromptEngine.ownerDidChange()` (drops the slot without writing a resolution, re-evaluates, refetches). DesktopHomeView's owner-keyed `.task(id: currentOwnerId)` — the app's established owner-transition seam (same pattern as the chat-first capability task) — invokes both on every transition; the rating manager additionally observes `.userDidSignOut` for the sign-out leg. ## Verification Two new `PromptStateAccountScopingTests` exercise the transition with a VISIBLE prompt and no manual flag reload or extra question: the armed rating bar disappears the moment the owner flips to B and returns when it flips back to A; a visible remote banner survives the switch unresolved, B's dismissal doesn't stick to A. All prompt suites 25/25; swift build clean. Failure-Class: none
…#12315) ## Summary By request: `142a0fe44e` (Jul 27) limited the chat's Omi identity mark to only the newest assistant message, which left every older reply unattributed. The mark now follows the sender again — every Omi reply carries it, app personas keep their own avatars, user bubbles are unchanged. The last-assistant-message helper had no remaining caller and is retired together with the tests that asserted the old gating; the replacement test asserts the sender-based rule. ## Verification - Running dev bundle, real chat: two turns ("Say only: echo" / "Say only: foxtrot") — **both** replies render the Omi mark (screenshot: `.cross-review-verify.png`). - `swift test --filter ChatWorkingIndicatorTests` — 13 passed. - `swift build -c debug` — Build complete. ## Product invariants affected - **INV-CHAT-1** — unchanged; presentation-only (which rows show the identity mark), no journaling or routing changes. - **INV-CHAT-2** — unchanged; the transcript's message ordering, sessions, and content are untouched — only the per-row avatar gate changed. Failure-Class: none <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/BasedHardware/omi/pull/12315?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
…failure Cross-review: setActive flipped the switch optimistically and never checked response.ok, so a failed PATCH falsely showed a prompt as activated or — worse — killed. The PATCH now goes through patchPromptActive, which reports HTTP and network failures; the page rolls the optimistic flip back and surfaces the error. Tests: desktop-prompts-toggle 3/3 (success, failed HTTP status, network throw all reported honestly). Failure-Class: none
…ure (BasedHardware#12316) Cross-review finding on the new Prompts page: `setActive` flipped the switch optimistically without checking `response.ok` — a failed PATCH falsely showed a prompt as activated or (worse, it is the kill switch) deactivated. The PATCH now goes through `patchPromptActive`, which reports HTTP and network failures; the page rolls the optimistic flip back and surfaces the error inline. ## Verification `desktop-prompts-toggle` vitest 3/3: success passes through; failed HTTP status returns `toggle failed (502)`; a network throw returns its message. `tsc` clean. The happy path (activate/deactivate against prod Firestore) was exercised live in the browser earlier today. Failure-Class: none <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/BasedHardware/omi/pull/12316?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
…il the new owner's fetch succeeds Cross-review: ownerDidChange re-evaluated the PREVIOUS account's server-filtered payload (rollout %, channel are per-user), so when the new owner's refetch failed, account B could indefinitely see a prompt targeted at account A. ownerDidChange now clears current AND specs and evaluates only after a successful authenticated fetch for the new owner. Test rewritten with per-owner fetch results: A inside the rollout, B outside AND with a failing refetch — B renders nothing at the switch, after the failed fetch, and A's prompt returns unresolved on switch-back. Suites 25/25. Failure-Class: none
…s fetch succeeds (BasedHardware#12317) Cross-review finding: `RemotePromptEngine.ownerDidChange()` re-evaluated the previous account's server-filtered specs before fetching for the new UID — audience filtering (rollout %, channel) is per-user, so a user outside a rollout could see another account's prompt indefinitely when their refetch failed. `ownerDidChange()` now clears `current` AND `specs` synchronously and evaluates only after a successful authenticated fetch for the new owner; a failed fetch renders nothing. ## Verification The account-switch test now uses per-owner fetch results (A inside the rollout; B outside with a THROWING fetch): at the switch B shows nothing and specs are empty, after B's failed refetch still nothing, and switching back restores A's prompt unresolved. All prompt suites 25/25; swift build clean. Failure-Class: none
Cross-review: an in-flight fetch started for account A could complete after switching to B and overwrite specs/current with A's audience-filtered payload. refreshFromServer captures the generation before awaiting fetch() and discards the result if an owner transition bumped it meanwhile. Test: A's fetch parks on an actor gate, the owner switches to B mid-flight, the gate opens — A's payload is provably discarded (current nil, specs free of A's prompt). Suites 26/26. Failure-Class: none
…2318) Cross-review race: an in-flight `fetch()` started for account A could complete AFTER an owner switch and land A's audience-filtered payload for B. `refreshFromServer()` now captures `fetchGeneration` before awaiting and discards the result unless the generation is unchanged; `ownerDidChange()` bumps it. ## Verification New test parks A's fetch on an actor gate, switches the owner to B mid-flight, opens the gate, and proves the stale payload is discarded (`current` nil, `specs` free of A's prompt). All prompt suites 26/26; swift build clean. Failure-Class: none
Cross-review: seedFromHistoryIfNeeded fetched without expectedOwnerId and wrote through scopedKey() for whichever account was current when the request finished — an account switch mid-flight could seed B from A's history. The seed now captures the owner at start, passes it as getMessages(expectedOwnerId:) so the transport owner-assertion applies, and discards the result if the owner changed or the task was cancelled before persistence (marker stays unset; the owner's next launch retries cleanly). Test: A's history fetch parks on an actor gate, the owner switches to B, the gate opens — B's count/seed marker are untouched and the stale result is fully discarded. Suites 27/27. Failure-Class: none
Cross-review finding: the history seed fetched without `expectedOwnerId` and wrote through `scopedKey()` for whichever account was current at completion — an account switch mid-flight could seed account B from account A's history. The seed now captures the owner at start, passes `getMessages(expectedOwnerId:)` (transport-level owner assertion), and discards the result if the owner changed or the task was cancelled before persistence — the marker stays unset so that owner's next launch retries cleanly. ## Verification New regression test parks A's history fetch on an actor gate, switches the owner to B, opens the gate: B's count and seed marker remain untouched and the stale result is fully discarded (A's marker also unset for a clean retry). All prompt suites 27/27; swift build clean. Failure-Class: none
Auto-generated: consolidates unreleased changelog fragments into v0.12.229 and regenerates CHANGELOG.json.
The main chat window's default thread now loads from GET /v2/desktop/messages (the same Firestore-backed store macOS and mobile read) before falling back to local SQLite. Mac/mobile/web turns become visible in the Windows main window immediately, closing the cross-device read gap. Call sites updated: mount loader, switchThread(null), and selectApp(null) — all three entry points to the default thread now share the same loadDefaultThreadHistory helper (backend-first, local SQLite fallback). Verification: - vitest run useChat.test.tsx: 37/37 pass (R4 and R5 are new) - Full pnpm vitest suite: 5437/5437 pass Failure-Class: none Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KADKRuaPJdho9CDE7nLXQP
formed2forge
force-pushed
the
feat/windows-default-thread-backend-hydration
branch
from
August 27, 2026 21:57
e85de0c to
2fe109a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GET /v2/desktop/messages(the same Firestore-backed store macOS and mobile read) before falling back to local SQLite.switchThread(null),selectApp(null)) share a singleloadDefaultThreadHistoryhelper — backend-first, local SQLite fallback.Test plan
useChat.test.tsx: 37/37 pass (R4 and R5 are new hydration tests)🤖 Generated with Claude Code
https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p