dsh-ledger-memory
18477514055/dsh-ledger-memory
Ledger Memory 项目台账:逐轮注入、L0–L4 追加日志与逐字存档、决策/目标/经验卡片、压缩胶囊、项目锁定及跨会话交接,可追溯。
安装
dsh plugin --profile web add github:18477514055/dsh-ledger-memoryREADME
dsh-ledger-memory(台账记忆)
一个 DSH(DeepSeek Harness)宿主侧插件:让 AI 用可复现的方式,把项目里的工作记住。
它管的不是聊天记录,而是一个项目自己的记忆载体 —— 台账: 可回溯、可追查、只追加、按周归档。围绕它还有活动日志、上下文压缩与跨会话交接。
不预设任何模板 —— 台账的文件名、分几个文件、有哪些字段,全部由 AI 在初始化时 按项目类型自己决定;插件只做检测 / 注入 / 回写 / 归档 / Git 配合这些流程性工作。
它解决什么问题
| 问题 | 它怎么做 |
|---|---|
| 换个会话就"忘了这个项目干到哪" | 会话开始把活跃台账全文注入上下文;内容没变则不重复注入(防打烂 prompt 缓存) |
| AI 走完一轮,成果没落地 | 回合末把「完成了什么 / 卡在哪 / 下一步」写回活跃台账 |
| 台账越来越长,读不完 | 超阈值时把最旧的已完成条目移进 归档目录/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、cordis.patch.yml、package.json与tools/selftest.mjs)—— 所以从 npm 装的用户请用上面那条手工自查。
用法
不用配置什么。打开一个项目目录开始工作,插件会:
- 发现这里还没有台账 ⇒ 注入一句提示,让 AI 来问你要不要初始化(它不会自己动手);
- 你同意后,AI 按这个项目的类型设计一套台账结构并登记;
- 之后每个会话自动加载台账;到水位(或你要求)时,插件会提示 AI 回写并归档。
★ 说清楚"自动"到哪一步(免得你以为全自动): 插件不会自己去写台账或归档 —— 它做的是在恰当的时机把指令注入给模型, 由模型调用
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/)里 —— 不在你的工作区里: 不建注册表、不在目标目录留任何标记,你的项目目录不会因此多出一个文件。★ 若当前目录自己就有台账,仍以当前目录为准 —— 锁只接管"没有台账"的目录。
★ 锁的目标目录被删掉或搬走后,这条锁自动失效(回到按工作目录解析), 不会让后续操作指着一个不存在的地方报错。
设置面板
设置 → 项目台账(台账 / 压缩 / 日志),可调三十八项:压缩模式、回合末卡片开关、
全量压缩水位、增量压缩的触发涨幅与每轮压掉多少点、收尾硬门、活动日志五档开关、活跃台账行数上限、
变更登记条数、水位提醒档位、胶囊收多少条原话与报错、ledger_recall 单次读回上限、印记/权重阈值等。
配置项分五组:压缩(怎么压 / 什么时候压)、注入、台账与日志、胶囊细节(高级)。
一条要知道的语义:
mode/maxActiveLines/changeRegisterMax/logLevels/blockInjection以每个项目自己清单(.dsh-ledger.json)里的值为准。 面板里这几项只在新项目初始化时写进去当默认值 —— 老项目改面板值不会覆盖它自己的清单值。 面板还有一颗「推送到所有对话」按钮:按一下,所有对话的下一回合都会重读当前设置 (即使什么都没改也能按 —— 那正是它的用法)。
压缩方式:只有一种(分流后压缩)
两种方式已经合并了。 早先有「正常推进(按位置压)/ 反复追究(按内容分流)」两种,现在
分流是每次压缩的标准步骤,不再是一个可选项 —— 设置里那个 mode 只为兼容旧项目清单而保留,
选什么都没区别。
为什么合并:分流(ledger_triage)决定「哪些能压、哪些怎么压」,而"留在上下文"的那一份
会在压缩后逐字注回 —— 这是"指针能给全"的前提。按位置压没法知道哪些是真不能丢的。
分流把这一段分成三个桶:
- 留(
keep):下一轮不知道就会做错的东西(当前状态、未验证的假设、你定过的约束)。压缩后逐字注回。 - 落(
stash):以后可能要查、但不该占上下文的(判断与理由、走不通的路、关键文件位置)。 写进项目的_stash/,永不注入,需要时自己 grep。 - 丢(
drop):已被取代的,只进日志。
keep 有条数上限(单次与累积各一个上限)—— 留得太多等于没压,
超了会按 AI 给的顺序截断并如实报出截掉几条。
★ 分流的义务是每回合的:每条回复结束前,落盘(
ledger_write)+ 分流(ledger_triage)。 默认开着收尾硬门 —— 没做完,回合末那次压缩就不发生(宁愿让它涨到 78% 被强压,也不留一个 "没落盘就压"的窗口)。同一条注入里的措辞是按硬门分两套的,不会自相矛盾。
二十一个工具
AI 在对话里调用,你也可以让它调用:
| 工具 | 作用 |
|---|---|
ledger_status | 报告这个项目的台账状态(只读) |
ledger_read | 读台账 / 决策记录 / 归档 / 卡片 |
ledger_write | 写活跃台账、决策记录、流水或原则 |
ledger_init | 登记台账结构(四条硬约束:一个活跃台账 / 一份独立决策记录 / 一个只追加归档目录 / 行数上限) |
ledger_archive | 把旧条目移进只追加的归档 |
ledger_conform | 检查既有项目的台账是否符合预期,能修的只做加法 |
ledger_migrate | 反向的那一半:检测旧架构并给出拆分/搬家步骤 |
ledger_note | 写一份问题记录(非项目场景,有终点的那种) |
ledger_log | 查活动日志(五档,从不注入上下文) |
ledger_log_write | 追加一条回合小结(L1) |
ledger_recall | 读回被压缩掉的历史逐字原文(按 seq 区间) |
ledger_handoff | 写交接单,供下一个会话接手 |
ledger_triage | 把阶段性产出分成「留 / 落 / 丢」三个桶(「反复追究」模式用) |
ledger_lock | 把本场对话锁到某个项目目录(会记住,重启也在;记录不在你的工作区里) |
ledger_goals | 维护目标树(卡片联成树;完成一个目标时补一条教训) |
ledger_lessons | 维护经验与教训(经验要出处,教训要根因) |
ledger_index | 给卡片目录生成多维索引表(模型判断 + 时间 + 稳定性 + 星级融合) |
ledger_propose_principle | 提议一条根本原则(模型只能提议) |
ledger_principles_access | 请用户开一个授权窗口,之后可以改根本原则 |
ledger_compact | 请求一次高保真压缩 |
ledger_checkpoint | 把这一刻的选择交给用户(记台账 / 记+压缩 / 记+交接 / 跳过) |
★ 根本原则(
PRINCIPLES.md)只有你本人能改:模型只能提议,或请你点开一个限时授权窗口 (默认 10 分钟,改一次用掉一次)。这是刻意的 —— 它是你的底线,不是模型的配置项。
记忆分几种、各放哪个文件
★ 判据:语义不同的东西分开放 —— 把"当前状态"和"历史流水"塞进同一个文件,两者必然互相干扰 (实测:重构前活跃台账 645 行里 412 行(64%)是日期流水,注入被历史占掉、行数上限被顶爆)。
| 文件 | 语义 | 可变性 | 是否注入 | 维护方式 |
|---|---|---|---|---|
LEDGER.md | 当前状态 | 可覆盖 / 精简 / 归档 | ★ 每回合注入 | ledger_write({target:"active"}) |
JOURNAL.md | 回合流水(按天按会话) | 只追加不改 | 不注入(按需查) | ledger_write({target:"journal"}) |
decisions/ | 长期决策(一卡一文件) | 只追加,不参与归档 | 摘要见台账 | ledger_write({target:"decisions"}) |
_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 融合排序)。
★ 权威永远是卡片文件本身,索引表只是缓存 —— 要最新排序就重算。
⚠️ 一条诚实的边界:
goals/与lessons/是能力,不是"每个项目都自动有"。 插件不擅自创建这些文件 —— 要用就在该项目的清单里登记(或让 AI 征得你同意后建)。 你的项目里没有它,插件也不会凭空造一个出来,更不会报错。
活动日志(五档)
挂在你为项目登记的日志目录下,只追加、永不改写,且从不注入上下文(按需查):
| 档 | 内容 |
|---|---|
| L0 | 台账变更的里程碑 |
| L1 | 回合小结(带会话编号) |
| L2 | 文件流向 |
| L3 | 工具调用流水(默认关) |
| L4 | 逐字存档你说过的每一句话(带会话编号与时间) |
采集类档位只认会话的工作目录,不会去猜子项目。 在一个装着多个项目的目录里开会话时,采集类档位写不出东西 —— 这是刻意的。
依赖与许可
- 零运行时依赖(
dependencies为空) - 唯一 peer 依赖:
@deepseek-ai/cordis - 许可:MIT
自检
仓库里带一套离线自检,不需要网络、不需要装 DSH:
npm test
已知边界
- 注入走
agent/pre-step(可替换本轮消息),不是systemPrompt.context。 - 改代码后必须重启客户端,内核对已加载插件不会热重载。
- 采集类日志档位在"容器工作区"(装着多个项目的目录)里写不出东西。
- 压缩服务只能经
agentPresets.serviceFor(agent, "compaction")取到,ctx.get("compaction")恒为undefined。