fix(btw): stop a third-party work run when its request is stopped - #173
Merged
YUZHEthefool merged 1 commit intoSep 14, 2026
Merged
YUZHEthefool merged 1 commit into
YUZHEthefool merged 1 commit into
Conversation
/task stop for a third-party runner records the stop on the event, but the detached run never read it: it kept pulling responses from the runner and finished as completed. Nothing was cancelled, and the event's own stop flag was set the whole time. Honour the stop on all three paths. The third-party consumer stops draining the runner, the work loop stops dispatching delivered results and closes the execution so the runner is released, and a run still queued behind the semaphore never starts. A stopped run is a cancellation, not a runner failure, so the third-party handler reports nothing instead of the fallback error an unfinished stream would produce. This ends the local wait only: it does not revoke a task the remote service already accepted, which the /task stop docs now say. Fixes #157 AI-Generated: true Generated-At: 2026-09-11T14:53:39Z
YUZHEthefool
added this pull request to stack #174
September 11, 2026 15:57
YUZHEthefool
force-pushed
the
fix/btw-stop-third-party-work-157
branch
from
September 11, 2026 16:00
712207a to
5df3a47
Compare
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
/task stopon a running third-party work task did not stop local execution:the run kept consuming the runner's responses after the stop and finished as
completed. Expected the local wait and consumption to end, the runner to beclosed, and the task recorded
cancelled.Related issue
Fixes #157
Root cause
/task stopfor a third-party runner goes throughActiveEventRegistry.stop_all,which sets the event's stop flag — and the detached run never read it. The flag
was set the whole time while
WorkLoop._run_detachedpulled every response andthe third-party consumer drained the runner.
Reproduction
The regression tests build the real
ActiveEventRegistry, the realThirdPartyAgentSubStagenon-streaming handler, and a realWorkLoop, then stopthe registered work event while a stub runner is still producing. Before this
change all three responses were consumed and the session ended
completed.Implementation notes
Honour the stop on each path where the run can be waiting:
run_third_party_agentgained an optionalshould_stoppredicate, polled asthe stream is consumed; both handlers pass
event.is_stopped. Optional anddefaulted, so existing callers are unaffected.
stream would otherwise produce — a stop is not a runner failure.
_run_detachedstops dispatching delivered results and closes the execution,which releases the executor and the runner behind it.
Scope, stated plainly: this ends the local wait and consumption. It does not
revoke a task the third-party service has already accepted, and the
/task stopdocumentation in both languages now says so rather than implying otherwise.
Validation
Both suites above ran on this branch's tip. The new tests were run against the
unmodified tree first: one consumed all three responses and delivered them, the
other started a task whose request had already been stopped.
pnpm run docs:buildwas not run locally (the docs dependencies are notinstalled in this checkout), and neither were
make check,make quality, or thecoverage-gated
--test-profile allrun; this PR relies on CI for those, as #168did.
After #168 was rebased onto the updated
masterhead, this branch was rebased ontothe new head as well and the whole stack was re-validated at its tip:
pytest tests/unit -q5233 passed / 6 skipped andpytest --test-profile blocking -q5354 passed / 6 skipped / 1 deselected.The count above was taken on this branch before that rebase.
Compatibility and risk
run_third_party_agentgained an optional defaulted parameter; existing callsites and tests are unchanged.
error result, and its work task is
cancelledrather thancompleted.is_stopped/stop_eventsurface, whichthe stage now uses. No route, schema, configuration, or dependency change.
Checklist
docs/zh/anddocs/en/when needed.docs/public/openapi.json, and tests change together when routes or schemas change. (No route or schema change.)pyproject.toml,requirements.txt, anduv.locktogether. (No dependency change.)!and aBREAKING CHANGE:footer. (None; the status change is the fix.)Agent note
Goal: resolve #157 by making an in-flight third-party work run
honour the stop request, keeping BTW off by default and preserving authorization
and request identity.
Paths touched:
astrbot/core/agent/btw/work_loop.py,astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py,docs/{zh,en}/use/command.md, and unit tests(
test_btw_delivery.pyextended;agent_sub_stage_support.py,test_btw_work_loop.py,test_third_party_agent_sub_stage.py,test_conversation_loop.pyupdated for the event surface the stage now uses).Checks run: the commands under Validation, plus the new tests against the
unmodified tree.
Residual risk: the stop is observed between responses, so a runner blocked inside
a single long network call is not interrupted until that call returns. The
should_stoppoll prevents pulling further responses, and closing the executionreleases the runner; neither claims the remote task was cancelled.
Tools used: Claude Code (Opus 5) with the repository's AGENTS.md and AI_POLICY.md.
This PR is stacked on #172; merge that first.