fix: keyboard stability, host-only analytics, and auto-reply dedupe
- record analytics/usage only in the host app; the extension no longer opens the SQLite queues, so hiding the keyboard cannot hold WAL locks (0xdead10cc) - sync clipboard history writes across processes so deletes made on one side are not reverted by the other - guard memory-relief reentrancy on controller and telemetry sides to break the recursive crash loop - resolve credentials off the main thread and migrate legacy keys once per install, so Keychain reads cannot be caught in the suspend window - gate Volcengine ASR ping completion to once (SIGTRAP on double resume) - treat same-text captures as the same logical copy and dedupe auto actions by content fingerprint within 30s, stopping Universal Clipboard storms - realign version line to 2.1.0 (build 110)
This commit is contained in:
@@ -155,6 +155,22 @@ private struct URLSessionManagedASRWebSocketFactory: ManagedASRWebSocketFactory
|
||||
}
|
||||
}
|
||||
|
||||
/// Once-only gate for callback APIs that may invoke their handler more than
|
||||
/// once (see `ping()`). The first `claim()` returns true; later calls return
|
||||
/// false so the wrapped continuation is resumed exactly once.
|
||||
private final class ResumeOnceGate: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var claimed = false
|
||||
|
||||
func claim() -> Bool {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
if claimed { return false }
|
||||
claimed = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class URLSessionManagedASRWebSocket: ManagedASRWebSocket, @unchecked Sendable {
|
||||
private let task: URLSessionWebSocketTask
|
||||
|
||||
@@ -167,8 +183,14 @@ private final class URLSessionManagedASRWebSocket: ManagedASRWebSocket, @uncheck
|
||||
}
|
||||
|
||||
func ping() async throws {
|
||||
// `URLSessionWebSocketTask.sendPing` can invoke its handler twice when
|
||||
// the task is cancelled with a ping in flight (cancel path + pong
|
||||
// timeout path). Resuming a CheckedContinuation twice traps (SIGTRAP),
|
||||
// so the handler is funneled through a once-only gate.
|
||||
let gate = ResumeOnceGate()
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
task.sendPing { error in
|
||||
guard gate.claim() else { return }
|
||||
if let error {
|
||||
continuation.resume(throwing: error)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user