From a4c9c77a87ec43ebd2b71579593447cd41e989d1 Mon Sep 17 00:00:00 2001 From: Matheus Quost <19558461+Quost@users.noreply.github.com> Date: Wed, 11 Nov 2026 10:11:19 -0300 Subject: [PATCH] fix(memory): stop crashing when Vision rejects a screenshot On failure, Vision calls the request's completion handler with the error and also throws from perform, so TextRecognizer resumed its continuation twice and the app aborted. Automatic Memory then retried the same image after each relaunch, so Menew quit every few minutes. The continuation is now resumed once. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++++ Sources/Menew/TextRecognizer.swift | 24 ++++++++++++++++++++++-- Tests/MenewTests/AgentSourcesTests.swift | 19 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f418e93..3fc0921e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Each version's section is shown in the in-app update window and on the GitHub release. +## 1.100.1 + +### Fixed +- **Menew quitting by itself every few minutes.** With automatic Memory on, reading the text of a screenshot that macOS's text recognition can't handle made Menew quit, and it tried the same image again after relaunching. Those images are now saved without text instead. + ## 1.100.0 ### New diff --git a/Sources/Menew/TextRecognizer.swift b/Sources/Menew/TextRecognizer.swift index b12df1e4..fd382e09 100644 --- a/Sources/Menew/TextRecognizer.swift +++ b/Sources/Menew/TextRecognizer.swift @@ -6,10 +6,12 @@ enum TextRecognizer { /// Linhas reconhecidas, de cima para baixo (vazio se não achou texto) static func recognize(_ image: CGImage) async -> String { await withCheckedContinuation { cont in + // Quando falha, o Vision chama o bloco com o erro E ainda lança a exceção: responder duas vezes derruba o app + let once = ResumeOnce(cont) let request = VNRecognizeTextRequest { req, _ in let lines = (req.results as? [VNRecognizedTextObservation] ?? []) .compactMap { $0.topCandidates(1).first?.string } - cont.resume(returning: lines.joined(separator: "\n")) + once.resume(lines.joined(separator: "\n")) } request.recognitionLevel = .accurate request.usesLanguageCorrection = true @@ -17,8 +19,10 @@ enum TextRecognizer { do { try VNImageRequestHandler(cgImage: image).perform([request]) } catch { - cont.resume(returning: "") + once.resume("") } + // Sem erro e sem chamar o bloco (não deveria acontecer): não deixa quem espera parado para sempre + once.resume("") } } @@ -77,3 +81,19 @@ enum TextRecognizer { text: lines == 1 ? L("Texto copiado") : L("Texto copiado (%d linhas)", lines)) } } + +/// Responde uma continuação uma vez só; as outras chamadas não fazem nada +final class ResumeOnce: @unchecked Sendable { + private var cont: CheckedContinuation? + private let lock = NSLock() + + init(_ cont: CheckedContinuation) { self.cont = cont } + + func resume(_ value: T) { + lock.lock() + let c = cont + cont = nil + lock.unlock() + c?.resume(returning: value) + } +} diff --git a/Tests/MenewTests/AgentSourcesTests.swift b/Tests/MenewTests/AgentSourcesTests.swift index 7fffec0a..e2a8f434 100644 --- a/Tests/MenewTests/AgentSourcesTests.swift +++ b/Tests/MenewTests/AgentSourcesTests.swift @@ -252,3 +252,22 @@ final class OtherAgentEventsTests: XCTestCase { hub.handle(["agent": "cursor", "event": "sessionEnd", "session": "c1"], now: t0) } } + +final class ResumeOnceTests: XCTestCase { + func testOnlyTheFirstResumeCounts() async { + let v: String = await withCheckedContinuation { c in + let once = ResumeOnce(c) + once.resume("primeiro") + once.resume("segundo") // antes, isto derrubava o app + } + XCTAssertEqual(v, "primeiro") + } + + func testRecognizeSurvivesAnImageVisionRejects() async { + // Imagem de 1×1: o Vision recusa, e a leitura devolve vazio em vez de derrubar o app + let ctx = CGContext(data: nil, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, + space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! + let text = await TextRecognizer.recognize(ctx.makeImage()!) + XCTAssertEqual(text, "") + } +}