fix(recorder): watchdog 在 sleep 醒来后重检 stop_flag,消除停采误报

当用户松开 hotkey 进入 Listening → Processing 转移时,coordinator 调
rec.stop() 设置 stop_flag 并 pause cpal Stream。但 watchdog 线程此时
可能正卡在 1 秒的 sleep 里:sleep 结束后旧逻辑直接读 last_callback_time,
看到"已 >3 秒未更新"就触发 EngineFailed("录音回调静默停止 N 秒"),
让 coordinator 把正常停采当成引擎崩溃,胶囊弹出 "audio engine failed"。

复现日志(~/Library/Logs/OpenLess/openless.log):

  27.011  [recorder] cb#4250 ...                <- 最后一次回调
  27.054  [asr] server JSON ...                 <- ASR 仍在收尾
  ~27.5   end_session 调 rec.stop() 设置 stop_flag + pause cpal
  31.667  [ERROR] [recorder] watchdog: 录音回调已停止 4 秒,触发错误恢复
  31.667  [ERROR] [coord] recorder runtime error: audio engine failed

修复:watchdog sleep 醒来后立即再 load 一次 stop_flag,flag 已置位则
直接 break 退出,不再做 elapsed 判定。这样:

- 主动停采路径(hotkey-release / cancel):watchdog 静默退出,无误报
- 真故障路径(CoreAudio 设备掉线、OS 音频复位):stop_flag 仍为 false,
  watchdog 照常按 3 秒静默阈值触发 EngineFailed,行为不变

附 18 行注释解释 race,避免后续重构时被无意识回退。
This commit is contained in:
baiqing committed 2026-05-09 14:06:20 +08:00
1 parent 15442b5aad
commit 1bb38ef0f7
1 file changed
+17
@@ -192,6 +192,23 @@ fn run_audio_thread(
while !stop_flag_for_watchdog.load(Ordering::SeqCst) {
thread::sleep(std::time::Duration::from_millis(WATCHDOG_CHECK_INTERVAL_MS));
// 关键:sleep 醒来后必须重新检查 stop_flag,再去看 elapsed。
//
// 否则会与 hotkey-release 停采路径产生竞态:
// 1. 用户松开 hotkey → end_session 调 rec.stop() → 设置 stop_flag
// → audio 线程 pause 了 cpal Stream → 回调真的静默
// 2. 但 watchdog 此时正卡在上面的 1 秒 sleep 里
// 3. sleep 结束后,若不重新检查 stop_flag,
// 就会读到 last_callback_time 已经"老 4 秒",
// 把"我们主动停掉的录音"错报成 EngineFailed("录音回调静默停止 N 秒"),
// coordinator 收到错误后会终止 session、胶囊弹错。
//
// 修复方式是 sleep 后立即再 load 一次:进入 stop 流程后 watchdog 静默退出,
// 不影响 watchdog 在真正活动期捕获 CoreAudio 设备掉线等真故障。
if stop_flag_for_watchdog.load(Ordering::SeqCst) {
break;
}
let last_callback = *state_for_watchdog.last_callback_time.lock();
match last_callback {
Some(last_time) => {