# dsh-engram > **[中文](README.zh.md) · [English](README.md) · [已交付 UI 控件清单](FEATURES.zh.md)** [![CI](https://github.com/skepsun/dsh-engram/actions/workflows/ci.yml/badge.svg)](https://github.com/skepsun/dsh-engram/actions/workflows/ci.yml) 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 设计的极简长期记忆插件,融合了 [symbolic-index](https://github.com/skepsun/symbolic-index) 与 [pi-esr](https://github.com/skepsun/pi-esr) 的思想—— 目标只有一个:**省 token**。 - **零 LLM 摄入** — 纯模式匹配从工具结果自动捕获有意义的事件(带书面 `-m` 提交信息的 git 里程碑、 关键文件编辑、反复出现的错误),另有显式 `engram_store`。热路径上没有任何模型调用,纯粹的操作 ——`git push` / `git stash` / 无提交信息的 commit——刻意**从不记录**(见下方「自动捕获策略」)。 每条写入先过一道确定性**密钥脱敏器**——API key、JWT、`Bearer` token、私钥、AWS/Stripe/Slack/GitHub token 与 `key=value` 形密钥一律替换为 `` 标记,且在去重哈希 / 字符上限 / 落盘之前完成, 敏感内容永远进不了磁盘。**测试失败**用纯模式识别(`npm test` / `node --test` / vitest/pytest/jest + 失败行)自动捕获为 `tags:["error","test"]`、信号上调。DSH **goal 域打通**:completed/blocked 的 goal(`goal/change` 会话事件)自动沉淀为 `handoff`/`error` 记忆(`tag:goal`),目标结局不随会话消散 (读取面见 `GET /api/dsh-engram/goals`)。 - **符号索引 + 渐进披露** — 一个紧凑的 `[ENGRAM]` 块(默认预算 700 字符 ≈ 175 token;每条记忆一行)在 组装提示词时注入,并**按会话冻结**,让请求前缀字节稳定以复用 KV 缓存。模型需要细节时用 `engram_recall` / `engram_detail` 下钻,而不是把命中的原文灌进上下文。召回对内存池做 **进程内 BM25 排序**(TF·IDF + 标签/短语加权 + **时间衰减因子**,确定性、零依赖); 命中实体锚定的记忆时附带**实体邻域关系简表**(复用 esr_link:`node --rel--> node · conf%`); 与历史失败**高度同源**的新错误会**唤醒旧 error 记忆**(刷新 recency + 命中,向 promoteHits 爬升直至重回索引,失败不重复堆积);本地零命中时自动兜底到 **DSH 自带的跨会话全文索引** (`ctx.sessionQuery`,按 cwd 过滤)——不另建 SQLite,完全复用宿主。当 FTS 兜底也返回空、 且查询含 **CJK 中文**(FTS5 中文分词整段只算一个 token)时,召回会对最近若干会话日志做 有界的**子串扫描**(zstd 解压 + LRU 缓存,限文件数与字节),追加命中会话的确定性 `# past sessions` 行。 - **会话启动自动召回(`autoRecallOnStart`,默认开)** — 纯拉取式设计有个被实证戳穿的盲区: 召回工具只交模型自觉,而真实会话里模型几乎从不主动调 `engram_recall`(实测 19 个会话数千次 工具调用只出现 ~5 次召回)。因此宿主在新会话**首次组装**时,用会话**首条用户消息**对工作区做 一次确定性 BM25 召回,把命中前 ≤3 条直接注入 `[RECALL]` 块(默认 700 字符预算,"少而准", agentmemory 候选①)——相关记忆不依赖模型想起来就已在上下文里。纯规则、零 LLM、 纯读不 bump 命中;superseded 过期真相不占注入槽(仍可 `engram_detail` 重取); 与 `[ENGRAM]` 一起按会话冻结保持前缀稳定。 - **记忆间语义(supersede/contradict)** — `engram_store` 可选收 `supersedes` / `contradicts` 记忆 id(同工作区校验)。被 supersede 的「过期真相」在召回里**降级到尾部**、并从 `[ENGRAM]` 块中**剔除**(新陈述占行);被 contradicts 的记忆保留排序但标注 `· contradicted by `。 旧行永不删除——可重取,只是排得诚实。可选 `autoSupersede` 配置(默认**关**)对"实体锚定 + 替换式更新(改用/不再/no longer/switched…)"自动打 supersedes;显式 `supersedes` 永远优先。 - **失败→解法闭环(零 LLM)** — error 记忆带 `cmd:` 签名标签;同命令随后跑通时自动沉淀 `procedure` 记忆(`fixed: — N earlier failing runs now succeed`),并把旧 error 行打上 `resolved`(不影响其排序),召回因此浮出解法而不是过期失败。 - **ESR-lite 证据闭环** — `esr_task` / `esr_close` / `esr_link` 给任务一个 `draft → active → stable` 生命周期,其中 `stable` 必须要有真实证据(`artifact` / `evaluation` / `memory_ref`),把"缺什么" 摊在明面上,而不是让 agent 没有证据就宣布完成。可开 `verifyArtifact`(默认开):非 URL 的 artifact 按工作区(= 会话 cwd)解析并在磁盘上实存校验,路径不存在则任务保持 ACTIVE、给出原因; `force:true`(或关掉该开关)可跳过磁盘校验——三种证据门照样必填。工具与网页表单共享同一个 证据门(`store.evidenceGate`),口径绝不会漂移。 任务不再是孤立节点:`esr_task(entity=…)` 一键挂点——自动建 `ent_` 节点并挂 `task --relates_to--> entity` 边(幂等),挂着领域锚点的任务立刻带关系; `esr_dep` 双写——依赖边同时进 `tasks[].deps`(blocker 逻辑)与 links 表,图谱里每条 任务依赖都是一等可见的边。 - **ESR 触发机制(pi-esr 对齐,混合:静态协议 + 冻结快照 + 单调渐进 actionables + 拉取)** — 前缀缓存稳定是核心。模型「何时用 ESR」靠**静态方法论**(`engram:esr-method`,每轮逐字节相同); `[ESR]` 块 = 确定性**冻结快照**(会话开始定下、确定性排序、时间戳排除、明示 WILL NOT auto-refresh)+ 本会话**单调渐进的 actionables**:`promote:/root-cause:/close:/stale:/escalate:` 在它们成熟的那一刻被**一次追加进块并冻结**(永不回撤)——前缀只在「真正出现新决策点信息」时变化、 绝不逐轮漂移,同时保留决策点提醒(漏斗最窄处 / 复发失败 / 收工 / stale / 平衡)。实时状态仍可 **拉取**:`esr_status`(全量视图 + `since_revision` 增量短响应 + 派生 actionables)。 recorder 由 `tools/result` 订阅喂入(actionables 的数据源);P4 转化度量照常(`#suggest-*` 标记 + 10 分钟归因 + `GET /api/dsh-engram/triggerstats`)。纯规则、零 LLM。 - **会话结束 todo 自动沉淀(draft 兜底,`autoSinkTodosOnEnd` 默认开)** — 会话内 todo 仍是 轻量、随会话而逝的"工作记忆";但当会话在其**拆解边界**(`session/disposed`)关闭时仍挂着 pending todo,这些待办会**自动转为该工作区的 ESR draft 任务**(名称=todo 原文、描述「源自会话计划」、 按已存在任务名去重、受 `maxTasksPerWorkspace` 上限约束),计划从此不静默蒸发。落到 **draft** 而非 active:不占 `[ESR]` 的 active 行、不产生证据义务,仍需刻意的 `esr_claim` / `esr_task` 推进才变 active——「进行中默认不写」的取舍不变,只把「收尾丢数据」的缺口补上。可关(profile patch 或设置卡)。 - **记忆 GC(pi-esr 约束)** — 定时、机械、只归档的回收:TTL 过期记忆归档、超容量工作区淘汰低价值条目、 stable 任务超保留窗离开 `[ESR]` 表面、悬空链接边清理。工作集(active 任务引用 / 任务记忆 / 已入索引命中) 永不触碰;**不硬删任何东西**——归档条目保留 id、始终可重取。 - **Context GC(自动 GC = 替代 DSH 自动 compact)** — 不是记忆面板 GC:接管 DSH 自带的上下文压缩 (`compaction` 服务),把「有损 LLM 全量摘要」换成**机械驱逐 + 重取指针**——扫描被驱逐轮次里的 engram/ESR 锚(`#记忆id`、`tsk_*`、`ent_*`、`file_path`),只留一行「细节在 `engram_detail` / `engram_recall` / `[ESR]` 里」;只有无锚轮次才走 scoped LLM 叙事兜底(`gcNarrative`,默认开、可关成纯机械)。 六条 GC 约束(working-set-protected / pointer-salience / no-provenance-no-evict…)全部落地;任何错误回退 默认压缩,永不破坏 compact。 - **Web 查看器** — 一个带统计的记忆浏览器和配置卡片,完全构建在 DSH 原生设置槽位上(不碰任何第三方 UI 包)。 ``` MIT · node >= 22.19 · host 半边 + 浏览器半边合在一包 ``` ## 为什么还要一个记忆插件? 对既有 DSH 插件生态的调研显示,记忆领域里"召回桥 / 审批门 / LLM 蒸馏 / 向量+图"这几个方向已经挤满了。 dsh-engram 补的是对 token 纪律真正重要的三个空白: 1. **写入路径没有模型** — 捕获是确定性的模式匹配。 2. **提示词里不灌全文** — 只注入有界的符号索引 + 会话启动时自动召回的最相关 ≤3 条(预算内), 更深的检索按需进行("检索到≠灌全文")。 3. **诚实的任务闭环** — 没有证据就不能宣布 STABLE。 DSH 已经提供跨会话 FTS(`ctx.sessionQuery`)、存储(`ctx.storageDomain`)、提示词注入钩子和设置槽位; dsh-engram 只是这些能力之上的一层薄组合层,而非重新实现。 ## 安全模型 DSH 插件生态还很年轻、缺乏背书:没有官方目录、没有签名校验,且 harness 的读写权限三档**约束不了插件自身代码**。因此记忆插件面对的信任何题比工具更高——它趴在你的提示词前缀上,还替你在磁盘上写东西。dsh-engram 的模型,直说如下: - **永远不联网。** 本插件没有出口 socket、没有遥测、没有更新探针。任何东西都不可能把会话带出本机。 - **无 shell、除一个存储文件外不碰文件系统。** 不执行命令、不碰任意路径。全部数据只落在一个 storage-domain 单元 `~/.dsh/storages/dsh_engram.json`(外加经 DSH 自身 `ctx.sessionQuery` 做的会话日志读取)。 - **密钥在落盘之前就被脱敏。** 每条写入路径——`engram_store`、自动捕获,以及本节的**导入/恢复**——都先把文本过一遍确定性的 `redactText` 规则引擎(API Key、JWT、`Bearer` token、私钥、`key=value` 密钥形态),*之后*才做去重哈希、字符上限与落盘。敏感文本永不落盘,召回时自然也不会有。 - **要做什么坏事,必须借你 harness 自己的权限。** 插件只通过你已信任的工具同款的 seam 向宿主"请求"做事;不扩沙箱策略、不拉高到 `danger-full-access`——你保持 profile 默认,engram 就只继承那些边界。 - **Web API 只读为主、loopback 栅栏。** `/api/dsh-engram` 下的 GUI 路由拒绝任何非回环调用方,除非你用 `trustedHosts` 显式放行某个主机名;即便放行,同源栅栏依然生效。 - **可核验。** `npm run dsh-engram -- doctor`(`scripts/dsh-engram.mjs doctor`)报告插件被配置去碰什么;整个 store 可检视、不透明的东西为零。如果对这一段有怀疑,要查的代码就是 `lib/redact.js`、`lib/store.js` 和本插件的 `cordis.patch.yml`。 这**不是**说它是个气闸:如果你的 profile 给了 agent `danger-full-access` 或 shell,agent 可以用——engram 是记忆层而不是沙箱。上面这句话的意思是:*dsh-engram 自己*不新增任何攻击面。 ## 数据契约:导出 / 导入 记忆不该当人质。四张表(memories / tasks / links / entities)可以用一个稳定、带版本号的机器可读格式整体倒出再恢复——重装、换机,或者(写一层薄适配器之后)搬到别的 harness / MCP 端点 / Claude Code skill,语料都跟得走。契约是带自描述头的纯 JSON: ```http GET /api/dsh-engram/export?workspace=/path/to/project # 单个工作区 GET /api/dsh-engram/export # 全部工作区 ``` ```jsonc { "meta": { "format": "dsh-engram/export", "version": 1, "exportedAt": 1724…, "workspace": "/path/to/project" }, "memories": [ /* 全部行,含归档——备份不丢溯源 */ ], "tasks": [ /* draft/active/stable + 归档 */ ], "links": [ /* 类型化边 */ ], "entities": [ /* 图节点 */ ] } ``` 恢复默认幂等、破坏性操作有门禁: ```http POST /api/dsh-engram/import # body: { payload, mode, dryRun?, workspace?, confirm? } ``` - **`mode: "merge"`**(默认)— 只写 `id` 尚不存在的行,备份可以随便重复套用而不会重复;每行保留自己的 `workspace`。 - **`mode: "replace"`** — 先把目标 `workspace` 的四张表清空,再只恢复属于它的行。破坏性操作,因此 body 里还要求 `confirm: "restore"`。 - **`dryRun: true`** — 只算出完全相同的执行计划(会写什么 / 跳过什么),不写任何一行。 - **导入遵守与 `storeMemory` 相同的不可变约定**:记忆文本落盘前先过密钥脱敏;会破坏契约的行(缺 id/workspace、空文本、超 `maxMemoryChars`、merge 时 id 已存在、超工作区记忆上限)会被跳过并写进响应报告,而不是中止整批。 单工作区的恢复往返: ```sh curl -s "http://127.0.0.1:3080/api/dsh-engram/export?workspace=$PWD" -o engram-backup.json curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \ -H "content-type: application/json" \ -d "{\"payload\": $(cat engram-backup.json), \"mode\": \"merge\"}" ``` ## 安装 ```sh # 从 GitHub(本仓库) dsh plugin --profile web add github:skepsun/dsh-engram # 发布到 npm 之后 dsh plugin --profile web add dsh-engram # 本地开发(符号链接——改动立即生效) dsh plugin --profile web add link:/path/to/dsh-engram ``` 然后**重启 `dsh web`**。数据保存在 `~/.dsh/storages/dsh_engram.json`。 > npm 与 GitHub 两种装法**都不需要手动补依赖**:pnpm 会自动安装 `zod`, > 并把可选的 `@deepseek-ai/*` peers 嵌套装进插件自身的 `node_modules`,CLI 也会 > 自动把插件登记进 profile 的 `dsh.profile.bundles`。下面的 `setup-links` 只在 > `link:` 开发工作流里需要——pnpm 故意不为符号链接目录安装依赖。 > 需要新建会话才能看到注入的 `[ENGRAM]`/`[ESR]` 块和全部工具——提示词与工具注册表都是按会话装配的。 ### `link:` 安装的依赖准备 符号链接安装的插件从**自身 checkout 的 `node_modules`** 解析 import,而这层依赖 不被 git 跟踪,换机器(尤其 Windows)会报 `ERR_MODULE_NOT_FOUND: Cannot find package 'zod'`(接着是 `@deepseek-ai/*` peers)。 一条命令重建依赖层: ```sh cd /path/to/dsh-engram node scripts/setup-links.mjs # 把 @deepseek-ai 工作区包软链进 node_modules, # 并安装 zod(优先复用 harness pnpm store, # 找不到则回退 `npm install`) ``` 脚本会自动定位 harness:`../deepseek-harness`(仓库上一级平级),也支持「仓库父级平级」布局 (如 `E:\deepseek-harness` 与 `E:\kototoro_demo\dsh-engram`)——都找不到再用 `DSH_HARNESS_DIR` 指定。 `node scripts/setup-links.mjs --check` 只打印状态不写入。 ## 在 Web 端能得到什么 重启后,全部落在 **DSH 原生**设置界面里: - **侧边栏「ESR 看板」入口 + 全屏看板** — 侧栏(New Session 下方)新增一行 `ESR 看板` 入口,右侧带**实时活动任务数徽标**(30s 轮询 /overview 汇总各工作区 active 任务)。点击在中间列打开全屏看板:**草稿 / 进行中(证据缺口) / 就绪(证据齐) / 已闭环** 四列 + 工作区筛选 + 搜索 + 内联新建表单 + 每张卡片的「补齐证据 → 关闭」表单(与 esr_close 同一证据门:artifact + evaluation + memory_refs)。头部带「**看板 / 图谱**」切换:图谱视图复用完整的关系图谱(esr_node/esr_link 力导向图,实体圆节点 + 任务勾选徽标,支持拖拽/缩放/点选查看关系明细),跟随工作区筛选,20s 轮询保持实时。入口与看板按 task-board 的 DOM 级挂载惯例自愈(MutationObserver 重插/重挂),并与 task-board / ssh 面板做跨面板互斥(打开本面板会关掉对方,点侧栏会话/项目行自动回到对话)。对话子树始终挂载在下方、由 `html[data-dsh-engram-board-active]` 控制显隐,切换零状态丢失。 - **输入框上方的「任务」统一条** — 接管 DSH 内建 todo 工具的自己同款 dock 槽位 (同一个 `conversation.input.dock` 单元格 / `id: todo`、更低 `priority`,从而遮蔽内建 TodoPanel),把两套任务平面**合并成一个**现代化控件:会话当前计划(`todo_write` 的 `todos` 投影)+ 工作区持久 **ESR 任务**(证据缺口徽标 + 内联「补齐证据 → 关闭」表单) + **关系图**(以 节点 → 关系 → 节点 芯片呈现,实体/任务名自动解析)。有内容才显示, 15s 轮询保持实时;若 loopback 围栏的 API 不可达,内建计划仍照常渲染(只是不显示 ESR 部分)。 条首有**工作区切换 chip**:默认跟随当前会话(标题注明),下拉可把 ESR 任务/关系的来源**固定到任意 工作区**(打 ✓ 标记),再点 × 或「跟随会话」即恢复;切换即时重取,内置 todo 仍属本会话——纯 UI 焦点 切换,不动模型会话上下文(注入块按会话冻结,前缀稳定)。 - **设置 → Engram 记忆** — 独立的一级设置页签(位于「插件」之后),不再是「插件」页里的子 tab;默认 「全部工作区」视图完整展示所有工作区的记忆/任务/关系(按工作区分组,工作区下拉 + 上一/下一工作区 翻页;记忆表格另行 10 条/页分页 + 跳页下拉,仅「类型 / 内容 / 操作」三列——正文列占满,时间、 标签、signal/hits/TTL 等全部折叠进内容行内(meta 行 + 标签行),正文限高三行省略、 行内「展开全文/收起」与 hover 均可看全文,归档/删除按钮竖向堆叠)。概览统计卡片(各工作区/类型的计数、自动捕获总量、各工作区 `[ENGRAM]` 索引 token 估算、GC 累计统计)、可搜索/可过滤的记忆表格(含归档与删除操作)、ESR 任务看板(「新建任务」 表单 + 点击「填写证据关闭…」补 artifact/evaluation/memory_ref 转 STABLE)、节点与关系清单 (节点 = 模型用 esr_node 登记的领域对象,如包/服务/仓库/概念;关系 = esr_link), 以及一个独立的 **关系图谱** 页签:手写 SVG 力导向图(无第三方图库,保持 bundle 纯净), 实体为圆形节点、任务为勾选徽标、关系按类型着色带方向箭头;支持拖拽节点/平移/滚轮缩放/重组, 悬停高亮邻域、点选节点在悬浮面板查看其全部关系与关联对象,悬空链接(端点缺失)单独计数提示。 以及一个 **注入预览** 页签:用与 systemPrompt 完全相同的纯函数实时渲染模型每个会话看到的 `[ENGRAM]` 索引块(order 40)与 `[ESR]` 任务/闭环块(order 41),终端风双栏展示(行级着色: 块头/任务行/drill 行/escalate 提醒高亮),附行数·字符·~tokens 成本与记忆/任务/关系/节点计数芯片, 每 20s 自动刷新、可一键复制注入块原文(新增 `GET /api/dsh-engram/preview?workspace=…`)。 以及记忆 GC 面板(dry-run 开关 + 运行按钮 + 指针报告)。 任务卡片(看板与 ESR 页)都带 **证据进度环**:一个三弧 SVG 圆环对应 artifact · evaluation · memory_ref 三道闭环门——全绿=证据齐可闭环、琥珀=有缺口、灰=尚无证据; 看板头部还有一个**聚合环**,显示全部进行中任务的证据完备度(%)与就绪数,一次看清整盘闭环进度。 纯 SVG 实现(无图表库,保持 bundle 纯净)。 以及一个 **遥测仪表盘** 页签:把 /stats 的真实调用累计(工作区 × 天滚动)画成纯 SVG 仪表盘——三枚大圆环 直读 **ESR 主动性**(与 escalate 阈值 0.34 比对,偏低标橙并提示)、**召回命中率**、**detail 转化**, 五张小指标卡(累计调用 / esr / 记忆 / 平均命中每查询 / 失败),近 14 天 mem-vs-esr 堆叠柱状图 + 工具调用 Top 8 横向条形图(mem 蓝 / esr 紫,与全文配色一致),20s 自动刷新,样本不足(<10 次)自动标注。 以及 **详情侧栏**(主从布局):任务 / 记忆 / 节点 / 关系行都可一键在右侧打开详情卡——任务含 状态徽标、证据进度环、完整 id/时间线、缺口清单、可跳转的记忆引用与「补齐证据→闭环」表单(独立于行内 表单,成功后联动刷新);记忆含全文、标签、signal/hits/TTL/来源会话元数据;节点含全部入射/出射关系 (按类型着色 + 方向 + 置信度);点击任务里的记忆引用可直达该记忆详情,找不到时给提示。 `POST /api/dsh-engram/tasks` 与 `POST /api/dsh-engram/tasks/close`(与 esr_task / esr_close 同一证据门)。 模型侧的主动行为由 [ENGRAM]/[ESR] 注入块驱动:多步工作即时建任务、反复出现的领域对象即时登记节点、相关任务/节点即时互连。 **真实行为观测(agent 遥测)** — ESR 页顶部新增「agent 行为观测」面板:每次模型调用 `engram_*`/`esr_*` 工具都实时累计到 按(工作区 × 天)的 usage 滚动行(新增 `usage` 表 + `GET /api/dsh-engram/stats`),折算成指标: **ESR 主动性** = esr 工具调用数 /(记忆 + esr 工具调用总数);**召回命中率** = 有命中的 engram_recall 次数 / 总次数; **平均命中/查询**;**detail 转化** = 命中召回后很快跟一次 engram_detail 的比例(会话内 8 事件窗口);失败数按工具记。 面板同时列出各工具调用计数与最近 14 天逐日滚动。这些是真实会话的真实数字——想提升 ESR 主动性, 就观察面板上 esr 占比并调整注入提示。 - **设置 → 插件 → 插件配置 → dsh-engram** — 与内置「终端 / Agent 循环 / 网页搜索」同款的 **默认折叠卡片**:标题 + 一行描述 + 箭头,点击展开/收起;展开后 12 个设置项按 「捕获与检索 / 索引 / 生命周期与 GC / 安全」四个分组展示。改动对新建会话即时生效 (已冻结的块保持稳定);支持放弃修改 / 保存,有未保存改动时标题上出现「未保存」徽标。 卡片通过连接自身的 settings RPC 直连命名空间(不走 isLoopback 门控的 scope),因此即使 GUI 经运营商授权的隧道访问也保持可编辑。 浏览器半边由 DSH 的 client-module loader 直接从本包提供(`dsh.client` + `exports["./client"]`,无需重建 web 应用);数据来自 loopback 围栏保护的 `/api/dsh-engram/*` 路由族。围栏默认关闭隧道访问;如需经授权的 隧道域名访问记忆查看器,把域名加进插件的 `trustedHosts` 配置(如通过 registry 或 profile patch): ```jsonc // patch/engram.json { "engram": { "trustedHosts": ["cream-club-fragrances-caught.trycloudflare.com"] } } ``` 修改 `client/src` 后重建 bundle: ```sh npm run build:client ``` ## 测试与评测 ```sh npm test # 152 项单元测试(含 usage 滚动 / /stats 路由 / Context GC / ESR 触发) npm run eval # 离线召回 + 结构基准(确定性语料,跑真实 store/recall 路径) ``` `npm run eval` 的检索部分参照 LongMemEval 的问答式评测:受控语料(ASCII + CJK、标签/实体/时间戳已知), 对真实 `domain.recall()` 逐一校准 **Precision@k / Recall@k / MRR / Hit@1**(probe 覆盖 tag 精确、子串、 多词、CJK、短语唯一、标签排序、负样本无回);结构部分借鉴 StructMemEval:精确去重率(同文本存 3 次折叠为 1)、 实体锚定覆盖率、节点/链接卫生(无悬空链接)。数字诚实、非调优——任何人跑出来都一样,复现即所得。 两层「真实测试」的分工:`npm run eval` 回答「检索层本身有多好」(确定性、可复现); `/api/dsh-engram/stats` + 观测面板回答「真实会话里模型实际怎么用」(ESR 主动性、召回命中率、detail 转化), 两者结合才能判断:召回层没问题但命中率低 = 模型没学会问;反之亦然。 ## 工具 | 工具 | 用途 | 类型 | |---|---|---| | `engram_store` | 显式存入一条记忆(kind、tags、可选实体锚点、可选 supersedes/contradicts id,可选 `source`/`conditions`) | 写 | | `engram_recall` | 工作区记忆的确定性关键词召回;可选 `search_sessions` 跨会话 FTS;可选 `scope=global`/`recallScope` 跨工作区召回(带 `[W:]` 来源标记) | 读 | | `engram_detail` | 一条记忆 id 的完整记录(来源、标签、命中数) | 读 | | `esr_task` | 创建任务实体(draft → active) | 写 | | `esr_close` | 按证据协议关闭任务(artifact + evaluation + memory_ref) | 写 | | `esr_link` | 在两个实体之间添加类型化关系(迷你图) | 写 | | `esr_dep` | 在任务间添加依赖边(blocks / relates-to / parent-of) | 写 | | `esr_claim` | 原子认领任务(assignee + claimedAt,draft → active) | 写 | | `esr_unclaim` | 释放已认领任务的 assignee | 写 | | `esr_ready` | 列出可认领任务(无 blocker、无人认领) | 读 | | `esr_status` | 拉取实时 ESR 状态 + 派生提示(`since_revision` 增量短响应) | 读 | | `esr_node` | 创建/更新实体节点(稳定符号) | 写 | | `esr_gc` | 运行本工作区的记忆 GC(`dry_run:true` 预览不落库) | 写 | | `esr_model` | 工作区预计算心智模型(`brief`/`full`,`max_chars`) | 读 | `[ESR]` 块是每会话冻结快照,绝不中途刷新——要最新状态调 `esr_status`。 ## GC:两块——记忆面板回收 + Context GC(替代自动 compact) ### 记忆面板回收(存储维护) 定时回收(`gcIntervalHours`,默认 24h)+ `esr_gc` 手动触发 + GUI 按钮,按 pi-esr 方式把存储保持在 有界内——**机械、工作集保护、只归档**: - TTL 过期记忆归档(软删;id 保留,可通过 GUI 的 archived 筛选检索); - 超容量工作区淘汰最低价值的*非保护*记忆; - stable 任务超 `gcStableRetentionDays` 归档、离开 `[ESR]`; - 两端点都已消失的链接被清理(悬空边)。 GC 永不触碰工作集:active 任务 `memory_refs` 引用的记忆、task 类记忆、已入索引的命中 (`hits >= promoteHits`)。用 `esr_gc` + `dry_run: true` 先预览。**不硬删**——报告的末尾为所有 归档项附上重取指针,归档可恢复、不是丢失。 ### Context GC(替换 DSH 自动 compact) DSH 默认的上下文压缩是**有损 LLM 全量摘要**(`compaction-basic`):被驱逐的历史被压缩成散文, 不可查询、摘要本身还烧 token。dsh-engram 的自动 GC 替代它:`ContextGcEngine extends BasicCompactionEngine` 只重写唯一的 `summarize()` 钩子,把摘要正文换成: 1. **扫描**被驱逐消息的 provenance 锚——`engram_store`/`engram_recall`/`engram_detail` 回显的 `#记忆id`、`esr_*` 涉及的 `tsk_*`/`ent_*`、`file_path` 锚(`esr_gc` 等管理工具**不算**锚); 2. **指针摘要**:每条被驱逐类别都带显式重取调用(`engram_detail(id: "…")` / `engram_recall(query)` / `[ESR]` 块 / `esr_ready`),active 工作集在摘要里**复述**不驱逐; 3. **兜底叙事**:只有无锚轮次(纯对话/推理)才走 scoped LLM 摘要(`gcNarrative`,默认开;关掉后整条 路径零 LLM,无锚轮次截断原文保留)。 触发时机完全跟随 DSH 现有 compact(step pressure / context-overflow / `/compact`);锁、回放校验、 tool-call/result 配对、token 定价全部复用基本引擎。任何错误 → 回退默认压缩,**永不破坏 compact**。 装配即注册 `compaction` 服务,卸载/reload engram 自动还回默认引擎。 > **收缩闸门(shrink gate)**:harness 拒绝任何不比被驱逐片段更小的 checkpoint(`summary is not > smaller than the shadowed content`),否则压缩回滚、上下文永不缩小、最终触发模型侧溢出。Context GC > 据此**自预测闸门**:用 host `tokenMeter` 复刻 harness 的 framing 估算(实测逐 token 一致),把指针 > 摘要/叙事体按被驱逐 span 的 token 预算动态**裁剪尾部**(指针头与工作集保留,细节仍在会话日志可 > 重取),保证 checkpoint 严格缩得更小、压缩真正提交——不出现"start 涨、summary 不涨"的假接管。 > 完整用法见 [`docs/CONTEXT-GC-GUIDE.zh.md`](docs/CONTEXT-GC-GUIDE.zh.md)(从 0 开始的使用教程)。 #### 启用入口(这一节就是答案:该功能开在哪) Context GC 的装配在**两个平面**——web 现在**全部自动**,零配置: - **host 平面(headless / TUI / base 型 profile)——开箱即用,无需任何配置**: 主插件 `lib/index.js` 在 `ctx.effect` 里直接 `mountCompactionEngine(ctx, resolved, { readWorkspace }) → new Engine()` 注册 `compaction` 服务; 配合插件 patch 禁用基座 `compaction-basic` 行,engram 即为该平面唯一 provider。 - `gcReplacesCompaction: true`(**默认**)→ `ContextGcEngine`(机械驱逐 + 重取指针); - `gcReplacesCompaction: false` → 挂裸 `BasicCompactionEngine`(退化为 DSH 默认 LLM 摘要), **`compaction` 服务永不缺席**。 - 关闭/开启:profile patch 里给 engram 行加 `config: { gcReplacesCompaction: false }`,或设置卡 「记忆 GC」区的 `gcNarrative` 关掉叙事。 - **preset 平面(web profile)——全自动,零配置,覆盖全部预设**:web 面把 compaction 放在 agent preset 的 isolate realm(shipped 预设自挂 `compaction-basic`),host 条目进不去、profile patch 也 够不到预设文件(`mountPreset` 的 Include 不带 patches——已对源码核实)。所以插件在启动时 (`agentPresets` 服务就绪后)**自动改写每个仍处于出厂 stock 布局的预设**——默认预设 + 整张 roster (shipped 根 + `~/.dsh/.agent-presets` 用户根,`standard`/`code`/`cordis` 和你自己的预设都覆盖): 把 `compaction` 组里的 `compaction-basic` 行换成 `dsh-engram/compaction`。这一自动装配是: - **默认开**(`autoWebCompaction: true`,设置卡「记忆 GC」可关),幂等——已替换则跳过; **关掉的含义**:只是在之后的启动时不再自动接管(headless/TUI/base 的 host 平面不受影响)—— 它**不会**还原之前已接管的预设,要还原用 `npm run web-compaction:revert`; - **逐预设判定,只碰 stock 布局**——用户自定义过 compaction 组的预设,以及根本没有 compaction 组的 预设(如 shipped `minimal`),一律不写、只记日志; - **带备份 + 严格校验**:每个文件写前在旁边留 `agent.cordis.yml.engram.bak`(create-only,保留首次 原件),写后重读校验,任何一步可疑即回滚该文件;改写失败只 warn,该会话仍用 DSH 默认摘要, **永不影响宿主**。 - **配置真实传导**:写进预设行的引擎配置来自设置卡/配置里的 `gcReplacesCompaction` 与 `gcNarrative`——改了这两个开关,下次启动会自动把已接管的行刷新成新配置(幂等重写), 所以**设置卡对 web 平面同样生效**,不再硬编码 true/true。 - 手动的完全等价物(预览 / 审计 / 卸载前回归,随 npm 包发布的 CLI,默认扫 shipped + 用户两个根、 可 `--file` 指定单个): ```bash npx dsh-engram status # stock → 未动;wired → 已接管;custom → 不碰 npx dsh-engram doctor # status + 按缺口排序的下一步建议 npx dsh-engram enable # 等价于启动时自动装配(通常 no-op) npx dsh-engram revert # 还原全部预设的 stock 行(卸载 dsh-engram 前先跑这个) ``` (仓库内 `npm run web-compaction:*` 是同一 CLI 的别名。) 替换后的 `compaction` 组形如: ```yaml - id: compaction name: cordis:group group: true isolate: compaction: true toolResultPruner: true config: - id: engram-compaction # ← 替换原 compaction-basic name: dsh-engram/compaction config: gcReplacesCompaction: true # 来自设置;false=该 session 用默认 LLM 摘要 gcNarrative: true # 来自设置;false=纯机械无 LLM - id: command-compact name: '@deepseek-ai/dsh-command-compact' - id: tool-result-pruner name: '@deepseek-ai/dsh-compaction-tool-result-pruner' # 原有配置保留 ``` ⚠️ **卸载前先 revert**:预设引用 `dsh-engram/compaction` 后,卸载 dsh-engram 会让该行悬空、 web 会话挂 loading。所以**卸载 engram 前**先 `npx dsh-engram revert`(还原全部预设, 或逐个恢复各自的 `.engram.bak`)。harness 大版本升级会覆盖 shipped 预设,悬空引用随之自愈。 #### 从 npm 安装后的第一印象(可感知 / 易引导 / 易配置) - **自动**:装进 profile 后启动一次即可——host 平面由 `mountCompactionEngine` 直接接管;web 平面 由 `autoWebCompaction`(默认开)在启动时自动改写全部 stock 预设。**无需任何手动配置。** - **可感知**:插件启动时把权威状态写到 `$DSH_HOME/engram/context-gc.status.json` (`host` 平面模式 + `web` 平面每个预设的接管结果 + 生效配置)。随时 `npx dsh-engram status` 或 `npx dsh-engram doctor` 查看;web 记忆看板(ESR 任务看板)头部也有 一枚状态徽标("Context GC·主机 ·N 预设",来自 overview API)。启动日志也有两行关键确认: `engram context-gc: compaction = Context GC …` 与 `engram web-provision: wired Context GC into preset …`。 - **易配置**:一个设置卡(「记忆 GC」区)管全部:`autoWebCompaction`(web 自动接管开关)、 `gcReplacesCompaction`(用 Context GC 还是回退默认摘要)、`gcNarrative`(叙事兜底开关)—— 改动后 `dsh web` 重启生效,web 预设行会被自动刷新成新配置。 **验证是否生效**:`dsh web`(或对应 profile)重启后—— - host 平面(headless/TUI/base):启动日志出现 `engram context-gc: compaction = Context GC (mechanical eviction + re-fetch pointers)`; - web 平面:日志出现 `engram web-provision: wired Context GC into preset "standard" (…); restart dsh web so sessions pick it up` (每个被接管的预设一行);或 `npx dsh-engram status` 显示 `wired`;任意预设的会话内 `command-compact`/压力触发即走机械驱逐 + 重取指针。 - (若见 `… keeping the existing compaction service` 说明同域已有 provider、未替换成功;`web-provision … skipped (custom)` 说明某预设是自定义/无 compaction 布局、未被触碰;`dsh-compaction-basic unavailable` 说明环境缺依赖,自动回退默认压缩。) ## 自动捕获策略 捕获是确定性、离线的——只看到工具*结果*,从不看对话本身。什么会被记录成一条记忆: | 工具结果 | 行为 | 信号 | |---|---|---| | `git commit … -m "提交信息"` | 记录——书面提交信息就是这条记忆 | 0.55 | | `git merge` / `rebase` / `cherry-pick` / `tag` / `checkout -b` | 记录(里程碑) | 0.5 | | `git push` / `git stash` / 无 `-m` 的 commit | **跳过**——操作回显,不是决策 | — | | 写入/编辑关键配置与文档路径 | 记录 | 0.3 | | 读取配置路径 | 记录 | 0.3 | | 反复出现的工具错误 | 记录(按消息去重) | 0.25 | 显式 `engram_store` 的记录不受上述规则约束(按会话限流)。 **谁能拿到 `[ENGRAM]` 索引行**(这才是真正进提示词的部分): `signal >= minIndexSignal` **或** `hits >= promoteHits` **或** `kind === "task"`, 再由 `indexMaxLines` / `indexMaxChars` 封顶。另有一道额外的闸保持管道干净: 自动捕获的 git 命令回显——文本里嵌着 shell 命令链(`git push: cd … && …`)—— 即使信号超阈值也不进索引,直到被召回命中晋升为止。其余条目安静地躺在存储里, 按需用 `engram_recall` / `engram_detail` 取用——「检索到 ≠ 注入」。 ## 注入块 模型实际看到的内容(每个会话渲染一次,然后冻结): ``` ESR 操作协议(静态,每轮逐字节相同) 1. 动工前:esr_ready 看可认领工作;esr_status 拿实时状态。 2. 多步工作 → esr_task(draft 起步);动工 → esr_claim;收工 → esr_close(三证齐)。 state 是唯一真相:拿不准 state 就 call esr_status。 [ENGRAM] workspace: symbolic-index · 2 memories · 1 task(s) active · 0 links [D] 06-18 Decided: use sqlite-vec for retrieval #a2331d87 [T] 06-18 Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref #tsk_8b26 drill: use [RECALL] below · engram_detail (full record) · engram_recall (more) · esr_task/esr_node/esr_link (work) [RECALL] recall · 1 hit(s) · first msg: retrieval - [D] 06-18 Decided: use sqlite-vec for retrieval upgrade #a2331d87 ×2 context: use above · engram_detail · engram_recall [ESR] tasks: 1 active / 1 stable - tsk_0d: Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref - closed: tsk_9a (RAG eval) · +1 snapshot from session start — WILL NOT auto-refresh; call esr_status for live state # this-session actionables (frozen) promote: 2 pending todo(s) vs 1 ESR task(s) — esr_task(name="…") #suggest-promote ``` 前缀:`[D]` 决定 · `[E]` 错误 · `[P]` 流程 · `[F]` 事实 · `[I]` 洞察 · `[H]` 交接 · `[T]` 任务。 被反复使用(`hits >= promoteHits`)的流程记忆升格为「实证经验」——前缀变 `[P✓]` 且**稳定排在 索引块最前**(每组内部仍按新旧排序,块保持确定性);这是我们对 TencentDB Agent Memory「Skill= 经过验证的可执行经验」的零 LLM 对应物。 入选规则遵循「自动捕获策略」(信号阈值 / 命中晋升 / git 回显守卫),并按配置的行数与字符预算封顶。 `#` id 通过 `engram_detail` 取完整记录。工作区没有任务时,`[ESR]` 仍会渲染一行点名 `esr_task`/`esr_close`, 让机制对模型保持可见,而不是整体消失。 `[RECALL]`(会话启动自动召回,`autoRecallOnStart`)按会话首条消息对工作区做确定性 BM25 召回, 把命中前 `autoRecallLimit` 条注入,受 `autoRecallMaxChars` 字符预算约束;无命中或空工作区时不渲染。 它是一条**提示**而不是全文灌入——模型该把它当成"这条相关,可能需要用 `engram_detail` 看细节"。 纯读、不 bump 命中、superseded 过期真相不占槽位;随 `[ENGRAM]` 一起按会话冻结。 ## 配置 默认值以 token 为优先;可通过 profile 补丁(`~/.dsh/profiles/web/cordis.patch.yml`)或 Web 配置卡片覆盖任意键: ```yaml - id: engram config: autoCapture: true # 零 LLM 工具结果捕获 sessionSearch: true # engram_recall 也可对历史会话 FTS recallScope: workspace # 召回范围:workspace(严格工作区隔离,默认)| global(可选跨工作区召回,带 [W:] 来源标记) autoRecallOnStart: true # 会话启动自动召回:按首条消息注入 [RECALL] 块(false=回到纯拉取) autoRecallLimit: 3 # [RECALL] 最多注入条数(少而准) autoRecallMaxChars: 700 # [RECALL] 字符预算 autoCapturePerSession: 40 indexMaxLines: 12 # [ENGRAM] 行数上限 indexMaxChars: 700 # [ENGRAM] 字符上限(token 预算) minIndexSignal: 0.4 # 低于此信号的自动捕获不进索引 # (git 命令回显即便超阈值也不进,直到命中晋升) promoteHits: 3 # ……直到被召回这么多次才进索引 expireDays: 180 # 记忆 TTL(0 = 永不过期) maxMemoriesPerWorkspace: 2000 gcEnabled: true # 定时记忆 GC gcIntervalHours: 24 # 回收节奏 gcStableRetentionDays: 120 # 超过此天数的 stable 任务离开 [ESR] gcReplacesCompaction: true # Context GC:接管 DSH 自动 compact(false=回退默认 LLM 压缩) gcNarrative: true # 无锚轮次走 scoped LLM 叙事;false=纯机械(零 LLM) engramIndexOrder: 40 # systemPrompt section 顺序(位于 tools 段之前) esrOrder: 41 ``` ## 开发 ```sh npm test # 152 个测试:核心 + Web API + GC + Context GC + ESR 触发(node:test) npm run build:client ``` 仓库结构:`lib/`(宿主半边:store / capture / index-block / tools / api / settings)、 `client/`(浏览器半边,TSX + `build.mjs`)、`test/`(node:test)。 ## 故障排查 **Web 界面一打开就停在 “Failed to load plugins”**,loader 报错形如: ``` failed to apply loader entry … (@linxin666/dsh-client-ui-web-ui-settings): keyed slot "settings.plugin.item" requires options.key ``` 原因:DSH 自 `0.1.0-rc.7` 起把配置卡槽位 `settings.plugin.item` 声明为**按 settings 命名空间键控**(卡片用自身编辑的命名空间作 `key` 注册——dsh-engram 的配置卡正是用 `key: "dsh-engram"` 这样注册的)。`@linxin666/dsh-web-ui-all` **0.2.0 之前的** `dsh-client-ui-web-ui-settings` 向该槽位注册分组卡片时**没有 提供 `key`**;而 loader 只要有一个 entry 失败就会中止整个启动流程,于是 GUI 一直卡在失败页。 修复方式: - **正确修复——升级全家桶**:`@linxin666/dsh-web-ui-all@^0.2.x`。0.2 系列已把自身设置面从键控槽位迁出,改为一级 `settings.section`(上游正是 为这个报错做的修复)。 - **临时解阻**:在已安装的 `node_modules/@linxin666/dsh-client-ui-web-ui-settings/lib/client.js` 中给那 个 `settings.plugin.item` 注册补上 `key: "web-ui-plugins"`,然后重启 `dsh web`。(在按命名空间键控的派发下,分组卡片只是不显示,不影响页面其 他部分。) ## 相关项目 - [symbolic-index](https://github.com/skepsun/symbolic-index) — 原始跨会话记忆插件(5 信号 RRF 融合、sqlite-vec、Dream Engine)。 - [pi-esr](https://github.com/skepsun/pi-esr) — 项目全周期的证据驱动任务状态;这里的闭环协议是它的简化形态。 ## 许可 MIT