# 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` 形密钥一律替换为 `<REDACTED:…>` 标记，且在去重哈希 / 字符上限 / 落盘之前完成，
  敏感内容永远进不了磁盘。**测试失败**用纯模式识别（`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`）。
- **用户纠正/偏好学习（`autoCapturePreferences`，默认开，零 LLM）** — 「AI 犯错被纠正后，教训要留存」
  （[correx](https://github.com/ozoz5/correx)、[PAHF](https://arxiv.org/abs/2602.16173)、Mem0 偏好提取）：
  一个 `session/event` 监听器盯 `user/message`，用确定性纠正/偏好句式把用户话语沉淀为独立的
  `preference` 记忆 kind——中文（`不要用X`、`改用X`、`以后统一用X`…）与英文（`don't use X`、
  `switch to X`、`prefer X`、`never use X`…）模式，三种方向（`avoid` / `switch-to` / `prefer`），
  并带疑问句 / 一次性操作指令 / 记忆工具元话语过滤器抑制误报。`preference` 记忆在 `[ENGRAM]`
  块里**压过其它所有 kind**（`[R]` 前缀），让用户的长期规则成为每会话模型看到的第一行；
  每会话上限 + 近期窗口去重与自动捕获同纪律。零 LLM、零向量、确定性纯函数——提取片段有界，
  每条写入依旧先过密钥脱敏器。
- **符号索引 + 渐进披露** — 一个紧凑的 `[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]` 一起按会话冻结保持前缀稳定。
- **读路径激活（D）** — 命中条目的单行预览被截断（>140 字符）时带 `…` 标记，且块尾
  引导行点名该具体 id：`#<id> truncated — engram_detail <id> (full) · engram_recall <query>`。
  模型由此知道完整记录就在一次确定性 `engram_detail` 之后，而不是以为预览就是全文
  （实证盲区：66 个会话 0 次 `engram_detail`——下钻路径此前对模型不可见）。
- **记忆间语义（supersede/contradict）** — `engram_store` 可选收 `supersedes` / `contradicts`
  记忆 id（同工作区校验）。被 supersede 的「过期真相」在召回里**降级到尾部**、并从 `[ENGRAM]`
  块中**剔除**（新陈述占行）；被 contradicts 的记忆保留排序但标注 `· contradicted by <id>`。
  旧行永不删除——可重取，只是排得诚实。可选 `autoSupersede` 配置（默认**关**）对"实体锚定 +
  替换式更新（改用/不再/no longer/switched…）"自动打 supersedes；显式 `supersedes` 永远优先。
- **失败→解法闭环（零 LLM）** — error 记忆带 `cmd:` 签名标签；同命令随后跑通时自动沉淀
  `procedure` 记忆（`fixed: <cmd> — 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_<slug>` 节点并挂
  `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，opt-in）** — 不是记忆面板 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\"}"
```

## 兼容性

<!-- dsh-market 兼容信息块 —— 与 docs/RELEASE-NOTES.TEMPLATE.md 的市场 README 公约保持一致 -->

- **DSH 版本**：`>=0.1.2-alpha.2 <0.3.0-0`（本版 `0.7.2`；已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` —— 见下文矩阵）
- **最后验证日期**：2026-10-27 —— DSH `0.2.1-rc.2`（`dsh --version`）+ web profile 实机组合；npm test 483/483 通过
- **安装方式已验证**：`dsh plugin --profile web add dsh-engram`

> **一句话结论**：所有**已真正发布**的版本（`0.2.0` … `0.3.6`）**只支持旧版
> DSH**（`0.1.0-rc.7` / `0.1.1` API 图，peers `^0.1.0-rc.7`）。DSH
> `0.1.2-alpha.2` 及更新版本与它们**不兼容**——settings/client API 族在该版本
> 断裂。新版 DSH 支持**自 `0.3.7` 起**提供——它是新 API 代际的第一个发布（见下文）。

| dsh-engram 发布版本 | 状态 | 支持的 DSH | 声明方式 |
| --- | --- | --- | --- |
| `0.2.0` … `0.3.6`（npm 全量 + GitHub v0.3.3–v0.3.6） | 已发布 | **仅旧版** —— `0.1.0-rc.7` / `0.1.0-rc.8` / `0.1.1-rc.1` / `0.1.1-rc.2` | peers `@deepseek-ai/dsh-*@^0.1.0-rc.7` |
| `0.3.7` / `0.4.0`（npm） | 已发布 | **新版 DSH，settings `installSection`** —— `>=0.1.2-alpha.2 <0.2.0-0` | `engines.dsh` + peers 同范围 |
| `0.5.0`（npm） | 已发布 | **DSH `0.1.2-alpha.2` … `<0.3.0-0`** —— 设置卡迁移到 entry-id volatile Config（`0.1.7+`/`0.2.x`），`≤0.1.5` 自动回退 `installSection` | `engines.dsh` + peers `>=0.1.2-alpha.2 <0.3.0-0` |
| `0.5.1` | 已发布 | 同 `0.5.0` —— 会话扫描修复（`session.v<N>.jsonl.zstd`，issue #8）、数据导出/导入 GUI、替换/冲突可视化（issue #3） | 同 `0.5.0` |
| `0.6.0` | 已发布 | 同 `0.5.0` —— Context Ledger、BoundaryPrune/ResultPack 默认开 + 账本否决、JIT 推送、Context GC 改 opt-in | 同 `0.5.0` |
| `0.7.0` | 已发布 | 同 `0.5.0` —— 会话 episode、恢复简报、实体页/实体卡、可重验证锚点 + 分层 TTL | 同 `0.5.0` |
| `0.7.1`（npm） | 已发布 | 同 `0.5.0` —— web 平面 Context GC 适配 DSH `0.2.0` 声明式预设（14 空格缩进）、bundle patch 重新启用 host 平面 `tool-result-pruner`、压缩节省接入 Context Ledger | 同 `0.5.0` |
| `0.7.2` | 已发布 | 同 `0.5.0` —— L1 边界剪枝改经 `ctx.inject` 武装，修掉「pruner 行比插件晚激活一帧就永久休眠」的启动时序竞态 | 同 `0.5.0` |
| `0.7.3`–`0.7.4`（npm `latest`） | 已发布 | 同 `0.5.0` —— 设置卡迁到「Engram 记忆 → 设置」tab（DSH `0.2.1` 删了旧 section）；修 DSH `0.2.x` 上「推送腿」整条静默失效（`[RECALL]` 启动召回、JIT 推送） | 同 `0.5.0` |
| `0.7.5`–`0.7.13` | 本地 | 同 `0.5.0` —— 账本按会话落行 + `gate-audit`；L1 从「自己剪」转成「记账 + 归因」；压缩入账读真实事件形状；会话计数重启不清零；CLI 尊重 `DSH_HOME` | 同 `0.5.0` |
| `0.8.0`–`0.8.5` | 本地 | 同 `0.5.0` —— 证据化 procedure、导出 DSH Skill、subagent 交接胶囊、审批门 + GUI、记忆行文件信号（`⚠file-changed`/`⚠file-missing`）、G6 门改单边 + 隐藏规格语料（Δ=+58.3pp） | 同 `0.5.0` |
| `0.8.6`（npm） | 已发布 | 同 `0.5.0` —— 修 issue #9：宿主不暴露 `sessionQuery`/`snapshotEvents` 时退回 `seq`+`eventAt` 逐条读，四项注入特性不再静默休眠；warn 报出宿主 surface；G6 第五轮 Δ=+90.0pp | 同 `0.5.0` |
| `0.8.7`（npm） | 已发布 | 同 `0.5.0` —— 推送腿落盘（`$DSH_HOME/engram/push-legs.jsonl`）+ `status` 打印事件日志读法；`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
| `0.8.8` | 本地 | 同 `0.5.0` —— 修推送腿遥测写错目录（`engram/engram/`）导致状态显示「还没记录」；`pushLegsPath` 契约写死在一处 | 同 `0.5.0` |
| `0.9.0`（本期 / this release） | 本地（tag，未发 npm） | 同 `0.5.0` —— 产品代码只有一条 CLI 修复（`--presets-dir` 优先）；主体是评测：G6 网络围栏（per-uid 出网白名单 + 两条前置探针）、密钥语料第 4 条自证、污染三分类与重算、G7 实测 0.2956%。**不是 1.0**：宿主仍 rc、G1 未测、减法债未还 | 同 `0.5.0` |

### API 族断裂点:`0.1.2-alpha.2`

`0.1.2-alpha.2` 是新 API 族的第一个版本，也是第一个**让已发布插件失效**的版本：

- `@deepseek-ai/dsh-settings` **移除了** `settingsNamespace()` 与
  `installSettingsSection()`（≤ `0.3.6` 的每个发布都直接 import 这两个符号），
  改为 `settings.installSection`——旧宿主端在 `0.1.2-alpha.2+` 上直接加载失败。
- Web 客户端注入集整体更换：`@deepseek-ai/dsh-client-runtime` 在新代际**已不存在**
  （被 `dsh-api-remotes` + `dsh-client-locale` / `-conversation` / `-renderer` /
  `-ui-settings-plugins` 取代），设置卡此前改走 `ctx.remote.settings`。
- 同一边界的伴随升级：`@deepseek-ai/cordis` `^4.0.1 → ^4.0.2`、
  `@deepseek-ai/schemastery` `^3.18.1 → ^3.18.2`、`@deepseek-ai/dsh` 不再随包
  发布 `config/agent-presets`（改为 `dsh.configTrees` 机制）。

**web 设置卡已不依赖上面任何一项**：自 `0.3.7` 起，配置卡通过插件**自有**
HTTP 路由（`/api/dsh-engram/settings`，与记忆 viewer 同样的同源 fetch）读写
`dsh-engram` settings namespace，因此在 `0.1.2-alpha.2` 分界线两侧都是代际无关的。

**`0.1.7` / `0.2.0` 还有一次 settings 断裂**——`0.4.0` 在最新 DSH 上启动被拒的
真正原因：`@deepseek-ai/dsh-settings` 在 `0.1.7-alpha.2` **再次移除了
`settings.installSection`**（`0.2.x` 一路保留）。新设置模型是**条目式**的：可编辑字段
是插件自身 `Config` 的 `.volatile()` 字段（由 loader 校验并热提交），设置表单以
profile 条目 id（`engram`）为命名空间，不再需要任何“挂载”调用。`0.5.0`
在 `installEngramSettings` 里对两代际做特性检测：

- DSH `≥0.1.7` / `0.2.x` —— 34 个 GUI 旋钮即 `Config` 的 volatile 字段
  （`lib/settings.js` `makeEngineConfigSchema`，由 `lib/index.js` 导出为
  `Config`）；卡片仍走同一 `/api/dsh-engram/settings` HTTP 路由读写条目 `engram`，
  `service.settings` 接缝在 `loader/volatile-update` 事件上把 `resolved`
  重新同步，卡片编辑对新会话即时生效。
- DSH `≤0.1.5` —— 照旧注册旧 `settings.installSection` 命名空间（`dsh-engram`）。

声明范围 `>=0.1.2-alpha.2 <0.3.0-0` 同时覆盖两代际；HTTP wire 命名空间在两代际都保持
`dsh-engram`（由接缝的活动命名空间驱动写入）。

所以：跑 ≤ `0.3.6` 的已发布版本时请把 DSH 保持在 `0.1.1-rc.2` 或更旧；升级
DSH 到 `0.1.2-alpha.2` 之前，先升级插件到 `>=0.3.7`。

### 当前发布版本 —— `0.7.2`

修复 **L1 边界剪枝的启动时序竞态**（真机定位：组合里多挂两个 bundle 就复现）。

`BoundaryPrune` 在插件安装时用严格 `ctx.get("toolResultPruner")` 取宿主剪枝器；而 `0.2.0`
的 web profile 里，剪枝器的 fiber 比插件**晚激活约 100 ms**（实测：`dsh-context` +
`dshmarket` + `dsh-engram` 组合下 `t0 strict=undefined / loose=object`，100 ms 后 strict 才
拿到服务）。严格查找此时为空 → `installBoundaryPrune` 放弃 → **L1 整个进程生命周期都不武装**，
账本唯一的「剪枝节省」入账腿静默断掉；设备快照还会把这种"还没到"报成"行不存在"，把人引向错误的排查方向。

- **改经 `ctx.inject(["toolResultPruner"], …)` 武装** — cordis 在服务真正激活那一刻回调
  （重载后也会重新武装），不再赌安装顺序；武装成功后立刻重写状态快照。
- **区分「缺失」与「晚到」** — `boundaryPruneAvailability` 增加非严格查找：行被禁用/没装
  报 `no toolResultPruner service … (the bundle patch must re-enable …)`，只是 fiber 未激活
  报 `registered but its fiber is not active yet`。两种原因以前在快照里长得一模一样。
- **验证** — 用真实 profile 的 bundle 组合（含 `dsh-context`）在隔离 `DSH_HOME` 复现
  `armed:false`，修复后同一组合 `armed:true · threshold 8192`；`npm test` 406/406。

### 上一发布版本 —— `0.7.1`

修复 **DSH `0.2.0` 声明式预设下的 web 平面 Context GC 接管**：旧版自动装配以
`agent.cordis.yml` 的 4 空格布局为锚，而 `0.2.0` 把预设换成
`dsh-web-app/presets/<id>.patch.yml` 里的 `@deepseek-ai/dsh-agent-preset` 声明（行在
14/16/18 空格缩进），导致 `autoWebCompaction` 静默跳过所有预设。

- **结构化定位 compaction 行**：找 `group: true` 的 `compaction` 组，组内按 `- id:` +
  `name:` 双重校验定位 stock/engram 行，替换块继承该行自己的缩进；旧版与声明式布局都
  swap → restore 逐字节还原。顺带修掉旧 stock 替换串不含行尾换行、可能半改写文件的隐患。
- **声明式预设发现**：roster 不再带 `path` 时把 id 映射到出厂 `<presetsDir>/<id>.patch.yml`
  （`DSH_WEB_PRESETS_DIR` 可覆盖）；无出厂文件的用户自定义声明式预设跳过并打日志。
- **status/doctor 适配**：自动发现两种布局并显示布局类型；doctor 新增
  `autoWebCompaction=false`、node_modules 预设（`npm update dsh` 会恢复出厂布局）、
  profile patch `preset-*` 覆写（优先于 shipped 文件）三类提示。
- **迁移**：打开 `autoWebCompaction` 后重启一次 `dsh web`，下一次启动即幂等接管所有
  stock 声明式预设；`npm test` 396/396。

### 上一发布版本 —— `0.7.0`

把「记忆」从**事件记录**推进到**可维护的当前真相**：会话结束自动沉淀 episode、
新会话给恢复简报、`esr_node` 升级为实体页、文件锚点让过期真相可被检测。

- **会话 episode**：`session/disposed` 零 LLM 生成紧凑纪要（文件/命令/失败→修复/
  决策/未闭环），不进索引，每 workspace 保留 20 条。
- **恢复简报**：新会话注入最近 7 天最新 episode（`[RESUME]` ≤240 字符 + 下钻指针）。
- **实体页 + 实体卡**：`truth / timeline / open loops / conflicts / relations`；
  索引前置最多 2 张实体卡，`engram_detail(entity=…)` 下钻。
- **可重验证锚点**：带 `filePath` 的记忆记录 `mtime/size/sha256`；会话启动有界
  重验证，改动/缺失 → `stale`（不进索引/truth，仍可 detail 重取）。
- **分层 TTL**：preference/procedure 永久、episode 14 天、error 60 天、其余 180 天。
- **迁移**：只有新写入会落锚（不回溯历史）；升级后重启 `dsh web`；episode 要等一个
  会话结束，resume brief 在下一个新会话生效。`0.6.0` 的账本/默认值/JIT 保持不变。

已发布的 `0.4.0` 声明 `>=0.1.2-alpha.2 <0.2.0-0`——这个范围被 semver 门槛在
**所有 DSH `0.2.0-0` 预发布**上拒绝（`<0.2.0-0` 排除 `0.2.0-rc.x`），且其宿主设置端
仍在调用 `dsh-settings 0.1.7+` 已移除的 `installSection` API。`0.5.0`
**两处都修复**：范围放宽到 `<0.3.0-0`（用 DSH 自身的 `includePrerelease` 检查验证过，
`0.2.0-rc.2` 及整个 `0.2.x` 线都放行）；设置卡迁移到上面描述的 entry-id **volatile
Config** 模型（并带特性检测的旧代际回退）。

| 组件 | 适配版本 |
| --- | --- |
| DSH runtime / host / client 包族 | `>=0.1.2-alpha.2 <0.3.0-0` |
| `@deepseek-ai/cordis` | `^4.0.2` |
| `@deepseek-ai/schemastery` | `^3.18.2` |
| Node.js | `>=22.19.0` |

`0.1.2-alpha.2` 是这组 API 的最低版本：宿主设置注册按代际**特性检测**——DSH `≤0.1.5`
走 `settings.installSection` 注册 `dsh-engram` 命名空间；`≥0.1.7` / `0.2.x` 走插件
`Config`（34 个 GUI 旋钮标记为 `.volatile()`，条目 id `engram`）。浏览器设置卡在两代际
都经插件自有 HTTP 路由（`/api/dsh-engram/settings`，见上文 Route B）读写。完整回归
（npm test 396/396，含新增的 volatile-Config 模型 settings-v2 覆盖）在 DSH
`0.2.0-rc.2` 依赖集（cordis 4.0.4 + schemastery 3.18.4 + cosmokit，与全局 DSH CLI 同版本）
上通过；`.volatile()`/loader 热提交路径与 describe 侧投影已针对真实 `dsh-settings
0.2.0-rc.2` 工具函数验证；并在隔离的 `dsh 0.2.0-rc.2` profile 上完成了**真机启动烟测并
通过**：插件启动无版本范围拒绝、设置卡表单以条目 `engram` 对外提供（全部 34 个 GUI
旋钮，含 `trustedHosts`）、`/api/dsh-engram/settings` 的 GET → PUT（mutate ops，
revision 0→1）→ unset 往返落盘并读回。旧的 `0.1.0-rc.7` 与 `0.1.1` API 图不在
本发布版本的兼容范围内。

如需按验证版本安装 DSH：

```sh
npm install --global @deepseek-ai/dsh@0.2.0-rc.2
dsh --version
```

## 安装

```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 转化**，
  外加一块 **Context Ledger** 面板（净 token、节省/注入/重写罚金三卡、近 14 天净收益红绿柱），
  五张小指标卡（累计调用 / 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 占比并调整注入提示。
- **设置 → Engram 记忆 → 「设置」tab** — 配置卡默认展开（DSH 0.2.1 去掉了
  `settings.plugin.item` 槽位，旧的「插件 → 插件配置」位置不再渲染）。同一张卡、同一套传输：
  标题 + 一行说明 + chevron，展开后 ~12 个选项分四组——捕获与检索 / 索引 / 生命周期与 GC /
  安全。改动对新会话生效（已冻结的块保持不变）；Discard / Save，标题栏带「未保存」徽标。
  卡片走插件**自有** `/api/dsh-engram/settings` 路由（不经过 isLoopback 围栏），
  经授权隧道访问 GUI 时也能改。
- **设置 → 插件 → 插件配置 → dsh-engram**（仅 DSH ≤ 0.2.0，旧 `settings.plugin.item` 座位）— 与内置「终端 / Agent 循环 / 网页搜索」同款的
  **默认折叠卡片**：标题 + 一行描述 + 箭头，点击展开/收起；展开后常用 11 项 + 高级分组按
  「捕获与检索 / 索引 / 生命周期与 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    # 381 项单元测试（含 usage 滚动 / /stats 路由 / Context GC / ESR 触发 / BoundaryPrune / Context Ledger / JIT 推送）
npm run eval  # 离线召回 + 结构基准（确定性语料，跑真实 store/recall 路径）
```

另有三个**会话语料分析工具**（零 LLM、确定性，读 `~/.dsh/sessions` 真实日志）：

```sh
node eval/oracle-analysis.mjs             # 浪费源量化：结果重放暴露 / 融合候选 / 机制激活率
node eval/simulate-boundary-prune.mjs     # L0 反事实仿真：边界剪枝的预期节省与偿还门扫掠
node eval/dryrun-boundary-prune.mjs       # BoundaryPrune 全量干跑：真实激活面与节省
```

`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:<ws>]` 来源标记） | 读 |
| `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，opt-in）

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 自动还回默认引擎。**自 v0.6 起默认不接管**：
默认挂裸 `BasicCompactionEngine`（宿主 LLM 摘要），要换成指针摘要需显式打开
`gcReplacesCompaction`（web 预设还需 `autoWebCompaction`）。

> **收缩闸门（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 的装配在**两个平面**，但两个开关都是 opt-in（默认不接管）：

- **host 平面（headless / TUI / base 型 profile）——开箱即用，无需任何配置**：
  主插件 `lib/index.js` 在 `ctx.effect` 里直接
  `mountCompactionEngine(ctx, resolved, { readWorkspace }) → new Engine()` 注册 `compaction` 服务；
  配合插件 patch 禁用基座 `compaction-basic` 行，engram 即为该平面唯一 provider。
  - `gcReplacesCompaction: false`（**默认**）→ 挂裸 `BasicCompactionEngine`（DSH 默认 LLM 摘要），
    **`compaction` 服务永不缺席**；
  - `gcReplacesCompaction: true` → `ContextGcEngine`（机械驱逐 + 重取指针，opt-in）；
  - 开启：profile patch 里给 engram 行加 `config: { gcReplacesCompaction: true }`，或在设置卡打开。
- **preset 平面（web profile）——显式开启后覆盖全部 stock 预设**：web 面把 compaction 放在 agent
  preset 的 isolate realm（shipped 预设自挂 `compaction-basic`），host 条目进不去、profile patch 也
  够不到预设文件（`mountPreset` 的 Include 不带 patches——已对源码核实）。所以插件在启动时
  （`agentPresets` 服务就绪后）**自动改写每个仍处于出厂 stock 布局的预设**——默认预设 + 整张 roster，
  两代预设布局都覆盖：旧版 `<root>/<id>/agent.cordis.yml` 组合文件（DSH ≤ 0.1.x，行在 4 空格）与
  声明式 `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml`（DSH ≥ 0.2.0，行在
  `@deepseek-ai/dsh-agent-preset` 声明里、14 空格缩进；没有出厂文件的用户自定义声明式预设不碰）：
  把 `compaction` 组里的 `compaction-basic` 行换成 `dsh-engram/compaction`。行按 YAML 结构定位
  （带 `group: true` 的列表项）、按 `- id:` 与 `name:` 双重校验，**与缩进列数无关**。这一自动装配是：
  - **默认关（opt-in）**（`autoWebCompaction: false`；设置卡「记忆 GC」可开），开启后幂等——已替换则跳过；
    **关掉的含义**：只是在之后的启动时不再自动接管（headless/TUI/base 的 host 平面不受影响）——
    它**不会**还原之前已接管的预设，要还原用 `npm run web-compaction:revert`；
  - **逐预设判定，只碰 stock 布局**——用户自定义过 compaction 组的预设，以及根本没有 compaction 组的
    预设（如 shipped `minimal`），一律不写、只记日志；
  - **带备份 + 严格校验**：每个文件写前在旁边留 `.engram.bak`（create-only，保留首次
    原件；旧版布局为 `agent.cordis.yml.engram.bak`，声明式布局为 `<id>.patch.yml.engram.bak`），
    写后重读校验，任何一步可疑即回滚该文件；改写失败只 warn，该会话仍用 DSH 默认摘要，
    **永不影响宿主**。
  - **配置真实传导**：写进预设行的引擎配置来自设置卡/配置里的 `gcReplacesCompaction` 与
    `gcNarrative`——改了这两个开关，下次启动会自动把已接管的行刷新成新配置（幂等重写），
    所以**设置卡对 web 平面同样生效**，不再硬编码 true/true。
  - 手动的完全等价物（预览 / 审计 / 卸载前回归，随 npm 包发布的 CLI，自动发现两代预设布局——
    旧版根、出厂声明式预设与上次启动记录的路径；`--presets-dir <dir>` 覆盖声明式目录探测、
    `--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 安装后的第一印象（可感知 / 易引导 / 易配置）

- **半自动（opt-in）**：默认保留 DSH 自己的摘要；在设置卡打开 `gcReplacesCompaction`（host 平面）与
  `autoWebCompaction`（web 预设）后，启动一次即由 `mountCompactionEngine` 接管、并把全部 stock
  预设改写成 `dsh-engram/compaction`。开启后无需再手动改文件。
- **可感知**：插件启动时把权威状态写到 `$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` 说明环境缺依赖，自动回退默认压缩。）

### BoundaryPrune（子任务边界剪枝，默认开 + 账本否决）

Context GC 管「压缩的摘要内容」，BoundaryPrune 管「确定性剪枝的触发时机」——
在 todo 完成推进 / goal 终态这些子任务边界，主动调用 DSH 自带的
`toolResultPruner`（确定性 surface-replace，零 LLM），把早已超阈值的旧工具
结果剪掉头尾保留，省掉它们在后续每个请求里的整段重放。两个开关互相独立。

- **证据链**（[docs/ORACLE-ANALYSIS.zh.md](docs/ORACLE-ANALYSIS.zh.md) +
  [docs/PROPOSAL-resultpack.zh.md](docs/PROPOSAL-resultpack.zh.md)）：97 个真实
  会话实测，工具结果重放暴露 2.77B–7.53B 字符（≈692M–1.88M token），96% 的
  结果字节来自 bash/read/run_code；反事实仿真显示 todo 边界覆盖 79% 的暴露，
  且偿还门在所有配置下提升净收益 12–32%。
- **偿还门**：剪枝会重写前缀（KV 缓存击穿一次），只有当会话预计还要跑
  `boundaryPruneMinRemaining`（默认 50）个以上请求时才 fire——预计长度用
  workspace 历史先验 + 超过后 ×2 外推（有界乐观），长会话不会被挡死。
- **诚实激活面**：goal=0 且无 todo 推进的会话天然不触发（无边界不剪，零副作用）；
  全量干跑 77 会话激活 38%。pruner 服务缺席即自动禁用并打一条日志。
- **边界源**：默认 todo 完成推进 / goal 终态事件；可选**空闲步数边界**
  （`boundaryPruneIdleCadence`，默认 0=关）——覆盖约 62% 从不动 todo/goal 的
  会话（侦察：`eval/recon-idle-boundary.mjs`；反事实：`eval/simulate-idle-cadence.mjs`
  显示 cadence 50 时节省 +85%），仍受偿还门约束。
- **阈值覆盖**：`boundaryPruneThresholdChars`（默认 0=宿主 8192）。设为 2048
  并配更小保留边缘（`boundaryPruneHeadChars`/`boundaryPruneTailChars`）可更早
  开剪——T2048+小边缘在真实语料上首剪节省是宿主默认的 2.4 倍（10.14M vs 4.23M
  字符）。降阈值在 `resultPack` 开启时安全（被剪中段有归档兜底）。
- 默认**开**：`boundaryPrune: true`；`false` 是 opt-out。自适应闸门
  （`boundaryPruneAdaptive: true`，默认开）让 Context Ledger 在该 workspace 真实剪枝后
  净收益为负（≥3 次）时自动停剪——证据只能关、不会自行开。启动日志出现
  `engram boundary-prune (todo @N reqs): pruned …`；被账本否决时出现
  `engram boundary-prune: ledger gate holds for …`。

### ResultPack L2（结果归档 + `result_recall`，默认开）

让 BoundaryPrune 剪枝**无损**：每个被剪工具结果的被移除中段，按与记忆写入
相同的规则脱敏后归档到 `~/.dsh/engram/resultpack/<callId>.json`，并在剪枝
节点尾部追加 handle——`[middle archived: result_recall(<callId>) 可取]`——
模型知道完整结果就在一步之遥。`result_recall(callId, offset?)` 按页返回
归档中段（渐进披露，与 `[ENGRAM]` → `engram_detail` 同一哲学，也是 SoL-Pi
机制③ ObservationPack 的完整形态）。

- **构造性无损**：L1 剪掉的是重放、L2 保留的是可随时取回的原文——「模型回头
  找被剪结果」的失败模式归零。
- **成本受控**：归档在记忆域之外（大块内容永远不会撑大 domain JSON 或
  [ENGRAM] 索引）；保留期 `resultPackRetentionDays`（默认 7 天）按 mtime 清理，
  搭 GC 同一节奏。
- **自我禁用**：只开 L2 不开 L1 将无内容可归档，故不注册工具并打一条警告。
- 默认**开**：`resultPack: true`（与默认开启的 L1 配对，保证默认剪枝无损）；
  关掉后 L1 仍可工作，但被剪中段不可召回。开启时启动日志出现
  `engram result-pack enabled: result_recall(callId, offset) registered`。

### 激活遥测（SoL-Pi M15/C13）

机制必须可证明在激活，休眠机制默认关。`/api/dsh-engram/stats`（以及 GUI
overview）现在返回 `activation: { boundaryPrune, recall }`：

- `boundaryPrune`——BoundaryPrune 真实 fire 的累计持久记录（fires /
  prunedResults / charsRemoved / byKind todo|goal|step / lastFire），每次剪枝
  落盘到 `~/.dsh/engram/boundary-prune.status.json`，与 `resultPack` 无关。
- `recall`——召回漏斗的**注入腿**：实际进过会话的 `[RECALL]` 块数量
  （`recall-injections.json`），与从 session log 已统计的另一端
  （engram_recall 查询 → 命中 → engram_detail 下钻）闭合，可看到
  注入→查询→下钻的完整激活。

### Context Ledger（净收益账本）

把「省 token」变成可核对的账。每个模型请求记一笔：注入的 `[ENGRAM]`/`[RECALL]`
与 `[ESR]` 块按存活请求数计入成本；被 BoundaryPrune 剪掉的结果按后续请求数计入
节省；每次剪枝按 `(1 − ledgerCacheDiscount)` 计一次前缀重写罚金（KV 缓存击穿）。
`GET /api/dsh-engram/stats` 与 `/overview` 返回 `ledger`：

- `totals` — `requests / injectedChars / avoidedChars / rewriteCount / rewriteChars /
  netChars / netTokens`（`net = avoided − injected − rewriteChars × (1 − discount)`）；
- `byWorkspace` / `byDay` — 按工作区、按天的同一口径，`byDay` 倒序；
- `openSessions` — 仍在累计、尚未落盘的会话数（进程重启最多丢 `flushEvery` 个请求的账）。

GUI「遥测」页签直接画这块账本（净 token 大数字 + 节省/注入/重写罚金三卡 + 近 14 天红绿柱），
记忆概览页也多一张「上下文净 token」卡。落盘在 `~/.dsh/engram/ledger.json`（原子写、按天滚动、
delta 落盘），进程内只做整数累加，热路径零 I/O、零模型调用。默认 `ledgerEnabled: true`、
`ledgerCacheDiscount: 0.1`；关掉 = 不挂监听、不写文件。

**节省的两条入账腿**（v0.7.1 补齐，此前只记成本所以净值恒负）：

1. **L1 边界剪枝**——`toolResultPruner` 剪掉的 chars，按后续请求数计入（需要有 host
   平面的 `toolResultPruner`；DSH ≥ 0.2.0 的 web profile 把该行移进 agent preset 的
   isolate realm，本 bundle 的 `cordis.patch.yml` 会重新启用 host 行，状态快照里
   `boundaryPrune.armed` 可核对）；
2. **压缩（Context GC）**——每次 checkpoint 用「被替换区间可见字符数 − checkpoint 字符数」
   入账，host 平面与 web 预设域（经 `engramLedger` 服务桥接）走同一个账本。

**口径诚实**：token 按 `chars / 4` 估算，不含 provider 实际计费与模型质量差；
某工作区还没发生过剪枝/压缩时「节省」就是 0，净值等于注入成本 + 重写罚金（负值正常），
遥测页会直接提示这一点。

### JIT 推送（v0.6-C）

拉取式召回被实证休眠（112 会话仅 24 次 `engram_recall`、8 次 `engram_detail`），所以
v0.6 增加按事件**推送**：三个确定性事件，命中才推、每次一行、每会话最多 3 行（`jitMaxLines`，
每行 `jitMaxLineChars`=220）：

| 事件 | 触发条件 | 推什么 |
|---|---|---|
| 错误复发 | 同一 `cmd:` 签名第二次失败，且已有 error/procedure 记忆 | 该记忆的 id + 一行摘要 + `engram_detail` 下钻 |
| 文件焦点 | 同一文件连续第二次编辑，且有 filePath/dir 锚定记忆 | 优先 preference，其次 fact/decision |
| 目标完成 | goal complete/block 且有 active ESR 任务 | 前两个未闭环任务 + `esr_status` 提示 |

- **append-only**：推送行一旦出现就不再改写/撤回，前缀每次推送只变一次；
  每次推送按 `estimatePrefixChars + 行长度` 记一次重写罚金（Context Ledger 的
  `rewriteCount` / `rewriteChars`），净收益可在遥测页直接看。
- **无命中沉默**：没有可指向的记忆/任务就不推——宁可不推，也不灌噪声。
- **零 LLM、纯规则**；`jitEnabled:false` 一键关，`[JIT]` section 为空时不占任何 token。
- **尚未做**：resume brief（隔天继续）与「预计剩余请求不足时跳过推送」的显式 ROI 门——
  当前靠 3 行硬预算 + 账本观测。

### 会话纪要 + 恢复简报（v0.7-A/D）

- **episode**：会话结束（`session/disposed`）时，用零 LLM 规则把事件日志压成一条紧凑纪要——
  改过哪些文件、跑过哪些命令、哪些失败后修复（`cmd×N→fixed`）、git commit 的书面主题、
  仍挂着的 todo。只在有实质内容时生成（纯 `ls` 会话不写）；**永不进 `[ENGRAM]` 索引**；
  每个 workspace 只保留最近 20 条，超出自动归档。
  数据源全部是已有事件（`tool/call` 的 arguments、`tool/result`、`todo/write`），不调模型。
- **恢复简报**：新会话第一次组装时，取最近 7 天最新的 episode，注入 ≤240 字符的
  `[RESUME]` 行 + `engram_detail <id>` 指针；与 `[ENGRAM]`/`[RECALL]` 一起按会话冻结。
  没有近期 episode 就不注入——隔天继续项目时不用重新交代背景。
- 设置键：`episodeEnabled`、`resumeBriefEnabled`（均默认开），
  `episodeMaxPerWorkspace` / `resumeBriefMaxAgeDays` / `resumeBriefMaxChars` 可调。

### 实体页 + 实体卡（v0.7-B）

`esr_node` 从「图上的点」升级成**可读的页**：对一个实体（package / repo / 服务 / bug…）
聚合当前真相、时间线、未闭环任务、冲突与关系。

- **实体卡（索引内，默认开）**：`[ENGRAM]` 为活跃实体（有锚定记忆或未闭环任务）
  渲染一行，最多 2 张：
  `[ENT] dsh-engram · package · 3 mem · 1 open · truth: … #id · page: engram_detail(entity="dsh-engram")`
  这些实体的平铺记忆行不再重复（真相由卡片承载）；其余实体仍列在 `nodes:` 一行。
- **完整页（按需下钻）**：`engram_detail(entity="<name|id>")` 返回
  `truth / timeline / open loops / conflicts / relations` 五段，带 memory id 与证据缺口；
  没有注册节点但有记忆锚定的名字也会生成虚拟页。
- 纯函数、确定性排序：preference 排在 truth 最前，superseded 旧真相不进 truth，
  冲突按无序对去重（`#a vs #b`）。
- 设置键 `entityCardsEnabled`（默认开）；`entityCardsMax` / `entityCardChars` 可调。

### 可重验证锚点 + 分层 TTL（v0.7-C）

「把过去当真相」是记忆插件最大的可信度问题。v0.7 给带 `filePath` 的记忆加**锚点**：

- 写入时记录 `{path, mtimeMs, size, hash?}`（≤256KB 的文件存 sha256，大文件只存 stat），
  落盘在记忆行的 `anchor` 字段；相对路径按 workspace 解析为绝对路径。
- 会话启动时做一次**有界重验证**（最多 40 条）：文件缺失/改动 → 记忆打 `stale` 标签；
  `[ENGRAM]` 索引与实体页 `truth` **不再自动展示**它，但**不删除**——`engram_detail`
  仍可重取，实体页 timeline 会标 `· stale`。
- **分层 TTL**：`preference` / `procedure` 永久；`episode` 默认 14 天；`error` 默认 60 天；
  其余按 `expireDays`（默认 180 天）。真正删除仍只发生在 GC 的归档（不硬删）。
- 设置键：`anchorRevalidate`（默认开）、`episodeTtlDays`、`errorTtlDays`。
- **未做**：离线「巩固任务」（episode 抽象/合并）——确定性规则只能做 rollup，不是语义抽象；
  在没有证据前不引入，留到 v0.8 与 playbook 一起考虑。

## 自动捕获策略

捕获是确定性、离线的——只看到工具*结果*，从不看对话本身。什么会被记录成一条记忆：

| 工具结果 | 行为 | 信号 |
|---|---|---|
| `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 <id> (full record) · engram_recall <query> (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 <id> · engram_recall <query>

[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 工具结果捕获
    autoCapturePreferences: true  # 零 LLM 用户纠正/偏好捕获（prefer/avoid/switch-to 句式）
    preferenceCapturePerSession: 8  # 每会话偏好记忆条数上限
    boundaryPrune: true        # ResultPack L1：todo/goal 边界剪掉超阈值旧工具结果（默认开）
    boundaryPruneAdaptive: true  # 账本否决：该 workspace 净收益为负（≥3 次剪枝）时自动停剪
    boundaryPruneThresholdChars: 0   # 阈值覆盖（0=宿主 8192；如 2048 可更早开剪）
    boundaryPruneHeadChars: 0  # 覆盖时的保留头部（0=宿主 4096）
    boundaryPruneTailChars: 0  # 覆盖时的保留尾部（0=宿主 1024）
    boundaryPruneIdleCadence: 0   # 空闲步数边界（0=仅事件边界；覆盖无 todo/goal 会话）
    resultPack: true           # ResultPack L2：剪掉的中段脱敏归档 + result_recall 按页召回（默认开，须与 L1 同开）
    resultPackRetentionDays: 7 # 归档保留天数（mtime 清理，走 GC 节奏）
    sessionSearch: true      # engram_recall 也可对历史会话 FTS
    recallScope: workspace   # 召回范围：workspace（严格工作区隔离，默认）| global（可选跨工作区召回，带 [W:<ws>] 来源标记）
    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: false # Context GC：接管 DSH 自动 compact（默认关=宿主 LLM 压缩；true=opt-in 指针摘要）
    gcNarrative: true        # 无锚轮次走 scoped LLM 叙事；false=纯机械（零 LLM）
    jitEnabled: true         # JIT 推送：错误复发/文件焦点/goal 完成时推一行已有记忆
    episodeEnabled: true     # 会话纪要：结束时零 LLM 生成 episode（不进索引，供恢复简报）
    resumeBriefEnabled: true # 恢复简报：新会话注入最近 7 天最新 episode
    entityCardsEnabled: true # 实体卡：[ENGRAM] 为活跃实体渲染一行真相 + 任务 + 下钻指针
    anchorRevalidate: true   # 会话启动重验证 filePath 锚点（改动/缺失 → stale，不进索引/truth）
    episodeTtlDays: 14       # episode TTL（preference/procedure 永久，error 60 天，其余 expireDays）
    ledgerEnabled: true      # Context Ledger：注入成本/剪枝节省/前缀重写罚金 → 净 token
    ledgerCacheDiscount: 0.1 # 缓存折价：重写按 (1−0.1) 计罚金；0.1 = 命中只付 10%
    autoWebCompaction: false   # web 预设自动改写（默认关；开启需显式确认，可用 revert 还原）
    boundaryPruneMinRemaining: 50  # 偿还门：预计剩余请求数低于此值不剪
    engramIndexOrder: 40    # systemPrompt section 顺序（位于 tools 段之前）
    esrOrder: 41
```

## 开发

```sh
npm test            # 381 个测试：核心 + Web API + GC + Context GC + ESR 触发 + Context Ledger + JIT（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
