agentscope-ai / agentscope-ai/agentscope-java

[Bug]:配了远程 store 后 session 仍写本地工作副本,且该副本的路径与路由下层不一致、无人可读

未关闭
#2,691 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
area/core/memory bug
主要语言
Java
星标
5.6k
派生
1.3k
平均合并
4 天 12 小时
30 天内合并 PR
77

描述

> Labels 建议:`bug` / `harness` / `memory`

## 环境

| | |
|---|---|
| agentscope-harness | 2.0.2 |
| store | `agentscope-extensions-mysql` 2.0.2(`JdbcStore` → MySQL 8.0.21) |
| 场景 | **非 sandbox**,`CompositeFilesystem` + 远程 store,`IsolationScope.USER` |
| 部署 | K8s 2 副本,workspace 是 **`emptyDir`**(每 Pod 独立、随 Pod 生灭) |

## 问题

配置了远程 store 之后,两类运行态文件走了**两条完全不同的持久化路径**:

- `MEMORY.md` / `memory/*.md` → 只走 `AbstractFilesystem`,落远程,本地不留任何文件 ✅
- `sessions/*.jsonl` → **先同步写本地**,再异步镜像到远程

```java
// SessionTree.java:310-322
public void flush() {
...
appendToFile(contextFile, toWrite); // 本地,Files.newBufferedWriter (:534/:556)
appendToFile(logFile, toWrite);
scheduleMirror(); // 异步 fire-and-forget 推远程 (:459-467)
}
```

在 emptyDir + 多副本部署下,这个「本地工作副本」既不持久、也不共享,是纯粹的写放大。

## 更关键的:本地那份文件没有任何读者

我们实测发现 session 文件在系统里存在**三个互不相通的位置**:

| | 路径 | 由谁决定 |
|---|---|---|
| ① 实际写入的本地副本 | `workspace//agents//sessions/` | `resolveSessionContextFile` → `getSessionDir:332` → `resolveRuntimeDataPath:235`(**带命名空间前缀**) |
| ② 路由的下层模板 | `workspace/agents//sessions/` | `RemoteFilesystemSpec.overlayRoute():250` 的 `new LocalFilesystem(dir, true, 10, null)`(**namespaceFactory = null,不带前缀**) |
| ③ 远程 | `agents␟␟users␟␟sessions␟` | `mirrorToFilesystem` 用显式传入的 `contextRelPath`(`resolveRelativePath:634-644`) |

**①和②永远不会相遇**:overlay 的下层去 `workspace/agents/...` 找,而文件写在 `workspace//agents/...`。

所以那份本地副本,唯一的读者只有 `SessionTree` 自己(`load()`)和 `SessionSearchTool`(它构造 `SessionTree` 时第三个参数传的是 `null`,即 **local-only 模式**,见 `SessionSearchTool:199/279`)。经由 `AbstractFilesystem` 的任何读取路径都看不见它。

### 实测数据

本地(`ls`):

```
workspace/CY25352/agents/agent-qa/sessions/2087822803351994370.jsonl
workspace/CY25352/agents/agent-qa/sessions/2087822803351994370.log.jsonl
```

远程(`SELECT` 自 `agentscope_store`):

```
agents␟agent-qa␟users␟CY25352␟sessions␟ /2087822803351994370.jsonl
agents␟agent-qa␟users␟CY25352␟sessions␟ /2087822803351994370.log.jsonl
```

镜像本身是成功的;问题在于本地那份的存在既无必要,路径也与 overlay 下层对不齐。

## 影响

1. **写放大**:每轮问答都同步写两个本地文件,而它们在 emptyDir 上重启即丢
2. **丢数据窗口**:`scheduleMirror()` 是 fire-and-forget(`MIRROR_EXECUTOR` 是单线程 daemon,`:80-89`),失败只打 WARN;进程被杀时队列里未完成的镜像直接丢失
3. **副本间分歧**:每个 Pod 各写各的本地副本,靠 `load()` 时的 union-merge 才能拼全
4. **路径不一致本身就是缺陷**:即使有人想利用「本地作为远程不可用时的降级读」,也会因为①②路径不同而失效

### 5. 「本地优先」强制了整份重传,单会话内成本按 O(n²) 增长

因为本地是权威工作副本、远程只是镜像,同步时只能把**整个文件**推上去 ——
`RemoteFilesystem` 没有追加语义,`uploadFiles` 是整份覆盖:

```java
// SessionTree.java:576-590
private void mirrorToFilesystem(Path file, String relativePath) {
...
byte[] bytes = Files.readAllBytes(file); // 整份读
filesystem.uploadFiles(fsRc, List.of(Map.entry(relativePath, bytes))); // 整份传
}
```

而 `.log.jsonl` 按设计是 **append-only、从不压缩**。于是一个 n 轮的会话,
第 n 轮要重传 n 轮的内容,**整场会话的传输量是 O(n²)**,两个文件各来一遍。

每轮的完整开销(都在响应路径上):

| 动作 | 代价 |
|---|---|
| `tree.load()` | 整份读本地 |
| `tree.syncFromRemote()` | **一次同步的远程读**(整份) |
| `tree.flush()` → `appendToFile` ×2 | 本地追加(廉价) |
| `scheduleMirror()` → `mirrorToFilesystem` ×2 | 整份读 + **整份远程写**,×2 个文件 |

如果 session 直接走 `AbstractFilesystem`(或至少做增量镜像),这一项可以降下来。

## 建议

任一即可:

- 配置了远程 store(或 `filesystem != null`)时,`SessionTree` 直接走 `AbstractFilesystem`,不再维护本地工作副本
- 或提供开关(如 `SessionTree` 的 local-copy 模式 / `CompactionConfig` 级别的配置)让部署方选择
- 至少**让①和②的路径对齐**:要么本地副本不加命名空间前缀,要么路由下层也加上 —— 现状是两处对同一份数据用了不同的路径约定

## 相关

[#2654](https://github.com/agentscope-ai/agentscope-java/issues/2654) 从 **e2b sandbox** 视角提出了同一组现象,并询问这是否为有意设计。本条补充的新信息:

- **非 sandbox 场景(`CompositeFilesystem` + `JdbcStore`)同样复现**,说明这不是 sandbox 特有
- 本地副本的路径与路由下层**不一致**,因此它并不能充当降级读的来源
- 「本地优先」直接导致每轮整份重传,单会话内成本 O(n²)(见影响第 5 点)

[#2682](https://github.com/agentscope-ai/agentscope-java/issues/2682)(`MemoryFlushMiddleware` 拖慢事件流完成、回复后有可感延迟)
与本条是同一条链路上的问题:那一轮同步的 `syncFromRemote()` 远程读正落在响应路径上。
本条主张的「远程 store 在场时不再维护本地工作副本」如果成立,可以一并缓解。

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。