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.
## 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
`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 master2026-07-31 22:23:28 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The crash
BookmarksViewModel.startEnrichmentcapturedbookmarks.indicesup front, then awaited a network call per bookmark.bookmarksis replaced wholesale by loads, searches and the unread filter, so the loop could read a stale index: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
SpotlightRetrievalTestsgains 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