Skip to content

fix(skills): run orphan GC on empty merged skills to clear stale cache - #2665

Open
weixsun wants to merge 1 commit into
agentscope-ai:mainfrom
weixsun:main
Open

fix(skills): run orphan GC on empty merged skills to clear stale cache#2665
weixsun wants to merge 1 commit into
agentscope-ai:mainfrom
weixsun:main

Conversation

@weixsun

@weixsun weixsun commented Aug 11, 2026

Copy link
Copy Markdown

Description

Closing #2664

Checklist

Please check the following items before code is ready to be reviewed.

  • 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

@CLAassistant

CLAassistant commented Aug 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.21053% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rness/agent/middleware/HarnessSkillMiddleware.java 84.21% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@weixsun
weixsun force-pushed the main branch 2 times, most recently from 85a7d19 to e3f69a0 Compare August 11, 2026 10:58
@AgentScopeJavaBot AgentScopeJavaBot added bug Something isn't working area/harness agentscope-harness (test/runtime support) labels Aug 12, 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 real and impactful bug: when all skills are removed from repositories or filtered out by visibility/skill filters, stale content in .skills-cache/ was never cleaned up, causing outdated skill resources to leak into the sandbox. The fix introduces a MergeResult record that carries both the merged skills map and a loadFailed flag, enabling a new stageEmptyRetainedIfSafe() helper that triggers orphan GC with an empty retained set — but only when the empty result is intentional (not caused by a transient repository failure). The outage-protection design is thoughtful, the code reuse of stager.stage(List.of(), sourceNamespaces) is clean (no API surface expansion needed), and the 5-test suite covers the key scenarios well including the critical load-failure preservation case.

private Map<String, RepoBound> skillsForCall(RuntimeContext ctx) {
return frozenSkills != null ? frozenSkills : mergeRepositories(ctx);
private MergeResult skillsForCall(RuntimeContext ctx) {
return frozenSkills != null ? new MergeResult(frozenSkills, false) : mergeRepositories(ctx);

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.

[nit] When frozenSkills != null, loadFailed is hardcoded to false. This means if all repositories failed during construction (with freezeRepositories=true), the loadFailed signal is silently discarded and a subsequent empty-merge will trigger cache GC. In practice this is harmless (empty frozen skills + empty cache = no-op GC), but a brief inline comment explaining why frozen mode always reports loadFailed=false would help future maintainers understand the intent.

}

@Test
void onSystemPromptClearsSkillsCacheWhenVisibilityFilterHidesAll() throws IOException {

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.

[nit] There's a test for onSystemPrompt + visibility-filter-hides-all (this test), but no symmetric test for prestageMarketplaceSkills + visibility-filter-hides-all. The skill-filter variant is covered (prestageMarketplaceSkillsClearsCacheWhenSkillFilterHidesAll), but visibility filter takes a different code path (applyVisibility returns empty before applySkillFilter is even reached). Consider adding a prestageMarketplaceSkillsClearsCacheWhenVisibilityFilterHidesAll test for symmetry.

* is removed when no skill survives merging/filtering. Skipped when a repository failed to
* load — an empty result caused by an error must not wipe the cache (outage protection).
*/
private void stageEmptyRetainedIfSafe(boolean loadFailed) {

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.

[praise] Clean design: reusing stager.stage(List.of(), sourceNamespaces) to trigger orphan GC through the existing public API avoids expanding MarketplaceStager's API surface (no need to widen garbageCollectOrphans visibility). The loadFailed guard is a well-thought-out outage protection — an empty result caused by a transient error must never wipe the cache.

@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 real and impactful bug: when all skills are removed from repositories or filtered out by visibility/skill filters, stale content in .skills-cache/ was never cleaned up, causing outdated skill resources to leak into the sandbox. The fix introduces a MergeResult record that carries both the merged skills map and a loadFailed flag, enabling a new stageEmptyRetainedIfSafe() helper that triggers orphan GC with an empty retained set — but only when the empty result is intentional (not caused by a transient repository failure). The outage-protection design is thoughtful, the code reuse of stager.stage(List.of(), sourceNamespaces) is clean (no API surface expansion needed), and the 5-test suite covers the key scenarios well including the critical load-failure preservation case.

private Map<String, RepoBound> skillsForCall(RuntimeContext ctx) {
return frozenSkills != null ? frozenSkills : mergeRepositories(ctx);
private MergeResult skillsForCall(RuntimeContext ctx) {
return frozenSkills != null ? new MergeResult(frozenSkills, false) : mergeRepositories(ctx);

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.

[nit] When frozenSkills != null, loadFailed is hardcoded to false. This means if all repositories failed during construction (with freezeRepositories=true), the loadFailed signal is silently discarded and a subsequent empty-merge will trigger cache GC. In practice this is harmless (empty frozen skills + empty cache = no-op GC), but a brief inline comment explaining why frozen mode always reports loadFailed=false would help future maintainers understand the intent.

}

@Test
void onSystemPromptClearsSkillsCacheWhenVisibilityFilterHidesAll() throws IOException {

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.

[nit] There's a test for onSystemPrompt + visibility-filter-hides-all (this test), but no symmetric test for prestageMarketplaceSkills + visibility-filter-hides-all. The skill-filter variant is covered (prestageMarketplaceSkillsClearsCacheWhenSkillFilterHidesAll), but visibility filter takes a different code path (applyVisibility returns empty before applySkillFilter is even reached). Consider adding a prestageMarketplaceSkillsClearsCacheWhenVisibilityFilterHidesAll test for symmetry.

* is removed when no skill survives merging/filtering. Skipped when a repository failed to
* load — an empty result caused by an error must not wipe the cache (outage protection).
*/
private void stageEmptyRetainedIfSafe(boolean loadFailed) {

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.

[praise] Clean design: reusing stager.stage(List.of(), sourceNamespaces) to trigger orphan GC through the existing public API avoids expanding MarketplaceStager's API surface (no need to widen garbageCollectOrphans visibility). The loadFailed guard is a well-thought-out outage protection — an empty result caused by a transient error must never wipe the cache.

- Change mergeRepositories() to return both merged skills and a load failure flag
- Use new MergeResult wrapper for skills and load failure tracking
- Trigger orphan GC with empty retained white-list when merging yields no skills
- Skip orphan GC if repository load failure occurred to protect cache during outages
- Call stageEmptyRetainedIfSafe() in all code paths where merged skills are empty or filtered empty
- Add tests verifying orphan GC clears .skills-cache on empty skills and preserves cache on load failure
- Ensure safe behavior when MarketplaceStager is null during empty cache staging
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.

3 participants