# 触发、写入、更新、读取、删除的判定约定

这份文档写的是**代码现在真正执行的规则**，不是产品愿望。改行为先改这里，再改实现和测试。

入口按顺序只有五个：

1. **自动注入**：每轮真人消息后，`index.js` 在 provider 准备阶段判断是否把 1–3 张卡片塞进上下文。
2. **工具检索**：`memory_search`，模型主动查。
3. **工具读取**：`memory_read`，按 id 取正文；关系用它的 `view:"links"`，没有单独的 `memory_links` 工具。
4. **工具写入**：`memory_save`，新建或更新同一条。
5. **面板与路由**：`/lite-memory/*`，人点「记住 / 忽略 / 归档 / 搬移 / 撤销 / 移除」。

五个入口共用 `view.eligibility(record, runtime, purpose, now)` 这一道可用性闸门。任何入口都不得绕开它自行判断「这条能不能用」。

## 一、判定先后的硬顺序

任何一条记录，从「存在」到「能被用」，顺序固定：

1. 记录存在且未归档（`status !== 'archived'`）。
2. 归属项目匹配：`ownerProjectId` 有值时必须等于当前 `projectId`；全局层不设。
3. 实验来源（`origin === 'project_evolution'`）额外的三件事：来源没被关（`runtime.experimental !== false`）、学习状态仍在有效期内（`active`，或未到期的 `trial`）、没有 `paused` / `blockedReason`。
4. 旧库迁移且未复核的记录（`reviewState === 'legacy_unreviewed'`）：自动注入一律不碰；人工打开面板可以看。
5. 条件与任务状态：`statusOf(record, context)` 必须为 `ok`。条件读不出来算 `needs_context`，条件明确不符算 `not_applicable`。
6. 自动注入额外要求状态严格为 `ok`；工具检索也只把 `ok` 的条目放进结果，`needs_context` 的整条挡在外面，只在「有命中但全都不可用」时给一句统一说明，不递标题和 id。
7. 预算：请求级预算与自动注入预算是两道，先准入再记账（见第二、三节）。

前六步任何一步失败，错误状态只能是 `gone / not_applicable / needs_context` 三者之一；不要再造新状态，也不要把「不适用」包装成「没有记忆」。

## 二、自动注入

**触发点**：每轮新真人消息（`freshHumans` 有新增）且 `recallMode() === 'adaptive'`。工具模式、面板操作、同一请求内的模型调用都不触发。

**取什么**：

- 项目层 + 全局层当前投影；同一 id 两层都有时按 `project` 优先，只留一条。
- 归档条目、实验来源（普通自动召回不碰，实验规则单独走 `experimentFor`）、`legacy_unreviewed` 全部排除。
- 同一 `topicKey` 槽位只让胜出的记录进；槽位内冲突或条件未知的整组不进自动召回，不猜哪条对。
- 默认值 `__tasks.defaults` 会参与任务上下文，但只是「范围提示」，不是把某条记忆设成必用。
- 「继续 / 然后呢」这类续说句没有检索词时，沿用同会话上一轮原话；别把新的短问题接到旧查询上。

**预算**：普通卡片与实验规则**共用**一份账本：最多 3 张卡片、最终拼接文本最多 1200 字节。字节量的是 provider 最终看到的完整文本（含标题行、ID、换行、两段之间的空行），不是单张正文。超预算时跳过这张，不截半句、不把标题留在模型侧。

**实际 Token**：自动提示最多 3 条、1200 字节，不随全库条数线性增长；总量仍取决于命中频率、模型调用步数、主动读取正文，以及实验性自进化的整理次数。别把「每轮有上限」说成「不增加消耗」。

**失败**：

- 准备超过 `RECALL_DEADLINE_MS`、被取消、投影期间开关/代次变了：整批回退，预算退还，下一轮重新来。绝不发布半旧卡片。
- 召回为空是正常结果，不是错误；不弹提示、不占用额度。
- 已发布但用户随后关闭记忆：从下一轮起不再注入；当轮已发出的内容不追回。

**可解释性要求**：卡片标题行必须能看出「这是历史笔记，不是本轮指令」；引用条只展示被真正读进上下文的条目，不把「检索到但没用上」的算作引用。

## 三、工具检索与读取

**`memory_search`**

1. 当前请求的 `contextId` 是可选的：不给就按这次调用已经路由到的运行时上下文判定；显式 `bindForRequest` 却拿不到 id、给了过期/跨项目的 id，才返回 `needs_context`。
2. 先判范围是否登记过（栏目、任务类型、系列）。没登记过就明确说「本轮没有匹配的记忆范围」，不按关键词硬凑。
3. 按 `eligibility(..., 'search')` 过滤；再用 `status === 'ok'` 截一遍。`needs_context` 条目不进结果；全都被挡时只回一句「有相关的项目记忆，但适用条件还没确认 / 当前任务明确不适用」，不带标题、编号。
4. 结果条数受请求级 `reads` / `bytes` 限制：先按字节退条数，还剩至少一条就按 `ok` 返回已缩的部分；一条都放不下才 `budget_exhausted`，此时正文为空。
5. `reasonCode` 只在三种情况出现：`no_match`、`not_applicable`、`applicability_unresolved`。`budget_exhausted` / `memory_disabled` / `needs_context` 是 `status`，这些分支不带原因码。

**`memory_read`**

- 只认 `mem_` 开头且已过格式校验的 id；未知 id 返回 `gone`，不猜路径。
- 读正文同样要过 `eligibility` 与请求级 `bodies` 预算；读到的正文按当前版本返回，不返回历史版本冒充当前。
- 冷存（归档正文）按需读；冷存损坏就明确报 `cold_corrupt` / `cold_missing`，禁止用空正文顶上。
- `memory_read` 的 `view:"links"` 只返回真实关系；目标条目不可用的关系不返回，避免把死链当线索。

## 四、写入与更新

**先搜再写**：工具说明要求模型先 `memory_search`，能更新就不新建。服务端仍会独立做重复判定，不能只靠模型自律。

**身份**：`id` 是唯一身份；同一件事更新时带 `id`。相似度只在「没带 id」时用于拦截：文本相近但标题不同，返回 `needsResolution` 和 1–2 条相近条目，等模型确认「同一条」或带 `resolution="distinct"` 再存。不允许「看起来像就自动覆盖」。

**字段语义**：

- 带 `id` 更新时，没给的字段保持原值；给了就整段替换（正文不是追加，旧正文只留作内部撤回，不展示、不参与检索）。
- `title` / `summary` / `when` 是身份的一部分；`when` 为空表示「无条件限制」，不是「待补充」。
- `topicKey` 是槽位；同一槽位只允许一条当前胜出记录，冲突时先让人选，不自动合并。
- `kind` 只影响展示分组，不参与检索和可用性判断。

**范围路由**：模型给的 `scope` 是提案，不是命令。

- 显式 `scope="global"` 表示「用户在这一次对话里要求存全局」，只有用户确实这么说才传；模型自己觉得通用不是理由。显式全局不再因为缺少 `evidenceRef` 被降级。
- 仍然会拦住的两类：设备事实没有条件（`machine` / `targetOs` / `runtime` / `shell` 至少一个），以及正文或依据里写死本机路径、回环地址、端口却没声明适用环境的「通用流程」——后者换台机器/换个项目就不成立，留在项目层。
- 声明了上述环境条件的本机流程（例如本机特有的命令包装）可以进全局层：条件不成立的机器上不会拿出来用；命令名/工具名本身不算本机痕迹。
- `memoryClass="project_fact"` 不因写了 `global` 就变全局：项目事实换个项目本来就不成立。
- 按类别推导（没有显式 `scope`）保持更严：`portable_procedure` 没有 `evidenceRef`、也没有环境条件时落项目层。
- 每次降级都要能解释：只在降级时返回 `scopeNote` 一行，说明为什么没进全局、怎么改才进得去；正常落全局或落项目不带这行。

**限额与腾位**：每层活跃条目软上限 200、硬上限 1000；单条正文 4096 字节；单层总字节 1 MiB。到硬上限时把最久没被读的活跃条目移进归档，并在结果里写明「已归档「标题」腾位」，不拒绝新记忆；单条正文超限只报错、不截断。

**写入安全**：所有写都在作用域锁内、走 `transact` 的「意图文件 → 原子替换 → 发布」，失败按原状态回滚；不允许只改内存不落盘。工具返回 `changed:false` 时不得声称「已记住」。

## 五、条件与范围

条件只有登记过的键能用：`targetOs`、`shell`、`runtime`、`machine`、`projectRoot`、`pathPrefix`、`artifactKey`、`taskKind`、`seriesId`、`phase`。自由措辞不算条件。

- `conditions` 最多 8 条；同一标题但前置条件不同，是两条记录，不合并。
- 条件判定结果只有三种：`ok`、`not_applicable`、`needs_context`。读不出来时不许当成 `ok`。
- `applicability`：`all_in_scope` 普遍适用；`conditional` 必须给条件；`unresolved` 是还没定，自动注入不用，检索也不会把它的标题递出来。
- 项目根目录未确认（`confirmed:false`）时没有项目层身份，不能凭 cwd 新建命名空间；写入要么先确认项目，要么显式 `scope="global"`。

## 六、引用与展示

- 引用条只记「这次真的读进上下文」的条目：`memory_search` 的命中、`memory_read` 的正文、`memory_save` / `memory_forget` 造成的变更。
- 同一条在多个工具结果里出现只留一条，位置取最早、内容取最新。
- 展示分组固定三组：搜索命中、读过正文、本轮更新。没有内容且没有原因时整组不渲染。
- 引用是**展示**，不是新的事实来源；点开只读当前正文，当前已删除/移出就返回空，不回退旧缓存。
- 面板任何「整理 / 修复」动作只动展示与索引元数据，不改学习状态机、不改内容、不占召回额度；记忆总开关关闭时面板只读。

## 七、自进化（默认关闭）

链路只有一条：可信的 `agent/pre-step` 反馈 → `rememberFeedback` → `candidateFrom` → pending → 整理器 LLM → `parseOutput` → `commitLearning` / `admitChange` / `nextLearningState`。

- 只有实验/学习相关动作依赖 `scope.confirmed`；普通记忆的采集与使用不因它整体失效。
- 助手自己的声明不算支持证据，只有用户原话/确认算。
- 项目层规则从 `pending` 起，到 `trial`，再到 `active`；`paused` 只能被显式长期规则或重置证据解除，弱证据不会把 `active` 降级。
- 未改规范（`specDigest` 相同）保持现有状态；旧整理结果不能覆盖新纠正（`feedbackRevisions` 复查）。
- 整理失败只留回执，不产生规则副作用；配额已扣不退，界面显示失败原因。

## 八、关闭、删除与恢复

- **总开关关闭是权限变化，不是内容删除**：`memory_save` 返回 `changed:false, id:'', needsScope:true, reason:'memory_disabled'`；搜索/读取同样拒绝。面板只读。
- **`memory_forget`**：默认移入归档（可人工找回），只有显式 `purge` 才彻底删除正文与受管副本；删除不可复活。
- **撤销**：按 `id + revision` 精确撤销；版本对不上就不猜，避免撤掉别人后来的修改。
- **冷存隔离**：损坏的冷存文件可以被隔离或恢复；恢复前先校验，校验不过不进主库。
- **锁**：同一作用域写锁 500 ms 等不到就 `busy`；持锁进程已死的残留锁会自动回收，活进程占用才报忙。错误信息给「等一会儿重试」这一条动作，不暴露内部路径细节。

## 九、失败语义（模型与界面共同遵守）

| 状态 | 含义 | 下一步 |
| --- | --- | --- |
| `ok` | 可用 | 正常使用 |
| `gone` | 不存在或已归档 | 不重试；要找回走面板 |
| `not_applicable` | 条件明确不符 | 不重试；换任务/换条件 |
| `needs_context` | 当前请求无法核对（过期、跨项目、未确认） | 先重新问一次当前任务，或补 contextId |
| `budget_exhausted` | 本轮额度用完 | 下一轮再查，不绕预算 |
| `memory_disabled` | 本项目记忆关闭 | 让用户先在面板打开；不得自行打开 |
| `busy` | 锁竞争/残留 | 退避重试；持续失败提示清理锁 |
| `cold_corrupt` / `cold_missing` | 冷存损坏/缺失 | 明确报错，不空正文顶上；走隔离/恢复 |

错误文案必须包含「发生了什么 + 下一步动作」，不能只抛内部名词。同一失败不要在多处重复提示；一轮只留一条。

## 十、改规则时的检查单

1. 先改本文档，再改 `view.eligibility` / `store.js` / `index.js` / 测试。
2. 新状态必须在 `eligibility` 里有唯一归属，不能靠调用方各自 if。
3. 涉及落盘：`writeDurable` / `publish` / 冷存 fsync 三条路径都要过；旧 `state.json` 必须还能读。
4. 涉及预算：量最终文本，不量中间对象；先准入再记账，失败退还。
5. 涉及展示：只展示真实发生的事；没有原因的空结果也要能解释为什么空。
6. 跑 `npm test`；仓库没有 lint 脚本，界面改动另按全局设计规范跑文案与样式检查。契约测试里的字段和状态是硬约束，不靠「包含」放宽。
