fix(qqbot): streaming idle-flush with tool-call and stale-callback protection - #6204
Conversation
…otection Add streaming infrastructure to QQ Bot channel: - streamState Map with per-session buffer and 2s idle-flush timer - onResponseChunk: accumulates text, resets timer on each chunk - idleFlush: flushes accumulated buffer, coordinates with pendingStreamDelete - onToolCall: flushes buffer before tool execution with double-send guard - onResponseComplete: defers streamState cleanup during flush - _reconnectId monotonic counter for stale async callback detection - blockStreaming config-driven guard (skip streaming when enabled) - All sendMessage calls chained with .catch() for Node 22+ safety Add 25 stream tests covering: - idle-flush accumulation and timer reset - onToolCall immediate flush - pendingStreamDelete coordination - Stale reconnect callback guard - blockStreaming guard - Concurrent flush prevention
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Hey @Eric-GoodBoy-Tech — thanks for the PR! The streaming infrastructure work looks interesting.
However, the PR body doesn't follow our PR template. The template has specific headings that help reviewers quickly assess changes — things like What this PR does, Why it's needed, Reviewer Test Plan, Risk & Scope, and Linked Issues. Your current description uses a different structure (Summary, Changes, Testing, Dependencies).
Could you update the PR body to follow the template? It makes a big difference for review turnaround — maintainers prioritize PRs with a clear reviewer test plan.
A few specific things that would help:
- Why it's needed — explain the motivation: what user problem does streaming solve for QQ Bot users? Why idle-flush instead of sending every chunk?
- Reviewer Test Plan — how can a reviewer verify this works end-to-end? What should they expect to see?
- Risk & Scope — any known tradeoffs (e.g., the 2s idle timer adds latency to message delivery)?
- Linked Issues — link to the parent issue or PR #5902 with the right keyword (
Part of #5902)
Once the body is updated, happy to re-run the review. 🙏
中文说明
@Eric-GoodBoy-Tech 你好!感谢提交 PR,流式传输的基础设施看起来很有价值。
不过 PR 描述没有按照我们的 PR 模板 来填写。模板有固定的标题结构(What this PR does、Why it's needed、Reviewer Test Plan、Risk & Scope、Linked Issues 等),方便维护者快速评估。当前的描述用了不同的结构(Summary、Changes、Testing、Dependencies)。
请按照模板更新 PR 描述,这会显著加快审核速度。特别需要补充:
- Why it's needed — 为什么 QQ Bot 需要流式传输?idle-flush 方案解决了什么用户痛点?
- Reviewer Test Plan — 审核者如何端到端验证?预期行为是什么?
- Risk & Scope — 有没有已知的取舍(比如 2 秒 idle 计时器会增加消息延迟)?
- Linked Issues — 用正确的关键词关联到父 issue 或 PR #5902
更新后可以重新触发审核。
— Qwen Code · qwen3.7-max
…propagate-error, retry-timer, escape-fix
|
Thanks for the review! All feedback addressed in 480ab05: C1 — Duplicate message on C2 — Chunks dropped in C3 — Unreachable C4 — No retry timer after flush failure: Both C5 — PR body: Updated to follow the template with What/Why/Test Plan/Risk/Linked Issues sections. All 97 tests pass, typecheck clean. |
|
@qwen-code /resolve |
…or conflict Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge Conflict Resolution SummaryPR: #6204 - fix(qqbot): streaming idle-flush with tool-call and stale-callback protectionConflict Location
What ConflictedPR branch (HEAD) had: process.stderr.write(`[QQ:${this.name}] Send error: ${e}\n`);
if (propagateError) throw e;origin/main had: process.stderr.write(
`[QQ:${this.name}] Send error: ${sanitizeLogText(e instanceof Error ? e.message : String(e), 200)}\n`,
);ResolutionCombined both improvements:
Resolved code: process.stderr.write(
`[QQ:${this.name}] Send error: ${sanitizeLogText(e instanceof Error ? e.message : String(e), 200)}\n`,
);
if (propagateError) throw e;RationaleBoth changes serve different purposes:
The resolution preserves both improvements without conflict. Commit
|
|
@qwen-code /resolve |
…icts with origin/main
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge Conflict Resolution Summary — PR #6204PR: fix(qqbot): streaming idle-flush with tool-call and stale-callback protection Conflicted File
Conflict 1:
|
| Side | Code |
|---|---|
| HEAD (PR) | async sendMessage(chatId: string, text: string, propagateError = false): Promise<void> — 3-param signature, no <noreply> handling |
| main | async sendMessage(chatId: string, text: string): Promise<void> — 2-param signature + <noreply> suppression block at top of method |
Resolution: Took main's version (2-param + <noreply> suppression). The propagateError parameter is redundant because main's outer catch already does throw e; // Re-throw for .catch() callers, which achieves the same effect — callers with .catch() handlers receive the error regardless.
Post-conflict fix: Removed , true from two call sites:
this.sendMessage(state.chatId, buffer, true)→this.sendMessage(state.chatId, buffer)(idleFlush, ~line 493)this.sendMessage(state.chatId, buffer, true)→this.sendMessage(state.chatId, buffer)(onToolCall, ~line 542)
Conflict 2: Error handling after active retry fails
Location: ~line 339-389 (inside sendMessage, after markdown send fails and active retry also fails)
| Side | Code |
|---|---|
| HEAD (PR) | Old loop-based if (!resp.ok) with propagateError check and break to stop chunk iteration |
| main | Plain-text fallback path: when markdown fails and there's no reply context (no msg_id), sends as msg_type: 0 with comprehensive 429 and error handling |
Resolution: Took main's plain-text fallback entirely. HEAD's code was fro
- Extract IDLE_FLUSH_MS and MAX_FLUSH_RETRIES constants - Add JSDoc to state machine transitions - Implement onSessionDied lifecycle handler - Track retryCount per session for bounded retries - Sanitize log text via sanitizeLogText utility - Guard zombie timer with !current.timer check - Clean up flushedSessions and pendingStreamDelete entries - Add 8 error-recovery test cases (retry, max retries, pendingStreamDelete, onToolCall retry, stale closure, disconnect cleanup, onSessionDied, flushingSessions guard) - Fix vitest.config.ts server.deps.inline placement
wenshao
left a comment
There was a problem hiding this comment.
Qwen Code Review Summary
PR: #6204 — fix(qqbot): streaming idle-flush with tool-call and stale-callback protection
Agents: 9 parallel reviewers (correctness, security, code quality, performance, test coverage, attacker/oncall/maintainer mindsets, build verification)
Build/Tests: ✅ 36 tests passed, typecheck clean
Findings
| # | Severity | File | Summary |
|---|---|---|---|
| 1 | 🔴 Critical | QQChannel.ts:562 | flushedSessions.delete() in .catch() erases ALL prior delivery tracking → duplicate messages |
| 2 | 🔴 Critical | QQChannel.ts:546 | Orphaned .then() repopulates tracking sets after session death → silent data loss on session-ID reuse |
| 3 | 🟡 Suggestion | QQChannel.ts:552 | .then() accesses s.buffer without null guard — TypeError when streamState deleted in-flight |
| 4 | 🟡 Suggestion | QQChannel.ts:501 | Unbounded buffer accumulation — defeats streaming for fast LLM generation |
| 5 | 🟡 Suggestion | stream.test.ts | Missing test for .then() preserving streamState when chunks arrive during successful flush |
| 6 | 🟡 Suggestion | stream.test.ts:772 | onSessionDied test doesn't assert clearTimeout was called |
Note on existing issues
This PR already has ~38 inline comments from prior reviews. The findings above are new issues not covered by existing comments. The most impactful pre-existing issue remains the sendMessage resolves-on-HTTP-errors behavior (comment #31), which continues to affect the streaming layer's error recovery.
- Restore readyTimeout (30s READY guard) in dialGateway() - Restore heartbeatTimer.unref() in startHeartbeat() - Restore seenMessages.clear() in disconnect() - Restore event.author defensive check in handleC2C()
…move redundant chatId param
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Two bugs found in the streaming infrastructure:
-
Recursive
flushAndTrackdefeats concurrency guard + skips retry counter (Critical) — the.catch()handler's recursive call at line 636 passes the samestatereference, causing the outer.finally()to clearflushingSessionswhile the inner send is in-flight. Combined withreturnbypassingretryCount++, this creates both a concurrency guard defeat and a potential infinite async retry loop. -
readyTimeouttimer leaks ondisconnect()(Suggestion) —disconnect()callsthis.ws.close()and nullsthis.ws, which triggers the stale-close guard in theclosehandler (line 1122), skippingreadyTimeoutcleanup at line 1268. The 30-second timer keeps the Node.js process alive. Fix: addclearTimeout(this.readyTimeout); this.readyTimeout = null;before the streaming cleanup block.
… leak in disconnect
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Review Summary
Verdict: Comment (3 suggestions, no blockers)
This PR adds well-structured streaming infrastructure to the QQ Bot channel adapter. The idle-flush buffering, tool-call coordination, and stale-callback protection are thoughtfully designed, and the test coverage (41 tests, 1102 lines) is thorough.
Three minor suggestions below:
-
Missing
\nin.catch()stderr writes (lines 595, 621, 656) — These threeprocess.stderr.writecalls influshAndTrack's.catch()handler don't end with\n, unlike every other stderr write in this file. Output will run into the next log line. -
handleGroupmissing!event.authorguard —handleC2Cwas patched withif (!event.author) return;(line 1444) buthandleGroupaccessesevent.author.username(line 1481) without a null check, risking a TypeError on malformed events. -
readyTimeoutmissing.unref()(line 1099) — Every other timer in this class calls.unref()to avoid keeping the process alive. This 30-second timer is the exception.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
…stderr newlines, readyTimeout unref, reconnect log
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No new review findings beyond the 50+ existing inline comments from prior rounds. Downgraded from Approve to Comment: CI still running.
This PR has been reviewed to exceptional depth across multiple prior review rounds. The streaming state machine is well-structured and all 41 tests pass. Deterministic analysis (tsc, eslint) is clean. The extensive prior feedback covers race conditions, identity guards, retry logic, timer lifecycle, and test coverage gaps comprehensively.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
|
Thanks for the PR, @Eric-GoodBoy-Tech! Template: ✓ All required headings present. Problem: Real and observable. QQ Bot currently delivers responses as a single message only after full generation completes. For long responses or tool-call loops, users face 30+ seconds of silence. This is an inherent limitation of the current implementation. Direction: Aligned with the project. Feishu already implements progressive display via the same Solution: Scope is reasonable for streaming infrastructure. The ~1400 lines are mostly tests (1102 lines), with the streaming logic itself around 300 lines. Nothing here looks unnecessary for the stated goal of idle-flush buffering with tool-call coordination and stale-callback protection. Minor note: the PR title uses Moving to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:真实且可观测的问题。QQ Bot 目前仅在完整生成后才发送单条消息。对于长回复或工具调用循环,用户会面临 30 秒以上的无反馈等待。这是当前实现的固有局限。 方向:与项目对齐。飞书已通过相同的 方案:对于流式基础设施来说范围合理。约 1400 行主要是测试(1102 行),流式逻辑本身约 300 行。对于 idle-flush 缓冲加工具调用协调和过期回调保护的目标,没有看到不必要的内容。 小备注:PR 标题用了 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
|
Code review: The streaming state machine is well-documented with a clear comment block showing states (active → flushing → idle), transitions, and guards. All four override methods (
One minor cosmetic issue: a stray blank line was added between the No critical bugs found. Testing: 41 tests covering: idle-flush timer, tool-call flush, send failure re-buffer and retry, race conditions (stale reconnectId, flushingSessions guard), Real-scenario tmux testing: Not feasible — QQ Bot streaming requires a live QQ Gateway API WebSocket connection, which is unavailable in this CI environment. Unit tests are the primary verification for this channel-adapter change. Build: ✓ Full project build succeeds. The 中文说明代码审查:流式状态机有清晰的文档注释,展示了状态(active → flushing → idle)、转换和守卫。所有四个覆盖方法(
一个小的格式问题: 未发现关键 bug。 测试:41 个测试全部通过,覆盖 idle-flush 定时器、工具调用刷新、发送失败重新缓冲和重试、竞态条件、 真实场景 tmux 测试:不可行——QQ Bot 流式传输需要实时 QQ Gateway API WebSocket 连接,在当前 CI 环境中不可用。单元测试是此频道适配器变更的主要验证手段。 构建:✓ 完整项目构建成功。 — Qwen Code · qwen3.7-max |
|
Stepping back: this is solid engineering. The streaming coordination problem — concurrent sends, timer-based flushes, error recovery, session lifecycle — is genuinely complex, and it's handled cleanly. The state machine has clear documentation, the guards are disciplined (identity checks, flushing guards, reconnectId stale-callback protection), and the test suite covers state transitions and edge cases thoroughly (41 tests). The code follows project conventions. The override pattern matches what Feishu, Telegram, and other channel adapters already use. Every change in the diff is necessary for the stated goal — no drive-by refactors or scope creep. Approved. ✅ 中文说明退一步看:这是扎实的工程工作。流式协调问题——并发发送、基于定时器的刷新、错误恢复、会话生命周期——确实很复杂,而且处理得很干净。状态机有清晰的文档,守卫严谨(身份检查、刷新守卫、reconnectId 过期回调保护),测试套件全面覆盖了状态转换和边缘情况(41 个测试)。 代码遵循项目规范。覆盖模式与飞书、Telegram 和其他频道适配器已使用的模式一致。diff 中的每个更改都是实现目标所必需的——没有顺手重构或范围蔓延。 已批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
✅ Local verification report — real binary, no live QQ transportVerified PR head How it was verified
① + ⑤ Live streaming behavior (real base, real timers)The ② Why a real-base E2E was neededThe PR's own suite does
I also confirmed the invariant the completion path relies on: ③ Mutation testing (tests are non-vacuous)Each guard was broken in 🔎 Reverse audit
RecommendationMergeable as-is. The concurrent-send edge case above is optional hardening — it only triggers under a narrow rate-limit/replay window and does not lose data. Also independently sanity-checked: 🇨🇳 中文版(完整对应)✅ 本地验证报告 —— 真实二进制,无实时 QQ 连接在 PR head 验证方式
① + ⑤ 实时流式行为(真实 base + 真实计时器)
② 为什么需要真实 base 的 E2EPR 自带套件
我还确认了完成路径依赖的不变量: ③ 变异测试(证明测试非空过)逐个破坏 🔎 反向审计
建议可直接合并。 上述并发发送边界情况属于可选加固 —— 只在很窄的限流/回放窗口触发,且不丢数据。另外也独立核对了: Verified locally with a worktree at PR head; real compiled |



What this PR does
Adds streaming infrastructure to the QQ Bot channel adapter: idle-flush buffering, tool-call coordination, and stale-callback protection. Text chunks are buffered and flushed after 2 seconds of inactivity (idle-flush), or immediately before a tool call executes. Multiple race-condition guards prevent duplicate sends, stale callbacks from previous reconnects, and buffer loss during concurrent events. PR 3 of 4 splitting PR #5902.
Why it's needed
QQ Bot users currently receive AI responses as a single large message only after the entire generation completes. For long responses or agent tool-call loops, this creates a "blackout" period where nothing appears for 30+ seconds. Streaming with idle-flush delivers partial responses incrementally, giving users real-time feedback without overwhelming them (or hitting rate limits) with every individual token chunk. The 2s idle timer balances responsiveness against QQ Bot API rate limits.
Reviewer Test Plan
How to verify
blockStreaming: 'on'in the QQ Bot config — verify messages are sent only on completion, matching the pre-streaming behavior.Evidence (Before & After)
Before: User waits 30-60s with no feedback, then receives the full response as a single message.
After: User receives partial messages every 2s while the response generates, with tool-call results interleaved.
Tested on
Risk & Scope
blockStreaming: 'on'restores the original send-on-complete behavior.Linked Issues
Part of #5902