Skip to content

feat: enforce browser audio media gate - #18

Merged
lanyue-llk merged 5 commits into
lex-mainfrom
codex/browser-direct-audio-input
Aug 6, 2026
Merged

feat: enforce browser audio media gate#18
lanyue-llk merged 5 commits into
lex-mainfrom
codex/browser-direct-audio-input

Conversation

@guoweichen-ai

@guoweichen-ai guoweichen-ai commented Aug 3, 2026

Copy link
Copy Markdown

Summary

This PR completes the browser side of LexVoice's unified media-control contract. The existing Browser participant now consumes lk.media.control, applies the command to the real microphone track, and reports the observed result on lk.media.state.

Changes

1. Add the media-control protocol and executor

  • Decode and validate the versioned control envelope with strict size, type, sequence, epoch, target, and expiry checks.
  • Execute start-closed audio leases, fail closed on stale or malformed control, and publish correlated state acknowledgements.

2. Bind control to the real Browser microphone

  • Integrate the executor with the existing LiveKit room and browser audio track without adding a participant or PeerConnection.
  • Preserve local privacy priority, device replacement safety, bounded open leases, controller pinning, and disconnect cleanup.

3. Preserve current sandbox startup behavior

  • Merge the latest lex-main prewarm and observability changes.
  • Keep sandbox microphone/camera capture concurrent while the microphone remains closed behind the gate.
  • Re-derive a blank sandbox agentName override from the final input role before media startup.

Impact

  • LexVoice face-required Browser sessions now have an actual consumer for lk.media.control; commands change the real track and receive lk.media.state acknowledgements.
  • Automatic control cannot override the user's local mute, and loss of a valid controller or lease closes capture fail-safe.
  • Anonymous-agent fallback is intentional only for legacy agents without lk.agent.name: it requires exactly one eligible anonymous agent and pins that controller.
  • This PR is the frontend delivery dependency for lexmount/lex-voice#228.

Verification

  • npm test

    ✅ passed: 238 tests

  • npm run lint

    ✅ passed: ESLint completed without errors

  • npx tsc --noEmit

    ✅ passed: TypeScript completed without errors

  • Prettier check for all changed frontend files

    ✅ passed: all changed files match Prettier style

guowei added 2 commits August 3, 2026 14:54
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Claude finished @guoweichen-ai's task in 2m 41s —— View job


任务清单

  • 读取仓库配置与 diff
  • 审查 LiveKit room/track 生命周期与媒体权限失败路径
  • 审查 React hook 清理、Next.js 客户端/服务端边界
  • 审查 TypeScript 类型、env/secret、可访问性
  • 输出结论

Review 结论

有条件通过

问题及建议

  • [P1] hooks/useBrowserSourceClient.ts:447-451:当 audioConfigured 为真但 appConfig.agentName 未配置(或为空白字符串)时,start() 会直接抛出,导致 runOwnedRuntimeStart 触发 stopRuntime 把整个 runtime(包括本可正常工作的摄像头)一并回滚,且不会走 onVideoError/handleVideoStartFailure 通道。这是相对旧行为新增的强依赖,建议在 app-config.ts 层做启动期校验或给出更明确的用户可见错误,避免部署遗漏 agentName 时静默连视频都无法启动。

  • [P2] lib/livekit-media-gate.ts:466(调用处 hooks/useBrowserSourceClient.ts:463-468):allowAnonymousLiveKitAgentFallback 被硬编码为 true,只要房间中恰好存在一个未设置 lk.agent.name 属性的 agent-kind 参与者就会被信任为 controller,不再校验 agentName 匹配。测试已覆盖该分支行为本身,但作为生产默认值是否需要放开建议在 PR 描述/评审中显式确认,而非静默启用。

  • [P2] hooks/useBrowserSourceClient.ts:389-402stopRuntimeaudioGate.adapter.stop()executor.stop() 未 await 就同步调用 audioGate.device.close()。从 BrowserAudioGateDevice.close() 实现看是幂等安全的,不构成实际竞态,但顺序上不够直观,建议后续加注释说明为何可以在两个 stop 完成前调用 close。

  • [P2] hooks/useBrowserSourceClient.ts 全文:本次核心变更(media-control 协议、executor、gate device)均无对应组件/UI 代码改动,属预期内(仅浏览器侧执行器接线),可访问性方面无需额外检查;仅作说明,无需处理。

未发现阻塞合并的正确性问题:lib/media-control-protocol.ts 的信封校验(size/type/sequence/epoch/target/expiry)、lib/media-gate-executor.ts 的租约撤销与 operationVersion/AbortController 竞态处理、lib/browser-source-runtime-lifecycle.ts 的 runtime 所有权切换逻辑均有较完整的单元测试覆盖(828+512+422 行测试),实现与 PR 描述中的 fail-closed、controller pinning、disconnect cleanup 目标一致。

残余风险:未在本地实际运行 npm ci && npm test/tsc --noEmit(环境未预装依赖),仅通过静态阅读代码交叉核对了 PR 自述的验证结果,建议 CI 结果作为最终判断依据。

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Claude finished @guoweichen-ai's task in 1m 42s —— View job


任务清单

  • 读取仓库配置与 diff
  • 审查 LiveKit room/track 生命周期与媒体权限失败路径
  • 审查 React hook 清理、Next.js 客户端/服务端边界
  • 审查 TypeScript 类型、env/secret、可访问性
  • 输出结论

Review 结论

通过

问题及建议

本次新增提交 d36d3fb("fix: preserve browser gate controller identity")针对上一轮 review 的 P1 问题做了修复,已核实:

  • lib/utils.ts:347-349getAppConfig() 在应用 sandbox 远程覆盖后重新调用 resolveAgentNameForInputSource(config.inputSource, config.agentName),避免远端下发空字符串 agentName 覆盖掉本地已派生的默认值。已确认 canApplySandboxConfigEntrylib/utils.ts:140-153)对空字符串类型校验会放行,因此该修复是必要的,能阻止 hooks/useBrowserSourceClient.ts:449-451throw new Error('agentName is required for browser audio media control') 被触发进而导致 stopRuntime 误回滚摄像头的问题。

  • hooks/useBrowserSourceClient.ts:397, 467-468:为 allowAnonymousLiveKitAgentFallback 硬编码和 stopRuntime 中同步 close() 添加了说明性注释,回应了上一轮 P2 建议,不改变行为,符合预期。

  • [P2] tests/local-dispatch-config.test.mjs:44-48:新增测试对 resolveAgentNameForInputSource('browser', ' ') 的断言有效,但另一条断言用正则匹配 lib/utils.ts 源码文本来验证修复存在,属于字符串匹配而非行为验证,重构时容易被无意改动打破而不影响测试真实性。建议改为对 getAppConfig() 走一次带 mocked fetch 返回空 agentName 字段的集成测试,直接断言最终 config.agentName 非空。

残余风险:未在本地实际运行 npm test/tsc --noEmit(沙箱环境未预装依赖、且无网络权限安装),仅通过静态阅读源码与测试用例交叉核对了本次修复的正确性,建议以 CI 结果为准。

@guoweichen-ai

Copy link
Copy Markdown
Author

已处理 review:P1 在 d36d3fb 修复,sandbox 将 agentName 覆盖为空时会在配置层按最终输入角色重新推导,避免媒体启动阶段回滚;新增回归后前端全量 238 passed,TypeScript、ESLint、Prettier 均通过。两条 P2 也已明确固化设计意图:匿名 fallback 只允许唯一、符合 agent identity namespace 的无名 Agent,并在首次绑定后 pin controller;stop 时先同步 close 是隐私边界,listener teardown 随后异步完成。

@guoweichen-ai

Copy link
Copy Markdown
Author

已修复该 P2(b87e75e):删除对 lib/utils.ts 源码文本的正则断言,改为 mocked fetch 返回空白 sandbox agentName,真实调用 getAppConfig() 并断言最终恢复为 lexvoice-browser-agent。红绿验证中,临时移除归一化代码时测试以实际值 " " 失败,恢复后通过;前端全量 238 passed,TypeScript、ESLint、Prettier 均通过。

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Claude finished @guoweichen-ai's task in 1m 30s —— View job


任务清单

  • 读取仓库配置与 diff
  • 审查 LiveKit room/track 生命周期与媒体权限失败路径
  • 审查 React hook 清理、Next.js 客户端/服务端边界
  • 审查 TypeScript 类型、env/secret、可访问性
  • 输出结论

Review 结论

通过

问题及建议

本次 push(b87e75e)仅新增测试,未引入新的产品代码变更。已核实:

  • tests/local-dispatch-config.test.mjs:41-73:上一轮 P2(回归测试用正则匹配 lib/utils.ts 源码文本、不验证真实行为)已按建议修复为集成测试——mock fetch 返回空白 agentName,真实调用 getAppConfig() 并断言 config.agentName 恢复为 lexvoice-browser-agent。已核实 lib/utils.ts:347-349resolveAgentNameForInputSource 调用仍在,修复有效。

  • 此前两轮 review 提出的 P1(sandbox 空白 agentName 覆盖导致媒体启动阶段回滚摄像头)与另一条 P2(controller pinning / 同步 close 注释)均已在 d36d3fb 中修复并核实。

  • [P2] tests/browser-room-session.test.mjstests/session-start-dispatch.test.mjs:本次同步更新的正则同样是对 hooks/useBrowserSourceClient.ts 源码文本做结构匹配,与上一轮被指出的问题属同一模式;但这是仓库已有的既定测试写法(非本 PR 新增模式),且已对其中一处做了行为化改造,不阻塞本次合并,后续可考虑逐步替换为行为断言。

未发现新的阻塞合并问题。残余风险:未在本地实际运行 npm test/tsc --noEmit(沙箱环境无法安装依赖/无网络权限),核实以静态阅读代码 + diff 交叉比对为主,建议以 CI 结果为最终判断依据。

@lanyue-llk lanyue-llk 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.

发现 1 个阻塞恢复性问题:late controller bind 的首次状态发布若失败,adapter 与 executor 的 controller 状态会分叉,后续无法自动恢复或绑定替代 Agent。其余检查通过:本地 pnpm test(238/238)、pnpm lintpnpm exec tsc --noEmit 均成功,GitHub Actions 的 Lint and Build 也为 success。

Comment thread lib/livekit-media-gate.ts
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Claude finished @guoweichen-ai's task in 1m 55s —— View job


任务清单

  • 读取仓库配置与 diff
  • 审查 LiveKit room/track 生命周期与媒体权限失败路径
  • 审查 React hook 清理、Next.js 客户端/服务端边界
  • 审查 TypeScript 类型、env/secret、可访问性
  • 输出结论

Review 结论

通过

问题及建议

本次核实针对 lanyue-llk 提出的 P1 阻塞问题(late controller bind 首次状态发布失败导致 adapter/executor controller 状态分叉)在 381382b("fix: roll back failed media controller bind")中的修复:

  • lib/media-gate-executor.ts:125-138bindController() 现将首次 publishSnapshot 纳入绑定事务——发布失败时,若 controllerIdentity 仍等于本次绑定值,则同步清空 controllerIdentity、调用 resetCommandOrdering()invalidateAndClose('controller_disconnected', true),再重抛错误。结合 lib/livekit-media-gate.ts:186-190discoverAndBindControllerbindController 失败的 catch(同样条件清空 adapter 侧 controllerIdentity),两侧状态在失败路径下能保持一致回滚,不再出现旧 identity 卡死、新 controller 持续 controller_conflict 的问题。
  • tests/livekit-media-gate.test.mjs:429-479 新增回归测试,用真实 LiveKitMediaGateAdapter + MediaGateExecutor 模拟 late controller 首次 publish 失败,验证失败后旧 participant 断开不再触发无效的 disconnectController(因 pin 已清空),且新 controller 能正常绑定并发布 state(room.published 记录两次目标分别为 first/second)。逻辑与既有 disconnectController/handleMalformedControl 等回滚路径模式一致,未引入重复发布或递归调用风险。
  • 检查了 publishSnapshot 的其余调用点(lib/media-gate-executor.ts:168,230,248,375,417,533,540,573),均发生在 controller 已稳定绑定之后,发布失败不会导致 adapter/executor 两侧状态分裂,不属于同类问题,无需一并修复。

未发现新的阻塞合并问题。残余风险:沙箱环境无网络权限、未预装依赖,无法本地执行 npm test/tsc --noEmit 复验;核实以静态阅读 381382b diff + 交叉比对既有测试模式为主,建议以作者所述本地 239 passed(含新增 45 项 focused 测试)及 CI 结果为最终判断依据。

@lanyue-llk
lanyue-llk merged commit 7c91699 into lex-main Aug 6, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants