# Memory as File

ReMe 的核心思想是：**Memory as File, File as Memory**。

<p align="center">
  <img src="../figure/memory-as-file.svg" alt="ReMe Memory as File 文件化记忆模型" width="92%">
</p>

**Memory as File**：长期记忆不是藏在黑盒数据库里，原始材料和可读记忆都落在 workspace 内由用户拥有的文件中。用户和 Agent
可以直接读、写、移动、删除它们；`metadata/` 里的索引和快照是可重建的派生状态。

**File as Memory**：每个文件不只是普通文本，也是一个可索引、可链接、可演化的记忆节点。ReMe 会从文件中解析 frontmatter、正文
chunk、wikilink 边，并把它们组织成检索和图谱。

换句话说，文件是人的可读界面，也是 Agent 的操作接口；目录结构负责承载记忆分层，Markdown 语法负责表达内容、元数据和关系。

## 设计目标

ReMe 把记忆设计成文件，不只是为了“方便存储”，而是为了让长期记忆具备几个基本性质：

| 目标   | 含义                                                                                               |
|--------|----------------------------------------------------------------------------------------------------|
| 可读   | 用户可以直接打开 workspace，像读普通笔记一样读 daily、digest 和原始材料。                          |
| 可编辑 | 用户和 Agent 都能用文件操作修正、补充、移动或删除记忆，不必依赖专用数据库客户端。                  |
| 可追溯 | digest 中的长期结论可以通过 Sources 章节回到 daily、resource 或 session 原文。                     |
| 可迁移 | workspace 是普通目录，Markdown、JSONL、YAML 和资源文件可以被备份、同步、版本管理或迁移到其他工具。 |
| 可索引 | 文件虽然是普通文本，但 ReMe 会解析 frontmatter、chunk、wikilink，构建检索索引和文件图谱。          |
| 可协作 | 人负责判断和修正，Agent 负责整理、链接和检索；二者看到和操作的是同一套文件。                       |

因此，ReMe 的记忆不是“数据库里的一条隐藏记录”，也不是“只给 LLM 看的 prompt 片段”。它首先是用户拥有的文件，其次才被系统索引成可召回的记忆。

## 记忆分层

ReMe 的 workspace 把记忆分成四层：

```text
source records -> session/ + resource/
working memory -> daily/
long memory    -> digest/
system state   -> metadata/
```

这四层解决的是不同问题。

`session/` 和 `resource/` 保存来源记录。`resource/` 文件保持原路径和原内容；标准 Auto Memory 保留对话消息，但会有意排除
tool-result 和 base64 data block，防止召回结果和二进制负载被误当成用户证据。Agent 运行时生成状态则放在 `mem_session/`。

`daily/` 是浅加工层。它把当天发生的对话和资源整理成更适合阅读的 daily note：什么事情发生了、有哪些结论、留下了哪些后续任务、对应原文在哪里。
daily 不追求最终抽象，它更像当天工作台。

`digest/` 是深加工层。这里保存的是可以长期复用的记忆节点，例如用户偏好、项目背景、流程经验、概念知识、决策先例。digest 不应该只是复制
daily，而应该把多次出现的事实、方法和关系合并成更稳定的表述。

`metadata/` 是系统索引层。它保存 file catalog、chunk 索引、图谱快照等运行状态。用户通常不需要手写这里的内容；真正的人工编辑入口是
`daily/`、`digest/` 和必要时的 `resource/`。

这个分层让 ReMe 可以同时保留“现场”和“抽象”：daily 负责还原当时发生了什么，digest 负责回答以后还能复用什么。

## 目录结构

ReMe 用目录表达记忆组织和记忆分层。原始材料先进入 `resource/` 或 `session/`，再沉淀到 `daily/`，最后由 `auto_dream`
整合到 `digest/`。

对应的自动流程分别是 [Auto Memory](./auto_memory.md)、[Auto Resource](./auto_resource.md)
和 [Auto Dream](./auto_dream.md)。检索这些文件时使用 [Memory Search](./memory_search.md)。

```text
<workspace_dir>/
├── metadata/                    # 系统索引层；ReMe 索引、图谱、catalog 等持久状态，不作为人工编辑入口
├── session/                     # 来源记录层；对话来源记录
│   ├── dialog/
│   │   └── <session_id>.jsonl        # auto_memory 保存的来源消息
│   └── claude_code/
│       └── <session_id>.jsonl        # auto_memory_cc 使用的 ReMe 副本
├── mem_session/                 # Agent wrapper 生成的 session/配置，不是用户记忆
│   ├── agentscope/
│   ├── claude_config/
│   └── codex/
├── resource/                         # 来源记录层；外部原始材料
│   ├── <resource>.<ext>               # 根目录文件使用今天日期
│   └── YYYY-MM-DD/
│       └── <resource>.<ext>            # 按目录日期进入 daily
├── daily/                            # 浅加工层；按日期组织当天事实、对话摘要、资源解读
│   ├── YYYY-MM-DD.md                 # 当天索引页
│   └── YYYY-MM-DD/
│       ├── <generated_name>.md       # 按主题命名的对话或资源卡片
│       └── interests.yaml            # auto_dream 产出的主动兴趣主题
└── digest/                           # 深加工层；可长期复用的个人事实、流程经验、知识节点
    ├── personal/
    │   └── <memory>.md               # 用户画像、偏好、长期个人事实
    ├── procedure/
    │   └── <memory>.md               # 流程、方法论、操作经验
    └── wiki/
        └── <memory>.md               # 通用知识、概念、决策先例
```

典型流转如下：

```text
对话
  -> session/dialog/<session_id>.jsonl
  -> daily/YYYY-MM-DD/<generated_name>.md
  -> digest/personal | digest/procedure | digest/wiki

外部资料
  -> resource/[YYYY-MM-DD/]<resource>.<ext>
  -> daily/YYYY-MM-DD/<generated_name>.md
  -> digest/wiki | digest/procedure
```

前两步偏向记录和整理，最后一步偏向长期沉淀。`auto_memory` 和 `auto_resource` 负责从原始输入生成 daily，`auto_dream`
负责从 daily 抽取并整合 digest。daily 文件名来自经校验的 frontmatter `name`；`session_id`、`source_conversation`
和 `source_resource` 负责稳定追溯与定位，不用来强制决定文件名。

## Markdown 格式

ReMe 优先使用 Markdown 表达记忆，因为它同时适合人读、Agent 编辑和程序解析。

一个典型记忆文件：

```markdown
---
name: 光伏产业链研究
description: 从硅料到组件的全链条梳理
tags: [新能源, 光伏]
---

# 结论

光伏产业链可以拆成 [[digest/wiki/硅料.md]]、硅片、电池片和组件。
主要生产商包括 [[digest/wiki/隆基绿能.md|隆基]]。
```

### Frontmatter

Frontmatter 是文件开头的 YAML 块，用 `---` 包住：

```markdown
---
name: 文档名
description: 文档描述
source_conversation: [[session/dialog/abc.jsonl]]
---
```

当前代码固定识别 `name` 和 `description`，其他字段会作为额外 metadata 保留。写入接口会把 `name`、`description` 和
`metadata` 合并成 frontmatter。

推荐把 frontmatter 当作“节点级摘要”，把正文当作“证据、解释和关系”。例如：

```markdown
---
name: 用户偏好：文档说明风格
description: 用户偏好直接、工程化、有上下文但不冗长的中文技术说明。
kind: preference
confidence: observed
---

用户多次要求文档补充动机、边界和例子，但避免营销式表述。

执行 [[digest/procedure/技术文档写作.md]] 时应用这个偏好。

## Sources

该偏好记录于 [[daily/2026-06-20/文档说明风格.md]]，其中保留了用户多次提出的指导。
```

这样做有三个好处：

1. `name` 和 `description` 可以在列表、召回结果和 Agent 判断中作为轻量摘要。
2. 正文可以承载更完整的事实、条件、反例和来源。
3. 普通 Wikilink 可以被图谱解析，后续移动文件时也能被维护。

Frontmatter 适合放稳定、短小、结构化的字段；正文适合放需要人读的解释。不要把大段正文塞进 YAML 字段。

### Wikilink

Wikilink 用 `[[...]]` 表达文件之间的关系：

```text
[[daily/2026-06-20/session.md]]
[[notes/example.md#L9]]
[[notes/example.md#L9-L10]]
[[notes/example.md#L9-L10,L15-L20]]
```

ReMe 的 wikilink 是 **字面路径语义**：

```text
[[X]]  -> target_path = "X"
```

它不会自动补 `.md`，不会按文件名搜索，也不会自动解析 folder note。推荐写完整的 workspace 相对路径，并带上扩展名。

`[label](../wiki/example.md)` 这类普通 Markdown 链接不会建立 `FileLink`，move 或 retarget 操作也不会改写它们。

`#L9`、`#L9-L10` 和 `#L9-L10,L15-L20` 这类锚点会作为普通 `target_anchor` 字符串保存在图谱中。图谱解析器不会校验行号锚点，因此
`#L0`、`#L10-L9`、`#L9,` 也会被保存。`read` 不会解析追加在 `path` 后的锚点；读取指定范围时需要分别传入从 1 开始、首尾均包含的
`start_line` 和 `end_line`，例如
`read(path="digest/wiki/光伏.md", start_line=9, end_line=10)`。

Wikilink 的作用：

```text
正文链接       -> 建立 FileLink
move 文件      -> 默认改写入边中的 [[旧路径]]
delete 文件    -> 返回仍存在的入边，提示清理引用
search 命中    -> 可展开出入链，帮助理解上下文
```

解析结果：

```text
FileLink
  source_path = 当前文件
  target_path = notes/example.md
  target_anchor = L9-L10,L15-L20
```

旧文档中的 `related:: [[path]]`、`- related:: [[path]]` 或
`[related:: [[path]]]` 仍然可以读取。ReMe 会忽略外围文本，把内部 `[[path]]`
作为普通链接建立索引。从曾存储 typed link 的版本升级后，应执行一次 `reme reindex`，用源文件重建不含旧关系字段的派生图索引。

### 来源和关系

ReMe 里最重要的两类链接是来源链接和概念关系链接。

Sources 章节说明“这条长期记忆从哪里来”：

```markdown
## Sources

该偏好观察自 [[daily/2026-06-20/文档说明风格.md]]，支撑它的报告证据保留在
[[resource/2026-06-20/report.pdf]] 中。
```

概念关系链接说明“这个节点和哪些长期记忆有关”，并自然织入正文：

```markdown
这份分析扩展了 [[digest/wiki/光伏产业链.md]]，遵循
[[digest/procedure/调研报告拆解流程.md]]，并与
[[digest/wiki/集中式逆变器.md]] 对比。
```

## 人工编辑和 Agent 编辑

因为记忆就是文件，用户可以直接在编辑器里改 workspace；Agent 也可以通过 ReMe 的文件工具读写同一批文件。两者遵守同一套约定：

| 操作       | 建议                                                                                |
|------------|-------------------------------------------------------------------------------------|
| 新增记忆   | 写入合适目录，Markdown 使用 frontmatter，并尽量写完整 workspace-relative wikilink。 |
| 修改正文   | 保留已有来源和关键 wikilink；如果是修正旧结论，在正文里说明新材料如何改变旧判断。   |
| 移动文件   | 使用 ReMe 的 move 工具时会默认改写入边中的旧路径；手工移动后建议重新检查入链。      |
| 删除文件   | 删除前检查入链；ReMe 的 delete 会返回仍然指向目标的来源文件，方便清理悬空引用。     |
| 修改元数据 | 用 frontmatter 表达短字段；正文发生实质变化时同步更新 `description`。               |

一个实用规则是：**可以让 Agent 重写表达，但不要让它丢掉证据边**。尤其是 digest 节点中的 Sources 条目和已有
digest-to-digest Wikilink，是长期记忆可追溯和可扩展的基础。

## 路径语义

所有文件工具和 wikilink 都以 workspace-relative path 为基本单位：

```text
digest/wiki/光伏.md
daily/2026-06-20/文档说明风格.md
resource/2026-06-20/report.pdf
```

这带来一个明确边界：ReMe 不把 `[[光伏]]` 当作全库标题搜索，也不假设 Obsidian 式的同名解析。`[[digest/wiki/光伏.md]]`
就是指向这个具体路径。

推荐习惯：

1. 链接 Markdown 文件时带上 `.md`。
2. 从 digest 指向 daily 或 resource 时写完整来源路径。
3. 文件重命名或移动尽量通过 ReMe 的 move 工具完成，避免留下旧路径。
4. 对外部资源使用 `resource/YYYY-MM-DD/...`，对长期抽象使用 `digest/...`，不要把原始资料直接塞进 digest。

这种显式路径语义牺牲了一点手写便利性，但换来的是可预测、可迁移和可自动维护。

## Memory Chunking

Memory chunking 是把一个文件拆成可检索片段的过程。ReMe 不是直接按固定长度切 Markdown，而是尽量保持语义结构。

本节说明文件如何被切成检索 chunk；索引更新、BM25、向量召回和链接展开流程见 [Memory Search](./memory_search.md)。

传统 RAG 常见做法是固定窗口切分：

```text
Document
  |
  | every N tokens + overlap
  v
chunk 1 | chunk 2 | chunk 3 | ...
```

这种方式简单，但容易把标题、表格、代码块、列表和 `[[wikilink]]` 从中间切开。检索命中后，Agent
往往只看到一段孤立文本，不知道它属于哪个章节，也不清楚它和其他记忆节点的关系。

ReMe 的 chunking 更接近“按文件结构切记忆”：

```text
Markdown file
  |
  | frontmatter + headings + blocks + wikilinks
  v
semantic chunks with document skeleton
```

对比：

```text
传统 RAG chunk
  = 固定长度文本片段 + overlap

ReMe memory chunk
  = 章节结构 + 正文片段 + 行号范围 + wikilink 关系上下文
```

Markdown 文件使用 `MarkdownFileChunker`：

```text
Markdown
  |
  | mistletoe AST
  v
Document
  └─ H1 section
      ├─ paragraph / list / table / code
      └─ H2 section
          └─ ...
  |
  v
FileChunk[]
```

分块规则：

```text
1. 先解析 frontmatter，正文单独进入 chunker。
2. 按标题层级构建章节树。
3. 优先让一个完整章节成为一个 chunk。
4. 章节过长时，向下递归拆子章节和正文块。
5. 表格拆分时重复表头。
6. 代码块拆分时重复 fence。
7. 列表按 item 打包。
8. 最后才按行贪心拆分，并添加 [Part X/N]。
```

每个 chunk 默认会带上标题骨架：

```text
# 一级标题

## 当前章节

命中的正文片段

## 后续章节标题
```

这样检索命中时，Agent 不只看到孤立段落，还能看到它在原文件中的结构位置。

非 Markdown 默认走 `DefaultFileChunker`：按字节大小切分，并保留少量 overlap；对 Markdown 则会避免把 `[[wikilink]]` 从中间切开。

`DefaultFileChunker` 和 `MarkdownFileChunker` 使用各自配置的 `encoding` 解码文件，并在索引前将平台换行符统一为
LF。默认的 `invalid_encoding_policy: replace` 会在源文件含无效字节时保留其中可解码的内容用于检索，但不会修改源
文件；如需拒绝此类文件，可在 chunker 组件上设置 `invalid_encoding_policy: strict`。
