Skip to content

fix: don't capture SwiftData models across the notification Task (switch-device crash)#1923

Merged
garthvh merged 1 commit into
mainfrom
fix/textmessage-task-capture-crash
Jun 12, 2026
Merged

fix: don't capture SwiftData models across the notification Task (switch-device crash)#1923
garthvh merged 1 commit into
mainfrom
fix/textmessage-task-capture-crash

Conversation

@garthvh

@garthvh garthvh commented Jun 10, 2026

Copy link
Copy Markdown
Member

Summary

Backtrace-confirmed fix for the recurring crash when switching devices:

Fatal error: This model instance was destroyed by calling ModelContext.reset and is no longer usable. (MyInfoEntity/p65)
  frame #7  MyInfoEntity.channels.getter()
  frame #8  closure #11 in MeshPackets.textMessageAppPacket(...)  MeshPackets.swift:1310

Root cause (from the backtrace)

On an incoming text message, textMessageAppPacket deferred its notification/badge work to a Task { @MainActor in … } that captured context-bound model objects — the fetched MyInfoEntity and newMessage — and read them (.channels, .unreadMessages, newMessage.*) when it ran. A device switch recreates/resets the MeshPackets ModelContext between the capture and the Task firing, so by the time the Task executes those objects belong to a torn-down context and any access traps.

This is why it was intermittent (depends on a message in flight around the switch) and why the container/context fixes (#1918, #1922) didn't catch it — the bug is the cross-Task capture, not the clear path.

Fix

Snapshot everything the Task needs before the hop; capture no model objects:

  • Channel branch: the channel mute-check runs synchronously while the context is alive; the Notification (and CarPlay intent) is pre-built; only plain values cross into the Task. The badge recompute (unreadMessages is @MainActor) re-fetches a fresh, live main-context MyInfoEntity inside the Task instead of touching the captured one.
  • DM branch: the Notification is built before the Task and only the resulting value is captured.

Relationship to the other open PRs

Test plan (on-device — the race is timing-dependent)

  • Switch between two nodes repeatedly while channel/DM traffic is arriving — no ModelContext.reset crash
  • Clear App Data → reconnect with traffic — no crash
  • Confirm incoming channel + DM notifications still fire, and the unread badge still updates (throttled ~1/sec)
  • Confirm CarPlay sender intent still attaches (iOS)

Builds clean for the iOS Simulator. The crash only reproduces at runtime under a switch with a message in flight, so it needs the on-device checks above.

…extMessageAppPacket

Backtrace-confirmed fix for the "destroyed by ModelContext.reset" crash when
switching devices:

    MyInfoEntity.channels.getter()
    closure #11 in MeshPackets.textMessageAppPacket(...)  MeshPackets.swift:1310

On an incoming text message, both notification branches deferred work to a
`Task { @mainactor in ... }` that captured context-bound model objects
(`newMessage`, and the fetched `MyInfoEntity`) and read them — `.channels`,
`.unreadMessages`, `newMessage.*` — when it ran. A device switch recreates/resets
the MeshPackets context between the capture and the Task, so those objects are
dead by the time the Task executes and any access traps.

Snapshot everything the Task needs before the hop instead:
- Channel branch: do the channel mute-check synchronously (context still alive),
  pre-build the Notification, and capture only plain values. The badge recompute
  (unreadMessages is @mainactor) re-fetches a fresh, live main-context entity
  inside the Task rather than touching the captured one.
- DM branch: build the Notification (and CarPlay intent) before the Task and
  capture only the resulting value.

No model object is captured across the Task in either branch now.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a backtrace-confirmed crash that can occur during device switching by preventing SwiftData model instances (bound to a soon-to-be-reset ModelContext) from being captured and later accessed inside a deferred Task { @MainActor in … } in MeshPackets.textMessageAppPacket.

Changes:

  • Pre-build DM notifications synchronously while the MeshPackets context is still valid, and pass only value snapshots into the deferred @MainActor Task.
  • For channel messages, snapshot mute/notification decisions synchronously, and re-fetch MyInfoEntity from a fresh main context inside the @MainActor Task before recomputing the unread badge count.
  • Remove cross-Task access to MyInfoEntity.channels, MyInfoEntity.unreadMessages(...), and newMessage.* that could trap after a ModelContext.reset.

@garthvh
garthvh merged commit 735493c into main Jun 12, 2026
5 checks passed
@garthvh
garthvh deleted the fix/textmessage-task-capture-crash branch June 12, 2026 00:21
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