agentscope-ai / agentscope-ai/agentscope-java

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

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

Descripción

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

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.