fix(harness): make memory flush fire-and-forget to unblock conversation completion - #2777
Open
chcodex wants to merge 3 commits into
Open
Conversation
…s the conversation completion signal MemoryFlushMiddleware used concatWith to chain the flush after the agent stream, so the downstream onComplete was delayed by a full LLM extraction round-trip plus disk writes on every call (default FlushMode.ALWAYS). Switch to doOnComplete + subscribeOn(boundedElastic()).subscribe() so the flush runs in the background and the conversation stream completes immediately. Snapshot the conversation list (new ArrayList<>(state.getContext())) before the async flush to avoid concurrent modification if the next agent call clears the state while the flush is still running. Also apply the same fire-and-forget pattern to MemoryMaintenanceMiddleware, which had the identical concatWith structure. Add MemoryFlushMiddlewareAsyncFlushTest to verify the agent stream completes before a never-finishing flush does, and that the flush is still triggered. Closes agentscope-ai#2774
…oid temp-dir race The async flush runs in the background after the agent stream completes, so HarnessAgent.close() could release the workspace while the flush was still writing memory/*.md — racing with @tempdir deletion in tests (intermittent "Failed to delete temp directory" under a full test run). Track in-flight fire-and-forget memory tasks (flush + maintenance) via a new process-wide MemoryBackgroundTasks counter, and wait for quiescence in HarnessAgent.close() before releasing resources, mirroring the existing SessionTree.awaitMirrorQuiescence pattern. Also rework MemoryFlushMiddlewareAsyncFlushTest to gate (rather than never complete) the flush stream so the background task is released at the end of the test and does not hold the in-flight tracker open for other tests.
Contributor
Author
Update: graceful shutdown for the async flush (commit
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…op flushes Address two review findings from the fire-and-forget flush change: 1. begin()/end() were wrapped around the subscribed Mono via doOnSubscribe/ doFinally, so they ran even when doFlush returned Mono.empty() (null state, empty messages, or trigger=skipped) — needless synchronized work on every call. Move begin() inside doFlush/doMaintenance so it only fires when there is actual work, and pair it with doFinally(end). 2. subscribeOn(boundedElastic()) schedules begin() asynchronously, leaving a tiny window where awaitQuiescence() could read a zero counter before begin() runs. Calling begin() synchronously before the Mono is returned eliminates that window.
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
MemoryFlushMiddlewareusedconcatWithto chain the memory flush after the agent stream, so the downstreamonCompletewas delayed by a full LLM extraction round-trip plus disk writes on every call (defaultFlushMode.ALWAYS). Switched todoOnComplete+subscribeOn(boundedElastic()).subscribe()so the flush runs in the background and the conversation stream completes immediately.new ArrayList<>(state.getContext())) before the async flush to avoid concurrent modification if the next agent call clears the state while the flush is still running.MemoryMaintenanceMiddleware, which had the identicalconcatWithstructure.MemoryFlushMiddlewareAsyncFlushTestto verify the agent stream completes before a never-finishing flush does, and that the flush is still triggered.Test plan
MemoryFlushMiddlewareAsyncFlushTestpasses (verifies stream completes before flush + flush still triggered).MemoryFlushMiddlewareTriggerTest(17 tests) passes.MemoryMaintenanceMiddlewareStaleEntryTest(3 tests) passes.agentscope-harnessmodule test suite passes (831 tests, 0 failures).Closes #2774