<div align="center">

  <h1>DSH金字塔记忆</h1>

  <p>
    <img src="https://img.shields.io/badge/%E6%97%A0%E9%99%90%E8%AE%B0%E5%BF%86-ff6b35?style=for-the-badge" alt="无限记忆" height="90" />
    <img src="https://img.shields.io/badge/%E5%9B%BA%E5%AE%9A%20TOKEN-2ea44f?style=for-the-badge" alt="固定 TOKEN" height="90" />
    <img src="https://img.shields.io/badge/DSH%20%E5%8E%9F%E7%94%9F-3178c6?style=for-the-badge" alt="DSH 原生" height="90" />
  </p>

  <p>dsh生态，零依赖，即插即用<br/>通过本地对话文件，生成记忆金字塔，自带寻址<br/>给 <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness (dsh)</a> 补上带时间轴的、无限记忆<br/>Agent自维护，一万条记忆，开局仍最多只读 96 行，固定token</p>

  <p>
    <a href="#快速开始"><strong>快速开始</strong></a>
    ·
    <a href="#金字塔记忆结构"><strong>金字塔记忆结构</strong></a>
    ·
    <a href="#roadmap"><strong>Roadmap</strong></a>
  </p>

  <p>
    <img src="https://img.shields.io/static/v1?label=License&message=MIT&color=blue&style=flat-square" alt="License: MIT" />
    <img src="https://img.shields.io/static/v1?label=Node&message=%E2%89%A519&color=green&style=flat-square" alt="Node >= 19" />
    <img src="https://img.shields.io/static/v1?label=Dependencies&message=0&color=brightgreen&style=flat-square" alt="Zero dependencies" />
  </p>

  <p><sub>Fable 5 辅助</sub></p>

  <p>中文 | <a href="README.en.md">English</a></p>

</div>

---

装上之后，模型每次自动注入“金字塔”记忆；攒多之后，视图自动压成金字塔形：

```
### Memory view (40 facts)

#0-31   摘要：这段时间在打通插件加载链路，结论是...
#32-35  摘要：注入改走 systemPrompt 的动态上下文...
#36-37  摘要：数据目录改落工作区，避免家目录被运...
#38     2026-08-15 摘要树按块大小分层存储，待办...
#39     2026-08-15 定宽记录不存序号：位置即身份...
```

越靠近现在越是原文，越老越粗——但**原文一条都没丢**，随时可以下钻回去。

这是人这边看到的同一份东西：

<img src="assets/panel-full.png" alt="记忆看板" width="900" />

## 金字塔记忆结构

每记满两条事实，agent 就继续往上层进行摘要总结一次；两行这样的话再凑满一对，就写一行更粗的。每往上一层块数减半，形状就是一座塔：

```
                  [ #0-127 ]                      ← 1 行 · 代表全部 128 条
            [ #0-63 ]    [ #64-127 ]              ← 每行代表 64 条
      [#0-31] [#32-63] [#64-95] [#96-127]         ← 每行代表 32 条
   ········································
  #0 #1 #2 #3 ··················· #126 #127       ← 塔基：128 条逐字原文
```

开局那份视图，就是沿着塔**斜着下楼梯**：去年的事读塔尖一行，上个月的隔一块中层砖，昨天的直接站在塔基逐字读。塔基永远不动——粗砖只是地图，`memory_zoom` 两行就下到原文。所以记忆涨十倍，开局要读的行数一行不多。

**形状来源**：金字塔形状参考自 Victor Taelin 的 [OptMem](https://github.com/VictorTaelin/OptMem)极简记忆理念。真正实现在 dsh 里自己探索、摩擦出来的，目前在致力于更加完善的记忆机制-**（记忆与对话事实链接、面板、后台并行）**——见后续路线图。

## 常见记忆机制比较

|                                          | 切哪根轴 | 记的是                               |
| ---------------------------------------- | -------- | ------------------------------------ |
| `agent-instructions`（AGENTS.md 门规链） | 空间     | 在哪个目录下该守什么规矩             |
| 检索式记忆（RAG 一类）                   | 相关性   | 跟当前问题像的东西                   |
| **dsh-memory-pyramid**                   | **时间** | **先后发生过什么、当时为什么那么定** |

三轴正交、可以叠着用。官方目前只占了空间轴，**时间轴整根空着**——这就是本插件的落点。

目前仅串行，agent自主决定工具调用，**没有任何后台进程**。

**记忆看板**（金字塔式的直观可视化，人也看得见自己的记忆）与**回溯**（点一条记忆跳回产生它的那段对话，看到记忆的全生命周期）已经做完。下一步是**并行后台结算**：写记忆交给专职分身，主线一个字的注意力都不付。

## 特性

**金字塔机制**

- 事实一行 ≤280 字节，只追加、永不修改；摘要由 agent 记录时顺带维护——无后台进程、无定时器
- 固定阅读预算：装得下就一点不压缩，装不下越老越粗、**从不截断**
- 摘要写坏有正式重画通道（`memory_forget`），并连坐丢弃由它推出的更粗摘要

### 🔧 dsh 原生适配

- **原生 dsh 插件积木**，不是外部命令行，脚本——不需要 shell 权限
- **注入通道是拿真实账单选出来的**：视图放系统提示词会打爆 prompt cache，改走官方动态上下文通道后整场命中率 40%→91%（下文有对账表）
- **会话区间锚点**：每条事实记下它蒸馏自哪段对话（`sessionId` + seq 区间）——dsh 有全量会话流水这份原料，这是 OptMem 的运行环境里不存在的能力
- **官方AGENTS.md的机制依旧生效**
- 数据目录认领 / 自动搬迁 / 拒绝拼接——宁可不工作，也不在别人的目录上写字

**工程性质**

- **零 npm 依赖、零原生模块**，Node ≥ 19
- Windows / Linux / macOS / ARM64
- 多进程共写同一份记忆**无锁且正确**——靠减掉一个字段，不是加一把锁
- 数据落工作区、纯文本、对 diff 友好，可建 git 进一步管理

## 快速开始

### 系统要求

- 已安装 DeepSeek Harness，`dsh web` 可正常启动。
- 已安装 pnpm（dsh 用它装插件，Windows / Linux / macOS 同要求）：`npm install -g pnpm`
- 从仓库安装另需 git；插件本体零依赖、无构建步骤，Node.js ≥ 19。

### 三步上手

1. 安装：`dsh plugin --profile web add dsh-memory-pyramid`
2. 重启 `dsh web`（profile 启用了 HMR 时会直接热生效，这步可省；没见到再重启）
3. 新建会话——开局即见 Memory view，模型开始自动记忆

### 从 npm 安装（推荐）

一条命令，装完即激活——包自带 `dsh.bundle` 声明，dsh 自动把它加入 profile 层栈，**一个配置文件都不用改**：

```sh
dsh plugin --profile web add dsh-memory-pyramid
```

也可以把这条命令直接交给 dsh agent 替你执行。

### 从 GitHub 仓库安装（开发调试）

零依赖、纯 ESM、无构建步骤；`link:` 装的是引用，之后 `git pull` 即更新：

```sh
git clone https://github.com/33moren33/dsh-memory-pyramid.git
dsh plugin --profile web add "link:/绝对路径/dsh-memory-pyramid"
```

### 验证与卸载

装好重启 `dsh web`，新建会话开局出现 `### Memory view` 段就是生效了；也可以用 `dsh --profile web --dump-config` 确认 `dsh-memory-pyramid` 已进合成树。启动日志里还会打印一行数据目录认领信息。

卸载：`dsh plugin --profile web remove dsh-memory-pyramid`，然后重启 `dsh web`。

想改配置（如 `wakeLines`）：在 profile 的 `cordis.patch.yml` 里对同一个 id 覆盖，热生效：

```yaml
- id: memory
  config:
    wakeLines: 192
```

**实际注入**（真实会话实拍，模型不用任何工具，直接凭注入的视图作答）：

<img src="assets/memory-view.png" alt="实际注入" width="640" />

## 常见问题

**装插件时报 `pnpm not found` / 装 pnpm 时报目录没有写权限？** dsh 靠 pnpm 管理插件，先 `npm install -g pnpm`。写权限报错常见于 Linux 用系统包管理器装的 Node——装到用户目录即可：`npm install -g pnpm --prefix ~/.local`，并把 `~/.local/bin` 加进 PATH。

**记忆到底落在哪？** 落在**你在 dsh 里打开的那个工作区**下的 `dsh_memory/`。在界面里换一个工作区，记忆也跟着换——一个工作区一个库，跟 dsh 按工作区分会话是同一个道理。

**换了工作区，记忆怎么空了？** 多半没丢，是另一个项目的库。每个工作区各记各的，这是设计不是故障：A 项目的经历不该出现在 B 项目的上下文里。想让某个工作区读固定那一份，在配置里把 `dataDir` 填成绝对路径——它压过一切。

**peer 警告 `@deepseek-ai/cordis missing` 要紧吗？** 不要紧。cordis 只被类型注释引用、没有运行时 import，dsh 自己带着 cordis。v0.1.1 起已去掉这条声明；旧版本看到可直接忽略。

**会话开到一半新记的东西，视图里怎么没有？** 默认就是这样：视图只在会话开局注入一次（省注入 token），会话内新写的记忆在工具回执里、模型看得见，新开会话即见全部。想要实时更新，配置里把 `liveView` 设为 `true`。

## 配置

| 键           | 默认   | 说明                                                                           |
| ------------ | ------ | ------------------------------------------------------------------------------ |
| `namespace`  | 无     | 公共区名字。填了就落在 `<工作区>/<名字>/dsh_memory`。只收单层目录名。          |
| `dataDir`    | 无     | 完全自定义路径（绝对，或相对工作区根）。填了就**压过** `namespace`。给绝对路径时还会压过工作区——所有工作区读同一份库，想要「一个大脑」时用它。 |
| `migrate`    | `true` | 换落点时，自动把工作区里找到的旧记忆整体搬过来。                               |
| `wakeLines`  | `96`   | 记忆视图的行数预算。**这是阅读预算不是存储预算**——随时改，一块摘要都不用重算。 |
| `injectWake` | `true` | 是否把记忆视图注进开局上下文。关掉后工具照常可用，只是不再自动出现。           |
| `packs`      | 无     | 再多挂几个别的记忆库到看板上当参照，形如 `[{name: "示例", dir: "路径"}]`。**一个字节都不写**：不记它的用量、不补它的摘要、不给它渲染注入视图。插件已自带四个示例库（见上文），这一项是给你挂自己的。 |
| `liveView`   | `false` | 会话中途记忆变化时是否实时更新视图。默认关：视图只在会话开局注入一次，会话内新写的记忆走工具回执、新会话开局可见全部。开了之后每次记忆变化都会给会话追加一份最新视图（多花注入 token）。改配置热生效，当前对话下一条消息起按新开关走。 |

默认落在 `<当前工作区>/dsh_memory`（不写家目录）。`LOG.txt` 纯文本只追加——进 git 团队可共享一份经历；`TREE/` 是纯缓存，删了不丢事实。目录已存在时：标记对得上就认领（一条不动），对不上就拒绝启动并说明原因。**两份记忆绝不会被拼接**（记录靠位置寻址，拼接会让所有摘要整体指错），找到多份旧记忆时拒绝启动、交给人裁决。

## 工具面

| 工具               | 干什么                                                   |
| ------------------ | -------------------------------------------------------- |
| `memory_note`      | 记一条事实。一行，≤280 字节，只追加，永不修改。          |
| `memory_summarize` | 交摘要树的维护费：某一块凑满时，写下将来代表它的那一行。 |
| `memory_zoom`      | 把任意 `#a-b` 节点打开成它的两个半块——下钻的便宜路子。   |
| `memory_recall`    | 用正则扫全部事实（`from`/`to` 可夹住范围），或读某段原文。命中被截断时如实报出总数。每条都带会话区间锚点。 |
| `memory_open`      | 顺着一条事实的锚，交回它蒸馏自的那份原文全文。           |
| `memory_forget`    | 丢掉一块写坏的摘要，排队重写。**只动地图，不动领土。**   |

## 记忆看板

装上之后界面右下角多一个悬浮球，点开是占右半屏的看板——**记忆第一次成了人看得见的东西**。

一座真的塔：一块砖就是一块摘要，最底下一排是逐条事实。亮着的那些正在注入给模型，与模型此刻收到的视图**同一份数据、同一套算法**，不存在第二个真相。颜色只编码一件事——热度，也就是这条记忆被用到的频次。

<img src="assets/panel-tower.png" alt="塔的全貌：越往上越粗，橘环是此刻正注入的那些" width="740" />

越往上砖越少、每块管的事越多，塔尖一行代表全部。**橘色描边的那几块就是此刻进了上下文的**——一眼看得出模型开局到底读了哪些。

拉近看，每块砖上写着它代表的那段：

<img src="assets/panel-zoom.png" alt="拉近之后砖上带着摘要文字" width="740" />

底下一条时间轴标出这些记忆横跨的时间，拖得动、缩得放；塔太高时纵向也拖得动。

右下角是当场生效的旋钮：注入行数、每条字节、冻结/实时。

看板走官方给第三方留的正门挂载；没有 web 界面的场景（如 headless）自动缺席，记忆本体照常工作。

### 点一条记忆，回到它的出处

点一块砖读它的正文。点最底排的一条事实，可以顺着它的出处走回去：

- **出自对话的**，跳回那段对话——每条事实落盘时记着 `sessionId` 加一段序号区间，所以框住的是**完整的一整轮**，不是一个点。
- **出自导入文本的**，直接打开那份原文全文。

没有出处的按钮是灰的，不会假装有。

### 自带四个示例库，装完就有得看

塔在几十条和几千条上**完全是两回事**——几十条时整库原文全进上下文、摘要层一个块都用不上；上千条时才看得见那座分层的塔。可是刚装上插件的人手里一条记忆都没有。

所以插件随包带了四个现成的库，**50 / 100 / 500 / 1000 条**各一个，装完打开看板就能在左上角切着看，不用配置、不用下载、不用往你的工作区里拷任何文件。换工作区也照样在。

切过去看到的东西**和看自己的库完全一样**：同一座塔、同一套卡片、同一份算法算出来的记忆视图。差别自己会跳出来——50 条那个库，视图是 51 行逐字原文，摘要层一块都用不上；1000 条那个，视图 97 行、开头就是 `#0-63` 这样的粗砖。**两边都卡在 96 行预算里**，「记忆涨 20 倍、开局要读的行数一行不多」这句话到这里就不用再解释了。

它们**全部标着 ⚗ 合成示例**：里面的正文是《西游记》，时间戳与热度是算法生成的，不是谁的真实使用记录。这个标记写在库自己身上，拷到哪里都跟着，**不会被当成真账读**。

示例库只读——看板读它们的时候一个字节都不写。也正因为它们住在插件的安装目录里而不是你的工作区里，面板上没有卸载按钮。**在示例库上「回到出处」还不通**：它们的对话原文虽然就在包里，但没有登记进 dsh，点了不会有反应；在你自己的库上是正常的。

还有一档一万条的，体积太大不随包发，后续放进 Releases 按需下载。想挂任何别的库都是同一个办法：**扔进 `<你的工作区>/dsh_memory/packs/` 就行**，看板会自动认出来，或者在看板上直接填路径。

## 把已有文本导进记忆

塔有三层：**基底**是原料，**事实**是从原料里蒸出来的那一行（≤280 字节），**摘要**是事实合并而成的粗砖。基底不限字数——一段对话是基底，你手上任何一份文本也可以是。

记忆目录下有个 `memory_handoff/` 文件夹，**把文件放进去就算上架**——没有写入工具，文件夹本身就是入口。md 只是最顺手的格式，纯文本都行。

上架不等于入库：文件放进去只是成了原料，要有人读它、写下一条事实，它才真正进塔。**一份原料对一条事实，严格一对一**，所以顺着任何一条事实都能唯一地回到它的出处。

写下的每条事实都带着「出自哪一份」的锚，`memory_open` 顺着锚交回原文全文：一行事实说不清的细节，随时回得去。

上架时记下字节数与指纹。文件事后被改过，打开时会当场点破，而不是照吐给你。

## 它怎么做到不随记忆量变慢

1. **定宽记录 ⇒ 位置即身份。** 第 N 条永远住在第 N×384 字节，读一条是一次 `pread`。记录里不存序号——顺带让多进程并发追加天然正确。
2. **摘要树也定宽，每层一个稠密前缀文件。** 「这层做到哪了」＝文件长度 ÷ 288，一次 `stat`。待办是**推导出来的**，没有可以失同步的队列。
3. **压缩工作量恒定。** 小块（≤16 条）读原文压；大块只读两个半块的摘要。写第 10 层和写第 1 层一样便宜，旧摘要永不返工。
4. **阅读预算固定，从不截断。** 二分搜索找到恰好塞进预算的粗细；装得下时一点不压。改预算不重算任何摘要。

### 不打断 prompt cache

**纪律段（静态）住在系统提示词里；记忆视图（写一条就变）住在动态上下文里。** 分界只有一条：保证上下文缓存命中。

### 摘要会错，这是设计的一部分

塔尖是多代传话游戏，语义会漂移。所以原文永存（地图坏了不等于领土沉了）、`memory_forget` 是正式重画通道、摘要缺失时视图就地拆细直到落回原文——绝不显示一块不存在的摘要。

## Roadmap

- [x] v0.1 串行版：五件工具、区间锚点、缓存安全注入、OptMem `TREE/` 字节级兼容
- [x] v0.1.1 注入开关：视图默认只在会话开局注入一次（`liveView` 可换回实时更新，热生效）
- [x] **记忆看板**：金字塔层级视图 + 时间轴，点一条记忆跳回产生它的那段会话，自带四个示例库可对照
- [x] **文本入塔**：把已有 md 等任意文本放进 `memory_handoff/` 当基底，事实带锚、随时回得去原文
- [ ] **注入通道完善**：增量动态注入——按每个会话已见过的水位，只注入它没见过的部分
- [ ] **跨工作区统一大脑**：多个工作区的库合并成一个视图看
- [ ] **金字塔教程**：配流程图与截图，讲清楚这座塔怎么建、怎么读、怎么错、怎么修
- [ ] **并行结算**：写记忆交给专职分身，主线零打扰；空闲时扫未认领的对话流水补漏
- [ ] OptMem `LOG.txt` 迁移转换器（`TREE/` 可原样搬）

## 已知边界

- 多进程共写支持且无锁；但**子 agent 不该写记忆**（它看不见已记了什么），目前靠提示词约定，未在代码强制。
- **一个工作区一个库，多个库还不能合成一个视图看。** 想跨项目共用一份记忆，目前只能把 `dataDir` 填成同一个绝对路径。
- 早期版本，注入形态还在演进（见 Roadmap 第一条）。**完整的可靠性结论，留给后续实际测试与 issues 反馈来检验**——发现问题请开 issue。

## License

本项目基于 [MIT 许可证](./LICENSE)开源。
