agentscope-ai / agentscope-ai/agentscope-java

[Bug] `.skills-cache` is never cleaned up when skill repositories return an empty collection; HarnessAgent still reports installed skills

Open
#2,664 4 comments 0 reactions 0 assignees View on GitHub
area/harness bug
Dominant language
Java
Stars
5.6k
Forks
1.3k
Avg merge
4d 12h
Merged PRs (30d)
77

Description

**Describe the bug**

When using a database-backed skill repository (e.g. `MysqlSkillRepository`), after all skills are removed from the repository so that `getAllSkills()` returns an empty collection, the skill files previously staged under `.agentscope/workspace/.skills-cache` in the `HarnessAgent` workspace are **never cleaned up**.

Since `.skills-cache` is part of the sandbox workspace projection allow-list (`SandboxFilesystemSpec.DEFAULT_WORKSPACE_PROJECTION_ROOTS`), the stale SKILL.md / script files are still hydrated into the sandbox on every sandbox start. The model can see these files via file-browsing/shell tools and, combined with `` context left in the conversation history, keeps answering something like "According to my memory, there is currently 1 available Skill installed: ...", which is inconsistent with the fact that the database has no skills at all.

Root cause: `HarnessSkillMiddleware.onSystemPrompt()` and `prestageMarketplaceSkills()` return early when `merged.isEmpty()` / `visible.isEmpty()` and never invoke `MarketplaceStager.stage()`, while orphan directory reclamation (`garbageCollectOrphans()`) only runs inside `stage()` — as long as the repositories keep returning an empty collection, GC is never triggered.

**To Reproduce**

1. Build a HarnessAgent with `MysqlSkillRepository` as the skill store and sandbox enabled:

```java
MysqlSkillRepository skillRepo = new MysqlSkillRepository(dataSource, true, true);

HarnessAgent agent = HarnessAgent.builder()
.name("demo")
.model(model)
.workspace(workspaceRoot)
.skillRepository(skillRepo)
// sandbox mode enabled
.build();
```

2. Save one skill to the database and call the agent so it gets staged into `.skills-cache`:

```java
AgentSkill skill = new AgentSkill(
"demo-skill", "demo", "content",
Map.of("run.sh", "#!/bin/bash\necho hello"), "mysql");
skillRepo.save(List.of(skill), false);

agent.call("list the currently available skills", ctx).block();
// now /.skills-cache//demo-skill/ exists
```

3. Remove all skills from the database so that `skillRepo.getAllSkills()` returns an empty collection, then call the agent again:

```java
skillRepo.delete("demo-skill"); // or TRUNCATE the related tables
assert skillRepo.getAllSkills().isEmpty();

agent.call("list the currently available skills", ctx).block();
```

4. See error:
- `/.skills-cache//demo-skill/` still exists with its content intact;
- a freshly started sandbox still projects the stale skill files;
- the agent still claims that 1 skill is available.

**Expected behavior**

- After the repositories return an empty collection, the next agent invocation (system prompt build / pre-sandbox-start prestage) should reclaim every orphan directory under `.skills-cache` via `garbageCollectOrphans()`;
- the sandbox projection should no longer contain files of deleted skills;
- the agent should no longer report skills that no longer exist.

**Error messages**

No exception is thrown; this is a silent consistency defect. Observable symptoms:

```text
# Expected: after emptying the DB and calling again, .skills-cache is empty
# Actual:
/.skills-cache//demo-skill/run.sh # still present

# Agent answer (gist):
"According to my memory, there is currently 1 available Skill installed: demo-skill ..."
```

**Environment (please complete the following information):**

- AgentScope-Java Version: 2.0.1-SNAPSHOT (main branch, including PR #2059)
- Java Version: 17 (also reproducible on JDK 21)
- OS: macOS

**Additional context**

1. As long as the database keeps **at least one skill** (and the visibility filter does not exclude everything), `stage()` runs and performs orphan GC along the way, so the cache reconciles correctly — the problem only occurs on the "all empty" edge case.
2. Skipping GC when `merged` ends up empty because repository loading threw (transient DB outage) is a reasonable protection and should be preserved by the fix.
3. Related PR #2059 (fix: pre-stage marketplace skills before workspace projection) fixes the opposite timing issue (cache empty at projection time) and does not cover this bug; moreover, the early-return branch of `prestageMarketplaceSkills()` introduced by that PR is one of the gaps.

---

**Describe the bug**

使用数据库型技能仓库(如 `MysqlSkillRepository`)时,当仓库中所有技能被删除、`getAllSkills()` 返回空集合后,`HarnessAgent` 工作区下的 `.agentscope/workspace/.skills-cache` 目录中之前 stage 的技能文件**不会被清理**。

由于 `.skills-cache` 位于 sandbox 工作区投影白名单(`SandboxFilesystemSpec.DEFAULT_WORKSPACE_PROJECTION_ROOTS`)中,残留的 SKILL.md / 脚本文件每次 sandbox 启动时仍会被注入沙箱,模型可通过文件浏览/shell 工具看到这些文件,并结合会话历史中的 `` 上下文,继续回答类似"根据我的记忆记录,目前安装的可用的 Skills 只有 1 个:..."的内容,与数据库中技能已清空的事实不一致。

根因:`HarnessSkillMiddleware.onSystemPrompt()` 与 `prestageMarketplaceSkills()` 在 `merged.isEmpty()` / `visible.isEmpty()` 时提前 return,从不调用 `MarketplaceStager.stage()`,而孤儿目录回收 `garbageCollectOrphans()` 只在 `stage()` 内部执行——只要仓库持续返回空集合,GC 永远不会触发。

**To Reproduce**

1. 构建 HarnessAgent,技能存储使用 `MysqlSkillRepository`,并启用 sandbox:

```java
MysqlSkillRepository skillRepo = new MysqlSkillRepository(dataSource, true, true);

HarnessAgent agent = HarnessAgent.builder()
.name("demo")
.model(model)
.workspace(workspaceRoot)
.skillRepository(skillRepo)
// 启用 sandbox 模式
.build();
```

2. 向数据库写入一个技能后调用 Agent,使其 stage 到 `.skills-cache`:

```java
AgentSkill skill = new AgentSkill(
"demo-skill", "demo", "content",
Map.of("run.sh", "#!/bin/bash\necho hello"), "mysql");
skillRepo.save(List.of(skill), false);

agent.call("列出当前可用的 skills", ctx).block();
// 此时 /.skills-cache//demo-skill/ 已生成
```

3. 清空数据库中的全部技能,使 `skillRepo.getAllSkills()` 返回空集合,再次调用 Agent:

```java
skillRepo.delete("demo-skill"); // 或 TRUNCATE 相关表
assert skillRepo.getAllSkills().isEmpty();

agent.call("列出当前可用的 skills", ctx).block();
```

4. See error:
- `/.skills-cache//demo-skill/` 目录依然存在,内容未被清理;
- 新启动的 sandbox 中依然投影出该技能文件;
- Agent 回答仍声称存在 1 个可用 Skill。

**Expected behavior**

- 仓库返回空集合后,下一次 Agent 调用(system prompt 构建 / sandbox 启动前 prestage)时,`.skills-cache` 下所有孤儿目录应被 `garbageCollectOrphans()` 回收清空;
- sandbox 投影不应再包含已删除技能的文件;
- Agent 不应再报告已不存在的技能。

**Error messages**

无异常抛出,属于静默的一致性缺陷。可观察的现象:

```text
# 期望:清空 DB 后再次调用,.skills-cache 为空
# 实际:
/.skills-cache//demo-skill/run.sh # 依然存在

# Agent 回答(大意):
"根据我的记忆记录,目前安装的可用的 Skills 只有 1 个:demo-skill ..."
```

**Environment (please complete the following information):**

- AgentScope-Java Version: 2.0.1-SNAPSHOT(main 分支,含 PR #2059)
- Java Version: 17(JDK 21 下同样复现)
- OS: macOS

**Additional context**

1. 只要数据库中**至少保留一个技能**(且未被可见性过滤器全部过滤),`stage()` 就会执行并顺带完成孤儿 GC,缓存可正常对账——问题仅出现在"全部为空"的边界。
2. 仓库加载抛异常(DB 短暂不可用)导致 `merged` 为空时不触发 GC 属于合理保护,修复时应保留该语义。
3. 相关 PR #2059(fix: pre-stage marketplace skills before workspace projection)修复的是反方向的时序问题(投影时缓存为空),不覆盖本问题;且其引入的 `prestageMarketplaceSkills()` 的提前 return 分支正是缺口之一。

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.