Skip to content

Hotfix: stop Import-All crash loop + make recent interactions raise the score - #48

Merged
mayeack merged 1 commit into
mainfrom
claude/hotfix-import-crash-and-score
Jun 4, 2026
Merged

mayeack merged 1 commit into
mainfrom
claude/hotfix-import-crash-and-score

Conversation

@mayeack

@mayeack mayeack commented Jun 4, 2026

Copy link
Copy Markdown
Owner

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. ContactSyncService ran mergeOrInsert + save() on the main ModelContext. "Import All" inserts hundreds of Contact rows; that main-context save fires ContactListView's @Query(sort: \Contact.lastName) mid-transaction and faults the Contact.interactions inverse under Release optimization → EXC_BAD_ACCESS — the exact failure mode 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 now apply on a background ModelContext (autosaveEnabled = false) with a single settled save(), so the main context's @Querys only ever observe one committed state — never the partial-state insert window. startAutoSync also defers the launch import by 1s so the first frame renders before it runs. Posts .blackbookSyncDidComplete after 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 carrying lastInteractionDate is 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.recalculateAll now re-derives each contact's lastInteractionDate from the Interaction records this device already holds — a single FetchDescriptor<Interaction> grouped by the to-one interaction.contact?.id (the same controlled pattern the server's IMessageSyncService uses; never touches the faulting Contact.interactions to-many inverse). Uses max() so a newer manual date is never lowered. recalculateAll now 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

  • +3 regression cases in RelationshipScoreEngineTests: stale date healed from a record, priority contact recovers above the boost, newer manual date preserved.
  • Full macOS suite green: 220 tests, 0 failures. macOS + BlackbookServer Debug builds clean.

Manual verification (for reviewer / after merge)

  • Settings → Import from Contacts → Import All completes without crashing; relaunch succeeds (no crash loop).
  • After a sync, Hugo Dooner's score rises above 20 to reflect the recent texts.

Note: iOS Simulator tests couldn't run in the dev environment (CoreSimulator version drift on Xcode 26.5); suite was run on the macOS destination instead. CI will run the standard iOS path.

🤖 Generated with Claude Code

…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>
mayeack pushed a commit that referenced this pull request Jun 4, 2026
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mayeack
mayeack merged commit 5e1d772 into main Jun 4, 2026
2 checks passed
@mayeack
mayeack deleted the claude/hotfix-import-crash-and-score branch June 4, 2026 02:53
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>
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.

1 participant