# 抽象记忆 (Virtual Memory)

## 1. 概念

抽象记忆是从实体记忆提炼出的紧凑索引。它不替代实体层，而是给实体层建一张**带坐标的导航地图**。

核心属性：
- **Provenance-linked**：每条记忆都带指针回指实体层出处
- **Bi-temporal**：每条记忆记两个时间——`event_time`（事情发生时）和 `ingestion_time`（记下来时）
- **不可变追加**：旧记录不删除，冲突时标 `superseded_by`

抽象记忆有三种类型：

| 类型 | 回答的问题 | 存储 |
|------|-----------|------|
| Timeline | 何时发生了什么 | `memory/timeline/YYYY-MM.jsonl` |
| Knowledge Graph | 什么和什么是什么关系 | `memory/graph/{entities,edges}.jsonl` |
| Core.md | LLM 每次必看的最小快照 | `memory/core.md` |

## 2. Timeline（时间轴）

按时间组织的事件流。回答"何时发生了什么"。

### 2.1 创建

**触发**：
- Session 空闲（idle ≥30 min）→ watcher 触发提炼
- Daily sleep（03:00）→ 批量整理

**过程**：
1. 从实体层读新增的 DAG 节点（`iter_nodes_since()`）
2. LLM 提取事件（Stage 2: extract）
3. 每个事件挂 `Provenance` 指针
4. Append 到对应月份的 JSONL 文件

### 2.2 存储

```
~/.openprogram/memory/timeline/
├── 2026-05.jsonl
├── 2026-06.jsonl
└── ...
```

单条记录：
```json
{
  "id": "ev_abc",
  "summary": "在 OpenProgram 修了 Windows cp1252 编码 bug，涉及 38 个文件",
  "kind": "work",
  "provenance": {
    "project_id": "proj_openprogram",
    "session_id": "local_13d5",
    "commit": "73bfc05",
    "event_time": 1779900000,
    "ingestion_time": 1779986400
  },
  "entities": ["project.openprogram", "issue.cp1252"]
}
```

`kind` 枚举：`work` | `decision` | `learning` | `event`

### 2.3 读取

- 按时间范围：`memory_timeline(since, until)`
- 按关联实体：`memory_timeline(entity="project.openprogram")`
- 召回时注入最近高信号事件到 Core.md

### 2.4 更新

Append-only。同一事实被重新提炼时，旧记录标 `superseded_by` 指向新记录 ID，不删除。

### 2.5 删除

不删除。理由：事件是历史事实的记录，删除会破坏时间线完整性。标记 superseded 即可。

## 3. Knowledge Graph（知识图谱）

实体 + 关系的图结构。回答"什么和什么是什么关系"。

### 3.1 创建

**触发**：同 Timeline（session-end / daily sleep）。

**过程**：
1. 提炼管道 Stage 2 从 DAG 抽取实体和关系
2. Stage 3 做 alias resolution（"worker"/"后端"/"daemon" → 同一节点）
3. Stage 4 做矛盾检测（新边与旧边冲突 → 标 superseded）
4. Append 到 JSONL

### 3.2 存储

```
~/.openprogram/memory/graph/
├── entities.jsonl           点
├── edges.jsonl              边（带 bi-temporal + provenance）
└── views/
    └── entity/<slug>.md     每个实体一个可读页面
```

实体：
```json
{
  "id": "project.openprogram",
  "type": "project",
  "name": "OpenProgram",
  "attrs": {"path": "/Users/fzkuji/OpenProgram", "lang": "python"},
  "scope": "global"
}
```

边：
```json
{
  "from": "issue.cp1252",
  "to": "commit.73bfc05",
  "relation": "fixed-by",
  "event_time": 1779900000,
  "ingestion_time": 1779986400,
  "provenance": {"project_id": "proj_openprogram", "session_id": "local_13d5"},
  "confidence": 0.95,
  "superseded_by": null
}
```

### 3.3 读取

- 邻居遍历：`memory_graph_neighbors(entity, hops=2)`
- Hybrid search：`memory_search(query)` — FTS + 可选向量
- Scope 过滤：查询时按当前上下文过滤（`global` + `project:<current>`）

### 3.4 更新

- **Alias resolution**：相同实体的不同名称合并为一个节点
- **矛盾处理**：新边与旧边冲突时，旧边标 `superseded_by` 指向新边，不删除旧边
- **Views 重建**：Stage 5 重新投影 `views/entity/*.md`

### 3.5 删除

不删除。旧边标 `superseded_by`，保留完整历史。

### 3.6 Scope 标签

每个 entity/edge 带 scope，查询时按上下文过滤：

```
scope: "global"                  跨所有项目（如用户语言偏好）
scope: "project:openprogram"     仅此项目
scope: "agent:research"          仅此 agent
```

## 4. Core.md（注入快照）

LLM 每次调用时注入 system prompt 的最小记忆快照。≤2KB。

### 4.1 创建/更新

**触发**：sleep::deep（daily 03:00）重新生成。

**来源**：
- Timeline 中最近的高信号事件（top-N by recency × importance）
- Graph 中高频/高置信的实体和关系

**每一行都带 provenance 指针**（`↪ session:<id>`），LLM 看到后可用导航工具深入。

### 4.2 读取

每次 LLM 调用时，Core.md 内容被注入 system prompt：

```
═══════════════════════════════════════════════
OpenProgram 记忆 — 项目: OpenProgram, 最后整理 2026-06-18
═══════════════════════════════════════════════
[时间轴 · 最近]
· 2026-06-15 重构 Programs 页面为三 tab 布局            ↪ session:local_fc03
· 2026-06-17 修复 CLI attended mode 问题                ↪ session:local_d125

[图谱 · 当前项目相关]
· OpenProgram 在 /Users/fzkuji/OpenProgram (python, next.js)
· worker ──listens-on──► :18109

需要细节: memory_open_session(<id>) / memory_git_log(<project>)
═══════════════════════════════════════════════
```

### 4.3 删除

覆盖式更新。每次 sleep::deep 重新生成整个文件，旧内容被完全替换。

## 5. 跟线性总结链的关系

[`overview.md`](overview.zh.md) 记录的线性链用更粗的结构覆盖同一片范围：

```
short-term/YYYY-MM-DD.md  → (sleep::light) →  wiki/<kind>/<slug>.md  → (sleep::deep) →  core.md
```

- **short-term**：session-end 时追加 0–10 条事实到当日文件
- **wiki**：sleep::deep 把 short-term 事实提升为知识页面
- **core.md**：sleep::deep 从 wiki 投影最小快照

这条链从 `get_branch()` 渲染的对话文本读取，不读 DAG 节点，因此丢失工具调用链（agent 跑了什么、参数、结果）、`reads` 边（什么影响了决策）和 project-git commit 历史。Timeline 取代 `short-term/`，Graph 取代 `wiki/`，`core.md` 从两者重新投影。

## 6. 提炼管道（实体 → 抽象）

### 6.1 触发时机

| 触发 | 频率 | 目的 |
|------|------|------|
| Session-end | 会话空闲 ≥30 min | 增量提炼新 turn |
| Sleep (03:00) | 每天一次 | 批量整理、消歧、矛盾检测、重建 core |
| Pre-compress | context 接近上限时 | flush 对话中的关键信息到实体层 |

### 6.2 五阶段 (Five Stages)

```
Stage 1: Collect    从 session-git + project-git 拉自上次提炼以来的新 commit
                    读 DAG 节点全量（user/llm/code + reads 边）

Stage 2: Extract    LLM 一遍：抽取时间轴事件 + 图实体/关系，每条挂 provenance

Stage 3: Link       新实体跟现有图做 alias resolution

Stage 4: Reconcile  矛盾检测，旧边标 superseded，不删

Stage 5: Project    重新投影 core.md / entity views / timeline 分片
```

Stage 2 是最贵的（需要 LLM），可先用规则版（pattern match）起步，prompt 版逐步替换。

### 6.3 关键：直接读 DAG

管道直接读 session-git 里的 `Call` DAG，包括 `code` 节点（工具调用）和 `reads` 边（上下文引用）。这些是图谱投影的关键数据来源，改读渲染出的对话文本就会丢掉它们。

读层是 `store/session/provenance.py`，提供 `iter_nodes_since()` / `node_provenance()` / `session_commits()` / `project_commits()`。

## 7. 召回机制

### 7.1 注入（只给抽象）

LLM 每次调用时注入：
- Core.md（≤2KB，始终注入）
- 可选：按当前 query 召回的 timeline/graph 结果

不注入任何 raw chat。

### 7.2 导航（LLM 自取）

LLM 需要实体层细节时，调导航工具：

| 工具 | 作用 | 落到实体层哪 |
|------|------|-------------|
| `memory_open_session(session_id, turn)` | 读某会话原始消息 | Session-Git history/ |
| `memory_git_log(project_id, since)` | 看某项目提交历史 | Project-Git |
| `memory_git_show(project_id, commit)` | 看某次改了什么 | git show |
| `memory_timeline(entity\|since\|until)` | 时间轴切片 | Virtual timeline |
| `memory_graph_neighbors(entity, hops)` | 图的邻居 | Virtual graph |
| `memory_search(query)` | 跨虚拟层 hybrid 搜索 | Virtual (FTS + 向量) |

## 附录：实现状态

提炼所需的读取层（`store/session/provenance.py`）已就位。§2、§3 描述的 Timeline 和 Graph 存储，以及 §7.2 的导航工具尚未建成；当前在跑的是 §5 描述、[`overview.md`](overview.zh.md) 记录的线性总结链，它读的仍是渲染出的对话文本而不是 DAG。
