# 触发与展示

写入分三档，档位决定「谁说了算」和「什么时候出现在对话里」；读取另有一条自动注入。

| 档 | 谁发起 | 什么时候 | 存不存 | 用户看到什么 |
| --- | --- | --- | --- | --- |
| L0 | 用户 | 说「记住 / 以后 / 别用 / 下次」这类话 | 直接存 | `memory · saved "X"` + `✕` |
| L1 | 模型 | 用户没说，但这经验跨会话有用 | 点了才存 | `memory · suggested "X" · sug_x` + 「保留」按钮 |
| L2 | — | 常识、一次性参数、临时路径 | 不存 | 无 |

## 什么时候触发

模型不会常驻读记忆。两条路：工具描述里的触发词（模型自己判断要不要查），或下面这条自动命中（插件判断这句话跟哪条记忆有关）。

- 查：用户说「上次 / 之前 / 记住过 / 规范 / 约定 / 路径 / 凭据怎么取」，或要做的事像是在这个项目里做过。
- 写：用户明确要求；用户自己的话已经是长期规则（「以后都这样 / 别再 X」）时直接存，不先问；或用户对某条建议点了「保留」。
- 建议：一条经验能跨会话复用、又只存在于用户嘴里（偏好、惯例、踩过的坑），且同一项目 48 小时内没建议过。只出按钮，不写「要不要记」这种问句——问句要用户打字，按钮不要。
- 不写：`1+1=2` 这种常识、一次性任务参数、能从仓库直接读到的正文；凭据只记位置与调用方式。
- 自动命中：用户这句话与某条记忆的标题、旧标题、标签、触发词或摘要真有重合、且分数过线时，这一步的运行时上下文里多一段「已有记忆可能相关」——最多三条标题加 id，普通卡片与实验规则共用 3 条 / 1200 字节预算，正文仍要 `memory_read`。只有当前任务条件判为 `ok` 的条目能进；条件没确认或明确不符的既不进自动注入，也不进检索结果，检索只在全都被挡时说明「有相关的项目记忆，但适用条件还没确认 / 当前任务明确不适用」。没重合为空字符串，整条索引不进上下文，也不改 hits / lastUsed，更不落盘。用户这句话没提到动作、一张卡都没命中时，再用上一轮助手的话和工具调用里的实词补一次有界查询（预算不变）；补不到就保留第一次的「为什么没提醒」。同一句话只算一次；换句话重新算。

## 触发后长什么样

每次调用只占一行、灰色、12px：

```
· memory · 命中 2 条 · 引用 120 字
· memory · 没有命中
· memory · saved "插件市场条目字段" · mem_xxx
· memory · suggested "发布前先跑文案检查" · sug_xxx
· memory · skipped
· memory · archived "插件市场条目字段" · mem_xxx，可找回
```

「保留」按钮只在建议行出现。点一下直接写盘：不产生消息、不动输入框、不占用下一轮对话；按钮消失，行变成 `memory · kept`。同源写盘通道不通时才退回旧路——插入 `保留记忆「X」` 并排队，至少不丢这条建议。真写过的行尾才有 `✕`，带着写入时的版本撤销；条目后来又改过就停下并提示版本已变，不覆盖后来的修改。

这些行只属于产生它的那一轮：每轮按那轮自己的工具结果重建，重进会话还能看见，不会串到别的轮。行出现在调用本身的位置——一轮正在跑时，那就是对话的最下面，不会悬浮在输入框上方。

## 通用还是项目

写入时先定层，三层各管一件事，别混着写：

- **全局**（`scope: 'global'`）：跨项目成立的习惯与约定——回复语言、提交信息风格、凭据放在哪。只放「所有项目都这样」的；沾一个仓库的就不放。200 条是提示线，1000 条或正文预算才是硬上限；到顶时最久没读的一条进归档。
- **项目**（默认）：这个仓库的事实、路径、流程、踩坑。换项目就查不到。
- **场景**（`when`）：同一件事在不同前提下的不同做法。带 `when` 的条目只在该条件下成立，检索时显示成「标题（场景）」。

检索一次查两层，带「[全局]」前缀的是跨项目那层；两层同名条目各存各的，不互相合并。归档、读正文按 id 走，全局条目在别的项目里也能归档。

内部判定细节（准入顺序、状态码、失败语义）见 [docs/CONVENTIONS.md](docs/CONVENTIONS.md)。

为什么是这三层：[Claude Code](https://code.claude.com/docs/en/memory) 分 user / project / local，[Codex 的 AGENTS.md](https://agents.md/) 分 `~/.codex` 与仓库根、子目录，都是「按作用域分层、就近优先」，不做语义去重。我们照这个骨架，多的一层是 `when`：同一主题在不同前提下必须能并存，否则合并就是拿一种情况覆盖另一种。[Graphiti](https://github.com/getzep/graphiti) 的做法是每条事实带有效期、冲突只让旧事实失效而不删除，我们收敛成「同键覆盖、旧要点保留」。

## 什么时候更新，什么时候新建

判重是工具做的，不是模型的感觉：`memory_save` 落盘前自己找同一条，找到就更新，找不到才新增。用户只说「记下来」、只点「保留」，走的都是这条路。带 `id` 的保存就是明确更新那条。

判定顺序：

1. 先看层：全局和项目各判各的，不互相合并。
2. 再看前置条件：`when` 不同（含一边写了自己没写）→ 两条，标题后面跟场景，如「构建耗时长｜CI 容器」。
3. 同层同条件下只认标题完全一致，或撞上这条存过的旧标题；相似度不参与判定——「前端 API 地址」和「后端 API 地址」字面 0.5 像，但它们是两条。 <!-- ui-copy-ignore -->
4. 换过说法的标题会存成别名，第三次再用回旧标题也认得出。
5. 标题不像但很接近（两字重合 ≥ 0.3）时不写：保存结果里回「相近：X」和它的 id，确认是同一条就带 `id` 再存，确认是另一条再加 `resolution="distinct"`。这一步在任何写入之前完成，拦下来时磁盘上什么都没动。

更新时没给的字段保持原值，给了才改（`when`、`kind`、`tags`、正文都一样）；正文整段替换，不做章节合并。

- 旧正文原样进 `versions`，最多留两版，可据此回滚——`✕` 一次把标题、场景、标签和正文一起还原；版本对不上时只提示，不动数据。
- 真要把一条记忆作废重写（不是补充），先 `memory_forget` 再 `memory_save`。

按钮那条路也一样：`memory_suggest` 遇到已有同名条目就不出按钮，回「已有「X」，这次不重复建议」；用户点了「保留」的，落到同一个判重函数上。候选里存着确认时的目标 id 与版次：那条记忆在你确认前改过，就重新建议一次，不会拿旧内容盖掉新内容。

## 频率与退让

- 建议：同一项目 48 小时一条、一次对话最多一条；同一主题存过就不再建议。
- 结果：搜索最多 4 条，默认 3 条；每条一行摘要，全文只在 `memory_read` 里出现。
- 用户说「别记」→ `memory_forget` 归档，不删文件。
- 拒绝三次后仍反复建议，说明触发词太宽：收窄 `memory_suggest` 的描述，而不是加状态。

## 新会话为什么不会乱

- 记忆只在工具结果和命中时的上下文槽里出现：命中才占位，整卡受预算限制，也从不伪造用户消息。
- 结果行永远是「记忆 · …」，模型能分清这是历史笔记，不是用户本轮要求。
- 摘要必须是名词短语；「你要…」「请…」这类指令式写法违反约定（工具描述里也会提醒），但写入校验不会替模型改。
- 存储按项目目录哈希隔离，别的项目看不到。
