Fix crash: enrichment held array indices across awaits #10

Merged
admin merged 1 commits from fix/enrichment-index-crash into master 2026-07-31 22:23:28 +00:00
Owner

The crash

BookmarksViewModel.startEnrichment captured bookmarks.indices up front, then awaited a network call per bookmark. bookmarks is replaced wholesale by loads, searches and the unread filter, so the loop could read a stale index:

Array._checkSubscript(_:wasNativeTypeChecked:)
closure #3 in closure #1 in BookmarksViewModel.startEnrichment()

The write path already guarded with i < bookmarks.count. The read — await MainActor.run { bookmarks[i] } — did not. enrichAll() had the same shape.

The fix

Both loops track bookmark ids and re-resolve the position after each await, through a shared apply(summary:tags:toId:).

That also closes the quieter half of the bug: a stale index that happened to still be in range would have attached one bookmark's AI summary to a different bookmark. The new path skips a bookmark that is no longer loaded and logs it.

How it surfaced

Investigating leftover Spotlight translation error ... Code=1 "(null)" entries after the indexing fix in #9. They were a symptom, not a separate bug — the crash tore the process down mid-donation, and the in-flight items failed to translate. With the crash fixed they are gone.

Verification

  • Suite went from crashing (0 tests executed, test host SIGTRAP) to green across three consecutive runs.
  • 0 translation errors in the system log during those runs.
  • SpotlightRetrievalTests gains a source round trip, so both donation paths — app entities and raw searchable items — are covered. 16 XCTest + 2 swift-testing all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM

## The crash `BookmarksViewModel.startEnrichment` captured `bookmarks.indices` up front, then awaited a network call per bookmark. `bookmarks` is replaced wholesale by loads, searches and the unread filter, so the loop could read a stale index: ``` Array._checkSubscript(_:wasNativeTypeChecked:) closure #3 in closure #1 in BookmarksViewModel.startEnrichment() ``` The write path already guarded with `i < bookmarks.count`. The read — `await MainActor.run { bookmarks[i] }` — did not. `enrichAll()` had the same shape. ## The fix Both loops track bookmark **ids** and re-resolve the position after each await, through a shared `apply(summary:tags:toId:)`. That also closes the quieter half of the bug: a stale index that happened to still be *in range* would have attached one bookmark's AI summary to a different bookmark. The new path skips a bookmark that is no longer loaded and logs it. ## How it surfaced Investigating leftover Spotlight `translation error ... Code=1 "(null)"` entries after the indexing fix in #9. They were a symptom, not a separate bug — the crash tore the process down mid-donation, and the in-flight items failed to translate. With the crash fixed they are gone. ## Verification - Suite went from **crashing** (0 tests executed, test host SIGTRAP) to green across **three consecutive runs**. - 0 translation errors in the system log during those runs. - `SpotlightRetrievalTests` gains a source round trip, so both donation paths — app entities and raw searchable items — are covered. 16 XCTest + 2 swift-testing all pass. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-30 04:46:09 +00:00
Fix crash: enrichment held array indices across awaits
CI / build-and-deploy (pull_request) Successful in 30s
2e1b454c09
`startEnrichment` snapshotted `bookmarks.indices`, then awaited a network
call per bookmark. `bookmarks` is replaced wholesale by loads, searches
and the unread filter, so by the time the loop read `bookmarks[i]` the
index could be out of range — EXC_BREAKPOINT in
Array._checkSubscript, straight from the read. The write path already
guarded with `i < bookmarks.count`; the read did not.

`enrichAll()` had the same shape and the same exposure.

Both now track bookmark ids and re-resolve the position after each await,
via a shared `apply(summary:tags:toId:)` that skips a bookmark that is no
longer loaded rather than writing to whatever now sits at that index —
which is the other half of the bug: a stale-but-in-range index would have
silently attached one bookmark's summary to another.

Found while investigating leftover Spotlight "translation error ...
Code=1 (null)" entries after the indexing fix. Those turned out to be a
symptom, not a separate bug: the crash tore the process down mid-donation
and the in-flight items failed to translate. With the crash fixed they
are gone, and the suite went from crashing (0 tests executed) to green
across three consecutive runs.

Adds a source round-trip to SpotlightRetrievalTests, so both donation
paths — app entities and raw searchable items — are covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin merged commit b3d8719874 into master 2026-07-31 22:23:28 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/linkding-ios#10