Skip to content

Phase 4A:建立受控 ASR consumer routing 与语音设置 - #2396

Open
MomiJiSan wants to merge 9 commits into
Project-N-E-K-O:mainfrom
MomiJiSan:codex/phase4-a-voice-settings
Open

Phase 4A:建立受控 ASR consumer routing 与语音设置#2396
MomiJiSan wants to merge 9 commits into
Project-N-E-K-O:mainfrom
MomiJiSan:codex/phase4-a-voice-settings

Conversation

@MomiJiSan

@MomiJiSan MomiJiSan commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

改动概述 / Summary

Phase 4A 已按最新 main 重新实现,不复用旧 PR 的 Core/runtime 历史。该 PR 只收敛语音识别设置、turn identity、受控 transcript consumer registry,以及 Core/game 内建接线。

共 6 个阶段提交、36 个文件:

  1. fix(voice): align ASR defaults and optimization semantics
  2. feat(voice): add shared recognition settings popover
  3. refactor(voice): attach turn identity to partial transcripts
  4. feat(voice): add controlled transcript consumer registry
  5. feat(asr): route Core and game transcripts through registry
  6. test(voice): enforce cancellation and empty-final barriers

冻结语义

  • 新用户默认启用独立 ASR 与资源优化;显式保存的 false 保持不变。
  • Web/Electron 共用同一语音识别设置 popover,覆盖 8 个 locale,并完整清理 DOM、listener 与 ARIA 状态。
  • VoicePartialEvent 和 final 使用完整 VoiceTurnToken;任一 lease、route 或 audio generation 不匹配时都 fail-closed。
  • VoiceInputRegistryLLMSessionManager 同生命周期,内建 core_chatgame consumer 只注册一次。
  • MicLease owner 决定激活的 consumer;game 不可用时不回落聊天,切换、注销、迟到或重复事件不会串投递。
  • partial/final/empty-final/cancel 均按 token 精确路由;final 在 callback 前消费 route,empty-final 仍完成一次终止与清理。
  • cancel/abort/suspend/close/failure 会等待 terminal cancellation,并精确清理原 Core preview/turn;不使用模糊 abandon(None)
  • consumer callback 被阻塞时不会阻塞下一轮 registry 生命周期。

依赖与硬边界

  • 新增 voice_input 依赖方向合同:仅允许依赖自身包、voice_turn.contractsutils.game_route_state;同时禁止 asr_client 反向导入 voice_input。静态和动态 import 都覆盖。
  • 未修改 Provider worker/policy、TTS、Omni、WebSocket PCM、SmartTurn 4B 生命周期、speaker shadow 或 game router 实现。
  • Core/game 接线只通过既有 utils.game_route_state 边界完成。
  • 插件侧只定义 registrar/handle/SPI 与受控生命周期;不接入插件 SDK、IPC/RPC、插件进程管理或 MicLease 权限。

回归报告 / Regression Report

  • 改动内容:将 partial/final/cancel 的 transcript 投递统一收口到 token-bound VoiceInputRegistry,完成 Core/game consumer 与 MicLease owner 接线,并补齐默认设置、共享 popover 和依赖方向合同。
  • 理由与必要性:旧实现缺少 partial 的完整 turn identity,也没有受控 consumer 生命周期,切换、迟到、重复或取消事件存在错误投递/模糊清理风险;Phase 4A 需要在不改变 Provider、TTS 和路由边界的前提下建立 fail-closed 控制面。
  • 改动前:partial 只携带文本;Core 接收路径与 consumer identity 隐式耦合;取消和旧事件无法全部按 lease/route/audio generation 精确隔离。
  • 改动后:每轮准备时固定完整 VoiceTurnToken 与 consumer;所有事件按同一 identity barrier 校验,final/empty-final 只终止一次,切换或不可用路径直接丢弃且不回退;Core preview/turn 按原 token 精确清理。
  • 潜在回归点:ASR turn 生命周期、MicLease owner 切换、Core preview/turn 清理、game consumer 可用性、popover 生命周期。已用 default chat、active/unavailable game、partial/final/empty-final/cancel、late/duplicate、callback 阻塞、registration 失效和同-token retry 回归覆盖。

不拆分理由 / Why Not Split

该改动按 6 个独立提交分阶段审阅,但必须作为一个原子 PR 合入:设置默认值、前端入口、turn token 合同、registry、Core/game 接线和竞态/结构合同共同定义同一 fail-closed 语义。拆成可独立合入的 PR 会在中间状态留下 partial 无身份、registry 未接线或取消屏障不完整的组合。36 个文件中还包含 8 个同步 locale 文件、新增模块和集中测试,实际生产改动仍保持在 Phase 4A 冻结边界内。

测试 / Testing

  • 完整 unit 回归:8445 passed, 46 skipped;rebase 后 Phase 4A 聚焦回归:494 passed
  • Chromium popover 回归:8 passed;i18n sync gate 通过。
  • 重点回归:Core independent ASR 215 passed、audio stream queue 119 passed、Registry 24 passed、preview 专项 14 passed
  • Core contract gate、Ruff、前端脚本 node --checkgit diff --check 均通过。
  • 最终 GitNexus detect_changes(upstream/main...HEAD):风险 LOW,545 个映射符号、0 条受影响执行流;禁止路径命中 0。

Summary by CodeRabbit

  • 新功能

    • 新增语音识别设置浮层弹窗,集中管理语音识别开关、状态提示与“智能资源优化”待生效逻辑。
    • 引入语音转写路由注册机制,支持内置 Core 与游戏路由的切换与受限分发。
  • 改进

    • “智能资源优化”默认启用;关闭后不再跳过安静帧,提升处理一致性。
    • 语音转写 partial/final/cancel 交付更严格,避免重复、迟到与错误回退;并完善多语言文案与占位符。
  • 测试

    • 补充前端弹窗渲染/销毁、可访问性与待生效状态的端到端覆盖;增加语音路由与资源优化相关单元/集成测试。

@MomiJiSan
MomiJiSan requested a review from wehos as a code owner July 18, 2026 15:47
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

该 PR 引入 VoiceInputRegistry 统一管理语音转录消费者、turn token 路由、取消与失效;重构 Core、游戏和独立 ASR 分发链路;新增资源优化设置,并将语音识别设置迁移到带生命周期管理的 portal popover,喵。

语音输入路由重构

Layer / File(s) Summary
消费者契约与注册表
main_logic/voice_input/*, tests/unit/test_voice_input_*.py
新增消费者协议、内置/插件注册、能力声明、固定路由、空 final 消费和终态取消机制,喵。
Core 与独立 ASR 接入
main_logic/core/asr_runtime.py, main_logic/asr_client/runtime.py, main_logic/voice_turn/contracts.py
将 partial/final、预览、MicLease 切换和会话清理接入 Registry,并使用完整 VoiceTurnToken 管理 stale turn,喵。
资源优化与分层契约
main_logic/asr_client/detector_runtime.py, scripts/check_core_contracts.py, tests/unit/test_check_core_contracts.py
新增资源优化门控,并限制 voice_input 与 asr_client 的依赖方向及动态导入行为,喵。
前端语音设置与同步
static/app/app-audio-capture.js, static/app/app-settings.js, static/app/app-state.js, static/app/app-websocket.js, static/locales/*
新增 portal popover、渲染代际、生命周期清理、待生效状态、资源优化同步和多语言文案,喵。
集成与边界验证
tests/unit/test_core_independent_asr.py, tests/frontend/test_voice_recognition_popover.py, tests/unit/test_app_websocket_static.py
覆盖路由切换、空 final、取消、stale token、游戏接管、UI 重入和设置生命周期,喵。

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested labels: enhancement

Poem

路由转身稳如喵,
Token 相随不乱飘喵。
空 final 轻轻落,
Popover 亮晶晶喵。
ASR 资源听开关,
失败闭合守边疆喵。

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题简洁且准确概括了本次受控 ASR consumer routing 与语音设置的主要改动喵。
Description check ✅ Passed 描述已按模板提供改动概述、回归报告、不拆分理由与测试,内容也足够具体喵。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds controlled ASR transcript routing and shared voice-recognition settings.

  • Adds token-bound Core and game transcript consumers.
  • Routes partial, final, empty-final, and cancellation events through a registry.
  • Adds shared Web/Electron recognition settings and resource-optimization handshakes.
  • Updates locale strings, contract checks, and focused tests.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
main_logic/core/asr_runtime.py Adds resource-optimization handshake handling and token-bound routing into the controlled voice-input path.
main_logic/voice_input/registry.py Adds consumer registration, active-route selection, and token-scoped partial, final, and cancellation dispatch.
main_logic/voice_input/consumers/core_chat.py Adds exact-turn Core preview, final delivery, and cancellation cleanup.
main_logic/voice_input/consumers/game.py Adds game final-transcript routing while keeping unavailable routes closed.
static/app/app-audio-capture.js Adds recognition-settings popover rendering, pending-state display, and cleanup lifecycle handling.
static/app/app-settings.js Adds authoritative resource-optimization state and cross-window voice-settings synchronization.

Sequence Diagram

sequenceDiagram
    participant UI as Voice settings UI
    participant WS as WebSocket router
    participant ASR as ASR runtime
    participant Registry as Voice input registry
    participant Consumer as Core or game consumer

    UI->>WS: Start session with settings handshake
    WS->>ASR: Apply handshake and start session
    ASR->>Registry: Begin token-bound utterance
    ASR->>Registry: Dispatch partial or final transcript
    Registry->>Registry: Validate active token and route
    Registry->>Consumer: Deliver authorized event
    Consumer-->>ASR: Complete or cancel exact turn
Loading

Reviews (14): Last reviewed commit: "fix(voice): preserve session-scoped sett..." | Re-trigger Greptile

@MomiJiSan
MomiJiSan marked this pull request as draft July 18, 2026 16:58
@MomiJiSan
MomiJiSan force-pushed the codex/phase4-a-voice-settings branch 2 times, most recently from c24fdf7 to 99b67c3 Compare July 20, 2026 02:22
@MomiJiSan
MomiJiSan force-pushed the codex/phase3-segmented-smart-turn branch from fc68230 to 9730015 Compare July 20, 2026 02:22
@MomiJiSan MomiJiSan changed the title 修复语音识别默认值并接入资源优化设置 Phase 4A:收敛独立 ASR 默认值与资源优化设置 Jul 20, 2026
@MomiJiSan
MomiJiSan marked this pull request as ready for review July 20, 2026 02:59
@MomiJiSan

Copy link
Copy Markdown
Contributor Author

@codex 全面审查一下

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99b67c358f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread static/app/app-audio-capture.js Outdated

Copy link
Copy Markdown
Contributor Author

已处理 Codex P1 review(139369a8):

  • 关闭独立 ASR 后,UI 摘要与状态提示现在准确说明语音输入仍走 Omni 原生语音识别路径;
  • 同步修正 8 个 locale 以及旧 independentAsrNative 兼容文案;
  • 更新静态契约测试,防止再次出现“语音识别已关闭 / 不会切换 Omni”的误导文案;
  • 相关 ASR detector、core 与静态设置回归共 108 项通过,8 个 locale JSON 和 diff 检查通过。

PR 描述中的对应路由说明也已同步修正。

Copy link
Copy Markdown
Contributor Author

本次已按 #2345 的最终架构重新收敛并更新堆叠基线:Phase 4A 的 ASR 运行时改动现位于 main_logic/asr_client/runtime.py,设置读取保持走 utils.preferences;未恢复 core/asr_runtime.py,也未增加兼容壳。专项回归 111 passed,Ruff、compileall、core contracts 通过;GitNexus 为 LOW、0 affected processes。

@MomiJiSan
MomiJiSan marked this pull request as draft July 21, 2026 13:30

Copy link
Copy Markdown
Contributor Author

两个 P2 已在 14fe8f5a 收敛并推入当前 PR:

  1. 并发渲染资源泄漏

    • renderFloatingMicList() 增加 render generation;
    • 麦克风权限异步返回后校验 generation 与 popup 可用性,过期渲染不再创建 portal、Observer 或监听器;
    • disposer 改为幂等,并仅由当前 owner 清理全局引用。
  2. 设置 Pending 状态长期覆盖

    • Pending 改为目标 session epoch;
    • 普通 lifecycle 变化与启动失败不清除;
    • 仅下一次成功的 neko:voice-session-started 达到目标 epoch 后清除,并恢复 Ready / Blocked / Native 实际状态;
    • dispose 同步移除新增事件监听器。

边界保持不变:只修改 static/app/app-audio-capture.js 并新增真实行为测试;未修改后端、设置协议、i18n、Provider、Electron 原生层或四种模型调用方。

验证结果:

  • 新增行为测试:3 passed;
  • 既有前端/static contracts:43 passed;
  • ASR 回归:106 passed;
  • Ruff、JS 语法、diff check:通过;
  • 完整启动 N.E.K.O 后再启动 N.E.K.O-PC Electron 验收通过,//chat 无本次相关页面/控制台错误;
  • GitNexus compare:low risk,0 affected processes。

堆叠检查:#2398#2408 均未单独修改该前端文件,不应复制同一修复;后续只需按 #2396#2398#2408 顺序继承新基线。

@MomiJiSan
MomiJiSan force-pushed the codex/phase4-a-voice-settings branch from 14fe8f5 to 0fb3c77 Compare July 22, 2026 02:38

MomiJiSan commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Stacked rebase / Provider recovery owner 边界

本 PR 已纯 rebase 到修复后的 #2345 e5336cfb4,当前 head 为 2d6a6bec3

精确组合验证:129 passed;Core package contract、Ruff 与 diff check 通过。前端浏览器测试 2 项通过,剩余 1 项仅因本地未生成 React chat bundle 而出现既有静态资源 404,与设置或 Soniox recovery 无关。

@MomiJiSan
MomiJiSan force-pushed the codex/phase4-a-voice-settings branch 2 times, most recently from 2d6a6be to 392e89e Compare July 23, 2026 02:59
@MomiJiSan MomiJiSan changed the title Phase 4A:收敛独立 ASR 默认值与资源优化设置 Phase 4A:建立受控 ASR consumer routing 与语音设置 Jul 23, 2026
@MomiJiSan

Copy link
Copy Markdown
Contributor Author

ASR consumer routing 最终边界补充

本 PR 的最终职责已收敛为:在最新版 #2345 之上建立统一、受控的 ASR consumer routing;#2345 不再扩容。

  • MicLease owner 只负责麦克风占用与 ASR 生命周期;Consumer identity 只决定 transcript 投递目标。聊天/游戏切换不会重新申请 MicLease。
  • 首版实际接入 core_chatgame:默认聊天,进入游戏切到游戏,退出恢复聊天;不允许双投递。
  • 每个 utterance 开始时固定 ConsumerHandle。consumer 中途失效后,迟到的 partial/final 直接丢弃,不回落到聊天,也不影响 MicLease。
  • 插件侧只提供受控契约:PluginVoiceInputConsumerPluginVoiceInputRegistrar、capabilities 与 registration/handle 生命周期;插件身份必须由宿主验证和分配,不能用任意字符串冒充 core_chatgame 或其他插件。
  • 本 PR 不接入插件 SDK、IPC/RPC、插件进程生命周期或伴学 adapter;不允许插件直接申请 MicLease;不修改 TTS,也不修改 ASR Provider 协议。

验证结果:consumer routing/core 单测 97 passed,main_logic.voice_input 覆盖率 86.69%;扩展回归中的 Python 路径通过。现存的一个前端浏览器失败来自仓库缺少预构建 React chat bundle(静态资源 404),与本次 Python routing 改动无关。

后续堆叠 PR #2398#2408 将依次 restack 到本 PR 的新历史上。

Copy link
Copy Markdown
Contributor Author

Restack 完成回执

已按上面的收敛边界完成实现整理和依次 restack:

GitHub 上的 stacked base 镜像分支已经同步。目前三层 PR 均为 Draft / CLEAN,祖先关系为:

#2396 392e89e0a
  └─ #2398 b35c3251f
       └─ #2408 a4899c2b1

边界核对:

验证结果:

@MomiJiSan
MomiJiSan force-pushed the codex/phase4-a-voice-settings branch from 392e89e to 9a58231 Compare July 24, 2026 01:51
@MingTianSang

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 9a58231804

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@MomiJiSan
MomiJiSan force-pushed the codex/phase4-a-voice-settings branch from d6a479b to 4c6d71d Compare July 30, 2026 03:47
@coderabbitai coderabbitai Bot removed the Needs Check! need doctor to check label Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
scripts/check_core_contracts.py (1)

512-529: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

顺手把 docstring 里的参数名也改成复数形态吧喵~

签名已经变成 str | tuple[str, ...],但文档里还写着单数的 forbidden_prefix,读起来会以为只能传一个前缀喵。

📝 建议改动
-    Flags a literal target inside ``forbidden_prefix`` the same way the static
-    import ban does, and any non-literal target outright — the gate cannot
-    prove a computed module name stays on the right side of the boundary.
+    ``forbidden_prefix`` accepts one prefix or a tuple of them. Flags a literal
+    target inside any of them the same way the static import ban does, and any
+    non-literal target outright — the gate cannot prove a computed module name
+    stays on the right side of the boundary.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check_core_contracts.py` around lines 512 - 529, Update the
_dynamic_import_violations docstring to refer to the plural forbidden_prefixes
parameter concept consistently, reflecting that the function accepts multiple
forbidden prefixes while leaving the implementation unchanged.
tests/unit/test_check_core_contracts.py (1)

337-368: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

参数化里少了一个「非白名单顶层包」的用例喵。

现在覆盖了 main_logic / main_routers / utils 三个受管根,但 scripts/check_core_contracts.pyguarded_roots 只有这三个,所以像 import plugin.plugins.xxx 这样的仓内反向依赖是不会被拦的喵。加一条会红的用例,才能把那个缺口钉住喵~

💚 建议补充用例
         "import main_routers.game_router\n",
         "from utils import preferences\n",
+        "import plugin.plugins.demo\n",
         "import importlib\nimportlib.import_module('main_logic.core')\n",

(需要先按 scripts/check_core_contracts.py 那条评论扩大 guarded_roots,否则这条用例会失败喵。)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_check_core_contracts.py` around lines 337 - 368, 补充
test_run_flags_forbidden_voice_input_dependencies 的参数化输入,覆盖非白名单顶层包 plugin
的导入场景,并确认仍产生 VOICE_INPUT_LAYERING 违规。同步扩大 scripts/check_core_contracts.py 中
guarded_roots 的检查范围,使该反向依赖能够被 contract_checker.run 检测到;保留现有
main_logic、main_routers 和 utils 用例行为不变。
static/app/app-settings.js (1)

1290-1312: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

优化开关也加上 ASR 的 writerId 裁决吧喵~

optimizationWriteIsNewermeta.writeId === _lastAppliedSharedWriteId 时只比较显式标记,而 ASR 在平局时会再用 writerId 打破顺序、保证并发同 id 写入双方选出同一赢家。这个优化钥匙也共享到每次 snapshot,如果直接复用 ASR 那套平局比较,能避免本窗口 own-id 的显式选择被同秒并发写入盖掉喵。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/app/app-settings.js` around lines 1290 - 1312, Update
optimizationWriteIsNewer for voiceInputResourceOptimizationEnabled to use the
same writerId tie-breaker as the independentAsrEnabled arbitration logic when
writeId values are equal. Preserve the explicit-change requirement while
selecting a deterministic winner for concurrent same-id writes, so the current
window’s own writer is not overwritten by another writer.
main_logic/core/asr_runtime.py (1)

2136-2140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

丢弃的 final 建议留一条日志喵~

dispatch_final 返回 REJECTED 时代表一整句用户语音被 fail-closed 丢掉了(token 过期、consumer 不可用、重复 final),但这里把返回值直接扔掉,日志里什么都看不到。线上排查"我说了话但没反应"会很痛苦喵。

♻️ 建议加一条 debug 级观测
     async def _dispatch_voice_input_final(
         self,
         event: VoiceTranscriptEvent,
     ) -> None:
-        await self._voice_input_registry.dispatch_final(event)
+        result = await self._voice_input_registry.dispatch_final(event)
+        if result is VoiceInputDispatchResult.REJECTED:
+            logger.debug(
+                "[%s] voice input final rejected turn=%s-%s",
+                self.lanlan_name,
+                event.turn_token.ingress.session_epoch,
+                event.turn_token.turn_id,
+            )

(需要从 main_logic.voice_inputVoiceInputDispatchResult 导入喵。)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main_logic/core/asr_runtime.py` around lines 2136 - 2140, Update
_dispatch_voice_input_final to capture the result from
_voice_input_registry.dispatch_final(event), import VoiceInputDispatchResult
from main_logic.voice_input, and emit a debug log when the result is REJECTED so
discarded final transcripts are observable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@main_logic/core/asr_runtime.py`:
- Around line 748-751: 调整 _abort_independent_asr 的执行顺序,先调用
self._asr_runtime.abort(reason) 完成 generation bump,再执行
_invalidate_voice_pcm_sync(reason) 和 _voice_input_registry.wait_idle();确保
backpressure 路径在等待取消任务前就拒绝后续过时帧,并保留现有清理流程。
- Around line 2263-2272: 将 Core 语音 turn 处理流程中 `token`、`_voice_lease_owner` 和
`session_ref` 的三个前置校验移入覆盖后续逻辑的 `try/finally`,确保任何提前 return 都会执行现有的
`_abandon_core_voice_turn(...)` 清理。保持校验条件和成功路径不变,仅扩大 `finally` 的保护范围以避免
external-turn pause 泄漏。

In `@scripts/check_core_contracts.py`:
- Around line 1104-1126: 扩大 voice_input 导入边界检查中 guarded_roots
的覆盖范围,使仓库内非标准库/第三方包(包括 plugin、scripts、config 等)也进入校验,并继续要求其匹配
allowed_dependency_prefixes;同时将 transcript_registry_dir 缺失路径产生的 ASR_LAYERING
违规分类改为 VOICE_INPUT_LAYERING,保持合同文本与实际诊断一致。

In `@static/app/app-audio-capture.js`:
- Around line 3336-3347: 在 createVoiceSettingToggle 中为隐藏的 input 补充键盘可见的焦点样式,使用
:focus-visible 将焦点环呈现在对应的可见切换控件或容器上,而不是 opacity 为 0 的 checkbox 本身。确保
togglePinnedVoicePanel 的焦点转移行为保持不变,并让降噪和资源优化等选项在 Enter/Space 打开面板后能明确显示当前焦点。

In `@static/locales/en.json`:
- Around line 936-939: 统一 asyncStatusToast 相关的 ASR 本地化文案插值变量:将 8 个 locale 中这些消息的
{{provider}} 改为 {{providerKey}},以匹配调用方传入的 providerKey;保持文案内容及其他插值变量不变。

---

Nitpick comments:
In `@main_logic/core/asr_runtime.py`:
- Around line 2136-2140: Update _dispatch_voice_input_final to capture the
result from _voice_input_registry.dispatch_final(event), import
VoiceInputDispatchResult from main_logic.voice_input, and emit a debug log when
the result is REJECTED so discarded final transcripts are observable.

In `@scripts/check_core_contracts.py`:
- Around line 512-529: Update the _dynamic_import_violations docstring to refer
to the plural forbidden_prefixes parameter concept consistently, reflecting that
the function accepts multiple forbidden prefixes while leaving the
implementation unchanged.

In `@static/app/app-settings.js`:
- Around line 1290-1312: Update optimizationWriteIsNewer for
voiceInputResourceOptimizationEnabled to use the same writerId tie-breaker as
the independentAsrEnabled arbitration logic when writeId values are equal.
Preserve the explicit-change requirement while selecting a deterministic winner
for concurrent same-id writes, so the current window’s own writer is not
overwritten by another writer.

In `@tests/unit/test_check_core_contracts.py`:
- Around line 337-368: 补充 test_run_flags_forbidden_voice_input_dependencies
的参数化输入,覆盖非白名单顶层包 plugin 的导入场景,并确认仍产生 VOICE_INPUT_LAYERING 违规。同步扩大
scripts/check_core_contracts.py 中 guarded_roots 的检查范围,使该反向依赖能够被
contract_checker.run 检测到;保留现有 main_logic、main_routers 和 utils 用例行为不变。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6dd06a82-c982-487f-9355-732f01193a39

📥 Commits

Reviewing files that changed from the base of the PR and between d6a479b and 4c6d71d.

📒 Files selected for processing (36)
  • docs/design/smart-turn-v3-provider-neutral.md
  • main_logic/asr_client/detector_runtime.py
  • main_logic/asr_client/runtime.py
  • main_logic/core/asr_runtime.py
  • main_logic/core/notify.py
  • main_logic/voice_input/__init__.py
  • main_logic/voice_input/consumers/__init__.py
  • main_logic/voice_input/consumers/core_chat.py
  • main_logic/voice_input/consumers/game.py
  • main_logic/voice_input/contracts.py
  • main_logic/voice_input/plugin_api.py
  • main_logic/voice_input/registrar.py
  • main_logic/voice_input/registry.py
  • main_logic/voice_turn/contracts.py
  • scripts/check_core_contracts.py
  • static/app/app-audio-capture.js
  • static/app/app-settings.js
  • static/app/app-state.js
  • static/locales/en.json
  • static/locales/es.json
  • static/locales/ja.json
  • static/locales/ko.json
  • static/locales/pt.json
  • static/locales/ru.json
  • static/locales/zh-CN.json
  • static/locales/zh-TW.json
  • tests/frontend/test_voice_recognition_popover.py
  • tests/unit/test_app_websocket_static.py
  • tests/unit/test_asr_detector_runtime.py
  • tests/unit/test_audio_stream_queue.py
  • tests/unit/test_check_core_contracts.py
  • tests/unit/test_core_independent_asr.py
  • tests/unit/test_voice_input_consumers.py
  • tests/unit/test_voice_input_registry.py
  • tests/unit/test_voice_recognition_settings_static.py
  • tests/unit/voice_turn/test_contracts.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Project-N-E-K-O/N.E.K.O.-PC (manual)
🚧 Files skipped from review as they are similar to previous changes (11)
  • main_logic/voice_input/consumers/init.py
  • static/locales/pt.json
  • static/locales/ko.json
  • static/locales/zh-CN.json
  • main_logic/core/notify.py
  • static/locales/es.json
  • tests/unit/test_voice_input_consumers.py
  • static/locales/ru.json
  • static/locales/ja.json
  • docs/design/smart-turn-v3-provider-neutral.md
  • static/locales/zh-TW.json

Comment thread main_logic/core/asr_runtime.py Outdated
Comment thread main_logic/core/asr_runtime.py Outdated
Comment thread scripts/check_core_contracts.py
Comment thread static/app/app-audio-capture.js
Comment thread static/locales/en.json Outdated
@MomiJiSan

Copy link
Copy Markdown
Contributor Author

Review 修复回执(最新 head 4c6d71d3a

上一轮 5 项审查意见已完成代码修复、专项回归、逐条回复并关闭线程:

  1. Core prepare 终态清理:prepare rejection、异常及 cancellation 不再提前弹出原 turn context,统一等待 Registry 按 token 触发 on_cancelled 精确清理。
  2. 会话切换摘要:设置变更后的 pending epoch 保留实际 active route;目标 session 成功启动后才切换摘要,避免提前显示 Omni。
  3. 跨窗口资源优化同步:有 metadata 时仅接受 changedKeys 明确声明且新鲜的变更,拒绝启动默认副本覆盖已保存设置;真实切换仍会持久化并传播。
  4. Omni 模式降噪控制:关闭独立 ASR 时只禁用智能资源优化,降噪开关继续可用。
  5. Popover 键盘可达性:Enter/Space 打开固定面板后将焦点移入首个可用控件,保留 Escape 返回触发按钮;pointer/hover 行为不变。

补充的 failure-mode 覆盖包括 prepare rejection、CancelledError、Core→Registry terminal cancellation、session epoch、跨窗口 freshness、Omni 降噪与键盘焦点。专项回归 296 passed,扩展相关回归 929 passed, 2 skipped;Ruff、Core contracts、JS 语法、diff check 与 GitNexus 范围审计通过。

提交历史继续保持原定 6 个 Phase 4A 阶段,没有重开 PR,也没有扩大到 Provider worker/policy、TTS、Omni、WebSocket PCM、SmartTurn 4B、speaker shadow 或 game router。

@MomiJiSan
MomiJiSan force-pushed the codex/phase4-a-voice-settings branch from 4c6d71d to ac4b1ca Compare July 30, 2026 05:48
@MomiJiSan

Copy link
Copy Markdown
Contributor Author

本轮 review 修复回执(head ac4b1ca0d

本轮 CodeRabbit 的 5 个 inline finding + 4 个 review-body nitpick 均已逐项核验并处理:

  1. abort 先推进 ASR generation barrier,再等待 Registry cancellation,封住 backpressure hole。
  2. Core final 的 ingress/owner/session guards 全部纳入 try/finally,提前返回也会按 token 释放 pause。
  3. voice_input 依赖合同覆盖所有仓内一方顶层包/模块,缺失路径使用 VOICE_INPUT_LAYERING
  4. 隐藏 toggle 通过 :focus-visible 在可见 slider 上显示键盘焦点环。
  5. 8 个 locale 的 ASR toast 插值统一为 {{providerKey}}
  6. 动态 import 合同 docstring 改为描述多个 forbidden prefixes。
  7. 新增 import plugin.plugins.demo 绕过路径合同测试。
  8. 资源优化跨窗口写入采用 (writeId, writerId) 确定性仲裁,并用两个真实模块实例覆盖并发分叉/收敛。
  9. REJECTED final 仅记录 route/token identity 的 debug 信息,不记录 transcript 内容。

5 个 inline thread 已逐条回复并全部解决。验证结果:

  • 完整 unit:8442 passed, 46 skipped
  • Phase 4A 聚焦回归:496 passed
  • popover Chromium:5 passed
  • Ruff、Core contracts、JS syntax、i18n sync、diff check:全部通过
  • GitNexus:相对 upstream/main 36 files / 513 symbols / LOW / 0 affected flows

历史仍保持原定 6 个 Phase 4A 阶段提交,详细职责说明已更新到最新哈希。没有扩大到 Provider worker/policy、TTS、Omni、WebSocket PCM、SmartTurn 4B、speaker shadow 或 game router。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@main_logic/voice_input/registry.py`:
- Around line 113-119: Ensure route removal and asynchronous cancellation are
serialized with in-progress prepare_turn callbacks: update VoiceInputRegistry
around activate and its related lifecycle methods to track or lock each route’s
pending callback, and defer terminal cancellation until that callback completes
so cancellation cannot be followed by prepare_turn restoring state. In
tests/unit/test_voice_input_registry.py lines 308-338, add assertions covering
the cancellation-before-prepare-resumption interleaving and verify both
operation order and final state.

In `@scripts/check_core_contracts.py`:
- Around line 547-560: Update _dynamic_import_target() to resolve relative
module names using the supplied package argument and to expand __import__
targets with non-empty statically known fromlist entries, so checks evaluate the
actual absolute module path. When either form cannot be inferred statically,
fail closed instead of skipping validation, and add regression tests covering
relative import_module calls and __import__ fromlist imports.

In `@static/app/app-audio-capture.js`:
- Around line 2534-2537: 修正弹窗渲染的三个边界问题:在异步失败并进入 catch 的清理路径中复用 renderGeneration
与 voiceRecognitionPopoverRenderGeneration 的代际校验,避免旧渲染清空新
DOM;将宿主销毁检测从仅观察自身属性改为观察稳定父节点的 childList,或接入宿主 dispose 流程以清理 portal 和全局监听器;为每个
toggle.input 关联对应可见标签和提示文本,补充无障碍名称与描述关联。

In `@static/app/app-settings.js`:
- Around line 60-130: 修正 _noteSettingDecision 及其调用方
_noteAsrDecision、_noteOptimizationDecision:不要仅因值相同就复用旧决议;结合当前运行时设置值区分 dirty
导致的重复保存与用户真实切换,用户从启动恢复值切换后必须生成新的 writeId/writerId
元组。同步检查相关保存/合并路径,确保新决议可被其他窗口正确判定。补充覆盖“启动恢复 false、服务器合并为 true、用户再切回 false”的回归测试。

In `@tests/frontend/test_voice_recognition_popover.py`:
- Around line 389-444: 为 root_page 增加针对 /static/app/app-audio-capture.js 的
console error 监听和 pageerror 收集,并在根路由加载断言脚本响应成功后补充空错误断言;保持现有 root_page
脚本加载与关闭流程不变。

In `@tests/unit/test_voice_input_registry.py`:
- Around line 308-338: 增强 test_switch_during_prepare_rejects_old_route_once,记录
slow_prepare 完成与 game.on_cancelled 回调的执行顺序,断言 prepare 完成后才发生取消。模拟 prepare
完成后的状态写入,并在等待空闲后验证旧路由状态已清理、最终无残留,同时保留现有返回值和仅取消一次的断言。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 03705a73-cded-4d01-acbe-3fa63bfc07e1

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6d71d and ac4b1ca.

📒 Files selected for processing (33)
  • docs/design/smart-turn-v3-provider-neutral.md
  • main_logic/asr_client/runtime.py
  • main_logic/core/asr_runtime.py
  • main_logic/core/notify.py
  • main_logic/voice_input/__init__.py
  • main_logic/voice_input/consumers/__init__.py
  • main_logic/voice_input/consumers/core_chat.py
  • main_logic/voice_input/consumers/game.py
  • main_logic/voice_input/contracts.py
  • main_logic/voice_input/plugin_api.py
  • main_logic/voice_input/registrar.py
  • main_logic/voice_input/registry.py
  • main_logic/voice_turn/contracts.py
  • scripts/check_core_contracts.py
  • static/app/app-audio-capture.js
  • static/app/app-settings.js
  • static/locales/en.json
  • static/locales/es.json
  • static/locales/ja.json
  • static/locales/ko.json
  • static/locales/pt.json
  • static/locales/ru.json
  • static/locales/zh-CN.json
  • static/locales/zh-TW.json
  • tests/frontend/test_voice_recognition_popover.py
  • tests/unit/test_app_websocket_static.py
  • tests/unit/test_audio_stream_queue.py
  • tests/unit/test_check_core_contracts.py
  • tests/unit/test_core_independent_asr.py
  • tests/unit/test_voice_input_consumers.py
  • tests/unit/test_voice_input_registry.py
  • tests/unit/test_voice_recognition_settings_static.py
  • tests/unit/voice_turn/test_contracts.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Project-N-E-K-O/N.E.K.O.-PC (manual)
🚧 Files skipped from review as they are similar to previous changes (9)
  • main_logic/core/notify.py
  • static/locales/en.json
  • static/locales/ru.json
  • static/locales/ko.json
  • docs/design/smart-turn-v3-provider-neutral.md
  • tests/unit/test_check_core_contracts.py
  • tests/unit/test_voice_input_consumers.py
  • static/locales/zh-TW.json
  • static/locales/ja.json
👮 Files not reviewed due to content moderation or server errors (8)
  • main_logic/voice_turn/contracts.py
  • main_logic/voice_input/consumers/init.py
  • main_logic/voice_input/consumers/core_chat.py
  • main_logic/voice_input/consumers/game.py
  • main_logic/core/asr_runtime.py
  • main_logic/asr_client/runtime.py
  • tests/unit/test_audio_stream_queue.py
  • tests/unit/test_core_independent_asr.py

Comment thread main_logic/voice_input/registry.py
Comment thread scripts/check_core_contracts.py
Comment thread static/app/app-audio-capture.js
Comment thread static/app/app-settings.js Outdated
Comment thread tests/frontend/test_voice_recognition_popover.py
Comment thread tests/unit/test_voice_input_registry.py
@MomiJiSan
MomiJiSan force-pushed the codex/phase4-a-voice-settings branch from ac4b1ca to f55d67c Compare July 30, 2026 06:38
@MomiJiSan

Copy link
Copy Markdown
Contributor Author

按备忘录完成本轮 review comment 分析与修复。结论:这些 comment 不是单纯“硬编码误报”,而是 5 个真实的通用语义/覆盖缺口;其中 2 条 Registry comment 属于同一个 asyncio 顺序竞态。

本轮没有加入业务特判:

  • Registry:route-level in-flight prepare barrier,保证 terminal cancel 必定发生在 prepare callbacks 全部退出之后,并覆盖并发 prepare。
  • import 合同:按 Python 的相对 import、levelfromlist 真实语义解析;非静态目标继续 fail-closed。
  • popover:render generation + 实际祖先链 observer + identity-derived ARIA IDs,处理 stale render、host 替换/移除与可访问性。
  • settings:通用 baseline-divergence/fresh-choice 逻辑,同时服务 ASR 与 optimization,不复制 key-specific 状态。
  • tests:补 root-page console/pageerror,以及 prepare-before-cancel、无残留状态等精确断言。

所有修复已 autosquash 回原 6 个阶段提交并推送到同一 PR(HEAD f55d67cfb)。

验证:完整 unit 8444 passed, 46 skipped;Core/Registry 371 passed;合同 50 passed;设置 72 passed;Chromium popover 8 passed。GitNexus 对正确 worktree 的最终比较为 36 files / 542 symbols / 0 affected flows / LOW。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
tests/unit/test_voice_input_consumers.py (1)

106-125: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

给 CancelledError 路径也补一个重复取消的幂等断言喵~

test_core_consumer_rejected_prepare_retains_context_for_cancel(lines 83-104)已经验证了 prepare 返回 False 时重复调用 on_cancelled 只会消费一次(cancelled.assert_awaited_once())。但紧邻的这个 test_core_consumer_cancelled_prepare_retains_context_for_cancel 测试 prepare 抛出 asyncio.CancelledError 的路径时,只调用了一次 on_cancelled,没有像姊妹测试那样再调用一次并断言仍然只消费一次,喵~两条终止路径本该有对称的覆盖,不然本喵怎么放心这条路径也是幂等的呢,哼!

♻️ 建议补充的断言
     with pytest.raises(asyncio.CancelledError):
         await consumer.prepare_turn(token)
     await consumer.on_cancelled(token, "prepare_cancelled")
+    await consumer.on_cancelled(token, "duplicate_cancel")
 
+    cancelled.assert_awaited_once()
     context, reason = cancelled.await_args.args
     assert context.token == token
     assert context.session_ref is session
     assert reason == "prepare_cancelled"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_voice_input_consumers.py` around lines 106 - 125, Update
test_core_consumer_cancelled_prepare_retains_context_for_cancel to invoke
consumer.on_cancelled a second time with the same token and reason, then assert
cancelled.assert_awaited_once() while preserving the existing context and reason
assertions.
tests/frontend/test_voice_recognition_popover.py (1)

11-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

切片标记一旦被改动,报错会变成一句莫名其妙的 ValueError 喵。

source.index(...) 依赖 // 监听设备变化/** 轻量级更新:仅更新选中状态 */ 这两处注释,后人重构注释时只会看到 substring not found,根本猜不到要去改 harness 喵~加个显式断言把契约说清楚更友好。

♻️ 建议改动
 def _voice_popover_sources() -> tuple[str, str]:
     source = APP_AUDIO_CAPTURE.read_text(encoding="utf-8")
 
+    for marker in (
+        "async function ensureMicrophonePermission()",
+        "// 监听设备变化",
+        "window.renderFloatingMicList = async function",
+        "/** 轻量级更新:仅更新选中状态 */",
+    ):
+        assert marker in source, (
+            f"harness slice marker missing in app-audio-capture.js: {marker!r}"
+        )
+
     permission_start = source.index("async function ensureMicrophonePermission()")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/frontend/test_voice_recognition_popover.py` around lines 11 - 27,
Update _voice_popover_sources to explicitly validate that the expected slicing
markers—“// 监听设备变化” and “/** 轻量级更新:仅更新选中状态 */”—remain present before calling
source.index, raising a clear assertion that identifies the missing contract
marker instead of exposing a generic ValueError.
static/app/app-settings.js (1)

987-1007: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

两条 boot 决议恢复分支的判定条件不对称喵。

ASR 只在 changedKeys 含键时恢复,optimization 则额外接受 bootMeta.optimizationDecision 存在的情况。后者更严谨(决议元组本身就是显式证据),但两边不一致会让人以后读代码时犯迷糊喵~建议把 ASR 分支也补上 bootMeta.asrDecision ||,语义统一。

♻️ 建议改动
-                if (bootMeta && bootMeta.changedKeys.indexOf('independentAsrEnabled') !== -1) {
+                if (
+                    bootMeta
+                    && (
+                        bootMeta.asrDecision
+                        || bootMeta.changedKeys.indexOf('independentAsrEnabled') !== -1
+                    )
+                ) {
                     const bootDecision = bootMeta.asrDecision || bootMeta;
                     _noteAsrDecision(bootDecision.writeId, bootDecision.writerId, settings.independentAsrEnabled);
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/app/app-settings.js` around lines 987 - 1007, 统一两条 boot 决议恢复分支的判定条件:更新
ASR 分支,使其在 bootMeta.asrDecision 存在时也执行恢复,同时保留 changedKeys 包含
independentAsrEnabled 的现有条件;继续使用 _noteAsrDecision 的现有参数传递逻辑。
main_logic/core/asr_runtime.py (1)

318-321: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_core_asr_preview_turn_token 没有在这里补兜底喵。

同一族的 _core_asr_preview_turn_id / _core_asr_preview_text 都在 _ensure_asr_runtime_state 里有 hasattr 兜底,唯独新加的 token 字段没有,所以 _restore_core_asr_preview_after_final 只能靠 getattr(..., None) 兜着。现在不会炸,但这种「一半靠初始化、一半靠 getattr」的写法以后很容易踩坑喵~

♻️ 建议对齐现有兜底模式
         if not hasattr(self, "_core_asr_preview_turn_id"):
             self._core_asr_preview_turn_id = ""
+        if not hasattr(self, "_core_asr_preview_turn_token"):
+            self._core_asr_preview_turn_token = None
         if not hasattr(self, "_core_asr_preview_text"):
             self._core_asr_preview_text = ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main_logic/core/asr_runtime.py` around lines 318 - 321, 在
_ensure_asr_runtime_state 中补充对 _core_asr_preview_turn_token 的 hasattr 兜底初始化,使其与
_core_asr_preview_turn_id 和 _core_asr_preview_text 使用一致的运行时状态初始化模式,并保留现有
_init_voice_input_registry 调用不变。
tests/unit/test_core_independent_asr.py (1)

1559-1564: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

断言 registry._records 私有字典有点越界喵。

前四条断言(对象同一性 + closed is False)已经足够证明 builtin 注册没被 close 重建了;最后一条却把测试钉死在 registry 的内部存储结构上,将来换成别的容器就会无谓地红一片喵~ 如果确实想锁「只有两个消费者」,更建议 registry 暴露一个只读的计数/标识视图再断言喵。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_core_independent_asr.py` around lines 1559 - 1564, 移除对
registry._records 私有存储结构的断言,保留 runtime 注册表对象同一性及两个 registration 的 closed
状态断言;不要让测试依赖 registry 的具体容器实现。若必须验证消费者数量,应改用现有的公开只读计数或标识接口,勿新增私有字段依赖。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@main_logic/voice_input/registry.py`:
- Around line 185-220: Wrap the consumer callbacks in dispatch_partial and
dispatch_final with exception isolation so on_partial or on_final failures do
not propagate to the ASR dispatch task. Preserve the existing token-bound
routing and fail-closed state transitions, including consuming the final route
before invoking on_final, and return the appropriate non-delivery result after a
callback failure using the file’s established error-handling convention.

---

Nitpick comments:
In `@main_logic/core/asr_runtime.py`:
- Around line 318-321: 在 _ensure_asr_runtime_state 中补充对
_core_asr_preview_turn_token 的 hasattr 兜底初始化,使其与 _core_asr_preview_turn_id 和
_core_asr_preview_text 使用一致的运行时状态初始化模式,并保留现有 _init_voice_input_registry 调用不变。

In `@static/app/app-settings.js`:
- Around line 987-1007: 统一两条 boot 决议恢复分支的判定条件:更新 ASR 分支,使其在 bootMeta.asrDecision
存在时也执行恢复,同时保留 changedKeys 包含 independentAsrEnabled 的现有条件;继续使用 _noteAsrDecision
的现有参数传递逻辑。

In `@tests/frontend/test_voice_recognition_popover.py`:
- Around line 11-27: Update _voice_popover_sources to explicitly validate that
the expected slicing markers—“// 监听设备变化” and “/** 轻量级更新:仅更新选中状态 */”—remain
present before calling source.index, raising a clear assertion that identifies
the missing contract marker instead of exposing a generic ValueError.

In `@tests/unit/test_core_independent_asr.py`:
- Around line 1559-1564: 移除对 registry._records 私有存储结构的断言,保留 runtime 注册表对象同一性及两个
registration 的 closed 状态断言;不要让测试依赖 registry
的具体容器实现。若必须验证消费者数量,应改用现有的公开只读计数或标识接口,勿新增私有字段依赖。

In `@tests/unit/test_voice_input_consumers.py`:
- Around line 106-125: Update
test_core_consumer_cancelled_prepare_retains_context_for_cancel to invoke
consumer.on_cancelled a second time with the same token and reason, then assert
cancelled.assert_awaited_once() while preserving the existing context and reason
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 72cd066a-02d4-4156-a170-06e2fc3fa907

📥 Commits

Reviewing files that changed from the base of the PR and between ac4b1ca and f55d67c.

📒 Files selected for processing (33)
  • docs/design/smart-turn-v3-provider-neutral.md
  • main_logic/asr_client/runtime.py
  • main_logic/core/asr_runtime.py
  • main_logic/core/notify.py
  • main_logic/voice_input/__init__.py
  • main_logic/voice_input/consumers/__init__.py
  • main_logic/voice_input/consumers/core_chat.py
  • main_logic/voice_input/consumers/game.py
  • main_logic/voice_input/contracts.py
  • main_logic/voice_input/plugin_api.py
  • main_logic/voice_input/registrar.py
  • main_logic/voice_input/registry.py
  • main_logic/voice_turn/contracts.py
  • scripts/check_core_contracts.py
  • static/app/app-audio-capture.js
  • static/app/app-settings.js
  • static/locales/en.json
  • static/locales/es.json
  • static/locales/ja.json
  • static/locales/ko.json
  • static/locales/pt.json
  • static/locales/ru.json
  • static/locales/zh-CN.json
  • static/locales/zh-TW.json
  • tests/frontend/test_voice_recognition_popover.py
  • tests/unit/test_app_websocket_static.py
  • tests/unit/test_audio_stream_queue.py
  • tests/unit/test_check_core_contracts.py
  • tests/unit/test_core_independent_asr.py
  • tests/unit/test_voice_input_consumers.py
  • tests/unit/test_voice_input_registry.py
  • tests/unit/test_voice_recognition_settings_static.py
  • tests/unit/voice_turn/test_contracts.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Project-N-E-K-O/N.E.K.O.-PC (manual)
🚧 Files skipped from review as they are similar to previous changes (5)
  • main_logic/voice_input/init.py
  • main_logic/core/notify.py
  • docs/design/smart-turn-v3-provider-neutral.md
  • static/locales/pt.json
  • static/locales/ko.json

Comment thread main_logic/voice_input/registry.py
@MomiJiSan
MomiJiSan force-pushed the codex/phase4-a-voice-settings branch from f55d67c to 66f749c Compare July 30, 2026 07:15
@MomiJiSan

Copy link
Copy Markdown
Contributor Author

补充:CodeRabbit 完成后新增的 consumer callback exception 评论也已处理。该问题是实际异常隔离缺口,不是硬编码问题。修复采用通用 CALLBACK_FAILED disposition,而不是按插件/Core/game 写特判;并保留既有 Core 脱敏失败通知。最新 HEAD 66f749c,仍严格 6 个阶段提交;完整 unit 8445 passed / 46 skipped,聚焦 372 passed,最终 GitNexus LOW / 0 affected flows。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
tests/unit/test_check_core_contracts.py (2)

140-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

断文档字符串可不算测行为哦喵~

这个用例只要有人改一下措辞就会红,却证明不了多前缀真的生效;而且目前没有任何测试直接给 _dynamic_import_violations 传元组前缀(helper 只传单个字符串)。建议换成真正的行为断言喵。

💚 建议改动(改为多前缀行为测试)
 `@pytest.mark.unit`
-def test_dynamic_import_docstring_describes_multiple_forbidden_prefixes(
+def test_dynamic_import_violations_match_each_forbidden_prefix(
     contract_checker,
 ) -> None:
-    docstring = contract_checker._dynamic_import_violations.__doc__ or ""
-
-    assert "forbidden prefixes" in docstring
+    source = (
+        "import importlib\n"
+        "importlib.import_module('main_logic.voice_input.registry')\n"
+    )
+    tree = ast.parse(source)
+    aliases = contract_checker.module_alias_paths(tree, "main_logic.asr_client")
+
+    messages = [
+        violation.message
+        for violation in contract_checker._dynamic_import_violations(
+            Path("loader.py"),
+            tree,
+            aliases,
+            ("main_logic.core", "main_logic.voice_input"),
+            "asr_client",
+        )
+    ]
+
+    assert messages == [
+        "asr_client must not import main_logic.voice_input (dynamic import)"
+    ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_check_core_contracts.py` around lines 140 - 146, Replace
test_dynamic_import_docstring_describes_multiple_forbidden_prefixes with a
behavior-focused test for contract_checker._dynamic_import_violations: pass a
tuple containing multiple forbidden prefixes and assert imports matching each
prefix are reported as violations, while an unrelated import is not. Remove the
docstring-content assertion and preserve the helper’s expected violation result
format.

405-412: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

assert messages 太宽松了,闸门报错原因错了它也照样绿喵~

参数化里已经明确了每个 source 的越界模块,顺手把它一起断言进去,才能防止「报了违规但指错模块」的回归喵。

♻️ 建议改动(参数化期望模块)
-@pytest.mark.parametrize(
-    "source",
-    [
-        "import main_logic.core\n",
+@pytest.mark.parametrize(
+    ("source", "expected_module"),
+    [
+        ("import main_logic.core\n", "main_logic.core"),
         ...
     ],
 )

并把断言收紧为 assert any(expected_module in message for message in messages)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_check_core_contracts.py` around lines 405 - 412, 收紧
`VOICE_INPUT_LAYERING` 测试中的断言:使用参数化用例已定义的 `expected_module`,改为断言 `messages`
中至少有一条消息包含该期望模块。保留现有违规筛选逻辑,确保不仅检测到违规,还验证报告指向正确模块。
scripts/check_core_contracts.py (1)

584-597: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

这个 helper 已经通用化了,code 却还钉死在 ASR_LAYERING 上喵~

因为 code 不可配置,1217-1234 的 voice_input 动态导入检查只能把 _dynamic_import_target 的 target/detail 逻辑再抄一遍。把 code 提成参数就能让两边共用同一条闸门,以后加新分层也不用再复制粘贴喵。

♻️ 建议改动(参数化 violation code)
 def _dynamic_import_violations(
     path: Path,
     tree: ast.Module,
     alias_paths: dict[str, str],
     forbidden_prefix: str | tuple[str, ...],
     where: str,
+    code: str = "ASR_LAYERING",
 ) -> list["Violation"]:

随后把两处 Violation(..., "ASR_LAYERING", ...) 改为使用 code,voice_input 侧即可直接调用并传入 "VOICE_INPUT_LAYERING"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check_core_contracts.py` around lines 584 - 597, 将
_dynamic_import_violations 增加可配置的 violation code 参数,并在该 helper 内两处构造 Violation
的位置改用 code,而不是写死 "ASR_LAYERING"。同步更新现有调用方传入 "ASR_LAYERING",让 voice_input
动态导入检查复用此 helper 并传入 "VOICE_INPUT_LAYERING",保持现有 target/detail 校验逻辑不变。
main_logic/core/asr_runtime.py (2)

2508-2510: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

_asr_notification_lockawait wait_idle(),取消回调将来一旦发状态就会自锁喵。

wait_idle() 会把 registry 的 cancellation task 跑完,也就是在持锁期间执行 _cancel_core_chat_voice_turn。现在它只做 _send_core_asr_preview_clear(直写 websocket)+同步 abandon,所以不会重入这把锁、暂时安全喵。但 _send_core_asr_status / _send_core_asr_lifecycle 都要拿同一把锁,哪天有人往 cancel 回调里加一句状态推送,这里就直接死锁给你看喵——建议在这行加一句「cancel 回调禁止走 _send_core_asr_*」的不变量注释,或者把 wait_idle() 挪到锁外面啦喵~

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main_logic/core/asr_runtime.py` around lines 2508 - 2510, 在持有
_asr_notification_lock 的流程中移除 await
self._voice_input_registry.wait_idle(),并将其移动到释放该锁之后执行;确保取消任务仍会等待完成,同时避免取消回调重入同一把锁。若现有控制流无法安全移动,则在该调用处补充明确不变量注释,说明取消回调禁止调用
_send_core_asr_*。

259-261: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

preview 三件套只初始化了两个,_core_asr_preview_turn_token 被落下了喵。

_core_asr_preview_turn_id / _core_asr_preview_text 都在这里(以及 _ensure_asr_runtime_state 的 Line 336-339)建好了,但同属一组的 _core_asr_preview_turn_token 只在 _prepare_core_voice_turn(Line 2192)第一次被赋值;Line 2435 因此只能用 getattr(..., None) 兜着喵。现在不会炸,可这个不对称状态迟早会有人直接 self._core_asr_preview_turn_token 一读就 AttributeError 啦,顺手补齐比留个 getattr 拐杖干净多啦喵~

♻️ 建议补齐初始化喵
         self._core_asr_preview_turn_id = ""
+        self._core_asr_preview_turn_token: VoiceTurnToken | None = None
         self._core_asr_preview_text = ""

另外在 _ensure_asr_runtime_state 里同步补一条(Line 336-339 附近):

        if not hasattr(self, "_core_asr_preview_turn_token"):
            self._core_asr_preview_turn_token = None

之后 Line 2435 的 getattr 就可以直接换成属性读取啦喵。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main_logic/core/asr_runtime.py` around lines 259 - 261, Initialize
_core_asr_preview_turn_token alongside _core_asr_preview_turn_id and
_core_asr_preview_text in the runtime setup, and add the same missing-attribute
guard in _ensure_asr_runtime_state using None. Then replace the defensive
getattr access near the preview handling path with direct
self._core_asr_preview_turn_token access.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/check_core_contracts.py`:
- Around line 1081-1090: Update the violation message in the
VOICE_INPUT_LAYERING path-validation branch to say “voice input layering”
instead of “ASR layering,” while preserving the existing violation code and
missing-path behavior.

---

Nitpick comments:
In `@main_logic/core/asr_runtime.py`:
- Around line 2508-2510: 在持有 _asr_notification_lock 的流程中移除 await
self._voice_input_registry.wait_idle(),并将其移动到释放该锁之后执行;确保取消任务仍会等待完成,同时避免取消回调重入同一把锁。若现有控制流无法安全移动,则在该调用处补充明确不变量注释,说明取消回调禁止调用
_send_core_asr_*。
- Around line 259-261: Initialize _core_asr_preview_turn_token alongside
_core_asr_preview_turn_id and _core_asr_preview_text in the runtime setup, and
add the same missing-attribute guard in _ensure_asr_runtime_state using None.
Then replace the defensive getattr access near the preview handling path with
direct self._core_asr_preview_turn_token access.

In `@scripts/check_core_contracts.py`:
- Around line 584-597: 将 _dynamic_import_violations 增加可配置的 violation code 参数,并在该
helper 内两处构造 Violation 的位置改用 code,而不是写死 "ASR_LAYERING"。同步更新现有调用方传入
"ASR_LAYERING",让 voice_input 动态导入检查复用此 helper 并传入 "VOICE_INPUT_LAYERING",保持现有
target/detail 校验逻辑不变。

In `@tests/unit/test_check_core_contracts.py`:
- Around line 140-146: Replace
test_dynamic_import_docstring_describes_multiple_forbidden_prefixes with a
behavior-focused test for contract_checker._dynamic_import_violations: pass a
tuple containing multiple forbidden prefixes and assert imports matching each
prefix are reported as violations, while an unrelated import is not. Remove the
docstring-content assertion and preserve the helper’s expected violation result
format.
- Around line 405-412: 收紧 `VOICE_INPUT_LAYERING` 测试中的断言:使用参数化用例已定义的
`expected_module`,改为断言 `messages`
中至少有一条消息包含该期望模块。保留现有违规筛选逻辑,确保不仅检测到违规,还验证报告指向正确模块。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4add1a39-4983-4cfc-8df3-e6618ec33b0e

📥 Commits

Reviewing files that changed from the base of the PR and between f55d67c and 66f749c.

📒 Files selected for processing (17)
  • docs/design/smart-turn-v3-provider-neutral.md
  • main_logic/core/asr_runtime.py
  • main_logic/core/notify.py
  • main_logic/voice_input/__init__.py
  • main_logic/voice_input/consumers/__init__.py
  • main_logic/voice_input/consumers/core_chat.py
  • main_logic/voice_input/consumers/game.py
  • main_logic/voice_input/contracts.py
  • main_logic/voice_input/plugin_api.py
  • main_logic/voice_input/registrar.py
  • main_logic/voice_input/registry.py
  • scripts/check_core_contracts.py
  • tests/unit/test_audio_stream_queue.py
  • tests/unit/test_check_core_contracts.py
  • tests/unit/test_core_independent_asr.py
  • tests/unit/test_voice_input_consumers.py
  • tests/unit/test_voice_input_registry.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Project-N-E-K-O/N.E.K.O.-PC (manual)
🚧 Files skipped from review as they are similar to previous changes (3)
  • main_logic/voice_input/consumers/init.py
  • main_logic/core/notify.py
  • docs/design/smart-turn-v3-provider-neutral.md

Comment thread scripts/check_core_contracts.py
@MomiJiSan

Copy link
Copy Markdown
Contributor Author

最新 review 修复说明(head cd4c0ce69

本轮 CodeRabbit finding 已核验、修复并关闭原 thread。该问题是结构合同诊断文案与 violation code 不一致:缺失 main_logic/voice_input 时已经正确归类为 VOICE_INPUT_LAYERING,但 message 仍沿用 “ASR layering”。它不是生产路由或 consumer 的硬编码缺陷。

修复保持通用语义:缺失分层路径的 message 直接由既有 violation_code 生成,例如 required layering path is missing (VOICE_INPUT_LAYERING);没有新增 Core/game/voice-input 特判,也没有改变合同判定、退出状态或运行时行为。对应测试补充了 code 与 message 的精确断言。

范围仍严格限于:

  • scripts/check_core_contracts.py
  • tests/unit/test_check_core_contracts.py

未触及 Core runtime、VoiceInputRegistry、consumer/SPI、Provider、game router、PCM/WebSocket、SmartTurn、TTS、Omni、speaker shadow 或前端设置。

验证结果:合同测试 50 passed;合同门禁、Ruff、diff check 通过;GitNexus 为 2 files / 2 symbols / 0 affected flows / LOW。完整 unit 本轮出现 2 个未改动 GeoIP 后台探测竞态,隔离复跑 2 passed;当前 GitHub 静态门禁、CodeRabbit 与 Greptile 均已通过,Windows unit 仍在运行。CodeRabbit 已确认本次修复正确。

@wehos

wehos commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

@MomiJiSan

Copy link
Copy Markdown
Contributor Author

@greptil review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd4c0ce698

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread static/app/app-audio-capture.js
Comment thread main_logic/core/asr_runtime.py Outdated
Comment thread static/app/app-audio-capture.js

Copy link
Copy Markdown
Contributor Author

Review comments 修复回归说明(bfbc7eda9)

本次集中处理 3 个 review thread:

  • 跨窗口修改 ASR 设置时,将 pending epoch 与当前会话实际 route 快照保存到共享状态,并主动通知已打开的麦克风弹层;popup 重建后不再把“下次会话偏好”误报成“当前会话路由”。
  • voiceInputResourceOptimizationEnabled 纳入 start_session 权威握手,并在 core 首次 await 前完成会话级快照;POST 失败、教程期间不持久化、另一 tab 抢先启动等竞态下,本次会话仍使用用户刚选择的值。
  • 当前 generation 渲染失败时先执行 voice popover disposer,再显示错误占位,避免 portal、observer 和 window listener 泄漏。

回归范围:

  • 531 项定向 pytest 通过(core/路由/前端静态/Chromium/核心契约)。
  • node --check:4 个修改过的前端脚本全部通过。
  • ruff checkscripts/check_core_contracts.pygit diff --check 全部通过。
  • GitNexus 提交前变更审计:13 个预期文件,未发现额外执行流扩散,综合风险 LOW。

兼容边界:旧前端或缺失/畸形握手字段仍回退到已持久化的后端设置;本次未改变设置 API schema、语音协议 action 或非语音会话行为。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
static/app/app-audio-capture.js (1)

3452-3453: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

disposeVoiceRecognitionPopover 赋值晚了一步,createVoicePanel() 中途炸了监听器就收不回来了喵!

createVoicePanel() 内部会注册 pointerdown/keydown/resize/scroll/voice-input-lifecycle-changed/neko:voice-session-started/neko:voice-settings-pending-changed 一串 document/window 监听器(Line 3132-3151),直到函数末尾 updateVoiceRecognitionUi()(Line 3166)才 return。要是这中间随便哪一步抛了异常,外层 try/catch(Line 3979-3986)这时候 disposeVoiceRecognitionPopover 还是 null,压根不会去调用 destroyVoicePanel(),那些已经注册好的监听器就永远挂在 document/window 上收不回来了喵!

destroyVoicePanel() 对没注册过的监听器调用 removeEventListener 本来就是安全的空操作,voicePanel/voiceBridge 判空也稳,所以只要把赋值提前到 createVoicePanel() 调用之前就行,一点风险都不用担心喵~

🐛 建议改动
-            createVoicePanel();
-            disposeVoiceRecognitionPopover = destroyVoicePanel;
+            disposeVoiceRecognitionPopover = destroyVoicePanel;
+            createVoicePanel();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/app/app-audio-capture.js` around lines 3452 - 3453, Move the
assignment of disposeVoiceRecognitionPopover to destroyVoicePanel before calling
createVoicePanel(). Keep the surrounding initialization and cleanup flow
unchanged so the existing error handler can invoke destroyVoicePanel if
createVoicePanel fails during listener registration or UI setup.
main_logic/core/asr_runtime.py (1)

1793-1844: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

_apply_voice_lease_state() 需要先推进 ASR generation barrier,再 wait_idle() 哦喵

abort/suspend() 会先执行生命周期和 transport 层面的无效化/断连,但不会像 _abort_independent_asr() 那样预先推进 generation barrier;现在 game_releaseowner==gameshould_abort 这几处都是先 wait_idle(),挂起期间可能还有当前入口的旧 token/旧 generation PCM 继续送出去喵。改成先执行 asr_runtime.abort()/suspend() + PCM sync invalidation,再等待语音输入空闲喵。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main_logic/core/asr_runtime.py` around lines 1793 - 1844,
调整_apply_voice_lease_state()中的生命周期顺序:在owner为game、game_release以及通用should_abort分支中,先调用_asr_runtime的suspend()/abort()推进ASR
generation
barrier并完成PCM同步失效,再调用_voice_input_registry.wait_idle()。保留现有game_release的generation/owner校验及后续resume逻辑,确保旧generation的PCM不会在等待输入空闲期间继续发送。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/frontend/test_voice_recognition_popover.py`:
- Around line 269-301: Extend the listener-balance key list in
test_current_voice_popover_failure_disposes_owned_portal to include
window:neko:voice-settings-pending-changed, ensuring the failure-disposal test
also asserts this popover listener is removed.

In `@tests/unit/test_websocket_binary_audio.py`:
- Around line 369-379: 更新该测试中围绕 manager.calls 的握手顺序断言:不要使用只返回首次匹配位置的
call_names.index;分别收集两次 start_session 及对应握手调用的索引,并逐项断言每次启动的 asr_handshake 和
resource_optimization_handshake 都发生在对应 start_session 之前。

---

Outside diff comments:
In `@main_logic/core/asr_runtime.py`:
- Around line 1793-1844:
调整_apply_voice_lease_state()中的生命周期顺序:在owner为game、game_release以及通用should_abort分支中,先调用_asr_runtime的suspend()/abort()推进ASR
generation
barrier并完成PCM同步失效,再调用_voice_input_registry.wait_idle()。保留现有game_release的generation/owner校验及后续resume逻辑,确保旧generation的PCM不会在等待输入空闲期间继续发送。

In `@static/app/app-audio-capture.js`:
- Around line 3452-3453: Move the assignment of disposeVoiceRecognitionPopover
to destroyVoicePanel before calling createVoicePanel(). Keep the surrounding
initialization and cleanup flow unchanged so the existing error handler can
invoke destroyVoicePanel if createVoicePanel fails during listener registration
or UI setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 815cb042-d098-4705-921c-89f25a73ff35

📥 Commits

Reviewing files that changed from the base of the PR and between cd4c0ce and bfbc7ed.

📒 Files selected for processing (13)
  • main_logic/core/asr_runtime.py
  • main_logic/core/lifecycle.py
  • main_routers/websocket_router.py
  • static/app/app-audio-capture.js
  • static/app/app-settings.js
  • static/app/app-state.js
  • static/app/app-websocket.js
  • tests/frontend/test_voice_recognition_popover.py
  • tests/unit/test_app_websocket_static.py
  • tests/unit/test_audio_stream_queue.py
  • tests/unit/test_core_independent_asr.py
  • tests/unit/test_voice_recognition_settings_static.py
  • tests/unit/test_websocket_binary_audio.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Project-N-E-K-O/N.E.K.O.-PC (manual)

Comment on lines +269 to +301
@pytest.mark.frontend
def test_current_voice_popover_failure_disposes_owned_portal(page: Page) -> None:
_install_voice_popover_harness(page, deferred_permission=False)

result = page.evaluate(
"""async () => {
const popup = window.__voicePopoverTest.popup();
window.__voicePopoverTest.failMicVolumeVisualization();
const rendered = await window.renderFloatingMicList(popup);
return {
rendered,
panels: window.__voicePopoverTest.panels(),
errorText: popup.textContent,
listenerBalance: {
...window.__voicePopoverTest.listenerBalance,
},
};
}"""
)

assert result["rendered"] is True
assert result["panels"] == 0
assert result["errorText"] == "microphone.loadFailed"
for key in (
"document:pointerdown",
"document:keydown",
"window:resize",
"window:scroll",
"window:voice-input-lifecycle-changed",
"window:neko:voice-session-started",
):
assert result["listenerBalance"].get(key) == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

监听器归零清单里漏了新增的 neko:voice-settings-pending-changed 喵,补上才算真的锁死喵!

这条测试就是专门用来锁定"渲染失败后所有 popover 监听器都要归零"这个不变量的,可是循环里的 key 列表还是老六件套,本 PR 新加的 window:neko:voice-settings-pending-changed(createVoicePanel/destroyVoicePanel 对称注册/移除的那个)没被列进去喵。代码大概率是对的,但测试没盯住它,以后这个监听器的清理万一被改坏了也不会被这条测试抓出来喵~

✅ 建议补充
     for key in (
         "document:pointerdown",
         "document:keydown",
         "window:resize",
         "window:scroll",
         "window:voice-input-lifecycle-changed",
         "window:neko:voice-session-started",
+        "window:neko:voice-settings-pending-changed",
     ):
         assert result["listenerBalance"].get(key) == 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.mark.frontend
def test_current_voice_popover_failure_disposes_owned_portal(page: Page) -> None:
_install_voice_popover_harness(page, deferred_permission=False)
result = page.evaluate(
"""async () => {
const popup = window.__voicePopoverTest.popup();
window.__voicePopoverTest.failMicVolumeVisualization();
const rendered = await window.renderFloatingMicList(popup);
return {
rendered,
panels: window.__voicePopoverTest.panels(),
errorText: popup.textContent,
listenerBalance: {
...window.__voicePopoverTest.listenerBalance,
},
};
}"""
)
assert result["rendered"] is True
assert result["panels"] == 0
assert result["errorText"] == "microphone.loadFailed"
for key in (
"document:pointerdown",
"document:keydown",
"window:resize",
"window:scroll",
"window:voice-input-lifecycle-changed",
"window:neko:voice-session-started",
):
assert result["listenerBalance"].get(key) == 0
`@pytest.mark.frontend`
def test_current_voice_popover_failure_disposes_owned_portal(page: Page) -> None:
_install_voice_popover_harness(page, deferred_permission=False)
result = page.evaluate(
"""async () => {
const popup = window.__voicePopoverTest.popup();
window.__voicePopoverTest.failMicVolumeVisualization();
const rendered = await window.renderFloatingMicList(popup);
return {
rendered,
panels: window.__voicePopoverTest.panels(),
errorText: popup.textContent,
listenerBalance: {
...window.__voicePopoverTest.listenerBalance,
},
};
}"""
)
assert result["rendered"] is True
assert result["panels"] == 0
assert result["errorText"] == "microphone.loadFailed"
for key in (
"document:pointerdown",
"document:keydown",
"window:resize",
"window:scroll",
"window:voice-input-lifecycle-changed",
"window:neko:voice-session-started",
"window:neko:voice-settings-pending-changed",
):
assert result["listenerBalance"].get(key) == 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/frontend/test_voice_recognition_popover.py` around lines 269 - 301,
Extend the listener-balance key list in
test_current_voice_popover_failure_disposes_owned_portal to include
window:neko:voice-settings-pending-changed, ensuring the failure-disposal test
also asserts this popover listener is removed.

Comment on lines +369 to +379
assert [
payload
for name, payload in manager.calls
if name == "resource_optimization_handshake"
] == [False, None]
call_names = [name for name, _payload in manager.calls]
assert call_names.index("asr_handshake") < call_names.index("start_session")
assert (
call_names.index("resource_optimization_handshake")
< call_names.index("start_session")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

请按每次启动逐项校验握手顺序喵。

当前 .index() 只检查第一次出现;即使第二次握手错误地发生在第二个 start_session 之后,测试仍会通过。请收集两次调用的索引并逐项比较,锁住“每个启动请求都先写入对应握手”的契约喵。

建议修复喵
-    assert call_names.index("resource_optimization_handshake") < call_names.index("start_session")
+    start_indices = [i for i, name in enumerate(call_names) if name == "start_session"]
+    resource_indices = [
+        i for i, name in enumerate(call_names)
+        if name == "resource_optimization_handshake"
+    ]
+    assert all(handshake < start for handshake, start in zip(resource_indices, start_indices))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert [
payload
for name, payload in manager.calls
if name == "resource_optimization_handshake"
] == [False, None]
call_names = [name for name, _payload in manager.calls]
assert call_names.index("asr_handshake") < call_names.index("start_session")
assert (
call_names.index("resource_optimization_handshake")
< call_names.index("start_session")
)
assert [
payload
for name, payload in manager.calls
if name == "resource_optimization_handshake"
] == [False, None]
call_names = [name for name, _payload in manager.calls]
assert call_names.index("asr_handshake") < call_names.index("start_session")
start_indices = [i for i, name in enumerate(call_names) if name == "start_session"]
resource_indices = [
i for i, name in enumerate(call_names)
if name == "resource_optimization_handshake"
]
assert all(handshake < start for handshake, start in zip(resource_indices, start_indices))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_websocket_binary_audio.py` around lines 369 - 379, 更新该测试中围绕
manager.calls 的握手顺序断言:不要使用只返回首次匹配位置的 call_names.index;分别收集两次 start_session
及对应握手调用的索引,并逐项断言每次启动的 asr_handshake 和 resource_optimization_handshake 都发生在对应
start_session 之前。

@wehos

wehos commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants