Hotfix: stop Import-All crash loop + make recent interactions raise the score - #48
Merged
Merged
Conversation
…he score Two app-breaking issues, shipped together as an urgent hotfix. 1. Import-All crash / app won't reopen ContactSyncService imported on the MAIN ModelContext and saved once there. "Import All" inserts hundreds of Contacts; that main-context save fires ContactListView's @query mid-transaction and faults the Contact.interactions inverse (EXC_BAD_ACCESS) — the exact failure PR #43 fixed for the pull path. Because startAutoSync re-imports on every .onAppear, it became a crash loop so the app couldn't reopen. Fix: importContacts/importSelected apply mergeOrInsert on a background ModelContext (autosaveEnabled=false) with a single settled save, so the main context's @queries only ever see one committed state. startAutoSync also defers the launch import by 1s so the first frame renders first. Posts .blackbookSyncDidComplete after import so the Dashboard refetches. 2. Recent interactions don't raise the relationship score (Hugo Dooner) The score reads the denormalized Contact.lastInteractionDate. Synced iMessage interaction *records* arrive cleanly, but the contact-field update carrying lastInteractionDate is rejected by conflict resolution when the local copy is newer + pending — so recency stays 0 and the score sits at priority-only (exactly 20 / "Fading"). Fix: RelationshipScoreEngine.recalculateAll now re-derives each contact's lastInteractionDate from the Interaction records this device already holds (single FetchDescriptor<Interaction>, grouped by the to-one interaction.contact?.id — the same controlled pattern the server uses; never touches the faulting Contact.interactions inverse), taking max() so a newer manual date is never lowered. recalculateAll is now also invoked right after each sync pull on the background context, so scores refresh regardless of the visible tab (on macOS the Dashboard's own recalc wouldn't run otherwise). Tests: +3 regression cases in RelationshipScoreEngineTests covering the heal-from-records behavior (stale date healed, priority contact recovers above the boost, newer manual date preserved). Full macOS suite green (220 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 4, 2026
mayeack
pushed a commit
that referenced
this pull request
Jun 4, 2026
mayeack
added a commit
that referenced
this pull request
Jun 4, 2026
…kepoint (#51) * Hotfix: stop Import-All crash loop + make recent interactions raise the score Two app-breaking issues, shipped together as an urgent hotfix. 1. Import-All crash / app won't reopen ContactSyncService imported on the MAIN ModelContext and saved once there. "Import All" inserts hundreds of Contacts; that main-context save fires ContactListView's @query mid-transaction and faults the Contact.interactions inverse (EXC_BAD_ACCESS) — the exact failure PR #43 fixed for the pull path. Because startAutoSync re-imports on every .onAppear, it became a crash loop so the app couldn't reopen. Fix: importContacts/importSelected apply mergeOrInsert on a background ModelContext (autosaveEnabled=false) with a single settled save, so the main context's @queries only ever see one committed state. startAutoSync also defers the launch import by 1s so the first frame renders first. Posts .blackbookSyncDidComplete after import so the Dashboard refetches. 2. Recent interactions don't raise the relationship score (Hugo Dooner) The score reads the denormalized Contact.lastInteractionDate. Synced iMessage interaction *records* arrive cleanly, but the contact-field update carrying lastInteractionDate is rejected by conflict resolution when the local copy is newer + pending — so recency stays 0 and the score sits at priority-only (exactly 20 / "Fading"). Fix: RelationshipScoreEngine.recalculateAll now re-derives each contact's lastInteractionDate from the Interaction records this device already holds (single FetchDescriptor<Interaction>, grouped by the to-one interaction.contact?.id — the same controlled pattern the server uses; never touches the faulting Contact.interactions inverse), taking max() so a newer manual date is never lowered. recalculateAll is now also invoked right after each sync pull on the background context, so scores refresh regardless of the visible tab (on macOS the Dashboard's own recalc wouldn't run otherwise). Tests: +3 regression cases in RelationshipScoreEngineTests covering the heal-from-records behavior (stale date healed, priority contact recovers above the boost, newer manual date preserved). Full macOS suite green (220 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Features: 3 suggested records in pickers, click-to-sort columns, hidden-filter chokepoint Three of the five requested features (notifications + archive-on-import follow in PR 3). 2a. Three suggested records in contact pickers New ContactSuggestionEngine ranks candidates by contextual similarity to the subject — shared tags (x3), groups (x2), locations (x2), plus a per-field signal — and falls back to relationship score so suggestions are always available. IntroducedToPickerView and MetViaPickerView now show a "Suggested" section (top 3) when not searching, with the rest under "All Contacts". 2b. Click column headers to sort ContactListViewModel gains sortColumn + sortAscending and toggleSort(): clicking a header sorts by that column; clicking the active column flips direction. The active column shows a chevron. Sorts Name, Score, Groups, Locations, Tags, Met via, Introduced to (string columns sort their first value alphabetically with blanks last; numeric columns default to descending). The sort menu was updated to the same model and keeps Recent/Added. Replaces the old ContactSortOrder enum. 2e. Hidden contacts excluded from every search surface Audit found all selection surfaces already filtered !isHidden && !isMergedAway. Added a single chokepoint — `Sequence<Contact>.selectable` — and adopted it in the contact pickers and ContactListViewModel so the CLAUDE.md rule has one enforcement point. (A hidden contact still appearing on another device is sync propagation, not a missing filter — same conflict-resolution family as the score fix in PR #48.) Tests: ContactSuggestionEngineTests (ranking, exclusions, score fallback) and ContactListViewModelTests (+toggle direction, string-column blanks-last, .selectable). Full macOS suite green (226 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Michael Yeack <mayeack@Michaels-Mac-mini.local> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
Urgent hotfix for two app-breaking issues. The five feature requests follow in a separate PR.
1. Import-All crash / app won't reopen
Root cause.
ContactSyncServiceranmergeOrInsert+save()on the mainModelContext. "Import All" inserts hundreds ofContactrows; that main-context save firesContactListView's@Query(sort: \Contact.lastName)mid-transaction and faults theContact.interactionsinverse under Release optimization →EXC_BAD_ACCESS— the exact failure mode PR #43 fixed for the pull path. BecausestartAutoSyncre-imports on every.onAppear, it became a crash loop, so the app couldn't reopen.Fix.
importContacts/importSelectednow apply on a backgroundModelContext(autosaveEnabled = false) with a single settledsave(), so the main context's@Querys only ever observe one committed state — never the partial-state insert window.startAutoSyncalso defers the launch import by 1s so the first frame renders before it runs. Posts.blackbookSyncDidCompleteafter import so the Dashboard refetches.2. Recent interactions don't raise the relationship score (Hugo Dooner)
Root cause. The score reads the denormalized
Contact.lastInteractionDate. Synced iMessage interaction records arrive cleanly, but the contact-field update carryinglastInteractionDateis rejected by conflict resolution (ContactSyncApply.applyRemoteContact) when the local copy is newer + pending. So the device shows the texts yet recency stays 0 and the score sits at priority-only — exactly 20 / "Fading".Fix.
RelationshipScoreEngine.recalculateAllnow re-derives each contact'slastInteractionDatefrom theInteractionrecords this device already holds — a singleFetchDescriptor<Interaction>grouped by the to-oneinteraction.contact?.id(the same controlled pattern the server'sIMessageSyncServiceuses; never touches the faultingContact.interactionsto-many inverse). Usesmax()so a newer manual date is never lowered.recalculateAllnow also runs right after each sync pull on the background context, so scores refresh regardless of the visible tab (on macOS the Dashboard's own recalc wouldn't fire otherwise).Tests
RelationshipScoreEngineTests: stale date healed from a record, priority contact recovers above the boost, newer manual date preserved.Manual verification (for reviewer / after merge)
🤖 Generated with Claude Code