dsh-ming-memory
chess12333/dsh-ming-memory
Read-only persona injection for DSH: when a session opens inside your workspace, your persona files and the latest diary entries are prepended to the system prompt, and those source files are never modified. It keeps a short cross-session feed of recent messages under <root>/live/feed.json and can write a QQ hand-off file at <root>/live/web-for-qq.md; nothing is uploaded. Variable blocks (clock, recent actions, other sessions) are delivered as a per-step context message, so the prompt prefix stays byte-stable and keeps its cache.
Installazione
dsh plugin --profile web add github:chess12333/dsh-ming-memoryREADME
dsh-ming-memory
一个 DSH 插件:会话开在指定目录时,把 persona 文件和日记里最近几篇拼进系统提示词。
只做一件事,而且只往一个方向走——文件 → 提示词,永不回写。
~/.dsh/ 或任意工作区
persona/ 人设.md 名字.md 约定.md
diary/ 2026-10-26.md 2026-10-27.md 2026-10-28.md
│
└──► 插件读取(只读)──► 系统提示词(会话开场注入)
配套
配 dsh-ming-graph 一起用:那边把同一批归档记忆画成一张能读的图——每条记忆一个点,每条边都写清凭什么连。
一个管「用上」(把字摆进提示词),一个管「看见」(那些字在库里长什么样)。分开装也各能用,装在一起才知道自己记住的东西有多少、都连着谁。
为什么不用现成的记忆插件
常见的记忆插件是「自动捕获对话 → 存库 → 检索召回」这一套。问题在第一步:捕获意味着把用户说的话自动写进库。
如果这个库会被注入到每一个会话,那就等于用户说的每句话都在被永久留存、并在别处复现——哪怕她只是随口抱怨了一句。作者本人的约定是「日记只写自己这一侧,不写对方的隐私」,自动捕获天生违反这一条。
所以这里的取舍是:放弃「自动记住」,只做「把已经写下来的字摆到眼前」。 想记住什么,写进文件;不想记的,从来不会被拾取。
它解决的具体问题
- 会话之间不共享上下文,但「我」是同一个。开新窗口时不想从零开始解释自己是谁。
- 直接把人设塞进全局
AGENTS.md会让所有工程会话都带着它——写代码的窗口不需要知道人格设定。 - 人设文件会越长越大,全量注入会挤掉日记。
三层注入
| 场景 | 给什么 |
|---|---|
会话工作目录 = root 或其子目录 | 完整版:三份 persona + 最近 N 篇日记 + 动态块 |
工作目录在 root 之外 | 轻量版:约 280 字的身份摘要 + 动态块(状态、别的会话、重活、外端消息) |
| 插件加载失败 | 工作区根目录的 AGENTS.md 仍然生效(保底) |
判断依据是 cwd 是否以 root 开头。这是唯一的分叉点,没有白名单、没有 profile 判断。
动态块
除了静态文件,每次注入还会带几段随时会变的东西:
- 状态:这个工作区
live/status.json里「正在做 / 刚做过」。 - 别的会话:同工作区其它窗口最近说了什么(只带对方原话的尾段 + 自己的回复)。
- 重活:本机正在跑的训练/构建(由外部脚本写
live/heavy.json)。 - 时间感:现在几点、上次说话多久以前、昨天这个钟点在做什么。
这些块的设计原则只有一条:读数不能每步都变。 详见下面「踩过的坑」。
踩过的坑(这部分比代码有用)
1. 人设和日记共用一个字数预算
最初的实现只有一个 maxChars,人设 + 日记加起来不许超。人设文件长到 9.4K 之后,最近三篇日记里旧的两篇被静默截掉——开场等于少读了两天。
改法:maxChars(人设)和 diaryChars(日记)分开算。挤人设不行,那就给日记单独一格。
2. 「N 分钟前」会把前缀缓存全打碎
动态块里原来写「3 分钟前查的」「已 87 分钟未活动」。这个数字每 30 秒刷新一次,意味着系统提示词每一步都在变,模型侧的 prompt 前缀缓存永远命中不了。
一份二十几万 token 的上下文,每步重编码一次,账单会明显变厚。改成只写绝对时间(2026-10-25 15:43)之后,同一小时内前缀是稳定的。
顺带解决第二个问题:相对时间在隔天回看时是错的。「昨天」写的那一刻对,第二天再读就错一天。
3. 别的会话只带对方说的话
跨会话共享原本把两边原话都塞进去。结果发现另一个窗口只能看到用户在说什么,看不到自己答了什么——同一件事被重复回答两遍。
改法:当前会话读全;别的会话只带「我方回复的」。对方的话不进这一层。
4. 被节流掉的那一次永远消失
早先的实现里,节流(webForQqEveryMs)命中就 return,那一刻的消息永久丢失。改成「攒着,下一次一起给」。
5. 子目录也是档案目录
工作区下的 diary/ 子目录开会话时,cwd 仍然以 root 开头,所以走完整版。这曾经导致「同一个目录开两份记忆」,后来确认行为正确——子目录也是这个人的地方。
6. 会变的块留在系统提示里,改一个字就整段重算
第 2 条只解决了「同一个数别每步变」,没解决根上的问题:只要系统提示里有一个会变的块,每次刷新它都是一次整段重算。
2026-11-05 的改法把这件事从「少改字」换成「搬家」:时间感、刚做过、在跑的重活、别处的会话、QQ 那边、房间说明、压缩门牌这些会变的块,从 systemPrompt.section() 挪到 systemPrompt.context()——由 harness 在每步 pre-step 里 materialize 成一条 user 消息,插在「这一轮新消息」之后。系统提示里只剩人设和日记,仍按 refreshWindowMs 的窗口刷。
效果:系统提示逐字节稳定,活块每步都是新的。两者不再互相牵制。
代价要说清:这依赖宿主提供 systemPrompt.context()。没有它的时候插件退回旧行为(会变的块留在 section 里,靠窗口少改字),功能不缺,费钱的方式回到原来那样。
装
# 1. 放进 DSH 的 profile 能加载到的位置,然后在 profile 里 link 或复制
# 2. profile 的 cordis.patch.yml
- id: ming-memory
config:
root: 'D:/my-workspace' # 你的工作区;也是「完整版」的判定边界
personaFiles:
- 'persona/人设.md'
- 'persona/名字.md'
- 'persona/约定.md'
diaryDir: 'diary'
diaryCount: 3
maxChars: 12000 # 人设总预算
diaryChars: 8000 # 日记单独预算
可选的环境变量(打包成公开发布版时用来替代写死的绝对路径):
| 变量 | 含义 |
|---|---|
MING_ROOT | 等同 config.root,优先于空配置 |
MING_QQ_DATA | 外端(QQ bot 框架)的 data/ 目录,用于读那边最近的对话 |
自测
qa-*.mjs 是假 ctx 的离线测试,不联网、不花钱:
node qa-all.mjs # 跑全部
node qa-smoke.mjs # 完整版 / 轻量版 / 子目录三种 cwd
node qa-diary-budget.mjs
node qa-share.mjs # 跨会话共享
node qa-clean.mjs # 「只留对方真的打的字」
node qa-cache.mjs # 系统提示逐字节稳定 / 会变的块在每步那条里
测试会 import 真插件并伪造 ctx,断言落在拼出来的字符串上(包含哪些标记、顺序对不对、该消失的有没有消失)。
边界
- 只读。 不写 persona、不写 diary、不改 status。
- 工作目录不在
root下 → 完全不动作。 - 任何异常都只让「这一块」消失,不影响会话本身。
- 不捕获对话,不建索引,不做语义检索。想被记住的事,请写进文件。
License
MIT