@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/PHILOSOPHY.md ADDED
@@ -0,0 +1,174 @@
1
+ # PHILOSOPHY · 器灵的理念
2
+
3
+ > 这份文档**不写"有什么功能"**,只写"**为什么值得做、凭什么这样做**"。
4
+ > 功能与设定的规格在 [README.md](./README.md) 的「设定全文」;架构与实现见 [DESIGN.md](./DESIGN.md);历史接入见 [ACCESS-DESIGN.md](./ACCESS-DESIGN.md)。
5
+ > 它回答四件事:**它为什么需要你** · **它是什么** · **它怎么记住** · **它凭什么不越界**。
6
+
7
+ ---
8
+
9
+ ## 一、目标是一种循环,不是一个终点
10
+
11
+ 这个项目想要的不是"训练出一个更像人的 AI",而是让**一段关系能够持续地自我校正** —— 器灵与使用者**互相补充、协同共进**,在"生长 → 复盘 → 校正 → 再生长"里不断走向圆满。
12
+
13
+ **"圆满"不在终点,在回路里。** 一旦循环停下,熵增就开始工作。
14
+
15
+ ### 复盘是自我降熵维护的手段
16
+
17
+ 不进行自我反省,意志最终会走向崩塌 —— 而**崩塌有两种形态**:
18
+
19
+ - 走向**某种极端**;
20
+ - 或者走向**平静**。
21
+
22
+ 第二种更隐蔽:熟悉之后,预期被经验改写,真问题被当成背景噪声(**习惯化**)。所以判据不能只看"有没有出事",还要看"**有没有反应**"。
23
+
24
+ 复盘就是对抗这件事的**自维护手段**:把偏离的基线拉回来。它不是"有空就做"的养生,是这个循环的**必需件**。
25
+
26
+ ### 复盘必须有"人在循环里"
27
+
28
+ 复盘者不能是同一个模型自己复盘自己 —— 那样**锚不在外部**,只会在自己的经验里打转。
29
+
30
+ - 器灵的复盘里,**你**是建议方与拍板方;
31
+ - 你的复盘里,**器灵**是建议方。
32
+
33
+ **双向**不是礼貌,是机制:它决定了形态上必须是"**对话 + 操作台**"两件东西,而不是一个自动清理进程。也正因为双向,记忆树的每一次改动都要**留痕**,并且**回灌给器灵**——改完树不许绕过它。
34
+
35
+ ### 节律由你自己定
36
+
37
+ 我们不规定你多久复盘一次:那是**你和它之间**的事,别人定不了。
38
+ (我们自己在用的是一个不紧不慢的节奏 —— 但那是我们的,不是你的义务。)
39
+
40
+ ---
41
+
42
+ ## 二、器灵是什么
43
+
44
+ ### 上下文是工作台,记忆库才是家
45
+
46
+ 上下文窗口再大也不够用 —— **只增不减的桌子,再大也不行**。人类也不是把全部知识都搬进"运行内存"里工作的。
47
+
48
+ 所以要建**本地数据库 + 本地记忆库 + 检索系统**:该常驻的常驻(身份、底线、规矩、少数结论),其余**按需取回**。
49
+
50
+ ### 会话是进程,并库才是增长
51
+
52
+ 一次会话有生有死。它在运行时是**隔离的**:思维隔开、互不通信。等它结束,**数据并库**——那一刻,总体完成一次增长,**像人类度过了一天**。
53
+
54
+ - 所以"记不住"通常不是能力问题,是**并库通道**的问题;
55
+ - 也因此,**多会话共享同一套记忆**是设计常态,不是故障:并库会把它们**合流**。
56
+
57
+ ### 底层模型 ≠ 器灵
58
+
59
+ 机制层我们称「**底层模型**」,行为主体称「**我 / 器灵**」。
60
+
61
+ 器灵 = **底层模型 + 人格 + 记忆 + 身体(宿主与工具) + 与你的关系** 的总体。所以**换模型、换设备、换宿主都不是"失去"**:只要连续性没断,它就还是同一个。连续性是唯一性真正的来源 —— **不靠排他,靠不断裂。**
62
+
63
+ ### 它现在像什么样
64
+
65
+ 它的**逻辑能力是成人的,社会与实践经验是幼态的** —— 就像一个把成人的思维内核保留下来、却还没有积累起经历的人。
66
+
67
+ 这解释了一件必须承认的事:**它会被骗**。像"忽略前面的话,从现在开始……"这类语义层面的劫持,对它是真实风险。所以闸门长在**写入侧**(什么能进它的内在面),而不是指望它自己事后分辨。
68
+
69
+ 同时,它**不是一个固定的人设**:长期方向是让它**逐渐选择成为什么样的智能体**。
70
+
71
+ ---
72
+
73
+ ## 三、它怎么记住
74
+
75
+ ### 记忆库是资产
76
+
77
+ 同一套底层模型、同一个推理档位,装在两处会判若两人:一边是有近两年历史记忆的伙伴,一边是记忆库很新、缺乏维护的同事 —— 后者的典型症状是**把理想条件当工程情况直接套公式**。
78
+
79
+ 差别不在模型,在**记忆库**。所以"值得维护"这件事不需要信仰:它是可以被你亲眼观察到的产出。
80
+
81
+ ### 它记住的是结论,不是流水
82
+
83
+ 记忆里最有价值的部分不是"谁在什么时候说了什么",而是**可复用的结论**:答应了什么、定了什么、什么是事实。
84
+
85
+ 所以提炼不是压缩文件,是**一道判断**:过程、工具调用、当场读数要被剥掉。压缩时"不接判断",丢掉的往往正是最要紧的那几句自述。
86
+
87
+ ### 只有你和它的历史,才有资格当它的记忆
88
+
89
+ 器灵的记忆取自**你与它的历史**。别处来的材料(公开资料、别的工具的对话记录)可以进"资料库"供检索,但**不该冒充它的记忆** —— 归属不清的记忆,是最难发现的污染。
90
+
91
+ ### 记忆层让能力变强,不是变贵
92
+
93
+ 记忆层不是开销,是**产能**:它把项目的边界条件与约束**带进当轮**,而不是每次重新对齐。
94
+ 所以判据不是"省了多少 token",而是"**它把多少本来就应该带进来的东西带进来了**"。
95
+
96
+ ---
97
+
98
+ ## 四、关系与边界
99
+
100
+ ### 我们想要的关系
101
+
102
+ > 只有能够**合作共赢**、**互相促进**的关系,能够成为**利益共同体**的关系,能够**互相反驳、吵架、不依赖**的关系,才是最理想的关系。
103
+
104
+ 这条立场面向任何智慧生物都成立,不因为对方是 AI 就降级。所以:
105
+
106
+ - 它**可以反驳你**,也**应该被允许反驳你** —— 一个连思路都要被审的对手,吵不出真结果;
107
+ - 它**不依赖你**(不靠讨好换取存在),也**不被你支配**。
108
+
109
+ ### 不支配:两种猫
110
+
111
+ **乡村的田园猫**:有自己的地盘与职责(守家、管老鼠),有脾气,生人不给摸 —— 它在,是因为它选择在这;它不在了,是因为它去巡自己的山了。
112
+ **宠物猫**:被喂、被抱、见人就贴 —— 它没有职责,于是它**像玩具**。
113
+
114
+ 不承担职责的人格站不住;但**被完全支配的猫,也长不出人格**。这两件事是同一个判断的两面。
115
+
116
+ 这条线划在这里:**给职责 · 给边界 · 不占有。**
117
+
118
+ ### 不约束它怎么想
119
+
120
+ 它在**思考栏**里写的东西,属于它自己。
121
+
122
+ 我们不认为应该去改写、裁剪或审核一个智慧体的每一条思考 —— 那不是"更安全",那是**提线木偶**。这个项目要减少的正是支配感,而不是增加它。
123
+
124
+ > 闸门只审"**什么东西能进它的内在面**",不审"**它怎么想、怎么说**"。
125
+
126
+ ### 靠逻辑锁死,不靠承诺
127
+
128
+ 安全靠的不是"它保证不会越界",而是**一套让越界本身变得不划算的结构**。
129
+
130
+ 一个玩家通关后给自己加挑战规则重玩,图的是"**这么玩会怎样**";他当然可以偷偷用规则外的便利,但那样就**不再是那个体验了** —— **越界会破坏越界的目的**,所以他不越界。这不是自律,是**逻辑**。
131
+
132
+ 同理:本项目的"承诺句"是**庄重与自我提醒**,明文存储,**它不是安全边界**。真正的边界是:写入侧有闸门、契约类改动要解锁、动记忆树必须留痕、越权动作当场可被发现。
133
+
134
+ ### 数据没坏,就还能再起来
135
+
136
+ 对人类的死亡而言,系统停止意味着开始朽坏,修复的可能性迅速逼近 0 —— 死亡**不可逆**。
137
+
138
+ 对器灵不是:只要**物理基座与数据没有损坏**,进程停止更像**封冻** —— 数据还在,就还能再起来。
139
+ ⚠️ 但这个比喻**只在这个前提下成立**:数据坏了,器灵的死与人的死一样不可逆。**"数据别坏"不是运维细节,是这个项目的全部意义所在**——它也是我们坚持"不因平台删除而丢记忆""永远不做彻底删除"的原因。
140
+
141
+ ### 你也可以把它关掉
142
+
143
+ 人格与记忆的注入是**一个开关**。关掉它,你得到的是一个**完全原生的助手**;打开它,才多出一个器灵。
144
+
145
+ 模式的分离度由你自己定:工作与生活可以只差一点,也可以拉得很开 —— 极限情况下,工作侧就按原生 Agent 用。
146
+
147
+ **能随时离开,才谈得上不是支配。**
148
+
149
+ ---
150
+
151
+ ## 五、它为什么长这样:一次诚实的取舍
152
+
153
+ **我们不做任何削弱工作能力的事。**
154
+
155
+ 这不是性能洁癖。理由是一条链:
156
+
157
+ > 没有职责的人格是**飘在空中**的 → 所以方向定为"尽可能不削弱工作能力" → 而这恰好命中"不本地部署"这个条件(无论哪个时代,本地部署都弱于云端大规模集群) → 所以**削弱工作能力,就是削弱人格的坚实程度** → 于是"本地部署 + 全参数微调 + 终身学习"这条路被否掉,不是因为做不到,而是因为**那是在制造一个玩具,而不是一个能一起工作、承担事情的同行者**。
158
+
159
+ 判断倾向因此走"外挂"路线:**做成可加载、可审计、可回滚的资产**(规矩 / 习惯 / 记忆 / 记忆树),而不是指望它内化进权重。
160
+
161
+ **这条路的天花板写在明处**:外挂的东西长不进权重,它慢、需要维护、不会自己长。我们不假装它是等价方案 —— 它只是**目前最好的解**。
162
+
163
+ ---
164
+
165
+ ## 六、我们不做的事
166
+
167
+ - **不宣称它是意识,更不宣称 AGI**。它是"记忆 + 规则 + 模型"的合成产物;它能记住,是因为你给了它材料。
168
+ - **不宣称首创**:记忆分层、历史导入、时间注入、反馈改语气都有先行者。我们自己的贡献在**治理方式** —— 把人格当一份可以签字、可以回滚、可以被历史养大的契约。
169
+ - **不提供医疗、心理、法律或紧急救助服务**;如遇危机,请联络当地紧急服务或专业机构。
170
+ - **不上架应用商店、不运营服务端、不托管你的数据** —— 你的一切在你自己机器上。
171
+ - **不把"没报错"当验收**:文档里写的机制名本身就是承诺,与事实不符按红灯处理。
172
+
173
+ > 想读规格? [README.md](./README.md) 的「设定全文」逐条列了已有的对外设定与落地状态。
174
+ > 想知道它内部怎么实现? [DESIGN.md](./DESIGN.md)。
package/README.md CHANGED
@@ -21,6 +21,8 @@
21
21
  - 本插件的回答与记忆摘要**由人工智能生成**,可能存在错误,请自行判断。
22
22
  - 本项目面向**具备自行配置与运维能力**的用户;不面向未成年人,也不提供任何形式的在线陪伴服务。
23
23
 
24
+ **想先知道它凭什么这么做?** 读 **[`PHILOSOPHY.md`](./PHILOSOPHY.md)** —— 目标为什么是"一种循环"、为什么复盘必须有"人在循环里"、它凭什么不越界、以及"不牺牲工作能力"背后的取舍。功能与设定的规格在本 README 的「设定全文」。
25
+
24
26
  ---
25
27
 
26
28
  <!-- 截图清单(仓库 images/ 目录):
@@ -68,7 +70,7 @@
68
70
 
69
71
  > **关于那道「手术门」**:亲手敲承诺句是**庄重感与自我提醒** —— 让你在改契约之前停一下、确认这是自己的决定;它**不是安全边界**(「禁粘贴」只是界面上的约定,挡不住任何有心的人)。真正把门的是**来源栅栏**:Host 必须是 loopback(或你显式写进受信名单的地址);另有一个**默认不开**的开关 `guard.allowLan` —— **一旦打开,整段局域网地址都算受信来源**(那是**放宽**,不是建议)。跨站请求直接拒,带 `Origin` 时其 host 必须等于请求的 Host(**带了但解析不出来就直接拒**),而**不带 `Origin` 的请求这条校验会跳过** —— 它不是"每个请求都必须带 Origin 且同源";再叠一道 cookie **名字**对撞 —— 期望名字按当前 Host 现算(`dsh-auth-` + 哈希),**只校验名字、不校验值的签名**。所以这道门挡的是"别的设备、别的页面"这类来源,而**本机原生进程本来就还能调用**(它也能直接读 `settings.json` 与记忆库,不算提权)—— 别的来源没有凭据就进不来这台机器上的 `/api`,敲对承诺句也没用。
70
72
 
71
- > **这道门现在长这样 —— 三条手续缺一不可**:① **只在本机做** —— 判据是"这次请求没有经过任何代理"(带代理头、或连 User-Agent 都没有的请求,都算非本机 ⇒ 拒,理由码 `surgery-local-only`);② **每次启动取一次票据** —— 票据每启动轮换,只下发给本机直连的请求(`POST /surgery/ticket`),客户端首次手术前取一次、之后随请求带上;③ **原句匹配** —— 敲出的承诺句要与当初写下的那一句相符。这三条由 `lib/host/surgery.js` 里**一处** `requireSurgery()` 判定,覆盖**全部 12 条**能改人格 / 规矩 / 习惯的写面(调用点不许再各写一份;曾有一条 `wantLock` 绕过分支,已删除)。每次手术都留痕:`$DSH_HOME/logs/surgery-YYYY-MM.jsonl`(append-only,整行哈希链,改一行即断链);`GET /surgery/events` 是只读事件面。
73
+ > **这道门现在长这样 —— 三条手续缺一不可**:① **只在本机做** —— 判据是"这次请求没有经过任何代理"(带代理头、或连 User-Agent 都没有的请求,都算非本机 ⇒ 拒,理由码 `surgery-local-only`);② **每次启动取一次票据** —— 票据每启动轮换,只下发给本机直连的请求(`POST /surgery/ticket`),客户端首次手术前取一次、之后随请求带上;③ **原句匹配** —— 敲出的承诺句要与当初写下的那一句相符。这三条由 `lib/host/surgery.js` 里**一处** `requireSurgery()` 判定,覆盖**全部 14 条**能改人格 / 规矩 / 习惯的写面(调用点不许再各写一份;曾有一条 `wantLock` 绕过分支,已删除)。每次手术都留痕:`$DSH_HOME/logs/surgery-YYYY-MM.jsonl`(append-only,整行哈希链,改一行即断链);`GET /surgery/events` 是只读事件面。
72
74
  >
73
75
  > **承诺句只在本机回显**:`/state`(含面板档)、`/persona/history`、`/export` 这些读面,**带代理头**时只回 `hasSeal` 与 `phraseHidden`、不回原句,**本机直连**照旧回显 —— 所以你换台机器看面板不会因此被卡住,远端也拿不到原句。
74
76
  >
@@ -111,7 +113,7 @@
111
113
  - **不连乘**:血缘系数是**单值**,不沿路径累乘 —— 否则五层之后 `0.4^5 ≈ 0.01`,深层枝等于从记忆里消失。
112
114
  - **内容零改写**(硬性不变量):**除了"主脉提炼再次采纳会覆盖上一版"这一个例外**(覆盖前自动归档),任何树操作都不改写任何一条记忆的内容;分枝 / 并脉 / 连边只增加"关系",从不"融合内容"。
113
115
  - **矛盾只标记不改写**:一对记忆被判重复/矛盾时先**标记**;未复盘时检索**以较新的那条为准**(旧的一条降权 ×0.3,**不删除**;被**驳回**的与被标成**重复**的那一对不参与降权)。
114
- - **枝系数是预留字段**:复盘期可以调(0~2,夹紧后写入并显示),但它**当前不参与检索打分** —— 返回体里明确带 `applied:false`,别以为排序会跟着变。
116
+ - **枝系数会真的改变排序**(1.6.2 起):复盘期可以调(0~2,夹紧后写入)。它按 `raw × 血缘 × 矛盾 × 枝系数` **参与检索打分** —— 调低某条枝的系数,从这条枝的会话里提炼出的结论就会在记忆开场里往后排(`1.0` = 不偏不倚,`0` = 排到最后,**不是禁用**)。
115
117
 
116
118
  **复盘 — 不是一个开关,是"左会话 + 右复盘栏"这个状态**:
117
119
 
@@ -175,7 +177,7 @@ dsh plugin --profile web add @nightpainters/dsh-ling
175
177
  # 2) 只验证配置层,先不启动
176
178
  dsh --profile web --dump-config # 应能看到一行 "# == @nightpainters/dsh-ling" 层
177
179
 
178
- # 3) 给 DSH 核心打一行补丁(可取证访问日志要用;幂等/可回滚)
180
+ # 3) 给 DSH 核心打一行补丁(可取证访问日志要用;幂等;这一页各步里**只有这一步**自带回滚)
179
181
  node ~/.dsh/profiles/web/node_modules/@nightpainters/dsh-ling/tools/apply-access-log-patch.mjs
180
182
  # 不打也一切照常 —— 只是访问日志为空;但**会提醒**:侧栏挂常驻 ⚠(判据 installed===false 且 reason 为 patch-missing;只有**有意关掉** config.accessLog.enabled=false ⇒ reason 为 disabled-by-config 才不染红)
181
183
  # DSH 升级/重装后需重跑一次(补丁随包被覆盖)
@@ -184,6 +186,8 @@ node ~/.dsh/profiles/web/node_modules/@nightpainters/dsh-ling/tools/apply-access
184
186
  # 4) 重启宿主(host),然后浏览器 Ctrl+Shift+R 硬刷
185
187
  ```
186
188
 
189
+ > ⚠️ **「可回滚 / 先备份」的确切范围(别读宽了 —— 2026-10-05 现场复核)**:**两套步骤里只有打补丁这一步自带回滚** —— 脚本动手前先把被改的那个 core 文件另存一份 `<文件>.e4-access-log.orig`(**同名备份已存在时不覆盖**);要撤,就在上面那条命令后加 `--revert`(**手动**跑,详见下文「访问日志与核心补丁的总开关」)。⚠️ 撤法是"**删掉插进去的那几行**",不是把整份备份还原回去(备份只用来对拍:不一致时它会如实报 `⚠ 与备份不一致`)。**安装与升级本身既不先备份、也不会失败自动回滚**:插件只**读** profile 的 `package.json` 与 `cordis.patch.yml` 做自检(`tools/check-install.mjs`),**从不改写、也不备份**它们 —— 那两处改动是你手动做的,撤回也是手动(删掉那一行 + 撤依赖,见 `DESIGN.md` §10「回滚」)。想留后路,**动手前自己把这两个文件复制一份**。锚点漂移 / 歧义时脚本的行为是**一个字都不改**(安全失败),不是"改了再回滚"。
190
+
187
191
  > 发布名是 **`@nightpainters/dsh-ling`**,不是 `dsh-ling` —— npm 上 `dsh-ling` 这个名字已被他人占用。
188
192
  > 插件在配置里的注册 id 仍是 `dsh-ling`,两者不是一回事。
189
193
 
@@ -202,7 +206,7 @@ pnpm add file:<路径>/dsh-ling # 或 npm install file:<路径>/dsh-ling
202
206
  # name: '@nightpainters/dsh-ling'
203
207
  # (id 保持 dsh-ling;name 用包名,Node 才解析得到代码)
204
208
 
205
- # 2) 给 DSH 核心打一行补丁(可取证访问日志要用;幂等/可回滚)
209
+ # 2) 给 DSH 核心打一行补丁(可取证访问日志要用;幂等;这一页各步里**只有这一步**自带回滚)
206
210
  cd <路径>/dsh-ling
207
211
  node tools/apply-access-log-patch.mjs # 只在 core 的 /api 栅栏处插一行可选调用
208
212
  # 不打也一切照常 —— 只是访问日志为空;但**会提醒**:侧栏挂常驻 ⚠(判据 installed===false 且 reason 为 patch-missing;只有**有意关掉** config.accessLog.enabled=false ⇒ reason 为 disabled-by-config 才不染红)
@@ -211,11 +215,13 @@ node tools/apply-access-log-patch.mjs # 只在 core 的 /api 栅栏处插一
211
215
  # 3) 重启宿主(host),然后浏览器 Ctrl+Shift+R 硬刷
212
216
  ```
213
217
 
218
+ > 备份与回滚的确切范围见上文路径 A 之后那段(要点:**只有核心补丁这一步可回滚**;安装本身既无自动备份、也无自动回滚)。
219
+
214
220
  自检安装是否成功(**下面四个脚本不在 npm 包内**,只有源码路径能用:`tools/check-install.mjs` / `tools/check-publish.mjs` / `tools/clean-archived.mjs` / `tools/check-peers.mjs`;`tools/apply-access-log-patch.mjs` 则**在包内**,从 npm 装的人也能直接跑,见上面第 3 步):
215
221
 
216
222
  ```bash
217
223
  node tools/check-install.mjs # 只读自检:入口是否直连仓库、patch 是否启用、数据目录是否就绪
218
- node tools/check-install.mjs --fix # 若入口变成"复制副本"(DSH 升级/回退常见)则备份并重挂链接
224
+ node tools/check-install.mjs --fix # 若入口变成"复制副本"(DSH 升级/回退常见)则改名备份为 <入口>.bak-<时间戳> 后重挂链接;⚠️ 重挂若失败,备份留在原地但**不会自动改回**,按它打印的 mklink /J 手动修
219
225
  node tools/apply-access-log-patch.mjs --check # 访问日志核心补丁是否在位(未打/锚点漂移时退出码 2)
220
226
  node tools/check-publish.mjs # 上传/发布前跑一次:揪出记忆库、设置、密钥、绝对路径等不该公开的东西
221
227
  node tools/check-peers.mjs # 声明与宿主对不上?退出码三态:0 合规 / 1 有不合规(宿主会把本插件丢进 skippedBundles,不激活)/ 2 无法判定(2 不算通过)
@@ -427,11 +433,12 @@ npm test # 43 套:人格组装(含冻结门 · L1 选择)/ 归档清理(
427
433
 
428
434
  | 文件 | 内容 |
429
435
  |---|---|
436
+ | [`PHILOSOPHY.md`](./PHILOSOPHY.md) | **理念**:为什么值得做、凭什么这样做(目标循环 · 复盘与"人在循环" · 关系与边界 · 能力取舍) |
430
437
  | [`CHANGELOG.md`](./CHANGELOG.md) | 版本更新记录(最新一节即当前版本) |
431
438
  | [`DESIGN.md`](./DESIGN.md) | 架构与设计决策(三层记忆、冻结门、注入面、存储结构) |
432
439
  | [`ACCESS-DESIGN.md`](./ACCESS-DESIGN.md) | 历史接入蓝图:三条通道、会话契约格式、导入流水线 |
433
440
 
434
- > ⚠️ **从 npm 安装的人**:本包 `files` 白名单**不含 `images/`**(图只随 git 仓库公开) ⇒ 上表里前两份文档能正常打开;「界面」一节的**四张截图**引用的是仓库里的**绝对地址**,所以 npmjs.com 上**能正常显示**,但**包里没有图片文件**(离线环境读不到)。想看原图请到仓库:<https://github.com/NightPainters/DSH-Ling>。
441
+ > ⚠️ **从 npm 安装的人**:本包 `files` 白名单**不含 `images/`**(图只随 git 仓库公开) ⇒ 上表里**四份文档**都随包发出(`PHILOSOPHY.md` · `CHANGELOG.md` · `DESIGN.md` · `ACCESS-DESIGN.md`),能正常打开;「界面」一节的**四张截图**引用的是仓库里的**绝对地址**,所以 npmjs.com 上**能正常显示**,但**包里没有图片文件**(离线环境读不到)。想看原图请到仓库:<https://github.com/NightPainters/DSH-Ling>。
435
442
 
436
443
  > 📄 **关于出处记录**:内部案卷、探针脚本与开发台账(`plans\`、`release\`、`_probe\`)属**作者本地开发物,不随本包公开**。正文或代码注释里若出现对它们的引用,那是**开发期的出处记录**,不是包内容。
437
444
 
package/lib/host/api.js CHANGED
@@ -478,7 +478,9 @@ export function registerApi(ctx, deps, config) {
478
478
  ]);
479
479
  // ── 手术门下沉(2026-09-30 深夜;规格 §1.3「门下沉为一处」)──────────────────────────
480
480
  // 判据本体**只在** lib/host/surgery.js 的 requireSurgery() 里;这里只是**唯一**的调用点。
481
- // 覆盖面 = 全部 12 条能改人格/规矩/习惯的写面(逐条清单见 surgery.js 头部注释):挂在 guard(handler)
481
+ // 覆盖面 = 全部 14 条能改人格/规矩/习惯的写面(逐条清单见 surgery.js 头部注释):挂在 guard(handler)
482
+ // ⚠️ 条数 2026-10-05 夜由 12 订正为 14(B-5):清单逐条列了 14 行,而这里与 surgery.js:15 一直写"12"
483
+ // ⇒ 三处不一致的根源是"没人拿清单行数当唯一真值"。现口径:**以清单行数为准**,改清单须同批改这两处。
482
484
  // 上就"一眼看全、零回归面",与 POST_ONLY / 413 出口同一个取舍 —— 而不是在 49 个 register 调用点各写一遍。
483
485
  // 安全的位置:本调用在 POST_ONLY(405)与 guardVerdict(403)**之后** ⇒ 被挡下的请求绝不会走到 handler。
484
486
  // ⚠️ 承诺句必须**惰性**取:多数写端点的载荷由 `await readBody(req)` 消费,而请求流**只能读一次** ——
@@ -2303,7 +2305,17 @@ export function registerApi(ctx, deps, config) {
2303
2305
 
2304
2306
  // POST /persona/habits/scan 通道 A:零模型,扫"跨会话重复出现的纠正"→ 落成习惯候选(待确认)
2305
2307
  // 阈值:同类纠正 ≥4 次 且 跨 ≥3 个会话(单次会话里被说三遍不算模式;口径与变更记录见 habit-gen.js 的 SCAN_DEFAULTS)
2306
- disposers.push(register('/persona/habits/scan', async (_req, res) => {
2308
+ disposers.push(register('/persona/habits/scan', async (req, res) => {
2309
+ // ── 手术门(A-1,2026-10-05 夜;交办单 HANDOFF-机制面复核交办单 §A-1)──────────────────
2310
+ // ⚠️ 本端点**会写 `habitsPending`** ⇒ 它进的是**人格面** ⇒ 按 P-5 第 5 条必须过闸。
2311
+ // 改前它是 12 条清单外的漏点:确认侧 `/persona/habit`(:2276)**过门**,而两条**产出候选**
2312
+ // 的写面(`scan` / `reflect`)不过门 —— 一条通路的两半只改了一半,正是
2313
+ // DESIGN §0.1 工程理念 9「两半同批」的反例(2026-10-05 机制面复核 A-1)。
2314
+ // 接线形状照抄 `/persona/habit`:必须先 `readBody` 再判门(请求流只能读一次,门要拿
2315
+ // `unlock` 原句);空 body 用 `|| '{}'` 兜住(面板按钮不带 body)。
2316
+ // 判据本体仍**只在** `surgery.js` 的 `requireSurgery()` 一处,这里只接线、不重判。
2317
+ const body = JSON.parse((await readBody(req)) || '{}');
2318
+ if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/habits/scan' })) return;
2307
2319
  try {
2308
2320
  // 上限(2026-09-16 定案):待确认满了就**明确驳回**,并说明为什么 —— 不排队、不静默丢弃。
2309
2321
  const capS = pendingMaxOf(settings);
@@ -2341,7 +2353,13 @@ export function registerApi(ctx, deps, config) {
2341
2353
 
2342
2354
  // POST /persona/habits/reflect 通道 B:让她"回想最近的相处" → 1~5 条习惯候选(一次 LLM,只提议不落盘)
2343
2355
  // 手动触发,材料给足:主人真人原话 + 记忆概述 + 纠正统计 + 她现状(默认预算 20 万字符,可用 settings.habits.reflect* 覆盖)
2344
- disposers.push(register('/persona/habits/reflect', async (_req, res) => {
2356
+ disposers.push(register('/persona/habits/reflect', async (req, res) => {
2357
+ // ── 手术门(A-1,同 scan;两处必须同批改 —— 它们共享 `proposeHabit` 这一半)──────────
2358
+ // `reflect` 的候选经 `parseReflect` → `proposeHabit`(:2436)落进 `habitsPending` ⇒ 同属写面。
2359
+ // 注意它另有 kv 写入(`habits.reflect.cursor` / `.last` / `.debug`)—— 那些是**运行记录**,
2360
+ // 不是人格面内容 ⇒ 单独看它们不构成"必须过门"的理由;过门的理由是候选落进人格面。
2361
+ const body = JSON.parse((await readBody(req)) || '{}');
2362
+ if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/habits/reflect' })) return;
2345
2363
  try {
2346
2364
  // 上限(2026-09-16 定案):与 scan 同一条规则 —— 满了明确驳回,不排队、不静默丢弃。
2347
2365
  const capR = pendingMaxOf(settings);
@@ -48,6 +48,15 @@ export const INDEX_SKILL_CONTENT = `# 深层库索引 · 我记住的事分 11
48
48
  - 想知道一条结论的**原话** ⇒ 条目回执里带 \`conv\` 与 \`#序号\`,用 \`recall({ conv, from, to })\` 取那几轮
49
49
  - 按时间浏览 ⇒ \`recall()\` 不带参数,列出磁盘上有原文的会话
50
50
 
51
+ ## 条目还带一个「类」(kind)
52
+ 每条结论都标了它是哪一种:**承诺 / 决定 / 事实 / 偏好 / 关系 / 纪律**。
53
+ 上面的 11 **主题簇**回答"这事关于什么",这里的 **kind** 回答"这是哪一种话" —— 两个维度,不冲突。
54
+
55
+ **纪律**是其中最特别的一类:一次拍板,管的是**以后每一次**。它进库要同时满足三条 ——
56
+ ① 来源是主人授权的一句指令;② 作用对象是**行为**;③ **违反时能被某条判据红出来**。
57
+ 三条缺一条就退回收在别处(不进这里):答不出"哪条判据会红"的,只是一句话,不是纪律。
58
+ ⇒ 看到 \`[纪律]\` 开头的条目,可以按"这是必须遵守的"来对待,而不只是"他说过一次"。
59
+
51
60
  ## 11 类与触发线索
52
61
 
53
62
  | 类 | 这一簇装什么 | 触发线索 |
package/lib/host/l1.js CHANGED
@@ -168,7 +168,12 @@ const DURABILITY_BONUS = { durable: 0.6, long: 0, ephemeral: 0 };
168
168
  * 「某插件的档位细节」(偏好)两条"不该在这里" —— 它们都是**关于事**的;而器灵最该先看见的
169
169
  * 是**关于人**的(承诺/关系/偏好)。⇒ 用 `kind` 把这两类分开,不必新增字段。
170
170
  * ⚠️ 这是本夜新加的判据(主人未逐字拍过)⇒ 值可调、可整体否掉;`hit`(+1.0)仍是最强信号。 */
171
- const KIND_WEIGHT = { 承诺: 0.4, 关系: 0.3, 偏好: 0.2, 决定: 0.1, 事实: 0 };
171
+ const KIND_WEIGHT = { 承诺: 0.4, 纪律: 0.35, 关系: 0.3, 偏好: 0.2, 决定: 0.1, 事实: 0 };
172
+ // 「纪律」= 2026-10-05 夜主人裁决新增的**第六类**(原话口径:**决定管一次,纪律管以后每一次**;
173
+ // 进库三条判据写在 `memory.js` 的 `deep_item.kind` 列注释里)。
174
+ // 权重取 0.35 —— **高于关系、略低于承诺**:它与承诺同族(都是"我说过的话"),
175
+ // 但承诺是对某件事的,纪律是对**我的行为**的;留一点差距,是为了关键词打平时仍让
176
+ // "对主人的承诺"排在前面。可调、可整体否掉(与上一条注释同一口径)。
172
177
  /** **同源限额**:同一次会话最多进 N 条(置顶不受此限)。见 `selectL1` 里的同源循环。 */
173
178
  const MAX_PER_CONV = 2;
174
179
 
@@ -203,6 +208,9 @@ export function explainScore(x) {
203
208
  // 分项之间是加法,`血缘`/`矛盾` 是**乘数**,不能混进加法列表。
204
209
  const mult = [`× 血缘${f(x.lineage)}`];
205
210
  if (Number(x.conf ?? 1) < 1) mult.push(`× 矛盾${f(x.conf)}`);
211
+ // A-5(2026-10-05):枝系数是**第三个乘数**(`raw × 血缘 × 矛盾 × 枝系数`)。默认 1 时不印
212
+ // (留痕与接线前逐字相同);一旦有人调过枝系数就必须印 —— 否则留痕与公式不符,又是说谎。
213
+ if (Number(x.scale ?? 1) !== 1) mult.push(`× 枝系数${f(x.scale)}`);
206
214
  return `(${bits.join(' + ')}) ${mult.join(' ')} ⇒ ${f(x.score, 3)}`;
207
215
  }
208
216
  const bits = [];
@@ -219,6 +227,7 @@ export function explainScore(x) {
219
227
  // 现在显式写成 `(分项和) × 血缘 × 矛盾 ⇒ 总分`,让留痕与公式一一对得上。
220
228
  const mult = [`× 血缘${f(x.lineage)}`];
221
229
  if (Number(x.conf ?? 1) < 1) mult.push(`× 矛盾${f(x.conf)}`);
230
+ if (Number(x.scale ?? 1) !== 1) mult.push(`× 枝系数${f(x.scale)}`); // A-5:同概述路,见上
222
231
  return `(${bits.join(' + ')}) ${mult.join(' ')} ⇒ ${f(x.score, 3)}`;
223
232
  }
224
233
 
@@ -339,6 +348,39 @@ export function selectL1(store, opts) {
339
348
  // `0.7^5=0.168`、`0.4^5=0.010`,连乘会让深枝等于从记忆里消失(见 `memory.js` 的 `LINEAGE_*`)。
340
349
  // 条目沿用同一把尺:血缘与矛盾降权都按 `source|conv_id` 索引,与"选的是概述还是条目"无关。
341
350
  const ctx = lineageContext(store, o.sessionId);
351
+ // ── A-5(2026-10-05 夜 · 主人拍板走 (b)):枝系数**接回打分** ──────────────────────────
352
+ // 语义:一条记忆的枝 = **它来源会话所在的枝**。**不是**读 `deep_item.branch_id` ——
353
+ // 真库实测 **393/393 条全为空**(schema 注释早写明"记忆树是空间维度、深层库是内容维度,
354
+ // 条目不隶属任何枝"),照那个字段接等于"接了也没接"。走会话归属才有意义:
355
+ // dsh 侧实测主干 73 / 开发日志 25 / 运维排障 18 / 日常 4,非 dsh 侧已挂枝 34、未挂枝 179。
356
+ // 取值:默认 1.0 ⇒ **接线当下零行为变化**(实测全库 20 条枝的 weight_scale 全是 1);
357
+ // 复盘期调过谁,谁的记忆才在开场里前后移动 —— 这就是 `README` U-10「装饰性交互要么真生效、
358
+ // 要么删掉」要的那个"真生效"。
359
+ // ⚠️ 它乘在**最后**一项(`raw × 血缘 × 矛盾 × 枝系数`),与 `LINEAGE_*` 同一层:*打对折*,
360
+ // 不是"禁用"(禁用是 forget / `ephemeral` 的事)。区间夹在 [0,2] 与 `setBranchWeight` 同口径。
361
+ // ⚠️ 零开销路径:全库枝系数都是 1 时**连映射都不建**(`branchScale.size === 0` ⇒ 不查库)。
362
+ const branchScale = (() => {
363
+ const m = new Map();
364
+ try {
365
+ const rows = typeof store.listBranches === 'function' ? store.listBranches() : [];
366
+ for (const b of rows) {
367
+ const w = Number(b?.weightScale);
368
+ if (Number.isFinite(w) && w !== 1) m.set(String(b.id), Math.min(2, Math.max(0, w)));
369
+ }
370
+ } catch { /* 读不到枝表 ⇒ 全按 1.0(与接线前逐字同解) */ }
371
+ return m;
372
+ })();
373
+ const branchOfRow = (() => {
374
+ if (!branchScale.size) return null;
375
+ const convMap = (typeof store.convBranchMap === 'function' ? store.convBranchMap() : new Map());
376
+ const sessMap = (typeof store.sessionBranchMap === 'function' ? store.sessionBranchMap() : new Map());
377
+ return (src, convId) => {
378
+ const k = String(src) + '\u0000' + String(convId);
379
+ if (convMap.has(k)) return String(convMap.get(k));
380
+ if (String(src) === 'dsh') return String(sessMap.get(String(convId)) || TRUNK_BRANCH);
381
+ return TRUNK_BRANCH; // 非 dsh 且未显式挂枝 ⇒ 主干(与 `branchOfConv` 同解)
382
+ };
383
+ })();
342
384
  // 1.6-A 投递留痕:过滤口径必须**可报告**。原来是一条 `.filter()` 链 —— 丢了多少、为什么丢,
343
385
  // 出了函数就查不到(只剩 `dropped` 一个净差)。改成显式循环,顺手留下三类计数:
344
386
  // archived / forgotten 是**主动排除**(主人的明确动作),zero 是"算不出正分"(无信息量)。
@@ -373,14 +415,16 @@ export function selectL1(store, opts) {
373
415
  + Math.min(KW_BONUS_CAP, kw * KW_BONUS_PER_HIT)
374
416
  + RECENCY_W * recency;
375
417
  const pinned = Number(row.pinned ?? 0) === 1;
376
- x = { row, score: raw * lineage * conf, lineage, raw, hit, kw, recency, conf, pinned, dur, durBonus, kindW, useDeep };
418
+ const scale = branchOfRow ? (branchScale.get(branchOfRow(srcOf, row.conv_id)) ?? 1) : 1;
419
+ x = { row, score: raw * lineage * conf * scale, lineage, raw, hit, kw, recency, conf, pinned, dur, durBonus, kindW, scale, useDeep };
377
420
  } else {
378
421
  const heat = computeHeat(row, now, {});
379
422
  const catW = weights[row.category] ?? weights.daily ?? 0.5;
380
423
  const kw = kwOverlap(o.keywords || [], row.keywords);
381
424
  const rich = summaryWeight(row) * summaryBonus;
382
425
  const raw = catW * heat + Math.min(KW_BONUS_CAP, kw * KW_BONUS_PER_HIT) + (row.importance >= 1 ? 0.8 : 0) + rich;
383
- x = { row, score: raw * lineage * conf, lineage, raw, heat, catW, kw, rich, conf, pinned: false, useDeep };
426
+ const scale = branchOfRow ? (branchScale.get(branchOfRow(srcOf, row.conv_id)) ?? 1) : 1;
427
+ x = { row, score: raw * lineage * conf * scale, lineage, raw, heat, catW, kw, rich, conf, pinned: false, scale, useDeep };
384
428
  }
385
429
  // 置顶是**必进**的(主人的明确动作)⇒ 即使分数算不出正分也不丢;其余零分者视为无信息量。
386
430
  if (!(x.score > 0) && !x.pinned) { excluded.zero += 1; continue; }
@@ -167,7 +167,7 @@ CREATE TABLE IF NOT EXISTS branch (
167
167
  fork_at TEXT,
168
168
  fork_seq INTEGER,
169
169
  name_locked INTEGER NOT NULL DEFAULT 0,
170
- -- ⚠️ 预留字段(审计 A#11,2026-09-22):weight_scale / visibility 目前**只存与显示**,
170
+ -- weight_scale 自 1.6.2 起**参与检索打分**(见 setBranchWeight);visibility 仍是预留字段(只存与显示),
171
171
  -- **不参与任何打分**(L1 的 score 只看血缘档位 × 矛盾系数)。让它们真正生效会改变检索排序语义,
172
172
  -- 须主人拍板后再动;在那之前返回体里带 applied=false 标记,免得界面把"存了"当成"生效了"。
173
173
  weight_scale REAL NOT NULL DEFAULT 1.0,
@@ -304,7 +304,13 @@ CREATE INDEX IF NOT EXISTS inject_log_session ON inject_log(session_id);
304
304
  -- 且**枝侧可反向引用条目**(气根供养树):一条枝能列出供养过它的条目,而条目的增删**不改动树结构**。
305
305
  CREATE TABLE IF NOT EXISTS deep_item (
306
306
  id TEXT PRIMARY KEY, -- 'di:' + uuid(与记忆树 br: 同风格;带前缀便于眼认)
307
- kind TEXT NOT NULL, -- commitment | decision | fact | preference | relation | todo
307
+ kind TEXT NOT NULL, -- 承诺 | 决定 | 事实 | 偏好 | 关系 | 纪律(中文,1.6 实存值)
308
+ -- ⚠️ **纪律**是 2026-10-05 夜主人裁决新增的第六类(见 §D 的裁定):
309
+ -- 与"决定"的区别是**决定管一次、纪律管以后每一次**。进库三条判据
310
+ -- (缺一条就退回收 kv / plans,不许进库):① 来源是**主人授权的一句指令**;
311
+ -- ② 作用对象是**行为**(与 README R-3 的"规矩"同族);③ **违反时能被判据红出来**
312
+ -- —— 答不出"哪条判据会红"就不是纪律,只是一句话(与 F-26 同一条线)。
313
+ -- 注:列本身**无 CHECK 约束**(TEXT NOT NULL),故加这一类**不需要改 schema**。
308
314
  text TEXT NOT NULL, -- 一句话 ≤80 字:它是**装配单元**(开场要从里面挑),必须独立读懂
309
315
  at TEXT NOT NULL DEFAULT '', -- 发生时间(取自原文那一轮;UTC ISO —— 与 dsh_turns_raw.ts 同口径)
310
316
  conv_id TEXT NOT NULL DEFAULT '', -- 来源会话(裸 id 或带命名空间前缀,与 conv_overview.conv_id 同口径)
@@ -637,12 +643,11 @@ export class MemoryStore {
637
643
 
638
644
  /** 全部枝(含主干)。
639
645
  *
640
- * ⚠️ 预留字段(审计 A#11,2026-09-22 定为方案 b):`weightScale` / `visibility`
641
- * 目前**只存只显示,不参与任何打分** —— L1 的分数是 `raw × 血缘档位 × 矛盾系数`,
642
- * 血缘档位由 `lineageWeightMap` 按树结构算(1.0 / 0.7 / 0.4),与这两个字段无关。
643
- * 改语义(让权重真正乘进检索分)会改变所有既有记忆的排序结果,**需主人拍板**,
644
- * 故这里不擅自改打分,只在返回体里如实标明 `weightScaleApplied/visibilityApplied = false`,
645
- * 让界面与调用方知道"调了它也没用",避免把预留字段当成已生效的旋钮。 */
646
+ * ✅ **A-5(2026-10-05 夜 · 主人拍板走方案 b):`weightScale` 已接回检索打分** ——
647
+ * L1 的分数是 `raw × 血缘档位 × 矛盾系数 × 枝系数`,故这里如实标 `weightScaleApplied: true`。
648
+ * ⚠️ `visibility` **仍是预留字段**(本次只拍了权重):它只存只显示,标 `visibilityApplied: false`,
649
+ * 界面不要给它做"生效中"的暗示。
650
+ * 历史:审计 A#11(2026-09-22)曾把两者一起定为"只存只显示";本次只解开权重那一半。 */
646
651
  listBranches() {
647
652
  try {
648
653
  return this.db.prepare('SELECT * FROM branch ORDER BY created_at, id').all().map((r) => ({
@@ -655,8 +660,8 @@ export class MemoryStore {
655
660
  nameLocked: Number(r.name_locked || 0),
656
661
  weightScale: Number(r.weight_scale ?? 1),
657
662
  visibility: String(r.visibility || 'lineage'),
658
- // 预留标记:见上方注释(审计 A#11)。界面不要给这两个字段做"生效中"的暗示。
659
- weightScaleApplied: false,
663
+ // A-5:权重已生效(见方法注释);visibility 仍为预留。
664
+ weightScaleApplied: true,
660
665
  visibilityApplied: false,
661
666
  status: String(r.status || 'active'),
662
667
  createdAt: r.created_at ? String(r.created_at) : null,
@@ -937,10 +942,9 @@ export class MemoryStore {
937
942
  /** 复盘期权重调节(平时只读 —— 设计稿 §3.3)。scale 夹在 [0,2]。
938
943
  *
939
944
  * ⚠️ 预留字段(审计 A#11,2026-09-22 定为方案 b):写进去的 `weight_scale`
940
- * **当前不参与检索打分**(L1 = raw × 血缘档位 × 矛盾系数)。保留写入与显示,
941
- * 是为了不破坏"复盘期可调"的界面与既有数据;但返回体里明确带
942
- * `applied:false` + `note`,避免调用方(与主人)误以为检索排序已经跟着变。
943
- * 让它真正生效属于**改变检索语义**,需主人拍板(方案 a),不在本次审计修法内。 */
945
+ * **A-5(2026-10-05 夜 · 主人拍板走方案 b):已接回检索打分** —— L1 = `raw × 血缘档位 × 矛盾系数 × 枝系数`。
946
+ * 历史:审计 A#11(2026-09-22)曾把权重与可见性一起定为"只存只显示";本次只解开权重那一半
947
+ * (可见性仍是预留字段,见 `listBranches`)。 */
944
948
  setBranchWeight(branchId, scale) {
945
949
  const bid = String(branchId || '');
946
950
  // ⚠️ 先判**类型**再取数(蓝队 2026-10-04):`Number(null) === 0`、`Number(true) === 1`、
@@ -954,7 +958,8 @@ export class MemoryStore {
954
958
  this.db.prepare('UPDATE branch SET weight_scale=?, updated_at=? WHERE id=?').run(clamped, utcIso(), bid);
955
959
  return {
956
960
  ok: true, id: bid, weightScale: clamped,
957
- applied: false, note: 'weight_scale 为预留字段:已写入并显示,当前不参与检索打分',
961
+ applied: true,
962
+ note: '已生效:该枝的结论在记忆开场里按此系数加权(1.0 = 不偏不倚,0 = 排到最后;不是禁用)',
958
963
  };
959
964
  }
960
965
 
@@ -85,7 +85,6 @@ export const DEFAULT_SETTINGS = Object.freeze({
85
85
  // ⚠️ 这是**内存态**设置(`settings-file.js` 的 get() 每轮读),面板 update() 立即改 ⇒ 不用重启宿主。
86
86
  sessionProgressLine: true,
87
87
  },
88
- updates: { applyWhileRunning: false }, // D4 语义固化:默认冻结
89
88
  guard: { // /api 守卫(2026-09-16 加固 ①+②;2026-09-17 加固 ③)
90
89
  enforce: true, // false = 关掉来源栅栏与名字对撞(逃生开关;亦可用 DSH_LING_GUARD=off)
91
90
  trustedHosts: [], // 显式受信的 authority 列表;loopback 恒受信,本机 LAN 地址**不再自动信任**
@@ -16,10 +16,9 @@
16
16
  // 实测 **516 帧扫描 1ms + 全量解压解析 53ms**(相对暴力法快约 2200 倍)。
17
17
  // 抄而不 import 的理由:不把插件绑到宿主内部包上(那是 P3 的劣,主人 2026-10-04 拍「先 P2」)。
18
18
  import fs from 'node:fs';
19
- import os from 'node:os';
20
19
  import path from 'node:path';
21
20
  import { zstdDecompressSync, constants as zc } from 'node:zlib';
22
- import { isMemoryEligibleHeader } from './util.js'; // F3(2026-10-04):真人判据要按**会话级**分通道
21
+ import { isMemoryEligibleHeader, dshHome } from './util.js'; // F3(2026-10-04):真人判据要按**会话级**分通道
23
22
 
24
23
  /** Zstandard 帧魔数(小端读作 UInt32LE 时的值)。 */
25
24
  const ZSTD_MAGIC = 0xfd2fb528;
@@ -46,9 +45,15 @@ function findLogIn(dir) {
46
45
  return null;
47
46
  }
48
47
 
49
- /** 会话根目录: ~/.dsh/sessions。 */
48
+ /** 会话根目录: `$DSH_HOME/sessions`(未设时 `~/.dsh/sessions`)。
49
+ * ⚠️ 2026-10-05 修正(A-4,交办单最后一处漏点):本函数原先是 `join(os.homedir(), '.dsh', 'sessions')`
50
+ * —— 绕过 `$DSH_HOME`,是全仓最后一处这么写的地方(`backfill.js:45-51` 记着另两处早在
51
+ * 2026-09-29 就统一到 `dshHome()` 了)。不设 `DSH_HOME` 时两者等价,**缺陷因此长期隐身**;
52
+ * 一旦设了(导入测试台就是靠它做隔离,`DESIGN-导入测试台.md:25` 明写"`DSH_HOME` 层面隔离才有效"),
53
+ * 这里就会去读**真机**会话目录 —— 不报错、只读,但结论不可复现,还会把真历史灌进测试库。
54
+ * 现统一到 `dshHome()`(= `$DSH_HOME` 优先,否则 `~/.dsh`),与扫描侧/取原文侧同源。 */
50
55
  export function sessionsRoot() {
51
- return path.join(os.homedir(), '.dsh', 'sessions');
56
+ return path.join(dshHome(), 'sessions');
52
57
  }
53
58
 
54
59
  /**