agentscope-ai / agentscope-ai/agentscope-java

[Bug]:WorkspaceIndex 快速路径返回空 mtime,使 MemoryConsolidator 的水位线与 pruneOldSessions 同时失效(且无法关闭)

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

Description

> 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,如果将来有按大小过滤的逻辑,会踩同一个坑。

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.