Skip to content

fix: skip the respond stage on an empty message chain - #9653

Merged
Soulter merged 2 commits into
AstrBotDevs:masterfrom
KBVsent:fix/respond-stage-empty-chain
Sep 1, 2026
Merged

fix: skip the respond stage on an empty message chain#9653
Soulter merged 2 commits into
AstrBotDevs:masterfrom
KBVsent:fix/respond-stage-empty-chain

Conversation

@KBVsent

@KBVsent KBVsent commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

stop_event() builds a fresh empty MessageEventResult whenever _result is None, and the respond stage clears the result right before it returns, so a plugin that yields its result and only then calls stop_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 contentless Prepare to send log line and a second after_message_sent hook 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_stopped flag so is_stopped() no longer depends on _result, but it only touched the read path: stop_event() still calls set_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 from stop_event() would fix this at the root, but that changes the behaviour of a core event API (plugins may still read event.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 after Reply/At segments are extracted, so a result whose chain is [] from the start bypasses them entirely.

Modifications / 改动点

  • astrbot/core/pipeline/respond/stage.py: return early when result.chain is empty, right after the two streaming early-returns. This matches the existing _is_empty_message_chain guard further down, which also skips the hook and clear_result(). STREAMING_RESULT is excluded because its content lives on async_stream and an empty chain is normal there.
  • tests/test_respond_stage.py: new tests covering the three paths — an empty result from stop_event() is skipped silently, a streaming result with an empty chain is still delivered, and a result with content still goes through send plus after_message_sent plus clear_result().
  • This is NOT a breaking change. / 这不是一个破坏性变更。

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.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Bug Fixes:

  • Skip sending and post-send hooks for non-streaming results with empty message chains, preventing phantom response passes and duplicate hooks.

@dosubot dosubot Bot added size:XS This PR changes 0-9 lines, ignoring generated files. area:core The bug / feature is about astrbot's core, backend labels Aug 13, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • 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.
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.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Soulter
Soulter merged commit 385e4b6 into AstrBotDevs:master Sep 1, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]触发网页搜索后会先回复一个空的消息

2 participants