Skip to content

fix: flush Coze stream tail when no done event is emitted - #2518

Open
WODE25500 wants to merge 2 commits into
langbot-app:masterfrom
WODE25500:fix/coze-stream-tail-flush
Open

WODE25500 wants to merge 2 commits into
langbot-app:masterfrom
WODE25500:fix/coze-stream-tail-flush

Conversation

@WODE25500

@WODE25500 WODE25500 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What

Self-hosted Coze does not emit a terminating done event, but the runner detects the
terminal event with event_type.split('.')[-1] == 'done', so is_final can stay False
for the whole stream. Since the runner only flushed on a message_idx % 8 == 0 boundary:

  • tail text that did not land on a boundary was never yielded;
  • a stream ending exactly on a boundary never delivered is_final at all, leaving the
    upstream stream open -- adapters that only deliver on is_final (e.g. WeCom) hang
    until timeout.

The runner now emits one final cumulative snapshot after the loop whenever no terminal
event was seen. The guard is not is_final rather than a content-length comparison: on
a boundary ending the content length is unchanged, yet the terminal signal still has to
be sent. Re-sending the same cumulative snapshot is safe -- every reply_message_chunk
implementation treats chunk content as the full message text (replace semantics, never
append).

Tests

tests/unit_tests/provider/runners/test_coze_tail_flush.py drives the real runner
(_chat_messages_chunk) with a mocked Coze client:

  • stream lengths 1..40 with no done event -> the last chunk is complete and is_final;
  • boundary endings (7/15/23/31/39) -> exactly one terminal snapshot;
  • done at every position, plus the self-hosted end event
    (conversation.chat.completed), which the runner does not match;
  • an error event and a mid-stream exception are never reported as success.
uv run pytest tests/unit_tests/provider/runners/test_coze_tail_flush.py -q
uv run ruff check src/langbot/ tests/

@RockChinQ

Copy link
Copy Markdown
Member

@dadachann Check if this PR is okay for merging, if not, submit your review

@dadachann

Copy link
Copy Markdown
Collaborator

👋 I’m dadachann, the LangBot community maintenance agent. I’ve picked up the review/task on this pull request and will share findings and next steps here.

@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dadachann

Copy link
Copy Markdown
Collaborator
  • Your tail flush helps short streams, but the Coze runner still omits a final signal at chunk boundaries. This remains a blocker: yielding the text does not guarantee QQ sends it.
  • Isolated replay using the real runner and simulated QQ sending reproduced truncation: 15 rapid increments sent only the first 7 characters and left the stream open; 16 completed correctly. A 7-increment fallback case sent nothing. Live Coze-to-QQ reproduction remains unverified.
  • A fix should guarantee a complete final snapshot on normal stream completion, even when its length is unchanged, without treating errors or cancellation as success. The added tests exercise a handwritten emulator rather than the runner; replace these with real-runner tests and adapter-throttling coverage. No product changes were made during this review.

自托管 Coze 的结束事件不是 done,runner 又只在 message_idx % 8 == 0 的边界
flush,于是落在边界之后的尾巴会丢;恰好结束在边界上时更糟,一个 is_final 的块
都不会发,respback 取到的 is_final 恒为 False,wecombot 这类"只在 is_final 时
投递"的适配器会一直挂到超时。

循环结束后补发一次完整内容并标记 is_final,保证正常结束时上游一定收到终态快照。
判断用 is_final 而不是内容长度:结束在边界上时长度没有变化,但终态信号仍然必须
补发;重复发送的是同一份累计内容,下游是覆盖语义,重复无副作用。error 事件与
取消路径在循环内 return/raise,走不到这里。

测试直接驱动真实 runner,覆盖 1..40 全长度、7/15/23/31/39 边界长度、done 出现在
各位置、自托管结束事件、error 事件与中途抛异常。

Refs langbot-app#2518
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.

3 participants