Skip to content

fix(desktop): macOS bug audit cycle 3 follow-up — Bee audio, conversation cache, installer atomicity, floating bar, WAL sync#10047

Merged
eulicesl merged 8 commits into
mainfrom
claude/macos-bug-fixes-testing-i8z7cm
Jul 19, 2026
Merged

fix(desktop): macOS bug audit cycle 3 follow-up — Bee audio, conversation cache, installer atomicity, floating bar, WAL sync#10047
eulicesl merged 8 commits into
mainfrom
claude/macos-bug-fixes-testing-i8z7cm

Conversation

@eulicesl

@eulicesl eulicesl commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

macOS bug audit — cycle 3 follow-up (deferred low-severity set)

Follow-up to the merged cycle-3 audit (#10035). Cycle 3 fixed the high-severity crash/data-loss/leak/security findings and consciously deferred a set of lower-severity items. This PR completes the deferred findings that have a clean, hermetically-testable seam — each re-verified against current main before fixing.

Fixes

  1. Bee audio frame drift (C6)processAudioPacket popped only the first ADTS frame per BLE notification; a notification completing two frames emitted the second one notification late (~64ms/frame, cumulative) and discarded surplus complete frames on stop. Now drains every complete frame per notification. Regression test feeds a two-frame packet and asserts both drain in order.

  2. Conversation optimistic-edit revert (C24) — the cache-first load() published bare cached rows without overlaying pending optimistic mutations, so a filter change mid-mutation visually reverted an in-flight star/title/folder edit. Cache emit now overlays pendingMutations (mirrors the server merge path). Hermetic test suspends a star mutation and asserts the cache reload keeps it.

  3. Non-atomic app install (C14) — the DMG install did removeItem then copyItem; a copy failure after the remove destroyed the user's working /Applications/Omi.app. replaceInstalledApp now stages then replaceItemAt-swaps, leaving the existing install untouched on failure. Real-FileManager temp-dir tests assert survival on copy failure + successful swap.

  4. Floating-bar pill drift (C10)savePreChatCenterIfNeeded snapped the draggable pill's restore origin (computed for the glow-inflated window) using the bare pill size, drifting the recorded center ~22pt left / 18pt down per rapid re-open cycle. Now stores + snaps the full restore frame; restore-origin math routes through pure FloatingControlBarGeometry helpers with a round-trip identity test.

  5. WAL sync stuck on auth failure (C1)fetchSyncJobStatus swallowed an auth-header failure with try? (sending an unauthenticated GET) and mapped 401 → .transient, parking the WAL in a forever-retry. Now returns a distinct .unauthorized (pure forStatusCode mapping) without an unauthenticated request; the reconciler reverts to .miss for re-upload via the authenticated path (poll stays session-preserving, INV-AUTH-1).

  6. Unbounded WAL retention (C19)cleanupOldWals was dead code, so synced audio + metadata grew without bound. Now invoked at the end of each syncToCloud, selecting reclaimable entries via a pure WALCloudSyncLogic.cleanupCandidates (synced-only, past the 7-day window) using the injected clock.

Verification

  • Hermetic regression tests added for every fix (behavioral where a production seam exists; pure-function tests for the mapping/geometry/retention predicates).
  • Swift compiles/tests only in CI (no Swift toolchain in this sandbox). Local static gates green: check_desktop_test_quality.py, line-count ratchet (FloatingControlBarWindow baseline raised with justification), e2e flow-coverage (AppInstaller added to desktop-update-relaunch covers).

Product invariants affected

  • INV-AUTH-1
  • INV-CHAT-1

C1 names INV-AUTH-1 because the WAL status poll is a background, session-preserving request: on an auth failure it throws .unauthorized and reverts the WAL for re-upload through the authenticated path — it does not invalidate the Firebase session (that ownership stays with AuthSessionCoordinator). No session-death path changes.

INV-CHAT-1 is named because the C10 pill-drift fix touches FloatingControlBarWindow.swift / FloatingControlBarGeometry.swift (path globs). The change is presentation geometry only — it stores and snaps the full restore frame instead of pairing an inflated origin with the bare pill size. It does not touch any chat write-path surface: no ChatProvider, kernel journal/projection, floating viewport cursor, turn identity, or pill projection code changes. The floating bar's chat continuity contract is unaffected — only where the collapsed pill window comes to rest on screen.

Failure class (fixes)

Failure-Class: none

Independent instance fixes across unrelated subsystems (Bluetooth audio, conversations cache, installer, floating bar, WAL sync); none recurs against a registered failure class.

Deferred (not in this PR)

  • C27 BYOK keys → Keychain migration: security-sensitive migration, unverifiable without a macOS Keychain here; needs a dedicated PR + on-device proof + sign-off.
  • C8 notch cursor-follow: the pure decision seam is testable, but the state-desync side effect has a residual on-device gap (verified by investigation); needs a notched Mac.
  • C5 dormant AudioSourceManager: unreachable dead code; the correct resolution is delete + relocate the still-live AudioSource enum across many files, which needs a local Swift compile.
  • S1 voice-turn watchdog: INV-VOICE-1 reducer logic; needs the macOS live continuity/voice gauntlet.

🤖 Generated with Claude Code


Generated by Claude Code

Review in cubic

claude added 7 commits July 19, 2026 18:58
processAudioPacket popped only the first complete ADTS frame per notification;
a notification that completed two frames (common after a resync) emitted frame 2
only when the NEXT notification arrived, shifting all later audio ~64ms/frame
late (cumulative) and discarding surplus complete frames on stop. It now drains
every complete frame in a while-let loop; a genuinely partial trailing frame
still stays buffered for the next notification.

Failure-Class: none

Co-Authored-By: Claude <noreply@anthropic.com>
The cache-first load() published bare cached rows without overlaying in-flight
optimistic mutations, so a load() that raced a pending star/title/folder edit
(e.g. the user toggled a filter mid-mutation) visually reverted the edit until
the remote call landed. The cache emit now overlays pendingMutations per row via
ConversationReconciliationPolicy.apply, mirroring the server merge path.

Failure-Class: none

Co-Authored-By: Claude <noreply@anthropic.com>
…t brick it

The DMG-install path did removeItem(destination) then copyItem(source →
destination); a copy failure after the remove (e.g. ENOSPC off a near-full DMG)
destroyed the user's working /Applications/Omi.app, leaving only the DMG copy.
replaceInstalledApp now stages the fresh bundle beside the destination and swaps
it in with replaceItemAt only after the copy succeeds, so a failed copy leaves
the existing install untouched.

Failure-Class: none

Co-Authored-By: Claude <noreply@anthropic.com>
savePreChatCenterIfNeeded snapped the draggable pill to the pending restore
origin paired with the bare collapsed pill size, but that origin was computed for
the glow-inflated window. Each rapid open→close→re-open recorded a center offset
by one glow outset (~22pt left / 18pt down), compounding every cycle. It now
stores the full restore frame (origin + inflated size) and snaps to it, so the
recorded center round-trips exactly. Restore-origin math routes through pure
FloatingControlBarGeometry helpers covered by a round-trip test.

Failure-Class: none

Co-Authored-By: Claude <noreply@anthropic.com>
…tion cleanup

Two independent WAL cloud-sync defects:

- fetchSyncJobStatus swallowed an auth-header build failure with try? and sent an
  UNAUTHENTICATED status GET, and mapped HTTP 401 to .transient. A token-mint
  failure (account transition race / locked Keychain) or a stale 401 therefore
  parked the WAL in a forever-retry. It now returns a distinct .unauthorized
  outcome without issuing an unauthenticated request (pure forStatusCode mapping),
  and the reconciler reverts the WAL to .miss for re-upload through the
  authenticated path — the poll never invalidates the session (INV-AUTH-1).

- cleanupOldWals was dead code (never called), so already-synced WAL audio +
  metadata grew without bound. It now runs at the end of each syncToCloud pass,
  selecting reclaimable entries via a pure WALCloudSyncLogic.cleanupCandidates
  (synced-only, past the 7-day retention window) and using the injected clock.

Failure-Class: none

Co-Authored-By: Claude <noreply@anthropic.com>
…ty test

TasksStore.localOnlyRestoreRecord is a @mainactor static; the test class is not
MainActor-isolated, so the call is implicitly async and must be awaited. This
test (added in #10035) merged before its Swift CI finished, leaving main's test
target red; the await fixes the compile and unblocks the build.

Failure-Class: none

Co-Authored-By: Claude <noreply@anthropic.com>
testCleanupRemovesOldSyncedWalButKeepsYoungAndUnsynced gave the old .synced and
old .miss entries the same (device, timerStart), so they shared the WAL id
dev1_<ts>. cleanupOldWals removes by id, so deleting the old synced entry also
removed the old miss entry with the identical id, failing the "unsynced WAL is
kept" assertion. Give the .miss entry a distinct device so ids differ (production
reserves unique timerStarts, so real WALs never collide). Test-only fixture fix.

Failure-Class: none

Co-Authored-By: Claude <noreply@anthropic.com>
@eulicesl
eulicesl marked this pull request as ready for review July 19, 2026 21:53
@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

…s-testing-i8z7cm

# Conflicts:
#	desktop/macos/Desktop/Tests/ActionItemLocalIdentityMutationTests.swift
@eulicesl
eulicesl merged commit 51de36c into main Jul 19, 2026
10 checks passed
@eulicesl
eulicesl deleted the claude/macos-bug-fixes-testing-i8z7cm branch July 19, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants