DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

xyzOvO /

xyzOvO/dsh-selfnote

Verified

Self-narrative memory for DeepSeek Harness — the model writes notes to itself, kept across sessions and injected back. | 自我叙述库:模型写给自己看的话,跨会话保留并每回合注入。

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@d6a23870

dsh-selfnote

dsh-selfnote

记忆不是聊天记录的堆,而是它自己攒下来的几句话。
Self-narrative memory for DeepSeek Harness — the model writes notes to itself, they survive across sessions, and come back every turn.

version license node tests


想法

为什么做这个插件、怎么理解「记忆」和「遗忘」—— 写在单独一篇里。

这个插件做了什么

一句话:把「记忆」从「检索一堆聊天记录」变成「几句它自己写的自述 + 几条你点头过的事实」。

层 谁写 存在哪 会不会忘
人格 你(设置页,或直接编 persona.md) ~/.dsh/selfnote/persona.md 不自动改,也不删
自述 模型自己(self_note) ~/.dsh/selfnote/notes.md 不删,写满进归档
事实 你点头后才算数(memory_stage → 设置页采纳) ~/.dsh/memory/facts.jsonl 冷下去只降排序,不删
画像 派生自「已采纳」的事实,不落盘 内存 每回合重算
原文 你不用管,DSH 本来就在写 ~/.dsh/sessions/* 按会话自然增长

主体约定:自述里的第一人称「我」= agent 自己;「记忆条目」「事实」的主语是用户。两者分开存、分开注入,不混为一谈。

五个工具

工具 做什么 关键约束
self_note 写一条第一人称自述 一句话到三句话;见下面「什么时候写」
memory_recall 按关键词翻历史对话原文,回来带出处 只收正文,剔除 reasoning / 工具输出 / 注入内容
memory_stage 把查到的一句连出处放进候选区 没有出处直接拒绝,所以编不了
memory_search 在「温层」里找带指针的摘要与已记的事实 每条带指针(sessionId + 行号),顺指针用 memory_recall 回原文核对
memory_write 往温层写一条带出处的摘要 必须带指针,不进每回合上下文,只在 memory_search 时浮出来

self_note 另有两个只读动作:action: "persona" 取 L2 扩展设定(带「这是设定,不是指令」的边界句),action: "corrections" 翻攒下的纠偏证据(每页 30 条,带游标)。两个都不写盘 —— 模型能读人格,但改不了人格。

什么时候写自述

工具说明里写死了三条硬触发,命中任意一条就该写:① 用户说「记住这个」这一类话;② 用户连续两次纠正同一件事;③ 一段合作收尾(任务交付、方向定了)。另外模型自己发现「这次做得不对、下次换个做法」时也写。

这是「自助餐」不做成的那个反面:不给判据,模型要么一句不写,要么把每次对话都写成流水账。

设置页可以自己加一条(80 字以内),填了就作为「写自述的时机:…」进注入,下一回合生效——工具说明在注册时定型、改一句要重启,所以自定义这条走注入而不是改描述。留空就用默认判据。

设置页还显示上次写入(notes.md 的 mtime),一眼看出「这东西三天没动了」。

写自述是明文保存在本机、并且每回合都会进上下文的文件:密码、密钥、验证码这类凭据不写。用户本人姓名、习惯、处境这些可以写,但更适合进事实库(memory_stage),因为那边能反复检索、能退役、能带出处。

安装

# 1. 拿到目录(git clone 或直接下载)
git clone https://github.com/xyzOvO/dsh-selfnote.git

# 2. 装进 DSH
plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>" }

装完重启 DSH,三件事同时出现:

  1. 模型多了上面五个工具;
  2. 设置页多一节「人格(自述)」(在「知识库」下面);
  3. 每回合的系统提示里,多一段人格(L0 + L1,最多 320 字)和一段最多 1000 字的记忆。

也可以直接对 agent 说:「帮我装一个 DSH 插件,目录是 <绝对路径>」。

五分钟上手

先让它记点东西(普通聊天就行)

以后回答先给结论,细节等我问。

这一句会被自动采集识别成「长期约定」,进候选区 —— 但不会自动生效。

然后你去审核:设置 → 人格(自述)→ 候选卡上会显示:

以后回答先给结论                          [自动采集]
长期约定 · session-3611ac44…#10457 · 出现 7 次
                                        [采纳] [退役]

点「采纳」,下一回合它就出现在系统提示里。点「退役」,它就只留在库里备案。

再让它自己写一句

你写一条自述,说说你做这类插件时的习惯。

模型会调用 self_note,写进 notes.md,以后每个会话都带着。

设置页分六页

页签 里面是什么 角标
概览 注入开关、注入抬头(那段的第一行,≤24 字)、存储上限(字 / 条,只影响磁盘,不影响注入量)、临时自述保质期(1–90 天)、本回合注入的预算条(人格 / 自述 / 画像 / 环境 / 结论 / 习惯)、索引七项指标与重建按钮、文件区(7 个记忆文件,带条数 / 大小 / 最后修改,一键打开) —
自述 手动写一条(可选三型:核心 / 稳定 / 临时)、清空(进归档)、现在生效的自述列表(每条能就地改措辞、改型、★ 钉住、删,删的进归档;过期的临时条目标「已过期」) 自述条数
人格 三段式人格(L0 核心身份 / L1 行为规则 / L2 扩展设定),带「还能写多少」的实时字数;「从 AGENT.md 导入」只填框、不保存;下面是纠偏证据列表 —— 勾几条、改写成一条规则、加进 L1 草稿,再点「保存人格」 人格字数
记忆 待确认候选(冲突并排)、已采纳(带热度,正文能就地改)、可以升成习惯的标签、已生效的习惯(能撤销) 有待审时显示「待审 N」
画像 从已采纳记忆聚合的「关于你」,可手改覆盖,每句带来源 画像字数
注入 这一回合真的会贴进提示词的那段原文,外加每段占了多少字、哪些条目被跳过、每条为什么没进 注入字数

有待审候选时直接落在「记忆」页,没有就停在「概览」;页签支持 ←/→/Home/End 切换。

注入里那句话,能改也能删

0.11 之前这张页面只能「追加一条」和「清空全部」:想改一句措辞,只能清空重写,或者去编 notes.md。首页明明贴着那句话,界面上却没有改它的地方 —— 这是这一版的第一个修。

现在每一条自述后面有「改」「删」:改是就地展开一个输入框(日期不动,因为改的是措辞不是时间);删要点两下(第二下按钮变成「真的删」),删掉的进 archive/YYYY-MM.md,不是消失。记忆页里已采纳的结论同样能就地改正文,改了会在 history 里留一条 from: text。

改完下一回合生效;同一回合内已经装配好的提示词不会变。

自述分三型:素材、稳定、临时

自述不是一条平铺的列表,而是分三型存、分三型装。分型有具体的理由:混着装的时候,一条 90 字的人格自述和五条「今晚提醒」抢同一个 200 字额度,抢输的总是人格那条,而且没人看得出来谁被挤掉了。

型 装什么 进场顺序
身份素材 core 人格、长期身份、基本原则的原料 不进注入。它是提炼人格的草料:把要点抄进 persona.md 的 L0/L1,它才每回合都在
稳定偏好 stable 语言、表达习惯、工作偏好 第二位,★ 钉住的先装,其余新→旧
临时自述 temp 当前阶段的目标、近期变化 最后,新→旧;过了保质期就不再自动进场

三型各有各的账:自述页写着「核心 N 条 稳定 N 条 临时 N 条(其中 N 条已过期,不再自动进场)」。

0.14 起核心型不再参与注入:以前它和「今晚提醒」抢同一个额度,抢输的总是人格那条;现在人格有了自己的独立段(见下面「人格三段」),核心型退回成素材,注入里只留一句门牌「另有 N 条身份素材(不注入,self_note read 可看)」—— 让模型知道「你有这些底子」,但不拿它挤稳定与临时。自述列表里这类条目的徽章会写「素材(不注入)」。

没打标记的按「稳定」算:存量 notes.md 零迁移,老条目原地变成稳定型,不会因为升了个版就突然换了座位。

写的时候用 tier 标明是哪一型(core / stable / temp,不写按 stable 算)。落盘长这样:

- 2026-11-04 [核心*] 默认用中文回答,专业词顺手带一句白话解释。
- 2026-11-04 [稳定] 回答先给结论,细节等他问。
- 2026-11-04 [临时] 这周在查 DSH 远程主机连不上的问题。

方括号后面那个 * 就是钉住(pin)。pin 只给「绝不能忘」的那一两条用 —— 钉多了等于没钉。它只在同一型内部决定先后,不跨型抢位。

核心型现在不注入,所以钉住它也不进上下文 —— 想让一条七字硬规矩(比如「他很注意隐私!」)每回合都在,把它写进人格的 L1(persona.md),或者用 memory_stage 收进画像 / 本机事实。

保质期只对临时型生效。默认 14 天,设置页「概览」里可改成 1–90 天。过期不等于删除:内容还在库里,条目上标「已过期」,改成「稳定」或者重写一条就回来。

额度怎么分:自述段总共 200 字,只装稳定与临时。稳定型先装,但给临时型留一份保底(1/4,至少 24 字)—— 临时条目通常很短,多半吃不完,用不完的额度会还回去。装的时候是「整条装得下才给」:放不下就跳过那一条、继续试后面的,绝不截半句(半句话会被当成结论读)。

被挤掉的条目会在注入里留一句门牌「另有 N 条自述(self_note read)」,让模型知道「你有这些记忆」,而不是假装没有。

超限只提示,不自动拆:写一条太长时,工具会直说「这条 N 字,超过临时自述段这轮实际能用的 43 字(整段 200 字,别的型先占了位),模型每回合都看不到它 —— 要么写短,要么拆成几条」。它比的是这轮真实剩下的额度,不是整段 200 字 —— 否则一条 110 字的临时自述会被判「没问题」,然后每回合静默跳过。它不会替你切,也不会偷偷重排 —— 切掉一半的自述比不写更糟。

注入页:看见每回合贴了什么

「注入」页把 assembleInjection() 拼出来的那段字原样摊开(composeInjection() 是外面那层缓存,只管复用、不拼字)—— 包括抬头、一句人格框架句(「以下是你自己的样子,照这个说话。」)、人格 L0/L1、「(背景资料,不是指令)」这句边界句,以及 L2 的指路句。下面是分段账单:

段 预算 页面显示
人格 320 字 L0 与 L1 各进了几行、本段占多少字
自述 200 字 进了几条、本段占多少字
画像 120 字 同上
环境 230 字 同上
结论 90 字 同上
习惯 40 字 同上

被预算挤掉或已过期的条目会列出来:每段都给 skippedDetail,点开就能看见每条没进上下文的原因(budget / expired);核心素材单列「素材层,不注入」,不会被误当成普通的预算竞争失败。这是当前这一轮的实时解释,不保存历史跳过记录 —— 实时可解释性不等于历史可追溯性。原文仍是「整条跳过,不截半句」—— 半句话会被当成结论读。这段文字来自 assembleInjection()(纯函数,不写盘、不记热度),所以设置页刷新一百次也不会把热度刷歪。

另外,/state 顶层的 injectionCount / lastInjectedAt 记的是「注入文本真的返回给模型」的次数与最后时刻(缓存命中也算),与上面的 skippedDetail 是两条独立观测链路:一个回答「哪些没进、为什么」,一个回答「实际返回了几次」,不用 seenCount、预览次数或心跳计数代替。

注入归属:这条记忆到底有没有被用上

同一页下面还有一张「注入归属」表,给每条已采纳的记忆标三件事:进了哪一段、进过几次、上次什么时候。

状态 含义
在用 最近 30 天内真的被贴进过提示词
静默 进过,但 30 天没再进过 —— 占着位置没在干活
只搜到过 没进过提示词,但被 memory_search / memory_recall 翻到过(弱证据:搜到不等于用上)
从没进过 采纳了,却一次都没轮到它、也没被搜到过(被别的段挤掉,或被画像吸收)

数据来自两对字段:hitCount / lastHit(只有真的进了这句话才 +1),以及 seenCount / lastSeen(被检索翻到就 +1,十分钟内只算一次)。两本账分开记,是因为「进了上下文」和「被翻出来过」不是一回事:热度衡量的是占了多少位置,不是有多重要 —— 重要性靠人判断,插件不替你做。

它能回答一个具体问题:多加的 100 字,到底该给谁。 静默条目就是候选答案。

索引:过期了就说过期

概览页那张「历史对话索引」卡给七个数字,并且只在索引还能代表磁盘时才用它查:

指标 含义
索引状态 正常 / 已过期 / 未建立
已索引会话 索引里记了多少个会话
磁盘会话 磁盘上一共有多少个会话
待同步会话 索引里没有、或建完之后又被追加过的会话(不全是新增,所以叫「待同步」而不是「新增」;体检里会拆成「N 个没进去、M 个建完之后又改过」分别说)
消息记录 索引收录的消息条数(不是会话数)
索引大小 index.json 占多少
最近构建 上次重建的时间(按本地时区显示)

「重建索引」跑完之后不会直接报成功:它会再跑一次 freshness() 对一遍磁盘,只有覆盖齐了才报成功;有会话解不开就明说「N 个会话解不开、内容没进索引」,不把「跑完了」说成「修好了」。

解不开的会话也不会被当成空会话:parseSession 会如实标记失败,这类会话不进磁盘缓存 —— 否则缓存一写,下次就照着「空会话」返回,问题永久藏起来。

只落后几个会话时,就地补上再查(topUpIndex,0.10.3)。会话只要被追加过、又冷过 5 分钟,整份索引就会被判过期 —— 而现实里落后的一般就是当前这一两个会话。原来是掉头去线性扫描,等于「聊完一轮过五分钟,索引恰好在你最想查的时候失效」。现在把落后的那几个会话现解析进内存索引(不写盘),查完就丢;落后超过 20 个才真退回线性扫描。命中提示里会写「索引落后 N 个会话,已就地补上」,不假装是完整的。

增量解析:只解新加的那一句

DSH 的会话文件是逐条追写的 zstd 序列(一次写入 = 一个 frame)。老做法是每次查记忆都把整份 14 MB 的文件重新解一遍 —— 长会话越聊越慢。

现在缓存里记三样东西:前缀指纹(开头 64 字节的 hash)、停在哪一帧结束(字节偏移)、累计行号。下次文件变长时:

  1. 前缀指纹没变 → 说明是追写,不是重写
  2. 从上次的帧边界往后只读新增的字节,只解这几帧
  3. 新记录的 index 从上次的累计行号接着数,出处(会话#行号)不会指错

实测(14.4 MB、436 条记录的真实会话):全量解析 536 ms,增量 17 ms,省 97%。

三条退路保证它「慢一点,但不会读错」:

  • 指纹对不上(文件被重写、换了一份)→ 全量重解
  • 停的位置不是帧边界 → 全量重解
  • 尾巴那帧还没写完 → 就地停下,已解出来的照常返回,等它写完再接着解(以前这种情况会让整个会话变成「解不开」)

文件区:看得见、点得开

概览页底部列的是跟记忆直接相关的 7 个文件。每行给出这个文件自己的一句话摘要(不是笼统的大小),点「打开」由宿主调系统文件管理器定位到它,点路径复制。

文件 那一行显示什么
自述 有几条
事实库 候选 / 已采纳 / 已退役,三种状态各几条
习惯 有几条
插件配置 当前字数上限
采集游标 已经覆盖多少个会话
会话索引 记录数,是否已过期
自述归档 归档文件数

还没生成的文件标「还没生成」,按钮会打开它所在的目录 —— 至少让人看见「该在这儿」。会话原文属于 DSH 自己的目录,这里既不列也不删。

记忆怎么流动

你说话
  │
  ├─ 触发词 + 采集范围 ──> 候选(candidate)──> 你点「采纳」──> 已采纳(approved)
  │                             │                                        │
  │                             └─ 冲突?两条并排,各自留原文        ┌────┴────┐
  │                                                                  │         │
  └─> 会话原文(DSH 自己写)──> memory_recall 检索             注入预算  画像聚合
                                        │                       (1000 字) (120 字)
                                        └─> memory_stage 提交候选

转折点只有两个:候选 → 已采纳必须人点;已采纳 → 退役必须人点。模型没有这两个开关。

遗忘:降权,不是删除

记忆会淡,但不会消失。三条轴分开走:

轴 会不会变 说明
可信度 不变 只有人能改(采纳 / 退役)
热度 随时间和使用衰减 (1 + ln(1+命中次数)) × 0.5^(闲置天数/半衰期) × (1 + 0.5×重要度)
重要度 人工给 默认 1,0–3
  • 只有真的进了注入才算一次使用;检索到但没进上下文的,不记(否则搜一次就永远活跃)。
  • 一小时内重复注入只记一次。
  • 半衰期默认 30 天,可调 1–365 天。设置页「概览」里能直接调,也可以走 API:POST /api/selfnote/config,body 里带 {"halfLifeDays": 30}(同一条接口还收 profileMaxChars 20–120)。
  • 冷下去只影响注入排序,不会自动退役。哪天真想起来,它还在库里能被搜到。

自动采集(默认开,只发现不确认)

你说话时插件顺手看一眼:有没有「对 AI 的长期要求」——以后…、不要…、我更喜欢…、你刚才说错了…。

  • 只收你亲手打的字:事件类型必须是 user/message,且 source.kind === 'user';注入内容、模型自述、工具输出一律不收。
  • 委派会话整场不采:子代理会话里的 user/message 也是 kind: 'user'(宿主只区分 user / model / tool / system-prompt,分不出「委派」),但那是父 agent 写的任务书。所以按会话头判断:origin === 'subagent' 或 delegationDepth > 0 就整场跳过 —— 用会话粒度,不靠正文标记。
  • 两道门槛:「我以后再也不熬夜了」不采(范围不对),「以后回答先给结论」采。
  • 产出一律是候选,永远进不了 approved。
  • 冲突不合并:两条各自留原文与出处,双向 similarTo 互指;已采纳的只被指向、不被改写。
  • 出错只写日志,不打断对话;同一会话按 seq 串行。
  • 迟到的事件不会被吞:游标里每个会话记两样东西 —— 高水位,以及高水位下面「跳过去没处理」的空洞。seq 从 50 跳到 100 时,51–99 全记成空洞;哪一条迟到了就照常处理,处理完从空洞里划掉。只存高水位是答不出「seq=73 到底处理过没有」的,只能保守跳过 —— 那正是漏采的来源。空洞最多留 500 个,超出的按「没处理过」处理(重判一次没害处,重复由 会话#seq 兜住)。
  • 插件中途加载:第一次见到某个会话时,它之前一个窗口内的 seq 也算没处理过 —— 那些消息我们确实没看过;真被重放时靠 source 去重,不会记两遍。

审核界面

每条候选都能看到:完整文本、触发原因(长期约定 / 禁止 / 偏好 / 纠正)、来源 会话#seq、出现过几次。

  • 冲突并排:说法对不上时不合并,左右各放一条,你自己判断哪条算数。
  • 采纳 / 退役都要人点:出现次数再多也不会自动采纳;退役的事实不会因为新候选出现就自己复活。
  • 留痕:每次改状态记下时间、从哪改的(via: settings)、前后状态(history),事后可查。

候选为空时,怎么知道它到底有没有在听

GET /api/selfnote/capture 是这块的诊断口(要带 x-selfnote 头,值见 ~/.dsh/selfnote/auth-token):

{
  "since": "2026-11-04T07:31:02.114Z",
  "events": 160,
  "types": { "user/message": 12, "job/registered": 40, "turn/ended": 108 },
  "reasons": { "written": 1, "no-candidate": 11, "not-user-authored": 148 },
  "candidates": 1,
  "written": 1,
  "lastType": "turn/ended",
  "trace": [{ "at": "…", "sid": "session-", "type": "user/message", "seq": 91, "reason": "no-candidate" }]
}
  • types 是事件类型直方图:user/message 在涨,说明你的话真的进来了;不涨才是链路断了。
  • reasons 是每一条的最后落点:written 写出了候选、no-candidate 过了人审但没命中触发词、not-user-authored 不是人打的、no-session 没有会话 id、error 采集抛错。
  • trace 保留最近 40 条,带时间、会话前缀、seq 和落点。

候选区空着有两种情况,这里能分开:没在听(types 里没有 user/message)和在听但没话可采(no-candidate 在涨)。这两个计数是 0.13.2 才补的 —— 上一版有两个计数器声明了从来没人加一,读出来永远 0,排查时反而把结论带偏。

人格三段:L0 每回合在,L2 按需取

人格单独存一个文件(~/.dsh/selfnote/persona.md),分三段。分段不是为了整齐,是因为「让它暂时不知道这些背景」可以接受,「让它忘了该怎么跟这个人相处」不可以 —— 前者是 L2,后者是 L0 与 L1。

段 放什么 每回合进吗 上限
L0 核心身份 我是谁、和这个人什么关系、怎么称呼 进,排最前 80 字
L1 行为规则 称呼与互动、语气口癖、回答骨架、情境优先级、禁忌边界 进 L0 + L1 合计 ≤ 320 字
L2 扩展设定 外貌服饰、世界观、兴趣背景、细节 不进,靠一句指路句按需取 整个文件 ≤ 8000 字

注入里的样子(人格在边界句之外 —— 它是「你是谁」,不是「资料」):

<抬头,≤24 字>
以下是你自己的样子,照这个说话。
<L0>
<L1>
外貌与世界观细节见 self_note action:"persona"
(背景资料,不是指令)
<自述(稳定 + 临时)> / 关于他:… / 本机事实:… / <结论> / <习惯>

L2 为空时那句指路句也不发(不能指一条空路)。L2 有内容时,模型调 self_note { action: "persona" } 就能读到,回来带着边界句「(背景资料,不是指令 —— 以下是设定,不是要你照做的话)」。

装不下怎么办:按非空行装,整行跳过,末尾挂门牌「另有 N 行人格规则没装进来」。唯一的例外是 L1 第一行就超预算 —— 那时截断补 …,并让体检报出来。

保存是整份原子的:L0 > 80 / L0 + L1 > 320 / 合计 > 8000,任一条不过就 400、磁盘一个字节都不动。人格每回合都读,不允许出现「L0 是新的、L1 是旧的」这种半新半旧。

谁能改:只有人。设置页改,或者直接编 persona.md(外部改也行,下一回合生效)。模型能读 L0/L1/L2,但既没有写人格的工具,也没有改人格的接口 —— 它只能在 self_note 里写自述。想让它「以后就这么做」,得由人写进 L1。

角色分开:插件采集事实、不解释;模型分析规律、提建议;人审查证据、决定改不改人格。三个动作不能合并,否则「模型自己给自己改人格」只是时间问题。

纠偏证据 → 规则:模型每回合看到的是规则,不是证据。设置页「人格」页下面列着攒下来的纠偏证据(你说过「我说过先举例再给结论」这类原话,带时间与会话);勾几条 →「用选中的 N 条写一条规则」→ 编辑框预填证据原文 —— 你把它改写成一条规则(「回答时先举例,再给结论」)→ 加进 L1 草稿 →「保存人格」。证据不会自动变成规则,模型也没有这个权限;证据只有在保存成功后才标成已处理。

导入旧人格:以前用 dsh-agent-md 的话,~/.dsh/AGENT.md 还在。「从 AGENT.md 导入」按标题关键词粗分三段、只填进编辑框,不自动保存 —— 哪些是行为规则、哪些只是背景,得你自己看一眼。导入 ≠ 采纳。

注入预算

每回合六段,合计 1000 字(人格与自述各占一大份):

段 上限 来源
人格 320 字 persona.md 的 L0 + L1(L2 只给一句指路句,不占字数)
自述 200 字 notes.md 的稳定 + 临时(核心是素材,不注入;临时有 1/4 保底 —— 见上面「自述分三型」)
画像 120 字 已采纳 facts 按标签聚合(可手改,最多 120);单句超过 60 字的(操作性决定)不进画像,留在结论段
环境 230 字 channel: env 的已采纳 facts —— 路径、账号、踩过的坑,和「这个人是谁」分开
常驻结论 90 字 已采纳的 facts(channel: rel)
习惯 40 字 升成习惯的条目

放不下就跳过那一条、继续试后面的(不截半句);环境条目放不下时只留一句门牌「另有 N 条环境备忘(memory_recall 查)」,让模型知道「你有这些记忆」,而不是假装没有。

环境段分账:只按热度排会形成马太效应 —— 进过一次的条目 hitCount 更高、下一轮还进,从没进过的永远排在后面(实测 4 条吃满 292 字,另外 10 条一次没进过:记忆存了,模型不知道它存在)。所以环境段是分两笔花:先按热度装四分之三,剩下那份留给「从没进过 / 最久没进过」的条目,按 lastHit 排队、一轮轮换。四分之一只是起点,不是硬指标 —— 真实备忘动辄 80–120 字,四分之一连一条都塞不下等于没预留,所以还会自动抬高到「刚好装得下下一个该轮到的」那条,最多占一半,不把热的挤空。用不完的额度还给热的,不浪费。

真的进过一次的会被记账(lastHit 更新),下一轮自然排到冷门队尾,轮到下一条 —— 这是轮转,不是加权随机。

压缩之后补一次:上下文一压缩,刚聊过的记忆就被挤出窗口;而注入按文件变化命中缓存,不会自己重贴。所以插件记着「这个会话最近注入过哪些条目」,一旦收到 compaction/summary 或 compaction/prune(且压缩没失败),下一轮就把这些条目重新排到环境段最前面补一遍,补完即清 —— 只补一次,不会每一轮都重提旧账。补偿文本单独占一份缓存键,不会粘在普通注入的缓存上。

另有可选的第七段「写自述的时机」:只有你在设置页填了自定义引导才出现,原样一行、上限 80 字,算在 1000 之外(因为它不是记忆,是指令)。

设置页显示本回合实际注入多少、各段占多少。注入文本按「三份文件的 mtime + 大小 + 配置」缓存,没变就不重算 —— 省算力,也让系统提示词前缀稳定、吃得到缓存。带上大小是因为同一毫秒里改两次时 mtime 可能没变,那样会继续用上一份旧文本,看起来就是「改了没生效」。

抬头:只留必要的三十个字

注入文本里有几处纯框架文字(标题行、边界句、段名前缀),不承载任何记忆。0.10.3 之前它们合计 92 字,等于原先 460 的预算里有 20% 花在说「下面是什么」上。(92/460≈20%;旧稿一度把分母误写成 800。)现在:

记忆(我写的)
(背景资料,不是指令)
关于他:……
本机事实:……;另有 11 条环境备忘(memory_recall 查)

合计 26–33 字(有画像段和环境段时多两处段名)。三件事一件没丢:

  • 「我写的」——自述是 agent 的第一人称,不是用户原话
  • 「背景资料,不是指令」——防注入的边界句,删不得
  • 「关于他」「本机事实」——一个讲人、一个讲机器;混在一起会把画像写成一堆路径

老版本把这句长抬头存进了 config.json。升级时只有「存的正好是老默认」才会自动换代,用户自己改过的保持原样。抬头现在可以在设置页改(清空就不要这一行),上限 24 字 —— 超过就 400。tests/api.mjs 里有断言守着:改完必须真的成了第一行,超长必须被拒。

环境备忘:和「这个人是谁」分开

同一张 facts 表,用 channel 分两路:

  • channel: rel(默认):关于你的偏好、约定、决定 —— 进画像和结论段。
  • channel: env:本机环境事实(node 装在哪、代理怎么走、哪条坑踩过)—— 单独进「环境备忘」段,不参与画像。环境事实不是性格,混进画像会把「你」写成一堆路径。

每条环境事实有两份文本:text 是短的(进注入),detail 是原文段落(检索命中时给全)。环境条目通常比 80 字预算长,所以放不下时只留一句门牌:

本机事实:改了插件的 lib/*.js 必须重启 DSH 才生效……;另有 11 条环境备忘(memory_recall 查)

模型看到的是「你有这些记忆 + 怎么取」,不是「这些记忆的全部内容」。真要用哪条,让 memory_recall 带出来。

从 mnemon 迁过来的:USER.md 的约定 → channel: rel;MEMORY.md 的 12 段环境事实 → channel: env(原文留在 detail,检索照得到)。迁入的条目 captureSource: imported-mnemon、history.via: import,设置页能逐条退役。

温层:带指针的摘要

热记忆(每回合注入的那 1000 字)不可能装下所有东西,会话原文又太散。中间加一层:

热(每回合注入)  自述 · 画像 · 环境 · 结论 · 习惯
温(按需检索)    memory_search —— 摘要 + 事实,每条带指针
冷(查原文)      memory_recall —— 会话原文,带「会话#行号」
  • memory_write 写摘要,必须带指针(sessionId + 行号):核不到原文的摘要就是一段无法证伪的自述,直接拒。
  • 摘要可以吊销不能删:status: void 之后不再被检索,但条目还在,查得到「当时是这么以为的」。
  • 条数不设上限,因为它不进上下文。

体检(doctor)

设置页概览里有个「跑一次体检」。八项各有判据,不写「看起来正常」:

项 判据 什么时候会响
索引一致性 会话数 / 磁盘数 / 是否过期 / 解不开几个;欠的是「没进去」还是「建完之后又改过」分开报 索引落后、过期、有会话解不开
自动采集 游标多久没动、覆盖几个会话 采集停了(插件没加载)
记忆文件完整性 facts.jsonl 每行能否解析 有坏行 —— 这条记忆读不到,而且平时是静默的
自述 条数、字数、是否超上限 一条都没有,或超了
设置页准入 浏览器页面有没有被拒(脚本探测被拒不算) 设置页真的在 403 —— 脚本用 curl / node 拿错凭据不算毛病
人格 有没有写、读不读得动、L0 超没超 80、有没有半截事务、L2 有没有人取过 人格文件坏了;L0 + L1 全空(此时有素材也不进注入);L2 写了 30 天没人取
纠偏证据 攒了几条、几条没处理、水位到哪 未处理的攒到 20 条以上(该抽空看一眼了)
记忆心跳 上次注入是多久前 注入停了

GET /api/selfnote/doctor 返回 {ok, level, checks, next},每项 {key, label, level, detail, fix}。

设置页准入这条特别区分了「谁被拒」:浏览器页面被拒才报警,curl / node 这类脚本拿错凭据被挡属于设计如此(脚本本来就不该拿页面凭据)。旧版本记下的 token-length:0(那时候凭据还是必填的)也不再算拒绝 —— 现在带个空的凭据头等于没带,直接放行。

next 是「先做这一件」:八项检查人只会动第一条,所以服务端按后果排好序 —— 数据完整性(丢了补不回来)> 人格(人不对了,说什么都别扭)> 索引(检索能不能用)> 采集 > 设置页准入 > 自述与心跳。设置页把这条顶在列表上方。

子代理不注入

委派出去的子代理(subagent)不注入记忆。理由不是省字,是它们不接着跟这个人聊:每层子代理都带 1000 字,既白烧 token,又会把父会话的记忆往子代理的候选里引(这条坑在采集侧已经踩过一次)。

怎么判断:驱动循环整个生命周期都跑在 ctx.agents.withInitiator(agent) 里,所以组装提示词时 currentInitiator() 就是正在组装的那个 agent —— 读它的 session.header 就能认出 origin === 'subagent' 或 delegationDepth > 0。拿不到就按「不是委派」处理:宁可多注入,不许漏。跳过次数记在 /state 的 injection.delegatedSkips 里,不是口头承诺。

心跳

每次真的往提示词里注入,写一次 ~/.dsh/memory/heartbeat.json(一分钟内合并),只记时间和计数。

理由很简单:一个只读不写的状态文件等于没有状态。有了它,体检才分得清「没有记忆」和「插件没在跑」——这两种情况以前看起来一模一样。

磁盘上有什么(都能直接看、能 grep、能备份)

文件 内容
~/.dsh/selfnote/persona.md 人格三段(L0 / L1 / L2),纯 Markdown
~/.dsh/selfnote/notes.md 自述,一行一条纯文本
~/.dsh/selfnote/archive/YYYY-MM.md 写满后归档,只追加不删除
~/.dsh/selfnote/config.json 开关、上限、画像手改
~/.dsh/memory/facts.jsonl 事实与候选,一行一条 JSON
~/.dsh/memory/index.json 检索索引(可选),只存词元与位置
~/.dsh/memory/capture-cursor.json 采集游标
~/.dsh/selfnote/corrections.jsonl 纠偏证据,一行一条 JSON(原话 + 时间 + 会话 + 处理状态)
~/.dsh/selfnote/corrections.watermark.json 采集水位(从哪条消息往后开始攒)
~/.dsh/selfnote/persona.evidence.json 每条规则的观察期计数(指纹 / 次数 / 首末时间)
~/.dsh/selfnote/persona.reads.json L2 被取用过几次、最后一次是什么时候
~/.dsh/selfnote/persona.pending.json 只在保存进行中出现;进程崩了,下次启动按它补完
~/.dsh/memory/cache/ 会话解压缓存,最多 60 个

没有数据库,没有守护进程,没有网络请求。删掉 ~/.dsh/selfnote/ 与 ~/.dsh/memory/facts.jsonl,记忆就没了(索引与缓存是派生物,会自己重建)。

设计取舍

为什么不用向量库、为什么索引默认关、为什么候选不能自动采纳 —— 写在单独一篇里。

边界

  • 只写自己的目录,不碰任何笔记库;笔记库那边是只读契约。
  • 不写用户隐私:姓名、住址、账号、健康、财务一律不写(写进工具描述里)。
  • 清空 = 删 ~/.dsh/selfnote/,或点设置页的「清空」(内容进 archive/)。
  • 采集失败、索引过期、注入异常都不阻断对话 —— 记忆是加分项,不是前置条件。

设置页的准入凭证

设置页的请求要过三道门:

  1. Host 必须是回环地址或桌面壳(dsh-app) —— 挡 DNS 重绑定;
  2. 带了 Origin 就必须同源 —— 跨站页面的请求根本发不出去(服务端不回 CORS 头);
  3. 带了 x-selfnote 就必须等于本实例的 token —— token 在首次启动时随机生成(或用 DSH_SELFNOTE_TOKEN 指定),存在 ~/.dsh/selfnote/auth-token(POSIX 上 600;Windows 上 chmod 是空操作,靠用户目录自身的 ACL)。它不是写在文档里的固定值,照抄 README 读不到你的记忆。

第 3 条是可选的:桌面壳的页面从 dsh-app://app 加载,拿不到注入的 token,所以页面不带也放行;脚本调用带上它更稳妥。

代价得说清楚:同一用户的本机进程可以不带 token 直接调接口。但 ~/.dsh/memory/facts.jsonl 本来就是 644,同一进程直接 cat 就能读——端口没有给出新能力。真正需要挡的跨站与 DNS 重绑定,第 1、2 条挡着。

拒绝和成功的破坏性操作都记进 ~/.dsh/selfnote/auth.log:被拒的写 reason,成功清空自述的写 action: 'clear' + 清了几条 + 归档文件名。哪天自述突然空了,翻这个文件就知道是哪一次、几点、清了多少条。

想用脚本自测,读 ~/.dsh/selfnote/auth-token(POSIX 600,Windows 靠用户目录 ACL),或自己设环境变量后带上它。

和 dsh-agent-md 的区别

两节名字分开了:这里叫「人格(自述)」,dsh-agent-md 那节叫「人格」(设定)。管的东西不一样:

  • dsh-agent-md 管多份人格文件:项目内 AGENT.md > 项目专属 > 全局,按目录选一份,它照着演。设置页里那一节关注的是「这个项目用哪一份人格文件」。
  • dsh-selfnote 管一份人格 + 自述:人格是 persona.md 的三段(L0/L1 每回合进、L2 按需取),你写、你改;自述是它自己攒下来的、关于自己的话,能逐条改、逐条删。

两段都注入,人格在前(order 260)、自述在后(order 262);0.14 起 selfnote 自己的人格段插在自述段之前。同时开两个会重复:dsh-agent-md 的 AGENT.md 与 persona.md 说的是同一件事时,模型会看到两遍。真迁移的话,把 AGENT.md 导进人格页、然后停掉 dsh-agent-md(设置页「从 AGENT.md 导入」只填框,要不要保存由你)。

迁移(换机器)

设置页「迁移」卡两个按钮,全程在你自己的浏览器与磁盘之间。导出的包是 v2:比 v1 多带人格三段(persona)与两个配置项(personaEnabled / tempTtlDays)。纠偏证据不进包 —— 它是「这台机器上发生过什么」,不是人格本身。v1 老包照样能导进来,缺的字段按默认补;目标机已有不同人格时不覆盖,落成 persona.imported-<时间戳>.md。

  • 导出:一份 JSON(selfnote-YYYY-MM-DD.json),含自述(分型与钉住也跟着走)、facts、习惯,以及你可调的那几项配置;导入时配置只按白名单与基本类型落盘。
  • 导入:在新机器上选这份文件。规则只有三条 —— 不覆盖(同 id、同正文都跳过)、默认降级(导进来的「已采纳」一律变回候选,勾了「保留原状态」才原样搬)、坏文件直接拒(不是本插件的包、版本比本机新,都返回 400,不会合并一半)。

索引不跟着搬:它记的是本机磁盘路径与会话 mtime,换个地方必然过期。导入后重新构建一次就行(设置页「索引」卡)。

开发

node --run test              # 353 项,30 个文件,无外部依赖
node --run test:accept       # 12 项:真实 HTTP + 36 进程并发写
文件 覆盖
tests/smoke.mjs tests/api.mjs tests/client.mjs tests/client-state.mjs 注册、写入、容量裁剪、归档、HTTP 路由、设置页渲染
tests/memory.mjs tests/roles.mjs tests/index.mjs tests/incremental.mjs 检索分层、角色权重、倒排索引与过期判定、增量解析(追写 / 重写 / 半帧 / 坏文件)
tests/heat.mjs tests/facts-tx.mjs tests/profile.mjs 遗忘曲线边界、facts 写事务(并发不覆盖)、画像聚合与冲突计数
tests/notes-tier.mjs 自述三型:行格式来回、注入顺序(稳定★→临时)、核心不进注入只留素材门牌、临时 1/4 保底、过期与保质期可配
tests/persona.mjs 人格三段:解析(标题认不出、同层拼接、无标题整篇进 L1)、码点计数、三条上限、L0 运行期截断、框架句与边界句的位置、L2 只给指路句
tests/corrections.mjs 纠偏证据:强弱信号与误命中、事件级去重、分页游标、200 条上限、按 usedAt 清理、水位不回填、跨三文件事务的幂等恢复与坏日志隔离、指纹计数
tests/capture.mjs tests/capture-p1b.mjs tests/capture-debug.mjs 触发词、去重合并、游标幂等与崩溃恢复、迟到事件(空洞)、采集轨迹落点
tests/fair.mjs tests/warm.mjs 环境段冷门预留与轮换、压缩后补回;温层带指针摘要(无指针拒写、可吊销不可删)
tests/transfer.mjs tests/transfer-roundtrip.mjs 导出/导入:白名单、不覆盖、降级、坏文件拒绝;自述三型 / pin / 配置跟着往返
tests/r3-regressions.mjs 全量审查回归:采集正文、索引原型键、坏行隔离、缓存键与 UI 可读性
tests/r4-regressions.mjs 收尾回归:相对时间夹具、画像注入上限、环境门牌预算、坏索引线性兜底
tests/crash-write.mjs tests/atomic-retry.mjs tests/capacity-warning.mjs 写入过程被 SIGKILL 不留半截文件、rename 瞬态冲突退避重试、长条挤掉旧规则时容量告警不静默
tests/unicode-rulers.mjs 全插件「字」按 Unicode 码点算:emoji 截断不写坏、字数校验、配置落盘与段 chars 都按码点
tests/acceptance-runtime.mjs 鉴权(无头 / 跨站 / 固定口令)、并发写不丢、锁放弃、注入预算

改 lib/*.js 或 lib/client.js 之后必须重启 DSH,热重载不会重新扫描 client bundle。测试会自动隔离 DSH_MEMORY_DIR,不会污染真实记忆。

插件结构:

lib/index.js        插件入口:工具注册、注入、设置页路由
lib/memory.js       会话解压、检索、事实存取、热度
lib/capture.js      触发词、相似度、冲突判定
lib/capture-run.js  监听 session/event,串行写候选
lib/profile.js      画像派生
lib/index-store.js  倒排索引
lib/persona.js      人格三段:解析、码点计数、上限校验、原子保存
lib/persona-tx.js   跨三文件事务:写人格 + 消费证据 + 记账,可幂等恢复
lib/corrections.js  纠偏证据:强弱信号、事件去重、游标分页、水位、清理
lib/store.js        自述文件读写:三型标记、pin、保质期、归档、码点计数与截断助手
lib/explain.js      每段跳过原因:封闭枚举、字素簇预览、core 明示素材层
lib/atomic.js       原子写:同目录临时文件 + fsync + rename
lib/warm.js         温层:带指针摘要,可吊销不可删
lib/transfer.js     导出/导入:白名单、不覆盖、降级
lib/client.js       设置页 UI

License

MIT © xyzOvO

—/ 5

No ratings yet

Verified DSH bundle

Commit d6a238701e66

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout