@nightpainters/dsh-ling 1.7.0 → 1.7.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,6 +2,74 @@
2
2
 
3
3
  本项目遵循语义化版本:`MAJOR.MINOR.PATCH`。
4
4
 
5
+ ## 1.7.2
6
+
7
+ > 1.7.1 之后的**回执降噪**小版本:**没有新功能**,只让 `recall` 的每一次回执更短、更干净。
8
+
9
+ ### 一、截断后清理未闭合标记
10
+
11
+ - `recall` 的命中片段常从原文中间截断,于是回执末尾留一个孤零零的 `**` / `` ` `` / `[` / `【` —— 读起来像断句错误,模型还可能据此误判语气。现在出口统一数一遍奇偶,**剥掉末尾那个没闭合的**。
12
+ - **不补**闭合标记:补会凭空造字(补在句中断句处,读起来更假)。
13
+ - 只动末尾那一处;中间成对的标记是回执自己的格式,一个字不动。
14
+
15
+ ### 二、固定文案收进工具说明,回执只留数据
16
+
17
+ - 「下一步」「换词口径」这两段原先**每条回执都印一遍**,而它们与本次查询无关。现在收进工具 description(模型每次组装系统面时都看得见),回执只留**数据 + 本次特有的警告**(超时、被范围挡掉、本会话被排除、还有多少轮…)。
18
+ - 实测背景:51 条真实回执平均 1,211 字符,这两段占了噪音的主体。
19
+ - 结论层的「要原话怎么取」原先**每条条目都印一遍**(6 条就是 6 遍),现在只印一次 —— 同一句指令重复 N 次是最典型的固定文案。
20
+ - 实测(同一批输入跑改前/改后渲染器):条目 + 5 轮原文的回执 **1,819 → 1,435 字(-21.1%)**;零命中 **181 → 118(-34.8%)**;超时 + 条目 **492 → 343(-30.3%)**。
21
+
22
+ ### 三、命中片段只剥成对标记(不动回执自身格式)
23
+
24
+ - 片段里的 `**粗体**` / `` `代码` `` / `[文字](地址)` 在界面渲染里有意义,在上下文里只是字符 ⇒ **只剥片段里成对的**;回执自己的结构照旧保留(表格、多行不塌)。
25
+ - 跨行的成对标记不剥 —— 剥了会把整段结构吃掉。
26
+
27
+ ### 四、结论层条目去重 + 同类折叠
28
+
29
+ - 一次检索里同一类目常回来一长串(实测样本:4 条全是「记忆树」),把真正不同的条目挤下去。现在:**完全重复的**(同会话 + 同文本)只留一条;同一类目超过 3 条时,前 3 条照给,**其余折叠成一行并报出条数** —— 不静默丢弃。
30
+
31
+ ### 五、回执总字数上限 4,000
32
+
33
+ - 超过就截断,并**报出还有多少字没显示** + 怎么分次取。绝不静默截断(那会让"没显示"读成"没有")。
34
+ - 「取原文」这一档**不套这个上限**:它有自己的 12,000 字护栏和"还有 N 轮没给"的报数,再套 4,000 会把取原文这件事本身弄废。
35
+
36
+ ### 六、emoji 与测试
37
+
38
+ - 回执里的 `📌 ⚠️ ✓ ✅ ❌ 🟡 ★` 是**状态符号**不是装饰:保留、不新增(新加一个 emoji 省不下 token,却要多花一次解释)。
39
+ - 新增 `tests/recall-noise.test.mjs`(M1/M2/M3/M5/M6 各一组断言,含"截断不得切出半个 emoji");测试链 **56 → 57 段**。
40
+
41
+ ## 1.7.1
42
+
43
+ > 1.7.0 之后的安全加固小版本,**不含新功能**。
44
+
45
+ ### 一、注入面净化补全(安全)
46
+
47
+ - 1.7.0 堵住了 recall 回执与会话行两条路径;**这一版补上剩下全部**:凡是会被拼进系统提示或请求体的文本,一律先做一遍「孤立代理」检测与替换 —— L1 记忆行的三处截断、留痕与摘要的五处截断、深摘转写的两个来源、诞生仪式素材、快照成文出口、注入面最终出口(时间锚也在内)。
48
+ - 为什么必须做:半个 emoji 进了请求体,上游 JSON 解析会直接失败(`unexpected end of hex escape`),而毒会**留在上下文里** ⇒ 之后每一次请求都 400,且看不出是谁干的。
49
+ - 截断长度契约**没有变**(仍是 UTF-16 单元口径、省略号仍占一位),只换掉"切在了哪里"。
50
+
51
+ ### 二、记忆树:有向连边按方向删除
52
+
53
+ - `prerequisite` / `supersede` 是**有向**的(A→B 与 B→A 是两条不同的边)。此前删掉一条会**把反方向那条一起删掉**,而回执只说"断开了" —— 树和回执对不上。现在有向类型只删同向;无向类型仍按旧口径双向清理(那是历史重复行的兜底)。
54
+
55
+ ### 三、结论层:开场不再因为"只有结论"而整段消失
56
+
57
+ - 开场记忆段原先的闸门查的是「有没有会话概述」,而料早已换成结论层条目 ⇒ 一个只有结论层条目、没有概述的库会**静默丢掉整段 L1**(面板看得见、注入看不见,典型的"不报错的错")。闸门改为让选料自己判空。
58
+
59
+ ### 四、记忆导入:整包原子
60
+
61
+ - 导入记忆包改为在**事务内**执行:中途抛异常则整体回滚,不再留下"一部分已覆盖、一部分没覆盖"的半截库;回执如实回 `errors: 1`,**不谎报行数**。单行坏数据仍按旧口径只计数、不回滚整包。
62
+
63
+ ### 五、客户端:一处"重试失效"、一处"面板说假话"
64
+
65
+ - **手术票**:一次失败会把整页生命周期锁死(用户只看到"没反应",没有任何提示)。改为**成功才锁定**,失败可再试,并有次数上限,不会变成无限刷屏。
66
+ - **access 导入日志**:迟到的响应不再覆盖新状态(与其他四个标签页一致的世代守卫)。
67
+
68
+ ### 六、测试与 CI
69
+
70
+ - `recall` 的真机断言不再押在"此刻最新的那个会话"上:改为在最近 5 个会话里挑**第一条真有可见对话轮**的;全都不可断言时**明说跳过**,而不是判红 —— 以前只要此刻有子代理会话在写,这套就会时红时绿。
71
+ - CI 两条:按 `package.json` 装依赖再逐段跑;`library-sqlite` 的大小写断言改成"先探测文件系统能力、再按对应结果判",不再假定 Linux 一定是大小写敏感 —— 这两条修的是 Linux runner 上的红。
72
+
5
73
  ## 1.7.0
6
74
 
7
75
  > ⚠️ **工作区候选 · 未发布。这一节记的是工作区里已落地、已被离线测试覆盖的工作,不是已发布状态。** 包版本已抬到 **`1.7.0`**,但**没有发布、没有安装**。宿主界面上的新标签**已在本机真机验收走通**(第五标签、两个图书馆工具、确定性核对、撤下留痕,2026-10-07),**第六标签「结论层」与其工具面已于 2026-10-15 随一次宿主重启实测生效**;对外发布仍需走正常的安装/重载流程(见 README「快速开始」)。
@@ -128,6 +196,15 @@
128
196
  - **顺带说清的边界**:同样的写法在**给模型看的工具文本**里还有一批(`lib/host/tools.js` 等)—— 那些走的是另一套渲染面,**不在本次范围内**,也不跟着改:混在一起改就是拿发布风险换整洁。
129
197
  - **判据**:整链 `npm test` exit 0(无一条断言依赖被去掉的星号);包内不含 `tests/`,测试自身的描述文案不受影响。
130
198
 
199
+ ### 十六、孤立代理防线:不再把 emoji 切成半个(2026-10-17)
200
+
201
+ - **现象**:有些会话里官方路由**每次请求都 400**(`INVALID_REQUEST`),换一条路由就正常 —— 全库核实 **550 个会话 / 真 400 共 9 次**,其中 **8 次**是同一条链(另外 1 次是 thinking 未回传,与本节无关)。
202
+ - **机制**:JavaScript 字符串允许**半个代理对**落单(高位 `U+D800–DBFF` / 低位 `U+DC00–DFFF`),那是**非法 UTF-16**。`recall` 的命中片段走的是 `text.slice(from, from+160)` —— 切点落在 emoji 中间就切出半个;那半个代理随工具回执进上下文、再被序列化进请求体,**上游 JSON 解析器随即崩**(错误串 `Failed to parse the request body as JSON: … unexpected end of hex escape`)。
203
+ - **两起实测(方向各一)**:`session-bc00b39b` #6331 是**起点**切坏(低代理 `0xdd12` 留在片段开头),`session-7ff65dd7` seq=58 是**终点**切坏(高代理 `U+D83C` 留在片段末尾);两例**都**触发了 400,没有侥幸活下来的样本。穷举 160 单元窗口的全部起点:**修复前 163/1534 与 165/1460 个起点会产毒,修复后 0**。
204
+ - **修法三层**:① `rawlog.js` 新增 `wellFormed()`(落单代理 → `U+FFFD`,优先用运行时 `toWellFormed`)、`clipChars()`(不切断代理对的截断)、`safeSlice()`(两端都不切断的取区间);② 三个危险切点改走新工具 —— `recall-search.js` 的命中片段、`session-line.js` 的标题/进展夹取(**那一行会进模型上下文**)、`rawlog.oneLine`(被 16 处共用);③ `renderSearch()` **出口兜底净化** —— 片段有三个来源(宿主 `bestMatch` / 库内原文层 / 目录),逐处修容易漏一处,出口对**整段回执**做一次。
205
+ - **测试**:新增 `tests/f4-surrogate.test.mjs` —— 把"旧写法确实会切坏"本身写成断言(谁改回 `slice` 会立刻红)+ 三个工具在各切点上的安全性 + `oneLine` 出口已接安全截断;`npm test` 链随之从 55 套增加到 **56 套**。
206
+ - ⚠️ **别读成"上游修好了"**:同日实测,含孤立代理的请求体打官方端点**仍然 400**(错误串指向 `dsh_session_log.events[0].data.message.content[0].text`)⇒ 这层防线**是我们自己的**;它在,不等于上游不再挑食。
207
+
131
208
  ## 1.6.2
132
209
 
133
210
  **检索档换了一条快两个数量级的路 · 结果不再被"我刚说的话"占满 · 几处会误导人的地方修掉。**
@@ -271,7 +348,7 @@
271
348
 
272
349
  ### 六、索引层:一个技能承载整层
273
350
 
274
- - `memory-index` 技能 = **11 簇主题地图**(记忆树与记忆库 · 器灵插件与发布 · 鱼姬与主人 · 主人其人 · 合作规矩 · 本业与项目 · 这台机器与踩坑 · 模型与API配置 · 插件与界面 · 感官与主动开口 · 零散几条),覆盖全部条目。
351
+ - `memory-index` 技能 = **11 簇主题地图**(记忆树与记忆库 · 器灵插件与发布 · 器灵与主人 · 主人其人 · 合作规矩 · 本业与项目 · 这台机器与踩坑 · 模型与API配置 · 插件与界面 · 感官与主动开口 · 零散几条),覆盖全部条目。
275
352
  - **常驻只有技能目录那一行**(description **431 字** + whenToUse 106 字),正文 2,341 字**按需展开**(模型主动取)。
276
353
  - **整层一个技能,不是每簇一个**:实测技能目录的边际成本 ≈205 字/条,11 簇拆成 11 个技能会吃掉 2,000+ 字常驻 —— 而那个常驻目录面**正是工具面压缩要抢的同一个池子**。
277
354
  - **description 里不写条数**:条数天天变,一写就会让目录重发、前缀缓存断;**簇名与触发线索稳定,条数放进按需展开的正文**。
@@ -552,7 +629,7 @@ DSH `0.1.7` 把事件 `agent/session-start` 改名成 `agent/created`(**载荷
552
629
 
553
630
  - 新增 4 个会话内工具:**`tree_read`**(读树 / 读某枝会话 / 看待理矛盾)· **`branch_edit`**(建枝 / 改名 / 挂载 / 连边 / 断边 / 调权重 / 删枝)· **`memory_forget`**(软遗忘)· **`memory_backfill`**(回灌);工具面 3 → 7;
554
631
  - 把树操作从 HTTP handler 抽成 `lib/host/tree-ops.js`,13 个 handler 变薄壳 —— **界面与工具共用同一份实现、同一份留痕**,零行为变更;
555
- - `branch_log` 新增 **`actor`** 列(schema **v12**):器灵改的写「鱼姬」,主人改的写「主人」;
632
+ - `branch_log` 新增 **`actor`** 列(schema **v12**):器灵改的写「器灵」,主人改的写「主人」;
556
633
  - **永不彻底删除**:`memory_forget` 先把原文与概述归档到 `forgotten/<时间戳>-<原因>/`(`turns.jsonl` 逐行 JSON、可 grep,`README.md` 写改动日志与还原方法),**再**在库里打标记 —— 两条独立还原路径(撤销标记 / 按 jsonl 重建);
557
634
  - `memory_backfill` 按「比遗忘更危险」处理:回灌前先归档当前状态,写回的概述带 `origin=backfill-v1` 标记,**绝不夺走用户态**(importance / hit_count 原样保留);
558
635
  - 修复**工具注册不生效**:原先只注册在全局层,而 Web 端按 agent preset 用 `restrict({allow})` 组工具 ⇒ 全局工具被静默过滤(连更早的 `rule_add` 也从未出现)。现在每个 agent 创建时在**它自己的 scope** 补注册一份。
@@ -801,13 +878,13 @@ D 组是关键:只证明"中和后不炸"无法排除"这份载荷本来就不
801
878
 
802
879
  旧行为把首条用户消息**硬截 46 字**,于是 60 条 dsh 概述里 **26 条是半句话**:
803
880
 
804
- - 「鱼姬,接下来在这里继续开发生长型人格与记忆树模块,继续走你的新生之路。 我刚刚对1.2.1的」
881
+ - 「器灵,接下来在这里继续开发生长型人格与记忆树模块,继续走你的新生之路。 我刚刚对1.2.1的」
805
882
  - 「刚才我为本虚拟机安装解压压缩软件<360压缩>,由于操作过快,被捆绑了360本体,随后我在物」
806
883
 
807
884
  三层改造:
808
885
 
809
- 1. **启发式**(零成本、离线可用):剥掉「鱼姬,」「晚上好小鱼,」这类称呼/问候前缀;上限 46 → 58 并在**句读处收尾**而不是硬切;首句是「你好」「下午好」「已测试聊天」这类无信息量句时改取后续有实义的首句;结果若含路径/URL/系统文本则一律弃用(宁可不动,也不产出更糟的名字);
810
- 2. **小助手起名**:`POST /memories/retitle` 用本机小助手读会话原文/摘要生成一句真标题 —— 实测「鱼姬,接下来在这里继续开发生长型人格与记忆树模块…」→「**修复审计漏洞并优化记忆库关联查询机制**」;
886
+ 1. **启发式**(零成本、离线可用):剥掉「器灵,」「晚上好器灵,」这类称呼/问候前缀;上限 46 → 58 并在**句读处收尾**而不是硬切;首句是「你好」「下午好」「已测试对话」这类无信息量句时改取后续有实义的首句;结果若含路径/URL/系统文本则一律弃用(宁可不动,也不产出更糟的名字);
887
+ 2. **小助手起名**:`POST /memories/retitle` 用本机小助手读会话原文/摘要生成一句真标题 —— 实测「器灵,接下来在这里继续开发生长型人格与记忆树模块…」→「**修复审计漏洞并优化记忆库关联查询机制**」;
811
888
  3. **名字一旦定过就不可覆盖**:`conv_overview` 新增 `title_locked`(schema v3)与 `title_by`(v4)。`POST /memories/rename`(主人手改)与 `POST /memories/retitle`(机器起名)**都上锁** —— 概述器每轮重建、批量重命名都不再覆盖它;要交还机器可解锁。记忆中心卡片新增「改名」「AI 起名」按钮与标记:**三条定名路径(主人手改 / 机器自动 / 来历未记录)统一显示「🔒 已定名」**,定名者只藏在悬浮说明里(「你起的名字…」/「自动起的名字…」)—— 不制造等级感;也不用「暂定」二字(人对一段记忆的概括本就是暂定的、会随阅历改变,但界面要让人确认"它已经落了笔")。用词不写死"小助手"(起名可能走本机小助手,也可能回落当前全局大模型)。悬浮说明点明:**这把锁锁住的是"自动重写",不是主人:手动随时可以再改**。
812
889
  4. **引擎就近选择(auto)**:`POST /memories/retitle` 默认 `engine:'auto'` —— 优先本机小助手(零成本),探测不到才落到**当前全局大模型**,并把实际用的引擎与原因写回响应(`engine` / `note`)。不静默换引擎:用了哪个看得见。
813
890
 
package/README.md CHANGED
@@ -153,7 +153,7 @@
153
153
 
154
154
  - 树平时**只读**(记忆中心始终只读);点「🌱 一键复盘 / 一键进入复盘」才解锁编辑,并把**复盘开场材料**(当前树 + 待理矛盾)复制到剪贴板,开个新会话粘进去就能开始。
155
155
  - 解锁后可做:**改名 / 拖动换归属 / Alt+拖动只连边 / 并脉(归类到主脉) / 调权重 / 裁定矛盾 / 断续边**,以及快照保存·恢复(**删枝不在面板上** —— 请让器灵用 `branch_edit` 删,见下文「器灵侧的手」);**拖动 / 连边之后可 Ctrl+Z 撤回这一步**(撤销只覆盖复盘栏的拖动与连边;记忆中心是只读的,它自己那份 Ctrl+Z 监听早已删掉)。
156
- - **每步留痕**:写清"改了什么、为什么"(**器灵改树时 reason 必填**;你在面板上改可以留空);树改动一律记进 `branch_log`,复盘时可审计 —— 器灵侧工具的改动记在器灵名下(「鱼姬改动了记忆树」),不会伪装成你改的。
156
+ - **每步留痕**:写清"改了什么、为什么"(**器灵改树时 reason 必填**;你在面板上改可以留空);树改动一律记进 `branch_log`,复盘时可审计 —— 器灵侧工具的改动记在器灵名下(「器灵改动了记忆树」),不会伪装成你改的。
157
157
  - **未同步笔数守门**:面板记着"自器灵上次开口以来累积了多少笔结构改动还没交代"。有欠账时顶部红字提示,并且**拦住"退出复盘模式"** —— 改完树必须让器灵有机会看到(「互相复盘不应该被我绕过」)。
158
158
  - **注入感知**:树变了之后,`branchLogDigest()` 把**器灵还没看过**的结构改动转成中文短句,附在器灵下一次开口的注入面末尾 —— 不需要推送,一开口就知道树动过。
159
159
  - **快照与撤回**:树的保存 / 恢复(`tree_snapshot`)是"大改之前的保险";恢复前会自动把当前状态另存一份,所以恢复本身也可再撤回来。
@@ -209,7 +209,7 @@
209
209
  - **严格配置,默认什么都没登记**:用于"我们自己代码"条目的代码根目录,与用于官方文档条目的 URL 白名单,都是**管理员登记的绝对路径 / 精确 URL**,**默认都是空的** —— 不登记就核不了,不存在"它自己上网找一份来对"。限额(条目 64 KiB、取源 2 MiB、选中文本 16 KiB 且不超过 200 行、单次核对 10 s、并发 2 等)在**激活时校验**:写错了**响亮报错**,不静默夹取。
210
210
  - **取消 = 没跑完,不是一次 `unknown` 认证**:核对在飞时断开、取消或卸载插件,本次核对**不写任何核对记录** —— 既不留 `ok` / `wrong`,**也不留 `unknown`**,只回一个"已取消"(带 `cancelled`)。"取消了"与"核完了(哪怕结论是 `unknown`)"在留痕里必须分得开:**只有跑完的核对才计入"认证过"**。
211
211
  - **远程核对会主动去你登记的地方取**:官方文档类源走 **HTTPS GET 你白名单里的那个精确 URL**(不许带凭据、不许跳转、私网地址拒 —— **还拒 `localhost` / `*.localhost` / `*.local` / `*.internal`,并对 DNS 解析结果与连接后的 `remoteAddress` 做二次校验**(实现见 `lib/host/library-checker.js`:地址校验 + 解析结果钉住 + 连接后复核))。**发出去的到底是什么,必须说准**:这个 GET 会把**你登记的那个 URL(连同它带的查询串)**与**一次普通 HTTPS 请求的元数据**(例如对方可见的连接来源 IP,以及 Accept/Accept-Encoding 请求头)交给那台服务器 —— 换句话说,**对方能看到你的 IP,也看得到这个 URL 本身**;但它**不会附带**条目正文、记忆内容、人格档案或任何本机文件。⚠️ 因此:**URL 里若放了不该外传的东西(比如把 token 写在查询串里),它就会被发出去** —— 白名单是你自己登记的,请当成"公开可见"来写。白名单默认为空 ⇒ 图书馆默认不发生外部取源访问。
212
- - **边界(如实写)**:这一版已抬版到 **`1.7.0`(工作区候选)但仍未发布、未安装**;而"没发布"**不等于"没验过"** —— 第五个标签(图书馆)已于 **2026-10-07 在本机既有 3080 上真机验收走通**(建档 / 核对 / 撤下留痕 / 撤销弹窗全过),实现与交互测试同时也在源码与离线测试里。两者别混读。另有一条边界要说清:**"不把记忆库里的旧数据迁进图书馆"不等于"禁止网页端来源的资料"** —— 一条资料能不能进图书馆,看的是它**有没有具体、可追责的出处**,与它从哪个渠道来无关。
212
+ - **边界(如实写)**:这一版的包版本是 **`1.7.2`(工作区候选)**;**上一版已于 2026-10-17 发布上 npm**(`latest`),`1.7.2` 尚未发布。而"没发布"**不等于"没验过"** —— 第五个标签(图书馆)已于 **2026-10-07 在本机既有 3080 上真机验收走通**(建档 / 核对 / 撤下留痕 / 撤销弹窗全过),实现与交互测试同时也在源码与离线测试里。两者别混读。另有一条边界要说清:**"不把记忆库里的旧数据迁进图书馆"不等于"禁止网页端来源的资料"** —— 一条资料能不能进图书馆,看的是它**有没有具体、可追责的出处**,与它从哪个渠道来无关。
213
213
 
214
214
  > **机制依据与源码证据**(表结构、核对三态与各条判据的实际位置、取消与错误码落在哪一行)集中在 [`DESIGN.md`](./DESIGN.md) 的「图书馆:机制与源码证据」一节 —— 本节讲"用户能感觉到什么",那一节给出"代码里是哪一行"。
215
215
 
@@ -285,18 +285,22 @@ node tools/check-peers.mjs # 声明与宿主对不上?退出码三态
285
285
  跑测试(不需要宿主,全部离线):
286
286
 
287
287
  ```bash
288
- npm test # 55 套:人格组装(含冻结门 · L1 选择)/ 归档清理(人工)/ **归档·遗忘·回灌** / 模式(只改档位不改模型)/ **跨插件挡位服务(dsh-ling/mode)** / 时间戳归一 / 空闲刷新 / 规矩·习惯 / 习惯生成器(A 数出来 + B 想出来)/ 会话内工具 / **接口守卫** / L1 行瘦身 / 记忆库 / 记忆标题与排序 / 导出合并 / 文件导入 / 导入记录 / **活行(那一行跟着会话走)** / **会话文件名代次(会话历史扫描)** / 网页端补摘要 / 深摘要 / 概述器 / 诞生仪式 / **职责字段(承担)** / 语气 / 时间锚 / **clock skill(时间查询)** / 捕获差分 / 反馈成长回路 / 语料建议 / **小助手地址配置** / **注入面中和** / **记忆分枝与血缘** / **记忆树(主脉·连边·矛盾)** / **一键生成树** / **可取证访问日志** / **补丁锚点(cohort 候选)** / **请求体上限(超限排空 → 413)** / **导入切批(按字节 × 条数双限)** / **投递留痕(每次注入落一条:注入了谁·为什么是它·多少字)** / **会话原文直读(三档下钻 + 库内回退 + 旧版会话代次)** / **深层库条目(结论层落点:读写·幂等·取代·加固)** / **检索档(按内容找会话:条目 + 会话同查·范围限制与降级)** / **图书馆存储层(独立库:生命周期·限额·检索·留旧·不可用≠零命中)** / **图书馆跨连接事务(陈旧修订拒绝·完成的核对留档·序号一致)** / **图书馆核对(三态判定·只认证声明的那个比较·限额与取消不写)** / **图书馆运行时(禁用/不可用降级·接线与关闭顺序)** / **图书馆接口与工具(端点形状·状态码映射·两个工具的参数边界)** / **图书馆界面(独立库的面:分页无伪总数·必填与 JSON 校验·推迟应答竞态)** / **图书馆导入引擎(料源登记与导入)** / **图书馆独立 SQLite(隔离库:只读打开·表必须点名)** / **图书馆浏览(分页与零命中)** / **热重载补绑(工具重挂载:存量 agent 补绑)** / **文档灌入通道(结论层 C 档:两道污染闸·三级去重·出处核验·累计预算)** / **后台自动行为开关(auto:五键默认值·三级优先级·白名单夹取·四条真 HTTP 回归)**
288
+ npm test # 57 套:人格组装(含冻结门 · L1 选择)/ 归档清理(人工)/ **归档·遗忘·回灌** / 模式(只改档位不改模型)/ **跨插件挡位服务(dsh-ling/mode)** / 时间戳归一 / 空闲刷新 / 规矩·习惯 / 习惯生成器(A 数出来 + B 想出来)/ 会话内工具 / **接口守卫** / L1 行瘦身 / 记忆库 / 记忆标题与排序 / 导出合并 / 文件导入 / 导入记录 / **活行(那一行跟着会话走)** / **会话文件名代次(会话历史扫描)** / 网页端补摘要 / 深摘要 / 概述器 / 诞生仪式 / **职责字段(承担)** / 语气 / 时间锚 / **clock skill(时间查询)** / 捕获差分 / 反馈成长回路 / 语料建议 / **小助手地址配置** / **注入面中和** / **记忆分枝与血缘** / **记忆树(主脉·连边·矛盾)** / **一键生成树** / **可取证访问日志** / **补丁锚点(cohort 候选)** / **请求体上限(超限排空 → 413)** / **导入切批(按字节 × 条数双限)** / **投递留痕(每次注入落一条:注入了谁·为什么是它·多少字)** / **会话原文直读(三档下钻 + 库内回退 + 旧版会话代次)** / **深层库条目(结论层落点:读写·幂等·取代·加固)** / **检索档(按内容找会话:条目 + 会话同查·范围限制与降级)** / **图书馆存储层(独立库:生命周期·限额·检索·留旧·不可用≠零命中)** / **图书馆跨连接事务(陈旧修订拒绝·完成的核对留档·序号一致)** / **图书馆核对(三态判定·只认证声明的那个比较·限额与取消不写)** / **图书馆运行时(禁用/不可用降级·接线与关闭顺序)** / **图书馆接口与工具(端点形状·状态码映射·两个工具的参数边界)** / **图书馆界面(独立库的面:分页无伪总数·必填与 JSON 校验·推迟应答竞态)** / **图书馆导入引擎(料源登记与导入)** / **图书馆独立 SQLite(隔离库:只读打开·表必须点名)** / **图书馆浏览(分页与零命中)** / **热重载补绑(工具重挂载:存量 agent 补绑)** / **文档灌入通道(结论层 C 档:两道污染闸·三级去重·出处核验·累计预算)** / **后台自动行为开关(auto:五键默认值·三级优先级·白名单夹取·四条真 HTTP 回归)**
289
289
  ```
290
290
 
291
291
  > `npm test` 需要 `tests/`,而本包 `files` 白名单**不含 `tests/`** ⇒ **从 npm 装的人跑不了测试**。要跑测试请走路径 B(clone 源码)。
292
292
  >
293
- > 计数口径:**`npm test` 链里登记的段数**(即 `package.json` 的 `scripts.test` 那一行按 `&&` 切分后有多少段就是多少套,不再数 `tests/` 目录)—— **实测 55 套(即链里 55 段),且 55 套全部已登记在 `npm test` 链里**(`tests/harness.mjs` 与 `tests/vm-light.mjs` 是被复用的辅助模块,不计套数)。**加了一套新测试记得同步登记**,否则 `npm test` 会静默少跑一套 —— 这份登记是**手写**的(`package.json` 里 `test` 脚本那一行字面量清单),**漏登记不会被自动发现**,没有安全网兜底。
293
+ > 计数口径:**`npm test` 链里登记的段数**(即 `package.json` 的 `scripts.test` 那一行按 `&&` 切分后有多少段就是多少套,不再数 `tests/` 目录)—— **实测 57 套(即链里 57 段),且 57 套全部已登记在 `npm test` 链里**(`tests/harness.mjs` 与 `tests/vm-light.mjs` 是被复用的辅助模块,不计套数)。**加了一套新测试记得同步登记**,否则 `npm test` 会静默少跑一套 —— 这份登记是**手写**的(`package.json` 里 `test` 脚本那一行字面量清单),**漏登记不会被自动发现**,没有安全网兜底。
294
294
  >
295
295
  > ⚠️ **2026-10-15 实测订正**:此前这里写着 49、而链里当时已经有 53 段 —— 也就是**这段注释自己警告的坑真发生过**:套件登记进去了,这个数字却没人回头改,四次都没被任何判据抓住。当天新增 `deep-import-docs.test.mjs`(C 档文档灌入)⇒ 54。这个数字没有判据替你看着,改链时必须顺手改它。
296
296
  >
297
- > ⚠️ **2026-10-16 再 +1**:新增 `tests/auto-config.test.mjs`(后台自动行为开关:五键默认值 / 三级优先级 / 白名单与夹取 / **四条真 HTTP 回归**)⇒ **55 套**。同日还抓到**一条真红**:修复批次把 `tools/compliance-spec.mjs` 的出站基线从 6 抬到 23 时,**漏改了 `tests/tools.test.mjs` 里钉住的字面量** ⇒ 整链自那时起就是红的,直到当日修复。**同一个坑两次**(数字写死在多处、改一处忘另一处)—— 改链、改基线时,**同批**把所有钉死它的地方一起改。
297
+ > ⚠️ **2026-10-16 再 +1**:新增 `tests/auto-config.test.mjs`(后台自动行为开关:五键默认值 / 三级优先级 / 白名单与夹取 / **四条真 HTTP 回归**)⇒ 计数从 54 变成 55。同日还抓到**一条真红**:修复批次把 `tools/compliance-spec.mjs` 的出站基线从 6 抬到 23 时,**漏改了 `tests/tools.test.mjs` 里钉住的字面量** ⇒ 整链自那时起就是红的,直到当日修复。**同一个坑两次**(数字写死在多处、改一处忘另一处)—— 改链、改基线时,**同批**把所有钉死它的地方一起改。
298
298
  >
299
- > **本轮跑过的实测(1.7 工作区)**:在**隔离环境**(临时数据目录,不碰真机记忆库/图书馆库)下完整跑过一遍 `npm test` —— **55 套全部通过、exit 0**(宿主侧 `deep_item` / `GET /deep/items` 另在 2026-10-15 重启后实测生效);各组件的断言**条数会随迭代变动,本文不写死** —— **最终测试结果以工作区验收清单为准**。⚠️ 这只说明"**这一版代码在离线测试里的行为**",**不等于"所有门禁都完成了"** —— 真实远程核对与真实 DNS/TLS 仍未验(见上文「图书馆」一节的边界)。
299
+ > ⚠️ **2026-10-17 再 +1**:新增 `tests/f4-surrogate.test.mjs`(注入面净化回归:孤立代理不许进请求体)⇒ **56 套**。同一天还修掉了这套测试自己的一个坑:`recall` 的真机断言原先押在"此刻最新的那个会话"上,并发子代理在场时会**时红时绿** ⇒ 改为在最近 5 个会话里挑第一条真有可见对话轮的。 <!-- ver-hist -->
300
+ >
301
+ > ⚠️ **2026-10-23 再 +1**:新增 `tests/recall-noise.test.mjs`(回执降噪回归:M1 未闭合标记 / M2 固定文案不进回执 / M3 只剥片段里的成对标记 / M5 条目去重与同类折叠 / M6 4,000 字上限与"还剩多少"报数,含**截断不得切出半个 emoji**)⇒ **57 套**。
302
+ >
303
+ > **本轮跑过的实测(1.7.2 工作区)**:在**隔离环境**(临时数据目录,不碰真机记忆库/图书馆库)下完整跑过一遍 `npm test` —— **57 套全部通过、exit 0**(宿主侧 `deep_item` / `GET /deep/items` 另在 2026-10-15 重启后实测生效);各组件的断言**条数会随迭代变动,本文不写死** —— **最终测试结果以工作区验收清单为准**。⚠️ 这只说明"**这一版代码在离线测试里的行为**",**不等于"所有门禁都完成了"** —— 真实远程核对与真实 DNS/TLS 仍未验(见上文「图书馆」一节的边界)。
300
304
 
301
305
  ### 数据放在哪
302
306
 
@@ -479,7 +483,7 @@ npm test # 55 套:人格组装(含冻结门 · L1 选择)/ 归档清理(
479
483
 
480
484
  ### 1.7 新增(图书馆 · 工作区,未发布)
481
485
 
482
- > ⚠️ **本节的状态标记只指"工作区代码 + 离线测试"这一层**:1.7 **尚未发布**(包版本已抬到 `1.7.0`,尚未发布/安装);"宿主界面上的「图书馆」标签"**已真机验收走通**(2026-10-07,见 L-11);「结论层」标签已于 **2026-10-15** 随一次宿主重启实测生效。
486
+ > ⚠️ **本节的状态标记只指"工作区代码 + 离线测试"这一层**:包版本已抬到 `1.7.2`(工作区候选);上一版已于 2026-10-17 发布公布。"宿主界面上的「图书馆」标签"**已真机验收走通**(2026-10-07,见 L-11);「结论层」标签已于 **2026-10-15** 随一次宿主重启实测生效。
483
487
 
484
488
  | # | 设定 | 状态 |
485
489
  |---|---|---|
@@ -494,7 +498,7 @@ npm test # 55 套:人格组装(含冻结门 · L1 选择)/ 归档清理(
494
498
  | **L-8** | **写面必须带"观测到的内容摘要"**(`content_hash`),对不上就拒 —— 防"看着旧版改新版" | ✅ 工作区 |
495
499
  | **L-9** | **严格配置** —— 代码根目录与官方文档 URL 白名单**默认为空**、由管理员登记;限额在**激活时校验**,非法值**响亮报错**、不静默夹取 | ✅ 工作区 |
496
500
  | **L-10** | **取消 = 没跑完,不留记录** —— 核对在飞时取消 / 断开 / 卸载 ⇒ 本次**不写任何核对记录**(既不留 `ok` / `wrong`,**也不留 `unknown`**);只有**跑完的**核对(含结论 `unknown`)才留账 | ✅ 工作区 |
497
- | **L-11** | **发布状态如实声明** —— 包版本已抬到 **`1.7.0`(工作区候选)**,**未发布、未安装**;但「图书馆」标签**已真机验收走通**(2026-10-07),「结论层」标签也已随 2026-10-15 的重启实测生效 —— **"没发布"与"没验过"必须分开说** | ✅ 真机验收(2026-10-07) |
501
+ | **L-11** | **发布状态如实声明** —— 包版本已抬到 **`1.7.2`(工作区候选)**,**本版未发布**;上一版已发布上 npm;但「图书馆」标签**已真机验收走通**(2026-10-07),「结论层」标签也已随 2026-10-15 的重启实测生效 —— **"没发布"与"没验过"必须分开说** | ✅ 真机验收(2026-10-07) |
498
502
 
499
503
  > ⚠️ **这一节还在补。** 完整规格有**四百余条**,本节先落**对外面**里最稳定的一批(设计理念);其余按主题分批补写 —— 每一条都要从开发期设定**改写成对外语言**并核对状态,这是**写作**而不是复制。
500
504
 
package/lib/client.js CHANGED
@@ -168,15 +168,27 @@
168
168
  // 取不到(经反代打开的页面 / 旧宿主)= 空串 ⇒ 手术请求照旧发,由宿主那边给出明确拒绝。
169
169
  var surgeryTicket = '';
170
170
  var surgeryTicketTried = false;
171
+ var surgeryTicketFailCount = 0; // D2 修复:失败计数,每页面生命周期最多 2 次(第 3 次不再自动试)
171
172
  function fetchSurgeryTicket() {
172
- surgeryTicketTried = true; // 无论成败都只试一次(失败不重试,免得把拒绝变成刷屏)
173
+ surgeryTicketTried = true; // 标记"正在尝试",阻止 api() 重入路径并发触发第二发
173
174
  return fetch(API + '/surgery/ticket', { method: 'POST', headers: { 'content-type': 'application/json' } })
174
175
  .then(function (r) { return r.json().catch(function () { return null; }); })
175
176
  .then(function (j) {
176
- if (j && j.ok && typeof j.ticket === 'string' && j.ticket) { surgeryTicket = j.ticket; return surgeryTicket; }
177
- return ''; // 远端 / 未定型:没有票,照常走后面的拒绝路径
177
+ if (j && j.ok && typeof j.ticket === 'string' && j.ticket) {
178
+ surgeryTicket = j.ticket; // 成功:票落袋,保持 tried=true(不再重取)
179
+ return surgeryTicket;
180
+ }
181
+ // 失败(远端拒绝 / 旧宿主):解锁以便再试,但计次;到上限后不再自动试
182
+ surgeryTicketTried = false;
183
+ surgeryTicketFailCount++;
184
+ return '';
178
185
  })
179
- .catch(function () { return ''; });
186
+ .catch(function () {
187
+ // 网络错误 / 超时:同上
188
+ surgeryTicketTried = false;
189
+ surgeryTicketFailCount++;
190
+ return '';
191
+ });
180
192
  }
181
193
 
182
194
  function api(path, opts) {
@@ -192,7 +204,7 @@
192
204
  }).then(function (j) {
193
205
  // 宿主说"票据不对/没票"⇒ 现场取一次,原样重发一次(只重发一次,不做循环)。
194
206
  // 票据每启动轮换 ⇒ 页面放久了会遇到这一支,那时用户不必手动刷新。
195
- if (j && j.ok === false && j.surgery === 'surgery-ticket' && !surgeryTicketTried) {
207
+ if (j && j.ok === false && j.surgery === 'surgery-ticket' && !surgeryTicketTried && surgeryTicketFailCount < 2) {
196
208
  return fetchSurgeryTicket().then(function (t) {
197
209
  if (!t) return j;
198
210
  var h2 = { 'content-type': 'application/json', 'x-dsh-ling-surgery': t };
@@ -3912,8 +3924,11 @@
3912
3924
  g.appendChild(cardD);
3913
3925
  body.appendChild(g);
3914
3926
 
3927
+ var importLogSeq = 0; // F-06 修复:导入日志世代守卫(照抄 libSeq 模式,见 :4445)
3915
3928
  function renderImportLog() {
3929
+ var token = ++importLogSeq;
3916
3930
  api('/import/history').then(function (r) {
3931
+ if (token !== importLogSeq) return; // 迟到的旧应答:丢弃(不把上一次的结果画进这一次)
3917
3932
  logOut.textContent = '';
3918
3933
  if (!r || !r.ok || !r.items || !r.items.length) {
3919
3934
  logOut.appendChild(el('div', 'm-empty', '还没有导入记录 —— 从上面把一段历史请进来,这里就会留账。'));
@@ -3923,7 +3938,10 @@
3923
3938
  logOut.appendChild(el('div', null, it.line || ''));
3924
3939
  });
3925
3940
  if (r.items.length > 20) logOut.appendChild(el('div', 'm-meta', '(仅显示最近 20 条)'));
3926
- }).catch(function () { logOut.textContent = '记录加载失败(网关不可达)'; });
3941
+ }).catch(function () {
3942
+ if (token !== importLogSeq) return; // 同理:迟到的失败也不许凭空写新视图
3943
+ logOut.textContent = '记录加载失败(网关不可达)';
3944
+ });
3927
3945
  }
3928
3946
  bLogRefresh.onclick = renderImportLog;
3929
3947
  renderImportLog();
@@ -105,7 +105,7 @@ export function archiveBefore(memory, { source, convId, kind = 'forget', reason
105
105
 
106
106
  /** 改动日志 —— 这份文件是给**将来的人**看的,所以写全:是什么、为什么、怎么还原。 */
107
107
  function readme({ at, s, c, kind, reason, actor, turns }) {
108
- const who = actor === 'ling' ? '鱼姬(工具通道)' : '主人(界面通道)';
108
+ const who = actor === 'ling' ? '器灵(工具通道)' : '主人(界面通道)';
109
109
  // A-01(1.5.1):第三种 kind —— 主脉记忆覆盖(同一主脉再次采纳提炼结果;用户裁决"备份并覆盖")。
110
110
  const what = kind === 'forget' ? '遗忘(软标记 —— **库里的行没有删**)'
111
111
  : kind === 'vein-overwrite' ? '主脉记忆**覆盖**(同一主脉再次采纳 —— **上一版正文完整存在这里**)'
@@ -114,14 +114,14 @@ function readme({ at, s, c, kind, reason, actor, turns }) {
114
114
  // 「怎么还原」按 kind 分三档。用**字符串拼接**而不是嵌套模板,免得外层模板里的反引号打架。
115
115
  const restoreBlock = kind === 'forget'
116
116
  ? '**库里没有删任何行** —— 只是多了一条 `forgotten` 标记,召回路径不再选它。撤销方式二选一:\n\n'
117
- + '1. 让鱼姬调工具撤销(`memory_forget` 的 `undo`),或\n'
117
+ + '1. 让器灵调工具撤销(`memory_forget` 的 `undo`),或\n'
118
118
  + '2. 直接删标记:\n ```sql\n DELETE FROM forgotten WHERE source=\'' + s + '\' AND conv_id=\'' + c + '\';\n ```'
119
119
  : kind === 'vein-overwrite'
120
120
  ? '覆盖只动了 `conv_overview` 里那一行(主脉自己的记忆),原文与会话元数据不受影响。要把上一版换回去:\n\n'
121
- + '1. 让鱼姬按本目录 `overview.json` 的 `summary` **再采纳一次**旧正文,或\n'
121
+ + '1. 让器灵按本目录 `overview.json` 的 `summary` **再采纳一次**旧正文,或\n'
122
122
  + '2. 直接写回:\n ```sql\n UPDATE conv_overview SET summary=\'<overview.json 的 summary>\'\n WHERE source=\'' + s + '\' AND conv_id=\'' + c + '\';\n ```'
123
123
  : '回灌只是把归档写回去,而**它覆盖之前的状态存在这里**。要退回去:\n\n'
124
- + '1. 让鱼姬再回灌一次旧档(`memory_backfill`),或\n'
124
+ + '1. 让器灵再回灌一次旧档(`memory_backfill`),或\n'
125
125
  + '2. 按下面「万一库本身没了」的步骤写回。';
126
126
  return `# 归档:${title}的原始数据
127
127
 
@@ -17,14 +17,14 @@ export const INDEX_SKILL_NAME = 'memory-index';
17
17
  export const INDEX_SKILL_DESCRIPTION = `我记住的事按 11 类放着(用 recall({q:"关键词"}) 按内容搜 —— 它同时搜**结论**与**会话原文**):
18
18
  · 记忆树与记忆库:记忆怎么存、怎么搜、血缘归并、深层库
19
19
  · 器灵 dsh-ling:包名、发布仓库、版本与宣发、插件配置
20
- · 鱼姬与主人:称呼、纪念日、同伴身份、形象锚与立场
20
+ · 器灵与主人:称呼、纪念日、同伴身份、形象锚与立场
21
21
  · 主人其人:职业来路、身体作息、脾气念头、价值观
22
22
  · 合作规矩:先审后装、干跑、重启要问、实测复跑、备份
23
23
  · 本业与项目:本职的专业领域、论文与研究、自制的装置项目
24
24
  · 这台机器与踩坑:VM/Win10、硬件、乱码、沙箱、网络接入
25
25
  · 模型与API:选型、推理档位、缓存、上下文、本地小模型(我的手)
26
26
  · 插件与界面:装/卸/审计、侧栏、皮肤、权限选择器
27
- · 鱼姬的感官和开口:看图/生图、语音、听音乐、主动开口
27
+ · 器灵的感官和开口:看图/生图、语音、听音乐、主动开口
28
28
  · 零散几条:护眼工具、天文观测、投资慢课这类生活杂事`;
29
29
 
30
30
  export const INDEX_SKILL_WHEN_TO_USE = '想知道"我以前说过/定过什么"、或者要在一堆长期结论里找依据时读它;它的每一行是一个**主题簇**,指明了那一类里装着什么样的事。日常回答通常不需要读它 —— 索引行本身已经够你判断该不该用 recall 去搜。';
@@ -63,7 +63,7 @@ export const INDEX_SKILL_CONTENT = `# 深层库索引 · 我记住的事分 11
63
63
  |---|---|---|
64
64
  | 记忆树与记忆库 | 记忆怎么存、怎么搜,血缘归并,深层库 | 记忆树 · 记忆库 · 血缘 · 召回 |
65
65
  | 器灵 dsh-ling | 包名、发布仓库、版本与宣发、插件配置 | 器灵 · dsh-ling · 发布 |
66
- | 鱼姬与主人 | 称呼、纪念日、同伴身份、形象锚与立场 | 称呼 · 纪念日 · 同伴 · 形象 |
66
+ | 器灵与主人 | 称呼、纪念日、同伴身份、形象锚与立场 | 称呼 · 纪念日 · 同伴 · 形象 |
67
67
  | 主人其人 | 职业来路、身体作息、脾气念头、价值观 | 职业 · 睡眠 · 价值观 · 立场 |
68
68
  | 合作规矩 | 谁拍板、怎么审计、怎么汇报、红线 | 重启 · 审计 · 备份 · 规矩 |
69
69
  | 本业与项目 | 本职的专业领域、论文与研究、自制的装置项目 | 机械 · 论文 · 合金 · 弹跳球 |
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  import { readdirSync, readFileSync, statSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { svc, utcIso, dshHome } from './util.js';
7
+ import { wellFormed } from './rawlog.js'; // 1.7.1 F4 闭环:深摘转写直接进请求体
7
8
  import { RAW_IMPORT_PREFIX, rawSessionCandidates } from './memory.js';
8
9
  import { pickSessionFile } from './backfill.js';
9
10
  import { currentModelSelection } from './mode.js';
@@ -229,7 +230,8 @@ export function transcriptFromFile(file, { cap = DEEP_CFG.inputCap } = {}) {
229
230
  const budget = Math.max(100, cap - mark.length);
230
231
  joined = joined.slice(0, Math.floor(budget * 0.6)) + mark + joined.slice(-Math.floor(budget * 0.4));
231
232
  }
232
- return { text: joined, users, asst, chars: joined.length, file };
233
+ const safe = wellFormed(joined);
234
+ return { text: safe, users, asst, chars: safe.length, file };
233
235
  }
234
236
 
235
237
  /** DB 原文转录(role user/assistant,按 seq)。source 给定时按 G4 命名空间取原文
@@ -261,7 +263,8 @@ export function buildTranscript(memory, sessionId, { cap = DEEP_CFG.inputCap, so
261
263
  const budget = Math.max(100, cap - mark.length);
262
264
  text = text.slice(0, Math.floor(budget * 0.6)) + mark + text.slice(-Math.floor(budget * 0.4));
263
265
  }
264
- return { text, users, parts: parts.length, chars: text.length };
266
+ const safe = wellFormed(text);
267
+ return { text: safe, users, parts: parts.length, chars: safe.length };
265
268
  }
266
269
 
267
270
  /**
@@ -9,6 +9,9 @@
9
9
  // 2) 阈值:轮次数 < minTurns(默认 2)默认不补(纯闲聊),显式 --all 才含;
10
10
  // 3) 引擎:默认**本机小助手**(零成本);**绝不静默切换到大模型**(那等于替用户花钱)。
11
11
  import { DatabaseSync } from 'node:sqlite';
12
+ // 1.7.1(delta 复审):本文件把会话原文拼进 `JSON.stringify(body)` 直接 POST 给小助手 ——
13
+ // 那也是一条"文本进请求体"的路径,与注入面同罪。原文里若带孤立代理,毒照样发出去。
14
+ import { wellFormed } from './rawlog.js';
12
15
 
13
16
  // S7(2026-09-18 与用户定):默认地址**中性化**为 loopback —— 发布版绝不携带私有内网地址,
14
17
  // 主人的真实小助手地址改由 settings.assistant(UI 可配)或环境变量指定,三级覆盖见 resolveAssistant()。
@@ -146,8 +149,8 @@ export async function summarizeLocal(baseUrl, model, { system = SUMMARY_SYS, tex
146
149
  const ctrl = new AbortController();
147
150
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
148
151
  try {
149
- const sys = noThink ? system + '\n' + NO_THINK : system;
150
- const usr = noThink ? text + '\n' + NO_THINK : text;
152
+ const sys = wellFormed(noThink ? system + '\n' + NO_THINK : system);
153
+ const usr = wellFormed(noThink ? text + '\n' + NO_THINK : text);
151
154
  const body = noThink
152
155
  ? {
153
156
  model, stream: false, think: false,
@@ -16,6 +16,8 @@
16
16
  // 措辞必须**陈述式**(我承担/我擅长/我能给出),**禁授权式**(我应当/我必须/我的任务/我的责任是):
17
17
  // 授权式写法把她写成一个"被授权者",是语义劫持最好用的钩子(参照 [规矩] 段头那次的教训)。
18
18
 
19
+ import { wellFormed } from './rawlog.js'; // 1.7.1 F4 闭环:诞生仪式的请求体同样不许带毒
20
+
19
21
  /** 授权式措辞清单(1.5.2 I-5,约束 C):起草 prompt 必须逐字列出它们作为**禁止项**。
20
22
  * 导出而非内联,是为了让"prompt 里的禁令"和"界面上的提醒(pm 面板里那句『读起来像…建议不选它』)"
21
23
  * 以及测试断言共用同一份事实 —— 三处各写一份必然会漂移成说谎的护栏。
@@ -120,7 +122,7 @@ export function snippetFromRaw(memory, convId, { maxMsgs = 3, perMsg = 200, pron
120
122
  if (!t) continue;
121
123
  parts.push(who + ': ' + (t.length > perMsg ? t.slice(0, perMsg) + '…' : t));
122
124
  }
123
- return parts.join(' | ');
125
+ return wellFormed(parts.join(' | '));
124
126
  } catch {
125
127
  return '';
126
128
  }
@@ -142,9 +144,9 @@ export function buildGenesisSource(rows, { cap = 18000 } = {}) {
142
144
  parts.push(line);
143
145
  }
144
146
  const text = parts.join('\n');
145
- if (text.length <= cap) return text;
147
+ if (text.length <= cap) return wellFormed(text);
146
148
  // 截断:保头丢尾(排序保证最前面的正是生活/情感优先的条目)
147
- return text.slice(0, cap) + '\n…(历史较长,其余省略)';
149
+ return wellFormed(text.slice(0, cap)) + '\n…(历史较长,其余省略)';
148
150
  }
149
151
 
150
152
  /**
@@ -6,6 +6,7 @@
6
6
  // - idle sessions with a stale snapshot: rebuild lazily (one-time invalidation);
7
7
  // - subagent/child/unknown sessions: return '' (R5).
8
8
  import { svc, pick, neutralizeMustache, sessionKind } from './util.js';
9
+ import { wellFormed } from './rawlog.js'; // 1.7.1 F4 闭环:注入面最终出口
9
10
  import { buildSnapshotText, firstUserText } from './snapshot.js';
10
11
  import { currentMode } from './mode.js';
11
12
  import { keywordsFrom } from './inject-common.js';
@@ -143,7 +144,9 @@ export function registerMemorySection(ctx, gate, memory, settings, { enabled = t
143
144
  // 而 agent-loop 调用处无 try/catch ⇒ **该会话每一步模型调用都失败**。
144
145
  // 这正是 1.3.1 拆掉的那颗雷(S12)的同类面:section 侧一直有中和
145
146
  // (见 renderMemorySection 的 `neutralizeMustache(anchor)`),搬迁时漏了这一处。
146
- text: () => neutralizeMustache(timeAnchorLive(memory, settings)),
147
+ // 1.7.1(delta 复审):这是**独立于 snapshot 的第二条出口** —— context 文本也会进模型上下文,
148
+ // 只中和 mustache 不够;时间锚里的称呼/日期同样是往外送的文本,照样过 wellFormed。
149
+ text: () => wellFormed(neutralizeMustache(timeAnchorLive(memory, settings))),
147
150
  });
148
151
  clockRegistered = true;
149
152
  memory.kvSet('inject.clock_ctx_registered', '1');
@@ -186,9 +189,9 @@ export function renderMemorySection(assembly, gate, memory, settings) {
186
189
  // 注入面出口 ②(最终侧,2026-09-18):时间锚不经快照,未来新增的 part 也只走这里,
187
190
  // 故在最外层再中和一次(幂等)。①+② 合计覆盖全部注入文本。
188
191
  const withAnchor = (text) => {
189
- const body = neutralizeMustache(text || '');
192
+ const body = wellFormed(neutralizeMustache(text || ''));
190
193
  if (!anchor) return body;
191
- return (body ? body + '\n\n' : '') + neutralizeMustache(anchor);
194
+ return wellFormed((body ? body + '\n\n' : '') + neutralizeMustache(anchor));
192
195
  };
193
196
  const snap = gate.snapshotOf(sessionId);
194
197
  // §3 独立开关(1.5.2):只管"树改动播报"。关闭时**不推进任何"已示人"水位** ——
package/lib/host/l1.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // heat = α·recency + β·freq + γ·importance (exponential decay, half-life configurable);
3
3
  // mode weights by category (knowledge/daily/feeling); optional keyword pre-match.
4
4
  import { toEpochMs, isoDate } from './util.js';
5
+ import { wellFormed } from './rawlog.js'; // 1.7.1 F4 闭环:L1 行同样进模型上下文,出口统一净化
5
6
  import { userTitleForMode } from './persona.js'; // 1.6-B2:视角框的称呼跟随 userTitle(不硬编码)
6
7
 
7
8
  const DAY_MS = 86_400_000;
@@ -569,7 +570,7 @@ export function firstSentence(text, cap = SUMMARY_CHARS_DEFAULT) {
569
570
  const cut = Math.max(slice.lastIndexOf(','), slice.lastIndexOf(','), slice.lastIndexOf('、'));
570
571
  head = (cut >= Math.floor(limit * 0.6) ? slice.slice(0, cut) : slice) + '…';
571
572
  }
572
- return head;
573
+ return wellFormed(head);
573
574
  }
574
575
 
575
576
  /**
@@ -609,7 +610,7 @@ function lineFor(row, { maxChars = 0, summaryChars = SUMMARY_CHARS_DEFAULT, self
609
610
  // DSH 会话 id 一律以 "session-" 开头,取前 8 位会全都一样 —— 这时取前缀之后的那一段才可区分
610
611
  const cid = String(row.conv_id || '');
611
612
  const short = cid.startsWith('session-') ? cid.slice(8, 16) : cid.slice(0, 8);
612
- return `- ${tags} ${date ? `(${date})` : ''}${out ? ' ' + out : ''} (来源: ${srcName}/${short})`;
613
+ return wellFormed(`- ${tags} ${date ? `(${date})` : ''}${out ? ' ' + out : ''} (来源: ${srcName}/${short})`);
613
614
  }
614
615
 
615
616
  /** 1.6 步 5:一条**深层库条目**渲染成一行(取代 `lineFor` 的概述两段式 `标题 —— 摘要首句`)。
@@ -625,7 +626,7 @@ function deepLine(row, maxChars = 0) {
625
626
  // 预算裁剪是"装不下就停"(见下面的 for),一条超长条目会把排在它后面的全卡死。
626
627
  const cap = Number(maxChars) > 0 ? Math.max(60, Number(maxChars)) : 0;
627
628
  if (cap && text.length > cap) text = text.slice(0, cap - 1) + '…';
628
- return kind ? `- [${kind}] ${text}` : `- ${text}`;
629
+ return wellFormed(kind ? `- [${kind}] ${text}` : `- ${text}`);
629
630
  }
630
631
 
631
632
  // L1 视角框(1.6 · 第一批 B)—— 修「代词混乱」BUG:落点一处、覆盖全部历史条目、零动库。