Skip to content

fix(harness): drain session-tree-mirror on HarnessAgent.close - #2301

Open
jialiuyang wants to merge 1 commit into
agentscope-ai:mainfrom
jialiuyang:fix/session-tree-drain-mirror-on-close
Open

fix(harness): drain session-tree-mirror on HarnessAgent.close#2301
jialiuyang wants to merge 1 commit into
agentscope-ai:mainfrom
jialiuyang:fix/session-tree-drain-mirror-on-close

Conversation

@jialiuyang

Copy link
Copy Markdown

AgentScope-Java Version

2.0.x (main)

Description

SessionTree schedules fire-and-forget remote mirrors on a daemon session-tree-mirror executor. Example tests that close HarnessAgent via try-with-resources can still race JUnit @TempDir cleanup when that thread writes under the workspace (agents/.../sessions), causing intermittent DirectoryNotEmptyException failures in CI.

This change adds SessionTree.awaitPendingMirrors and calls it from HarnessAgent.close() so teardown waits for in-flight mirrors. SessionTreeMirrorTest now uses the same drain helper instead of a fixed sleep.

Fixes #2251

Checklist

  • Code has been formatted with mvn spotless:apply
  • Targeted tests passing
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated — N/A
  • Code is ready for review

SessionTree mirrors session JSONL asynchronously on a daemon executor. Tests that
close HarnessAgent via try-with-resources can still race JUnit @tempdir cleanup when
that thread writes under the workspace. Await pending mirrors during close and use the
same drain helper in SessionTreeMirrorTest.

Fixes agentscope-ai#2251
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 31.25000% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...cope/harness/agent/memory/session/SessionTree.java 21.42% 11 Missing ⚠️

📢 Thoughts on this report? Let us know!

@AgentScopeJavaBot AgentScopeJavaBot added bug Something isn't working area/harness agentscope-harness (test/runtime support) labels Jul 19, 2026

@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 a race condition (issue #2251) where fire-and-forget async mirror tasks in SessionTree could still be uploading session files to the remote filesystem after HarnessAgent.close() returns, racing with JUnit @TempDir cleanup or workspace teardown. The fix introduces a clever awaitPendingMirrors() drain mechanism that exploits the FIFO ordering of the single-thread MIRROR_EXECUTOR: by submitting a no-op task and waiting for it, all previously queued mirrors are guaranteed to have completed. The test helper awaitMirror() is also upgraded from a fragile Thread.sleep(200) to the new deterministic drain. The implementation is clean, the exception handling is thorough (timeout, interrupt with flag restore, execution exception), and the approach is sound.

@@ -471,6 +475,38 @@ private void scheduleMirror() {
* Fetches the remote copy of {@code file} and parses it as JSONL session entries.

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.

[recommended] Orphaned Javadoc — placement breaks doc-to-method binding. The new awaitPendingMirrors method is inserted between the existing Javadoc for pullRemoteEntries (line 473-476) and the pullRemoteEntries method itself (line 508). In Java, only the last /** */ block immediately before a declaration is effective. The result: the old Javadoc (line 473) is now orphaned, and pullRemoteEntries (line 508) silently lost its documentation. Fix: Move the awaitPendingMirrors method above the orphaned Javadoc block, or move the orphaned Javadoc down to sit directly above pullRemoteEntries.

try {
// Drain fire-and-forget session mirrors so they cannot race TempDir / workspace
// cleanup after the agent is closed (see #2251).
SessionTree.awaitPendingMirrors(30, TimeUnit.SECONDS);

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.

[nitpick] Return value of awaitPendingMirrors is silently discarded. The method returns false on timeout/interrupt/failure, but close() ignores it. The internal log.warn calls inside awaitPendingMirrors already provide observability, so this is functionally fine for a best-effort drain. Consider adding a brief inline comment (e.g., // best-effort; warnings logged internally) to make the intent explicit for future maintainers.

try {
// Drain fire-and-forget session mirrors so they cannot race TempDir / workspace
// cleanup after the agent is closed (see #2251).
SessionTree.awaitPendingMirrors(30, TimeUnit.SECONDS);

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.

[nitpick] Hardcoded 30-second drain timeout. Mirrors typically complete in milliseconds, so 30 s is a generous safety net. If close() latency matters in any path, consider extracting this as a named constant (DRAIN_TIMEOUT_SECONDS) or making it configurable. Not blocking — the current value is reasonable.

@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 a race condition (issue #2251) where fire-and-forget async mirror tasks in SessionTree could still be uploading session files to the remote filesystem after HarnessAgent.close() returns, racing with JUnit @TempDir cleanup or workspace teardown. The fix introduces a clever awaitPendingMirrors() drain mechanism that exploits the FIFO ordering of the single-thread MIRROR_EXECUTOR: by submitting a no-op task and waiting for it, all previously queued mirrors are guaranteed to have completed. The test helper awaitMirror() is also upgraded from a fragile Thread.sleep(200) to the new deterministic drain. The implementation is clean, the exception handling is thorough (timeout, interrupt with flag restore, execution exception), and the approach is sound.

@@ -471,6 +475,38 @@ private void scheduleMirror() {
* Fetches the remote copy of {@code file} and parses it as JSONL session entries.

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.

[recommended] Orphaned Javadoc — placement breaks doc-to-method binding. The new awaitPendingMirrors method is inserted between the existing Javadoc for pullRemoteEntries (line 473-476) and the pullRemoteEntries method itself (line 508). In Java, only the last /** */ block immediately before a declaration is effective. The result: the old Javadoc (line 473) is now orphaned, and pullRemoteEntries (line 508) silently lost its documentation. Fix: Move the awaitPendingMirrors method above the orphaned Javadoc block, or move the orphaned Javadoc down to sit directly above pullRemoteEntries.

try {
// Drain fire-and-forget session mirrors so they cannot race TempDir / workspace
// cleanup after the agent is closed (see #2251).
SessionTree.awaitPendingMirrors(30, TimeUnit.SECONDS);

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.

[nitpick] Return value of awaitPendingMirrors is silently discarded. The method returns false on timeout/interrupt/failure, but close() ignores it. The internal log.warn calls inside awaitPendingMirrors already provide observability, so this is functionally fine for a best-effort drain. Consider adding a brief inline comment (e.g., // best-effort; warnings logged internally) to make the intent explicit for future maintainers.

try {
// Drain fire-and-forget session mirrors so they cannot race TempDir / workspace
// cleanup after the agent is closed (see #2251).
SessionTree.awaitPendingMirrors(30, TimeUnit.SECONDS);

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.

[nitpick] Hardcoded 30-second drain timeout. Mirrors typically complete in milliseconds, so 30 s is a generous safety net. If close() latency matters in any path, consider extracting this as a named constant (DRAIN_TIMEOUT_SECONDS) or making it configurable. Not blocking — the current value is reasonable.

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

This PR contains 107 lines of changes. Review in progress.


Automated review by github-manager-bot

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]: LocalFilesystemPersonalAssistantExampleTest flaky JUnit @TempDir cleanup race with session-tree-mirror thread

3 participants