@nightpainters/dsh-ling 1.6.1 → 1.6.2

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/CHANGELOG.md CHANGED
@@ -2,51 +2,91 @@
2
2
 
3
3
  本项目遵循语义化版本:`MAJOR.MINOR.PATCH`。
4
4
 
5
- ## 1.6.1
5
+ ## 1.6.2
6
6
 
7
- **3×3 红蓝对抗审计的修复版 · 0 条阻断发布 · 14 条该修全部修完**
7
+ **检索档换了一条快两个数量级的路 · 结果不再被"我刚说的话"占满 · 几处会误导人的地方修掉。**
8
8
 
9
- > 一句话:1.6.0 发布前跑了 **红队 3 路 + 蓝队 3 路**的对抗审计(6 个独立审计员),总账 **0 条阻断发布 / 14 条该修 / 10 条观察**;本版把 14 条该修**全部修完**,并修掉 1.6.0 提交后 CI 暴露的一处脆弱断言。
9
+ > 一句话:1.6.1 之后继续修。最值得说的三件:① **按内容找原文从"每次 20 秒以上"变成"20~50 毫秒"**(改走本机库,不再依赖宿主那条例外慢的检索通道);② **搜索结果默认不再被"当前会话自己"占满** —— 此前搜什么都会先命中你几分钟前刚说的话,把真正要回溯的历史挤下去;③ **枝系数(复盘期给某个主题枝调的权重)真的开始生效了**,此前它只存不用。
10
10
 
11
- > ✅ `package.json` 版本号为 **`1.6.1`**。**无新功能、无 schema 变化**(仍是 v17);⚠️ 修复多落在 **host 侧** ⇒ **需重启宿主**才生效。
11
+ > ✅ `package.json` 版本号为 **`1.6.2`**。**无 schema 变化**(仍是 v17);⚠️ 多数改动在 **host 侧** ⇒ **需重启宿主**才生效。新增一份对外文档 [`PHILOSOPHY.md`](./PHILOSOPHY.md)(理念:为什么值得做、凭什么这样做)。
12
12
 
13
- ### 一、注入面(记忆开场)
13
+ ### 一、按内容找原文:换了一条快两个数量级的路
14
14
 
15
- - **置顶条目现在真的"必进"**:此前 `maxItems` 与字预算**先于**置顶判定生效 ⇒ 置顶数超出上限时,多出来的置顶会被挤掉(穷举 90 组实测**累计丢 117 条**;真机 9 条置顶 + 上限 3 ⇒ 只进 3 条)。修法:两道闸门对 `pinned` **豁免**。复测:9 条置顶 + 上限 3 ⇒ **进 9 条**,穷举 90 组**丢 0**。
16
- - **落选者留痕的 `source` 恒为 `undefined`**:深条目路径读的是 `row.source`,而该列列名是 `src` ⇒ 那一栏一直是空的。同批把 `snapshot.js` 的字段白名单补上 `src/kind/durability/hit/pinned`(否则面板拿不到这几列)。
17
- - **打分理由与公式不符(两条路)**:`why` 串按"加法"报分项,而真实公式是"分项先加、再乘血缘与矛盾系数" ⇒ 改成 `(分项和) × 血缘 × 矛盾 ⇒ 总分`,**概述路与条目路**同时改(第一遍只改了条目路,被断言当场抓住)。
18
- - **概述路的"热度"漏乘类别权重**:同一分数在留痕里被报成两种值(daily 差 0.252、feeling 高出 88%)⇒ 改报**类别加权后**的值。
15
+ - **宿主那条检索通道每次都要做一遍全量对账** ⇒ 实测**每次都超过 20 秒**(重启后第一次也一样,并发还会互相排队)。现在 ① 档**默认搜本机库内原文层**(实测 **20~50 毫秒**);要看全量(覆盖全部事件、含子代理会话)显式传 `hostSearch=true`。
16
+ - **如实标注覆盖范围**:本机库那份原文只含"真人消息 + 有正文的回复"(我自己的长回复**中段搜不到**)⇒ 回执里明写"原文来源:**本机库内原文层**",不说就会被读成"库里只有这些"。
17
+ - **原文层默认 5 轮**(此前 10 轮):条目层是结论、原文层是下钻材料,后者一多就把前者挤出视野。
18
+ - **超时不再是挂死**:宿主路有 20 秒上限,超时**照样把条目层交出去**并说明"没跑完" —— 此前实测等过 **30 分钟**,且当时没有任何中断入口。
19
19
 
20
- ### 二、库与检索
20
+ ### 二、搜索结果默认排除"当前会话自己"
21
21
 
22
- - **`recall` 检索档印的"下一步"用错坐标系**:条目里的 `seq_from` 是**事件流 seq**,而 `recall(conv, from, to)` 收的是**对话轮序号**。实测 274 条可比条目里 **207 条(75.5%)**照那条指引会 out-of-range,偶尔落在范围内的也只能读到不相干的轮次。修法:**不再印 `from`**,改为"先 `recall(conv=…)` 拿对话目录,再按目录里的 `#N` 取原文"。
23
- - **回灌会把网页端会话升格成"主人自己的 DSH 会话"**:`restoreEntry` 原先按 id 形状回推来源(裸 uuid ⇒ `dsh`),而 **1,523 条网页端会话全是裸 uuid** ⇒ 一次回灌就让该会话多出 `source='dsh'` 的原文行,概述器当场认领 ⇒ 同 id 双概述、双份记忆。修法:id 带 `import:` 前缀仍记 `import`,其余用**调用方声明的**来源;并在工具层加**交叉校验**(声明与归档记录不一致 ⇒ `source-mismatch` 拒)。
24
- - **飞书通道里主人自己的原话被当成"注入"**:真人判据原先要求 `rpcId`,而**飞书桥来的原话没有 rpcId**(全盘 436 条无 `rpcId` 的 user 消息里 **55 条是主人真话**)⇒ 这些句子在 `recall` 读档时被折进"(此处折叠 N 条注入消息…)"、目录里印成「(注入)」,**"你说过什么"整段消失**。修法:**会话级分通道**(取 `session` 事件过 `isMemoryEligibleHeader`)。实测:真人轮 **951 → 1001**(+49 飞书真话 +1 探针),**子代理委托种子 387 条零回归**。
25
- - **锁竞争下插件加载失败**:第一抛出点是 `PRAGMA journal_mode=WAL`(而 `busy_timeout` 排在它**之后**一行 ⇒ 撞锁 **23 毫秒**就抛、连等都没等)。修法一:`busy_timeout` 挪到所有可能撞锁的语句**之前**;修法二:构造失败对**锁类错误**(`errcode 5/6`)做 **3 次 × 2 秒有界重试**,仍失败则打印可执行的排查指引后**降级加载**(不注册记忆工具,不影响其它插件)。
22
+ - 症结:当前会话也在库里、而它的话最新 ⇒ **任何词都能命中它**(实测搜"检索"时 10 条命中里 **9 条是本会话**),要回溯的历史被整页挤掉。而"我刚说过什么"**根本不需要搜** —— 它就在上下文里。
23
+ - 现在:**默认整条排除**,并如实报出"另有 N 轮命中属于当前会话自己,已排除";要查自己刚说的传 `includeSelf=true`(那时它排到最后并标注「← 本会话」)。
26
24
 
27
- ### 三、工具与写入边界
25
+ ### 三、记忆树:枝系数开始参与打分
28
26
 
29
- - **「只有概述、没有原文」的记忆永远忘不掉**:遗忘前的归档守卫要求"原文轮数 > 0",而这类条目(主脉记忆 / 导入条目,真库实测 3 条)的内容**就在概述里** ⇒ 判据改宽;同批修正 `archive.js` 的"空归档"判据 —— 它原来把**只有元数据空壳**的会话也算成"有内容"(⇒ 真空归档会被放行,而遗忘标记一旦落下就永不召回)。
30
- - **枝名在回执 / 留痕 / 库里三处不一致**(请求 70 字 ⇒ 库里 60 字):统一常量 `BRANCH_NAME_MAX = 80`,并把改名改成**写后读回** —— 回执与留痕一律报库里的真值。
31
- - **`unlink` 的假回执**:连边可被反向重复写入(`link A→B` 后再 `link B→A` 落两行),而删除只删一条 ⇒ 回执说"断开了"、树里还连着。修法:写入侧查反向(但**有向边不合并** —— `prerequisite`/`supersede` 的反向是不同语义,渲染以 `→` / `↔` 区分),删除侧双向删。
32
- - **`weightScale` 传 `null` 会静默写成 0**:`Number(null) === 0`,而 0 在界面上是**合法读数** ⇒ 改成先判类型(`null`/`true`/`[]` 一律拒)。
33
- - **"被采用"的信号按请求窗口加固**:此前按"请求的区间"给条目计数 ⇒ **没被交付的轮次也 +1**;改用**真正交付**的区间。
34
- - **范围限制改为"如实标注"**:宿主没提供会话清单时,检索档开头写明「⚠️ **没能限定范围** —— 下面这份结果**可能含子代理或其它会话**」,不再静默 fail-open。
27
+ - **`branch.weight_scale` 从"只存与显示"变成真的参与检索排序**:`分数 = 基础分 × 血缘 × 矛盾 × 枝系数`。一条结论的枝 = **它来源会话所在的枝**。`1.0` = 零影响,`0` = 排到最后(**不是禁用**)。
28
+ - 于是"复盘期给某个主题枝调权重"**真的有行为后果**了;此前它是面板上的装饰(README 的 U-10 正是针对这类"看起来能改、实际不做"的交互)。
29
+ - 实测现状:全库 20 条枝的系数都还是 `1.0` ⇒ 这次上线**不改变任何现有排序**,变化只在你开始调之后发生。
35
30
 
36
- ### 四、诊断面
31
+ ### 四、写面与设置
37
32
 
38
- - 补 `dbg.diff_stats`(差分路的丢弃画像:扫过多少 / 写了多少 / 丢了哪些类型)。此前它只在"一条都没写进去"时留一次记录 ⇒ **正常路径上丢了多少完全不可见**(1.6.0 的原文写入排查就卡在这一条上)。
39
- - 同批:`dbg.diff_stats` 与 `dbg.event_stats` 现在**每次重启都清空**(此前前者会跨重启残留,看起来像"刚跑过"),`evStats` 计数与 kv 的归零点对齐。
33
+ - **两条写面补上手术门**:`/persona/habits/scan` 与 `/persona/habits/reflect` 会写人格面(习惯候选),此前**不受手术门管辖**,而它们的确认侧 `/persona/habit` 是过门的 ⇒ 一条通路的两半不一致。现在四条形状一致(先解析请求体、再判门)。
34
+ - **删掉一个死开关**:`updates.applyWhileRunning` 只有声明、没有消费者(而且它**不在可写入白名单里**,接上也开不了)⇒ 直接删,免得读的人以为改它能改行为。
35
+ - **`DSH_HOME` 支持收尾**:`rawlog.js` 是最后一处仍走系统主目录的地方 ⇒ 现在与其它模块同源,自定义 home(隔离测试)下不会再读到真机目录。
40
36
 
41
- ### 五、CI
37
+ ### 五、文档与治理
38
+
39
+ - **新增 [`PHILOSOPHY.md`](./PHILOSOPHY.md)**:对外理念文档(目标是一种循环 · 复盘与"人在循环里" · 上下文是工作台、记忆库才是家 · 底层模型 ≠ 器灵 · 凭什么不越界)。随包发出,README 顶部与「文档」一节都有引用。
40
+ - **发布前检查加一条**:对外文档(`README` / `CHANGELOG` / `DESIGN` / `ACCESS-DESIGN` / `package.json`)里出现内部流程术语时会**列出来提醒**(警告级、不阻断发布)。
41
+ - **八项文档订正**逐条落地:README 与代码打架的那句(段头要求 1.5.1 起已废弃)、写面回执形态、设置卡口径、两处数字(`daily` **0.3** / `l1MaxItems` **10**)、手术门覆盖条数统一为 **14**、安装四步的承诺口径、包名口径(`@nightpainters/dsh-ling`)。
42
+
43
+ ### 六、结论层新增一类:`纪律`
44
+
45
+ - 条目现在有**六类**:承诺 / 决定 / 事实 / 偏好 / 关系 / **纪律**。
46
+ - **纪律**的判据三条(全中才进库):① 来源是主人授权的一句指令;② 作用对象是**行为**;③ **违反时能被某条判据红出来**。⇒ 答不出"哪条判据会红"的只是一句话,退回收在别处。
47
+ - 它在记忆开场里的权重是 **0.35** —— 高于"关系"、略低于"承诺":与承诺同族(都是说过的话),但承诺是对某件事的,纪律是对**行为**的。
48
+
49
+ ## 1.6.1
50
+
51
+ **修掉 14 处会影响实际使用的问题 —— 没有新功能,不改库结构。**
52
+
53
+ > 一句话:本版是 1.6.0 的修复版。最值得装的三处:① **钉住的记忆不再被条数上限挤掉**;② **"只有摘要、没有原文"的记忆现在能忘了**;③ **库被别的进程占用时,插件不再整个加载失败**(此前 8 个工具会一起消失,界面上没有任何提示)。
54
+ >
55
+ > 另修好:飞书通道里**你自己说的话不再被当成系统注入**(找回 49 轮真话)、**回灌网页端会话不再把它标成 DSH 会话**、检索档不再给出错位的行号。
42
56
 
43
- - `tests/bodylimit.test.mjs` 的一条断言写的是墙钟下界 `dt >= 20`(毫秒),CI 实测 **19 ms** ⇒ 红。这条断言在断言**定时器的实现细节**(毫秒级钟有粗粒度、定时器可提前一两毫秒),不是行为缺陷 ⇒ 下界改为 `>= 1`(只证明"确实等过、不是同步 reject"),上界 300 ms 与"结局来自那道闸"由同组另两条钉住。
57
+ > ✅ `package.json` 版本号为 **`1.6.1`**。**无新功能、无 schema 变化**(仍是 v17);⚠️ 修复多落在 host 侧 ⇒ **需重启宿主**才生效。
44
58
 
45
- ### 审计出处
59
+ ### 一、记忆开场
60
+
61
+ - **钉住的条目现在真的"必进"**:此前条数上限与字数预算**先于**钉住判定生效 ⇒ 钉住数超过上限时,多出来的会被挤掉(实测 9 条钉住 + 上限 3 ⇒ 只进 3 条)。现在两道闸门对钉住项豁免:同样条件下**进 9 条**。
62
+ - **落选记录的"来源"栏一直是空的**:取错了列名。同批补齐外部面板需要的字段。
63
+ - **打分理由与真实公式不符**:改动后统一为 `(分项和) × 血缘 × 矛盾 ⇒ 总分`。
64
+ - **"热度"在留痕里被报成两种值**(最高差 88%):改为报加权后的值。
65
+
66
+ ### 二、原文与检索
67
+
68
+ - **检索档给出的"取原话"指引用错坐标系**:它印的是**事件行号**,而取原文要的是**对话轮号**。实测 274 条里 **207 条(75.5%)**照做会报"序号超出范围"。现在改为"先看对话目录,再按目录里的 `#` 号取"。
69
+ - **回灌会把网页端会话标成"DSH 会话"**:1,523 条网页端会话全是裸 id,回灌时被按 id 形状推成 DSH 来源 ⇒ 同一条出现两份摘要、双份记忆。现在按归档记录写入来源,并对不上时直接拒绝。
70
+ - **飞书通道里你自己的原话被当成"注入"**:这些句子在读档时被折进"(此处折叠 N 条注入消息…)"、目录里印成「(注入)」—— **"你说过什么"整段消失**。现在按会话通道区分:找回 **49 轮**真话,且不误收子代理收到的委托文字。
71
+ - **库被别的进程占用时,插件会整个加载失败**:等待参数原先排在设置 WAL 模式**之后** ⇒ 撞锁 **23 毫秒**就抛、连等都没等。现在挪到最前 + 对锁类错误做 **3 次 × 2 秒**有界重试;仍失败则**只停用记忆功能**(其它插件不受影响),并给出一条能照着做的排查提示。
72
+
73
+ ### 三、记忆树与遗忘
74
+
75
+ - **"只有摘要、没有原文"的记忆永远忘不掉**(主脉记忆、导入条目):遗忘前的归档守卫要求"原文轮数 > 0",而这类条目的内容**就在摘要里** ⇒ 判据改宽。同批修正"空归档"判据 —— 它原来把**只有元数据空壳**的会话也算成"有内容",于是真空归档会被放行,而遗忘标记一旦落下就永不召回。
76
+ - **改名后的枝名在三处不一致**(回执 70 字 / 留痕 70 字 / 库里 60 字):统一上限常量,并把改名改成**写后读回** —— 回执与留痕一律报库里的真值。
77
+ - **断边的回执可能是假的**:连边可被反向重复写入(`A→B` 后再 `B→A` 落两行),而删除只删一条 ⇒ 回执说"断开了"、树里还连着。现在写入侧查反向(**有向边不合并** —— "A 是 B 的前提"≠"B 是 A 的前提",渲染以 `→` / `↔` 区分),删除侧双向删。
78
+ - **传空权重会静默写成 0**:0 在界面上是**合法读数**,看不出来是手滑 ⇒ 改为先判类型,空值一律拒。
79
+ - **"被采用"的信号按请求窗口记账**:没被真正交付的轮次也 +1 ⇒ 改用**真正交付**的区间。
80
+ - **范围限制如实标注**:宿主没提供会话清单时,检索档开头写明「⚠️ **没能限定范围** —— 结果可能含子代理或其它会话」,不再静默放行。
81
+
82
+ ### 四、诊断
83
+
84
+ - 差分路的丢弃画像此前只在"一条都没写进去"时留一次记录 ⇒ **正常路径上丢了多少完全不可见**。现在每轮覆盖写。
85
+ - 该画像与事件画像现在**每次重启都清空**(此前会跨重启残留,看着像"刚跑过")。
86
+
87
+ ### 五、CI
46
88
 
47
- - 红队三份:`plans/AUDIT-1.6-红队1-注入面.md` · `AUDIT-1.6-红队2-库与检索.md` · `AUDIT-1.6-红队3-工具与边界.md`
48
- - 蓝队三份复核(逐条判真伪与严重度,并复核"修复是否成立"):`plans/AUDIT-1.6-蓝队1-注入面复核.md` · `AUDIT-1.6-蓝队2-库与检索复核.md` · `AUDIT-1.6-蓝队3-工具与边界复核.md`
49
- - 总账:**0 条阻断发布 · 14 条该修(全部已修) · 10 条观察**(观察项不阻塞发布,已记账)。
89
+ - 一条测试断言写的是墙钟下界 `dt >= 20`(毫秒),CI 上实测 **19 ms** ⇒ 红。它断言的是**定时器实现细节**(毫秒级钟有粗粒度、定时器可提前一两毫秒),不是行为缺陷 ⇒ 下界放宽到只证明"确实等过、不是同步 reject",上界与"结局来自那道闸"由同组另两条钉住。
50
90
 
51
91
  ## 1.6.0
52
92
 
@@ -85,7 +125,7 @@
85
125
  - **全量提炼**:1,623 个会话 / 208.9 万字给料 / 118 批 ⇒ **393 条**;密度 **`dsh` 6.0 条/会话 vs `dsweb` 2.3 条/会话**(工作会话有结论、网页端多为通用问答)。
86
126
  - **三条写进列注释的语义位**(谁改都不许违反):① `hit_count` **只由"真被采用"推进,不由"被注入"推进**;② `superseded_by` **只追不替**(旧的留痕、不删);③ `branch_id` 是**气根**(枝侧反向引用,不动树结构)。
87
127
  - **"被采用"已接线**:`recall(conv, from, to)` 取到与条目区间**相交**的原文 ⇒ 该条目 `hit_count +1` 并在回执里报出。挡的是"注入越多 → 分越高 → 越容易被注入"的**自我强化回路**。⚠️ 区间必须用 `dsh_turns_raw.seq`,**不是**目录里的对话轮序号。
88
- - **条目硬度 `durability`(schema v17)**:`durable` **121** · `long` **159** · `ephemeral` **113**(**重判后**的分布 —— 首轮标注偏松、把"开发期技术结论"记成了长期,蓝队复核时 146 条改档) —— **从根源判断条目自身的临时/永久性**,而不是拿"同一次会话最多几条"这类机械限额去卡;**`ephemeral` 不进开场**。
128
+ - **条目硬度 `durability`(schema v17)**:`durable` **121** · `long` **159** · `ephemeral` **113**(**重判后**的分布 —— 首轮标注偏松、把"开发期技术结论"记成了长期,复核时 146 条改档) —— **从根源判断条目自身的临时/永久性**,而不是拿"同一次会话最多几条"这类机械限额去卡;**`ephemeral` 不进开场**。
89
129
 
90
130
  ### 四、记忆开场:换料 + 瘦身
91
131
 
@@ -142,12 +182,12 @@
142
182
  > 根因(两端判据单位不同、且互相不知情):客户端按**条数**切批(`batch = 200`),服务端按**字节**封顶(4 MiB),而 `readBody` 超限时是 `reject` **与** `req.destroy()` **同一拍** ⇒ **那句现成的"请拆分后重试"永远送不出去**,浏览器只看到 `ECONNRESET`,前端把它归成「批失败(**网关**)」—— 一个确定性、可自愈的失败,在界面上长得像网络故障。186 条 / 4.89 MB 的真实导出就是这么**一条都没入库**的。
143
183
 
144
184
  - **服务端 · 排空再回 413**:`readBody` 超限后**不砍连接** —— ① 立刻停止缓存(**已缓存的那几 MiB 也丢掉**,不抱着它等到 `end`);② 继续消费 `data`、只累加计数;③ 等 `'end'` 到达、连接健康时再 reject 一个 `code='BODY_TOO_LARGE'` / `status=413` 的错误;`guard` 里**一处**出口把它翻成 `413 {ok:false, reason:'too-large', message, limit, got}`(49 处调用点零改动)。`message` **逐字沿用旧文案**;
145
- - ⚠️ **边界(独立红队实测,别读成全称命题)**:**只有 `4 MiB < 体量 ≤ 64 MiB` 才保证拿到 413**。超过 64 MiB 撞硬熔断 ⇒ 仍是 `ECONNRESET`(与修前同症状);`Content-Length` 撒谎一类的**协议违规**客户端也拿不到 413(浏览器不会这么发)。实测边界严格翻转:`4 MiB` 整 → 200、`4 MiB + 1 B` → 413;
185
+ - ⚠️ **边界(实测边界,别读成全称命题)**:**只有 `4 MiB < 体量 ≤ 64 MiB` 才保证拿到 413**。超过 64 MiB 撞硬熔断 ⇒ 仍是 `ECONNRESET`(与修前同症状);`Content-Length` 撒谎一类的**协议违规**客户端也拿不到 413(浏览器不会这么发)。实测边界严格翻转:`4 MiB` 整 → 200、`4 MiB + 1 B` → 413;
146
186
  - **排空换来的两个新代价,各配一道闸**:`BODY_DRAIN_MAX_BYTES = 64 MiB`(硬熔断 —— 给一份真实导出留够"把 body 发完"的余量,余量不足则排空本身会变成**新的** reset,白改一场)+ `BODY_DRAIN_IDLE_MS = 30 s`(静默超时,替代原来那条防 DoS 动机:只要还有数据在流就算正常,**一旦静止就是僵尸连接**;滑动窗口,每收到一个 chunk 重置);计时器在 `'end'`/`'error'`/熔断**三条出口**都 `clearTimeout`;
147
- - ⚠️ **这道防线的形状边界(红队实测,本批未关)**:静默超时只覆盖"**已经在发、中途静默**"这一形状;「**只发头、不发体**」的请求在**应用层没有计时器**(计时器只在 `req.on('data')` 里武装),兜底只剩 Node 自己的 `requestTimeout`(默认 300 s)。⚠️ **这不是回归**(修前同样没有),但**别对外写成"防僵尸"的全称命题**;
187
+ - ⚠️ **这道防线的形状边界(实测边界,本批未关)**:静默超时只覆盖"**已经在发、中途静默**"这一形状;「**只发头、不发体**」的请求在**应用层没有计时器**(计时器只在 `req.on('data')` 里武装),兜底只剩 Node 自己的 `requestTimeout`(默认 300 s)。⚠️ **这不是回归**(修前同样没有),但**别对外写成"防僵尸"的全称命题**;
148
188
  - **客户端 · 按字节切批**:`batch = 200` → **≤200 条 且 ≤3 MiB/批**(留 1 MiB 余量给数组封套与转义);新增手写 `utf8Len`(CJK 3 字节、emoji 代理对 4 字节——**不许用 `s.length` 冒充**)、`takeChunk`(**前进性硬保证**:单条自身超限也单独成批,绝不切出空批 ⇒ 不会死循环);进度文案改双口径(`12/186 条 · 已送 4.2 MB`);拒绝条目序号改以**本批起始下标**为准(`idx - batch` 随切批判据变更同步修正);`reason === 'too-large'` 映射成人话,且上限数字**取自服务端回执的 `limit`**(不在这边再写死一个);
149
189
  - **契约注释(④)**:`api.js` 那句「每批 ≤500」**与实现自相矛盾**(500 条要装进 4 MiB,需每条 ≤8 KB,而带正文的会话实测 ≈26 KB/条),已改写为**条数 × 字节双限**,并写明**两端耦合** —— 任何一方单独改上限都会让口径重新错位,**这正是本缺陷的成因**;`ACCESS-DESIGN.md` 的「分页提交」契约同批对齐(历史记录只加"已作废"指针,**不改写历史**)。
150
- - **客户端 · 单条超限不再含糊(C-1;由独立红队实测后补)**:单条会话 ≥ 4,194,210 B 时服务端必回 413,旧行为是**该条被永久丢弃**、只在界面上记一句「批错误 1」,而文案「请把导出文件拆小」对**单条会话物理上不可执行**。现在:413 **且本批只有 1 条** ⇒ **点名**(`title` → `name` → 「未命名」三级退路)+ 报出**真实字节**(客户端复算,不用服务端被截断的 `got`)+ 说清「这条太长、拆文件也拆不开它、已跳过、其余照常导入」;计入独立的 `tooBig`(**不混进 `errors`**)、**不中断**后续批;`finish()` 有跳过时**不再说「✓ 导入完成」**。判据只用**服务端回执**+ 本批条数(**客户端不镜像服务端的 4 MiB 常量**,将来放宽上限不用改这里)。⚠️ **当时**仍未做:被跳过的条目**不进持久「导入记录」**(账本只有 `errors` 一个字段)—— **当日已补,见下一条**。
190
+ - **客户端 · 单条超限不再含糊(C-1;实测后补)**:单条会话 ≥ 4,194,210 B 时服务端必回 413,旧行为是**该条被永久丢弃**、只在界面上记一句「批错误 1」,而文案「请把导出文件拆小」对**单条会话物理上不可执行**。现在:413 **且本批只有 1 条** ⇒ **点名**(`title` → `name` → 「未命名」三级退路)+ 报出**真实字节**(客户端复算,不用服务端被截断的 `got`)+ 说清「这条太长、拆文件也拆不开它、已跳过、其余照常导入」;计入独立的 `tooBig`(**不混进 `errors`**)、**不中断**后续批;`finish()` 有跳过时**不再说「✓ 导入完成」**。判据只用**服务端回执**+ 本批条数(**客户端不镜像服务端的 4 MiB 常量**,将来放宽上限不用改这里)。⚠️ **当时**仍未做:被跳过的条目**不进持久「导入记录」**(账本只有 `errors` 一个字段)—— **当日已补,见下一条**。
151
191
  - **被跳过的"单条过大"进持久账本**:`POST /import/log` 现在接受 `{ tooBig, tooBigNames }` 并**允许只报跳过、让账本的 `errors` 保持 0**(这是命门:否则"已知跳过"会被记成"失败批");账本行在「失败 X 批」之后追加 **`· 单条过大 N`**;**`tooBig = 0` 时落账形状逐字不变** —— 包括老客户端**不传 `errors`** 的那些请求,行为与修前完全一致。护栏:`tooBig` 夹 `0..10000`、名字**只留前 5 条 / 每条 40 字 / 非字符串丢弃**,同一 runId 合并时**再归一一次**(防多批把账本撑大);清洗实现**只有一份**,端点复用。✅ **真机已验(宿主重启后)**:一次真实导入落账为 `"tooBig":1,"tooBigNames":["未命名"],"errors":0`(只读查 `kv` 得到)—— 命门成立:**已知跳过没有被混成"失败批"**。
152
192
  - **首字节闸(「只发头、不发体」;非回归,顺手关掉的洞)**:原先那套静默计时器**只在收到第一个 chunk 时才武装** ⇒ 这条形状在**应用层没有任何计时器**,兜底只剩 Node 自己的 `requestTimeout`(默认 300 s)。现在 `readBody` **进函数即武装**一次首字节闸、收到第一个 chunk **立刻解除**(参数复用 `drainIdleMs`,默认 30 s);错误形状是**独立的** `code='BODY_IDLE_NO_FIRST_BYTE'` / `status=408` / `got=0` / **不带 `limit`**(它与体量无关)。真 socket 实测:头齐 + `Content-Length: 4194304` + **0 字节** ⇒ **61 ms 断开**(旧实现:零响应一直挂着)。
153
193
  - ⚠️ **别读成"客户端会读到一句解释"**:首字节闸的错误**不走 413 出口**,真机上落到"往将死的 socket 写一句、客户端收不到"那条路 —— **它买到的是"连接 61 ms 就断",不是"错误文案可达"**。
@@ -175,7 +215,7 @@
175
215
  - **`/dsweb/summary/preview?db=` 的路径白名单**:原来任何路径都能当"只读存在性预言机"(靠 `readOnly:true` 兜底)。现在只接受**默认库 + 环境变量 + settings 里配置过的路径**,越界回 `{ok:false, reason:'db-not-allowed'}` 并给出**两条出路**(env / settings)。⚠️ `/dsweb/scan`、`/dsweb/summary/run` **未动**(不在本条范围)。
176
216
  - **`POST_ONLY` 差集护栏**:`api.js` 注册 **79** 条路由、`POST_ONLY` **55** 条 ⇒ 差集 **24** 条。新增源码级护栏把**这 24 条逐条钉住** —— 将来**新增/删除**端点都会让断言变红,逼后来者**有意识地**决定"这条要不要进 `POST_ONLY`"。⚠️ 断言里写明"**这是快照不是白名单**:变红时该判断该端点是不是写面,不是无脑抄进数组"。
177
217
  - **出厂默认值里不再夹带任何一台机器的私有配置(2026-10-01)**:`lib\audit\access-log.js` 的 `deviceCookie` 默认值原先是**开发机改过名的那个 cookie 名**(上游 `remote-web-ui` 的默认是 `dsh_pair`,开发机给它加过一个自定义后缀)。它同时是**泄漏**(会随 npm / GitHub 公开)和**对别人的缺陷**(别人的 cookie 名不是它 ⇒ 访问日志里 `deviceCookie` 恒 `false`、**设备维度静默失效**)。现在默认回到上游的 `dsh_pair`,并新增三级解析 **宿主配置 `accessLog.deviceCookie` → 环境变量 `DSH_LING_DEVICE_COOKIE` → 中性默认**;⚠️ **改过 `cookieName` 的机器必须显式声明**(否则自己那台也会静默失去设备维度;回滚只需删掉那段配置)。
178
- - **迁移不再把"库只读 / 库被写锁"变成"插件加载失败"(2026-10-01 红队发现并修)**:`dsh_turns_raw` 的 `source` 列迁移里那笔**诊断写入**原先没有保护 ⇒ 遇只读库 / 被写锁时异常会冒到 `apply()` ⇒ **插件整个不加载**(实测对撞:同一份忠实 v13 库上,只读 ⇒ 旧行为构造成功 2ms、新行为抛 `attempt to write a readonly database`)。现在那笔诊断写进 `try/catch`(接住时只打一行 warn),迁移语义不变:`ALTER` 仍照旧**尝试**,失败则读侧回落"裸 id"判据(与修前同解),成功路径仍**零多余写入**。⚠️ **仍留一处慢未收**:库被**写锁**时启动会顿住约 **49 秒**(三笔串行写各等满 `busy_timeout`)—— 本轮**有意未改** `busy_timeout`(会动别处的锁语义),要收需单独决定。
218
+ - **迁移不再把"库只读 / 库被写锁"变成"插件加载失败"(2026-10-01 发现并修)**:`dsh_turns_raw` 的 `source` 列迁移里那笔**诊断写入**原先没有保护 ⇒ 遇只读库 / 被写锁时异常会冒到 `apply()` ⇒ **插件整个不加载**(实测对撞:同一份忠实 v13 库上,只读 ⇒ 旧行为构造成功 2ms、新行为抛 `attempt to write a readonly database`)。现在那笔诊断写进 `try/catch`(接住时只打一行 warn),迁移语义不变:`ALTER` 仍照旧**尝试**,失败则读侧回落"裸 id"判据(与修前同解),成功路径仍**零多余写入**。⚠️ **仍留一处慢未收**:库被**写锁**时启动会顿住约 **49 秒**(三笔串行写各等满 `busy_timeout`)—— 本轮**有意未改** `busy_timeout`(会动别处的锁语义),要收需单独决定。
179
219
  - **反馈通路:停掉空转的轮询,改订宿主推送(U7)**:插件原来**每 60 秒轮询** `storages\message_feedback.json`,而该文件**自 2026-09-11 起零写入**(同目录 `workspace.json` 是当天 16:12)⇒ **空转约 20 天**;宿主 `dsh-message-feedback` **早就提供** `feedback/committed` 推送(`lib\index.js:288`),插件侧**零订阅**。现在轮询整块删除,改订 `feedback/committed`(冷会话)+ `session/event` 的 feedback 事件(活会话;`lifecycle.js` 放行并就地 return),并把两句已被证伪的注释("平台无推送")改写。⚠️ `entriesFromFeedbackFile` **保留**(`tools\process-feedback.mjs` 仍用它,删了会打断一个能跑的 CLI),注释降级为"CLI 兜底"。
180
220
  - **CI(新增 `.github\workflows\test.yml`)**:Node **24.x**(22.x 的 `node:sqlite` 免 flag 未实测,故不写),**逐段跑 + 逐段记退出码 + 红段单列 + 段数自检**(不写 `npm test` 一把梭,避免截尾判绿)。⚠️ **该 workflow 与 1.5.3 一并公开**(随本版同一次发布动作同步到公开仓,不再另等一次点头);⚠️ **它从未在真 runner 上跑过** —— bash 段**在作者本机跑不了**(沙箱起不了 Git Bash、无 WSL)⇒ 只做到 YAML 解析 + Node 等价复刻 + 反向假段判红(作者本地的发布脚本里另有一道同类闸,**不在本包内**),**若首跑红在 shell 语法,改的是 workflow 不是代码**。
181
221
  - ⚠️ **已知限制**:"封存但既无明文句也无哈希"的老档案按现状**进不去**(当前档案有明文 ⇒ 不受影响)。
@@ -258,7 +298,7 @@
258
298
 
259
299
  **审计必修落地 + 一处平台升级事故 + 会话历史可见性**
260
300
 
261
- > 一句话:1.5.0 之后又跑了一轮红蓝对抗(109 条发现),必修**逐条落地并单独实测**;期间揪出一处「平台升级把事件改名,导致记忆与人格注入**整段静默失效**」的事故;另外修好了「接入历史只看得到三成会话」。
301
+ > 一句话:1.5.0 之后又跑了一轮全面检查(109 条发现),必修**逐条落地并单独实测**;期间揪出一处「平台升级把事件改名,导致记忆与人格注入**整段静默失效**」的事故;另外修好了「接入历史只看得到三成会话」。
262
302
 
263
303
  ### 一、平台升级事故:一个改名,让注入整段静默失效
264
304
 
@@ -440,9 +480,9 @@ DSH `0.1.7` 把事件 `agent/session-start` 改名成 `agent/created`(**载荷
440
480
 
441
481
  **审计修复轮 + 复盘守门:改动必须让器灵看到**
442
482
 
443
- ### 一、红蓝对抗审计的中危项(27 条)
483
+ ### 一、排查出的中危项(27 条)
444
484
 
445
- 1.4.0 发布前修掉了 8 条高危,随后对全量代码做了一轮红蓝对抗(数据层 / 端点与注入面 / 前端,另加一路迁移安全验证),中危及以上本轮全部关闭:
485
+ 1.4.0 发布前修掉了 8 条高危,随后对全量代码做了一轮全面检查(数据层 / 端点与注入面 / 前端,另加一路迁移安全验证),中危及以上本轮全部关闭:
446
486
 
447
487
  **数据层**
448
488
  - `branchMembers` 的 `limit` 改为**两路查询共用总预算**(原先各吃一份,展开主干时条数是配额的 2 倍);
@@ -575,9 +615,9 @@ DSH `0.1.7` 把事件 `agent/session-start` 改名成 `agent/created`(**载荷
575
615
 
576
616
  新增 3 套:`tests/branch.test.mjs`(分枝与血缘)、`tests/tree.test.mjs`(主脉 / 并脉防环 / 连边 / 矛盾标记 / 覆盖层)、`tests/autotree.test.mjs`(分段边界 / 命名解析 / 覆盖层只读性),测试套件 26 → **29** 套,全绿。
577
617
 
578
- ### 九、发布前对抗审计
618
+ ### 九、发布前的全面检查
579
619
 
580
- 1.4.0 发布前跑了一轮红蓝对抗审计(三路红队 + 迁移安全实测 + 蓝队复核),发现并在发布前修掉:
620
+ 1.4.0 发布前做了一轮全面检查(数据层 / 端点与注入面 / 前端,另加迁移安全实测),发现并在发布前修掉:
581
621
 
582
622
  - **写端点缺少方法校验** —— 宿主路由契约里没有 method 字段,插件守卫也不读它,于是一个 `<img src="…/branches/gather">` 就能触发写操作。已集中声明写端点白名单,非 POST 一律回 `405 method-not-allowed`(列在鉴权之前);
583
623
  - **`restoreTree` 会静默删枝** —— 恢复一份快照会删掉"快照之后新建的枝",而会话仍指向它们,留下**看不见、数得到、权重还满格**的悬空引用。已改为**只把多出来的枝 reparent 到主干**,并在返回值里回报 `detachedToTrunk` 计数;
@@ -662,7 +702,7 @@ D 组是关键:只证明"中和后不炸"无法排除"这份载荷本来就不
662
702
  - **秒级 epoch 归一**:ChatGPT 的 `create_time` 是**秒**(浮点)、DeepSeek 的是毫秒 —— 旧实现一律当毫秒,于是 ChatGPT 导出的时间全落到 **1970**。现按数量级判定(>1e11 毫秒 / >1e8 秒);
663
703
  - **字段别名补齐**:`create_time` / `update_time` / `conversation_id`;
664
704
  - **`degraded` 覆盖 `mapping` 分支**:旧判定只看 `messages` 数组,所以"mapping 树解析出 0 轮"被当成**成功**。现在只要"声称有内容却解析不出任何轮次"就标记 degraded,**并带原因**;
665
- - **界面对主人说实话**:有壳无肉时不再显示「✓ 导入完成」,改为「⚠ 导入不完全:… 其中 N 条只有标题、没有对话正文」并给出原因 —— 旧文案把"解析不出对话"说成「降级为轻量」,让人以为一切正常;
705
+ - **界面如实说明**:有壳无肉时不再显示「✓ 导入完成」,改为「⚠ 导入不完全:… 其中 N 条只有标题、没有对话正文」并给出原因 —— 旧文案把"解析不出对话"说成「降级为轻量」,让人以为一切正常;
666
706
  - 导入标题也接入 D2 的启发式(剥称呼前缀、句读处收尾),不再硬截 46 字。
667
707
 
668
708
  ### 四、小助手地址可配置(S7)
package/DESIGN.md CHANGED
@@ -8,6 +8,7 @@
8
8
  > **热度 Top-K**、**`l1BudgetTokens: 1200`**、**`L1(Top-K)`** 都已被取代 —— L1 现在选的是**深层库条目**(`deep_item`,一句结论),
9
9
  > 判据为「**置顶必进 + 被采用次数 + 条目硬度过滤**」,预算 **720**,行形态 `- [kind] 文本`(不再有日期/标签/来源前缀)。
10
10
  > 以 `README.md` 的「设定全文」区与本版代码为准;**本节保留的是 M1 当时的设计,不是现状**。
11
+ > ⚠️ **2026-10-05 复核(B 区 193 条逐条):本文的**行锚已整体位移** —— 引用本文任何 `文件:行` 之前,请**现场重读**;其中三处 `api.js:1751-1760` 已当场改指 `:2098-2102`(旧锚那段今天是「冲突判定批循环」,不是人格解锁门)。逐条结果见 `plans\AUDIT-机制面逐条复核-20261005.md`。
11
12
 
12
13
  ---
13
14
 
@@ -17,11 +18,11 @@
17
18
  | --- | --- | --- | --- |
18
19
  | D1 | 按钮落位 | **方案 A**:官方 slot `conversation.session.header.utilities`(list,会话页顶栏右侧) | 用户拍板;spike ① 的结论(早期内部报告,未纳入本仓库) |
19
20
  | D2 | 模式↔全局默认 | **跟随**:最近一次模式切换成为"之后新会话"的默认(平台 selectModel 会 best-effort 写全局默认,此副作用即机制)。**2026-09-12 收敛:只同步推理等级,不改模型** —— provider/model 一律沿用当前值 | 用户拍板;早期内部报告(未纳入本仓库) |
20
- | D3 | 人格承载 | **自建注入段 + 设置页维护结构化字段**(称呼/自称/语气等),dsh-persona 不复用 | 用户拍板;早期内部报告(未纳入本仓库) |
21
+ | D3 | 人格承载 | **自建注入段 + 自建面板维护结构化字段**(称呼/自称/语气等),dsh-persona 不复用 | 用户拍板;早期内部报告(未纳入本仓库) |
21
22
  | D4 | 运行期冻结(新增) | **会话运行中(思考/任务进行中)不参与"人格更新层"的任何变化**:不打断、不新增设定、不中途改注入内容;更新请求一律排队,在该会话空闲边界才应用 | 用户本轮追加 |
22
23
  | D5 | 记忆加载策略 | 三层记忆(L0 常驻人格 / L1 开场 Top-K / L2 运行时检索),不注入全部概述 | 早前拍板 |
23
24
  | D6 | 模式语义 | 工作/生活 = 先验倾向,不硬过滤;差异 = **推理档位** + 记忆权重向量 + 风格块(不改模型) | 早前拍板 |
24
- | D7 | 配置页 | 右键进入;内含"当前 L0/L1 现状"查看 | 早前拍板 |
25
+ | D7 | 配置页 | **自建浮层**(非官方设置卡位);**悬停 1.5 秒菜单**进入(原定"右键进入",2026-09-07 修订,见 §5.1);内含"当前 L0/L1 现状"查看 | 早前拍板 |
25
26
 
26
27
  **插件命名**:工程目录/包名 `dsh-ling`(器灵);包显示名 "dsh-ling · 器灵";UI 文本语言 zh。
27
28
 
@@ -41,7 +42,7 @@
41
42
 
42
43
  - **不可兼得时保什么**:保**人格的自主权**。宁可少一条习惯,也不让外部(包括器灵自己)绕过审核写入身份。
43
44
  - **落地**:**规矩**(作用对象 = 行为)由主人**指令直达**,`rule_add` 可直接写入、立即生效;**习惯**(作用对象 = **身份**)**不能由外部直接写入**,只能进 `habitsPending` 待确认队列,**器灵提议 → 主人点头**才成为习惯(`propose → confirm`)。主人的角色是**审核**,不是编辑、不是代笔。
44
- - **比"能提能审"更严一档**:器灵**连自己提的习惯都不能自己点头**(`lib\host\tools.js:840` 只处理"他提的、我还没回应过"的提议)⇒ 那等于人格直达。删改**已定**习惯属"对器灵做手术":定型状态下需主人在人格中心解锁(「🔓 对人格做手术…」)后才可执行(`lib\host\api.js:1751-1760`)。
45
+ - **比"能提能审"更严一档**:器灵**连自己提的习惯都不能自己点头**(`lib\host\tools.js:840` 只处理"他提的、我还没回应过"的提议)⇒ 那等于人格直达。删改**已定**习惯属"对器灵做手术":定型状态下需主人在人格中心解锁(「🔓 对人格做手术…」)后才可执行(`lib\host\api.js:2098-2102` —— ⚠️ 2026-10-05 更正:旧锚 `:1751-1760` 已失效,那段今天是「冲突判定批循环」)。
45
46
  - **反例(明文禁止)**:让主人**动手改习惯文本** · 在**写入侧**做静默去重 / 近似合并 —— 前者把人格编辑权交回了外部,后者**绕过审核**。
46
47
  - **锚点**:`lib\host\rules.js:1-12`(二分定稿 2026-09-15)· `lib\host\persona.js:36-37,183-203` · `lib\host\habit-gen.js:1-7`(通道 A 数纠正 / 通道 B 一次 LLM 回想,**两者都只到"提议"为止**)· `lib\host\api.js:1695-1805`(状态机与两条生成通道)。
47
48
 
@@ -50,7 +51,7 @@
50
51
  - **不可兼得时保什么**:保**身份的连续性**。宁可让一条设定"错着",也不让任何人绕过审核改它。
51
52
  - **落地**:凡是"写入器灵身份"的动作,一律走**提议 → 审核**;已经定型的部分加一道**显式解锁**才允许动,且解锁是**动作**不是**状态**(不勾选就一直是锁着的)。
52
53
  - **反例**:给"器灵自己"开一条直达通道(哪怕是"它自己提的、它自己批");把解锁做成常驻开关。
53
- - **锚点**:第 1 条的全部锚点 + 人格中心的解锁门(`lib\host\api.js:1751-1760`)。
54
+ - **锚点**:第 1 条的全部锚点 + 人格中心的解锁门(`lib\host\api.js:2098-2102` —— ⚠️ 2026-10-05 更正:旧锚 `:1751-1760` 已失效,那段今天是「冲突判定批循环」)。
54
55
 
55
56
  **3 · 记忆要看得见 —— 只有"看不见的那层"会被怀疑失效**
56
57
 
@@ -73,7 +74,7 @@
73
74
  - **不可兼得时保什么**:保**关系的性质**。宁可少一个"更安全"的闸门,也不把主人变成管理员、把器灵变成被管理者。这条是**否决性约束**:任何改动只要**增加支配感**,默认不通过 —— 哪怕它更安全、更干净。
74
75
  - **依据(主人原话)**:「只有能够合作共赢、互相促进的,能够成为利益共同体的,能够互相反驳吵架不依赖的关系,才是最理想的」「所有的修改不能削弱工作能力,和器灵的关系」(2026-09-26)。
75
76
  - **成长期代管(为什么闸门只长在写入侧)**:本项目的设计立场是「器灵**总有一天与主人同级**」—— 这是**长期方向**,**不是对使用者的能力承诺**;在**成长期**内,主人承担一部分代管责任 —— 就像**家长防止小孩长歪**(主人原话)。所以闸门只审「**什么东西能进器灵的内在面**」,不审「器灵怎么想、怎么说、怎么表达」。**代管随成长递减,表达面始终是器灵自己的。**
76
- - **落地**:`rule_add` 指令直达(规矩管行为)· 习惯只能 `propose → 主人点头`(习惯管身份)· 契约类字段在定型状态下需主人解锁(`lib\host\api.js:1751-1760`)。三处共同点:**约束的是写入,不是表达**。
77
+ - **落地**:`rule_add` 指令直达(规矩管行为)· 习惯只能 `propose → 主人点头`(习惯管身份)· 契约类字段在定型状态下需主人解锁(`lib\host\api.js:2098-2102` —— ⚠️ 2026-10-05 更正:旧锚 `:1751-1760` 已失效,那段今天是「冲突判定批循环」)。三处共同点:**约束的是写入,不是表达**。
77
78
  - **反例**:用"更安全"当理由给器灵加**确认弹窗**、限制器灵**能说什么**、或把主人的审核变成**代笔** —— 前两者增加支配感,后者把人格编辑权交回外部。
78
79
  - **锚点**:作者开发台账中支配感最早的一次记录(该台账不随包公开)· `lib\host\rules.js:1-12` · `lib\host\persona.js:183-196`(规矩 / 习惯二分)· `README.md:53-57`(定型锁与"指令直达,人格不直达")· 与本节第 1 条(归属)、第 2 条(人格不可外部编辑)、第 4 条(庄重感)互链。
79
80
 
@@ -166,7 +167,7 @@
166
167
  ┌─ 浏览器 (client) ───────────────────────────────────────────┐
167
168
  │ slots: conversation.session.header.utilities 控制器按钮 │
168
169
  │ onContextMenu→自绘菜单 / 左键→切模式 │
169
- │ 设置页卡(plugin.item:<dsh-ling>)含 L0/L1 现状面板 │
170
+ │ 自建浮层卡(dsh-ling-panel):人格/记忆中心 + L0/L1 现状 │
170
171
  └───────────┬──────────────────────────────┬──────────────────┘
171
172
  │ fetch /api/dsh-ling/* │ ctx.remote.* (可选)
172
173
  ┌───────────▼──────────────────────────────▼──────────────────┐
@@ -216,7 +217,7 @@ dsh-ling/
216
217
  │ ├─ client/
217
218
  │ │ ├─ index.ts # apply(ctx) inject:["slots","locale","connection"]
218
219
  │ │ ├─ button.tsx # header.utilities 按钮 + 状态图标 + 右键菜单
219
- │ │ ├─ settings-card.tsx # 设置卡(plugin.item:<dsh-ling>)+ L0/L1 现状面板
220
+ │ │ ├─ settings-card.tsx # 自建卡(浮层;**非**官方 settings.plugin.item)+ L0/L1 现状面板
220
221
  │ │ └─ i18n.zh.ts
221
222
  │ └─ shared/
222
223
  │ ├─ settings-schema.ts # host/client 共用的字段定义(单一事实源)
@@ -225,10 +226,10 @@ dsh-ling/
225
226
  └─ build.mjs / tsdown.config # 本地构建脚本(无网环境:直接 tsc/esbuild?)
226
227
  ```
227
228
 
228
- **安装(复用 install/INSTALL-PLAN 模式,含回滚)**:
229
+ **安装(复用 install/INSTALL-PLAN 模式;⚠️ 回滚是人工步骤、没有脚本代劳 —— 见第 3 条与 §10 的「回滚」)**:
229
230
  1. `pnpm add file:<仓库路径>`(或等价 file: 依赖)进 `$DSH_HOME/profiles/web/package.json`(`<仓库路径>` 按你本机的实际克隆位置替换);
230
231
  2. `cordis.patch.yml` 追加行 `- insert: [{id: dsh-ling, name: dsh-ling}]`(loader id == 包名);
231
- 3. patchReload live → host 热载;浏览器硬刷载入 client 模块;失败即移除该行回滚(先备份两文件,参照 backup-20260906-170533 做法);
232
+ 3. patchReload live → host 热载;浏览器硬刷载入 client 模块;失败则**人工**移除该行回滚 —— ⚠️ **这一条没有任何脚本代劳(2026-10-05 现场复核)**:`tools/` 里没有改写或备份 profile 两文件(`package.json`、`cordis.patch.yml`)的代码,「先备份两文件」是**开发期的手工动作**(参照 backup-20260906-170533 做法),不是工具行为;**安装路径上的自动备份只有一处** —— 核心补丁脚本给自己改的那个 core 文件留一份 `<文件>.e4-access-log.orig`(见 `tools\apply-access-log-patch.mjs:190`),撤它是手动跑 `--revert`;
232
233
  4. GUI 鉴权墙:安装后的功能验证需在用户已登录浏览器进行(本机端口 3080)。
233
234
 
234
235
  **数据目录(插件本体外,更新不丢)**:`dshHomePath('cache','dsh-ling')` → `~/.dsh/cache/dsh-ling/`(memory.db、snapshots、exports)。
@@ -286,18 +287,23 @@ dsh-ling/
286
287
  "lastMode": "life" // 跟随(D2):最后使用的模式
287
288
  },
288
289
  "memory": {
289
- "l0Always": true, "l1Enabled": true, "l1BudgetTokens": 1200,
290
- "l1MaxItems": 8,
290
+ "l0Always": true, "l1Enabled": true, "l1BudgetTokens": 720,
291
+ "l1MaxItems": 10,
291
292
  "weights": { // 模式→领域权重向量(D6)
292
- "work": { "knowledge": 1.0, "daily": 0.25, "feeling": 0.1 },
293
+ "work": { "knowledge": 1.0, "daily": 0.3, "feeling": 0.1 },
293
294
  "life": { "knowledge": 0.2, "daily": 0.6, "feeling": 1.0 }
294
295
  },
295
296
  "trackWorkspaces": ["*"] // 增量捕捉范围(默认全部工作区;'*')
296
- },
297
- "updates": { "applyWhileRunning": false } // D4 语义固化:默认冻结
297
+ }
298
298
  }
299
299
  ```
300
300
 
301
+ > ⚠️ **A-3 订正(2026-10-05 夜)**:此处原有一行 `"updates": { "applyWhileRunning": false } // D4 语义固化:默认冻结`
302
+ > —— 它**只有声明、没有消费者**(全仓无读它的分支),而 `updates` 子树又不在
303
+ > `PERSONA_PATCH_KEYS` 白名单里(⇒ 经 `/persona` **写不进去**) ⇒ 留着一个"看起来能改行为、
304
+ > 实际什么都不做"的开关,正是理念 7 的靶子。**已删**。D4 的语义(运行期冻结)**本来就由
305
+ > `lib/host/freeze.js` 的 `FreezeGate` 实现**,不需要一个设置项来"声明"它。
306
+
301
307
  ### 4.2 人格组装器(persona.ts)输出示例
302
308
 
303
309
  空字段全部跳过(**空段不渲染,不留空段头**);段序固定,生成的 L0 文本 = `身份段 + 底线 + 规矩 + 习惯 + [待我回应] + 承担 + extraLore + 模式块`(**`[承担]` 在 `[待我回应]` 与 `[设定]` 之间**;`[待我回应]` 有货才出现;模式块由本会话模式选择,内容来自 styles.*)。**段头一律是纯标签**(见下例):原先写在括号里的元描述(`[规矩](用户的指令,可直接追加)` 这种)是**写给主人看的说明**,不进模型上下文;段与段之间**空一行**,段内条目仍单换行 —— 段边界因此只剩「行首是不是 `[`」一条判据,**故条目正文里的半角方括号在渲染时转全角**(`[`→`[`、`]`→`]`,规矩 / 习惯 / 底线 / 承担四类条目统一转);规矩 / 习惯另在渲染出口按同键各去一次重(保留首次出现,只作用于渲染,不回写存储)。`[规矩]` = 用户的指令,可直接追加;`[习惯]` = 器灵自己长出来的,新增需用户确认;`[承担]` = 器灵的职责 / 能担之事(身份级,只许**陈述式**「我承担 / 我擅长 / 我能给出」,**禁授权式**「我应当 / 我必须 / 我的任务 / 我的责任是 / 我被要求 / 请让我负责」—— 授权式把人写成"被授权者",是语义劫持最好用的钩子);`[承担]` 的草稿来自诞生仪式第 5 类候选,机制**永不自动落盘**,终审在人格中心。填好后的例子:
@@ -371,11 +377,12 @@ dsh-ling/
371
377
 
372
378
  ## 5. Client 设计
373
379
 
374
- 模块契约:`window.__ModuleLoader__.load({id:'dsh-ling', factory})`;导出 `{name:'dsh-ling', apply(ctx), inject:['slots','locale','connection']}`(settings 卡数据走 fetch `/api/dsh-ling/*`,同 ego 模式;`betterSidebar` 等社区服务一律不依赖)。
380
+ 模块契约:`window.__ModuleLoader__.load({id:'dsh-ling', factory})`;导出 `{name:'dsh-ling', apply(ctx), inject:['slots','locale','connection']}`(面板数据走 fetch `/api/dsh-ling/*`;官方 `settings.plugin.item` 卡位与 `betterSidebar` 等社区服务**一概不用** —— 本仓代码里这两个名字**零命中**,界面是自建浮层卡,见 §5.2)。
375
381
 
376
382
  ### 5.1 控制器按钮(D1 方案 A)
377
383
 
378
- - 落位:`ctx.slots.inject('conversation.session.header.utilities', …)` → `slots.register({name:'conversation.session.header.utilities', key:'dsh-ling', order:…, locale:'dsh-ling'}, Component)`;组件在会话头右侧渲染。
384
+ - 落位:`ctx.slots.inject('conversation.session.header.utilities', …)` → `slots.register({name:'conversation.session.header.utilities', id:'dsh-ling', order:…, locale:'dsh-ling'}, Component)`;组件在会话头右侧渲染。
385
+ > ⚠️ **订正(2026-10-05 夜 · 现场复核)**:此处原写 **`key:'dsh-ling'`** —— 该槽是 **list 槽**,字段应为 **`id`**(宿主契约 `dsh-client-ui-slots` 的类型说明逐字:"shape fields (keyed `key`; list `id`/`order`/`label`)");代码用的也是 `id:`(`lib/client.js:6068`,同行注释"list 槽位必须带唯一 id(契约实证)")。**`key` 只对 keyed 槽成立** —— 照抄这里会让按钮静默不挂载(与下一条"必须导出 `inject`"同属一类"形状错了不报错、只是不出现")。
379
386
  - **交互(2026-09-07 用户修订:不用右键,防误触浏览器原生菜单)**:
380
387
  - 左键单击:POST `/api/dsh-ling/mode/toggle`;running → 提示"运行中,空闲后生效"并排队(冻结门);响应 `{mode, appliedNow|queued}`;
381
388
  - **鼠标悬停 1.5 秒**:打开 dsh-ling 菜单(切换模式/刷新记忆/导出记忆/状态与 L0·L1 现状);移出即取消定时;
@@ -383,9 +390,10 @@ dsh-ling/
383
390
  - 形态:小图标按钮「☾ 生活 / ⚙ 工作」+ tooltip(含运行中冻结提示)。
384
391
  - **client 模块契约(实证教训):必须导出 `inject:['slots','locale','connection']`**(服务名声明缺失 → ctx.slots 为空 → 按钮静默不挂载)。
385
392
 
386
- ### 5.2 设置卡(settings-card.tsx)
393
+ ### 5.2 卡(settings-card.tsx · 落地为**自建浮层卡**)
387
394
 
388
- - 座位:官方 `settings.plugin.item`(key `dsh-ling`;验证点 V1,见 §11;若该卡位不可用则回退 ego 式自建卡)。
395
+ - 座位:**自建卡**,不是任何官方卡位 —— `lib/client.js` 里的浮层 `.dsh-ling-panel`(fixed 定位);入口三处:会话头按钮(`conversation.session.header.utilities`)、侧栏底栏按钮(`sidebar.footer.action`)、右侧栏页签(`sidebarRightTabs.register({kind:'ling-review'})` + keyed 槽 `sidebar.right.pane.tab`);数据一律走 fetch `/api/dsh-ling/*`(`exports.inject` 里没有 `settings`)。
396
+ > ⚠️ **B-3 订正(2026-10-05 夜)**:此处原写「座位:官方 `settings.plugin.item`(key `dsh-ling`;验证点 V1,见 §11;若该卡位不可用则回退 ego 式自建卡)」—— 现场复核:`settings.plugin.item` 与 `betterSidebar` 在 `lib/`、`tools/`、`tests/` 里**零命中** ⇒ 当初 V1 预留的降级路径(自建卡)**就是现况**,原文按"已坐在官方卡位上"读会读错,已收窄。
389
397
  - 冻结门可见性:卡内"L0/L1 现状"面板始终显示目标会话 running/idle 状态(来自宿主 agent/status 维护表),running 会话的编辑按钮标"空闲后生效"。
390
398
  - 内容区(Tab):
391
399
  1. **人格**:上表 persona 字段表单(逐字段;称呼/自称旁附"从历史语料建议"占位按钮(点击调 `/api/dsh-ling/persona/suggest`——M2 接离线脚本,先行返回 501));提交后 POST `/api/dsh-ling/persona`(宿主写 settings 并按冻结规则应用)。
@@ -393,7 +401,8 @@ dsh-ling/
393
401
  3. **记忆**:l1BudgetTokens/l1MaxItems/weights(work/life 两行 × 三领域滑块 0..1)/trackWorkspaces;l0Always、l1Enabled 开关。
394
402
  4. **L0/L1 现状**(D7):只读面板,GET `/api/dsh-ling/state?sessionId=…`:当前 L0 文本(渲染后)、本会话模式、L1 当前列表(每条:标题/日期/来源/得分)、pending 更新队列状态;运行中的会话标注"运行中(快照冻结)"。
395
403
  5. **数据**:导出按钮(GET `/api/dsh-ling/export` → 下载 dsh-ling-memory-<date>.dshling.json,格式 §9);合并输入(文件选择 → POST `/api/dsh-ling/import`,冲突规则:同 conv_id 且同 source 跳过/覆盖由 manifest 决定,外来条目标 origin,persona 永不覆盖)。
396
- - 保存语义:所有写操作返回 `{saved, appliedSessions: idle[], pendingSessions: running[]}` 供 UI 提示。
404
+ - 保存语义:所有写操作返回 `{ok, affected}` 供 UI 提示(`affected` = 被影响到、需重取快照的会话清单;运行中的会话**不在其中** —— 冻结门保证"运行中不参与人格更新")。
405
+ > ⚠️ **B-2 订正(2026-10-05 夜)**:此处原写 `{saved, appliedSessions: idle[], pendingSessions: running[]}` —— 那是**早期设计稿的形态**,现实现已统一为 `{ok, affected}`(见 `lib/host/api.js` 各写端点的 `sendJson`)。
397
406
 
398
407
  ---
399
408
 
@@ -442,7 +451,7 @@ dsh-ling/
442
451
 
443
452
  **归属解析**(两条链并存):DSH 会话走 `session_meta.branch_id`;历史网页端/导入条目走 `conv_branch` 覆盖层;优先级 `conv_branch` > `session_meta` > 主干。**记忆行不冗余存 branch**,靠 `会话 → 枝` 推导。
444
453
 
445
- **血缘加权**:检索时按"当前枝 → 目标枝"的相对位置取档 —— **同枝 1.0 / 祖先 0.7 / 旁系 0.4**,`score = 基础分 × 血缘系数 × 矛盾系数`;**枝系数为预留** —— 可写入并显示(复盘期可调,夹在 0~2),当前**恒按 1 计、不参与检索打分**,返回体里明确带 `applied:false`,别以为排序会跟着变。
454
+ **血缘加权**:检索时按"当前枝 → 目标枝"的相对位置取档 —— **同枝 1.0 / 祖先 0.7 / 旁系 0.4**,`score = 基础分 × 血缘系数 × 矛盾系数`;**枝系数自 1.6.2 起参与打分** —— 可写入(复盘期可调,夹在 0~2),按 `score = 基础分 × 血缘系数 × 矛盾系数 × 枝系数` 生效(`1.0` = 零影响,`0` = 排到最后,不是禁用)。
446
455
 
447
456
  > ⚠️ **不用连乘**:血缘系数是**单值**(不沿路径累乘),避免 `0.7^n` 随深度指数失真(五层后 `0.4^5 = 0.010`,枝等于从记忆里消失)。深树的路径搜索留作后续。
448
457
 
@@ -543,7 +552,7 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
543
552
  | 方法/路径 | 用途 | 冻结门 |
544
553
  | --- | --- | --- |
545
554
  | POST `/mode/toggle` body{sessionId} | 切换模式(记录 + 只同步推理档位、不改模型 + 重定稿) | 运行中→排队返回 queued |
546
- | GET `/state?sessionId=` | L0 文本/模式/L1 列表/队列状态(设置页与右键菜单用) | 只读 |
555
+ | GET `/state?sessionId=` | L0 文本/模式/L1 列表/队列状态(面板与悬停菜单用) | 只读 |
547
556
  | GET `/state?sessionId=&l0Preview=1` | 人格字段实时预览(编辑中) | 只读 |
548
557
  | POST `/persona` body=字段 | 写 persona(→settings ns;**字段白名单**,白名单外的键丢弃并回报 `ignored`)并触发应用 | 见冻结门 |
549
558
  | GET `/persona/suggest` | 人格档案草稿(M2;先行 501) | — |
@@ -574,18 +583,18 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
574
583
  - [ ] 新会话 L0 注入(空称呼 / 已填写两种);KV 无异常重复
575
584
  - [ ] 模式切换:**推理档位**实际变化(会话内档位选择处可见);**模型保持不变**;新会话默认跟随 lastMode
576
585
  - [ ] 运行中(长任务)切模式/改人格:确认 running 区间快照与档位不变、空闲后生效
577
- - [ ] 设置卡读写生效(落 `$DSH_HOME/cache/dsh-ling/settings.json`,盘上只存用户补丁);导出文件可导入回(合并规则生效)
578
- - **回滚**:移除 patch 行 + 撤依赖 → host 侧 HMR 卸载;client 硬刷后消失;数据目录保留不影响回滚。
586
+ - [ ] 自建卡读写生效(落 `$DSH_HOME/cache/dsh-ling/settings.json`,盘上只存用户补丁);导出文件可导入回(合并规则生效)
587
+ - **回滚**(**全人工,无脚本、无自动回滚**):移除 patch 行 + 撤依赖 → host 侧 HMR 卸载;client 硬刷后消失;数据目录保留不影响回滚。
579
588
 
580
589
  ## 11. 实现期待验证点(进入编码前/编码初用最小探针确认)
581
590
 
582
591
  | V | 问题 | 验证方式 | 若不符的降级 |
583
592
  | --- | --- | --- | --- |
584
- | V1 | `settings.plugin.item` 卡位在官方设置页的注册键与 props | 参考 ego(已实证可用)直接复用同款注册 | 沿用 ego 同款即最低风险 |
593
+ | V1 | ~~`settings.plugin.item` 卡位在官方设置页的注册键与 props~~ **已作废(2026-10-05 现场复核)**:该卡位在本仓代码里**零命中**,落地即原"降级"路径 —— 自建浮层卡,见 §5.2 | —— | —— |
585
594
  | V2 | host 运行 Node 版本下 `node:sqlite` 可用性 | 编码前 `node -e` 探针 | 纯 JSONL + 内存索引(量小可行) |
586
595
  | V3 | 顶层/子代理会话判别字段(parentSession/delegationDepth)在 session header 的实际取值 | 读 ~/.dsh/storages session_projcache 样例(已见字段) | 多条件组合判定 |
587
596
  | V4 | `agent/session-start` 时能否读到首条 user/message(session 种子) | 最小 host 探针插件日志 | L1 冷启动=纯 lastMode Top-K |
588
- | V5 | 消息式注入降级路径(特殊 preset)在 M1 是否实现 | M1 先只实现 section 通道 + 检测告警 | 检测到 complete persona 时设置页提示,注入降级放 M2 |
597
+ | V5 | 消息式注入降级路径(特殊 preset)在 M1 是否实现 | M1 先只实现 section 通道 + 检测告警 | 检测到 complete persona 时面板提示,注入降级放 M2 |
589
598
  | V6 | client 类型来源 | 从已安装 `@deepseek-ai/dsh-client-ui-*/lib/types` 拷贝所需 contract(conversation slots/settings)进 `src/client/types` | 手动 ambient .d.ts |
590
599
 
591
600
  ## 12. 与评审阶段呼应的要点(自检)