Skip to content

fix(harness): interrupt orphan sub-agent when AgentSpawnTool parent subscription cancels (#2062)#2064

Merged
chickenlj merged 2 commits into
agentscope-ai:mainfrom
6or7:fix/agentspawntool-orphan-cancel
Jul 9, 2026
Merged

fix(harness): interrupt orphan sub-agent when AgentSpawnTool parent subscription cancels (#2062)#2064
chickenlj merged 2 commits into
agentscope-ai:mainfrom
6or7:fix/agentspawntool-orphan-cancel

Conversation

@6or7

@6or7 6or7 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix an orphan sub-agent leak in AgentSpawnTool.execWithTimeoutPromotion: when the parent's subscription to the
agentSpawn Mono is cancelled (outer Mono.timeout(), user stop, or upstream Reactor cancel), the inner execution
Mono — subscribed via fire-and-forget .subscribe(...) — was never disposed, so the spawned ReActAgent kept running
indefinitely.

Closes #2062.

Root cause

execWithTimeoutPromotion builds the result Mono<String> by wrapping an inner Mono<Msg> in a MonoSink +
CompletableFuture bridge, then does plain .subscribe(...) on the inner Mono:

Disposable innerSub = inner.subscribe(
    bridge::complete,
    bridge::completeExceptionally,
    () -> { if (!bridge.isDone()) bridge.complete(null); });

3. The returned Disposable is captured but never wired to the outer subscription, so Reactor cancel on the outer Mono
never propagates into the inner one. Same shape as the core SubAgentTool bug fixed in 029cc55e.

Fix

3. Two-part, mirroring 029cc55e but adapted for AgentSpawnTool's bridge wrapper:

  - inner.doFinally(CANCEL -> interruptAgent(...)) — when the inner Mono is disposed, call ReActAgent.interrupt(ctx)
to break the ReAct loop.
  - sink.onCancel(innerSub) — propagate outer Reactor cancel into the inner subscription so doFinally(CANCEL) actually
 fires.

What this does NOT change

  - Timeout promotion (race.orTimeoutAdoptedTaskRunSpec) — terminates via sink.success(...), not Reactor cancel, so
 doFinally(CANCEL) doesn't fire and the agent continues. Pinned by AgentSpawnToolPromotionTest.
  - Async cooperation (timeout_seconds == 0taskRepository.putTask) — separate code path, untouched.

Test plan

Two new regression tests in agentscope-harness:

| Test                           | Scenario                                            | Assertion
                                                                                              |
|--------------------------------|-----------------------------------------------------|------------------------------
----------------------------------------------------------------------------------------------|
| AgentSpawnToolOrphanCancelTest | outer Mono.timeout(2s) cancels mid-flight           | interrupt() called on
sub-agent; file lines + model calls stop growing within 4s window (delta1). Pre-fix delta was 25. |
| AgentSpawnToolPromotionTest    | internal race.orTimeout fires before agent finishes | captured spec is
AdoptedTaskRunSpec; its future() completes with the agent's reply; interrupt() is never called.           |

Full harness suite: 608 tests pass, 0 failures, 3 pre-existing skips.

  - mvn -pl agentscope-harness test -Dtest=AgentSpawnToolOrphanCancelTest
  - mvn -pl agentscope-harness test -Dtest=AgentSpawnToolPromotionTest
  - mvn -pl agentscope-harness test (full suite)
  - mvn -pl agentscope-harness spotless:check

@6or7 6or7 requested a review from a team July 8, 2026 15:47
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../agentscope/harness/agent/tool/AgentSpawnTool.java 94.11% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@6or7

6or7 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Hi maintainers — I'm the reporter of #2062 and submitted this fix shortly after filing the issue. I noticed #2063 by
@mroldx addresses the same root cause; happy to coordinate.

Quick comparison in case it helps the review:

  • Smaller diff: this PR only adds doFinally(CANCEL) + interruptAgent() + sink.onCancel(innerSub), mirroring
    the canonical fix 029cc55e already applied to core SubAgentTool. fix(harness): cancel spawned agent on parent dispose #2063 additionally introduces AtomicBoolean outerCancelled + Disposable.Swap + bridge.cancel(false) to handle the post-cancel race.whenComplete callback —
    not strictly required since the inner Mono is disposed before bridge completes.
  • Extra regression coverage: AgentSpawnToolPromotionTest verifies the timeout-promotion path
    (AdoptedTaskRunSpec) is not accidentally interrupted when the inner cancel hook fires — i.e., the new
    cancel-handler doesn't regress the promotion flow.

Either PR works — if you prefer #2063 for any reason, please consider cherry-picking AgentSpawnToolPromotionTest so
the promotion path stays protected.

@chickenlj chickenlj merged commit dfd5d12 into agentscope-ai:main Jul 9, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]:AgentSpawnTool leaks orphan ReActAgent on parent subscription cancel

2 participants