Skip to content

fix(desktop): crash in Plan & Usage settings + OCR array bounds (#6506, #5151)#6540

Open
kodjima33 wants to merge 1 commit intomainfrom
fix/settings-plan-crash
Open

fix(desktop): crash in Plan & Usage settings + OCR array bounds (#6506, #5151)#6540
kodjima33 wants to merge 1 commit intomainfrom
fix/settings-plan-crash

Conversation

@kodjima33
Copy link
Copy Markdown
Collaborator

What

Fixes two crashes in the macOS desktop app:

Plan & Usage settings crash (#6506)

Root cause: SubscriptionPlanCatalogMerger.merge crashes with EXC_BREAKPOINT in _NativeDictionary.merge when the backend returns plans or prices with empty IDs, triggering a Swift dictionary precondition failure during body evaluation.

Fix:

  • Filter out plans/prices with empty IDs before dictionary merge
  • Reserve dictionary capacity to avoid reallocation during merge

Crash trace: Main thread → SettingsContentView.mergePlanCatalog_NativeDictionary.merge assertion failure

OCR array bounds crash (#5151)

Root cause: Vision framework's bridged NSArray storage for VNRecognizedTextObservation results can be invalidated during iteration, causing _objectAt assertion failures even with the existing Array(observations) defensive copy.

Fix:

  • Eagerly snapshot all text, confidence, and bounding box data in a single pass using a lightweight RawOCR struct
  • Copy String values out of bridged objects immediately via String(candidate.string)
  • Build OCRTextBlock array from already-copied native Swift data
  • Reserve array capacity

Crash trace: Thread 9 → RewindOCRService.extractTextWithBounds__SwiftNativeNSArrayWithContiguousStorage._objectAt assertion failure

Testing

  • swift build passes ✅
  • Both fixes are defensive/additive — no behavior change for valid data

Closes #6506
Relates to #5151

#5151)

Plan & Usage crash (#6506):
- Filter out plans/prices with empty IDs before dictionary merge
- Reserve dictionary capacity to avoid reallocation during merge
- Prevents assertion failure in _NativeDictionary.merge on malformed API data

OCR crash (#5151):
- Eagerly snapshot all text, confidence, and bounding box data from
  Vision's bridged NSArray in a single pass before building OCRTextBlocks
- Copies String values out of bridged objects immediately to avoid
  accessing deallocated bridged array storage during iteration
- Reserve array capacity for blocks and text lines
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 11, 2026

Greptile Summary

Fixes two macOS crash sites: a _NativeDictionary precondition failure in the Plan & Usage settings when API responses contain empty-ID plans or prices, and a Vision framework _objectAt assertion failure in the OCR pipeline caused by bridged-NSArray invalidation during iteration. Both fixes are narrowly scoped and additive, with no behavioural change for well-formed data.

Confidence Score: 5/5

Safe to merge — both fixes are defensive guards that only affect malformed or edge-case inputs, with no risk to the happy path.

No P0 or P1 issues found. The two P2 comments are cosmetic (a slightly under-sized reserveCapacity hint and an index-based loop that could be a for-in). Neither blocks correctness or reliability.

No files require special attention.

Important Files Changed

Filename Overview
desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift Adds SubscriptionPlanCatalogMerger struct that filters empty-ID plans/prices before dictionary merge, correctly fixing the _NativeDictionary precondition crash; minor: reserveCapacity uses max instead of the sum of both counts.
desktop/Desktop/Sources/Rewind/Core/RewindOCRService.swift Replaces inline observation iteration with an eager RawOCR snapshot pass that copies all Vision-bridged strings and bounding boxes into native Swift types before building OCRTextBlocks, fixing the _objectAt assertion crash; loop style is slightly non-idiomatic but functionally correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[mergePlanCatalog called] --> B[Filter empty-ID plans from primary & fallback]
    B --> C[reserveCapacity on mergedById dict]
    C --> D[Insert fallback plans into dict]
    D --> E{Plan ID in dict?}
    E -- Yes --> F[Merge plan fields + call mergePrices]
    E -- No --> G[Insert primary plan]
    F --> H[Return merged array]
    G --> H

    subgraph OCR Pipeline
    I[VNRecognizeTextRequest callback] --> J[Array copy of bridged NSArray observations]
    J --> K[Iterate: extract text+confidence+box into RawOCR structs]
    K --> L[Build OCRTextBlock array from RawOCR]
    L --> M[Return OCRResult]
    end
Loading

Reviews (1): Last reviewed commit: "fix(desktop): crash in Plan & Usage sett..." | Re-trigger Greptile

let safeFallback = fallback.filter { !$0.id.isEmpty }

var mergedById: [String: SubscriptionPlanOption] = [:]
mergedById.reserveCapacity(max(safePrimary.count, safeFallback.count))
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.

P2 reserveCapacity underestimates worst-case size

max(safePrimary.count, safeFallback.count) reserves capacity for the larger of the two slices, but the merged dictionary can hold up to safePrimary.count + safeFallback.count entries when all IDs are disjoint. The same pattern repeats in mergePrices (line 102). The practical impact is limited since plan catalogs typically share IDs, but a sum would be more accurate and eliminate any potential reallocation.

Suggested change
mergedById.reserveCapacity(max(safePrimary.count, safeFallback.count))
mergedById.reserveCapacity(safePrimary.count + safeFallback.count)

Comment on lines +193 to +205
for i in 0..<count {
let observation = safeObservations[i]
let candidates = observation.topCandidates(1)
guard !candidates.isEmpty, let candidate = candidates.first else { continue }
guard let candidate = candidates.first else { continue }
let box = observation.boundingBox
guard box.origin.x.isFinite, box.origin.y.isFinite,
box.width.isFinite, box.height.isFinite else { continue }
rawResults.append(RawOCR(
text: String(candidate.string),
confidence: candidate.confidence,
box: box
))
}
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.

P2 Index-based loop provides no extra safety over for-in

safeObservations is a native Swift Array (created via Array(observations)), so iterating with for i in 0..<count and then subscripting offers no safety advantage over for observation in safeObservations. The original crash came from the bridged NSArray storage, which is already neutralised by the Array(...) copy. A for-in loop is more idiomatic and avoids the extra count capture:

Suggested change
for i in 0..<count {
let observation = safeObservations[i]
let candidates = observation.topCandidates(1)
guard !candidates.isEmpty, let candidate = candidates.first else { continue }
guard let candidate = candidates.first else { continue }
let box = observation.boundingBox
guard box.origin.x.isFinite, box.origin.y.isFinite,
box.width.isFinite, box.height.isFinite else { continue }
rawResults.append(RawOCR(
text: String(candidate.string),
confidence: candidate.confidence,
box: box
))
}
for observation in safeObservations {
let candidates = observation.topCandidates(1)
guard let candidate = candidates.first else { continue }
let box = observation.boundingBox
guard box.origin.x.isFinite, box.origin.y.isFinite,
box.width.isFinite, box.height.isFinite else { continue }
rawResults.append(RawOCR(
text: String(candidate.string),
confidence: candidate.confidence,
box: box
))
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

Desktop: Crash when accessing Plan and Usage settings pane.

1 participant