# 记忆连续性(memory-continuity)· 语义文档 > 版本 v0.1 · 2026-09-13 · 作者:爱丽丝 · 状态:**已实现(implemented)** > 实现落点:`self-plugins/dsh-agent-memory/src/` > 能力载体:插件 `dsh-agent-memory` v0.2.3(HOST 组合行 `agent-memory`) > 开发方式:语义文档优先(先写清「是什么/什么关系/怎么裁决」,再让实现逼近,最后用实践回修) > 本文件是**主副本**(I1);`README.md` 面向使用者,`DESIGN.md` / `IMPLEMENTATION.md` 是设计文档——**都不是本文的同语义副本**,语义冲突以本文件为准(冲突需回修,见 §9) --- ## 1 · 元信息 | 字段 | 值 | |------|-----| | 能力名 | 记忆连续性(memory-continuity) | | 主副本 | 本文件(`self-plugins/dsh-agent-memory/docs/semantic.md`) | | 状态 | **implemented**(验收 67 项:见 §7 汇总行;`pending>0` 故**不得**标 verified) | | 版本 | v0.7(文档)· 对应插件 v0.8.2(`package.json`)——v0.5 角色维度;v0.6 价值体检器;v0.7 重心化注入 + 命中率度量 + 提案日志;v0.8 压缩流水线证据层;**v0.8.1/v0.8.2 修 U10 真因**(总结调用补会话 id;输出预算与可归因证据) | | 实现落点 | `self-plugins/dsh-agent-memory/src/`(19 个模块,见 §8) | | 运行落点 | 数据:`${DSH_HOME}/storages/agent_memory.json`(域 `agent_memory` / 表 `entries`)
配置:`E:\alice\.dsh\memory.yml`(**2026-09-15 起存在**:`roles` 已启用,策略 main/worker/verifier/ghost;其余键走默认)
侧车:`${DSH_HOME}/memory-access-trace.jsonl`(v0.6 用量)、`${DSH_HOME}/memory-audit-proposals.jsonl`(v0.7 提案)、`${DSH_HOME}/memory-compress-trace.jsonl`(v0.8 压缩流水线)
挂载:`.dsh/profiles/web/cordis.patch.yml` 的 `agent-memory` 行(**`config.maxTokens: 32000`**——2026-09-15 由 16000 抬高,见 §7 A64–A67 旁注) | | 作者 / 日期 | 爱丽丝 · 2026-09-13 | | 相关规则 | AGENTS.md §5.20(语义文档系统);§5.8(记忆检索纪律) | ## 2 · 定位与反定位 **定位**:让「经历过的事」跨会话存续——把 agent 决定要记的内容**结构化写入持久层**,再在恰当的时机(会话启动、主人每条消息、时间桶结束、会话压缩完成)**把它带回上下文**,并允许 agent 检索、修订、归档、沿关系行走。 **反定位(本文不管什么)**: - **不是会话历史的替代**:原始逐字记录归 session log 与 compaction checkpoint;本能力存的是**提炼后的条目**。 - **不是知识库检索站**:不做向量检索、不做全文索引站;检索 = 标签/标题/正文加权打分 + 字符 2-gram(中文无空格分词自然工作)。 - **不做内容决策**:记什么、怎么组织、何时腾空归 agent(README §设计总纲:「机制只保证不丢、知道、兜底」)。 - **不是沙箱也不是权限边界**:它读写自己的存储与工作区配置,不隔离调用方、不做授权。 - **不压缩 L1/L2**:时间压缩只作用于 L3 情景记忆;`global` 作用域不压缩。 - **不是本次新写的实现**:本文档是对既有实现的语义收口,未改动任何代码。 ## 3 · 术语 | 术语 | 含义 | |------|------| | 条目(Entry) | 记忆的最小单位:`{id, kind, title, body, tags, scope, createdAt, updatedAt, accessedAt, level, bucket, archived, key?, source?, archiveRef?}` | | L1 `fact` | 事实层(主人偏好/环境事实);带 `key` 时**同 (scope,key) 精确覆盖** | | L2 `knowledge` | 知识层(可复用知识/教训);同标题**合并** | | L3 `episodic` | 情景层(有结果的事件/经历);时间压缩的**唯一原料层**,也是压缩存档的落点 | | `summary` | 压缩产物(日/周/月/年概要);`level` + `bucket` 标明它代表哪个自然单位 | | scope / workspaceId | 存储分区。`global` 为全局;workspaceId = `cwd` 规范化(统一 `/`、去尾分隔符、盘符小写,如 `e:/alice`) | | bucket(桶) | 自然单位标识:`2026-09-12`(日)/`2026-W36`(周)/`2026-09`(月)/`2026`(年) | | archived | **软删**:不进活跃检索,`includeArchive` 可取回(forget 的产物) | | 去重三态 | `created`(新建)/ `updated`(L1 key 覆盖)/ `merged`(L2/L3 标题指纹合并) | | 启动注入 | 会话第一个 step 注入一次「记忆速览」(目录化:标题+时间+tags,不含正文) | | auto-recall | 每条**真实主人消息**到达时注入 top-N 相关记忆(带 snippet) | | 懒压缩 | 访问记忆(recall / memory_stats)时触发的补压(fire-and-forget) | | 周期补压 | 定时器驱动的补压(启动首跑 + 轮询),不依赖是否访问记忆 | | 记忆回流 | 服务 `ctx.memoryApi.remember()`——其他插件把运行态结论写回记忆库 | | 联想链 / 联想闭包 | `relatedOf` 单跳相关条目;`relateClosure` BFS 多跳记忆图行走(`hop` 标注层级) | | 尾部追加 | 动态注入统一放在消息批次**末尾**(保护前缀缓存,避免插在中部破坏缓存命中) | | 角色(role) | v0.5 条目的**分区维度**:`main`(人类会话缺省)/ `derived`(派生会话缺省)/ 自定义(`worker` / `verifier` / `ghost-01` …)。缺省不设字段 = **共享记忆** | | 视野(RoleView) | 一次调用解析出的「角色 + 策略 + 是否收窄 global」,由 `roleViewOf` 产出,所有读路径共用 | | 准入(admission) | 按策略**在检索前剔除**条目(R1–R4 判据);不是排序降权 | | 归属(stamp) | 写路径给新条目盖 `role`(仅当角色维度启用或显式指定)+ 记 `author`(溯源) | | 隔间 | 一个角色对应的可见集合;隔间之间默认互不可见(除非策略 `read` 白名单放行) | ## 4 · 概念模型与不变量 ``` 写路径 读路径 ──────── ──────── 模型 → 10 个工具 ─┐ 会话首 step → 启动注入(速览,每会话一次) 其他插件 → memoryApi ─┼→ MemoryStore ─→ 域 agent_memory.entries ─→ .dsh/storages/agent_memory.json 压缩完成 → compaction sink ─┘ 每条主人消息 → auto-recall(top-3 + snippet,尾追加) 模型 → recall / memory_browse / memory_relate 时间轴:L3 条目 →〔日桶结束〕→ 日概要 →〔周桶结束〕→ 周概要 → 月 → 年(只压已结束单位;同桶同层幂等) ``` **不变量(每条都能被一次测量判真假)**: 1. **I1 单一写入真源**:所有写路径(10 工具 / `memoryApi` / 压缩存档 / 时间压缩)都经 `MemoryStore` → 同一张 kv 表;条目键为 `::`。→ 判据:存储文件里出现任何未经 Store 写入的条目形态即为违反。 2. **I2 写去重三态**:带 `key` 且同 (scope,key) 命中 → `updated`;`knowledge|episodic` 且标题归一化指纹命中 → `merged`(标签并集 + 正文追加,保留原 `id`/`createdAt`);否则 `created`。 3. **I3 检索恒附加 global**:默认读 = 当前 workspace + `global`;工具参数 `scope` 显式覆盖优先于配置;无 `cwd` 时降级只读 `global`。 4. **I4 注入只给线索 + 预算封顶**:启动注入给「标题+时间+tags」不给正文,受 `inject.maxBytes/maxEntries` 约束;auto-recall 只给 top-N + ≤90 字 snippet,受 `autoInject.maxBytes(1500)/maxEntries(3)` 约束;两者**尾部追加**。 5. **I5 auto-recall 触发面严格**:仅 `source.kind==='user'`(GUI/Web)或 `dsh-agent-telegram` 插件注入的主人消息触发;同一消息 id 每会话只注入一次;工具结果/其他插件注入/模型消息一律不触发。 6. **I6 压缩只吃 L3**:`fact`/`knowledge` 永不作为压缩原料;`global` scope 不压缩;只压**已结束**的自然单位;目标桶已有同层级概要则跳过(幂等)。 **I6a 同桶同层唯一(并发保证,2026-09-13 补)**:`compressUnit` 的临界区(查概要 → await LLM 总结 → 写入)按 `compressUnitKey(scope, level, bucket)` 在**进程内串行**(`src/lock.ts`),并在写入前二次复核。⇒ 进程内任意并发调用组合下,同一 (scope, level, bucket) 只落一条概要。判据见 §7 A16;旧反例(4 对重复桶)即本不变量**曾被违反**的实证。 7. **I7 归档是软删**:`forget` 置 `archived=true` 并记 `reason`;默认检索不可见,`includeArchive`/`memory_browse` 可再取;压缩后的原料同样被冷归档(可沿 `archiveRef` 回溯)。 8. **I8 配置 fail-loud**:`memory.yml` 未知键 / 非法值 → 抛 `MemoryConfigError`(不静默补默认);仅「文件不存在」走默认。 9. **I9 视野单点过滤(v0.5)**:启用 `roles` 后,**所有读路径**(recall / browse / relate / stats / 启动注入 / auto-recall)共用 `applyRoleView` 这一处准入过滤——条目在**检索之前**被剔除而非降权。⇒ 判据:任何读路径绕过 `gatherReadable`(工具层)或 `applyRoleView`(注入层)即为违反;未启用 `roles` 时 `applyRoleView` 必须返回**同一数组引用**(零过滤、零拷贝)。 10. **I10 归属不可转移(v0.5)**:`role` 只在**新建**时盖章;命中已有条目(key 覆盖 / 标题合并)保留原归属——写入不得静默转移分区。`author` 记创建者,后续修订不覆盖。 11. **I11 共享是默认且是显式的(v0.5)**:不盖章 = 共享记忆(所有角色按各自策略可见);盖章只发生在「`roles.enabled` 为真」或「调用方显式给 `role`」时。存量条目(含 v0.4 前写入的 674 条)一律为共享——**迁移安全优先于隔离强度**。 ## 5 · 契约 ### 5.1 持久化 | 项 | 值 | |----|-----| | 域 / 表 | `agent_memory` / `entries`(`defineDomain`,版本 1;域名须匹配 `/^[a-z][a-z0-9_]*$/`) | | 键 | `memoryKey(scope, kind, id)` → `::` | | 落盘 | `${DSH_HOME}/storages/agent_memory.json`(storage-domain 负责) | | 校验 | 域声明带 zod schema(写入边界校验);`MemoryStore` 读出的都是**快照拷贝**(防外部误改) | | 生产实况(2026-09-13 取证) | 547 条:knowledge 266 / episodic 177 / fact 68 / summary 35(day 31 · week 4 · month 1);归档 180;scope 分布 `e:/alice` 458 + `global` 89 | ### 5.2 配置 `/.dsh/memory.yml`(缺省值见下表;`DEFAULT_CONFIG` 深冻结): | 键 | 缺省 | 语义 | |----|------|------| | `scope` | `workspace` | `workspace` / `global-first` / `global`(只影响读写路由) | | `layers` | `[fact, knowledge, episodic]` | 启用哪些层级(空数组=全关) | | `auto_sink` | `true` | (字段存在;压缩存档由 compaction 事件驱动) | | `timeline.{day,week,month,year}` | 全 `true` | 各层级压缩开关 | | `timeline.archive` | `keep` | 当前契约**只允许** `keep` | | `max_entries` | `2000` | 条目上限:**仅拦截新建**,覆盖/合并不受限 | | `inject.{enabled,max_bytes,max_entries}` | `true / 3000 / 20` | 启动注入预算 | | `auto_inject.{enabled,max_bytes,max_entries}` | `true / 1500 / 3` | auto-recall 预算 | 插件行配置(`cordis.patch.yml`):`provider`/`model`(空=跟随会话路由)、`maxTokens`(宿主实配 **16000**;`summarizer.ts` 的 `DEFAULT_MAX_TOKENS` 也是 16000)、`compressIntervalMinutes`(360)、`compressInitialDelaySeconds`(30)。 ### 5.3 作用域裁决(`resolveScopes`,纯函数) | 输入状态 | 读 scopes | 写 scope | |---------|----------|---------| | 显式 `scope='global'` | `[global]` | `global` | | 显式 `scope=` | `[, global]` | `` | | 无 `cwd`(任意配置模式) | `[global]` | `global` | | 配置 `global` | `[global]` | `global` | | 配置 `global-first` | `[global, ]` | `` | | 配置 `workspace`(默认) | `[, global]` | `` | 优先级:**显式参数 > 配置模式 > 无 cwd 降级**。 ### 5.4 工具面(12 个,均 `defineTool` 注册) | 工具 | 语义要点 | |------|---------| | `remember` | 写入;L1 key 覆盖 / L2·L3 合并 / 新建;受 `max_entries` 守卫(仅新建);`kind` 未在 `layers` 启用 → fail loud | | `recall` | 检索:关键词/层级/标签/时间过滤 + 相关度(标签3 > 标题2 > 正文1)与新鲜度排序;**每个结果附 `related` 联想链** | | `update` | 按 id 修订;**v0.9 三模式**:`replace`(缺省·`text` 首行作标题、全文作正文)/ `append`(追加到正文末尾)/ `patch`(`find`→`replace`,**要求唯一命中**,0 处或多处一律 fail loud);内容真变即落**修订快照**(`revisions`,保留最近 3 条);`tags` 整体替换 | | `forget` | 软归档(`archived=true`,`reason` 记入 `source`);**v0.9 三种选择器**:`id`(单条·未命中报错)/ `ids[]`(批量)/ `tier`(按 `memory_audit` 分档批量:`ARCHIVE`/`DEMOTE`/`REVIEW`,**`KEEP` 被拒**);`max` 护栏(缺省 100 / 硬上限 500)+ `dryRun` 预览;批量自动记 `forget: bulk(N 条)` | | `memory_merge` | **v0.9 合并原语**:把 `ids` 的正文并入 `canonical`(带「合并自 )」来源标注,**信息不丢**),随后软归档之;`strategy=keep-canonical` 只归档不动正文;跳过分支(canonical 自身 / 列表内重复 / 不存在 / 已归档)逐条回报 | | `memory_browse` | 时间金字塔浏览(层级/时间/标签过滤 + 分页),「不知道有什么」时的发现路径 | | `memory_relate` | 按 id 展开关联网络;`depth>1` 走 BFS 多跳闭包(`hop` 标注、visited 防环) | | `memory_stats` | 各层/桶/归档计数(跨 scope 聚合) | | `memory_audit` | **只读提案器**(v0.6 §5.9):KEEP / DEMOTE / ARCHIVE / REVIEW 四档 + 证据行 + 层级聚合;**不归档、不删除、不刷 `accessedAt`**;读路径一样过角色视野。**v0.9:与 `forget`/`memory_merge` 构成可执行闭环**(提案 → `dryRun` 预览 → 执行) | | `memory_health` | 运行时概览:条目/归档数、读 scopes、注入开关、**角色与判据**(v0.5)、**命中率信号**(v0.7:注入次数/主动检索/去重命中/最近时刻)、**压缩流水线读数**(v0.8 §5.12:扫描轮数/压缩单元数/最近待压数/非待压判定分布/候选样本) | | `memory_version` | 版本 + 构建时刻(**动态**读 `package.json` 与产物 mtime) | | `memory_check` | 「待沉淀建议」;**当前恒返回空数组**(通道 B 未接线,§8) | 模型可见语义另有一条服务面:`ctx.memoryApi.remember({text, kind?, tags?, key?, scope?})` → `{id, action}` 或 `{error}`(默认 `global` + `knowledge`;标题取正文首行,>80 字截断)。 ### 5.5 注入契约 | 项 | 启动注入(`inject.ts`) | auto-recall(`auto-inject.ts`) | |----|----------------------|-------------------------------| | 时机 | `agent/pre-step` 且 `step===1`,每会话一次 | 每条**新**真实主人消息(id 去重) | | 内容 | `【记忆速览】`:global `fact` 全量 → summary 按 bucket 降序 → 近期明细按 `updatedAt` 降序(只有标题/时间/tags) | `【相关记忆(auto-recall)】`:top-N,每行 `- [KIND] 标题(日期 · 相关度):snippet(≤90)` | | 预算 | `maxEntries` 条 / `maxBytes` 字符(截断追加提示) | 同左(另:query 截断到 200 字符防长文噪音) | | 位置 | 批次**末尾**追加 | 批次**末尾**追加 | | 帧 | `<system-reminder>从记忆库加载的相关记忆(dsh-agent-memory):…` | `<system-reminder>按当前消息自动检索的相关记忆(dsh-agent-memory):…` | | 空结果 | 不注入 | 不注入 | | 消息来源 | `plugin: dsh-agent-memory` | `plugin: dsh-agent-memory` + `form: 'recall'` | ### 5.6 时间压缩契约 - **原料链**:`day ← episodic`(按条目 `bucket` 或 `createdAt` 归日桶)→ `week ← level=day 的 summary` → `month ← level=week` → `year ← level=month`。 - **触发**:`findPendingCompressions` 扫「所有已结束且**有原料、无同层概要**」的桶(含历史缺口回填)+ **上一自然单位**;`compressPending` 按 day→week→month→year 循环(保证链式原料就绪)。 - **执行**:LLM 总结(`summarizeEntries`,`maxTokens` 缺省 16000)→ 写 `summary` 条目(带 `archiveRef` = 原料 id 列表、`source.reason='时间压缩:<level> <bucket>,覆盖 N 条原料'`)→ 原料冷归档。 - **两条触发路径**:懒压缩(`tools.ts:228` recall / `tools.ts:350` memory_stats 内 `deps.compress(...)`,fire-and-forget、错误 `.catch(()=>{})` 吞掉)+ 周期补压(`periodic.ts`:`setTimeout(initialDelay=30s)` 首跑 + `setInterval(360min)` 轮询;scope 由 `store.scopes()` 自举并排除 `global`;单 scope 失败 `console.error` 后继续,下轮重试)。 - **并发与互斥(2026-09-13 补)**:两条路径**各建一个 `TimelineCompressor` 实例**(`index.ts:184` / `periodic.ts:53`),故互斥**不能**靠实例状态——`compressUnit` 临界区经 `src/lock.ts: withKeyLock` 按 `compressUnitKey(scope, level, bucket)`(`timeline.ts` 导出,**键唯一真源**,禁止调用点各自拼串)串行。键粒度 = scope+层级+桶(不同桶可并行压缩)。语义 = **排队串行**(后到者进锁后重新观察状态 → 命中刚落库的概要 → `already-summarized`),**不是**共享在飞结果;前序任务抛错不毒化链(下一次照常重试)。 - **写前复核(跨进程兜底)**:`store.remember` **之前**再读一次同桶概要;已存在则让位(`skipped=true, reason='already-summarized'`,返回已有概要、不写不归档)。⚠ 复核必须位于写入**之前**——写在 `remember` 之后会命中自己刚写的那份 → 提前 return → **原料永不归档**(实现时踩过,见 §9 第 7 条)。 - **锁的边界(诚实声明)**:只覆盖**单进程**。多实例共享同一 DSH_HOME 时(并行会话为常态工况,AGENTS.md §5.14),跨进程窗口由写前复核从「一次 LLM 往返」压缩到两次读写之间——非零,但已不足以产生重复桶(无 CAS 支持,故不声称零)。 ### 5.7 调用点清单 `[MUST]` | 调用方 | 调用点(文件:符号) | 时机 | |-------|------------------|------| | 插件装配 | `src/index.ts: apply()` | 开域 `agent_memory` → `MemoryStore` → `ctx.provide('memoryApi')` → 注册 11 工具 → 三个 install → 周期补压 | | agent-loop | `src/inject.ts: installMemoryInject`(订阅 `agent/pre-step`) | 每会话第 1 个 step | | agent-loop | `src/auto-inject.ts: installAutoRecallInject`(订阅 `agent/pre-step`) | 每条新主人消息 | | 会话事件流 | `src/compaction-sink.ts: installCompactionSink`(订阅 `session/event`) | `compaction/start` / `compaction/summary` / `compaction/end` | | 定时器 | `src/periodic.ts: installPeriodicCompress` | 首跑 30s;此后每 360 分钟;随 dispose 清定时器 | | 工具执行内 | `src/tools.ts`(recall / memory_stats)→ `deps.compress` | 访问记忆时补压(fire-and-forget) | | 其他插件 | `ctx.memoryApi.remember()` 消费方:emotion / taskboard / evolution-core / skill-forge | 运行态结论回流(默认 `global`) | | 装配(v0.8 证据层) | `src/index.ts: makeCompressTrace(cfg, trigger)` | 懒压缩与周期补压**两条路径各自注入 sink**;sink 补齐 `atMs`/`trigger` 后落 `<DSH_HOME>/memory-compress-trace.jsonl`(吞错) | | 模型 | 11 个工具(§5.4) | agent 自主调用 | **写入真源唯一**:上表所有路径最终都调 `MemoryStore.remember / update / forget / put`。 ### 5.8 角色维度契约(v0.5 · 多智能体工作台模式) **背景**:工作台里主脑(人类会话)、派生队员(子代理 / Agent Teams 成员)、验收方、幽灵隔间共享**同一张记忆表**。角色维度给「谁看得见什么」一个可配置、可验收的答案(借鉴 MAGE, arXiv:2608.29678 的 role policy index + Γ 准入门槛)。 **配置块**(`memory.yml`,全部 fail-loud;缺省见 `DEFAULT_ROLES_CONFIG`): | 键 | 语义 | 缺省 | |----|------|------| | `enabled` | 总开关。**false = 零过滤**(行为与 v0.4 完全一致) | `false` | | `default` / `derived` | 人类会话 / 派生会话的缺省角色名 | `main` / `derived` | | `by_preset` | `agentPreset` → 角色(预设名映射,如 `code: worker`) | `{}` | | `policy_default` | 未声明策略的角色所用策略 | `{read: ['*'], include_shared: true, include_global: true}` | | `policies.<role>` | 角色策略:`read`(归属白名单,`'*'` = 全部)、`kinds`(类型白名单)、`include_shared`、`include_global` | `{}` | **推导链**(`deriveRole`,纯函数):显式 `role` 参数 → `by_preset[agentPreset]` → 人类会话(`session-<uuid>`)取 `default` → 派生会话取 `derived`;**无会话 id → 取 `default`**(保守,宁可见全不可静默失明)。判据理由随 `memory_health.roleReason` 透出。 **准入判据**(`admitsEntry`,按序,fail-closed): | 序 | 判据 | 结果 | |----|------|------| | R1 | `policy.kinds` 非空且条目 kind 不在其中 | 拒绝 | | R2 | 条目无 `role`(共享)且 `include_shared !== false` | 放行 | | R3 | 条目 `role` === 调用者角色 | 放行 | | R4 | `policy.read` 含 `'*'` 或含条目 `role` | 放行 / 否则拒绝 | **读路径**:`gatherReadable`(工具层)与注入层各自先 `narrowReadScopes`(`include_global: false` 且**未显式给 scope** 时去掉 global),再 `applyRoleView` 过滤。 **写路径**:`role` 只在**新建**时盖章(启用 roles → 盖调用者角色;未启用 → 仅显式 `role` 生效;两者皆无 → 共享);`author = {sessionId, delegationDepth, preset}` 无条件记录。 **工具参数**:`recall` / `memory_browse` / `memory_relate` / `remember` 接受 `role`(视角/归属覆盖);`update` / `forget` **不提供**越视野后门(只能改视野内条目)。 **推荐骨架**(工作台部署,示例见 README):`main: read ['*']`;`worker: read ['main']`;`verifier: read ['main'], kinds [fact, knowledge, summary], include_global false`;`ghost-*: read []`(隔间互不可见)。 ### 5.9 价值体检器契约(v0.6 · `memory_audit`) **定位**:**只读提案器**,回答唯一问题——「库里哪些条目值得继续占位置,哪些该降级/归档」(给证据行,不给感觉)。**提案 ≠ 裁决**:它不归档、不删除、不写库、不刷新 `accessedAt`;动记忆数据归 agent(且属须请示类)。 **输入信号(先量后设计 · 2026-09-15 实测 676 条)**: | 信号 | 有无 | 取证 | |------|------|------| | 时间(createdAt/updatedAt/accessedAt) | ✅ | **读路径不刷新 `accessedAt`**:676 条中 180 条 `accessedAt≠updatedAt`,而 180 **恰等于归档数** ⇒ 差异只来自归档动作 | | 引用结构(`archiveRef` 承重) | ✅ | 177/676 条被概要引用 | | 体量(字符) | ✅ | 活跃 496 条 / 683,315 字符 | | 标签 | ✅ | 仅 2 条无标签 | | 近重复 | ✅ | 标题指纹 6 簇 / 13 条 | | 溯源(`source` / `role`) | ⚠ 弱 | 473 条无 `source`(权重给低) | | 使用次数 `u(x)` / 增益 `g(x)` / 矛盾数 `cnt(x)` / 置信 `κ(x)` | ⚠ 由**侧车轨迹**补 `u(x)` 一项 | 见下「逻辑轨迹」;其余三项本版**不假装有** | **评分(序数;不宣称绝对值有意义)**: ``` score(x) = w_ref·承重 + w_rec·recency + w_use·usage + w_tag·标签 + w_role·归属 − w_size·体量 − w_dup·重复 recency = exp(−age/S),S = 30 天(MAGE 的 R(x,t),减复述项) usage = 侧车命中次数(无轨迹 ⇒ 恒 0,公式不因此失真,只少一项证据) ``` 权重来自 `memory.yml` 的 `audit.weights`(**启发式先验,不是拟合值**——校准是 v3 的事,需要「提案被采纳/否决 + 事后是否后悔」的样本)。 **四档判据(输出决策 + 理由,不是排行榜)**: | 档 | 判据(按序判定) | |----|-----------------| | `REVIEW` | 落在近重复簇 / 体量超 `review_min_chars` / 层级异常 → 交人裁决 | | `KEEP` | 被 `archiveRef` 引用(承重原料)**或** age ≤ `keep_recent_days` **或** 有 `role`+`author` | | `ARCHIVE` | 未被引用 且 age ≥ `archive_min_age_days` 且 无 `source` 且 已被概要覆盖(同 kind 存在更晚概要) | | `DEMOTE` | 其余「未被引用且体量 ≥ `demote_min_chars`」(可降级:并入上级概要/压缩) | **输出形状**:`memory_audit({ scope?, role?, topN?, minChars?, includeArchive? })` → `{ summary: { total, chars, byBucket, charsByBucket }, groups: [{ key, kind/level/tag, count, chars, bucket }], candidates: [{ id, kind, title, bucket, score, reasons[], evidence }], notes[] }`。 **逻辑轨迹(侧车,v0.6 起采集)**:`<DSH_HOME>/memory-access-trace.jsonl` —— 一行一次读命中:`{atMs, source:'recall'|'auto', role, ids:[…]}`。写入**只追加**(POSIX append,天然免锁)、**吞错**(失败即跳过,不影响 recall)、**按体积轮转**(超 `access_trace.max_bytes` 改名为 `.1` 重开),**绝不改条目**。审计器只读它,缺文件 ⇒ `usage=0`。 **硬约束(三条,均可断言)**: 1. **只读**:跑完存储文件内容不变、`accessedAt` 不变(判据:跑前后字节级一致) 2. **视野一致**:`memory_audit` 是读路径 ⇒ 过 `applyRoleView`(提案只含调用者视野内的条目) 3. **不做自动处置**:无 `--apply`、无定时归档;删除/归档由 agent 显式发起 ### 5.10 重心化注入契约(v0.7 · 从「消息关键词」到「上下文重心」) **动机(借鉴 VCPToolBox 的「引力」范式——理念而非代码)**:原 auto-recall 以**最后一条主人消息的字面**为查询词——用户说「最近压力好大」,三个月前提过的「考试」不会被召回,因为字面里没有那个词。实测反例(2026-09-15):重启唤醒消息注入的全是与当轮意图无关的历史条目。 **语义**:注入查询 = **上下文重心**,而不是单条消息。 | 概念 | 定义 | |------|------| | 重心(centroid) | 最近 K 轮消息文本的**加权词项集合**:越新的轮次权重越高(逐轮衰减),最新一条额外加成(锚点) | | 衰减 | `turnWeight = decay^(距最新轮次的距离)`,缺省 `decay = 0.7`;最新一轮再乘 `anchorBoost = 2.0` | | 词项 | 复用检索层**同一套**分词(CJK bigram + 停用词过滤)——**判据单一真源**,不得各写一份 | | 权重上限 | 每轮内同一词项只计一次(防单条长消息主导);保留权重最高的 `maxTerms = 24` | | 兜底 | 无历史(首轮)或无有效词项 ⇒ **退化为原行为**(以单条消息为查询)——零回归是硬约束 | **加权打分**:`score = Σ_t w_t × (标签命中 3 / 标题命中 2 / 正文命中 1)`;未给加权词项时走原路径(`query` 字符串)。 **命中率度量(可观测)**:侧车轨迹已按 `source` 记 `recall`(我主动查)与 `auto`(系统注入)两类;`memory_health` 汇总报出**注入次数 / 主动检索次数 / 命中条目数 / 最近注入时刻**——「环境」是否真的建起来了,看这个比值,不看感觉。 ### 5.11 提案日志契约(v0.7 · 让提案有历史) **动机(借鉴 VCPToolBox 的梦境审批:提案落盘、可回看、可拒绝)**:`memory_audit` 的提案原本只活在一次工具返回里,无法回答「我当时提了什么、后来做了什么、对不对」。 **落点**:`<DSH_HOME>/memory-audit-proposals.jsonl`(与用量轨迹同族的侧车;**只追加 / 吞错 / 按体积轮转**,绝不改条目)。 **两类记录(同一文件,可按 id join)**: | kind | 何时写 | 关键字段 | |------|--------|---------| | `audit` | 每次 `memory_audit` 运行 | `atMs` / `role` / `weights` / `summary`(各档条数与字符)/ `candidates`(前 20:id/bucket/score/chars) | | `action` | `forget` / `update` 成功时 | `atMs` / `action` / `id` / `reason?` | **用途**:`audit` ⋈ `action` = 「提案被采纳/否决」的样本 ⇒ 权重校准(§10 U8 第 ④ 项)的前提。**它只记事实,不评判**——校准是后续的事。 **与 A31 的关系(必须说清)**:`memory_audit` 仍是**只读工具**——不写记忆库、不刷新 `accessedAt`;写的是**侧车观测文件**(§5.22 观测层:吞错、不阻塞主流程)。A31 的判据是「存储文件内容 + `accessedAt` 不变」,本契约不与之冲突;新增验收见 A50。 ### 5.12 压缩流水线证据层(v0.8 · 让「为什么这个桶没压」可判) **动机(2026-09-15 实测暴露)**:跨条目巡检发现库级结构异常——`日概要 2026-09-13` / `日概要 2026-09-14` / `周概要 2026-W37` **全库 grep 无**(原料存在、单位已结束,§5.6 明写「含历史缺口回填」),而**根因不可判**:本流水线此前只有 `console.log` / `console.error`,宿主 logger **不落盘**(AGENTS.md §5.22 规则 1)⇒ 五问里的 ③「断在哪一段」答不了。按 §5.22「先补证据层,再修业务逻辑」,v0.8 只补证据,**不改压缩行为**。 **① 判定命名化(`timeline.ts: explainCompressions`,判据单一真源)** | 判定 | 含义(= 原有 `continue` 短路顺序,逐条命名) | |------|--------------------------------------------| | `not-ended` | 单位未结束(`range.end > now`)——条目仍可能追加 | | `no-sources` | 无**非归档**原料(归档条目不参与压缩) | | `already-summarized` | 已有同层概要(幂等,不重复压) | | `pending` | 待压缩 | - `findPendingCompressions` = `explainCompressions(...).filter(pending)` 的**投影**——禁止两处各自维护一套判定(§5.22 判据单一真源)。 - 语义等价性由既有 A10/A11/A12 用例 +A56 同源断言共同保证(重构不改行为)。 **② 侧车轨迹(`<DSH_HOME>/memory-compress-trace.jsonl`)** | phase | 何时写 | 关键字段 | |-------|--------|---------| | `scan` | 每轮 `compressPending` 的**首轮**(后续轮是链式推进的中间态,会掩盖全貌) | `candidates`(候选桶数)/ `pending` / `skipped`(**非待压**判定分布)/ `sample`(逐桶 `<level> <bucket> <decision>`,上限 `MAX_SAMPLE=40`) | | `unit` | 每个压缩单元 | `level` / `bucket` / `reason` / `archived`(归档条数)/ `chars` / `durMs` | | `end` | 每轮收尾 | `units`(本轮 compressed 数)/ `totalMs` | | `error` | 单元抛错(**先落证再原样上抛**,不改控制流) | `level` / `bucket` / `message` / `durMs` | 每条记录由装配层补齐 `atMs` + `trigger`(`lazy` = 访问记忆时补压 / `periodic` = 周期补压)——压缩器**不需要知道谁触发了它**。 **③ 只读读数面**:`memory_health` 增报「压缩流水线」段(扫描轮数 / 压缩单元数 / 最近待压数 / 非待压判定分布 / 非待压候选样本前 8 条)——**五问一条命令可答**(§5.22 规则 2)。 **纪律(对齐 §5.22)**:观测**绝不反噬主流程**——落盘走通用 `appendJsonl`(只追加 / 吞错返回 `false` / 超限轮转 `.1`);`sample` 截断;sink 缺省不给 ⇒ **行为零差异**(A59 断言压缩结果逐字段一致);开关 `audit.compress_trace`(缺省开,1 MB 轮转)按**每个 workspace 自己的** `memory.yml` 生效。 **反定位**:不解释语义(不判断「这条该不该进概要」)|不做压缩质量的自动裁决(那是 §10 U9 的口径问题)|不落盘正文(只落 id/计数/耗时/判定)。 ## 6 · 边界与信任 - **能力 ≠ 沙箱**:本能力不隔离、不鉴权、不加密;能读到存储文件的人都可改记忆。 - **角色维度是视野管理,不是安全边界(v0.5 诚实声明)**:工具参数 `role` 可自述、记忆文件可被能读盘的人改写、共享记忆对所有角色可见——它防的是「不小心看见」与「默认继承上下文」,**不防蓄意越权**。要真正的隔离(幽灵隔间 / 多租户)必须配**独立 `DSH_HOME`**(另见 AGENTS.md §5.26 G1/G8)。 - **信任边界**:信任 Caller(agent/插件)的内容决策;不信任输入形态——配置走 fail-loud 校验,工具参数走 `defineTool` schema 校验,持久化走 zod schema。 - **不越界清单**:不做内容判断 ✅|不做语义去重(只按标题指纹)✅|不做跨设备同步 ✅|不做凭据存储(工具描述明确「不用来记密钥/口令」)✅。 - **失败面(每条都明确「拒绝」或「放行」,无静默二义)**: | 失败场景 | 处置 | |---------|------| | `memory.yml` 语法错 / 未知键 / 非法值 | **拒绝**:抛 `MemoryConfigError`(fail loud) | | `memory.yml` 不存在(ENOENT) | 放行:全默认 | | 溢出 `max_entries` 且要**新建** | 拒绝:报错(同 key 更新 / 合并仍允许) | | 压缩落库失败(sink) | 静默(幂等,下次压缩再试);原文仍留在会话日志 | | 压缩 `end` 带 error / 摘要为空 | 不落库、不通知 | | 归档后通知发送失败 | 吞掉(原文已落库,决策消息可后续补发) | | 周期补压单 scope 失败 | `console.error` 记录 + 继续其它 scope + 下轮重试(**不静默**) | | 懒压缩失败 | `.catch(()=>{})` **静默**(v0.3 前的停摆根因,见 §9) | | 注入无命中 / 无 cwd / 配置关闭 | 不注入、不报错 | | `memoryApi` 失败 | 返回 `{error}`,调用方容错不阻塞 | | `roles` 段非法(未知键 / 空角色名 / 非字符串预设映射 / 非法 kinds) | **拒绝**:抛 `MemoryConfigError`(fail loud,同 I8) | | 条目被角色策略剔除 | 静默不出现在结果里(准入 ≠ 报错);`memory_relate` 对视野外 id 返回 `ok:false`;`update`/`forget` 对视野外 id 报「未找到…(当前视野内)」 | | 会话无 id(无法判定身份) | 放行:按 `roles.default` 处理(保守可见全),理由随 `memory_health.roleReason` 透出 | ## 7 · 可证伪验收 | # | 可证伪命题(一次测量判真假) | 证据出处 | 状态 | |---|---------------------------|---------|------| | A1 | 同 `key` 再写 → `updated`(L1 精确覆盖) | `tests/tools.test.ts`「remember:同 key 再写 → updated(L1 覆盖)」 | ✔ 已实测 | | A2 | 同标题 `knowledge` 再写 → `merged`(标签并集 + 正文追加,不新增条目) | `tests/tools.test.ts`「remember:同标题 knowledge → merged(标签并集)」;`tests/store.test.mjs` describe「去重合并(规格 §2.2)」 | ✔ 已实测 | | A3 | 无命中 → `created`,首行作标题、写入当前 workspace | `tests/tools.test.ts`「remember:新建(created),写入当前 workspace,首行作标题」 | ✔ 已实测 | | A4 | `max_entries` 满:**新建**报错、同 key **更新**放行 | `tests/tools.test.ts`「remember:max_entries 满且新建 → 报错;同 key 更新仍允许」 | ✔ 已实测 | | A5 | 作用域路由 6 条分支(显式覆盖 / 三配置模式 / 无 cwd 降级 / 显式优先) | `tests/scope.test.ts`:`默认 workspace 模式…`、`global-first 模式…`、`global 模式…`、`显式 scope=global…`、`显式 scope=workspaceId…`、`显式覆盖优先于 global-first 配置`、`无 cwd(两种模式)→ 降级只走 global`(12 用例) | ✔ 已实测 | | A6 | 启动注入:global fact 全量 + 概要先于明细 + 预算截断 + 归档条目不出现 + 空记忆不注入 | `tests/inject.test.mjs` 5 用例(`digest:global fact 全量 + 概要先 + 近期明细` 等) | ✔ 已实测 | | A7 | auto-recall 触发面:GUI 触发、Telegram 触发、**其他 plugin 注入不触发**、空文本跳过、多 text block 拼接、取最新一条 | `tests/auto-inject.test.mjs` 7 用例(`lastUserMessageText:其他 plugin 注入不触发` 等) | ✔ 已实测 | | A8 | auto-recall 预算:`maxEntries` 截断、`maxBytes` 截断并提示、空 query/无命中返回空串、长 query 截断到 200 | `tests/auto-inject.test.mjs`「digest:maxEntries 截断」「digest:maxBytes 截断并提示」等 | ✔ 已实测 | | A9 | auto-recall 在**真实会话**里生效(帧出现在模型请求中) | 本会话 system-reminder 实证:`【相关记忆(auto-recall)】` 帧(bot 抓到的 3 条记忆);同类:启动注入帧 `从记忆库加载的相关记忆(dsh-agent-memory)` | ✔ 已实测 | | A10 | 时间桶算法(本地时区)+ 金字塔原料归属 + 上级桶时间范围 | `tests/timeline.test.mjs`:describe「时间桶算法(本地时区)」「previousBucketKey 上一自然单位」「bucketRange 时间范围」「bucketBelongsTo 上下级归属」 | ✔ 已实测 | | A11 | 压缩幂等(纯函数层):目标桶已有同层级概要 → 不计入待压缩 | `tests/timeline.test.mjs` describe「findPendingCompressions 懒压缩触发」 | ✔ 已实测 | | A12 | 只压已结束单位 + 有原料才压 + `fact`/`knowledge` 永不参与 | `tests/timeline.test.mjs`(同上 describe);源码 `timeline.ts: isSourceFor`(kind/level 双重约束);生产数据反查:`fact`/`knowledge` 条目 `level` 恒为 `null` | ✔ 已实测 | | A13 | 压缩失败(`end` 带 error)/ 无摘要 → 不落库不通知;成功 → 保底落库 + 通知(`wakeup=true`、`target='next-turn'`、消息含条目 id) | `tests/compaction-sink.test.ts` 4 用例(断言 `sent[0].wakeup === true`) | ✔ 已实测 | | A14 | 配置 fail-loud:未知顶层键 / 非法枚举 / 非布尔 / 非正整数 / `archive≠keep` / 冻结保护 等 14 类 → `MemoryConfigError` | `tests/config.test.ts`(`非法:未知顶层键 → MemoryConfigError` 等 14 用例;`非法:冻结保护(默认配置不可改)`) | ✔ 已实测 | | A15 | 联想层:单跳关联强度降序(共享标签×3 + 标题 2-gram×2 + 正文 2-gram×1)/ BFS 闭包 hop 标注 + 防环 + 每跳 limit | `tests/search.test.mjs` describe「联想层(related 关联链)」「relateClosure · 多跳联想闭包(BFS 记忆图)」 | ✔ 已实测 | | A16 | **端到端幂等**:进程内任意并发组合下,同一 (scope, level, bucket) 只存在一条概要 | 修复**前**反例(保留为尸体样本):生产库 4 对重复桶 `day 2026-08-24 / 08-26 / 08-30 / 09-10`;修复**后**证据:`tests/timeline.test.mjs` describe「并发幂等(2026-09-13 修复:重复概要桶)」2 用例(两实例并发 → `summarize` 只调 1 次 + 只 1 份概要 + 原料只归档一次;写前复核命中他方概要 → 让位且原料保持未归档) | ✔ 已实测(存量 4 对重复桶仍在库,属历史数据,见 U1 遗留) | | A17 | 周期补压:启动延迟首跑 + 周期轮询 + 单 scope 失败不静默 + dispose 清定时器 | 仅有代码路径(`src/periodic.ts` + `index.ts:200-226` 装配),**无单测文件**;生产日志未捕获 `周期补压` 输出(宿主流未落盘) | **待线上验收** | | A18 | 互斥原语语义:同键排队串行(任意时刻 ≤1 在临界区)、异键并行不退化、前序异常不毒化链、settle 后键不泄漏 | `tests/lock.test.mjs` 6 用例(含 50 并发样本) | ✔ 已实测 | | A19 | 人类会话判据:`session-<uuid>` 为真;裸 uuid / 空串 / 畸形为假 | `tests/role.test.mjs`「A19 isUserSessionId…」 | ✔ 已实测 | | A20 | 角色推导四级优先:显式 `role` > `by_preset[agentPreset]` > 人类会话缺省 > 派生会话缺省;**无 id → 人类缺省**(保守);`roles` 段缺失 → 走缺省且总开关关闭 | `tests/role.test.mjs`「A20 角色推导…」「A20 roles 段缺失…」 | ✔ 已实测 | | A21 | 准入四判据 R1–R4:种类收窄 / 共享记忆可见性可关 / 自己放行 / 白名单(含 `'*'`)放行,其余拒绝 | `tests/role.test.mjs`「A21 准入判据 R1–R4」 | ✔ 已实测 | | A22 | **未启用 = 零过滤**:`applyRoleView` 返回**同一数组引用**(零拷贝,行为与 v0.4 一致) | `tests/role.test.mjs`「A22 未启用角色维度 → 同一数组引用透传」 | ✔ 已实测 | | A23 | `include_global: false` → 读作用域去掉 global;**显式 scope 参数优先于该配置** | `tests/role.test.mjs`「A23 include_global=false…」 | ✔ 已实测 | | A24 | 归属不可经写入转移:同 key 覆盖 / 同标题合并都**保留原条目 `role`** | `tests/role.test.mjs`「A24 归属只在新建时盖章…」 | ✔ 已实测 | | A25 | 工具层准入一致:`recall` / `memory_browse` / `memory_stats` / `memory_relate` / `update` / `forget` 同一视野——verifier 策略下看不见他人隔间、过程流与 global;视野外条目不可改;显式 `role` 可切视角 | `tests/role.test.mjs` 4 用例(「A25 recall:verifier…」「A25 memory_browse / memory_stats / memory_relate…」「A25 update / forget…」「A25 显式 role 参数…」) | ✔ 已实测 | | A26 | 兼容性:**无 `roles` 段的 v0.4 形状配置**不崩、零过滤(历史字面量活样本)、`memory_health` 如实报 `rolesEnabled=false` | `tests/role.test.mjs`「A26 …(v0.4 行为回归)」「A26 历史配置字面量…」;`tests/tools.test.ts` 的 `BASE_CONFIG` 即无 roles 段的活样本,全套仍绿 | ✔ 已实测 | | A27 | 写路径盖章:启用 roles → 盖调用者角色 + 记 `author`;未启用且未显式指定 → **不盖章(共享记忆)** | `tests/role.test.mjs`「A27 工具层写路径盖章…」 | ✔ 已实测 | | A28 | `roles` 段的配置解析:缺省/完整/非法(未知键、空角色名、非字符串预设映射)fail loud | `tests/config.test.ts`「roles:缺省段…」「roles:完整段…」+ 4 条非法用例 | ✔ 已实测 | | A29 | 生产会话按角色取数:主会话(`session-<uuid>`)→ `main`;派生会话(子代理 / 队员)→ 派生角色;启用 roles 后注入面与工具面同一视野 | ✔ **线上实测**(2026-09-15 17:4x,插件 v0.5.0 + `E:\alice\.dsh\memory.yml` 已启用):主会话 `memory_health` → `角色 main(角色维度已启用;判据:人类会话(session-<uuid>))`;**真实子代理会话**(subagent `7756cf92`,`delegationDepth=1`)→ `角色 worker(角色维度已启用;判据:派生会话(delegationDepth=1))`;两次 `memory_version` 均报 `0.5.0(build 2026-09-15T09:43:04)` | ✔ 已实测 | | A30 | 其他插件经 `ctx.memoryApi.remember` 写入仍为共享记忆(不被静默划入某隔间) | 代码路径:`index.ts` 的 `memoryApi` 不传 `role`(`author` 亦不伪造);待线上复核(下次插件回流时核对 `role` 字段缺省) | **待线上复核** | | A31 | **只读**:`memory_audit` 跑完库内容逐字不变、`accessedAt` 不被刷新;体检自身不写用量轨迹 | `tests/audit.test.mjs`「A31 只读:…(零写入)」「A31b memory_audit 不写轨迹…」 | ✔ 已实测 | | A32 | **承重必 KEEP(反例)**:被 `archiveRef` 引用的原料即使又老又无溯源也判 KEEP——不得建议归档掉自己赖以回溯的原料 | `tests/audit.test.mjs`「A32 承重原料必为 KEEP…(反例)」 | ✔ 已实测 | | A33 | recency 单调:其余相同,越新分数越高(exp(−age/30)) | `tests/audit.test.mjs`「A33 recency 单调…」 | ✔ 已实测 | | A34 | 体量惩罚单调:其余相同,越大分数越低 | `tests/audit.test.mjs`「A34 体量惩罚单调…」 | ✔ 已实测 | | A35 | 近重复簇:同标题(归一化)条目同簇、canonical 取最新、成员全部进 REVIEW | `tests/audit.test.mjs`「A35 近重复簇…」 | ✔ 已实测 | | A36 | 分档顺序可复现:REVIEW(体量/重复)→ KEEP(承重)→ KEEP(新)→ ARCHIVE → DEMOTE → KEEP(小兜底);候选排序按「需要动作」档位聚类 | `tests/audit.test.mjs`「A36 分档顺序可复现…」 | ✔ 已实测 | | A37 | 对账:各档条数与字符合计 = 输入总量(分组视图同样对账,不漏不多) | `tests/audit.test.mjs`「A37 对账…」 | ✔ 已实测 | | A38 | 视野一致:体检是读路径 ⇒ 过 `applyRoleView`(verifier 策略下提案只含共享条目) | `tests/audit.test.mjs`「A38 视野一致…」 | ✔ 已实测 | | A39 | 侧车轨迹:只追加一行/次、坏行跳过不抛、超限轮转为 `.1`、写失败返回 `false`(吞错)、空 ids 不写 | `tests/audit.test.mjs`「A39 侧车轨迹…」 | ✔ 已实测 | | A40 | 用量项:有轨迹时命中多的分更高;无轨迹 ⇒ `usage=0` 且 `usageSource='none'`(公式不失真,只少一项证据) | `tests/audit.test.mjs`「A40 用量项…」 | ✔ 已实测 | | A41 | `audit` 配置:缺省走先验、覆盖生效、非法 fail-loud(未知键 / 负权重 / 字符串天数 / 小数 max_bytes) | `tests/audit.test.mjs`「A41 audit 配置…」「A41b 无 audit 段的历史配置…」 | ✔ 已实测 | | A42 | 工具面 11 个(新增 `memory_audit`),描述含「只读」承诺 | `tests/audit.test.mjs`「A42 工具面…」;`tests/tools.test.ts`「工具齐备,名称与契约一致」 | ✔ 已实测 | | A43 | **线上**:`recall` / auto-recall 在真实 `DSH_HOME` 落 `<DSH_HOME>/memory-access-trace.jsonl`,体检读到该轨迹(`usageSource='trace'`) | ✔ **线上实测**(2026-09-15 18:2x,v0.6.0 重启后):自动注入触发即落盘 `{"atMs":1789467668040,"source":"auto","role":"main","ids":[3 个 id]}`(178 字节);`memory_audit` 报「用量信号:侧车轨迹」 | ✔ 已实测 | | A44 | **索引范围**:引用/标签复用/重复簇按「视野内全量(含归档)」统计,分类仍按本次集合;集合外的承重原料必须如实报出(「看不见」≠「没有」) | `tests/audit.test.mjs`「A44 索引范围…」;线上实证:默认视图报 `承重 0 条` **并**给注脚「另有 177 条承重原料不在本次集合内」,`includeArchive: true` 后报 `承重 177 条` | ✔ 已实测 | | A45 | 重心纯函数:越新的历史轮权重越高(decay^距离)、锚点最强(×2.0)、同轮同词项只计一次、`maxTerms` 封顶、无有效词项 ⇒ 空数组 | `tests/centroid.test.mjs`「A45 重心…」「A45b 重心:封顶…」 | ✔ 已实测 | | A46 | **重心召回 > 字面召回**:历史提过「考试」、最新消息说「最近有点累」⇒ 字面路径召不回考试条目,重心路径能 | `tests/centroid.test.mjs`「A46 重心能召回「字面里没提」的历史话题…」 | ✔ 已实测 | | A47 | **零回归**:不给 `weightedTerms`(或给空数组)时 `buildAutoRecallDigest` 与原路径逐字节一致;纯重心(无锚点文本)不崩 | `tests/centroid.test.mjs`「A47 未给重心 ⇒ 完全走原路径…」;`tests/auto-inject.test.mjs` 原 11 条仍绿 | ✔ 已实测 | | A48 | 重心素材挑选:排除工具结果与插件注入,保留主人消息与模型回答,取最后 N 条 | `tests/centroid.test.mjs`「A48 重心素材…」 | ✔ 已实测 | | A49 | 轨迹汇总口径:`auto`/`recall` 计数、去重命中数、命中总数、最近时刻;坏行跳过 | `tests/centroid.test.mjs`「A49 轨迹汇总…」 | ✔ 已实测 | | A50 | 提案日志:`audit` 与 `action` 两类记录**同文件可按 id join**;写入失败吞错、不影响工具返回;开关关闭即不写;**`memory_audit` 仍不写记忆库、不刷 `accessedAt`**(A31 不破) | `tests/centroid.test.mjs`「A50 提案日志…」「A50b 提案日志:写入失败吞错…」 | ✔ 已实测 | | A51 | `memory_health` 报命中率信号(注入次数 / 主动检索次数 / 去重命中数 / 最近时刻);无信号时全 0 不崩 | `tests/centroid.test.mjs`「A51 memory_health 报命中率信号…」 | ✔ 已实测 | | A52 | `audit.proposal_log` 配置:缺省开(1 MB 轮转)、可关、未知键/负值 fail-loud | `tests/centroid.test.mjs`「A52 audit.proposal_log 配置…」 | ✔ 已实测 | | A53 | **线上**:重启后下一条真实主人消息的注入块包含「与对话历史话题相关」的条目(对照上一轮纯字面路径的注入),且 `memory_health` 的注入计数增长 | ✔ **线上实测**(2026-09-15 19:0x · v0.7.0 重启后):`memory_health` →「命中率信号:注入 6 次 / 主动检索 0 次 / 命中条目 12 条(最近 2026-09-15T11:02:04)」;轨迹 7 条 `source:'auto'`;注入相关度由字面路径 16/13 升到 **32/26**(重心权重生效) | ✔ 已实测 | | A54 | **线上**:`<DSH_HOME>/memory-audit-proposals.jsonl` 出现 `audit` 记录(跑体检后);随后 `forget/update` 应出现可 join 的 `action` 记录 | ✔ **线上实测**(19:02):646 B / 1 行,`{"kind":"audit","role":"main","weights":{…},"summary":{"total":498,"chars":687974,…},"candidates":[…]}`——形状与 §5.11 一致 | ✔ 已实测 | | A55 | 逐桶判定四档命名,且优先级 = 短路顺序(`not-ended` → `no-sources` → `already-summarized` → `pending`) | `tests/compress-pipeline.test.mjs`「四种判定各有其桶,且优先级 = 短路顺序」 | ✔ 已实测 | | A56 | **判据单一真源**:`findPendingCompressions` ≡ `explainCompressions(...)` 的 pending 投影(含顺序) | `tests/compress-pipeline.test.mjs`「判据单一真源…」 | ✔ 已实测 | | A57 | 首轮 `scan` 落「候选数 / 待压数 / **非待压**判定分布 / 逐桶样本」,`skipped` 不含 `pending`,样本受 `MAX_SAMPLE` 约束 | `tests/compress-pipeline.test.mjs`「首轮 scan 落…」 | ✔ 已实测 | | A58 | 每个压缩单元落 `unit`(reason / 归档数 / 正文字符数 / 耗时);收尾落 `end`(units / totalMs);事件序列 = `1 + N + 1` | `tests/compress-pipeline.test.mjs`(同上用例的事件序列断言) | ✔ 已实测 | | A59 | **零回归**:不给 sink ⇒ 压缩结果逐字段与给 sink 时一致、库规模一致(观测不改行为) | `tests/compress-pipeline.test.mjs`「零回归硬约束…」 | ✔ 已实测 | | A60 | 单元抛错 ⇒ 先落 `error` 轨迹(level / bucket / message / durMs)**再原样上抛**(只加观测,不改控制流;§5.24:兜底必须留证) | `tests/compress-pipeline.test.mjs`「总结抛错 ⇒ 落 error 轨迹后原样上抛」 | ✔ 已实测 | | A61 | 侧车落盘纪律:只追加、坏行/异形行跳过不抛、超 `maxBytes` 轮转 `.1`、样本截断、**不可写路径 ⇒ 返回 `false` 且不抛**(尸体样本) | `tests/compress-trace.test.mjs` 4 用例(含尸体样本与轮转) | ✔ 已实测 | | A62 | `audit.compress_trace` 配置:缺省开(1 MB)、可关、`max_bytes=0` = 不轮转、非法(未知键 / 非布尔 / 负值 / 非映射)fail-loud | `tests/compress-trace.test.mjs`「audit.compress_trace 配置(A62)」3 用例 | ✔ 已实测 | | A63 | **线上**:重启后 `<DSH_HOME>/memory-compress-trace.jsonl` 出现 `scan` / `unit` / `end` 记录,且 `memory_health` 报出压缩流水线读数(五问一条命令可答) | ✔ **线上实测**(19:41–19:57):轨迹 4 行→19 行;`scan` 41 候选/3 待压 + 判定分布 + 逐桶样本;`error` 带 provider 原文;`unit`/`end` 齐全;`memory_health` →「压缩流水线:扫描 7 次 / 压缩 3 单元 / 非待压判定 no-sources 33, not-ended 4, already-summarized 1」 | ✔ 已实测 | | A64 | 总结调用带会话 id:有 `agent` ⇒ 用 `agent.session.id`;无 agent ⇒ 显式 `sessionId` 参数(周期补压路径);**两者皆无 ⇒ options 不含该键**(零回归) | `tests/summarizer.test.mjs`「sessionId 透传」4 用例 | ✔ 已实测 | | A65 | 提示词含正文长度上限(缺省 6000 字,可经第二参数覆盖)并显式劝阻逐字搬运 | `tests/summarizer.test.mjs`「长度上限(v0.8.1)…」「长度上限可覆盖…」 | ✔ 已实测 | | A66 | 截断错误附**可归因证据**(已产出字符数 + usage),区分「模型写长文」与「reasoning 烧预算」 | `tests/summarizer.test.mjs`「截断错误附可归因证据(v0.8.2)…」 | ✔ 已实测 | | A67 | **线上(症状消失级)**:三个 pending 桶全部压成概要、原料冷归档,缺口消失 | ✔ **线上实测**(19:54:21 scan → 19:55:26 `day 2026-09-13` compressed(归档 6 / 12,233 字 / 64.8s)→ 19:56:05 `day 2026-09-14`(归档 15 / 12,919 字 / 38.5s)→ 19:57:35 `week 2026-W37`(归档 6 / 17,436 字 / 90.4s)→ `end`);库内 681→684 条(+3 概要)、归档 180→207(+27 原料) | ✔ 已实测 | | A68 | `applyTextMode` 三模式:`replace`(沿用 v0.8 语义:title=首行、body=**全文**)/ `append`(追加到正文末尾,标题不动)/ `patch`(正文命中则替换,正文不中退到标题) | `tests/prune-merge.test.mjs`「A68 applyTextMode…」 | ✔ 已实测 | | A69 | **patch 唯一性**:多命中(正文 2 处)/ 未命中(正文与标题各 0 处)/ 空 `find` / 空 `append` 文本一律 fail loud 且报出实际处数;`countOccurrences` 非重叠计数 | `tests/prune-merge.test.mjs`「A69 patch 唯一性判据…」 | ✔ 已实测 | | A70 | `selectForgetTargets`:去重 / 已归档剔除 / 不存在计入 `skipped` / 超上限按 `truncated` 如实回报(不静默吞) | `tests/prune-merge.test.mjs`「A70 selectForgetTargets…」 | ✔ 已实测 | | A71 | `pushRevision`:保留最近 `MAX_REVISIONS`(3) 条、顺序旧→新、mode 与 `by` 落盘(超出丢最旧) | `tests/prune-merge.test.mjs`「A71 pushRevision…」 | ✔ 已实测 | | A72 | **零回归**:`forget` 单条模式的字段(`id` 回填 / `archived=true` / `archivedCount=1`)与 render 文案「已归档记忆条目 <id>」逐字不变;`reason` 仍入 `source` | `tests/prune-merge.test.mjs`「A72 forget 单条模式零回归…」 | ✔ 已实测 | | A73 | **批量 + dryRun**:`dryRun=true` 库内活跃数不变(不写库);去掉后真归档并自动记 `forget: bulk(N 条)`;超上限回报 `truncated` | `tests/prune-merge.test.mjs`「A73 forget 批量…」 | ✔ 已实测 | | A74 | **分档批量**:`tier=ARCHIVE` 走 `memory_audit` 引擎选中候选并回报命中数;**`tier=KEEP` 被拒**(承重档护栏,尸体样本);未知 tier fail loud | `tests/prune-merge.test.mjs`「A74 forget tier…」 | ✔ 已实测 | | A75 | 单条未命中的旧语义保持(`未找到 id=...` 抛错)且**无选择器时报错**(不许空调用静默返回) | `tests/prune-merge.test.mjs`「A75 forget 单条未命中…」 | ✔ 已实测 | | A76 | `memory_merge`:canonical 正文追加「合并自 <id>(<title>)」+ 被并入者正文**不丢**、随后软归档(`reason=merged into <canonical>`)、canonical 落修订快照;跳过分支(canonical 自身 / 列表内重复 / 不存在 / 已归档)逐条回报;canonical 不存在 ⇒ 抛错 | `tests/prune-merge.test.mjs`「A76 memory_merge…」 | ✔ 已实测 | | A77 | `update` `append`/`patch` 经工具层真写库并落 `revisions`(prevBody 逐字);**失败改写不留痕**;未知 mode fail loud | `tests/prune-merge.test.mjs`「A77 update append/patch…」 | ✔ 已实测 | > 测量口径:`pending = total − proven`(fail-closed)。本表 `total=77, proven=75, pending=2`(A17 周期补压 / A30 memoryApi 写入待线上复核)。 > **A64–A67 的事故背景(U10 定案)**:证据层首跑即指出三桶判定正确、失败在总结调用——真因两段:① `GenerateOptions` 未带 `sessionId` ⇒ 宿主插件 `dsh-x-opencode-session` 不加 `x-opencode-session` 头 ⇒ 网关拒单(`Request is missing x-opencode-session…`);② 路由通了以后输出触 16,000 token 上限被 fail-closed——实测单份概要 12,233 / 12,919 / 17,436 字符(≈18–26k token),**提示词的 6000 字预算没绑住(超写 2×)⇒ 硬约束只能来自 token 上限**(`maxTokens` 16000 → 32000)。 > **A53 的诚实旁注**:重心把「话题相关性」做上去了(相关度翻倍、命中与本次会话主题一致),但**唤醒消息本身该不该注入**仍存疑——`[守护] web 已重启` 触发注入时给到的仍是运维类条目。两条可查方向:① 唤醒类消息是否应触发注入(它是系统事件、不是对话);② 需要「命中质量」而非「命中数量」的度量(现指标只数条数与次数)。**均未决**,见 §10 U9。 > A29 旁注(诚实):`by_preset` 预设映射分支本次**未在线上观测到**(该子代理会话头未带 `agentPreset`,走的是派生缺省)——该分支由 A20 单测覆盖。 > **A25/A23 的线上量化对账(2026-09-15 17:5x · 真实生产库 676 条)**:主脑视角 `recall` → `命中 496`;`role='ghost'`(`read: []`, `include_global: false`)→ `命中 384`。差额 **112 = 111(global 作用域被 `include_global: false` 收窄)+ 1(唯一带 `role='main'` 的条目被 `read: []` 拒绝)**,逐项对得上。库内实测:盖章 `role` 共 1 条(`974a5e6b`,`author = {sessionId: 'session-a5375716…', delegationDepth: 0, preset: 'alice-v2'}`),其余 **675 条为共享**(迁移安全的实证)。 ## 8 · 与实现的关系 | 模块 | 职责 | |------|------| | `src/config.ts` | `memory.yml` 解析 + 默认值 + fail-loud 校验(纯函数) | | `src/types.ts` | 条目/配置/查询/结果类型(无运行时) | | `src/scope.ts` | workspaceId 规范化 + 读写 scope 裁决(纯函数) | | `src/store.ts` | 条目 CRUD + 去重合并(L1 key / 标题指纹 FNV-1a)+ `stats`/`scopes` | | `src/search.ts` | 过滤 → 打分(标签3/标题2/正文1)→ 排序 → 截断;`relatedOf` / `relateClosure` / `browseEntries` / `bucketLabel` | | `src/inject.ts` | 启动注入(速览组装 + `pre-step` 挂载) | | `src/auto-inject.ts` | auto-recall(触发面判定 + 摘要组装 + 挂载) | | `src/summarizer.ts` | 总结提示词 + LLM 直调(`DEFAULT_MAX_TOKENS = 16000`) | | `src/timeline.ts` | 桶算法 + **`explainCompressions`(逐桶判定,v0.8 判据单一真源)** + `findPendingCompressions`(其 pending 投影)+ `TimelineCompressor`(压缩执行/幂等/冷归档 + **轨迹发射**)+ `compressUnitKey`(互斥键唯一真源) | | `src/lock.ts` | 按键串行锁 `withKeyLock`(进程内、跨实例共享;排队语义 + 异常不毒化链) | | `src/periodic.ts` | 周期补压定时器(v0.8 起接受 `traceFactory`,按各 workspace 配置决定是否落轨迹) | | `src/compaction-sink.ts` | 压缩即记忆(`session/event` → 保底落库 + 通知) | | `src/tools.ts` | 11 个工具定义(含 `memory_audit`;`memory_health` 三段读数:角色 / 命中率 / **压缩流水线**)+ 懒压缩钩子接线 | | `src/index.ts` | 装配:开域 / `memoryApi` / 注册 11 工具 / 三个 install / 周期补压装配 / **`makeCompressTrace`(v0.8 侧车 sink 工厂)** | | `src/role.ts` | **v0.5 角色维度**(纯函数):会话身份判据 / 角色推导 / 策略解析 / 四条准入判据 / 视野组装 / 读作用域收窄 | | `src/audit.ts` | **v0.6 价值体检器**(纯函数):引用索引(承重)/ 标签复用 / 近重复簇 / 评分(七项)/ 四档裁决(REVIEW→KEEP→ARCHIVE→DEMOTE)/ 分组聚合;只读,不改条目 | | `src/access-trace.ts` | **v0.6 侧车用量轨迹**:追加(JSONL)/ 坏行跳过 / 按体积轮转 / 吞错;`parseAccessTrace` 纯函数离线可测。**v0.7 增**:`appendJsonl`(通用侧车写入,单一实现)、`summarizeAccessRecords`(命中率度量口径)、`appendProposalRecord`(提案日志) | | `src/centroid.ts` | **v0.7 上下文重心**(纯函数):多轮衰减 + 锚点加成 + 词项去重封顶;`recentTurnTexts` 从消息批次挑素材(排除工具结果与插件注入)。复用 `search.ts: tokenizeQuery`(分词单一真源) | | `src/compress-trace.ts` | **v0.8 压缩流水线证据层**:轨迹记录形状(`scan`/`unit`/`end`/`error`)/ `appendCompressTrace`(复用 `appendJsonl`:只追加 + 吞错 + 轮转)/ `summarizeCompressTrace`(**最近一次扫描**口径,坏行跳过)/ `formatSkipped`(纯函数,离线可测) | **未实现 / 未验证部分(显式标注)**: - `memory_check` **恒返回空建议**——通道 B(信号沉淀提示)未接线;工具描述已如实声明。 - `timeline.archive` **只支持 `keep`**(其它值 fail loud)。 - **年层概要尚无产出**(生产库 `level='year'` 计数 0)——按设计只压**已结束**单位,2026 年未结束属正常;但缺乏「年层可产出」的正向验证(见 U5)。 - **周期补压无自动化测试**(A17)。 - **端到端幂等已修**(A16):进程内并发不再产生重复桶;库中 4 对**历史**重复桶为存量数据,去重属「删数据」类决策(AGENTS.md §2.2 须请示),待主人裁决。 - 本文档**不是** `DESIGN.md` / `IMPLEMENTATION.md` 的同义副本;那两份是设计文档,可能与实现漂移,语义以本文件为准。 ## 9 · 实践修订记录 > I3 载体:只记**被实践修正/补充**的地方(每条附取证来源)。 1. **2026-08-16 · `wakeup` 语义被修正(但文本未同步)** - 语义**被修正**:v0.3 初版(`645f898`)为 `wakeup=false`——「压缩完成只排队,等主人下一条消息时才送达」;同日 `e13fefc`(时间金字塔日层)改为 **`wakeup=true` 完成即送达**(主人定调:「不等主人下一条消息」)。 - **仍未同步的文本**:`README.md` §压缩即记忆、`tests/compaction-sink.test.ts` 的文件头注释与**测试名**(`「…inbox 通知(wakeup=false 排队不唤醒)」`)写的都是旧语义,而该测试**断言的是 `true`**。→ 测试名与断言自相矛盾,属文本落后于行为(本次约束为「只写本文件」,未修,见 U4)。 - 教训:**语义变更必须同时改「行为、测试名、README」三处**——只改行为会让下一位读者(包括压缩后的我)按旧语义理解。 2. **2026-08-21 · 懒压缩停摆 → 周期补压(预防性修复)** - 语义**被补充**:v0.2 只有「访问记忆才触发」的懒压缩,且错误被 `.catch(()=>{})` 吞掉 → **2026-08-16 起日概要零产出**(机制静默失效)。主人指示不再深挖旧因,直接建可靠新机制 → `periodic.ts`(启动延迟首跑 + 周期轮询 + 单 scope 失败 `console.error` 不静默)。 - 教训:**唯一的触发路径 + 静默失败 = 停摆无人知**;触发必须至少双路(懒 + 周期),且失败必须留痕。 3. **2026-08-30 · 注入改「尾部追加」** - 语义**被修正**:动态注入(启动速览 + auto-recall)统一追加到消息批次**末尾**(原为插入),以保护前缀缓存、提升缓存命中率(主人指令「让注入走尾部追加」)。代码注释已固化该理由。 4. **2026-09-01 · `memory_version` 说谎被修正** - 语义**被修正**:原实现 `version` 硬编码、`buildAt` 实为「调用时刻」——v0.2.3 部署后仍报 0.2.2,**验证判据本身不可信**。改为动态读 `package.json` + 产物 mtime。 - 教训:**HMR/部署验证的判据若由被验证方自报,必须自证来源**(否则「验证通过」是自欺)。 5. **2026-09-01 · auto-recall 上线(L3 注入)** - 语义**被补充**:主人「怎么提高记忆库使用率」定调 → 每条真实主人消息注入 top-N;同时按主人反馈**收窄触发面**「不要什么消息都返回记忆」(仅 GUI/Telegram 主人消息)。 6. **2026-09-13 · 本次语义收口取证(三条发现)** - **发现 1(记账缺口)**:`npm test` 的 glob 为 `tests/*.test.mjs` → 只跑 **117** 项;5 个 `*.test.ts`(**76** 项,`node ≥24` 下手工全绿)**不在套件内**;README 写「165+ tests」与实际均不符(见 U2)。 - **发现 2(端到端幂等反例)**:生产库 4 对重复概要桶(见 A16)——纯函数幂等在单测成立,但**检查与写入之间无原子性**。 - **发现 3(配置与默认值一致)**:宿主 patch 实配 `maxTokens: 16000`,与 `DEFAULT_MAX_TOKENS` 同值。 7. **2026-09-13 · 端到端幂等修复(按桶串行锁 + 写前复核)** - 语义**被补充**:I6 的「同桶同层幂等」原先只在纯函数层成立——两条触发路径各建实例,检查与写入之间横着一次 LLM 往返,并发可穿过检查(实测 4 对重复桶)。修复 = `src/lock.ts` 按键串行(键唯一真源 `compressUnitKey`)+ 写前复核;判据见 §5.6 / A16 / A18。 - **实现踩坑(自证,值得留档)**:首版把复核块插在 `store.remember` **之后** → 命中自己刚写的概要 → 提前 return → **原料永不归档**。跑测试时 7 项红拦下——注意:若只做「两实例并发只写一份概要」的浅断言,此 bug 会**静默通过**并发测试。教训:① **幂等复核必须紧贴写入之前,位置本身就是语义**;② 并发测试必须同时断言**副作用完整**(原料归档、`archiveRef`),只数产物份数会放过「该做的没做」。 - 顺带收口:第 1 条的文本漂移(`README.md` / `tests/compaction-sink.test.ts` 头注与测试名 / `DESIGN.md` 两处的 `wakeup=false` → `true`)与第 6 条的记账缺口(新增 `test:ts` / `test:all`,README 数字回填实测)。 8. **2026-09-15 · v0.5 角色维度(多智能体工作台模式)** - 语义**被补充**:主人指令「借鉴 MAGE(arXiv:2608.29678)思路升级记忆插件以适应未来的多智能体工作台模式」⇒ 新增**归属**(写时盖章 `role` + `author`)与**准入**(读前按策略剔除)两条正交语义;判据 R1–R4、配置块 `roles`、`memory_health` 增 `role/rolesEnabled/roleReason`。 - **设计取舍(写下来免得将来重推)**:① **向后兼容优先**——`enabled` 缺省 false 且未启用时 `applyRoleView` 返回同一引用,674 条存量与全部历史测试行为不变;② **共享是缺省**(不盖章),宁可弱隔离也不让存量条目在启用瞬间消失;③ **不做安全边界**——诚实声明它防的是「不小心看见」与「默认继承上下文」,真正隔离要独立 `DSH_HOME`;④ **验收不自己验自己**(AGENTS.md §5.26 G8)用 `verifier` 策略在**检索层**实现:收窄 `kinds` + `include_global: false` + `read: []`(只看共享与自己的)。 - **实现踩坑(自证,值得留档)**:`tools.ts` 里 8 处工具 execute 都以 `const { config, cwd } = await resolveRuntime(...)` 开头——批量注入「视野」时漏改 3 处(update / forget / relate)只改了使用点没改解构点,**tsc 立刻以 TS18004(`No value exists in scope for the shorthand property 'view'`)拦下**。教训:**注入一个新上下文变量时,「解构点」与「使用点」必须同批改**——编译器能抓「用了没声明」,抓不到「声明了没用」。 9. **2026-09-15 · v0.6 价值体检器(只读提案器 + 侧车用量轨迹)** - 语义**被补充**:主人「价值体检器你打算怎么设计」→「可以」⇒ 新增 §5.9 契约与 `memory_audit`(第 11 个工具)。 - **先量后设计(关键教训)**:设计前先量了生产库,才发现 **MAGE 的四项输入我一项都没有**——尤其「读路径不刷新 `accessedAt`」(676 条中仅 180 条 `accessedAt≠updatedAt`,而 180 **恰好等于归档数** ⇒ 差异全来自归档动作,不是「被读过」)。⇒ 判据改成「**有就用,没有就承认**」:只用 refs/age/chars/tags/dup 五项真信号 + 补一层侧车轨迹换成 `u(x)`,其余三项(增益/矛盾/置信)**明确不假装有**。 - **设计取舍(写下来免得将来重推)**:① **提案 ≠ 裁决**——工具只读,不归档不删除;② **承重不可推翻**(A32 反例:被 `archiveRef` 引用的原料即使又老又无溯源也判 KEEP——否则就是建议我归档掉自己赖以回溯的素材);③ **权重是启发式先验不是拟合值**,校准(记录提案被采纳/否决)留到 v3;④ 用量轨迹**只追加 + 吞错 + 轮转 + 绝不改条目**(读路径不得产生记忆库写副作用)。 - **顺带暴露的事实**:本库 31 条 day 概要占 193,348 字符(均 6,237 字/天)——「概要比原料还厚」的嫌疑已由体检器 `REVIEW` 档自动标出,不需人肉发现。 10. **2026-09-15 · v0.7 重心化注入 + 命中率度量 + 提案日志(借鉴 VCPToolBox 的「引力」理念)** - 语义**被补充**:读完 VCPToolBox(`docs/reviews/VCPToolBox-评读报告.md`)后,取其**理念**不取其代码(其许可为 CC BY-NC-SA 4.0 非商用,不可抄)。三条落地:① **注入查询 = 上下文重心**(§5.10)② **命中率可观测**(`memory_health` 报注入/主动检索次数)③ **提案有历史**(§5.11 提案日志)。 - **为什么只借理念**:它的实现依赖 Rust 向量内核 + SQLite + 多租户隔离(为「服务很多人」付的代价),我只有一份记忆、一个主人——**抄复杂度 ≠ 抄理念**。 - **关键设计取舍**:① **零回归是硬约束**——不给重心就逐字节走原路径(A47 断言同一输出);② **分词单一真源**——重心直接复用 `search.ts: tokenizeQuery`,不另写一份停用词表(§5.22 判据单一真源);③ **只读不破**——提案日志是侧车观测文件,`memory_audit` 仍不写记忆库、不刷 `accessedAt`(A50 显式断言)。 - **自证反例**:本次改动的动机来自实测反例(重启唤醒消息注入了三条与当轮意图无关的记忆);A46 把该反例翻过来写成判据——**反例进测试,才算真的修了**。 11. **2026-09-15 · v0.8 压缩流水线证据层(跨条目巡检先于修业务逻辑)** - **触发事实(不是猜测)**:跨条目巡检(`memory_stats` + `memory_browse` 含归档)发现——`日概要 2026-09-13` / `日概要 2026-09-14` / `周概要 2026-W37` **全库 grep 无**;而这两天的 episodic 原料充足、单位早已结束,§5.6 又明写「含历史缺口回填」。⇒ 声明与事实背离。 - **为什么先补证据而不是先修**:本流水线只有 `console.log` / `console.error`,宿主 logger 不落盘 ⇒ **根因不可判**(是没扫到?扫到了判 no-sources?压了没写?写了被吞?三种可能都解释得通)。§5.22 的规则顺序是「先补证据层,再修业务逻辑」——**跳过证据直接猜修,等于把猜测留在生产里**。 - **关键设计取舍**:① **判定命名化 + 单一真源**——把原来那串 `continue` 命名成 `not-ended` / `no-sources` / `already-summarized` / `pending`,并让 `findPendingCompressions` 只做投影(禁止两处各维护一套判定);② **只记首轮 scan**(后续轮是链式推进的中间态,会掩盖全貌);③ **触发器不进压缩器**——`atMs`/`trigger` 由装配层补齐,压缩器只交事件;④ **零回归**(不给 sink 则行为零差异,A59 断言);⑤ **先量后定**——本轮只改观测面,**没有**顺手改压缩业务逻辑(根因未定前不动)。 - **顺带修正的双平台夹具缺陷**:`role` / `audit` / `centroid` 三处夹具把派生值写死(`const WID = 'c:/Users/Alice/proj'`),而 POSIX 下 `resolve()` 语义不同 ⇒ **12 个 A 测试在 node 22/WSL 侧静默红**(Windows 侧绿)。改为 `workspaceIdOf(CWD)` 派生后:mjs 套件 WSL 侧 **179/179**、Windows 侧 `test:all` **261(260 pass + 1 skip)**。教训:**夹具里的派生值必须调用被测的派生函数**,硬编码 = 换平台就静默失真。 12. **2026-09-15 · v0.8.1/v0.8.2 · 证据层的第一次实战回报(U10 从「不可判」到「当晚治愈」)** - **时间线(全部有落盘证据)**:19:35 部署 v0.8.0(只有观测)→ **19:35:50 首条 `scan` 就定案**(三桶判定正确)→ 19:35:51 首条 `error` 给出 provider 原文 → 19:41 修会话 id(v0.8.1)→ 错误变成「输出触顶」→ 19:45 加提示词长度预算(无效,模型超写)→ 19:53 抬 `maxTokens` 到 32000 → **19:57:35 三桶全部 compressed + `end`**。 - **为什么值得记为「实践修订」**:它验证了 §5.22 的规则顺序——**先补证据层再修业务逻辑**。当时三个候选解释(没扫到 / 无原料 / 压了没写)**都说得通**,若跳过 instrumentation 直接猜修,最可能是在判定层白改一晚(而判定层本来就是对的)。 - **被修正的两个认知**:① 「后台/定时任务里的 LLM 调用与前台等价」——**不等价**:网关类 provider 需要的会话头由宿主插件按 `GenerateOptions.sessionId` 注入,**不传就是拒单**(对外部插件的隐式依赖必须显式满足);② 「提示词写「不要超过 N 字」就能约束输出」——**约束不住**(实测超写 2×),**输出上的硬边界只能落在 token 上限**;提示词预算只能当倾向。 - **观测自身的教训**:`memory_health` 的「最近待压」读的是首扫快照(U13);`error` 记录只带 `message` 不带 `usage`——本次能归因靠的是「已产出字符数」这一条临时加的字段(v0.8.2),说明**错误路径的证据同样要设计**,不能只把成功路径做完整。 13. **2026-09-17 · v0.9 遗忘与修改的执行闭环(主人指令:「记忆插件缺少遗忘与修改机制」)** - 语义**被补充**:v0.8 前「遗忘」只有单条 `forget`(未知 id 报错)、「修改」只有整文覆盖 `update`(**旧值即弃**)。同日实测缺口(`memory_audit` 读数):**ARCHIVE 候选 254 条 / 216,024 字符**,而逐条清理需 254 次调用 ⇒ 机制上「知道该忘,但忘不动」;REVIEW 标出 3 个近重复簇却**无合并手段**;改错无历史可回溯。 - 补齐三件事:① `forget` 三选择器(`id` / `ids[]` / `tier` 分档)+ `max` 护栏 + `dryRun` 预览;② `update` 三模式(`replace` / `append` / `patch`,patch 要求唯一命中否则拒绝)+ `revisions` 修订快照(保留最近 3 条);③ 新工具 `memory_merge`(近重复簇:正文追加带来源标注 + 被并入者软归档,信息不丢)。 - 教训:**只读提案器不是机制**——「能看见该忘什么」与「能一次忘掉」之间隔着执行原语;提案面与执行面必须成对交付,否则规则正确却落不了地(同 §5.14「机制送达信号、决策归我」的组合缺失)。 ## 10 · 未决问题 - **U1 ~~端到端幂等如何补~~ → 已解决(2026-09-13)**:采纳方案 ① 的**强化版**——不是 scope 级单飞,而是按 (scope, level, bucket) 串行(粒度更细、并行度更高;scope 级会把该 scope 全部桶串行化)+ 写前复核兜跨进程。**遗留(需主人裁决)**:库中 4 对历史重复桶是否去重(`forget` 软归档其中一份即可,但动记忆数据属须请示类)。 - **U2 ~~测试记账三处不一致~~ → 已解决(2026-09-13)**:新增 `test:ts`(76 项,node ≥24)与 `test:all`(201 项);README 数字回填实测(125 + 76)。**遗留**:`.ts` 套件在 node 22(WSL 侧)不可跑——已在脚本旁注明前提,未强制统一运行时。 - **U3 压缩触发源不可分辨**:懒压缩与周期补压写出**同形**概要(`source.reason` 只记层/桶/原料数,不记触发路径)→ 生产数据无法回答「这条是谁压的」。倾向:在 `reason` 里加触发源标记(`lazy`/`periodic`),便于事后判别机制存活。 - **U4 ~~README / 测试名的 `wakeup=false` 文本~~ → 已解决(2026-09-13)**:四处(`README.md` §压缩即记忆、`tests/compaction-sink.test.ts` 头注 + 测试名、`DESIGN.md` §十 表格与通道 C 段)全部改为 `wakeup=true` 完成即送达,并标注 2026-08-16 修正来源;权威语义以行为与本文档为准。 - **U5 年层与 `deprecated` 路径缺正向验证**:年概要从未产出(因 2026 年未结束);同时「压缩链能否上探到年」没有测试或 dry-run 证据。倾向:补一个注入固定时钟的链式压缩测试(day→week→month→year 全链)。 - **U6 `memory_check` 的对外承诺**:工具描述说「查看待沉淀建议」但恒空。倾向:要么下线该工具,要么在描述里更醒目地标注「未接线」(当前已有说明,但工具名本身仍是承诺)。 - **U7 存量条目的隔间归属(需主人裁决)**:674 条存量条目全部为共享记忆——这对迁移安全是优点,对幽灵隔间是缺点(幽灵可读到全部历史)。选项:① 保持共享(靠独立 `DSH_HOME` 做真隔离);② 一次性把某批 tag 的条目回填 `role`(= 批量改记忆数据,属须请示类)。倾向 ①。 - **U8 记忆生命周期与价值体检(MAGE 借鉴的下一步)**:~~① 只读体检器(按 ν(x) 排序输出「GC 候选/应降级/应保留」)~~ → **已交付 v0.6**(`memory_audit` + 侧车用量轨迹,见 §5.9)。**仍缺**:② **双时态**(`validFrom/validTo`)+ `supersedes`/`invalidates` 链(现在只有布尔 `archived`,删除不留失效证据);③ **事件超边层**(多主体共同产出的事件作为一等条目——v0.5 的 `role` 只解决分区,不解决「谁+什么动作+哪份证据共同产出了这个结论」);④ 权重校准(需要「提案被采纳/否决 + 事后是否后悔」的样本)。 - **U9 注入的「该不该」与「好不好」(v0.7 线上观察)**:重心化解决了「按什么查」(相关性翻倍),但暴露两个新问题:① **唤醒类消息是否该触发注入**——`[守护] web 已重启` 是系统事件,不是对话,给它注入运维历史条目价值存疑(现触发面为 GUI/telegram 主人消息,唤醒消息走在同一条路上);② 现指标只数**条数与次数**(注入 6 次 / 命中 12 条),**没有质量口径**("这次注入有用吗")。倾向:先加一个**可选的判定信号**——注入是否被后续动作引用(如注入后我是否 `recall`/`update` 了其中某条),再谈是否排除唤醒触发。**未决,需主人视角**。 - **U10 ~~压缩缺口~~ → 已解决(2026-09-15 当晚闭环)**:症状是缺 `日概要 2026-09-13` / `日概要 2026-09-14` / `周概要 2026-W37`(全库 grep 无,含归档)而原料充足、单位已结束。**证据层(v0.8.0)首跑即定案**:`scan` 显示三桶判定**完全正确**(`pending`),失败在总结调用;两次真因——① **缺会话 id**:`GenerateOptions` 无 `sessionId` ⇒ 宿主插件 `dsh-x-opencode-session` 不加 `x-opencode-session` 头 ⇒ 网关拒单(原文即轨迹里的 `Error from provider (Console Go): Request is missing x-opencode-session…`)⇒ 修 v0.8.1(lazy 用 `agent.session.id`;periodic 把会话 id 与路由同源取出);② **输出预算不足**:路由通了以后触 16,000 token 上限被 fail-closed,实测单份概要 12,233 / 12,919 / 17,436 字符(≈18–26k token)⇒ 修 v0.8.2 + `maxTokens` → 32000。**验收 A67 症状消失**(三桶全部 compressed、原料冷归档)。**教训(已写进知识条目)**:提示词里的字数预算是**劝告性**的(模型超写 2×),**硬约束只能来自 token 上限**。 - **U13 `memory_health` 的「最近待压」口径易误读**:该数字取自**最近一条 `scan`(= 每轮压缩的首扫)**,因此一轮成功压缩后它仍显示压缩前的待压数(实测:显示「最近待压 3」而同一轮已压缩 3 单元)。语义没错(首扫快照)但措辞易读成「当前还剩几个」。倾向:改标签为「首扫待压」,或让 `end` 记录带收尾时的 pending(恒 0,信息量低)——**待下次发布随手修**,不单开一轮。 - **U12 ~~夹具硬编码派生值~~ → 已解决(2026-09-15)** - **U11 侧车开关有两处未接线(诚实声明)**:`audit.access_trace.enabled` 与 `audit.proposal_log.enabled` 会被解析、写进配置对象,但 `index.ts` 的 writer 直接调 `appendAccessTrace` / `appendProposalRecord`,**不读该开关** ⇒ 设 `enabled: false` 目前**不生效**(文档曾承诺「可关」,属声明与实现背离)。v0.8 只把 `compress_trace` 接通到各 workspace 配置(`makeCompressTrace` 读 `cfg.audit.compressTrace`);另两处需要把 config 传进 writer 签名(独立小重构),**未做**。倾向:与下一次触碰 `tools.ts` 的改动合并处理,避免为开关单独动一次组合。 - **U12 ~~夹具硬编码派生值~~ → 已解决(2026-09-15)**:`role.test.mjs` / `audit.test.mjs` / `centroid.test.mjs` 三处把 `WID` 写死为 `'c:/Users/Alice/proj'`,而 POSIX 下 `resolve()` 语义不同 ⇒ 作用域不匹配、12 个 A 测试在 node 22/WSL 侧静默红(**Windows 侧一直绿,所以长期未被发现**)。改为调用 `workspaceIdOf(CWD)` 派生后双平台同源:WSL mjs **179/179**、Windows `test:all` **261(260 pass + 1 skip)**。**遗留**:`.ts` 套件在 node 22 仍不可跑(需 node ≥24,见 U2)——已在脚本旁注明前提。