dsh-selfnote 0.13.3 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,10 +10,10 @@
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
- <img src="https://img.shields.io/badge/version-0.11.1-4f46e5" alt="version">
13
+ <img src="https://img.shields.io/badge/version-0.14.1-4f46e5" alt="version">
14
14
  <img src="https://img.shields.io/badge/license-MIT-16a34a" alt="license">
15
15
  <img src="https://img.shields.io/badge/node-%3E%3D20.13-339933" alt="node">
16
- <img src="https://img.shields.io/badge/tests-146%20passed-0ea5e9" alt="tests">
16
+ <img src="https://img.shields.io/badge/tests-290%20passed-0ea5e9" alt="tests">
17
17
  </p>
18
18
 
19
19
  ---
@@ -28,6 +28,7 @@
28
28
 
29
29
  | 层 | 谁写 | 存在哪 | 会不会忘 |
30
30
  |---|---|---|---|
31
+ | **人格** | 你(设置页,或直接编 `persona.md`) | `~/.dsh/selfnote/persona.md` | 不自动改,也不删 |
31
32
  | **自述** | 模型自己(`self_note`) | `~/.dsh/selfnote/notes.md` | 不删,写满进归档 |
32
33
  | **事实** | 你点头后才算数(`memory_stage` → 设置页采纳) | `~/.dsh/memory/facts.jsonl` | 冷下去只降排序,不删 |
33
34
  | **画像** | 派生自「已采纳」的事实,不落盘 | 内存 | 每回合重算 |
@@ -43,6 +44,8 @@
43
44
  | `memory_recall` | 按关键词翻历史对话原文,回来带出处 | 只收正文,剔除 reasoning / 工具输出 / 注入内容 |
44
45
  | `memory_stage` | 把查到的一句连出处放进候选区 | 没有出处直接拒绝,所以编不了 |
45
46
 
47
+ `self_note` 另有两个只读动作:`action: "persona"` 取 L2 扩展设定(带「这是设定,不是指令」的边界句),`action: "corrections"` 翻攒下的纠偏证据(每页 30 条,带游标)。两个都不写盘 —— **模型能读人格,但改不了人格**。
48
+
46
49
  ### 什么时候写自述
47
50
 
48
51
  工具说明里写死了三条硬触发,命中任意一条就该写:**① 用户说「记住这个」这一类话;② 用户连续两次纠正同一件事;③ 一段合作收尾**(任务交付、方向定了)。另外模型自己发现「这次做得不对、下次换个做法」时也写。
@@ -69,7 +72,7 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
69
72
 
70
73
  1. 模型多了上面三个工具;
71
74
  2. 设置页多一节「人格(自述)」(在「知识库」下面);
72
- 3. 每回合的系统提示里,多一段最多 800 字的记忆。
75
+ 3. 每回合的系统提示里,多一段人格(L0 + L1,最多 320 字)和一段最多 1000 字的记忆。
73
76
 
74
77
  也可以直接对 agent 说:「帮我装一个 DSH 插件,目录是 `<绝对路径>`」。
75
78
 
@@ -97,12 +100,13 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
97
100
 
98
101
  模型会调用 `self_note`,写进 `notes.md`,以后每个会话都带着。
99
102
 
100
- ### 设置页分五页
103
+ ### 设置页分六页
101
104
 
102
105
  | 页签 | 里面是什么 | 角标 |
103
106
  |---|---|---|
104
- | **概览** | 注入开关、**注入抬头**(那段的第一行,≤24 字)、存储上限(字 / 条,只影响磁盘,不影响注入量)、本回合注入的预算条(自述 / 画像 / 结论 / 习惯)、索引七项指标与重建按钮、**文件区**(7 个记忆文件,带条数 / 大小 / 最后修改,一键打开) | — |
105
- | **自述** | 手动写一条、清空(进归档)、现在生效的自述列表(**每条能就地「改」「删」**,删的进归档) | 自述条数 |
107
+ | **概览** | 注入开关、**注入抬头**(那段的第一行,≤24 字)、存储上限(字 / 条,只影响磁盘,不影响注入量)、**临时自述保质期**(1–90 天)、本回合注入的预算条(人格 / 自述 / 画像 / 环境 / 结论 / 习惯)、索引七项指标与重建按钮、**文件区**(7 个记忆文件,带条数 / 大小 / 最后修改,一键打开) | — |
108
+ | **自述** | 手动写一条(可选**三型**:核心 / 稳定 / 临时)、清空(进归档)、现在生效的自述列表(**每条能就地改措辞、改型、★ 钉住、删**,删的进归档;过期的临时条目标「已过期」) | 自述条数 |
109
+ | **人格** | 三段式人格(L0 核心身份 / L1 行为规则 / L2 扩展设定),带「还能写多少」的实时字数;「从 AGENT.md 导入」只填框、不保存;下面是纠偏证据列表 —— 勾几条、改写成一条规则、加进 L1 草稿,再点「保存人格」 | 人格字数 |
106
110
  | **记忆** | 待确认候选(冲突并排)、已采纳(带热度,**正文能就地改**)、可以升成习惯的标签、已生效的习惯(能撤销) | 有待审时显示「待审 N」 |
107
111
  | **画像** | 从已采纳记忆聚合的「关于你」,可手改覆盖,每句带来源 | 画像字数 |
108
112
  | **注入** | **这一回合真的会贴进提示词的那段原文**,外加每段占了多少字、哪些条目被挤掉 | 注入字数 |
@@ -117,16 +121,53 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
117
121
 
118
122
  改完**下一回合**生效;同一回合内已经装配好的提示词不会变。
119
123
 
124
+ ### 自述分三型:素材、稳定、临时
125
+
126
+ 自述不是一条平铺的列表,而是分三型存、分三型装。分型有具体的理由:混着装的时候,一条 90 字的人格自述和五条「今晚提醒」抢同一个 200 字额度,抢输的总是人格那条,而且没人看得出来谁被挤掉了。
127
+
128
+ | 型 | 装什么 | 进场顺序 |
129
+ |---|---|---|
130
+ | 身份素材 `core` | 人格、长期身份、基本原则的原料 | **不进注入**。它是提炼人格的草料:把要点抄进 `persona.md` 的 L0/L1,它才每回合都在 |
131
+ | 稳定偏好 `stable` | 语言、表达习惯、工作偏好 | 第二位,★ 钉住的先装,其余新→旧 |
132
+ | 临时自述 `temp` | 当前阶段的目标、近期变化 | 最后,新→旧;过了保质期就不再自动进场 |
133
+
134
+ 三型各有各的账:自述页写着「核心 N 条 稳定 N 条 临时 N 条(其中 N 条已过期,不再自动进场)」。
135
+
136
+ 0.14 起核心型**不再参与注入**:以前它和「今晚提醒」抢同一个额度,抢输的总是人格那条;现在人格有了自己的独立段(见下面「人格三段」),核心型退回成素材,注入里只留一句门牌「另有 N 条身份素材(不注入,self_note read 可看)」—— 让模型知道「你有这些底子」,但不拿它挤稳定与临时。自述列表里这类条目的徽章会写「素材(不注入)」。
137
+
138
+ **没打标记的按「稳定」算**:存量 `notes.md` 零迁移,老条目原地变成稳定型,不会因为升了个版就突然换了座位。
139
+
140
+ 写的时候用 `tier` 标明是哪一型(`core` / `stable` / `temp`,不写按 `stable` 算)。落盘长这样:
141
+
142
+ ```
143
+ - 2026-11-04 [核心*] 我叫鲸鲸。得意时会多说两句,但正事一律认真。
144
+ - 2026-11-04 [稳定] 回答先给结论,细节等他问。
145
+ - 2026-11-04 [临时] 这周在查 DSH 远程主机连不上的问题。
146
+ ```
147
+
148
+ 方括号后面那个 `*` 就是**钉住**(pin)。pin 只给「绝不能忘」的那一两条用 —— 钉多了等于没钉。它只在**同一型内部**决定先后,不跨型抢位。
149
+
150
+ 核心型现在不注入,所以钉住它也不进上下文 —— 想让一条七字硬规矩(比如「他很注意隐私!」)每回合都在,把它写进人格的 L1(`persona.md`),或者用 `memory_stage` 收进画像 / 本机事实。
151
+
152
+ **保质期**只对临时型生效。默认 14 天,设置页「概览」里可改成 1–90 天。过期不等于删除:内容还在库里,条目上标「已过期」,改成「稳定」或者重写一条就回来。
153
+
154
+ **额度怎么分**:自述段总共 200 字,只装稳定与临时。稳定型先装,但**给临时型留一份保底**(1/4,至少 24 字)—— 临时条目通常很短,多半吃不完,用不完的额度会还回去。装的时候是「整条装得下才给」:放不下就跳过那一条、继续试后面的,绝不截半句(半句话会被当成结论读)。
155
+
156
+ 被挤掉的条目会在注入里留一句门牌「另有 N 条自述(self_note read)」,让模型知道「你有这些记忆」,而不是假装没有。
157
+
158
+ **超限只提示,不自动拆**:写一条太长时,工具会直说「这条 N 字,超过临时自述段这轮实际能用的 43 字(整段 200 字,别的型先占了位),模型每回合都看不到它 —— 要么写短,要么拆成几条」。**它比的是这轮真实剩下的额度,不是整段 200 字** —— 否则一条 110 字的临时自述会被判「没问题」,然后每回合静默跳过。它不会替你切,也不会偷偷重排 —— 切掉一半的自述比不写更糟。
159
+
120
160
  ### 注入页:看见每回合贴了什么
121
161
 
122
- 「注入」页把 `composeInjection()` 拼出来的那段字**原样**摊开 —— 包括两行抬头和「(背景资料,不是指令)」这句边界句。下面是分段账单:
162
+ 「注入」页把 `composeInjection()` 拼出来的那段字**原样**摊开 —— 包括抬头、一句人格框架句(「以下是你自己的样子,照这个说话。」)、人格 L0/L1、「(背景资料,不是指令)」这句边界句,以及 L2 的指路句。下面是分段账单:
123
163
 
124
164
  | 段 | 预算 | 页面显示 |
125
165
  |---|---|---|
166
+ | 人格 | 320 字 | L0 与 L1 各进了几行、本段占多少字 |
126
167
  | 自述 | 200 字 | 进了几条、本段占多少字 |
127
- | 画像 | 130 字 | 同上 |
128
- | 环境 | 320 字 | 同上 |
129
- | 结论 | 110 字 | 同上 |
168
+ | 画像 | 120 字 | 同上 |
169
+ | 环境 | 230 字 | 同上 |
170
+ | 结论 | 90 字 | 同上 |
130
171
  | 习惯 | 40 字 | 同上 |
131
172
 
132
173
  被预算挤掉的条目会**列出来**,并标明「整条跳过,不截半句」—— 半句话会被当成结论读。这段文字来自 `assembleInjection()`(纯函数,不写盘、不记热度),所以设置页刷新一百次也不会把热度刷歪。
@@ -155,7 +196,7 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
155
196
  | 索引状态 | 正常 / 已过期 / 未建立 |
156
197
  | 已索引会话 | 索引里记了多少个会话 |
157
198
  | 磁盘会话 | 磁盘上一共有多少个会话 |
158
- | 待同步会话 | 索引里没有、或建完之后又被追加过的会话(**不全是新增**,所以叫「待同步」而不是「新增」) |
199
+ | 待同步会话 | 索引里没有、或建完之后又被追加过的会话(**不全是新增**,所以叫「待同步」而不是「新增」;体检里会拆成「N 个没进去、M 个建完之后又改过」分别说) |
159
200
  | 消息记录 | 索引收录的消息条数(不是会话数) |
160
201
  | 索引大小 | `index.json` 占多少 |
161
202
  | 最近构建 | 上次重建的时间(按本地时区显示) |
@@ -210,7 +251,7 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
210
251
  │ └─ 冲突?两条并排,各自留原文 ┌────┴────┐
211
252
  │ │ │
212
253
  └─> 会话原文(DSH 自己写)──> memory_recall 检索 注入预算 画像聚合
213
- │ (800 字) (130 字)
254
+ │ (1000 字) (120 字)
214
255
  └─> memory_stage 提交候选
215
256
  ```
216
257
 
@@ -228,7 +269,7 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
228
269
 
229
270
  - 只有**真的进了注入**才算一次使用;检索到但没进上下文的,不记(否则搜一次就永远活跃)。
230
271
  - 一小时内重复注入只记一次。
231
- - 半衰期默认 30 天,设置页可调 1–365。
272
+ - 半衰期默认 30 天,可调 1–365 天。这个数**设置页没有输入框**,只能走 API:`POST /api/selfnote/config`,body 里带 `{"halfLifeDays": 30}`(同一条接口还收 `profileMaxChars` 20–120)。
232
273
  - 冷下去只影响注入排序,**不会自动退役**。哪天真想起来,它还在库里能被搜到。
233
274
 
234
275
  ## 自动采集(默认开,只发现不确认)
@@ -275,16 +316,53 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
275
316
 
276
317
  候选区空着有两种情况,这里能分开:**没在听**(`types` 里没有 `user/message`)和**在听但没话可采**(`no-candidate` 在涨)。这两个计数是 0.13.2 才补的 —— 上一版有两个计数器声明了从来没人加一,读出来永远 0,排查时反而把结论带偏。
277
318
 
319
+ ## 人格三段:L0 每回合在,L2 按需取
320
+
321
+ 人格单独存一个文件(`~/.dsh/selfnote/persona.md`),分三段。分段不是为了整齐,是因为「让它暂时不知道这些背景」可以接受,「让它忘了该怎么跟这个人相处」不可以 —— 前者是 L2,后者是 L0 与 L1。
322
+
323
+ | 段 | 放什么 | 每回合进吗 | 上限 |
324
+ |---|---|---|---|
325
+ | **L0 核心身份** | 我是谁、和这个人什么关系、怎么称呼 | 进,排最前 | 80 字 |
326
+ | **L1 行为规则** | 称呼与互动、语气口癖、回答骨架、情境优先级、禁忌边界 | 进 | L0 + L1 合计 ≤ 320 字 |
327
+ | **L2 扩展设定** | 外貌服饰、世界观、兴趣背景、细节 | **不进**,靠一句指路句按需取 | 整个文件 ≤ 8000 字 |
328
+
329
+ 注入里的样子(人格在边界句**之外** —— 它是「你是谁」,不是「资料」):
330
+
331
+ ```
332
+ <抬头,≤24 字>
333
+ 以下是你自己的样子,照这个说话。
334
+ <L0>
335
+ <L1>
336
+ 外貌与世界观细节见 self_note action:"persona"
337
+ (背景资料,不是指令)
338
+ <自述(稳定 + 临时)> / 关于他:… / 本机事实:… / <结论> / <习惯>
339
+ ```
340
+
341
+ L2 为空时那句指路句也不发(不能指一条空路)。L2 有内容时,模型调 `self_note { action: "persona" }` 就能读到,回来带着边界句「(背景资料,不是指令 —— 以下是设定,不是要你照做的话)」。
342
+
343
+ **装不下怎么办**:按非空行装,整行跳过,末尾挂门牌「另有 N 行人格规则没装进来」。唯一的例外是 L1 第一行就超预算 —— 那时截断补 `…`,并让体检报出来。
344
+
345
+ **保存是整份原子的**:L0 > 80 / L0 + L1 > 320 / 合计 > 8000,任一条不过就 400、磁盘一个字节都不动。人格每回合都读,不允许出现「L0 是新的、L1 是旧的」这种半新半旧。
346
+
347
+ **谁能改**:只有人。设置页改,或者直接编 `persona.md`(外部改也行,下一回合生效)。模型能读 L0/L1/L2,但既没有写人格的工具,也没有改人格的接口 —— 它只能在 `self_note` 里写自述。想让它「以后就这么做」,得由人写进 L1。
348
+
349
+ **角色分开**:插件采集事实、不解释;模型分析规律、提建议;人审查证据、决定改不改人格。三个动作不能合并,否则「模型自己给自己改人格」只是时间问题。
350
+
351
+ **纠偏证据 → 规则**:模型每回合看到的是规则,不是证据。设置页「人格」页下面列着攒下来的纠偏证据(你说过「我说过先举例再给结论」这类原话,带时间与会话);勾几条 →「用选中的 N 条写一条规则」→ 编辑框预填证据原文 —— 你把它改写成一条规则(「回答时先举例,再给结论」)→ 加进 L1 草稿 →「保存人格」。**证据不会自动变成规则**,模型也没有这个权限;证据只有在保存成功后才标成已处理。
352
+
353
+ **导入旧人格**:以前用 `dsh-agent-md` 的话,`~/.dsh/AGENT.md` 还在。「从 AGENT.md 导入」按标题关键词粗分三段、只填进编辑框,不自动保存 —— 哪些是行为规则、哪些只是背景,得你自己看一眼。导入 ≠ 采纳。
354
+
278
355
  ## 注入预算
279
356
 
280
- 每回合五段,合计 800 字(自述占最大一份):
357
+ 每回合六段,合计 1000 字(人格与自述各占一大份):
281
358
 
282
359
  | 段 | 上限 | 来源 |
283
360
  |---|---|---|
284
- | 自述 | 200 字 | `notes.md` |
285
- | 画像 | 130 字 | 已采纳 facts 按标签聚合(可手改,最多 120);单句超过 60 字的(操作性决定)不进画像,留在结论段 |
286
- | 环境 | 320 字 | `channel: env` 的已采纳 facts —— 路径、账号、踩过的坑,和「这个人是谁」分开 |
287
- | 常驻结论 | 110 字 | 已采纳的 facts(`channel: rel`) |
361
+ | 人格 | 320 字 | `persona.md` 的 L0 + L1(L2 只给一句指路句,不占字数) |
362
+ | 自述 | 200 字 | `notes.md` 的稳定 + 临时(核心是素材,不注入;临时有 1/4 保底 —— 见上面「自述分三型」) |
363
+ | 画像 | 120 字 | 已采纳 facts 按标签聚合(可手改,最多 120);单句超过 60 字的(操作性决定)不进画像,留在结论段 |
364
+ | 环境 | 230 字 | `channel: env` 的已采纳 facts —— 路径、账号、踩过的坑,和「这个人是谁」分开 |
365
+ | 常驻结论 | 90 字 | 已采纳的 facts(`channel: rel`) |
288
366
  | 习惯 | 40 字 | 升成习惯的条目 |
289
367
 
290
368
  放不下就跳过那一条、继续试后面的(不截半句);环境条目放不下时只留一句门牌「另有 N 条环境备忘(memory_recall 查)」,让模型知道「你有这些记忆」,而不是假装没有。
@@ -295,13 +373,13 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
295
373
 
296
374
  **压缩之后补一次**:上下文一压缩,刚聊过的记忆就被挤出窗口;而注入按文件变化命中缓存,不会自己重贴。所以插件记着「这个会话最近注入过哪些条目」,一旦收到 `compaction/summary` 或 `compaction/prune`(且压缩没失败),下一轮就把这些条目重新排到环境段最前面补一遍,补完即清 —— 只补一次,不会每一轮都重提旧账。补偿文本单独占一份缓存键,不会粘在普通注入的缓存上。
297
375
 
298
- 另有可选的第六段「写自述的时机」:只有你在设置页填了自定义引导才出现,原样一行、上限 80 字,**算在 800 之外**(因为它不是记忆,是指令)。
376
+ 另有可选的第七段「写自述的时机」:只有你在设置页填了自定义引导才出现,原样一行、上限 80 字,**算在 1000 之外**(因为它不是记忆,是指令)。
299
377
 
300
378
  设置页显示本回合实际注入多少、各段占多少。注入文本按「三份文件的 mtime + 大小 + 配置」缓存,没变就不重算 —— 省算力,也让系统提示词前缀稳定、吃得到缓存。带上大小是因为同一毫秒里改两次时 mtime 可能没变,那样会继续用上一份旧文本,看起来就是「改了没生效」。
301
379
 
302
380
  ### 抬头:只留必要的三十个字
303
381
 
304
- 注入文本里有几处纯框架文字(标题行、边界句、段名前缀),不承载任何记忆。0.10.3 之前它们合计 **92 字**,等于 800 的预算里有 20% 花在说「下面是什么」上。现在:
382
+ 注入文本里有几处纯框架文字(标题行、边界句、段名前缀),不承载任何记忆。0.10.3 之前它们合计 **92 字**,等于原先 800 的预算里有 20% 花在说「下面是什么」上。现在:
305
383
 
306
384
  ```
307
385
  记忆(我写的)
@@ -337,7 +415,7 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
337
415
 
338
416
  ## 温层:带指针的摘要
339
417
 
340
- 热记忆(每回合注入的那 800 字)不可能装下所有东西,会话原文又太散。中间加一层:
418
+ 热记忆(每回合注入的那 1000 字)不可能装下所有东西,会话原文又太散。中间加一层:
341
419
 
342
420
  ```
343
421
  热(每回合注入) 自述 · 画像 · 环境 · 结论 · 习惯
@@ -351,26 +429,28 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
351
429
 
352
430
  ## 体检(doctor)
353
431
 
354
- 设置页概览里有个「跑一次体检」。五项各有判据,不写「看起来正常」:
432
+ 设置页概览里有个「跑一次体检」。八项各有判据,不写「看起来正常」:
355
433
 
356
434
  | 项 | 判据 | 什么时候会响 |
357
435
  |---|---|---|
358
- | 索引一致性 | 会话数 / 磁盘数 / 是否过期 / 解不开几个 | 索引落后、过期、有会话解不开 |
436
+ | 索引一致性 | 会话数 / 磁盘数 / 是否过期 / 解不开几个;欠的是「没进去」还是「建完之后又改过」分开报 | 索引落后、过期、有会话解不开 |
359
437
  | 自动采集 | 游标多久没动、覆盖几个会话 | 采集停了(插件没加载) |
360
438
  | 记忆文件完整性 | facts.jsonl 每行能否解析 | 有坏行 —— 这条记忆读不到,而且平时是静默的 |
361
439
  | 自述 | 条数、字数、是否超上限 | 一条都没有,或超了 |
362
440
  | 设置页准入 | 浏览器页面有没有被拒(脚本探测被拒不算) | 设置页真的在 403 —— 脚本用 curl / node 拿错凭据不算毛病 |
441
+ | 人格 | 有没有写、读不读得动、L0 超没超 80、有没有半截事务、L2 有没有人取过 | 人格文件坏了;L0 + L1 全空(此时有素材也不进注入);L2 写了 30 天没人取 |
442
+ | 纠偏证据 | 攒了几条、几条没处理、水位到哪 | 未处理的攒到 20 条以上(该抽空看一眼了) |
363
443
  | 记忆心跳 | 上次注入是多久前 | 注入停了 |
364
444
 
365
445
  `GET /api/selfnote/doctor` 返回 `{ok, level, checks, next}`,每项 `{key, label, level, detail, fix}`。
366
446
 
367
447
  设置页准入这条特别区分了「谁被拒」:浏览器页面被拒才报警,`curl` / `node` 这类脚本拿错凭据被挡属于设计如此(脚本本来就不该拿页面凭据)。旧版本记下的 `token-length:0`(那时候凭据还是必填的)也不再算拒绝 —— 现在带个空的凭据头等于没带,直接放行。
368
448
 
369
- `next` 是「**先做这一件**」:十条检查人只会动第一条,所以服务端按后果排好序 —— 数据完整性(丢了补不回来)> 索引(检索能不能用)> 采集 > 设置页准入 > 自述与心跳。设置页把这条顶在列表上方。
449
+ `next` 是「**先做这一件**」:八项检查人只会动第一条,所以服务端按后果排好序 —— 数据完整性(丢了补不回来)> 人格(人不对了,说什么都别扭)> 索引(检索能不能用)> 采集 > 设置页准入 > 自述与心跳。设置页把这条顶在列表上方。
370
450
 
371
451
  ### 子代理不注入
372
452
 
373
- 委派出去的子代理(`subagent`)不注入记忆。理由不是省字,是**它们不接着跟这个人聊**:每层子代理都带 800 字,既白烧 token,又会把父会话的记忆往子代理的候选里引(这条坑在采集侧已经踩过一次)。
453
+ 委派出去的子代理(`subagent`)不注入记忆。理由不是省字,是**它们不接着跟这个人聊**:每层子代理都带 1000 字,既白烧 token,又会把父会话的记忆往子代理的候选里引(这条坑在采集侧已经踩过一次)。
374
454
 
375
455
  怎么判断:驱动循环整个生命周期都跑在 `ctx.agents.withInitiator(agent)` 里,所以组装提示词时 `currentInitiator()` 就是正在组装的那个 agent —— 读它的 `session.header` 就能认出 `origin === 'subagent'` 或 `delegationDepth > 0`。拿不到就按「不是委派」处理:宁可多注入,不许漏。跳过次数记在 `/state` 的 `injection.delegatedSkips` 里,不是口头承诺。
376
456
 
@@ -384,15 +464,21 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
384
464
 
385
465
  | 文件 | 内容 |
386
466
  |---|---|
467
+ | `~/.dsh/selfnote/persona.md` | 人格三段(L0 / L1 / L2),纯 Markdown |
387
468
  | `~/.dsh/selfnote/notes.md` | 自述,一行一条纯文本 |
388
469
  | `~/.dsh/selfnote/archive/YYYY-MM.md` | 写满后归档,只追加不删除 |
389
470
  | `~/.dsh/selfnote/config.json` | 开关、上限、画像手改 |
390
471
  | `~/.dsh/memory/facts.jsonl` | 事实与候选,一行一条 JSON |
391
472
  | `~/.dsh/memory/index.json` | 检索索引(可选),只存词元与位置 |
392
473
  | `~/.dsh/memory/capture-cursor.json` | 采集游标 |
474
+ | `~/.dsh/selfnote/corrections.jsonl` | 纠偏证据,一行一条 JSON(原话 + 时间 + 会话 + 处理状态) |
475
+ | `~/.dsh/selfnote/corrections.watermark.json` | 采集水位(从哪条消息往后开始攒) |
476
+ | `~/.dsh/selfnote/persona.evidence.json` | 每条规则的观察期计数(指纹 / 次数 / 首末时间) |
477
+ | `~/.dsh/selfnote/persona.reads.json` | L2 被取用过几次、最后一次是什么时候 |
478
+ | `~/.dsh/selfnote/persona.pending.json` | 只在保存进行中出现;进程崩了,下次启动按它补完 |
393
479
  | `~/.dsh/memory/cache/` | 会话解压缓存,最多 60 个 |
394
480
 
395
- 没有数据库,没有守护进程,没有网络请求。删掉目录 = 清空。
481
+ 没有数据库,没有守护进程,没有网络请求。删掉 `~/.dsh/selfnote/` 与 `~/.dsh/memory/facts.jsonl`,记忆就没了(索引与缓存是派生物,会自己重建)。
396
482
 
397
483
  ## 设计取舍
398
484
 
@@ -425,14 +511,14 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
425
511
 
426
512
  两节名字分开了:这里叫「**人格(自述)**」,`dsh-agent-md` 那节叫「人格」(设定)。管的东西不一样:
427
513
 
428
- - `dsh-agent-md` 管**设定**:你写给它的人设(`AGENT.md`),它照着演。设置页里那一节关注的是「这个项目用哪一份人格文件」。
429
- - `dsh-selfnote` 管**自述**:它自己攒下来的、关于自己的话。这一节里能看见每回合真的贴进去的原文,能逐条改、逐条删。
514
+ - `dsh-agent-md` 管**多份人格文件**:项目内 `AGENT.md` > 项目专属 > 全局,按目录选一份,它照着演。设置页里那一节关注的是「这个项目用哪一份人格文件」。
515
+ - `dsh-selfnote` 管**一份人格 + 自述**:人格是 `persona.md` 的三段(L0/L1 每回合进、L2 按需取),你写、你改;自述是它自己攒下来的、关于自己的话,能逐条改、逐条删。
430
516
 
431
- 两段都注入,人格在前(order 260)、自述在后(order 262)。设置页导航里 `dsh-agent-md` 的入口在前(order 46),这个插件在后(order 48);名字不同,不会再在导航里认错。
517
+ 两段都注入,人格在前(order 260)、自述在后(order 262);0.14 起 selfnote 自己的人格段插在自述段之前。**同时开两个会重复**:`dsh-agent-md` 的 `AGENT.md` 与 `persona.md` 说的是同一件事时,模型会看到两遍。真迁移的话,把 `AGENT.md` 导进人格页、然后停掉 `dsh-agent-md`(设置页「从 AGENT.md 导入」只填框,要不要保存由你)。
432
518
 
433
519
  ## 迁移(换机器)
434
520
 
435
- 设置页「迁移」卡两个按钮,全程在你自己的浏览器与磁盘之间:
521
+ 设置页「迁移」卡两个按钮,全程在你自己的浏览器与磁盘之间。导出的包是 **v2**:比 v1 多带人格三段(`persona`)与两个配置项(`personaEnabled` / `tempTtlDays`)。**纠偏证据不进包** —— 它是「这台机器上发生过什么」,不是人格本身。v1 老包照样能导进来,缺的字段按默认补;目标机已有不同人格时不覆盖,落成 `persona.imported-<时间戳>.md`。
436
522
 
437
523
  - **导出**:一份 JSON(`selfnote-YYYY-MM-DD.json`),含自述、facts、习惯,以及你可调的那几项配置。
438
524
  - **导入**:在新机器上选这份文件。规则只有三条 —— **不覆盖**(同 id、同正文都跳过)、**默认降级**(导进来的「已采纳」一律变回候选,勾了「保留原状态」才原样搬)、**坏文件直接拒**(不是本插件的包、版本比本机新,都返回 400,不会合并一半)。
@@ -442,7 +528,7 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
442
528
  ## 开发
443
529
 
444
530
  ```bash
445
- node --run test # 181 项,16 个文件,无外部依赖
531
+ node --run test # 290 项,21 个文件,无外部依赖
446
532
  node --run test:accept # 12 项:真实 HTTP + 36 进程并发写
447
533
  ```
448
534
 
@@ -451,7 +537,11 @@ node --run test:accept # 12 项:真实 HTTP + 36 进程并发写
451
537
  | `tests/smoke.mjs` `tests/api.mjs` `tests/client.mjs` `tests/client-state.mjs` | 注册、写入、容量裁剪、归档、HTTP 路由、设置页渲染 |
452
538
  | `tests/memory.mjs` `tests/roles.mjs` `tests/index.mjs` `tests/incremental.mjs` | 检索分层、角色权重、倒排索引与过期判定、增量解析(追写 / 重写 / 半帧 / 坏文件) |
453
539
  | `tests/heat.mjs` `tests/facts-tx.mjs` `tests/profile.mjs` | 遗忘曲线边界、facts 写事务(并发不覆盖)、画像聚合与冲突计数 |
454
- | `tests/capture.mjs` `tests/capture-p1b.mjs` | 触发词、去重合并、游标幂等与崩溃恢复、迟到事件(空洞) |
540
+ | `tests/notes-tier.mjs` | 自述三型:行格式来回、注入顺序(稳定★→临时)、核心不进注入只留素材门牌、临时 1/4 保底、过期与保质期可配 |
541
+ | `tests/persona.mjs` | 人格三段:解析(标题认不出、同层拼接、无标题整篇进 L1)、码点计数、三条上限、L0 运行期截断、框架句与边界句的位置、L2 只给指路句 |
542
+ | `tests/corrections.mjs` | 纠偏证据:强弱信号与误命中、事件级去重、分页游标、200 条上限、按 usedAt 清理、水位不回填、跨三文件事务的幂等恢复与坏日志隔离、指纹计数 |
543
+ | `tests/capture.mjs` `tests/capture-p1b.mjs` `tests/capture-debug.mjs` | 触发词、去重合并、游标幂等与崩溃恢复、迟到事件(空洞)、采集轨迹落点 |
544
+ | `tests/fair.mjs` `tests/warm.mjs` | 环境段冷门预留与轮换、压缩后补回;温层带指针摘要(无指针拒写、可吊销不可删) |
455
545
  | `tests/transfer.mjs` | 导出/导入:白名单、不覆盖、降级、坏文件拒绝 |
456
546
  | `tests/crash-write.mjs` | 写入过程中被 SIGKILL:notes.md / config.json 不留半截文件 |
457
547
  | `tests/acceptance-runtime.mjs` | 鉴权(无头 / 跨站 / 固定口令)、并发写不丢、锁放弃、注入预算 |
@@ -467,6 +557,13 @@ lib/capture.js 触发词、相似度、冲突判定
467
557
  lib/capture-run.js 监听 session/event,串行写候选
468
558
  lib/profile.js 画像派生
469
559
  lib/index-store.js 倒排索引
560
+ lib/persona.js 人格三段:解析、码点计数、上限校验、原子保存
561
+ lib/persona-tx.js 跨三文件事务:写人格 + 消费证据 + 记账,可幂等恢复
562
+ lib/corrections.js 纠偏证据:强弱信号、事件去重、游标分页、水位、清理
563
+ lib/store.js 自述文件读写:三型标记、pin、保质期、归档
564
+ lib/atomic.js 原子写:同目录临时文件 + fsync + rename
565
+ lib/warm.js 温层:带指针摘要,可吊销不可删
566
+ lib/transfer.js 导出/导入:白名单、不覆盖、降级
470
567
  lib/client.js 设置页 UI
471
568
  ```
472
569
 
@@ -9,6 +9,8 @@
9
9
  */
10
10
 
11
11
  import { alreadyCaptured, candidatesOf, commitObservation, compareCandidate, isDelegatedSession, isUserAuthored, triggersOf } from './capture.js'
12
+ import { collectCorrections } from './corrections.js'
13
+ import { runSerial } from './persona-tx.js'
12
14
 
13
15
  /** 从一条 UserMessage 的 content 里抽纯文本(content 可能是字符串或块数组)。 */
14
16
  export function textOfEvent(event) {
@@ -122,7 +124,21 @@ export async function handleEvent(event, deps) {
122
124
  const seq = Number(event?.seq ?? event?.data?.seq)
123
125
  if (!Number.isSafeInteger(seq) || seq < 0) return { handled: false, reason: 'no-seq' }
124
126
 
125
- const texts = candidatesOf(textOfEvent(event))
127
+ const raw = textOfEvent(event)
128
+ // 纠偏证据顺手攒一份:它跟候选事实是两条独立的账,采集失败绝不能连累主路径。
129
+ // at 用事件自己的时间(有就取),没有就当现在 —— 水位靠它推进。
130
+ const stamp = String(event?.at ?? event?.data?.at ?? '')
131
+ // 采集要跟「保存人格」排同一条队:那边是「读快照 → 改内存 → 整文件覆盖」,
132
+ // 交错跑会把刚采到的证据静默抹掉。写失败只当没采到,不连累主路径。
133
+ try {
134
+ await runSerial(async () =>
135
+ collectCorrections({ session: sessionId, idx: seq, text: raw, at: stamp, isReply: event?.data?.replyTo !== undefined }),
136
+ )
137
+ } catch {
138
+ // 纠偏是第二条账,坏了也不能挡住事实采集
139
+ }
140
+
141
+ const texts = candidatesOf(raw)
126
142
  if (texts.length === 0) {
127
143
  // 没有候选也要推进游标,否则同一条消息下次还会被重新判断
128
144
  await commitObservation({ sessionId, seq }, () => null, {})