Skip to content

fix: 修复 run_agent() 共享引用污染和 send_message() 历史丢失#8443

Open
Sisyphbaous-DT-Project wants to merge 3 commits into
AstrBotDevs:masterfrom
Sisyphbaous-DT-Project:fix/shared-reference-and-history-persistence
Open

fix: 修复 run_agent() 共享引用污染和 send_message() 历史丢失#8443
Sisyphbaous-DT-Project wants to merge 3 commits into
AstrBotDevs:masterfrom
Sisyphbaous-DT-Project:fix/shared-reference-and-history-persistence

Conversation

@Sisyphbaous-DT-Project
Copy link
Copy Markdown
Contributor

@Sisyphbaous-DT-Project Sisyphbaous-DT-Project commented May 30, 2026

Modifications / 改动点

修复两个导致插件发出的消息在 LLM 对话历史中丢失或截断的核心 Bug。

问题 1:run_agent() 共享引用污染

run_agent() 在构造 MessageEventResult 时,将 Provider 层 LLMResponse.result_chain.chain 的 list 对象直接传递(未做拷贝)。当输出增强等插件在 OnDecoratingResultEvent 阶段对 event.get_result().chain 执行原地修改(clear() + extend())时,会直接污染 Provider 层的 LLMResponse.result_chain,导致 completion_text property(实时从 result_chain 计算)被永久截断或清空。

根因astr_agent_run_util.py 两处 MessageEventResult(chain=...) 直接传递了 list 引用。

问题 2:Context.send_message() 不写对话历史

Context.send_message() 仅将消息投递给平台适配器,无任何逻辑将消息追加到对应会话的 conversation.history。所有通过此 API 发出的消息(输出增强的前 N-1 分段、主动消息插件发出的消息、任何插件的主动发送)对 LLM 完全不可见。

根因context.pysend_message() 只调用 platform.send_by_session(),未写入 conversation_manager

修改的文件

  • astrbot/core/astr_agent_run_util.py — 两处 chain=... 改为 chain=list(...),浅拷贝切断共享引用

  • astrbot/core/star/context.pysend_message() 发送成功后自动追加 assistant 消息到对话历史,含去重检查(跳过最后一条已是同内容 assistant 消息的情况)

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

独立复现脚本 reproduce_bugs.py 验证修复前后对比:

修复前:

复现 1:共享引用污染
  provider_chain.chain = ['完整原文:第一段。第二段。第三段。']
  [SplitStep 执行后]
  provider_chain.chain = ['最后一段']
  llm_resp.completion_text = '最后一段'
  ❌ BUG 确认:completion_text 被截断

复现 2:send_message() 不写历史
  [send_message] 发送消息: '这是分段1'
  [send_message] 发送消息: '这是分段2'
  conversation.history = []
  ❌ BUG 确认:发送 2 条消息后 history 为空

复现 3:_save_to_history 守卫误触发
  completion_text = ''
  save_to_history 结果: skipped
  ❌ BUG 确认:当 result_chain 被清空时,历史完全丢失

修复后:

验证 1:共享引用污染
  ✅ llm_resp.completion_text 未被截断

验证 2:send_message() 写历史
  ✅ send_message 正确写历史且去重有效

验证 3:_save_to_history 守卫
  ✅ 守卫不会误触发

验证 4:主动消息插件切断引用
  ✅ 主动插件原始 chain 未被污染

验证 5:主动消息去重适配
  ✅ 主动插件正确处理 send_message 预写入的消息

🎉 所有修复验证通过!

Checklist / 检查清单

  • 👀 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.
    / 我确保没有引入新依赖库。

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

Summary by Sourcery

Ensure assistant messages sent via the platform API are persisted to conversation history and prevent plugins from mutating underlying LLM response chains through shared list references.

Bug Fixes:

  • Fix run_agent using shared list references for LLM result chains that allowed plugins to inadvertently truncate or clear provider-level responses.
  • Fix Context.send_message not appending sent assistant messages to the associated conversation history, causing proactive and split messages to be invisible to subsequent LLM turns.

- run_agent() 传递事件结果链时使用 list() 浅拷贝切断共享引用,
  防止输出增强等插件的原地修改污染 Provider 层的 LLMResponse,
  completion_text property 不再被截断或清空
- Context.send_message() 发送成功后自动追加 assistant 消息到
  对话历史,解决分段消息和主动消息对 LLM 不可见的问题,含去重检查
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:core The bug / feature is about astrbot's core, backend area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels May 30, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The new send_message history persistence logic duplicates conversation-handling concerns inside Context; consider extracting this into a reusable helper or pushing it down into conversation_manager so that all history updates follow a single code path.
  • In send_message, you're catching a broad Exception and only logging at debug level; for issues that cause history to stop updating it may be safer to log at warning/error or narrow the exception scope so silent failures are less likely.
  • The JSON encode/decode of conv.history inside send_message tightly couples this method to the storage format; if possible, hide the serialization detail behind the conversation manager (e.g., expose history as a list and let it handle JSON) to make future format changes easier.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `send_message` history persistence logic duplicates conversation-handling concerns inside `Context`; consider extracting this into a reusable helper or pushing it down into `conversation_manager` so that all history updates follow a single code path.
- In `send_message`, you're catching a broad `Exception` and only logging at debug level; for issues that cause history to stop updating it may be safer to log at warning/error or narrow the exception scope so silent failures are less likely.
- The JSON encode/decode of `conv.history` inside `send_message` tightly couples this method to the storage format; if possible, hide the serialization detail behind the conversation manager (e.g., expose history as a list and let it handle JSON) to make future format changes easier.

## Individual Comments

### Comment 1
<location path="astrbot/core/star/context.py" line_range="477-486" />
<code_context>
+                try:
</code_context>
<issue_to_address>
**issue (bug_risk):** The broad `except Exception` here can hide real issues in history persistence.

Catching `Exception` around all history operations will swallow bugs in JSON parsing, retrieval, or updates and only emit a debug log, making data issues hard to debug. Narrow the try/except to the specific risky calls (e.g., `json.loads`), or at least use `logger.exception` and include identifiers like `unified_msg_origin`/`cid` to capture a stack trace and improve observability.
</issue_to_address>

### Comment 2
<location path="astrbot/core/star/context.py" line_range="475" />
<code_context>
             if platform.meta().id == session.platform_name:
                 await platform.send_by_session(session, message_chain)
+
+                # Persist the sent message to conversation history so that
+                # LLM can see proactive / split messages in subsequent turns.
+                try:
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the new history persistence and deduplication logic into dedicated helper methods so `send_message` focuses only on sending messages.

You can keep the new behavior but reduce complexity in `send_message` by extracting the persistence logic and encapsulating the JSON/dedup rules.

### 1. Extract persistence out of `send_message`

Current core loop is doing both sending and history management. Move the persistence into a helper to make `send_message` focus on sending:

```python
for platform in self.platform_manager.platform_insts:
    if platform.meta().id == session.platform_name:
        await platform.send_by_session(session, message_chain)
        await self._persist_sent_message_to_history(session, message_chain)
        return True
```

New helper (same class):

```python
async def _persist_sent_message_to_history(self, session: MessageSesion, message_chain):
    try:
        import json

        unified_msg_origin = str(session)
        cid = await self.conversation_manager.get_curr_conversation_id(unified_msg_origin)
        if not cid:
            return

        conv = await self.conversation_manager.get_conversation(unified_msg_origin, cid)
        if not conv:
            return

        plain_text = message_chain.get_plain_text()
        if not plain_text:
            return

        history = json.loads(conv.history) if conv.history else []
        if self._is_last_assistant_message(history, plain_text):
            return

        history.append({"role": "assistant", "content": plain_text})
        await self.conversation_manager.update_conversation(
            unified_msg_origin=unified_msg_origin,
            conversation_id=cid,
            history=history,
        )
    except Exception as e:
        logger.debug(f"send_message: failed to persist to history: {e}")
```

This keeps all behavior but flattens the control flow in `send_message`.

### 2. Extract the deduplication rule

The inline deduplication check is noisy. Moving it to a small helper improves readability and keeps the rule centralized:

```python
def _is_last_assistant_message(self, history, plain_text: str) -> bool:
    if not history or not isinstance(history[-1], dict):
        return False
    last = history[-1]
    return last.get("role") == "assistant" and last.get("content") == plain_text
```

Then the helper reads more declaratively:

```python
if self._is_last_assistant_message(history, plain_text):
    return
```

These two extractions keep your new feature intact while reducing nesting, separating responsibilities, and making future changes to history/dedup behavior easier.
</issue_to_address>

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.

Comment thread astrbot/core/star/context.py Outdated
Comment thread astrbot/core/star/context.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request converts message chains to lists in astr_agent_run_util.py and adds a mechanism in context.py to persist sent messages to the conversation history. The reviewer identified a potential race condition in send_message when multiple messages are sent concurrently, as the asynchronous history retrieval and update operations are not atomic. They suggested using asyncio.Lock to serialize history updates per session.

Comment thread astrbot/core/star/context.py Outdated
将 send_message 中的历史持久化逻辑提取到 _persist_sent_message_to_history
和 _is_last_assistant_message 两个私有方法,减少 send_message 的职责耦合。

将 broad except Exception + logger.debug 改为分级异常处理:
- json.JSONDecodeError → logger.warning
- Exception → logger.warning
均包含 session 和 cid 标识,便于排查。
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 area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant