Skip to content

chore(sync): upstream/main @ 85198f42b — 2026-06-04#49

Merged
adamlsneed merged 20 commits into
mainfrom
codex/upstream-sync-20260604
Jun 4, 2026
Merged

chore(sync): upstream/main @ 85198f42b — 2026-06-04#49
adamlsneed merged 20 commits into
mainfrom
codex/upstream-sync-20260604

Conversation

@adamlsneed

Copy link
Copy Markdown
Owner

Upstream sync: merges BasedHardware/Omi upstream commits through 2026-06-04.

Changes from upstream (19 commits)

Conflict resolutions

  • app/pubspec.yaml — version conflict (HEAD 1.0.534+843 vs upstream 1.0.538+900); took upstream as base then bumped build number to +901

Build number bumps

  • app pubspec.yaml: +900+901
  • desktop Info.plist CFBundleVersion: 23

Verification

  • ✅ Zero conflict markers (grep -rn '^<<<<<<< HEAD' — clean)
  • ✅ All conflicts resolved per upstream-sync-and-backend-policy.mdx
  • ⚠️ flutter gen-l10n — flutter not available in remote environment; no ARB conflicts in this sync
  • ⚠️ xcrun swift build — macOS-only, must be verified locally after pulling main

Note: Run cd desktop && OMI_SKIP_TUNNEL=1 ./run.sh --yolo locally to deploy to Omi Dev. Run xcrun swift build -c debug --package-path Desktop locally to verify the Swift build.


Generated by Claude Code

beastoin and others added 20 commits June 4, 2026 07:34
…Hardware#7631)

When getIdToken() returns null and the token is already expired, clear
the SharedPreferences cache instead of falling through to the stale
token. This prevents infinite WebSocket retry loops after Firebase
signing key rotations.

Regression from 9d33d2e which preserved tokens on null refresh to
avoid clearing near-expiry-but-valid tokens on transient failures. The
fix preserves that intent (only clears when isExpirationDateValid is
false) while closing the gap for truly expired tokens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ardware#7631)

Update getAuthHeaderFixed simulation and test to verify that expired
tokens are cleared when refresh returns null, matching the production
fix. The test now expects an exception (triggering sign-out) instead of
silently reusing the stale cached token.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tighten the clearing condition from !isExpirationDateValid (which
includes the 5-min proactive refresh buffer) to expiry.isBefore(now)
(truly expired only). Near-expiry but still-valid tokens are preserved
on transient refresh failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add isExpired parameter to test simulation to distinguish truly expired
tokens from near-expiry tokens in the 5-min buffer. Add test verifying
near-expiry tokens are preserved when refresh fails transiently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…vert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sults

When the user combined filters (e.g. a folder + Starred) and the server
returned zero conversations, both the section title and the folder/Starred
chip row hid themselves — the guard checked `conversations.length > 0`
without considering whether a filter was active, so the only way to clear
the filter was to restart the app.

Add an `_hasActiveFilter` helper and OR it into the title and folder-tabs
guards so the chips remain reachable whenever a filter is on. Also drop
the older inline `!showStarredOnly && selectedFolderId == null` check on
the "no conversations yet" hero in favour of the same helper, which fixes
the same class of bug for the date filter.
Iterate a user's phone_numbers subcollection and call Twilio's
outgoing_caller_ids({sid}).delete() for each entry that carries a
twilio_sid. Best-effort: per-entry exceptions and the phone-number list
fetch are caught and logged so the caller (delete_account background
wipe) can keep going to the Firestore wipe even if Twilio is down.

Returns the count of successfully deleted caller IDs for visibility.
The lazy import of database.phone_calls keeps the test module
stub-friendly.

Refs BasedHardware#7640
Call delete_user_caller_ids(uid) inside _background_wipe_user_data
before delete_user_data(uid) so the twilio_sid metadata is still
available when the Twilio cleanup runs.

Without this, deleting an account left every verified caller ID
registered on our Twilio account forever. When another user (or the
same user signing back up with a fresh uid) later tried to verify the
same number, Twilio rejected the request with error 21450 and the
backend returned "This phone number is already registered." The only
remediation was an engineer manually calling outgoing_caller_ids.delete
on the orphaned sid.

Closes BasedHardware#7640
Five cases:
  - calls delete_caller_id once per phone_numbers doc with a twilio_sid
  - skips entries with missing / None / empty twilio_sid
  - keeps going when one twilio call raises (rest still attempted)
  - returns 0 cleanly when the user has no phone_numbers
  - swallows a Firestore error fetching phone_numbers so the caller
    can still proceed with the Firestore wipe

The test installs a stub for database.phone_calls before importing
utils.twilio_service so the unit suite does not require firebase_admin
or the rest of the database init chain.
…sults (BasedHardware#7639)

## Summary
- Conversations page hid the section title and the folder/Starred chip
row whenever the filtered result was empty, so combining filters (e.g. a
folder + Starred) with no matches left the user with no way to clear the
filter short of restarting the app.
- The guard for both rows checked `conversations.length > 0` only — once
the filtered fetch came back empty, the chips vanished.
- This adds a small `_hasActiveFilter` helper (covers `showStarredOnly`,
`selectedFolderId`, `selectedDate`) and ORs it into the title and
folder-tabs visibility guards so the chips remain reachable whenever a
filter is on.
- Also reuses the helper in the "no conversations yet" hero guard, which
previously inlined only the starred + folder checks and would mistakenly
show the brand-new-user hero when only a date filter yielded zero
results.

## Test plan
- [ ] With one or more conversations, tap a folder chip → folder filter
active, conversation list filters, chips stay visible.
- [ ] With folder filter on, tap Starred → if no starred conversation in
that folder, empty-state copy shows AND both chips remain visible so the
user can clear either filter.
- [ ] Clear both chips → list returns to default.
- [ ] Brand-new account with zero conversations and no filter active →
still shows the "No conversations yet" hero (unchanged).
- [ ] With a date filter set and zero matches → empty-filter state shows
(not the brand-new-user hero), chips visible.
…vert (BasedHardware#7638)

## Crash

ActionItemsProvider.updateActionItemDueDate

FlutterError - RangeError (length): Invalid value: Valid value range is
empty: 3

package:omi/providers/action_items_provider.dart


https://console.firebase.google.com/u/0/project/based-hardware/crashlytics/app/ios:com.friend-app-with-wearable.ios12/issues/d90ecbfbbb09ebbe4a55c0a08a034316?time=7d&types=crash&sessionEventKey=cbdc14cb933e4f28b52514202667422a_2225757600315898109

Logs:

```
Fatal Exception: FlutterError
0  ???  0x0 ActionItemsProvider.updateActionItemDueDate (action_items_provider.dart)
```

## Fix

`index` was captured before `await api.updateActionItem(...)`. If
`_actionItems` is modified during that await (e.g. user deletes items or
list is refreshed), the stale `index` (e.g. 3) used in the catch/else
revert blocks crashes with RangeError when the list now has fewer
elements.

Both revert paths now re-find the item by ID with `indexWhere` after the
await instead of using the pre-await index, matching the pattern already
used in the success path.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…BasedHardware#7637)

## Crash

`_TranscriptionSettingsPageState._buildCurrentConfig`

**Error:** `FlutterError - type 'bool' is not a subtype of type 'String'
in type cast`
`package:omi/pages/settings/transcription_settings_page.dart:293`

**Crashlytics:**
https://console.firebase.google.com/u/0/project/based-hardware/crashlytics/app/ios:com.friend-app-with-wearable.ios12/issues/2a64d646629244fdf0a70944b67c5f0a?time=7d&types=crash&sessionEventKey=47d6a65f63404b37851550687c6f0d6c_2225897673128842280

**Logs:**
```
Fatal Exception: FlutterError
0  ???  0x0 new LinkedHashMap.from (dart:collection)
1  ???  0x0 _TranscriptionSettingsPageState._buildCurrentConfig + 425 (transcription_settings_page.dart:425)
2  ???  0x0 _TranscriptionSettingsPageState._saveCurrentProviderConfig + 394 (transcription_settings_page.dart:394)
3  ???  0x0 _TranscriptionSettingsPageState._buildProviderSection.<fn> + 1218 (transcription_settings_page.dart:1218)
4  ???  0x0 _DropdownButtonState._handleTap.<fn> + 1473 (dropdown.dart:1473)
```

## Fix

`Map<String, String>.from(requestJson['headers'])` does a hard cast of
every value — if the user's saved JSON config has a non-string value in
`headers` or `params` (e.g. `"keepAlive": true`, `"timeout": 30`), Dart
throws a type cast error inside `LinkedHashMap.from`.

Replaced both `Map<String, String>.from(...)` calls with `.map((k, v) =>
MapEntry(k.toString(), v.toString()))`, which safely coerces any value
type to a string. Also tightened the guard from `!= null` to `is Map` so
it won't attempt the conversion if the field is a non-map type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two P2 comments on BasedHardware#7642:

- P2: in-function `from database import phone_calls` violated the
  backend module rule that all imports live at the top level. Hoisted
  to the top of utils/twilio_service.py; the test's sys.modules stub
  still works because it is installed before twilio_service is first
  imported.
- P2: the outer `try/except` around `delete_caller_id(sid)` cannot ever
  see a Twilio API error — those are absorbed by delete_caller_id's own
  internal handler and surface as a False return on the warning path.
  The outer guard is reachable only if `_get_client()` raises (missing
  TWILIO_* env vars, credential rotation, etc.). Clarified the
  docstring, the inline comment, and the test-case comment so future
  maintainers don't conflate the two paths. Renamed the simulated
  error string in the test to match the actual failure mode.
…Hardware#7642)

Closes BasedHardware#7640

## Problem

When a user deletes their omi account, the background wipe removes their
Firestore data but never tells Twilio about it. The verified outgoing
caller IDs they had registered stay on our Twilio account forever.

Twilio enforces uniqueness at the account level, so any later attempt to
verify the same number — by a brand-new omi user, or by the same person
signing back up — fails with error 21450 → \"This phone number is
already registered.\" Remediation today is an engineer manually calling
`client.outgoing_caller_ids({sid}).delete()` after confirming no live
omi user owns the number.

## Fix

- New `utils.twilio_service.delete_user_caller_ids(uid)` reads
`users/{uid}/phone_numbers`, calls `delete_caller_id(twilio_sid)` for
every entry that has one. Best-effort: per-entry Twilio errors and the
phone-list fetch are caught + logged so a momentary Twilio issue cannot
leave the user half-deleted.
- `_background_wipe_user_data` (in `routers/users.py`) calls it before
`delete_user_data(uid)` — must run while the `twilio_sid` metadata is
still in Firestore.
- 5 unit tests cover: per-entry call, skipping entries without
`twilio_sid`, continuing past a raising delete call, no-phone-numbers
no-op, and a Firestore list failure being swallowed.

## Test plan

- [x] `pytest tests/unit/test_twilio_account_deletion.py -v` → 5 passed
- [x] `pytest tests/unit/test_twilio_service.py -v` (existing) → still
passes
- [x] black-formatted at line-length 120, skip-string-normalization
- [x] No PII in commits / tests / PR
Sync fork with BasedHardware upstream/main through 2026-06-04.

Major upstream changes:
- feat: regenerate daily summary in app + backend endpoint (BasedHardware#7627)
- fix(app): clear stale cached token when Firebase refresh fails (BasedHardware#7631/BasedHardware#7634)
- fix(app): keep conversation filter chips visible on empty filtered results (BasedHardware#7639)
- fix action items stale index RangeError in updateActionItemDueDate revert (BasedHardware#7638)
- fix transcription settings Map cast crash on non-String header values (BasedHardware#7637)
- backend: clean up Twilio caller IDs on account deletion + unit tests (BasedHardware#7642)
- backend: greptile review — top-level import + clarify outer except scope

Conflict resolutions:
- app/pubspec.yaml: version conflict (HEAD 1.0.534+843 vs upstream 1.0.538+900);
  took upstream version as base, then bumped build number to +901

Build number bump: app +900 → +901, desktop CFBundleVersion 2 → 3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@adamlsneed
adamlsneed merged commit 133e51c into main Jun 4, 2026
1 check passed
@adamlsneed
adamlsneed deleted the codex/upstream-sync-20260604 branch June 10, 2026 18:28
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.

5 participants