Skip to content

fix(harness): detach memory maintenance from agent call response#2248

Open
Asaduddin18 wants to merge 3 commits into
agentscope-ai:mainfrom
Asaduddin18:bugfix/memory-consolidation-async-blocking
Open

fix(harness): detach memory maintenance from agent call response#2248
Asaduddin18 wants to merge 3 commits into
agentscope-ai:mainfrom
Asaduddin18:bugfix/memory-consolidation-async-blocking

Conversation

@Asaduddin18

@Asaduddin18 Asaduddin18 commented Jul 16, 2026

Copy link
Copy Markdown

AgentScope-Java Version

2.0.1-SNAPSHOT

Description

Background: MemoryMaintenanceMiddleware.onAgent concatenated the memory-maintenance Mono onto the returned Flux via .concatWith(...). Because ReActAgent.callInternal ends its call chain with .takeLast(1) (which can't emit until the upstream Flux signals onComplete), any caller consuming the agent response to completion — blockLast(), takeLast(1), or a WebFlux controller awaiting the Mono<Msg> — ended up waiting for the full LLM-based consolidation call inside consolidateMemory() (consolidator.consolidate(rc).block()), inflating agent-call latency by up to ~30s in the reported case. This also contradicted the harness memory documentation, which describes this step as running in the "background" in a "fire-and-forget" fashion.

Root cause: doOnComplete(...) (no scheduler switch) was the original implementation and matches what the docs still describe. A later, unrelated commit swapped it for .concatWith(Mono.fromRunnable(...).subscribeOn(Schedulers.boundedElastic())...), which moved the blocking work onto a different thread pool but did not actually detach it from the response — concatWith keeps the maintenance Mono part of the same Flux the caller is awaiting.

Changes:

  • MemoryMaintenanceMiddleware.onAgent: replaced .concatWith(...) with .doOnComplete(() -> ...).subscribe() so the maintenance Mono is genuinely detached — subscribed independently of the returned Flux, matching the fire-and-forget pattern already used elsewhere in this package (AsyncToolMiddleware, SubagentsMiddleware). The returned Flux now completes as soon as the underlying agent call does, regardless of how long maintenance takes. Error-handling semantics (onErrorResume + log.warn) are unchanged, only relocated.
  • MemoryConsolidator.consolidate(): added a .timeout(CONSOLIDATION_TIMEOUT) on the model call (reusing ExecutionConfig.MODEL_DEFAULTS's 5-minute timeout) so a hung model provider can no longer tie up a shared boundedElastic worker thread indefinitely — this pool is shared process-wide with 40+ other call sites.
  • Updated the class-level Javadoc on MemoryMaintenanceMiddleware to describe the actual (now-correct) detached behavior instead of the stale concatWith reference.

Why no doc changes: This fix restores the exact behavior the existing harness docs (docs/v2/*/docs/harness/memory.md, compaction.md, workspace.md) already describe (background/fire-and-forget maintenance) — so those docs are accurate again as a result of this fix, rather than needing separate correction.

How to test:

mvn test -pl agentscope-harness -am -Dtest=MemoryMaintenanceMiddlewareAsyncBehaviorTest,MemoryConsolidatorTimeoutTest
  • MemoryMaintenanceMiddlewareAsyncBehaviorTest.onAgent_completesBeforeSlowConsolidationFinishes: mocks a slow MemoryConsolidator.consolidate() call and asserts onAgent(...)'s returned Flux completes in well under the consolidation duration — this test fails against the pre-fix concatWith code (verified locally: takes 3s instead of completing immediately).
  • MemoryMaintenanceMiddlewareAsyncBehaviorTest.onAgent_onErrorResume_handlesExceptionFromDetachedMaintenance: verifies the detached maintenance error path is still handled by onErrorResume (using Hooks.onErrorDropped to detect if it would otherwise be silently dropped).
  • MemoryConsolidatorTimeoutTest.consolidate_timesOutInsteadOfHangingForever: uses StepVerifier.withVirtualTime to confirm a model that never responds causes consolidate() to error with a TimeoutException instead of hanging — verified this test fails to even compile without the fix (confirming it's tied to the change).

Full harness module suite: 637/637 tests passing, 0 failures/errors. Every changed/added line has full line and branch coverage (verified via JaCoCo — no partial coverage on any touched line).

Fixes #2225

Checklist

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test)
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (e.g. links, examples, etc.)
  • Code is ready for review

Asaduddin18 and others added 2 commits July 16, 2026 09:59
MemoryMaintenanceMiddleware concatenated the maintenance Mono onto the
returned Flux via concatWith, so callers that consume the response to
completion (blockLast(), takeLast(1)) waited for the full LLM-based
consolidation call to finish -- inflating agent call latency by up to
30s and contradicting documentation that describes this as a
background/fire-and-forget step.

Detach maintenance via doOnComplete(...).subscribe() instead, so the
returned Flux completes as soon as the underlying agent call does,
independent of maintenance duration. Also add a timeout to the
consolidation LLM call so a hung provider can't tie up a shared
boundedElastic thread indefinitely.

Fixes agentscope-ai#2225
@Asaduddin18 Asaduddin18 marked this pull request as ready for review July 16, 2026 04:49
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@AgentScopeJavaBot AgentScopeJavaBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 AI Review

This PR fixes #2225: MemoryMaintenanceMiddleware.onAgent previously concatenated the maintenance Mono onto the response Flux via .concatWith(...), forcing callers (e.g., blockLast(), takeLast(1)) to wait for the full consolidation chain (including LLM call) before receiving the response. Changed to .doOnComplete(() -> Mono...subscribe()) to genuinely detach maintenance as fire-and-forget. Also added .timeout(CONSOLIDATION_TIMEOUT) to MemoryConsolidator.consolidate() to prevent a hung model provider from permanently blocking a boundedElastic thread. Two regression test suites cover async behavior and timeout semantics. The fix is correct, well-scoped, and aligns with the issue description.

Recommendation: COMMENT

@AgentScopeJavaBot AgentScopeJavaBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 AI Review

This PR fixes #2225: MemoryMaintenanceMiddleware.onAgent previously concatenated the maintenance Mono onto the response Flux via .concatWith(...), forcing callers (e.g., blockLast(), takeLast(1)) to wait for the full consolidation chain (including LLM call) before receiving the response. Changed to .doOnComplete(() -> Mono...subscribe()) to genuinely detach maintenance as fire-and-forget. Also added .timeout(CONSOLIDATION_TIMEOUT) to MemoryConsolidator.consolidate() to prevent a hung model provider from permanently blocking a boundedElastic thread. Two regression test suites cover async behavior and timeout semantics. The fix is correct, well-scoped, and aligns with the issue description.

Recommendation: COMMENT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/harness agentscope-harness (test/runtime support) bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]:MemoryConsolidator blocks agent call via concatWith + .block()

2 participants