fix(realtime): 批量创建/删除日程时连续对话被强制挂断,且本地日历静默丢数据 - #343
Conversation
… writes Batch voice commands that call several tools within one realtime response raced two ways: send_tool_result() asked the vendor for a follow-up reply right after each tool call instead of waiting for that response's own response.done, so a second in-batch tool call collided with the vendor's "one response in flight" invariant and force-ended the call; and the frontend applied each voice.command.result fire-and-forget, letting concurrent local SQLite transactions race on withExclusiveTransactionAsync and silently drop writes the cloud had already committed. Fixes 1024XEngineer#341. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion Both bugs fixed in 407ab61 only surface when a single turn triggers several tool calls (batch create/delete). These tests exercise that shape directly through the real service/session code: - Two voice.command.result messages arriving back-to-back (no flush between them) must apply strictly in order; the second must not start until the first's transaction settles. - Two tool calls landing in one still-open realtime response must produce exactly one deferred response.create, sent only after that response's own response.done. Verified each test actually catches the regression by reverting its corresponding source fix locally and confirming the new test fails, then restoring the fix and confirming it passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
21cb0a8 to
1c74885
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Review conclusion
The realtime batch follow-up deferral is correct for multiple responding tools, and the frontend command-result chains correctly serialize local writes. One end-conversation path regresses turn settlement; see the inline finding. Focused validation reproduced it directly with the PR code.
The repository test dependencies are not installed in this workspace, so the full backend/frontend suites could not run.
response.done treated "a tool ran in this response" and "this response needs a follow-up" as the same condition and kept looping either way. A tool that ends the conversation asks for no follow-up (send_tool_result(..., respond=False)), so that response.done is the turn's actual last event: continuing past it left continuous mode never reporting turn_completed() (the call never hangs up) and push-to-talk reading past the end of the stream and reporting a spurious transport failure instead of settling cleanly. Only continue when a follow-up was actually requested and not suppressed; every other case -- including a suppressed one -- now falls through to the normal settlement path. Reported by the fennoai review bot on PR 1024XEngineer#343. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rite
commandResultChain used .then(onFulfilled, onRejected) to keep applying
queued voice.command.result writes after one fails. If the rejected branch
actually fires, that rejection sits unhandled until some later
queueCommandResult() call chains onto it -- and if there isn't one (e.g. it
was the last command result of the call), Node/Hermes treats it as an
unhandled rejection and crashes the process outright. Reproduced with a
state-subscriber listener that throws during markScheduleDataChanged()'s
notification.
Switch to .then(onFulfilled).catch(() => {}), the same idiom already used by
chainPlayback() in the same file: the .catch() is attached in the same
statement, so the rejection is neutralized immediately instead of waiting on
a future call that may never come.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Review conclusion
The response-follow-up state machine now defers exactly one response.create until the originating response.done, including the suppressed end-conversation path, and the two frontend services serialize command-result writes while preserving ack-after-write behavior. I found no additional actionable correctness or regression issues in the complete fixed diff.
git diff --check passed. The focused backend tests could not run because uv is not installed in the workspace, and frontend tests could not run because frontend/node_modules is absent.
变更说明
QwenAudioSession.send_tool_result()(后端)不再在每次工具调用完就立刻抢发response.create,而是延迟到该 response 自己的response.done到达之后再统一发一次。批量语音指令让模型在同一个 response 里连续调用多个工具时,不会再撞上 vendor "一个 session 同一时刻只能有一个 response 在跑" 的硬约束,也就不会再触发 "Cannot create response while another response is in progress." 报错、进而把连续对话强制挂断。AssistantContinuousConversationService/AssistantConversationService(前端)收到voice.command.result时改用commandResultChain排队(跟同文件里playbackChain/ExpoLocationMonitor.syncChain一个模式),不再并发触发。批量操作下多条voice.command.result前后脚到达时,本地 SQLite 写入严格按顺序执行,不会再因为并发抢withExclusiveTransactionAsync的原生连接而互相踩锁、导致云端已提交的数据静默丢失。Closes #341。
测试计划
uv run pytest tests/infrastructure/external/realtime/ tests/intelligence/realtime -q --no-cov(后端,全绿)npx tsc --noEmit(前端)npx eslint(改动到的前端文件)response.create只在该 response 的response.done之后统一发一次(push-to-talk / 连续模式各一条)。voice.command.result背靠背到达、第一条本地写入还没完成",断言第二条写入必须等第一条结束才能开始(两个 service 各一条)。