# seek PRD v1：三层认知记忆子系统

> 目标版本：seek v0.2.x
> 状态：✅ **v1 已交付**（M5.0–M5.8 全部完成；commit `08660a1` → `374cfad`）
> ✅ **M5.7-v2 已交付**（异步过滤 + 实时通知：memory_observe 工具默认注册；SessionEnd no-op；见 §6）
> ✅ **M5.9–M5.13 已交付**（快照+delta、L 层维护、auto_sourced 进化、observe 反馈闭环；见相关章节）
> ⬜ **M5.14–M5.15 待设计**（单项目 L 候选、M-index 智能保留；见 §11）
> 依赖基础：v0（架构分层、会话存储、Skill 机制、工具系统、permission policy）+ `internal/hooks` 生命周期 hook 子系统
> 前置阅读：[PRD v0](v0.md) §4.5（Session）、§4.6（Skill）、§4.8（Tool）

---

## 1. 动机

- **跨会话连续性**：当前 seek 的"记忆"只有 session 级 JSONL 消息历史。新会话从零开始，用户必须重复告知偏好、项目上下文、已修复的问题。
- **抽象层级缺失**：把"这个项目用 2 空格"和"用户倾向显式错误处理胜过 panic"放在一起平铺，是范畴错误——前者是项目事实，后者是用户特性。平铺存储会导致：(a) 项目级偏好污染其他项目；(b) 跨项目模式无法被识别。
- **进化方向**：长期记忆是 agent 从"工具"进化到"协作伙伴"的关键一步。Claude Code 有 Project Knowledge，Cursor 有 `.cursorrules` + index，但都缺一个**会从交互中学习并抽象出用户本源的机制**。seek 借此补齐并超出。

## 2. 三层架构（核心设计）

```
┌─────────────────────────────────────────────────────────────┐
│  L  长期记忆 / Soul        ~/.seek/soul.md                  │
│      跨项目，用户本源倾向、思维习惯。每次 Prompt 由 hook    │
│      以 user-role <context> 注入。~500 tokens 硬上限        │
└─────────────────────────────────────────────────────────────┘
                          ↑ 做梦（M → L 蒸馏）
┌─────────────────────────────────────────────────────────────┐
│  M  中期记忆 / Project    ~/.seek/projects/<hash>/memory.jsonl
│      项目内做过的关键决策 + 为什么。每项目独立              │
│      索引永远注入；详情按需 memory_recall(name) 取          │
└─────────────────────────────────────────────────────────────┘
                          ↑ 蒸馏（S → M）——v2：异步过滤 + 实时通知
┌─────────────────────────────────────────────────────────────┐
│  S  短期记忆 / Session    ~/.seek/sessions/<id>.jsonl       │
│      已有：当前 session 的完整消息历史                      │
│      S→M 有两条路径：手动 /distill（用户触发，session 结束时 review）│
│      和 memory_observe（v2，模型检测强信号 → 经 V4-Flash 判重后写 M）│
└─────────────────────────────────────────────────────────────┘
```

**为什么三层而不是扁平 KV**：

| 维度 | S | M | L |
|---|---|---|---|
| 边界 | 单 session | 项目目录 | 跨项目 |
| 内容性质 | 原始事件 | 决策 + 理由 | 偏好 / 习惯 |
| 抽象层级 | 具体 | 半结构化 | 高度抽象 |
| 写入触发 | 自动（每条消息） | 两条路径：`memory_remember`（用户触发，permission gate）+ `memory_observe`（v2，模型检测强信号 → V4-Flash 判重后写，`auto_sourced=true`） | 仅做梦产出 |
| 注入策略 | 是历史本身 | 索引常驻 + 按需检索 | 整段常驻 |
| 容量上限 | 由 /compact 控制 | ~200 entries（注入索引 ~3k tokens） | ~500 tokens |

**两次单向蒸馏**：

1. **S → M（蒸馏）**：两条路径。**手动**：用户触发 `/distill`，LLM 扫描当前 session 历史提取 ≤3 条决策，用户 y/n/e review 后落 M。**自动（v2）**：模型在对话中检测到强信号时调 `memory_observe`，经异步 V4-Flash 过滤判重后写 M（标记 `auto_sourced=true`），用户通过 scrollback 通知即时看到结果。详见 §6。
2. **M → L（做梦）**：用户运行 `seek -dream`。LLM 扫描所有项目的 M + 最近 N 次 session 的关键决策，归纳出用户的"本源特性"候选（"倾向 X 胜过 Y"），落 L-pending。用户在后续 K 次会话里没出现反例，才升正式 L。

## 3. 设计目标与不做什么

### 目标

1. **三层独立存储**，schema 各自演化，互不耦合
2. **召回策略稳定**：哪些进 prompt 在结构上固定，最大化 prefix-cache 命中
3. **写入显式化**：M 写入分两条路径——`memory_remember`（用户主动触发）走 permission policy 的 inline 批准；`memory_observe`（模型判断触发，v2）不走 permission policy，改为实时通知作为隐式审查。L 永远不被模型直写
4. **完全可观测可编辑**：L 是 markdown 单文件，用户随时打开看；M 是 JSONL，可以 `cat | jq`
5. **冲突可解释**：项目级 M > 全局 L > AGENTS.md > 系统默认（详见 §6）

### 不做什么（v1 明确延后到 v2 / v3）

- ❌ 向量检索 / RAG（v3）
- ❌ 多用户隔离 / 权限模型（v1 单用户）
- ❌ M 在多机器间同步（v2，靠用户自己 dotfiles 同步 `~/.seek/`）
- ❌ 模型直接写 L（v1 永不允许；只有做梦能落 L-pending）

> **范围调整**：原计划"自动 S→M 蒸馏"与"自动周期性做梦"延后到 v2；调整后纳入 v1 收尾里程碑 M5.7 / M5.8（详见 §9）。v1 自动蒸馏用启发式满意度检测 + SessionEnd reasoner 调用，通过 `$SEEK_AUTO_DISTILL` env var 门控且**默认关闭**。**v2 改为默认开启**——实时通知是安全网，用户看到后可即时纠正。

## 4. 数据模型

### L（长期 / Soul）

文件：`~/.seek/soul.md`，纯 markdown，frontmatter 含 schema_version。

```markdown
---
schema_version: 1
updated_at: 2026-05-22T13:15:00Z
---

# User profile

## Stable（已确认 ≥3 次会话无反例）

- **倾向显式错误处理胜过 panic**
  - 来源：proj-seek (3 sessions), proj-foo (1 session)
  - 首次观察：2026-05-15
  - 最近确认：2026-05-22

- **代码风格偏简洁，宁愿三行重复也不要早期抽象**
  - 来源：proj-seek (5 sessions)
  - 首次观察：2026-05-10

## Pending（做梦候选，等待后续会话确认）

- **倾向中文回答而非英文**
  - 来源：proj-seek (2 sessions)
  - 升正式还差：1 次确认 / 0 次反例
```

- 容量硬上限：注入到 prompt 的内容 ≤ 500 tokens（约 ~1500 中文字符）。超出时做梦阶段会先合并相似条目。
- 用户可以**直接编辑这个文件**——比如手动删除"模型猜错了"的条目，或把 Pending 直接提到 Stable。下次启动 seek 会重新加载。

### M（中期 / Project）

目录：`~/.seek/projects/<sha256(abs-project-path)[:16]>/`

```
~/.seek/projects/a3b9f1c2e8d4a7b6/
├── manifest.json          # 项目标识 + 反向定位
├── memory.jsonl           # 实际记忆条目（含 stale）
└── archived.jsonl         # 已归档的冷条目（GC 从 memory.jsonl 移入）
```

`manifest.json`:
```json
{
  "schema_version": 1,
  "project_id": "a3b9f1c2e8d4a7b6",
  "abs_path": "/Users/whyiyhw/code/github/seek",
  "first_seen": "2026-05-10T08:00:00Z",
  "last_seen": "2026-05-22T13:15:00Z"
}
```

`memory.jsonl` 每行一条：
```json
{
  "schema_version": 1,
  "name": "session-storage-format",
  "tagline": "session 用 JSONL 而不是 JSON，因为 prefix-cache 需要 byte-identical",
  "content": "在 65a31d5 之前用 .json；改 JSONL 是为了支持 loadMeta() 只读第一行...",
  "tags": ["architecture", "session"],
  "created_at": "2026-05-15T10:23:00Z",
  "updated_at": "2026-05-15T10:23:00Z",
  "last_recalled_at": "2026-05-20T14:11:00Z",
  "recall_count": 3,
  "pinned": false,
  "stale": false,
  "stale_since": "0001-01-01T00:00:00Z",
  "source_session_id": "20260515-102300-abc123",
  "auto_sourced": true,
  "observe_count": 2
}
```

**字段规范**：
- `last_recalled_at` 创建时默认为 `created_at`（不允许 null），让剔除分数计算永远有 `last_active_at` 可比；每次 `memory_recall(name)` 命中时更新
- `recall_count` 仅由 `memory_recall` 调用递增；`memory_remember` 覆盖写不重置（保留历史价值信号）
- `pinned` 默认 `false`，通过 `/memory pin <name>` 切换；置 true 后该条目**永远不参与剔除**
- `stale` 默认 `false`，由启动时的 GC pass 根据 score 自动维护（不需要手写）
- `stale_since` 首次标记 stale 的时间戳；用于归档规则（连续 stale ≥60 天才归档）。GC 恢复条目时清零
- `auto_sourced` 默认 `false`；为 `true` 表示该条目由自动蒸馏（M5.7）写入，尚未经用户确认
- `observe_count` 默认 `0`；每次 `memory_observe` 写入同名未确认条目时 +1。当 ≥3 时触发自动升格（`auto_sourced` 自动置 `false`，表示模型在多次独立会话中观察到同一模式）。**仅对 auto_sourced 条目有意义**；已确认条目不受此字段影响

- 在项目根写一个 `.seek/project-id` 文件，内容是 `<hash>`。这样项目移动了，下次启动 seek 用 `project-id` 文件反向找到 `~/.seek/projects/<hash>/`，不会丢历史。
- 容量软上限：注入到 prompt 的索引 ≤ ~3k tokens（一条 ~60 字符 × ~200 条）。超出时会按 `last_recalled_at` 淘汰冷条目到 archived。

### S（短期 / Session）

**不变**——继续是 `~/.seek/sessions/<id>.jsonl`，schema_version=2。Memory 子系统只读 S，不改它。

## 5. 召回机制（哪些进 prompt）

注入分两层：**系统 prompt 不被 memory 修改**；L / M 索引采用 **session-start 快照 + delta 追加**策略，而非每轮重注入。核心目标：**同 session 内前缀字节永不变化**，最大化 prefix-cache 命中。

### 5a. Session-start 快照

Turn 1（新 session 首次 `Prompt()`），`PrePromptHook` 构建一次快照，以 user-role `<context>` 前置到用户消息之前：

```
[系统 prompt（agent 启动时一次性构造，session 内永不变化）]
  原 system prompt + AGENTS.md + Skill 清单

[history（空——第一轮）]

[PrePromptHook 注入（仅 Turn 1 执行）]
  <context source="memory.soul">
    L soul.md 的 ## Stable 段             ← ≤500 tokens 硬上限
  </context>
  <context source="memory.index">
    M 索引：name + tagline 列表           ← ≤3k tokens 软上限
    auto_sourced 条目以 [auto] 前缀标记   ← 模型可见置信度差异
  </context>

[user 消息（用户输入 + workflowReminder）]
```

**快照一旦注入，Hook 记录其哈希/版本号**，后续轮次不再重复注入。

### 5b. 后续轮次（无写入时，最常见路径）

Turn 2+ 时：`PrePromptHook` 检测到 L/M 无变化 → **不产生任何 Prepend 消息**。消息历史中的初始快照继续对模型可见：

```
[系统 prompt]
<context source="memory.soul">...</context>    ← 来自 Turn 1，仍是历史的一部分
<context source="memory.index">...</context>   ← 同上
[user 1]
[assistant 1]
[user 2]   ← 无重复注入，前缀字节完全不变
```

**效果**：从 Turn 2 到 Turn N，只要无 M 写入，**每条消息的前缀字节与上一轮完全一致** → 热缓存全命中。

### 5c. 中途有 M 写入时（delta 追加）

当本轮 Prompt 之前发生了 M 写入（`memory_remember` / `memory_observe` / `/distill`），`PrePromptHook` 不修改已注入的快照，而是**在消息队列尾部追加一条 delta**：

```
[系统 prompt]
<context source="memory.soul">...</context>    ← 快照，不变
<context source="memory.index">...</context>   ← 快照，不变
[user 1]
[assistant 1]
[user 2]
[assistant 2]
<context source="memory.delta">               ← 尾部追加，不影响前缀
  新条目：
  - api-rate-limit: noted API rate limit constraint
</context>
[user 3]
```

**关键性质**：
- delta 只包含**新增或变更的条目**（name + tagline），不是完整索引
- delta 追加在**所有已有消息之后**，不影响之前任何字节 → prefix-cache 不受影响
- 多次写入累积：多个 delta 按顺序追加，不合并（保持字节确定性——合并会引入时间依赖）

### 5d. Soul 在 session 内被用户手动编辑

极低概率事件（用户手动修改 `~/.seek/soul.md` 中间不重启 seek），但处理策略与 M 一致：session-start 快照在整个 session 内保持不变。用户编辑的结果在**下一次 session 启动**时生效。不处理 mid-session soul 刷新。

### 设计对比

| | 旧方案（每轮重注入） | 新方案（快照 + delta） |
|---|---|---|
| Turn 1 注入 | ✅ 构建并前置 | ✅ 构建并前置 |
| Turn 2+（无写入） | ❌ 再次注入完整 index（token 浪费，但字节不变） | ✅ 不注入（省 token，前缀不变） |
| 写入后 | ❌ 完整 index 重新注入，**前缀变化 → 整个前缀缓存 miss** | ✅ delta 尾部追加，**前缀不变 → 缓存命中** |
| soul + index 在历史中的出现次数 | N 轮 → N 次 | 1 次（+ delta 次数） |
| token 浪费（无写入的 50 轮会话） | 50 × (soul + index) ≈ 100k tokens | 1 × (soul + index) ≈ 2k tokens |

### 为什么不放在 system prompt 里

原因不变（与旧方案一致）：

1. **系统 prompt 是 prefix-cache 的最敏感字节区**。L/M 快照放在 user-role 而非 system-role，让系统 prompt 的字节零耦合于 memory 内容
2. **user-role `<context>` 在新字节区**。Turn 1 的 `<context>` 跟 Turn 1 的用户消息一起 miss 成本是首次请求不可避免的；后续轮次零成本
3. **快照+delta 比每轮注入对缓存更友好**——即使写入发生，缓存破坏范围也从"整个前缀"缩小为"仅尾部新增的 delta"

### 工具入口

模型遇到需要详情时调：

| 工具 | 参数 | 行为 |
|---|---|---|
| `memory_recall` | `name` | 取一条 M 详情（更新 `last_recalled_at` 和 `recall_count`） |
| `memory_remember` | `name`, `tagline`, `content`, `tags?` | 写一条 M。**走 permission policy**，inline 弹 y/N 给用户确认 |
| `memory_observe` | `name`, `tagline`, `content`, `tags?` | **与 `memory_remember` 同 schema 但无 permission gate（v2 新增）**——聊天模型在对话中检测到**强信号**时调用。工具立即返回空字符串，后台走异步过滤（V4-Flash thinking 判重+价值评估），通过后写入 M（`auto_sourced=true`）并产生 scrollback 通知。不通过或超时则静默丢弃。详见 §6 自动蒸馏（v2） |

**L 没有工具入口**——做梦才能写，用户才能编辑。模型只能读 L（通过 `<context>` 注入看到），不能改。

### 其他读取路径

除了上面的 prompt 注入 + 工具调用外，M 层还有两条内部读取路径：

| 路径 | 触发时机 | 读取内容 | 用途 |
|---|---|---|---|
| **GC 全量扫描** | SessionStart（`RunGC`） | 所有 entry（含 stale） | 重新计算衰减分数，维护 stale/归档状态 |
| **dream 跨项目读取** | `seek -dream` / M5.8 自动做梦 | 所有 entry（含 stale，`Entries()`） | 跨项目模式分析，产出 L-pending 候选 |

两条都不会注入到 prompt 中——GC 只维护磁盘状态，dream 只给 reasoner 做离线分析。

## 6. 蒸馏与做梦

### S → M：`/distill`（统一入口）

`/distill` 是 S→M 蒸馏的**唯一用户入口**，负责两件事：
1. **拉取待确认的 AutoSourced 条目**（自动蒸馏写入的，见下方自动蒸馏节）
2. **对当前 session 跑一次新蒸馏**（手动触发，reasoner 提取 ≤3 条候选）

#### 候选列表构成

`/distill` 执行时合并两个来源，有序展示：

| 顺序 | 来源 | 来源说明 |
|---|---|---|
| **优先** | AutoSourced 待确认条目 | 从 `memory.jsonl` 拉取 `auto_sourced=true` 的条目（由 M5.7 自动蒸馏写入，用户尚未审核）。用户按 y/n/e 确认或否决后，**同一 session 内不会再次出现** |
| **其次** | 新蒸馏候选 | 对当前 session 调用 reasoner 提取 ≤3 条候选。reasoner 提示词见下方 M5.7 v1 节末尾（与 v1 手动 `/distill` 相同） |

合并后的列表统一用 y/n/e/q 逐条 review。每个候选顶部用标签区分来源：

```
⚡ distill candidate 1/3 [auto] session-storage-format
  自动条目待确认：uses JSONL for prefix-cache

⚡ distill candidate 2/3 session-api-rate-limit
  新候选：noted API rate limit constraint
```

#### y/n/e 语义（统一，按来源分支）

| 操作 | AutoSourced 条目 | 新蒸馏候选 |
|---|---|---|
| **y** | 确认：`auto_sourced` 置 `false`，升为正式 M 记录 | 写入 M（标准路径） |
| **n** | **删除该条目**（不只是丢弃，从 `memory.jsonl` 移除） | 丢弃候选（不写盘） |
| **e** | 编辑内容 → 确认（同 y 路径，`auto_sourced` 置 `false`） | 编辑内容 → 写入 M |

**关键约束**：AutoSourced 条目的 n 是**删除已落盘内容**，不是丢弃未落盘候选。v2 中自动蒸馏默认开启（`$SEEK_AUTO_DISTILL`），用户可设 `0` 关闭。

### 自动蒸馏（M5.7，✅ v1 已交付）— v2 弃用，仅存参考

> **已交付范围**：`$SEEK_AUTO_DISTILL=1` 时，SessionEnd 触发纯启发式满意度检测，通过则调用 reasoner 提取候选，落 M 标记 `auto_sourced=true`。commit `a9f89d5`。
>
> **v2 弃用**：SessionEnd 调 reasoner 的方案已被 v2 异步过滤 + 实时通知取代（见下方 v2 节）。v1 代码在 v2 中移除（SessionEndObserver 保留为 no-op）。`$SEEK_AUTO_DISTILL` 改为控制 `memory_observe` 工具注册，默认开启。

v1 满意度检测条件（`SessionEndObserver` 内）：
- 满意度分数 ≥0.7（基于 user_turns / 拒绝关键词 / tool_error 频率计算）
- user_turns ≥4 轮（太短的会话没有足够决策可提炼）
- 最近 5 条用户消息中无拒绝关键词

通过后，调用 reasoner 提取 ≤3 条候选，落 M 标记 `auto_sourced=true`，下次手动 `/distill` review 时优先确认/撤销（见上方 review 流程）。

reasoner 提示词（手动 `/distill` 和新蒸馏候选共用）：

```text
你看了这次 session 的完整对话（已经压成 X 条消息）。
找出 ≤3 条"如果未来在这个项目继续工作，知道这个会省时间"的具体决策。
每条给出：name (kebab-case)、tagline (一句话)、content (完整理由，≤500 字)。
不要泛泛之谈（如"用户喜欢简洁"），那是 L 层的事。
只要项目特定的、可操作的决策。
```

### 自动蒸馏（M5.7-v2，🔧 实现中）— 异步过滤 + 实时通知

> **升级动机**：v1 在 SessionEnd 用启发式关键词匹配检测满意度，流程复杂且有 90s 超时卡进程退出的问题（`OnSessionEnd` 调 reasoner 阻塞 defer）。v2 去掉 SessionEnd 的 reasoner 调用，改为聊天模型在对话中实时感知、实时写入、实时通知。

**门控**：`$SEEK_AUTO_DISTILL` 环境变量控制 `memory_observe` 工具是否注册。**默认开启**（未设置或设为 `1`/`true`/`yes`/`on` 时注册）；设为 `0`/`false`/`no`/`off` 时模型看不到此工具。v1 中此 env var 控制 SessionEnd reasoner 调用且默认关闭；v2 改为控制 tool 注册且默认开启，因为实时通知提供了足够的安全网。

不再有"缓冲区→SessionEnd→reasoner 蒸馏"的两阶段流程。取而代之的是**模型在对话中直接调用 `memory_observe`**。

#### 触发时机

`memory_observe` 的 schema 与 `memory_remember` 一致：`name`, `tagline`, `content`, `tags?`。但**不走 permission policy**——模型自行判断何时调用。

典型触发场景（强信号，模型在对话中可明确感知）：

| 场景 | 示例用户消息 | 模型行为 |
|---|---|---|
| 用户确认了方案 | "用 JSONL 吧"、"这个好" | 总结决策 → 调 `memory_observe` |
| 用户纠正确认了替代方案 | "不对，用这个 API" | 记录纠错后的方案 |
| 用户补充了关键约束 | "注意这个 API 有 rate limit" | 记录约束条件 |
| 用户明确满意 | "搞定了"、"谢谢" | 只在高价值决策时记录，非每句感谢 |

**非触发场景**（不调用）：
- 日常闲聊（"早上好"、"谢谢"）
- 中间状态（"再看看"、"也许"）
- 已经写过的同话题（`memory_observe` 按 `name` 覆盖更新）

#### 写入 + 通知

`memory_observe` 执行时**不直接写盘**，走异步过滤链路：

1. **工具立即返回空字符串**（不阻塞模型继续对话），但工具入口处先尝试**预占 name slot**：如果该 `name` 已有正在飞行的 goroutine（未完成），当前调用**静默合并**，不启动新 goroutine。
2. **后台 goroutine 异步过滤**：使用 `Distiller`（V4-Flash thinking）调用一次 reasoner，对比项目 M 现有条目，判断新候选是否有价值。goroutine 启动时通过 per-name mutex reserve 该 name，避免同一 name 并发写入产生重复条目。

   ```
   现有 M 条目：
   - session-storage-format: uses JSONL for prefix-cache, rejected protobuf
   
   新候选：
   name: jsonl-session
   tagline: decided to use JSONL for session storage
   
   判断：ACCEPT（内容一致，覆盖已有 auto_sourced 条目）
        REJECT（重复 / 太泛 / 用户偏好 / 临时状态）
        TIMEOUT → 静默丢弃
   ```

3. **过滤结果**：
   - **ACCEPT** → `Project.Add(Entry{...AutoSourced: true})`，通过 channel 向 TUI 发送通知事件，TUI 生成 scrollback 通知行：`· 📝 saved to M: <tagline> (auto-sourced)`
   - **REJECT** → 静默丢弃，不展示任何通知
   - **超时** → 静默丢弃（安全优先，用户可通过手动 `/distill` 补充）

```
你：用 JSONL 吧，protobuf 太重了
我：好，用 JSONL。
                            ← 此时后台在跑过滤，用户无感知
  · 📝 saved to M: uses JSONL for session storage (auto-sourced)
                            ← 过滤通过后通过 channel 通知 TUI
你：不对，删掉 → 我调 memory_remember 覆盖或删除
```

**goroutine → TUI 回传**：过滤 goroutine 通过 `Hook` 上的 `ResultChan chan ObserveResult` 通道回传结果，TUI 在事件循环中 select 该 channel。如果 TUI 退出（程序关闭），channel 关闭，goroutine 自然终止。

```go
type ObserveResult struct {
    Name    string
    Tagline string
    OK      bool   // true=写入成功, false=拒绝/失败
    Err     string // 失败原因（可选）
}
```

**写入失败**：`Project.Add` 返回错误（磁盘满、权限问题）时，通过 ResultChan 回传 `{OK: false, Err: "disk full"}`，TUI 生成 `⚠ failed to save: disk full`。

**过滤超时**：默认 10 秒。超时后静默丢弃，不通知、不写盘。超时不通过 ResultChan 回传（goroutine 直接 return）。

**每 session 过滤调用上限**：默认每次 session 最多启动 10 次过滤 goroutine。超出上限的 `memory_observe` 调用静默丢弃（不启动过滤）。通过 `$SEEK_OBSERVE_MAX` env var 配置。上限的目的是控制 V4-Flash 调用次数和 token 成本。

#### AutoSourced 全生命周期规则（v2）

`memory_observe` 写的条目标记 `auto_sourced=true`，与 v1 自动蒸馏使用同一条安全路径。但 auto_sourced 条目有特殊的生命周期规则：

**索引可见性**：
- M-index 中 auto_sourced 条目以 `[auto]` 前缀展示：`- [auto] session-persistence-durability: ...`。模型无需调用 `memory_recall` 即可区分未确认条目和已确认条目，避免在索引层面将推测当作事实。
- Delta 中同样带 `[auto]` 前缀。已确认条目不带前缀。

**置信度累进（observe_count）**：
- Entry 新增 `observe_count` 字段（默认 `0`）。每次 `memory_observe` 写入同名**未确认**条目时 +1。已确认条目（`auto_sourced=false`）不受此字段影响。
- **自动升格**：当 `observe_count ≥ 3`（模型在 ≥3 次独立会话中观察到同一模式），`auto_sourced` 自动置 `false`，升为正式 M 记录。这对称于 L 层的 K=3 反例规则——独立重复观察是比用户单一确认更强的信号。
- cross-session 去重：同名覆盖发生时保留历史 `observe_count` 并 +1，不重置为 0。

**Recall 驱动自升格**：
- `memory_recall(name)` 调用 `Project.TouchRecall` 时，若该条目 `auto_sourced=true` 且 `recall_count ≥ 3`（模型已经在至少 3 个回合中实际依赖了这条记忆），**自动置 `auto_sourced = false`**。
- 逻辑：模型"用出来的"记忆比"看到的"更有说服力。recall 是比 observe 更强的信号——observe 是"我推测这重要"，recall 是"我实际在用它做决策"。

**用户 review 前**：
- **GC 豁免**：`auto_sourced=true` 的条目在用户显式 y/n 之前，**不参与 GC 评估**。无论 score 多低、创建多久，都不被标记 stale 或归档。理由是：auto-sourced 条目存在的唯一目的就是等待用户 review，GC 不应在用户有机会看到之前删除它
- **注入索引**：同正式条目一样进入 `PrePromptHook` 的 M 索引，以 `[auto]` 前缀让模型知道"有这条记录但未确认"

**用户 review 后**：
- **y** → `auto_sourced` 置 `false`，升为正式 M 记录，纳入正常 GC 评估
- **n** → 从 `memory.jsonl` 删除（不是丢弃候选，是删除已落盘数据）

**自动升格后**：
- 通过 `observe_count ≥ 3` 或 `recall_count ≥ 3` 自动升格的条目与用户 y 确认的条目标志完全相同（`auto_sourced=false`），纳入正常 GC 评估
- 用户仍可在 `/distill` review 中看到这些条目（带 `[auto→confirmed]` 标签），可选择 n 删除——自动升格不剥夺用户否决权

**覆盖规则**：
- `memory_observe` 按 `name` 覆盖：如果 `name` 对应的条目 `auto_sourced=true`（未确认）→ **允许覆盖**（更新内容、刷新时间戳、`observe_count +1`）
- 如果 `name` 对应的条目 `auto_sourced=false`（已确认）→ **不允许覆盖**。该检查在异步过滤 goroutine 中进行，通过 ResultChan 回传 `{OK: false, Err: "entry 'xxx' already confirmed — use memory_remember to update"}`，TUI 生成 `⚠ skipped: entry confirmed, use memory_remember to update`。工具入口始终返回空字符串，不抛错误。已确认的条目只能用 `memory_remember`（走 permission policy）修改，这是防止模型事后改写用户确认过的记忆

#### SessionEnd 退化

v2 中 `OnSessionEnd` **不再调 reasoner**。满足满意度条件的 session 在对话过程中已经通过 `memory_observe` 产生了条目。SessionEnd 只保留原有的轻量级操作（项目 `Save`、session 持久化）。

**设计原则**：
- 模型在对话中拥有最佳上下文——刚讨论完的决策总结最准确
- 异步 V4-Flash 过滤做第二道把关——判重 + 价值评估，REJECT/超时静默丢弃
- per-name 并发去重 + 每 session 上限 10 次，控制 token 成本和避免重复
- 实时通知是隐式审查——用户看到"写了什么"可以立即纠正
- 不在 SessionEnd 调 reasoner → 没有 90s 超时卡退出的问题
- `auto_sourced=true` + `/distill` review 的安全网保留

#### Observe 反馈闭环（M5.13 — 设计确认中）

`memory_observe` 返回空字符串（不污染消息历史），模型无 per-call 反馈。这导致模型对其观察质量完全盲区——不知道 ACCEPT/REJECT 率，无法调整观察策略。长期可能退化出三种模式：过度观察（hit cap）、观察萎缩（无正向信号→放弃调用）、模式重复（反复提交同类被拒内容）。

**设计**：在 M-index 注入时追加一行聚合统计（不泄露具体条目），基于上个 session 的 observe 通过率：

```
<context source="memory.index">
...
最近 memory_observe 统计：5 次调用，3 条保存，2 条被过滤
</context>
```

- 统计来源：Hook 在 session 内累积 `observeAcceptCount` / `observeRejectCount`，`OnSessionEnd` 写入项目目录的 `observe-stats.json`
- 注入位置：M-index 快照底部（Turn 1 生效，后续轮次随快照不变——M5.9 快照机制已解决字节稳定性）
- 不破坏缓存：stats 在 session 内不变，只在跨 session 时更新
- 不泄露细节：仅显示通过/拒绝计数，模型不知道 "哪条具体被拒"

**效果**：模型看到 "3/5 通过了 → 我的观察质量还行" vs "1/8 → 需要更谨慎"，获得自校准信号。16 行代码量，无新依赖。

### M → L：`seek -dream`（手动命令）

```bash
seek -dream                # 扫所有项目的 M + 最近 30 次 session，输出 L-pending 候选
seek -dream -dream-write   # 预览 + 追加候选到 ~/.seek/soul.md 的 ## Pending 段
                           # （--review 交互式 review 留给后续版本）
```

做梦流程：

1. **读入**：所有 `~/.seek/projects/*/memory.jsonl`，最近 30 次 session 的 `messages[]` 末尾 K 条
2. **跑 reasoner**：找跨 ≥2 个项目重复出现的模式。提示词强调"区分项目事实和用户特性，只输出后者"
3. **置信度计算**：每条候选附 `evidence`（来源 M 列表 + session 列表）。N=出现的不同项目数
4. **入 L-pending**：N≥2 才入；N=1 太弱
5. **Stable 升级检查**：最近 K=3 次会话里**模型行为与该条目一致** AND **用户没纠正** → 升 Stable

> ⚠️ **v1 未实现**：Stable 升级检查需要 session 连续性追踪，v1 未做（见 §9 回顾）。`## Stable` 段目前只能**用户手动编辑** `~/.seek/soul.md` 来填充。程序从不写 Stable。

后续会话里如果某条 L-stable 跟用户行为冲突（用户明确反对），做梦时会**降级回 Pending**（不直接删，留 trace）。

**模型选型**：dream 使用 **V4-Pro + thinking**（`deepseek-v4-pro`），通过 `$SEEK_DREAM_MODEL` 可覆盖。理由：

- dream 是记忆子系统里**推理难度最高**的操作——需要从跨项目 M 条目中归纳出用户特质，不是简单的 ACCEPT/REJECT
- dream 是**低频**操作（手动触发或每 14 天自动一次），Pro 比 Flash 贵 3–5 倍的绝对成本可忽略
- dream 产出的 L 候选会注入**每一个后续 session** 的 prompt——质量杠杆极高。一条错误的 L 候选污染面远大于一条错误的 M 条目
- `/distill` 和 observe 过滤继续用 V4-Flash——它们是高频操作，且推理深度只需判重/提炼，Flash thinking 已足够

### 自动周期性做梦（M5.8，默认开启）

`$SEEK_AUTO_DREAM` 环境变量控制自动 dream。**默认开启**（未设置时启用；设为 `0`/`false`/`no`/`off` 关闭）。按 cadence 触发后台 dream：

- **Cadence**：每 N 次 session 或 K 天（默认 N=20, K=14），由 `$SEEK_AUTO_DREAM_SESSIONS` / `$SEEK_AUTO_DREAM_DAYS` 配置
- **≥2 项目前置检查**：dream 在调用 reasoner 前检查项目数。单项目用户自动跳过 API 调用（N≥2 过滤器会拒绝一切结果，避免浪费）
- **触发时机**：SessionStart（不阻塞首轮 Prompt——dream 跑在后台 goroutine 中）
- **写入位置**：追加到 `~/.seek/soul.md` 的 `## Pending` 段（同手动 `--dream-write`）
- **状态记录**：`~/.seek/dream-state.json` 记录 `last_dream_at` + `sessions_since_dream`
- **手动 dream 不重置 cadence**：只有自动 dream 成功完成才更新状态

**设计原则**：默认开启（与 `$SEEK_AUTO_DISTILL` 对称），但单项目用户不会产生 API 调用成本。Cadence 足够稀疏（两周一次）以免 reasoner 成本失控。用户可设 `SEEK_AUTO_DREAM=0` 关闭。

### 冲突解析（最终顺序）

执行优先级（高 → 低）：

1. 用户当前 session 内的显式指令
2. `AGENTS.md` / `CLAUDE.md`（项目规范）
3. **M**（当前项目的中期记忆）
4. **L-stable**（用户本源）
5. **L-pending**（候选，置信度低）
6. seek 系统默认

冲突顺序由 `PrePromptHook` 在注入的 `<context source="memory.soul">` 块顶部用 1 行显式声明（不放在 system prompt 里），让模型知道"如果 L 说你喜欢 tabs 但 AGENTS.md 说 spaces，用 spaces"。

**v2 补充**：`auto_sourced=true` 的条目在 M 索引中标记为**低置信度**。模型应优先采信用户当前 session 内的显式指令，而非未确认的 auto_sourced 条目。如果用户指令与 auto_sourced 条目矛盾，以用户当前指令为准（用户指令 > AGENTS.md > L > auto_sourced M）。

### 遗忘

**M 的剔除分数（指数衰减加权）**——把"频次"和"近因"在一个标量里捏在一起，避免两个独立阈值 AND 出来的反直觉案例（高频远古条目被冤剔、新建零调用条目被冤留）：

```
score(entry) = (recall_count + 1) * exp(-(now - last_active_at) / half_life)
where:
  last_active_at = max(created_at, updated_at, last_recalled_at)
  half_life      = 30 天（可配置 SEEK_MEMORY_HALFLIFE_DAYS）
  +1 基线        = 让新条目（recall_count=0）也能跟"老条目近期被翻出来"竞争
```

剔除规则（按顺序检查，命中第一条即落定）：

| 条件 | 行为 |
|---|---|
| `auto_sourced == true`（v2） | **GC 豁免**——不参与任何剔除评估。用户显式 y/n 之前永久保留（见 §6 AutoSourced 全生命周期规则） |
| `pinned == true` | **永不剔除**（用户手动标记，唯一硬保留） |
| `now - created_at < 7 天` | **保留**（grace period，新条目不参与剔除评估） |
| `score >= 0.5` | **保留**（一条 30 天前用过 ≥1 次 / 60 天前 ≥3 次 / 90 天前 ≥7 次 都在阈值之上） |
| `score < 0.5` | **标记 stale=true**（不删，仍可被 `memory_recall(name=...)` 取到；只是从 `PrePromptHook` 注入的 M 索引里剔出） |
| `stale=true` 且 `score < 0.1` 持续 60 天 | **归档到 `~/.seek/projects/<hash>/archived.jsonl`**（彻底退出活动集，仍可 grep） |

**直观对照**（半衰期 30 天）：

| 条目状态 | recall_count | last_active 距今 | score | 处置 |
|---|---|---|---|---|
| 上周用过的常用条目 | 5 | 7 d | 6 × 0.79 ≈ 4.74 | keep |
| 60 天前用过 1 次 | 1 | 60 d | 2 × 0.135 ≈ 0.27 | stale |
| 60 天前用过 5 次 | 5 | 60 d | 6 × 0.135 ≈ 0.81 | keep（高频抵消远古） |
| 90 天前用过 1 次 | 1 | 90 d | 2 × 0.05 ≈ 0.10 | stale 边缘，下次做梦时丢弃 |
| 新建 3 天，零调用 | 0 | 3 d | 1 × 0.90 ≈ 0.90 | keep（grace period 内根本不评估） |

**为什么 +1 基线**：没有它的话 `recall_count=0` 的新条目分数恒为 0，必然被剔。+1 让"刚创建的条目"和"30 天前 recall 过 1 次的条目"分数相当，符合"新条目有未来潜力"的直觉。

**实现注意**：score 不持久化，每次启动现算。GC 跑在 `SessionStartObserver` 里（agent 启动 / `--resume` 加载时触发），不阻塞首次 `Prompt()`——索引重建是同步、纯本地、毫秒级。`MemoryEntry` 新增可选 `pinned bool` 字段（v1 user 通过 `/memory pin <name>` 命令切换；v2 让模型在 `memory_remember` 里建议 `pinned`）。

### L 层维护机制（M5.10 — 设计确认中）

L 层当前只能**增长**（dream 追加 Pending），不能**清理或升级**。Stable 段需用户手动编辑。M5.10 补上维护循环，打通 Pending → Stable → 降级 → 删除的完整生命周期。

#### 设计：两阶段评估

dream 不仅是"发现新模式"，也是"评估现有模式"的时机。每次 dream 执行三步：

```
所有项目的 M entries ──→ dream reasoner ──→ 新 LCandidate[]
                                          │
                        ┌─────────────────┤
                        ▼                 ▼
              evaluatePending()    evaluateStable()
              (机械规则)           (reasoner 辅助)
                        │                 │
                        ▼                 ▼
                  升格/保留/删除      降级/保留
                        │                 │
                        └────────┬────────┘
                                 ▼
                           soul.Save()
```

**阶段 1：evaluatePending（机械规则，纯 Go，不需要 reasoner）**

每条 Pending 候选在 soul.md 中已有结构化元数据（来源项目、首次观察时间、最近确认时间）。三条规则：

| 条件 | 动作 | 原因 |
|---|---|---|
| 来源项目 ≥ 3 个 AND 首次观察 ≥ 14 天 | **升格到 Stable** | 跨项目 + 时间沉淀 = 不是偶然。对称于 M5.11 `observe_count ≥ 3` |
| 最近确认 > 30 天 | **从 Pending 删除** | 长期无新证据的候选——要么是误推断，要么用户已不关心 |
| 其他 | **保留在 Pending** | 仍在积累证据 |

升格时：从 `## Pending` 移除，追加到 `## Stable`，保留来源和时间戳。删除时：从 `## Pending` 移除，不保留 trace。

**阶段 2：evaluateStable（reasoner 辅助，需要调 API）**

Stable 特质的反例检测依赖语义匹配——"这条 M 条目的行为是否违背了 '倾向显式错误处理胜过 panic'"——机械规则难以判断。因此交给 dream reasoner 在其已有的跨项目 M 扫描中一起评估。

在 dream 的 user prompt 末尾追加评估段：

```
=== existing stable traits (evaluate for contradictions) ===
- **prefers explicit error handling over panic**
  sources: proj-seek (3 sessions), proj-foo (1 session)

- **code style is concise, 3-line repeat > early abstraction**
  sources: proj-seek (5 sessions)

For each stable trait, scan ALL project M entries above.
If you find ≥2 M entries across projects that explicitly go AGAINST
a trait, output a demotion:
  {"stable_trait": "...", "decision": "DEMOTE", "contradicting_evidence": "..."}
Otherwise output nothing.
```

demotion 时：条目从 `## Stable` 移回 `## Pending`，追加 `⚠ 反例` 标记和原因（保留 trace，不直接删除）。**不自动删除**——降级回 Pending 让用户 review。

#### 实现优先级

| 步骤 | 内容 | 预估行数 | 归属 |
|---|---|---|---|
| 1 | `soul.go` 中 `evaluatePending()` 机械规则（读 Pending 的 markdown 解析 + 三条判定） | ~60 行 | M5.10 |
| 2 | `soul.go` 中 `applyMaintenance()` 把升格/降级/删除写回 soul.md | ~40 行 | M5.10 |
| 3 | `dream.go` 中集成：产出新候选 → evaluatePending → applyMaintenance → 写盘 | ~20 行 | M5.10 |
| 4 | dream prompt 追加 evaluateStable 段 + 解析 demotion 输出 | ~80 行 | M5.12 |
| 5 | `applyMaintenance` 支持 demotion 动作 | ~30 行 | M5.12 |

**M5.10 交付范围**：步骤 1-3。机械 Pending 升格/过期删除。无 reasoner 辅助。
**M5.12 交付范围**：步骤 4-5。reasoner 辅助 Stable 反例检测 + 降级。

#### 与 M 层的对称性

| 维度 | M 层 (auto_sourced, M5.11) | L 层 (Pending, M5.10) |
|---|---|---|
| 初始状态 | `auto_sourced=true` | 在 Pending 段 |
| 累进信号 | `observe_count`（重复观察次数） | 来源项目数（跨项目证据） |
| 升格阈值 | ≥3 | ≥3 个项目 + ≥14 天 |
| 升格目标 | `auto_sourced=false` | 移到 Stable 段 |
| 过期/删除 | 30 天无 recall → GC stale → archive | 30 天无新证据 → 从 Pending 删除 |
| reasoner 辅助 | V4-Flash 判重 | V4-Flash 一致性评估（M5.12） |

同一个直觉：**重复独立证据 + 时间沉淀 = 可信**。

## 7. 与现有系统的集成

memory 子系统**不直接修改** `pkg/agent`——它实现 `internal/hooks` 的几个接口，注册到 `agent.Config.Hooks`，所有运行时挂点都通过 hook 完成。这让 memory 整个特性是可拔插的（关掉 hook → 退回 v0 行为）。

| 子系统 | memory 实现的接口 | 触发 / 用途 | 改动量 |
|---|---|---|---|
| internal/hooks | `PrePromptHook` | **M5.9 变更**：仅 Turn 1 注入 L stable + M 索引快照；后续无写入时不注入；写入时追加 delta 到尾部（user-role `<context>`） | 🔧 实现中 |
| internal/hooks | `SessionStartObserver` | 加载项目 M、跑一次 GC 重算 score、剔出 stale 不进索引 | 已交付 |
| internal/hooks | `PostToolUseObserver`（可选） | 记录 `memory_recall(name)` 后的 `last_recalled_at`（如果不放 tool 内部做） | 已交付 |
| internal/hooks | `SessionEndObserver` | M5.7 自动蒸馏（v2）：**不再调 reasoner**——实时写由 `memory_observe` 在对话中完成，SessionEndObserver 保留为 no-op | 已交付（v1 代码已移除，空壳保留）|
| internal/hooks | `SessionStartObserver`（增强） | 同上，增加 M5.8 cadence 检查：到期则后台 goroutine 执行 dream → append L Pending | 已交付（`$SEEK_AUTO_DREAM`） |
| Tool | — | 注册 `memory_recall` / `memory_remember` / `memory_observe`，沿用现有 tool pattern | v2 新增 `memory_observe` |
| Permission | — | `memory_remember` 跟其他写工具一样走 `permission.Policy` 的 inline 审批 | 0 |
| Skill | — | Skill 的 `.md` 可引用 `memory_recall("name")` 拉项目决策详情 | 0 |
| AGENTS.md | — | 仍在 system prompt 内，不被 memory 改动；conflict 顺序在 `<context>` 块顶部用 1 行声明 | 0 |
| prefix-cache | — | 见 §5：system prompt 永不变；prepend 区**仅 Turn 1 注入**，变化 delta 追加在尾部不在前缀 | 🔧 M5.9 改善中 |

**为什么这么分**：原本想把"加载 M / 注入 / GC"都堆在 `cmd/seek/main.go`，但那会让 `--resume`、`--continue`、未来的 RPC mode 各自踩同一段逻辑。挂到 hook 上，agent 生命周期是谁触发的都自动走对。

## 8. 验收标准

| # | 标准 | 验证方式 |
|---|---|---|
| 1 | 新 session 第一次 `Prompt()` 时，L stable + M 索引以 user-role `<context>` 块出现在用户消息之前；system prompt 不被 memory 改动 | 集成测试 |
| 2 | `memory_remember` 调用触发 permission.Policy 的 ask 流程，用户拒绝则不落盘 | 集成测试 |
| 3 | 跨项目切换：proj-A 的 M 不会被 proj-B 的 `PrePromptHook` 注入（按 cwd 的 project-id 严格隔离） | 集成测试 |
| 4 | 项目目录移动后，凭 `.seek/project-id` 反向找到原 M | 单元测试 |
| 5 | `seek -dream` 在 N=1 的样本上拒绝产出 L 候选 | 单元测试 |
| 6 | **Turn 1 总注入**：L stable + M 索引快照总和 < 4k tokens（不挤占 prefix-cache 预算）；后续轮次无注入时不计入 | 基准测试 |
| 7 | **快照字节稳定**：同一份 L + M 磁盘内容两次构建快照，输出的 `Prepend` 消息字节相同（Turn 1 缓存命中前提）；**delta 字节稳定**：同一组新增条目两次构建 delta，输出字节相同 | 比对 SHA-256 |
| 8 | 用户手动编辑 `~/.seek/soul.md` 后下次启动生效 | 集成测试 |
| 9 | `/distill` 手动蒸馏候选不自动落盘——任一条目用户按 n 都**不写入 M**。AutoSourced 条目按 n 是**删除已落盘内容**（语义不同，见 #15） | 集成测试 |
| 10 | 启动时 GC：score < 0.5 的 M 条目（含远古高频条目除外）被标记 stale；stale 条目从索引剔出但仍可 `memory_recall` 取到 | 单元测试 + 表驱动用例覆盖 §6 那张对照表 |
| 11 | `pinned=true` 的条目即使 score 为 0 也永不剔除，永远进索引 | 单元测试 |
| 12 | `created_at` 距今 < 7 天的新条目即使 recall_count=0 也不被剔除（grace period） | 单元测试 |
| 13 | **v2**：`memory_observe` 调用后工具返回空字符串，不阻塞模型；后台过滤通过后通过 ResultChan 回传通知，TUI 生成 `"📝 saved to M: ..."` 通知行；过滤拒绝或超时则无通知 | 集成测试（通过 `Distiller` 接口注入 mock 控制过滤结果，mock ResultChan 验证 TUI 通知行为）|
| 14 | **v2**：`memory_observe` 写入的条目标记 `auto_sourced=true`，且**不被 GC 评估**（无论 score 多低都保留） | 单元测试（mock RunGC 时验证 auto_sourced 条目被跳过）|
| 15 | **v2**：`/distill` 打开 review 列表时，`auto_sourced=true` 的条目以 `[auto]` 标签优先展示；用户按 n 后从 `memory.jsonl` 删除 | 集成测试（TUI 状态机 + 读取文件验证）|
| 16 | **v2**：`memory_observe` 覆盖已确认条目（`auto_sourced=false`）时通过 ResultChan 回传错误，不修改磁盘 | 单元测试 |
| 17 | **v2**：`memory_observe` 覆盖未确认条目（`auto_sourced=true`）时正常更新内容和时间戳 | 单元测试 |
| 18 | **v2**：同一 `name` 的 `memory_observe` 并发调用，只有第一个启动 goroutine，后续静默合并（per-name 去重） | 单元测试（模拟并发调用，验证 goroutine 计数）|
| 19 | **v2**：每 session 超过 `$SEEK_OBSERVE_MAX`（默认 10）次 `memory_observe` 调用后，后续调用不启动过滤 goroutine | 单元测试（模拟多调用，验证 goroutine 上限）|
| 20 | **v2**：goroutine ACCEPT/REJECT 结果通过 `ResultChan` 送达 TUI，TUI 正确渲染通知或保持沉默 | 集成测试（mock channel，验证 TUI 渲染）|
| 21 | **M5.11**：M-index 中 auto_sourced 条目标记 `[auto]` 前缀；已确认条目不带前缀 | 单元测试 |
| 22 | **M5.11**：`observe_count ≥ 3` 时 auto_sourced 自动置 `false`（跨 session 重复观察升格） | 单元测试 |
| 23 | **M5.11**：`recall_count ≥ 3` 且 `auto_sourced=true` 时，`TouchRecall` 自动置 `false`（recall 驱动自升格） | 单元测试 |
| 24 | **M5.11**：`memory_observe` 覆盖未确认条目时 `observe_count` 保留历史值并 +1，不重置为 0 | 单元测试 |
| 25 | **M5.10**：dream 后 `evaluatePending()` 对满足 ≥3 来源 + ≥14 天 的候选执行升格（Pending → Stable） | ✅ 单元测试 |
| 26 | **M5.10**：dream 后 `evaluatePending()` 对 30 天无新证据的候选执行删除（从 Pending 移除） | ✅ 单元测试 |
| 27 | **M5.10**：dream 后 `evaluatePending()` 对不满足升格/删除条件的候选保留在 Pending | ✅ 单元测试 |
| 28 | **M5.10**：`applyMaintenance()` 写入后 soul.md 的 Stable 和 Pending 段格式正确，不影响用户手动编辑的内容 | ✅ 集成测试 |

## 9. 实现计划

按子里程碑切，每个独立可发布。**全部已交付**：

| 子 ms | 内容 | 状态 |
|---|---|---|
| ~~**M5.h**~~ | ~~`internal/hooks` 生命周期 hook 子系统：装饰器 + 观察者接口、Registry、单测~~ | ✅ commit `08660a1` |
| ~~**M5.a**~~ | ~~`pkg/agent` 接入 hooks：`Config.Hooks` 字段 + `Prompt` / `runTurn` / `dispatchTool` / session 生命周期的 7 个挂点，附端到端集成测试~~ | ✅ commit `8b14061` |
| ~~**M5.0**~~ | ~~`internal/memory` 包：L 文件读写 + M JSONL 读写 + `manifest.json` + project-id 反向定位~~ | ✅ commit `6c8f475` |
| ~~**M5.1**~~ | ~~`memory_recall` / `memory_remember` 工具 + permission.Policy 接入~~ | ✅ commit `c748d2b` |
| ~~**M5.2**~~ | ~~memory `PrePromptHook` + `SessionStartObserver`：L stable + M 索引注入；GC 跑在 SessionStart~~ | ✅ commit `46a3e7e` |
| ~~**M5.3**~~ | ~~`/distill` TUI 命令：reasoner 跑提炼 + 逐条 y/n/e review + 落 M~~ | ✅ commits `6817a0b` (logic) + `a377a2a` (TUI) |
| ~~**M5.4**~~ | ~~`seek -dream` CLI 命令：跨项目扫描 + L-pending 候选输出 + Stable 升级检查~~ | ✅ commit `64f0250` |
| ~~**M5.5**~~ | ~~遗忘机制细化：`last_recalled_at` 更新路径、stale 标记、归档（M5.2 的 GC 跑这套规则）~~ | ✅ commit `21f5158` |
| ~~**M5.6**~~ | ~~集成测试 + 多项目场景 + pitfall 记录~~ | ✅ commit `86dd270` |
| ~~**M5.7**~~ | ~~**自动 S→M 蒸馏（v1）**：SessionEnd 时按启发式满意度信号自动跑 distill；写候选标记 `AutoSourced=true`~~ | ✅ commit `a9f89d5`（v2 中代码移除，SessionEndObserver 保留为 no-op）|
| ~~**M5.8**~~ | ~~**自动周期性做梦**：SessionStart 时按 cadence（每 N 次 session 或 K 天，默认 N=20 / K=14）触发后台 dream；候选 append L Pending~~ | ✅ commit `374cfad`（gated on `$SEEK_AUTO_DREAM`）|
| **M5.7-v2** | **自动 S→M 蒸馏（v2）**：新增 `memory_observe` 工具（同 `memory_remember` schema，无 permission gate）；模型在对话中实时检测强信号，调 `memory_observe` 后经异步 V4-Flash 过滤判重后写 M（`auto_sourced=true`）；SessionEnd 不再调 reasoner；用户通过 scrollback 通知即时看到结果 | 🔧 实现中 |

**v1 最终产出**：12 个 commit，`internal/memory` 子系统约 100 个测试（全部 `-race` 通过），3 条 pitfall 记入 `docs/pitfalls.md`。v1 两个自动化路径都默认关闭（`$SEEK_AUTO_DISTILL` / `$SEEK_AUTO_DREAM`），用户显式 opt-in 才启用。**v2 中 `$SEEK_AUTO_DISTILL` 改为默认开启**——`memory_observe` 工具默认注册，用户可设 `$SEEK_AUTO_DISTILL=0` 关闭。

**v1.5 纳入 M5.9**（M5.9 原属 v2，因缓存优化价值高提前到 v1 与 v2 之间的增量发布——见 §11）：

| 子 ms | 内容 | 状态 |
|---|---|---|
| ~~**M5.9**~~ | ~~M-index 注入改为 session-start 快照 + delta 追加；消除 mid-session 前缀变化导致的缓存 miss；PrePromptHook 无写入时零注入~~ | 🔧 实现中 |

**回顾**（vs PRD 预估）：
- M5.3 实际拆成 logic + TUI 两个 commit——TUI 状态机（y/n/e/q + 编辑模式）比预想 fiddly，分开提交让 API 单独可审
- M5.5 部分内容（`last_recalled_at` 路径、stale 标记）实际在 M5.1/M5.2 就已经落地；M5.5 专注于补归档机制（60 天双悬崖 + `archived.jsonl`）
- M5.7/M5.8 中途从 v2 提前到 v1——自动化路径默认关闭，启用门槛足够低（一个 env var）但默认安全
- M5.7-v2（异步过滤 + 实时通知）在后续沟通中确定：**去掉 SessionEnd reasoner 调用，改为模型在对话中实时检测强信号并调 `memory_observe`，经异步 V4-Flash 过滤判重后写 M**。加入 per-name 并发去重、每 session 过滤上限、ResultChan→TUI 回传通道三块保护。
- Stable 升级检查（K=3 次会话无反例 → Pending 升 Stable）需要 session 连续性追踪，v1 没做——归入 **v2 M5.9**（见 §11）；L stable 段目前只能用户手动编辑
- `seek -dream --review` 交互式 review 也留给后续——v1 用 `-dream`（预览）+ `-dream-write`（写盘）两段式替代

比扁平 KV 方案（5.5 天）贵 2.4x，但拿到的是一个**会学习的系统**而不是一个键值存储。

## 10. 风险

| 风险 | 缓解 |
|---|---|
| 做梦产生幻觉式 L（"用户讨厌 TypeScript"——其实只是没用过） | N≥2 项目证据才入 Pending；K=3 次会话无反例才升 Stable；用户可手动删 |
| M5.7-v2：模型调用 `memory_observe` 过于频繁 → token 浪费 + M 垃圾 | 工具 description 明确仅用于强信号场景（方案确认/纠错/关键约束）；同话题用 `name` 覆盖而非新增；即时通知让用户可当场否决 |
| M5.7-v2：异步过滤每 session 最多 10 次 V4-Flash 调用 → token/成本超预算 | `$SEEK_OBSERVE_MAX` 环境变量可调低上限；超限调用静默丢弃 |
| M5.7-v2：模型误写入错误决策 | `auto_sourced=true` 确保条目可被 `/distill` review 时删除；用户看到通知后可立即纠正（模型在对话中，响应成本为零） |
| M 写入太频繁 → permission ask 弹窗疲劳 | `memory_remember` 默认要审批，但加 `/memory-yolo` 开启会话内 auto-accept；session 结束恢复 |
| L 文件被用户改坏 | parse 失败时降级到"L 为空"，不阻塞启动；下次做梦会重新生成 |
| 注入文本变化导致 prefix-cache miss | 采用 session-start 快照 + delta 追加策略：快照前缀永不变化，变化仅限尾部 delta，miss 范围从"整个前缀"缩小为"仅新增 delta 字节"；CI 加测试断言快照和 delta 各自字节稳定（验收 §8 #7） |
| 未来其他 `PrePromptHook`（非 memory）打乱注入顺序，让旧 history 出现新字节 | hook 装饰器按注册顺序链式调用——保证 memory hook 在 registry 初始化时**第一个**注册；启动时 `Registry.Stack()` 打 debug 日志可追 |
| Skill / MCP / AGENTS.md 共同膨胀 system prompt | budget 监控加 `system_prompt_tokens` 维度；超 8k 触发告警 |

## 11. 后续版本

> 原本计划在 v2 的"自动 S→M 蒸馏"和"自动周期性做梦"已在 v1 收尾时纳入 M5.7 / M5.8（见 §9）。

**v2（seek v0.3.x）— 三个待交付里程碑**：

| 里程碑 | 内容 | 状态 |
|---|---|---|
| **M5.7-v2** | 自动蒸馏升级：新增 `memory_observe` 工具（同 `memory_remember` schema，无 permission gate）；模型检测强信号后经异步 V4-Flash 过滤判重写 M（`auto_sourced=true`）；SessionEnd 不再调 reasoner；用户通过 scrollback 通知看到结果 | 🔧 实现中 |
| **M5.9** | M-index 注入优化：session-start 快照 + delta 追加策略，消除 mid-session 前缀变化导致的缓存 miss；无写入时 Hook 零注入 | ✅ 已交付 |
| **M5.10** | L 层维护机制（阶段 1）：dream 新增 `evaluatePending()` 机械规则（来源≥3 项目 + 首次观察≥14 天 → 升格 Stable；30 天无新证据 → 删除）+ `applyMaintenance()` 写回 soul.md | ✅ 已交付 |
| **M5.11** | AutoSourced 灵活性升级：`[auto]` 索引前缀 + `observe_count` 累进置信度（≥3 自动升格）+ recall 驱动自升格（≥3 次 recall → 自动确认） | ✅ 已交付 |
| **M5.12** | L 层维护机制（阶段 2）：dream reasoner 辅助 Stable 反例检测（`evaluateStable()`）+ 降级回 Pending + `seek doctor` 检查 soul.md 一致性 | ⬜ 待设计 |
| **M5.13** | Observe 反馈闭环：M-index 注入聚合 observe stats（通过/拒绝率），让模型感知观察质量。Hook 累积 stats → OnSessionEnd 写 observe-stats.json → 下次 Turn 1 注入 | ✅ 已交付 |
| **M5.14** | 单项目 L 候选：将 dream 的 N≥2 过滤器放松为 N≥1 进入 Pending（不自动升格 Stable，仅供用户预览）。让单项目用户也能从 L 层受益 | ⬜ 待设计 |
| **M5.15** | M-index 智能保留：当前 1500 token 预算触发截断时按 pinned→recall_count→name 排序，低 score 条目被丢弃。探讨是否在截断线以下保留"高价值低频"条目（如 tagged lesson、pinned-light 机制） | ⬜ 待设计 |

**v3**：
- M 引入 embedding，长尾内容走 RAG（`memory_search(query)` 工具）——解决精确 name 匹配的局限，模型可按语义搜索全部 M 条目
- 多机器同步（基于 git push `~/.seek/` 的 dotfiles 工作流）

**v4**：
- 跨用户共享 L（团队级"我们这种工程师典型的偏好"）
