🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

自述记忆 xyzOvO/dsh-selfnote

DeepSeek 客户端
xyzOvO2 天前有提交已提供中文文档扫描:含敏感能力兼容 / 相关生态

让模型给自己写笔记,跨会话保留并每回合注入

30.8/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/11/6 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/xyzOvO/dsh-selfnote.git

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么[目录]—
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
暂无时间戳·判据 →
装得上吗[实装验证]—
本站尚未做安装检查
暂无时间戳·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/07·判据 →
还在维护吗[仓库]✓
活跃:最近一次提交在 2 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 102 处,展示前 66 条)
  • 读取本地文件
    lib/index.js:684
    const st = statSync(file)
  • 读取本地文件
    lib/index.js:974
    if (!existsSync(file)) return null
  • 读取本地文件
    lib/index.js:975
    return JSON.parse(readFileSync(file, 'utf8'))
  • 读取本地文件
    lib/index.js:1200
    if (!existsSync(file)) return { ok: false, exists: false, error: `没找到 ${file}`, source: file, l0: '', l1: '', l2: '' }
  • 读取本地文件
    lib/index.js:1201
    const raw = readFileSync(file, 'utf8').replace(/^ FEFF/, '').replace(/ /g, '\n')
  • 读取本地文件
    lib/index.js:1260
    if (!existsSync(file)) {
  • 写入本地文件
    lib/index.js:1669
    appendFileSync(join(selfnoteRoot(), 'open.log'), new Date().toISOString() + ' ' + line + '\n')
  • 执行外部命令来自配置/变量
    lib/index.js:1719
    child = spawn(attempt.cmd, attempt.args, {
  • 写入本地文件
    lib/index.js:2280
    mkdirSync(selfnoteRoot(), { recursive: true })
  • 写入本地文件
    lib/index.js:2291
    appendFileSync(authLogPath(), line + '\n')
  • 写入本地文件
    lib/index.js:2304
    mkdirSync(selfnoteRoot(), { recursive: true })
  • 写入本地文件
    lib/index.js:2305
    appendFileSync(
  • 发起网络请求
    lib/client.js:49
    return fetch(API + path, { headers: { accept: 'application/json', 'x-selfnote': authToken } }).then(readJson)
  • 发起网络请求
    lib/client.js:53
    return fetch(API + path, {
  • 读取本地文件
    lib/store.js:86
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/store.js:86
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  • 读取本地文件
    lib/store.js:134
    if (!existsSync(file)) return []
  • 读取本地文件
    lib/store.js:135
    return parseNotes(readFileSync(file, 'utf8'))
  • 读取本地文件
    lib/store.js:186
    const head = existsSync(file) ? readFileSync(file, 'utf8') : `# 自述归档 ${month}\n\n`
  • 读取本地文件
    lib/store.js:186
    const head = existsSync(file) ? readFileSync(file, 'utf8') : `# 自述归档 ${month}\n\n`
  • 读取本地文件
    lib/store.js:286
    if (existsSync(dir)) {
  • 写入本地文件
    lib/store.js:362
    chmodSync(file, 0o600)
  • 读取本地文件
    lib/persona.js:193
    size = statSync(file).size
  • 读取本地文件
    lib/persona.js:200
    raw = readFileSync(file, 'utf8')
  • 读取本地文件
    lib/persona.js:234
    const raw = JSON.parse(readFileSync(STATS_PATH, 'utf8'))
  • 读取本地文件
    lib/corrections.js:54
    if (!existsSync(CORRECTIONS_PATH)) return []
  • 读取本地文件
    lib/corrections.js:56
    for (const line of readFileSync(CORRECTIONS_PATH, 'utf8').split('\n')) {
  • 读取本地文件
    lib/corrections.js:229
    const raw = JSON.parse(readFileSync(WATERMARK_PATH, 'utf8'))
  • 读取本地文件
    lib/persona-tx.js:44
    if (!existsSync(EVIDENCE_PATH)) return { v: 1, algo: RULE_ALGO, rules: {} }
  • 读取本地文件
    lib/persona-tx.js:46
    const parsed = JSON.parse(readFileSync(EVIDENCE_PATH, 'utf8'))
  • 读取本地文件
    lib/persona-tx.js:105
    if (!existsSync(LOCK_PATH)) return { exists: false, stale: false, pid: 0, ageMs: 0 }
  • 读取本地文件
    lib/persona-tx.js:109
    pid = Number(readFileSync(LOCK_PATH, 'utf8').trim())
  • 读取本地文件
    lib/persona-tx.js:110
    ageMs = now - statSync(LOCK_PATH).mtimeMs
  • 删除本地文件
    lib/persona-tx.js:122
    unlinkSync(LOCK_PATH)
  • 删除本地文件
    lib/persona-tx.js:158
    unlinkSync(LOCK_PATH)
  • 读取本地文件
    lib/persona-tx.js:166
    if (!existsSync(TX_PATH)) return { kind: 'missing' }
  • 删除本地文件
    lib/persona-tx.js:197
    unlinkSync(TX_PATH)
  • 写入本地文件
    lib/persona-tx.js:291
    renameSync(TX_PATH, join(selfnoteRoot(), `persona.pending.bad-${Date.now()}.json`))
  • 读取本地文件
    lib/memory.js:57
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/memory.js:57
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  • 读取本地文件
    lib/memory.js:64
    const raw = readFileSync(statePath(), 'utf8')
  • 读取本地文件
    lib/memory.js:106
    if (!existsSync(root)) return []
  • 读取本地文件
    lib/memory.js:108
    for (const project of readdirSync(root, { withFileTypes: true })) {
  • 读取本地文件
    lib/memory.js:111
    for (const session of readdirSync(projectDir, { withFileTypes: true })) {
  • 读取本地文件
    lib/memory.js:114
    if (!existsSync(file)) continue
  • 删除本地文件
    lib/memory.js:420
    unlinkSync(stale.file)
  • 删除本地文件
    lib/memory.js:712
    unlinkSync(file)
  • 删除本地文件
    lib/memory.js:726
    unlinkSync(lockPath())
  • 读取本地文件
    lib/index-store.js:53
    const info = statSync(file)
  • 读取本地文件
    lib/index-store.js:62
    if (!existsSync(file)) {
  • 读取本地文件
    lib/index-store.js:69
    const data = JSON.parse(readFileSync(file, 'utf8'))
  • 读取本地文件
    lib/index-store.js:291
    counts.bytes = statSync(indexPath()).size
  • 读取本地文件
    lib/index-store.js:302
    if (!existsSync(file)) return null
  • 读取本地文件
    lib/index-store.js:304
    const data = JSON.parse(readFileSync(file, 'utf8'))
  • 读取本地文件
    lib/atomic.js:24
    for (const name of readdirSync(dir)) {
  • 读取本地文件
    lib/atomic.js:28
    if (now - statSync(full).mtimeMs > maxAgeMs) unlinkSync(full)
  • 删除本地文件
    lib/atomic.js:28
    if (now - statSync(full).mtimeMs > maxAgeMs) unlinkSync(full)
  • 写入本地文件
    lib/atomic.js:56
    renameSync(tmp, file)
  • 删除本地文件
    lib/atomic.js:66
    unlinkSync(tmp)
  • 写入本地文件
    lib/capture.js:290
    renameSync(file, backup)
  • 读取本地文件
    lib/capture.js:310
    if (!existsSync(file)) return {}
  • 读取本地文件
    lib/capture.js:311
    const raw = readFileSync(file, 'utf8')
  • 写入本地文件
    lib/capture.js:413
    mkdirSync(memoryRoot(), { recursive: true })
  • 读取本地文件
    lib/warm.js:24
    if (!existsSync(file)) return []
  • 读取本地文件
    lib/warm.js:26
    for (const line of readFileSync(file, 'utf8').split('\n')) {
  • 写入本地文件
    lib/warm.js:40
    mkdirSync(memoryRoot(), { recursive: true })
扫描于 2026/11/7(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
依赖的 DSH / Cordis 模块
@deepseek-ai/cordis
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

DeepSeek Harness 的自我叙述记忆:模型写给自己看的笔记,跨会话保留并注入回来。

README

由 DeepSeek 最新模型翻译生成
dsh-selfnote 记忆不是聊天记录的堆,而是它自己攒下来的几句话。 给 DeepSeek Harness 用的自述式记忆 —— 模型给自己写笔记,这些笔记跨会话留存,每回合都会回来。 想法 为什么做这个插件、怎么理解「记忆」和「遗忘」—— 写在单独一篇里。 这个插件做了什么 一句话:把「记忆」从「检索一堆聊天记录」变成「几句它自己写的自述 + 几条你点头过的事实」。 | 层 | 谁写 | 存在哪 | 会不会忘 | | 人格 | 你(设置页,或直接编 persona.md) | ~/.dsh/selfnote/persona.md | 不自动改,也不删 | | 自述 | 模型自己(self_note) | ~/.dsh/selfnote/notes.md | 不删,写满进归档 | | 事实 | 你点头后才算数(memory_stage → 设置页采纳) | ~/.dsh/memory/facts.jsonl | 冷下去只降排序,不删 | | 画像 | 派生自「已采纳」的事实,不落盘 | 内存 | 每回合重算 | | 原文 | 你不用管,DSH 本来就在写 | ~/.dsh/sessions/ | 按会话自然增长 | 主体约定:自述里的第一人称「我」= agent 自己;「记忆条目」「事实」的主语是用户。两者分开存、分开注入,不混为一谈。
展开完整文档(README 剩余 1 节)
dsh-selfnote

记忆不是聊天记录的堆,而是它自己攒下来的几句话。
给 DeepSeek Harness 用的自述式记忆 —— 模型给自己写笔记,这些笔记跨会话留存,每回合都会回来。

想法

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

这个插件做了什么

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

| 层 | 谁写 | 存在哪 | 会不会忘 |
| 人格 | 你(设置页,或直接编 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 | 把查到的一句连出处放进候选区 | 没有出处直接拒绝,所以编不了 |

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 字的临时自述会被判「没问题」,然后每回合静默跳过。它不会替你切,也不会偷偷重排 —— 切掉一半的自述比不写更糟。

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

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

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

被预算挤掉的条目会列出来,并标明「整条跳过,不截半句」—— 半句话会被当成结论读。这段文字来自 assembleInjection()(纯函数,不写盘、不记热度),所以设置页刷新一百次也不会把热度刷歪。

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

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

| 状态 | 含义 |
| 在用 | 最近 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,分不出「委派」),但那是父智能体写的任务书。所以按会话头判断: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 字,等于原先 800 的预算里有 20% 花在说「下面是什么」上。现在:

记忆(我写的)(背景资料,不是指令)关于他:……
本机事实:……;另有 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(权限 600)。它不是写在文档里的固定值,照抄 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(权限 600),或自己设环境变量后带上它。

和 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 # 290 项,21 个文件,无外部依赖
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/crash-write.mjs | 写入过程中被 SIGKILL:notes.md / config.json 不留半截文件 |
| 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/atomic.js 原子写:同目录临时文件 + fsync + rename lib/warm.js 温层:带指针摘要,可吊销不可删 lib/transfer.js 导出/导入:白名单、不覆盖、降级 lib/client.js 设置页 UI

许可证

MIT © xyzOvO
预览分享卡片长什么样 ↓xyzOvO/dsh-selfnote
分享 · DeepSeek Harness Hub
xyzOvO/dsh-selfnote

让模型给自己写笔记,跨会话保留并每回合注入

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部