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

Abierto
#2,664 4 comentarios 0 reacciones 0 asignados Ver en GitHub
area/harness bug
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

**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 分支正是缺口之一。

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.