feat: harden Flow cold-start/force-quit and polish macOS dictation UX

Fix cold-start overlay recursion that overflowed the main-thread stack when
recording began while the ready overlay was still up; also remove temporary
on-screen Flow DEBUG panels after the orange-mic investigation, and land the
macOS overlay/catalog/layout polish plus related Flow recovery hardening.
This commit is contained in:
Rocky
2026-07-10 12:39:41 +08:00
parent dcb66a9849
commit cdf833935a
104 changed files with 5794 additions and 853 deletions
@@ -33,6 +33,11 @@ public enum FlowSessionKeys {
public static let pendingHostBundleId = "flow.pendingHostBundleId"
/// Wall-clock timestamp of the last utterance completion or session start.
public static let lastActivityAt = "flow.lastActivityAt"
/// One-shot token rotated by every host-process launch. State written by
/// a previous generation is void by definition a fresh launch proves the
/// previous process is dead, whether or not its `applicationWillTerminate`
/// cleanup ever ran (it does NOT run when a suspended app is force-quit).
public static let hostGeneration = "flow.hostGeneration.v1"
/// Heartbeat older than this host is not actively reachable for recording.
public static let heartbeatStaleInterval: TimeInterval = 3
@@ -59,14 +64,23 @@ public enum FlowSessionKeys {
public static let localASRWaitTimeout: TimeInterval = 120
public static let cloudASRWaitTimeout: TimeInterval = 120
/// Keyboard watchdog after the user stops recording (not utterance max length).
/// Must cover worst-case post-stop backlog: remaining SpeechAnalyzer chunks
/// plus cloud LLM polish (see `PolishingService.effectiveTimeout` cap).
/// Hard cap on a single LLM polish request. `PolishingService`'s scaled
/// per-request timeout clamps to this value, so it participates in the
/// keyboard-watchdog budget below.
public static let maxPolishTimeout: TimeInterval = 120
/// Extra slack for result serialization, cross-process propagation, and
/// the host's own polling cadence.
public static let resultDeliveryMargin: TimeInterval = 20
/// Keyboard watchdog after the user stops recording (not utterance max
/// length). Derived from the host-side budget so it always outlasts the
/// host's worst case (ASR drain wait + polish cap + margin) hand-tuned
/// constants drifted below the real host maximum, making the keyboard
/// report a timeout for transcriptions that were still going to succeed.
public static func keyboardResultTimeout(engineMode: String) -> TimeInterval {
if engineMode == "local" {
return 180
}
return 240
let asrWait = engineMode == "local" ? localASRWaitTimeout : cloudASRWaitTimeout
return asrWait + maxPolishTimeout + resultDeliveryMargin
}
public enum RecordingState: String, Sendable, Equatable {