Memory
dsh-ledger-memory
18477514055/dsh-ledger-memory · ★ 3
台账记忆(Ledger Memory)—— 给 DSH 一个项目自己的记忆载体:活跃台账每回合自动注入、只追加的回合流水与活动日志(L0–L4,含逐字存档)、关键决策/目标树/经验教训的卡片化记忆,压缩前抓逐字胶囊、压缩后连台账一起注回,把对话锁定到某个项目目录(跨重启保持),以及跨会话的交接单闭环。可回溯、可追查。
README body is shown in Chinese.
Manifest details
安装
dsh plugin --profile web add 'dsh-ledger-memory@1.6.2'从 npm 安装预构建包
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
一个面向 DSH(DeepSeek Harness) 的宿主侧插件,为每个项目建立独立、可回溯、可追查的长期记忆载体。
插件管理的对象并非对话记录,而是项目自身的记忆载体 —— 台账:可追溯、只追加、按周归档。 围绕台账还提供活动日志、上下文压缩与跨会话交接能力。
不预设任何模板。 台账的文件划分、命名与字段结构,全部由模型在初始化时依据项目类型自行确定; 插件仅承担检测、注入、回写、归档与 Git 协作等流程性职责。
解决的问题
| 问题 | 处理方式 |
|---|---|
| 新会话无法接续前一会话的项目进度 | 会话开始时注入活跃台账全文;内容未变更则不重复注入,以避免破坏 prompt 缓存 |
| 单个回合的产出未能沉淀 | 回合结束时将「已完成 / 受阻 / 下一步」写回活跃台账 |
| 台账持续增长、难以完整阅读 | 超出阈值时将最旧的已完成条目移入 归档目录/YYYY-MM/weekN,只追加、不删除 |
| 细节在上下文压缩中被抹去 | 压缩之前将用户原话、报错原文、变更文件清单持久化为逐字胶囊,压缩后与台账一并注回 |
| 新会话缺少承接信息 | 生成交接单,并把指针推入新会话的首轮注入,无需使用者主动查询 |
| 需要核查台账变更来源与时间 | 台账末尾自动维护变更登记;完整历史保存在只追加的活动日志中 |
| 仅为一次性排查、不构成项目 | ledger_note 生成问题记录(现象 → 根因 → 结论 → 处理过程),不建立台账 |
上下文压缩机制
插件默认启用全量压缩;增量(分段)压缩默认关闭,可按需手动开启。
| 阶段 | 触发条件 | 行为 |
|---|---|---|
| ① 保底 · 全量 | 水位达到 70%(内核默认 80%,插件在其之前触发) | 执行全量压缩,同时保留指向原始内容的指针:用户原话、报错原文、变更文件清单与重读入口 |
| ② 收尾 · 交接 | 切换会话时 | 由交接单承载,使新对话无缝接续 |
| ③ 可选 · 增量 | 需手动开启(默认关闭) | 仅压缩最旧一段,削减上涨区间的约一半 |
关于默认关闭的增量压缩:该分支曾为主要路径,现已默认关闭且不再按固定节奏调用。
依据是实测取舍 —— 压缩幅度较浅的摘要大概率会被再次压缩(≤5 万 token 的浅层压缩中 72.7% 会被二次压缩),
层级增加会导致清晰度衰减。因此当前以全量压缩为主路径,单次压缩更深。
deltaTriggerPoints(触发涨幅)与 deltaRemovePoints(单轮最大压缩点数)两项配置仅在启用增量压缩时生效。
压缩遵循一条固定约束:先记录台账,再执行压缩 —— 尚未落盘的进展将成为唯一副本。
该约束由周期判定("自上次压缩以来是否落盘")保证,而非"是否已发出要求"
(后者会使模型被阻塞:历史上曾出现拦截近 4 小时、压缩零次的情况)。
收尾硬门(hardMaintenanceGate)默认开启:未落盘则不压缩;关闭后回退为
"拦 3 次或超 20 分钟即放行"的旧行为。
安装
需要 Node.js ≥ 22.19。
# 走 npm
npm install dsh-ledger-memory
# 或走 DSH 自己的插件命令
dsh plugin add dsh-ledger-memory
装完重启客户端才生效 —— 内核对已加载的插件不会因文件改动自动重载。
升级(★ 如果"界面显示有新版本,却怎么装都是旧的",看这里)
从 1.0.0 起,正常升级不需要做任何特殊操作 —— ^1.0.0 允许整段 1.x,
所以 1.1、1.2 都在范围内,能自己爬上去。
若从更早的 0.x 版本升级上来且恰好受阻,那是两个叠加的原因:
- 依赖被写成
^0.1.0这种旧范围。 0.x 的插入符语义是^0.1.0=>=0.1.0 <0.2.0—— 不允许跨次版本,所以天花板永远是0.1.1。 (1.x没有这个问题:^1.0.0的天花板是2.0.0。) pnpm-workspace.yaml里可能留着一行过期的豁免(pnpm 装成功时会自己往这里追加):
★ 卸载重装治不好它 —— 卸载动不了这个文件。这也正是"反复卸载重装仍是旧版"的原因。minimumReleaseAgeExclude: - dsh-ledger-memory@0.1.0 # ← 这行就是枷锁
修法:
cd <DSH_HOME>/profiles/<profile>
cp package.json package.json.bak # 备份
pnpm add dsh-ledger-memory@1.0.0 # ★ 关键就这一步:按精确版本装
# 然后重启客户端
★ 为什么"按精确版本装"这一步就能破局(实测):
pnpm 会把那一行自己改成 - dsh-ledger-memory@0.1.0 || 1.0.0(追加而非替换),
于是新版本进了候选集,解析就取其中最新的 ⇒ 一步到位。
★ 建议顺手把那行清掉(不是必需,但更干净):删掉
pnpm-workspace.yaml 里 - dsh-ledger-memory@… 那一行,以后就不会再有歧义。
自查(不需要任何额外文件):打开
<DSH_HOME>/profiles/<profile>/pnpm-workspace.yaml,看有没有
- dsh-ledger-memory@ 开头的那一行。有 ⇒ 就是它;没有 ⇒ 问题在依赖范围(上面第 1 条)。
本仓库里另有一个更省事的只读诊断:
node tools/_probe-install-version.mjs。 ⚠️ 它只在本仓库里有、不在 npm 包里(发布包内只有lib/、README.md、LICENSE、cordis.patch.yml与package.json—— 整个tools/目录都不发)—— 所以从 npm 装的用户请用上面那条手工自查。
使用方式
安装后无需额外配置。在一个项目目录中开始工作,插件将依次执行:
- 检测到尚未建立台账时,注入一条提示,要求模型向使用者确认是否初始化(插件不会自行创建);
- 使用者确认后,模型依据项目类型设计台账结构并完成登记;
- 此后每个会话自动加载台账;到达水位阈值(或使用者主动要求)时,插件提示模型回写并归档。
关于「自动」的边界:插件不会自行写入台账或执行归档 —— 它负责在适当时机向模型 注入指令,由模型调用
ledger_write/ledger_archive完成。 这是有意设计:台账内容需要判断(哪些值得保留),而插件不具备该判断能力。 因此模型忽略指令时,该回合不会落盘 —— 插件会在后续步骤再次要求(闸门机制)。 回合末的四选一卡片默认关闭(turnEndOffer),可在设置中开启。
手动命令
在对话里敲 /ledger(不带参数会弹一张可点的卡片),或用子命令:
/ledger status 看状态(有没有台账、用的哪个文件、几行、Git、日志开关)
/ledger show 把当前活跃台账全文打出来
/ledger init 初始化台账(由 AI 为这个项目设计一套结构)
/ledger update 让 AI 把本次会话的进展写回台账
/ledger archive 把最旧的已完成条目移进归档目录
/ledger compact 立刻做一次全量压缩(保留指针)
/ledger light 轻量压缩 · 只压**上一个回复**那一段(★ 需显式使用,不是常规动作)
/ledger log 查活动日志
/ledger handoff 写一份交接单
/ledger principles 看根本原则;`pending` 列提案;`approve <id>` 批准(仅限使用者本人批准)
/ledger mode 看/切换压缩模式(steady / surgical)
/ledger lock 把本场对话锁到某个项目目录(总工作区里用)
/ledger principles是唯一仅限使用者本人执行的子命令:模型可以提议,但批准必须由使用者完成。
会话建立于总工作区时:锁定到子项目
若会话建立在包含多个项目的总工作区上,插件无法判断本次工作针对哪个项目, 默认将全部操作落在当前目录。此时可通过锁定指定目标:
/ledger lock 20-DSH插件/04-其余插件/10.项目台账
锁定后,本场对话的全部工具调用均落在该目录,无需每次单独指定。
/ledger lock 不带参数可查看当前状态,/ledger lock off 解除锁定。
会话首轮弹出的卡片中也提供该选项。
锁定状态会保持:重启或重新打开后依然有效,每场对话独立记录,互不影响。 记录保存在 DSH 自身的数据目录(
plugin-data/)中,不写入工作区: 不创建注册表项,也不在目标目录留下任何标记 —— 项目目录不会因此新增文件。若当前目录自身已有台账,仍以当前目录为准 —— 锁定仅接管"没有台账"的目录。
若锁定的目标目录被删除或移动,该锁定自动失效(回退为按工作目录解析), 后续操作不会指向不存在的路径。
设置面板
设置 → 项目台账(台账 / 压缩 / 日志),共 43 项配置,分为四页:常规、注入、扩展记忆、高级。主要可调项包括:配置优先级、压缩水位、
收尾硬门、回合末卡片、活动日志各档开关、活跃台账行数上限、变更登记保留条数、
水位提醒档位、逐字胶囊的采集范围与 ledger_recall 单次回读上限、文件锚定的权重阈值等。
关于「谁说了算」:该项决定设置面板与项目清单(
.dsh-ledger.json)冲突时以哪一方为准。 默认值为「以设置面板为准」,即面板的修改对所有项目立即生效(含已初始化的既有项目)。 受该项管辖的是 6 项:mode、blockInjection、fileAnchor、maxActiveLines、changeRegisterMax、logLevels;其余配置项始终以面板为准。 若同一代码仓库内不同子项目需要不同上限(例如大型项目 800 行、小型项目 150 行), 可将该项切换为「以项目清单为准」。面板提供「推送到所有对话」操作:执行后所有对话在下一回合重新读取当前设置。
压缩流程:单一方式(分流后压缩)
此前的两种方式(按位置压缩 / 按内容分流压缩)已合并。分流现为每次压缩的标准步骤,
不再是可选项 —— 设置中的 mode 仅为兼容既有项目清单而保留,取值不影响行为。
合并原因:分流(ledger_triage)决定「哪些内容可被压缩、各自如何处理」,
而标记为「留在上下文」的部分会在压缩后逐字注回 —— 这是"指针得以完整保留"的前提。
按位置压缩无法识别哪些内容确实不可丢失。
分流将当前区间划为三个桶:
- 留(
keep):下一轮若缺失即会导致错误的内容(当前状态、未验证假设、既定约束)。压缩后逐字注回。 - 落(
stash):后续可能需要查阅、但不应占用上下文的内容(判断与依据、已排除的路径、关键文件位置)。 写入记忆目录的_stash/目录(V3 下位于<项目名>-ledger-memory/内),永不注入,按需检索。 - 丢(
drop):已被取代的内容,仅记录到日志。
keep 设有条数上限(单次与累积各一项)——保留过多等同于未压缩;
超出时按模型给出的顺序截断,并如实报告截断条数。
分流为每回合义务:每条回复结束前需完成落盘(
ledger_write)与分流(ledger_triage)。 收尾硬门默认开启 —— 未完成时该回合的压缩不会发生(宁可让水位上涨至 78% 触发强制压缩, 也不保留"未落盘即压缩"的窗口)。注入指令的措辞按硬门状态分为两套,二者不自相矛盾。
二十二个工具
以下工具由模型在对话中调用,也可要求模型调用:
| 工具 | 作用 |
|---|---|
ledger_status |
报告这个项目的台账状态(只读) |
ledger_read |
读台账 / 决策记录 / 归档 / 卡片 |
ledger_write |
写活跃台账、决策记录、流水或原则 |
ledger_init |
登记台账结构(四条硬约束:一个活跃台账 / 一份独立决策记录 / 一个只追加归档目录 / 行数上限) |
ledger_archive |
把旧条目移进只追加的归档 |
ledger_conform |
检查既有项目的台账是否符合预期,能修的只做加法 |
ledger_migrate |
反向的那一半:检测旧架构并给出拆分/搬家步骤(op:"split" 拆卡片 · op:"v3" 迁入记忆子目录,两者默认干跑) |
ledger_note |
写一份问题记录(非项目场景,有终点的那种) |
ledger_log |
查活动日志(五档,从不注入上下文) |
ledger_log_write |
追加一条回合小结(L1) |
ledger_recall |
读回被压缩掉的历史逐字原文。★ 两段式:mode:"index" 先给逐事件索引(seq / 类型 / 字数 / 摘要),再用 mode:"events" + seqs 只取点名的那几条;不带 mode 时按 seq 区间整段取(旧行为不变) |
ledger_handoff |
写交接单,供下一个会话接手 |
ledger_triage |
将阶段性产出划为「留 / 落 / 丢」三个桶 |
ledger_lock |
将本场对话锁定到指定项目目录(跨重启保持;记录不写入工作区) |
ledger_goals |
维护目标树(卡片联成树;目标完成时补记一条教训) |
ledger_lessons |
维护经验与教训(经验需注明出处,教训需注明根因) |
ledger_index |
为卡片目录生成多维索引表(融合模型判断、时间、稳定性与星级) |
ledger_extension |
维护自定义扩展记忆(6 种模式,按需挂载项目专属记忆单元) |
ledger_propose_principle |
提议一条根本原则(模型仅可提议) |
ledger_principles_access |
请求使用者开启授权窗口,之后可修改根本原则 |
ledger_compact |
请求执行一次高保真压缩 |
ledger_checkpoint |
将当前选择交由使用者决定(记录台账 / 记录并压缩 / 记录并交接 / 跳过) |
根本原则(
PRINCIPLES.md)仅限使用者本人修改:模型只能提议,或请求开启限时授权窗口 (默认 10 分钟,每次修改消耗一次授权)。该限制为有意设计 —— 根本原则属于使用者的约束,而非模型配置。
记忆的分类与存放位置
判定依据:语义不同的内容分别存放。 在 V3 架构下(清单声明 memoryRoot),所有记忆文件与目录统一存放在 <项目名>-ledger-memory/ 专用子目录中,与项目源码严格物理隔离;V2 项目则保持在项目根目录(向后兼容)。将"当前状态"与"历史流水"置于同一文件会相互干扰
(实测:重构前活跃台账 645 行中 412 行(64%)为日期流水,导致注入内容被历史挤占、行数上限被触发)。
★ 新建项目默认就是 V3。 ledger_init 在全新项目上不传 memoryRoot 时会自动建成第 3 代:
记忆目录取 <项目名>-ledger-memory/,并一并建出卡片三目录(decisions/ lessons/ goals/)与 _ledger-log/
⇒ 项目生而在最新代,不需要事后迁移。两条相关行为:
- 既有项目不会被偷偷升级:判断依据是清单是否为空。重跑
ledger_init补建东西时,原来怎么布局还怎么布局。 - 想用老布局(V2):显式传
layout: "flat"—— 记忆文件直接躺在项目根。
| 文件 | 语义 | 可变性 | 是否注入 | 维护方式 |
|---|---|---|---|---|
LEDGER.md |
当前状态 | 可覆盖 / 精简 / 归档 | 每回合注入 | ledger_write({target:"active"}) |
JOURNAL.md |
回合流水(按天、按会话) | 只追加、不修改 | 不注入(按需查阅) | ledger_write({target:"journal"}) |
decisions/ |
长期决策(一卡一文件) | 只追加,不参与归档 | 摘要见台账 | ledger_write({target:"decisions"}) |
extensions/ |
自定义扩展记忆(6 种模式,注册表 registry.json) |
按模式定义(只追加或可覆盖) | 摘要可选注入 | ledger_extension |
_ledger-log/ |
活动日志 L0–L4(含使用者原话) | 只追加 | 永不注入 | 自动采集;ledger_log 查询 |
_ledger-archive/ |
归档 | 只追加 | 否 | ledger_archive |
_stash/ |
分流中"后续可能需要查阅"的部分 | 只追加 | 否 | ledger_triage 的 stash 桶 |
_handoff/ |
交接单 | 只追加 | 否(仅提供指针) | ledger_handoff |
goals/ |
目标树(卡片联成树) | 卡片可更新 | 会注入 | ledger_goals |
lessons/ |
经验与教训(一卡一文件) | 只追加 | 按需查阅 | ledger_lessons |
PRINCIPLES.md |
根本原则 | 仅使用者本人可修改 | 按需读取 | /ledger principles(模型仅可提议) |
卡片化:核心记忆(decisions/、goals/、lessons/)以及卡片型扩展记忆均采用一卡一文件(文件名如 D195.md / g1.md / L3.md),
使单一大型文件无需整体载入 —— 模型可依编号仅检索所需卡片。
卡片数量增长后,可用 ledger_index 生成多维索引表(模型判断 3.0 / 新近度 1.5 / 稳定性 0.8 / 星级 1.0 融合排序)。
权威数据始终为卡片文件本身,索引表仅为缓存;需要最新排序时重新计算。
「卡片化」与「区块注入」是两条独立的轴
两者均涉及"切分",但切分对象不同 —— 一条决定内容在磁盘上的存储形态,另一条决定每回合推入上下文的内容:
| 轴 A:卡片化(存储) | 轴 B:区块注入(推送) | |
|---|---|---|
| 切分对象 | 文件 —— 单个大文件拆分为多个小文件 | 单次注入的正文 —— 按 Markdown 标题切分 |
| 粒度 | decisions/D195.md(一卡一文件) |
「一/二、总纲」段落(标题至下一标题) |
| 决定方式 | 项目清单中的 decisionsDir / lessonsDir / goalsDir |
设置中的 blockInjection |
| 适用范围 | 仅那三类长期记忆(决策 / 目标树 / 经验教训) | 活跃台账(LEDGER.md) |
| 目的 | 使模型按需仅取一张卡片,无需整体读取 | 使每回合的重注无需整份重推 |
| 关闭后 | 回退为"单文件承载全部" | 回退为"整份台账每回合全量注入" |
因此,"台账文件仍为整体"与"区块注入"并不矛盾:
LEDGER.md 与 JOURNAL.md 有意保持单文件 —— 前者为"当前状态快照"(本应一次读全),
后者为"只追加的流水"(天然线性)。需要卡片化的三类记忆已采用文件夹形式(一卡一文件)。
判定依据:询问"它存储在磁盘何处" ⇒ 卡片轴;询问"本回合向模型推送多少" ⇒ 区块轴。
扩展记忆(6 种模式)
为满足项目特有的记忆需求(游戏机制、专有术语、规范清单等),同时避免污染核心记忆体系,插件支持用户自定义的扩展记忆:
- 存储形态:统一存放在记忆目录的
extensions/下,以registry.json为注册表真源。 · 卡片型(decisions/goals/lessons):extensions/<id>/cards/<key>.md,一卡一文件。 · 单文件型:文件名随模式而定 ——active→LEDGER.md、journal→JOURNAL.md、plain→content.md。 - 六种模式:
decisions(决策型):卡片型,编号即地址(如D1.md)。active(活跃台账型):单文件,可覆盖与精简,适合维护当前业务状态。journal(流水记录型):单文件,只追加、不改旧条目。goals(目标树型):卡片型,一卡一节点,记录目标演进。lessons(经验教训型):卡片型,沉淀经验与踩坑教训。plain(普通独立记忆型):单文件,普通读写,无特殊约束。
- 隔离与防冲突:扩展记忆不得占用核心保留名(
ledger/decisions/journal/goals/lessons/principles),也不得改变基础模板;可通过工具ledger_extension或设置面板的 「扩展记忆」页管理。
为什么没有「原则型」:原则只该有一个出处 —— 核心的
PRINCIPLES.md。 再让使用者"额外扩展一份原则"没有语义,而且它会是唯一需要授权的扩展模式, 那条授权路径本身也得另造一套。故不提供该模式;根本原则仍走ledger_propose_principle(模型提议)→ledger_principles_access(使用者开授权窗口)→/ledger principles approve(使用者批准)。
能力边界说明:
goals/、lessons/以及extensions/均属于可选能力,并非所有项目自动具备。 插件不会擅自创建这些文件 —— 如需使用,应在该项目清单中登记(或由模型征得使用者同意后创建)。 项目未启用时,插件不会自动创建,也不会因此报错。
活动日志(五档)
写入项目登记的日志目录(V3 下位于 <项目名>-ledger-memory/ 内),只追加、永不改写,且从不注入上下文(按需查询):
| 档 | 内容 |
|---|---|
| L0 | 台账变更里程碑 |
| L1 | 回合小结(带会话编号) |
| L2 | 文件流向 |
| L3 | 工具调用流水(默认关闭) |
| L4 | 逐字存档使用者的每一句话(带会话编号与时间) |
采集类档位仅识别会话的工作目录,不会推断子项目。 在包含多个项目的目录中开会话时,采集类档位无法写出内容 —— 此行为为有意设计。
依赖与许可
- 零运行时依赖(
dependencies为空) - 唯一 peer 依赖:
@deepseek-ai/cordis - 许可:MIT
自检(仅存在于源码仓库,不随 npm 包发布)
安装包内不包含自检。 发布包的
files白名单仅含lib/、README.md、LICENSE、cordis.patch.yml、package.json—— 整个tools/目录不发布。 原因:tools/内含一批会改写源码的探针(变异测试),将其随包发布存在风险; 且自检脚本本身约 1.14 MB。此发布边界为有意设计,并非打包遗漏。 因此,从 npm 安装的副本中执行npm test会指向不存在的文件 —— 此为预期行为。
在源码仓库中(git clone 之后)执行,无需网络、无需安装 DSH:
npm test # 全套离线自检
npm run probes # 只读探针 + 变异测试
已知边界
- 注入通过
agent/pre-step实现(可替换本轮消息),不使用systemPrompt.context。 - 修改代码后需重启客户端;内核对已加载插件不进行热重载。
- 采集类日志档位在"容器工作区"(包含多个项目的目录)中无法写出内容。
- 压缩服务仅可通过
agentPresets.serviceFor(agent, "compaction")取得,ctx.get("compaction")恒为undefined。 - V3 记忆目录的 Git 隔离由三层共同负责:
.gitignore规则 · 插件内部的结构性拒绝暂存(V3 项目一律不自动git add)· 目录与源码物理分离。记忆目录内的SECURITY.md只是给模型的安全准则,不构成技术隔离,也不应被当作唯一防线。