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:
2026-09-25 12:14:29 +08:00
parent c0848a00de
commit a2abfc5e8c
31 changed files with 727 additions and 314 deletions
@@ -3,6 +3,8 @@
//
// Host-only analytics lifecycle. The shared module owns the wire contract,
// SQLite queue and uploader; this type only connects iOS lifecycle signals.
// The keyboard extension must never open these databases — an open WAL
// connection survives hide and is killed as RunningBoard `0xdead10cc`.
import BackgroundTasks
import Foundation
@@ -65,6 +65,9 @@ struct AssistantKeyboardUITestHarness: View {
}
.onAppear {
configure(width: proxy.size.width)
#if DEBUG
startVoiceMemorySampling()
#endif
}
.onChange(of: proxy.size.width) { _, width in
configureLayout(width: width)
@@ -83,6 +86,22 @@ struct AssistantKeyboardUITestHarness: View {
colorScheme == .dark ? Palette.dark.background : Palette.light.background
}
#if DEBUG
/// DEBUG-only: samples the host process footprint while the voice surface
/// renders, so the Liquid Glass cost can be read off the simulator's App
/// Group container directly (no device log capture needed).
private func startVoiceMemorySampling() {
MemoryDeviceProbe.log("harness.voice.beforeRender")
Task { @MainActor in
for i in 0..<25 {
try? await Task.sleep(for: .milliseconds(200))
MemoryDeviceProbe.log("harness.voice.t\(i)")
}
MemoryDeviceProbe.log("harness.voice.done")
}
}
#endif
private func configure(width: CGFloat) {
configureLayout(width: width)
guard !configured else { return }
+7
View File
@@ -391,6 +391,13 @@ struct MainAppRoot: View {
/// Starts foreground-only services once per active transition. Rime yields
/// the first frame; Flow and CLM keep their existing lazy-heavy-work rules.
private func activateForegroundServices(reason: String) {
// Clipboard history is a whole-array blob shared with the keyboard, and
// a suspended app receives no Darwin notifications — so everything the
// keyboard captured or deleted while we were backgrounded is visible
// only after this reload. Skipping it leaves every host view holding a
// stale array that silently reverts those writes on its next delete.
ClipboardHistoryStore.shared.startObservingCrossProcessChanges()
ClipboardHistoryStore.shared.reload()
scheduleRimeDeployment(reason: reason)
// Automatically arm the low-profile PiP on every host open.
// Capture/ASR remain lazy and start only on an actual mic press.