fix(btw): separate loop histories and submit work with a tool - #168
Merged
Conversation
Member
Author
|
@xero-team-bot r? @BegoniaHe |
|
✅ Requested a review from @BegoniaHe. |
The conversation loop and the work loop shared the session's conversation, so a detached work run and a chat run each committed a history snapshot read before the other wrote. Whichever finished second replaced the first, and a completion that carried an older in-process sequence was dropped as stale, so a finished turn went missing from the stored history (#152). Keep two histories instead of merging them. A session now owns one conversation per loop: the dialogue loop keeps the session's current conversation, and the work loop owns a second one, created on its first run and listed next to the chat under the same session. Work runs start from no history of their own, and the dialogue loop reads the work loop's recent complete turns as provider-only context, so the chat can refer to work results without either side entering the other's stored history. Handed-over turns are reduced to their text roles because tool calls would otherwise reach a provider without their paired results. Disabling BTW changes nothing: no work conversation is created, and a run reads its conversation history exactly as before. Refs #152 AI-Generated: true Generated-At: 2026-09-11T11:52:57Z
The work loop keeps a history-free conversation, so it needs the task itself rather than the chat it came from. Hand the task over the way an agent hands work to a tool: the conversation loop's model calls `submit_work_task` with a complete task prompt, the tool submits it and returns immediately, and the work loop runs the task detached in its own conversation and delivers the result through the normal result-decoration and send stages. The tool is offered only to the conversation loop of a profile with BTW and the work loop enabled, so the work loop cannot submit to itself. The handed over run is a new event: it mirrors the requesting event's subject, instance role, and auth resource, carries a fresh request id and no consumed WebChat step-up proof, and stays registered for stop and reset until its finalizer releases it. `btw_submit_work_task` exposes the same hand-off to plugins. Fixes #152 AI-Generated: true Generated-At: 2026-09-11T11:55:50Z
Break agent import cycles with deferred runtime imports and make asynchronous test awaits observable to CodeQL. AI-Generated: true Generated-At: 2026-09-11T14:29:55Z
BegoniaHe
force-pushed
the
fix/btw-dual-loop-history-152
branch
from
September 11, 2026 15:18
0acb0d4 to
621585f
Compare
| from astrbot.core.utils.task_utils import create_tracked_task | ||
|
|
||
| if TYPE_CHECKING: | ||
| from astrbot.core.execution_context import CoreExecutionContext |
|
|
||
| if TYPE_CHECKING: | ||
| from astrbot.core.execution_context import CoreExecutionContext | ||
| from astrbot.core.star.star import PluginRegistry |
| task.cancel() | ||
| with pytest.raises(asyncio.CancelledError): | ||
| await task | ||
| _cancelled_result = await task |
This was referenced Sep 11, 2026
YUZHEthefool
added this pull request to stack #174
September 11, 2026 15:57
10 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.
Summary
BTW's conversation loop and work loop shared one conversation, so a detached
work run and a chat run each committed a history snapshot that was read before
the other wrote. Whichever finished second won: the later commit replaced the
earlier turn, and when the later commit was the older in-process sequence it was
rejected as stale instead. A finished turn went missing either way.
Expected: both replies become complete history turns, and ordinary dialogue does
not wait for the background task.
Actual (issue #152): sequential commits return
[true, true]but the later oneoverwrites the earlier, and the reverse order returns
[true, false], discardingthe earlier run; only the second sequence's turns survive.
This change keeps two independent histories instead of merging them. A session
now owns one conversation per loop, a work run starts from no history of its own,
and the task reaches the work loop as an explicit tool call from the conversation
loop. The conversation loop can still read what the work loop produced.
Related issue
Fixes #152
Root cause
Every agent run resolves the session's current conversation, and both loops wrote
through the same
AssistantHistoryCommitter, whose per-conversation sequenceguard exists to stop an older snapshot from replacing newer data. But the two
loops hold different session locks (
<umo>for the dialogue loop,<umo>:workfor the work loop), so they genuinely overlap, and each one builds its snapshot
from the history it read before the other committed. The guard then had only two
outcomes, both lossy: overwrite with the newer sequence, or drop the older one.
Merging the two snapshots would keep both turns but leaves the two loops writing
one history, which is what the dual-loop split exists to avoid. Separating the
histories removes the conflict by construction.
Reproduction
group_sender_concurrencyat its defaultfalse, and use a local Agent./worktask that finishes slowly, then sendan ordinary chat message so both runs read the same initial history.
order reversed), then read
/conversation history.Before this change only the second sequence's turn remains in both orders. The
regression tests drive the same control flow: two runs over one in-memory
conversation store, released in both completion orders, asserting that both
complete turns are stored.
Implementation notes
conversation_mgr: each loop selects its own current conversation(
sel_conv_id,sel_conv_id:work). The work conversation is created on thework loop's first run and belongs to the same session, so both appear in the
session's conversation list. The parameter defaults to the dialogue scope, so
every existing caller keeps today's behavior.
_get_session_conv(scope)resolves per loop;_prepare_loop_contextsgives awork run an empty history and gives the dialogue run the work loop's recent
complete turns as provider-only context. Those turns carry
_no_saveand aredropped by the existing
_save_to_historyfilter, so neither stored historyreceives the other's content. Turns are reduced to their text roles: a tool
call without its paired result would be rejected by the provider.
submit_work_tasksubmits a model-authoredprompt, returns immediately, and the run is delivered through the existing
detached-work result path. The handed-over run is a new event that mirrors the
requesting event's subject, instance role, and auth resource but carries a
fresh request id and no consumed WebChat step-up proof.
reads its conversation history exactly as before.
Validation
make check,make quality(bandit, radon), and the fullpytest --test-profile blockingprofile were not run locally on this Windowscheckout; this PR relies on CI for those.
Compatibility and risk
submit_work_task, offered only to the conversation loop of a profilethat enables BTW and the work loop, and only to callers allowed
agent.manage(the same actionsend_message_to_userrequires). The work loopcannot submit to itself.
input is the explicit task, either the
/work <text>argument or the prompt theconversation loop supplies through the tool. A caller that expects the work loop
to see earlier chat must put that context in the prompt.
btw_submit_work_task;get_curr_conversation_idandnew_conversationgained a defaultedscopeparameter.by default.
Checklist
docs/zh/anddocs/en/when needed.docs/public/openapi.json, and tests change together when routes or schemas change.pyproject.toml,requirements.txt, anduv.locktogether.!and aBREAKING CHANGE:footer.Agent note
Goal: resolve #152 with the dual-loop history ownership the
report's acceptance criteria ask for, keeping BTW off by default and preserving
authorization and request identity.
Paths touched:
astrbot/core/conversation_mgr.py,astrbot/core/astr_main_agent.py,astrbot/core/agent/btw/{submission,work_loop,runtime_registry}.py,astrbot/core/tools/{work_tools,registry}.py,astrbot/core/tool_catalog.py,astrbot/core/pipeline/process_stage/stage.py,astrbot/api/__init__.py,docs/{zh,en}/dev/astrbot-config.md, and unit tests(
test_work_tools.pyis new;test_btw_work_loop.py,test_btw_capability_routes.py,test_astr_main_agent.py,test_conversation_manager.py, andtest_agent_internal_history.pyareextended).
Checks run: the commands listed under Validation above. The new dual-loop test
was also checked against the unmodified tree, where it fails for both completion
orders, so it is a real regression test rather than a description of current
behavior.
Residual risk: result delivery for a tool-submitted run reuses the detached-work
path with a synthetic event, which unit tests cover end to end through a stub
executor, but no live platform adapter was exercised here. The
/workcommandkeeps its explicit-argument semantics and therefore no longer implies the chat
history; that is the intended design but is worth a maintainer's eye.
Tools used: Claude Code (Opus 5) with the repository's AGENTS.md, AI_POLICY.md,
and conventional-commit references. Existing code was read directly, and the
reported symptoms were reproduced against the real
AssistantHistoryCommitterbefore the fix.