feat(qwen-live): protocol v7 playback receipts and interactive init — M5 - #10769
Conversation
`qwen-live init` walks a new user through setup: - Scans PATH for supported coding agents (qodercli, qwen, gemini, claude, codex) via `--version` probe, resolving each to an absolute path. Native agents (qodercli/qwen/gemini) use their resolved binary directly; adapter-based agents (claude/codex) configure npx to run the ACP adapter, passing the detected binary via env (CODEX_PATH). - Interactive prompts (prompts library) let the user pick a default backend, add additional backends, enter their DashScope API key (with env-var reuse option), and set a working directory. - macOS: checks the Live Host app via LiveHostInstaller.refresh(); offers to install if missing. Non-macOS: informs the user the Host is required. - Writes ~/.qwen-live/config.json atomically (tmp + rename) with user-only permissions (0o600). Subcommand dispatch in index.ts: `qwen-live init` runs the wizard, everything else starts the daemon.
The injector's playback window no longer guesses from byte counts;
the Host app sends real playback_started/completed events.
Protocol:
- LIVE_HOST_PROTOCOL_VERSION 6 → 7 (types.ts both copies +
live-host shared/protocol.ts)
- New Host → Daemon messages: host.playback_started, host.playback_completed
(both carry epoch for call-scoping)
- CLI coordinator copy accepts the new messages (v7 Host connecting to
built-in serve is not rejected as invalid)
- FakeHost in e2e harness bumped to v7
Daemon side (packages/qwen-live):
- Injector: replaces playbackDeadline (byte ÷ 48kB/s estimation) with
playbackInProgress boolean + playbackCompletedAt timestamp.
notePlaybackStarted/notePlaybackCompleted replace noteOutputAudio.
windowClosedForMs returns -1 while playback is active, waits
completedAt + quietGapMs after completion, returns 0 if no playback
or cleared.
- v6 forward compatibility: onOutputAudioDelta calls notePlaybackStarted
as a fallback (v6 Host never sends receipts; v7 Host does). The
fallback preserves the existing "speech after audio triggers
clearOutput" behavior without the byte-estimation wait.
- live-session: onPlaybackStarted/onPlaybackCompleted wired from
coordinator handlers to injector signals
- Coordinator: parseHostMessage + handleTextFrame dispatch the new
messages; LiveCallHandlers gains optional onPlaybackStarted/
onPlaybackCompleted; epoch-scoped (stale receipts ignored)
Host app (packages/live-host):
- audio-engine.ts: constructor gains onPlaybackStarted/
onPlaybackCompleted callbacks; play() fires started on 0→1 source
transition; source.onended fires completed on 1→0 transition
(generation-guarded so clearOutput's stop() doesn't false-fire)
- preload/index.ts: callbacks → IPC live:audio:playback-started/
playback-completed with the current epoch; play IPC payload changes
from bare Uint8Array to { audio, epoch }
- main/index.ts: IPC handlers validate epoch === daemon.getEpoch(),
call daemon.sendPlaybackStarted/sendPlaybackCompleted
- daemon-connection.ts: sendPlaybackStarted/sendPlaybackCompleted
via sendControl
Tests: injector tests rewritten for receipt-based timing (started
holds, completed + gap delivers); e2e discovery assertion bumped to v7.
- live-host parity test: version assertion bumped 6→7, 13/13 pass - injector tests rewritten: playback_started holds window, playback_completed + quiet gap delivers, clearOutput reopens immediately, speech-started returns playbackInProgress - e2e: FakeHost protocol version v7, discovery record assertion v7
|
Thanks for the PR! Template: all required sections present, with an honest Reviewer Test Plan about what was not validated ✓ (the Problem: this is a planned roadmap milestone, not a speculative change — M5 of the Live split roadmap (#10118, open). The playback-deadline byte estimate (bytes ÷ 48 kB/s) is the last v6 approximation the roadmap explicitly names for replacement, and the first-run gap ("no guided path from npm install to a working voice session") is real for a package that ships a daemon + Host app + adapters. Direction: aligned. M1–M4 (#10367, #10617) are already merged under this roadmap, the author is the roadmap owner and a maintainer, and both deliverables sit squarely inside the roadmap's scope. Size: 21 files — ~665 production lines, ~150 lines of README docs, ~45 test/harness lines (plus 3 lines of lockfile). Below the 1000-line advisory; the author is a maintainer, so the two-tier core gate is exempt either way. The cross-package spread (qwen-live + live-host + the cli serve copy) is inherent to a protocol change that must move the daemon and the Host together. Approach: two deliverables in one PR. The v7 receipts half is roadmap item M5-1, and the test-pinned parity surface (v6 fallback, generation guard) is the right shape. Two honest questions, neither a blocker: (1) roadmap M5 also lists retiring the built-in Live module ( Risk: no high-risk path matches from the revert-history signal. Moving on to code review. 🔍 中文说明感谢贡献! 模板:必需章节齐全,Reviewer Test Plan 对未验证部分写得很诚实 ✓( 问题:这是规划中的 roadmap 里程碑,不是投机性改动——Live 拆分 roadmap(#10118,开放中)的 M5。按字节估算播放截止时间(bytes ÷ 48 kB/s)是 roadmap 明确点名要替换的最后一个 v6 近似;首次配置缺口("从 npm install 到可用语音会话没有引导路径")对于同时交付 daemon + Host 应用 + 适配器的包是真实问题。 方向:对齐。M1–M4(#10367、#10617)已在同一 roadmap 下合并;作者是 roadmap 负责人兼维护者;两个交付物完全落在 roadmap 范围内。 规模:21 个文件——约 665 行生产代码、约 150 行 README 文档、约 45 行测试/harness(另 3 行 lockfile)。低于 1000 行提示线;作者是维护者,两档核心门禁本就豁免。跨包范围(qwen-live + live-host + cli serve 副本)是协议变更必须同步推进 daemon 与 Host 的固有属性。 方案:一个 PR 两个交付物。v7 回执这一半是 roadmap 条目 M5-1,测试钉住的平齐面(v6 回退、generation 守卫)形态正确。两个诚实的问题,都不构成阻塞:(1) roadmap 的 M5 还包括内置 Live 模块( 风险:回滚历史信号无高风险路径命中。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewThe design is sound and the state-machine work is well pinned by tests, but there is one blocking finding — an unauthenticated input path into the live session. Blocking: the two new receipt IPC channels skip Everything else checks out:
sequenceDiagram
participant P1 as Realtime model
participant P2 as qwen-live daemon and injector
participant P3 as Live Host main
participant P4 as Audio engine in preload
P1->>P2: audio delta
P2->>P3: output audio frame (epoch)
P3->>P4: play (audio, epoch)
P4-->>P3: first source starts (0 to 1)
P3-->>P2: host.playback_started (epoch)
Note over P2: injection window closed
P4-->>P3: last source ends (generation-guarded)
P3-->>P2: host.playback_completed (epoch)
Note over P2: reopen after quiet gap
Files changed (21)
Testing evidenceThis is an unattended CI run — I do not execute PR code; the evidence below is the PR's own CI read through the API, plus static verification of the diff against the base code. The previous head ( Still in flight at review time: the main unit suite ( Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Not verified: the macOS Host playback-receipt round-trip (Electron audio engine — the author marks it unvalidated too) and the wizard's interactive flow; the Linux unit/e2e claim currently rests on the suite still running above. Sandboxed verification would settle the daemon side of the remaining gap: 中文说明代码审查设计合理,状态机的测试钉得也牢,但有一个阻塞项——一条未鉴权的直播会话输入通道。 阻塞:两条新的回执 IPC 通道没有过 其余都核验通过:
(时序图见英文部分;参与者:实时模型 → qwen-live daemon/注入器 → Live Host main → preload 音频引擎;播放开始/完成回执驱动注入窗口开闭。) 测试证据这是无人值守 CI 运行——不执行 PR 代码;以下证据是通过 API 读取的 PR 自身 CI,外加 diff 对照基础代码的静态核验。 上一个 head( 审查时仍在运行:主单测套件( 未验证:macOS Host 播放回执端到端(Electron 音频引擎——作者同样标注为未验证)和向导交互流程;Linux 单测/e2e 的声明目前建立在上面仍在运行的套件上。 沙箱验证可以补上剩余缺口的 daemon 一侧: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 2/5 — an otherwise clean, well-pinned milestone held back by one unauthenticated IPC channel on a live surface; the fix is two lines. Stepping back: my independent proposal for "replace byte-count playback estimation with real receipts" — started/completed messages with an epoch, an injector tracking in-progress + completedAt instead of a deadline, a v6 fallback treating deltas as started, and the daemon, Host, and built-in CLI copy all moving together because the message shapes must agree — is exactly what this PR builds. The generation guard in the audio engine (increment before stopping sources, so clear-induced endings stay silent) is the detail I would have insisted on, and it is test-pinned. The wizard matches the config schema field for field, writes atomically at 0o600, and the README rewrite is honest about platform coverage and about deferring the built-in module's retirement. Both my Stage 1 questions are resolved by it. The previous head's CI failure was a gift: it pointed straight at the one place this PR drops the ball. The two receipt handlers take an For the record: the main unit suite, no-AK integration tests, Serve A/B, and the real-daemon E2E were still running on this head at review time. That does not change this verdict; once the fix lands they are what the next pass re-reads. 中文说明置信度:2/5 —— 其余部分干净、测试钉得牢的里程碑,被直播面上一个未鉴权的 IPC 通道挡住;修复只要两行。 退一步看:我对"用真实回执替代按字节估算播放"的独立方案——带 epoch 的 started/completed 消息、注入器用 in-progress + completedAt 替代 deadline、v6 回退把 delta 当 started、以及由于消息形状必须一致而让 daemon、Host、内置 CLI 副本同步推进——正是这个 PR 所构建的。音频引擎里的 generation 守卫(停 source 之前先递增,让 clear 引起的结束保持沉默)是我会坚持的细节,而且有测试钉死。向导与配置 schema 逐字段吻合、原子写入 0o600,README 重写对平台覆盖和内置模块下线推迟都很诚实。我 Stage 1 的两个疑问它都解决了。 上一个 head 的 CI 失败是个礼物:它直指这个 PR 唯一失手的地方。两条回执处理器接收 记录在案:主单测套件、no-AK 集成测试、Serve A/B、real-daemon E2E 在审查时仍在该 head 上运行。这不改变本结论;修复落地后,它们是下一轮复查要重读的内容。 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
One blocking finding: the new playback-receipt IPC channels (live:audio:playback-started / live:audio:playback-completed in live-host/src/main/index.ts) skip the isTrustedSender gate every sibling channel in that file applies — full detail in my review comment above. Everything else is ready; this is a two-line fix away. 🙏
一个阻塞项:新的播放回执 IPC 通道(live-host/src/main/index.ts 里的 live:audio:playback-started / playback-completed)跳过了该文件所有兄弟通道都应用的 isTrustedSender 门——详见上方审查评论。其余部分已就绪,只差这两行修复。
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 12 scenario(s). — Qwen Code · serve A/B |
The playback-started/completed IPC channels skipped the sender validation every sibling channel applies — an untrusted renderer could forge playback receipts. Added isTrustedSender(event) as the first guard, matching the pattern in live:set-output-muted etc.
|
Fixed in c530c46: both playback IPC handlers now check isTrustedSender(event) as their first guard, matching the pattern in live:set-output-muted and every other sibling channel. The _event prefix is removed since the event parameter is now used. |
qqqys
left a comment
There was a problem hiding this comment.
Requesting changes for one merge-blocking issue, verified at head fb8e611e3f19 (that head is a merge of main only — every packages/qwen-live, packages/live-host and packages/cli/src/serve/live file is byte-identical to c530c46c99ba, where I ran the checks below).
Critical — the v7 playback receipts never reach the injector, so the injection window latches closed for the rest of the call (inline at packages/qwen-live/src/orchestrator/live-session.ts:473)
The receipt chain is complete right up to the coordinator, and stops there:
- Host → preload → main → daemon:
live:audio:playback-started/playback-completed→daemon.sendPlaybackStarted/Completed→host.playback_started/host.playback_completedframes. packages/qwen-live/src/host/live-host-coordinator.tsparses both frames and dispatches them tothis.handlers.onPlaybackStarted?.()/this.handlers.onPlaybackCompleted?.()(:1123, :1129).packages/qwen-live/src/daemon.ts:154— the onlysetHandlerscall in the package — passes{ onStart, onStop, onInputAudio }and nothing else.
Both handlers are optional, and no caller anywhere in the repo supplies them: the only four non-test occurrences of onPlaybackStarted / onPlaybackCompleted are the two declarations and the two invocations, all inside the coordinator. So Injector.notePlaybackCompleted() has no production caller, while notePlaybackStarted() fires on every output audio delta (live-session.ts:473).
After the assistant's first audio frame of a call:
windowClosedForMs()returns-1becauseplaybackInProgressstays latched (injector.ts:204);poke()returns early onwait < 0without arming a re-check timer (injector.ts:215, "reopened by a state signal later" — no such signal will arrive);- the only remaining resets are
noteSpeechStarted()andnoteOutputCleared(), andnoteOutputCleared()is called solely inside the barge-in branch ofonSpeechStarted(live-session.ts:423).
Impact: everything the injector queues — progress notices, completion summaries and permission asks (8 enqueue sites), plus the permission reminder timer at live-session.ts:1288, which re-enqueues into the same closed window — is not delivered until the user happens to speak again. At base the byte estimate reopened the window on its own: playbackDeadline + quietGapMs yielded a finite wait, so poke() armed a timer and the queue flushed ~800 ms after the estimated end of playback. The standalone daemon therefore loses autonomous delivery, which is the injector's entire purpose; a permission ask can sit silently while the agent blocks on the answer.
Verified empirically on a scratch checkout of head (packages/qwen-live, fake timers, quiet gap 800 ms), replaying the exact signal order production uses:
| Scenario | Outcome |
|---|---|
head, production order: noteResponseCreated → notePlaybackStarted ×3 → noteResponseDone → enqueue, then 10 minutes of silence |
not delivered, pendingCount stays 1 |
head, same, plus the one missing notePlaybackCompleted() |
delivered after the 800 ms quiet gap |
base injector, base production order: noteOutputAudio(48_000) ×2 → noteResponseDone → enqueue |
delivered automatically at +2.8 s |
head, noteSpeechStarted() + noteInputCommitted() |
delivered — i.e. only user speech unblocks |
Why CI is green: injector.test.ts calls notePlaybackCompleted() directly on the Injector (I ran injector.test.ts + live-session.test.ts at head: 66/66 pass), so the window semantics are pinned but the wiring is not — nothing exercises coordinator → session → injector.
Related, same root cause: the comment at packages/cli/src/serve/live/live-host-coordinator.ts:1000 states "The standalone qwen-live daemon wires these to its injector", which is not true at this head. The built-in CLI path itself is unaffected — it has no Injector and no byte estimate left to lose (no playbackDeadline / noteOutputAudio remains anywhere in packages/cli).
Fix direction: pass the two handlers in daemon.ts's setHandlers and route them to the active call's injector, keeping the epoch check the coordinator already applies — e.g. add LiveSession.notePlaybackStarted(call) / notePlaybackCompleted(call) that resolve this.active by epoch and call context.injector.…. Then pin the wiring with a session-level test that feeds a host.playback_completed frame into the coordinator and asserts the queued item is actually injected; that is the assertion that would have caught this, and it is precisely the gap the triage review left open ("that the injector window really is driven by host.playback_started/completed receipts … CI's unit suite pins only at the injector level").
Everything else I checked at head reviewed clean: the isTrustedSender fix in c530c46c99 is correct and matches every sibling channel (sender gate first, then Number.isSafeInteger(epoch) and epoch === daemon.getEpoch()); the live:audio:play payload change to { audio, epoch } has exactly one sender (main/index.ts:1003) and one receiver (preload/index.ts:100), both updated, so no stale-shape path remains; all three protocol version constants moved to 7 together (live-host/src/shared/protocol.ts, cli/src/serve/live/types.ts, qwen-live/src/host/types.ts) with the parity test updated; the audio engine's generation guard keeps a clear-induced onended from emitting a false completion; agent-detector.ts resolves absolute paths and spawns without a shell under a 3 s timeout; and the init wizard's output matches parseBackends / parseBackend field for field (default: true → isDefault, name pattern, kind: 'acp' with command/args/env), written 0o600 via tmp + rename.
阻塞合并的问题(一个):v7 播放回执的链路只到 coordinator 就断了 —— daemon.ts:154 的 setHandlers 只传了 onStart/onStop/onInputAudio,全仓库没有任何调用方提供 onPlaybackStarted/onPlaybackCompleted,因此 Injector.notePlaybackCompleted() 在生产代码里无人调用,而 notePlaybackStarted() 会在每个音频 delta 上触发(live-session.ts:473)。结果是助手第一次出声后 playbackInProgress 永久锁死,windowClosedForMs() 恒返回 -1,poke() 直接返回且不排定重试定时器,队列里的进度播报、结论和权限询问只有等用户再次说话才会投递;而 base 的按字节估算会在 quiet gap 后自动重开窗口。本地用 head 与 base 的 injector 做了 A/B 复现(head 静默 10 分钟不投递;补一次 notePlaybackCompleted() 即在 800ms 后投递;base 在 +2.8s 自动投递)。CI 绿是因为 injector.test.ts 直接调用 injector 方法,钉住了语义却没钉住接线。修复方向:在 setHandlers 里补上这两个回调并转发到当前 call 的 injector,同时补一个从 coordinator 喂入 host.playback_completed 帧、断言队列真的被注入的会话级测试。其余部分(isTrustedSender 修复、live:audio:play 载荷改造、三处协议版本号同步为 7、音频引擎 generation 守卫、agent 探测无 shell 注入、init 向导与配置 schema 逐字段吻合且 0o600 原子写入)复查均无阻塞问题。
| // v6 Host doesn't, so treat each audio delta as playback | ||
| // started as a forward-compatible fallback. The injector's | ||
| // window stays closed until clearOutput or speechStarted. | ||
| context.injector.notePlaybackStarted(); |
There was a problem hiding this comment.
This is the latch with no clearer. notePlaybackStarted() fires on every output audio delta, but Injector.notePlaybackCompleted() has no production caller: packages/qwen-live/src/daemon.ts:154 passes only { onStart, onStop, onInputAudio } to coordinator.setHandlers, and the coordinator's onPlaybackStarted? / onPlaybackCompleted? (host/live-host-coordinator.ts:108-109, invoked at :1123 / :1129) are optional and supplied nowhere in the repo.
So once the assistant's first audio frame arrives, playbackInProgress stays true for the rest of the call: windowClosedForMs() returns -1 (injector.ts:204) and poke() returns early without arming a re-check timer (injector.ts:215). Queued progress notices, completion summaries and permission asks then only flush when the user next speaks (noteSpeechStarted → noteInputCommitted), where at base the byte estimate reopened the window by itself after the quiet gap.
Reproduced A/B on a scratch checkout of head vs base, with the fix direction and the session-level test that would pin the wiring, in the review body.
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
…e with Host release
Two fixes in one commit:
1. Playback receipt wiring (qqqys's blocking review):
- daemon.ts setHandlers now passes onPlaybackStarted/
onPlaybackCompleted to the session
- LiveSession gains notePlaybackStarted/notePlaybackCompleted
methods that resolve the active context by epoch and forward
to the injector
- The per-delta notePlaybackStarted fallback in onOutputAudioDelta
is removed — it latched playbackInProgress closed with no matching
completion, blocking all queued injections until the user spoke
- v7 Host receipts now flow end-to-end: Host audio engine →
IPC → daemon-connection → WS frame → coordinator → session →
injector
- Test updated to call session.notePlaybackStarted directly
(matching the production coordinator→session path)
2. qwen-live npm publish in live-host-release.yml:
- Added checkout + install + build + npm publish steps to the
Host release publish job
- Version aligned with the Host release version via npm version
- Published as @qwen-code/qwen-live with --provenance
- Tag: "preview" for prereleases, "latest" for stable
- Skips if the version is already published
|
Fixed in bb83a29 — two changes: 1. Playback receipt wiring (qqqys's blocking finding):
2. qwen-live npm publish added to live-host-release.yml:
|
@qwen-code/qwen-live does not exist on npm yet; provenance requires an existing package. The workflow now checks if the package exists and omits --provenance on the first publish, adds it on subsequent ones.
6f620a7 to
0f85530
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
GitHub Actions expressions require single quotes for string literals; double quotes cause actionlint to fail.
|
Follow-up on my Request Changes (review 5087016437): the Critical is fixed at head I flagged that the v7 receipt chain terminated in the coordinator —
So the window now closes on the receipt, the flush timer arms without waiting for the next utterance, and the This is the independent confirmation of @LaZzyMan's Scope note: this is a re-check of that one finding only, not a review of the other commits pushed since ( |
|
Released in v0.23.0. |
What this PR does
Delivers M5 of the Live split roadmap (#10118): protocol v7 playback receipts and an interactive
qwen-live initsetup wizard.Protocol v7: real playback receipts
The injector's playback window previously guessed when audio finished playing by dividing byte count by 48 kB/s (24 kHz mono PCM16) and adding a quiet gap. Protocol v7 replaces this with real confirmation from the Host app:
host.playback_started { epoch }andhost.playback_completed { epoch }playback_startedwhen the first audio source starts (0→1 transition) andplayback_completedwhen the last source ends (1→0, generation-guarded soclearOutput'sstop()doesn't false-fire)playbackDeadline(byte estimation) withplaybackInProgressboolean +playbackCompletedAttimestamp. The injection window closes while playback is active and reopenscompletedAt + quietGapMsafter completiononOutputAudioDeltacallsnotePlaybackStartedas a fallback. The window is cleared byclearOutput/speechStartedas before — no byte-estimation wait, but the existing "speech after audio triggers clearOutput" behavior is preservedqwen-live init: interactive setupqwen-live initwalks a new user through first-time configuration:qodercli,qwen,gemini,claude,codex) via--versionprobe, resolving each to an absolute path. Native agents use their binary directly; adapter-based agents (claude/codex) configurenpxto run the ACP adapter, passing the detected binary via envLiveHostInstaller~/.qwen-live/config.jsonatomically (tmp + rename, mode 0o600)Why it's needed
M1–M4 shipped the daemon, the tool surface, the permission/steering disciplines, and the ACP multi-backend routing. M5 closes two gaps: the playback estimation was the last v6-protocol approximation the roadmap called out for replacement, and new users had no guided path from
npm installto a working voice session.Reviewer Test Plan
How to verify
Manual smoke (macOS, needs DashScope key + Host app):
Open the Live Host, start a voice call. After the model speaks, the injector log should show
playback_started/playback_completedevents arriving (not byte-estimation timers).Tested on
Environment
Linux, Node 22. Unit + e2e run on Linux; Host app playback receipts require macOS (Electron audio engine).
qwen-live initruns on all platforms.Risk & Scope
notePlaybackStarted(no byte estimation, slightly faster window reopen). The CLI coordinator copy also accepts v7 messages so a v7 Host connecting to built-inqwen serveis not rejected.live:audio:playIPC payload changed from bareUint8Arrayto{ audio, epoch }— the Host app's preload and main are updated together in this PR.Linked Issues
Part of #10118 (M5). Built on top of #10367 (M1+M2) and #10617 (M4), both merged.
中文说明
这个 PR 做了什么
交付 Live 拆分 roadmap(#10118)的 M5:协议 v7 播放回执和交互式
qwen-live init引导。协议 v7:真实播放回执
注入器的播放窗口之前靠"字节数 ÷ 48kB/s + 安静间隔"猜测播放是否结束。v7 用 Host 应用的真实回执替代:
host.playback_started和host.playback_completed(都带 epoch)playbackInProgress布尔值 +playbackCompletedAt时间戳替代playbackDeadline(字节估算)。播放中窗口关闭,完成后等 quiet gap 再开放notePlaybackStarted作为回退。窗口由clearOutput/speechStarted清除——不依赖字节估算,但保留"说话后清除播放"的既有行为qwen-live init:交互式引导qwen-live init引导新用户完成首次配置:~/.qwen-live/config.json(0o600 权限)验证
300 单测 / 17 e2e / 13 parity / 34 scripts 全绿。macOS Host 播放回执端到端需 Mac 实测。
风险
协议版本 6→7 需要 Host 应用同步升级才能收到回执。v6 Host 仍可工作(daemon 回退到 per-delta started,不阻塞)。
live:audio:playIPC 载荷从Uint8Array改为{ audio, epoch }(Host preload + main 同步更新)。关联 Issue
属于 #10118(M5)。基于 #10367(M1+M2)和 #10617(M4),均已合并。