agentscope-ai / agentscope-ai/agentscope-java
[Bug]:WorkspaceIndex 快速路径返回空 mtime,使 MemoryConsolidator 的水位线与 pruneOldSessions 同时失效(且无法关闭)
- 主要語言
- Java
- 星號
- 5.6k
- 分支
- 1.3k
- 平均合併
- 4 天 12 小時
- 30 天內合併 PR
- 77
描述
> Labels 建议:`bug` / `harness` / `memory` / `filesystem`
## 环境
| | |
|---|---|
| agentscope-harness | 2.0.2 |
| store | `agentscope-extensions-mysql` 2.0.2(`JdbcStore` → MySQL 8.0.21) |
| 场景 | **非 sandbox**,远程 store,`IsolationScope.USER` |
## 问题
`RemoteFilesystem.glob()` 在**索引快速路径**下返回的 `FileInfo` 里,mtime 是**空字符串**:
```java
// RemoteFilesystem.java:443-462(index fast path)
if (index != null && index.hasPrefix(normalizedPath)) {
...
results.add(FileInfo.ofFile(key, 0, "")); // :458 size=0, modifiedAt=""
...
}
// 对比 :465-490 的 fallback 全量扫描分支
String modifiedAt = (fd != null && fd.modifiedAt() != null) ? fd.modifiedAt() : "";
results.add(FileInfo.ofFile(key, size, modifiedAt)); // :486 这里是有真实值的
```
而两处下游都依赖这个 mtime,且都采取「拿不到就按最保守处理」的策略 —— 于是双双失效。
### 受害者一:`MemoryConsolidator` 的水位线(成本问题)
```java
// MemoryConsolidator.java:243-249
private static boolean isModifiedAfter(FileInfo fi, Instant watermark) {
String modifiedAt = fi.modifiedAt();
if (modifiedAt == null || modifiedAt.isBlank()) {
return true; // 空 mtime → 一律当成「改过」
}
return Instant.parse(modifiedAt).isAfter(watermark);
}
```
水位线机制的意义是「每次合并只处理新增的日更文件」。空 mtime 让 `isModifiedAfter` **恒为 true**,于是每次 consolidation 都把 `memory/` 下**全部**日更文件重新喂给 LLM。
账本随天数线性增长 → 每次合并的输入 token 线性增长 → **成本线性增长**,而本该稳定在「新增的那几个文件」。
### 受害者二:`pruneOldSessions`(清理彻底不工作)
```java
// MemoryMaintenanceMiddleware.java:289-294
String modifiedAt = fi.modifiedAt();
if (modifiedAt == null || modifiedAt.isEmpty()) {
continue; // 空 mtime → 跳过,不删
}
```
这里是反向的保守策略:拿不到时间就不敢删。结果**一个文件都不会被清理**,`sessionRetentionDays` 完全失效。
(`pruneOldSessions` 另有一处独立缺陷 —— glob 路径层级与路由前缀对不上,见姊妹 issue「pruneOldSessions 的 glob 路径与路由前缀不匹配」。两个缺陷各自都足以让它什么都删不掉。)
## 关键:用户无法关闭这个索引
只要使用 `RemoteFilesystemSpec`,框架会**自动创建并注入** `WorkspaceIndex`,没有任何开关:
```java
// HarnessAgent.java:2175-2176
WorkspaceIndex workspaceIndex =
remoteFilesystemSpec != null ? WorkspaceIndex.open(resolvedWorkspace) : null;
// HarnessAgentBuilderSupport.java:142-144
if (workspaceIndex != null) {
b.remoteFilesystemSpec.workspaceIndex(workspaceIndex);
}
```
`RemoteFilesystemSpec.workspaceIndex(...)` 虽是 public,但传 `null` 也会被上面这段重新覆盖回去。
我们目前是走 escape hatch `abstractFilesystem(...)` 自建 `CompositeFilesystem`、不调 `.withIndex(...)` 才绕过的 —— 但这条路本身有额外维护成本(见姊妹 issue「RemoteFilesystemSpec 的 defaultBackend 应当可配置」)。**只要换回官方 spec 就会正面撞上。**
## 复现
1. 用 `RemoteFilesystemSpec` + 任意 `BaseStore` 构建 agent(索引会被自动注入)
2. 让 `memory/` 下积累若干日更文件,触发一次 consolidation 建立水位线
3. 再触发一次 consolidation(此时不应有新文件需要处理)
4. 观察喂给 LLM 的 `New daily ledger entries` 段落 —— 实际是**全部**日更文件,而非空
## 建议
任一即可:
- 索引快速路径也带上 mtime(索引里补存 mtime,或对命中的 key 回源补齐)
- 索引只用于「有没有」这类判定,凡是调用方需要 mtime 的路径就走 fallback
- 至少让 `FileInfo` 能区分「mtime 未知」与「mtime 为空」,并把 `withIndex` 做成调用方可真正关闭的开关
顺带一提:`:458` 那行 `size` 也恒为 0,如果将来有按大小过滤的逻辑,会踩同一个坑。
貢獻指南
評估
這個 Issue 還沒有評估資料。