fix: skip the respond stage on an empty message chain - #9653
Merged
Soulter merged 2 commits intoSep 1, 2026
Conversation
Contributor
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- RespondStage now has two separate empty-chain checks (the new
not result.chainguard and_is_empty_message_chain); consider consolidating them or reusing_is_empty_message_chainto keep the emptiness criteria in one place. - The early-return condition on empty
result.chaindepends onchainnever beingNone; ifMessageEventResult.chaincan beNonein other paths, it might be safer to normalize or explicitly handle that case to avoid surprising behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- RespondStage now has two separate empty-chain checks (the new `not result.chain` guard and `_is_empty_message_chain`); consider consolidating them or reusing `_is_empty_message_chain` to keep the emptiness criteria in one place.
- The early-return condition on empty `result.chain` depends on `chain` never being `None`; if `MessageEventResult.chain` can be `None` in other paths, it might be safer to normalize or explicitly handle that case to avoid surprising behavior.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Soulter
approved these changes
Sep 1, 2026
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
stop_event()builds a fresh emptyMessageEventResultwhenever_resultisNone, and the respond stage clears the result right before it returns, so a plugin that yields its result and only then callsstop_event()leaves an empty result behind; the scheduler walks the remaining stages one more time after a generator stage is exhausted, and that pass treats the empty result as real, producing a contentlessPrepare to sendlog line and a secondafter_message_senthook run for every plugin on the same message.This is the same root cause as #7922 (
fix/stop-event-state-reset-by-clear-result). That PR introduced the independent_force_stoppedflag sois_stopped()no longer depends on_result, but it only touched the read path:stop_event()still callsset_result(MessageEventResult().stop_event())when there is no result. Since the stop state is now carried entirely by_force_stopped, that empty result object no longer serves any purpose, and it is the sole cause of the phantom pass described above. Removing those two lines fromstop_event()would fix this at the root, but that changes the behaviour of a core event API (plugins may still readevent.get_result().result_type), so this PR takes the conservative route and guards in the respond stage instead. Happy to switch to the root fix if maintainers prefer it.Note that this is not a duplicate of the empty-chain guards added in #2757 (fix #2670). Both of those guards live inside the
if len(result.chain) > 0:branch and protect against a chain that becomes empty afterReply/Atsegments are extracted, so a result whose chain is[]from the start bypasses them entirely.Modifications / 改动点
astrbot/core/pipeline/respond/stage.py: return early whenresult.chainis empty, right after the two streaming early-returns. This matches the existing_is_empty_message_chainguard further down, which also skips the hook andclear_result().STREAMING_RESULTis excluded because its content lives onasync_streamand an empty chain is normal there.tests/test_respond_stage.py: new tests covering the three paths — an empty result fromstop_event()is skipped silently, a streaming result with an empty chain is still delivered, and a result with content still goes through send plusafter_message_sentplusclear_result().Screenshots or Test Results / 运行截图或测试结果
Checklist / 检查清单
😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in
requirements.txtandpyproject.toml./ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到
requirements.txt和pyproject.toml文件相应位置。😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Bug Fixes: