dsh-instruction-memory 1.4.1 → 1.4.3

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
@@ -7,6 +7,107 @@
7
7
  > 1.0.13 起由 CI 经 OIDC Trusted Publishing 发布(首个带 provenance 的版本)。
8
8
  > 这些历史 tag **不应回填**——npm 上的版本号已被占用。
9
9
 
10
+ ## [1.4.3] — 2026-10-27
11
+
12
+ 技术方案评审团(3 名专家独立评审 + 逐条复现)列出的缺陷**全部修复**,并补上对应的回归断言。
13
+ 其中两条是**数据安全级**的:都会让用户以为"已保存"而实际丢数据。
14
+
15
+ ### 修复(数据安全)
16
+
17
+ - **跨进程静默覆盖(P0)**:两个 DSH 进程共享同一 `$DSH_HOME` 时,后保存的进程会把**自己那份
18
+ 过期内存**整份写回磁盘,另一个窗口刚保存的条目静默消失,调用方还收到"已保存"。原因是写盘时
19
+ 只把 `rev` 递增到 `max(自己, 磁盘)+1`(看起来单调,内容却是旧的)。现在写盘前先读磁盘 `rev`,
20
+ 发现磁盘更新即**拒绝写入**并提示"另一个 DSH 进程刚保存过,请重试"。
21
+ - **`.bak` 被不可用的主文件覆盖(P0)**:备份保护只判断"主文件字节数不为 0",于是一份**非空但
22
+ 解析不了**的 `memory.json`(截断、手工改坏、空白)会把唯一完好的 `.bak` 覆盖掉——恰好在最需要
23
+ 备份的时刻销毁它。现在只有**解析得出合法存储**(或主文件缺失/为空)才覆盖 `.bak`。
24
+
25
+ ### 修复(预算与注入)
26
+
27
+ - **用户形象绕过注入字数上限(P1)**:形象段此前完全独立于预算,十几个填满的形象能在指令预算
28
+ 还没被考虑之前注入约 5 万字符。现在形象与指令**共用同一个上限**:形象优先占用,指令拿余量,
29
+ 超出部分按顺序截断并说明;形象自身仍有 4000 字符硬上限。`text.length ≤ budget` 与
30
+ `profileChars ≤ min(4000, budget)` 在 3000 组随机存储形状上零越界。
31
+ - **用量条只计指令、不计形象(P1)**:宿主按 `entryChars + profileChars` 裁剪,面板却只显示
32
+ `entryChars`,于是宿主已经在截断、面板仍显示"很空"。现在两者都计入(老宿主缺 `entryChars`
33
+ 时不重复累加)。
34
+ - **代理对截断(P2)**:按 UTF-16 长度截断会切在代理对中间,产生半个 emoji/CJK 扩展 B 字符。
35
+ 新增 `cutText()`,截断时若末位是高代理项则少切一位;`cleanText()` 与所有截断点统一走它。
36
+
37
+ ### 修复(请求门禁)
38
+
39
+ - **端口不参与 `Origin` 比对(P1)**:`127.0.0.1:1` 这类同主机不同端口的页面被当成同源。
40
+ 现在要求 scheme + 主机名 + 端口一致(默认端口 80/443 归一为无端口)。
41
+ - **`same-site` 被放行(P1)**:回环绑定下同站判定不看端口,等于放行任意本地端口上的页面改写
42
+ 长期指令。现在回环(受保护)时 `cross-site` 与 `same-site` 都拒绝;非回环绑定只拒绝
43
+ `cross-site`(反向代理可能合法改写端口)。
44
+ - **FQDN 尾点绕过(P2)**:`localhost.` 与 `localhost` 被当作两个主机名。`hostnameOf()` 统一
45
+ 去尾点。
46
+ - **重复 `Host` 头(P2)**:多个 `Host` 头取第一个即可被前置代理注入绕过,改为直接拒绝。
47
+ - **`content-type` 用子串匹配(P2)**:`text/plain; x=application/json` 这类可绕过,改为精确
48
+ 解析 media type 并拒绝参数夹带。
49
+ - **SSE 连接无上限(P2)**:订阅数不设限可被本机进程耗尽。新增连接上限,超出即以 503 拒绝。
50
+
51
+ ### 修复(其他)
52
+
53
+ - **`{enabled:true, disabledUntil:0}` 被拒(P2)**:两者都表示"未暂停",却被当成矛盾组合拒绝。
54
+ 现在只有**真实暂停**(未来时刻)与总开关同传才算矛盾。
55
+ - **`install.mjs --remove` 半卸载(P2)**:链接路径是真实目录(pnpm 物化的副本)时拒绝删除,
56
+ 但拒绝之后仍然把 `dependencies` 与 `dsh.profile.bundles` 里的条目删掉了——插件还在磁盘上,
57
+ profile 却报告它不存在。现在拒绝即**原样退出**,不碰 `package.json`。
58
+ - **`install.mjs` 卸载时 `unlinkSync` 抛错(P2)**:链接被占用时抛出未捕获异常,看起来像崩溃。
59
+ 现在捕获并提示"关闭 DSH 后重试",退出码非 0。
60
+ - **版本历史去重键含 `rev`(P2)**:同一份内容在不同 `rev` 下被重复记为两份。去重键改为内容哈希。
61
+ - **集合复制的命名(P2)**:派生集合的名字没做去重,可能出现两个同名集合。
62
+ - **临时文件与 0600 权限(P2)**:临时文件改为 `0600` 创建,避免短暂以默认权限暴露存储内容。
63
+
64
+ ### 测试
65
+
66
+ - 新增 12 项 `install.mjs` 断言(真实子进程跑安装/卸载/幂等/拒绝半卸载)。
67
+ - 新增 4 项跨进程与备份保护断言(真实双 handler 实例 + 三种不可用主文件)。
68
+ - 新增 4 项静音断言与 2 项用量条断言;`npm test` 全套通过(`lint` 在本机沙箱内因禁止管道子进程
69
+ 而误报,改用逐文件 `node --check` 等价校验)。
70
+
71
+ ## [1.4.2] — 2026-10-24
72
+
73
+ 适配性复核(宿主 `0.2.1-rc.3`,桌面端 + 网页端)。**没有任何运行时代码改动**——这一版把复核证据
74
+ 固化进仓库,并补上了一条此前一直缺的**直接**证据。
75
+
76
+ ### 新增证据
77
+
78
+ - **网页端 rc.3 隔离实测**:独立 `DSH_HOME` + 真实 `dsh web` 宿主(`0.2.1-rc.3`)驱动 21 项 HTTP
79
+ 断言——**21/21 PASS**(CRUD、落盘持久化、SSE 连接帧、`section` 注册/注销、4 项请求门禁、
80
+ IPv6 同源等价与混源拒绝、30 连发稳定性、清理回基线)。冒烟 `state` p50 0.45 ms(空库)/
81
+ 0.77 ms(1 条生效)、p95 ≤ 0.93 ms、SSE 首事件 0.54–0.61 ms。
82
+ 证据:`dsh-memory-verification/out/compat-1016-live-latest-rc3.json`、`…-rc3b.json`。
83
+ - **桌面端 rc.3 活体实测**:`live-host-smoke.mjs --desktop 14582` → `registered=true`、
84
+ `chars=546`、`activeCount=3`、**`pulls=5052`**(真实 Agent 回合真的拉取过 5052 次)、
85
+ p50 0.72 ms / p95 1.11 ms、SSE 首事件 1.2232 ms、`problems=[]`。
86
+ - **首次拿到「客户端模块确实被下发」的直接证据**:`0.2.1-rc.3` 网页宿主的 boot HTML 预加载清单里
87
+ 出现 `dsh-instruction-memory/client.js`,抓取该合并 bundle 返回 `200 text/javascript`
88
+ (2,204,270 字节),正文含本插件 Client 半的源码头注释与「指令记忆」文案。此前 `0.2.0-rc.1`
89
+ 只能用"合成器硬校验 + 契约包逐字节未变"间接推断(见 README ⁽*⁾),现在这一项对 rc.3
90
+ 是直接观测。
91
+
92
+ ### 兼容面判定
93
+
94
+ - **`0.2.1-rc.2` → `0.2.1-rc.3` 只是重新构建**:与 npm 上 `rc.2` 发布物逐文件 SHA-256 比对——
95
+ `dsh-system-prompt` / `dsh-client-ui-renderer` / `dsh-client-ui-settings` / `dsh-host-webserver` /
96
+ `dsh-app-boot` **仅 `package.json` 变**(版本号与 peer 指向自身 rc.3);`dsh-client-modules` 的
97
+ 1001 行里只有 `const commit` 一行变;`dsh-agent-loop` 自身代码未变(仅把 `zod` 打进包内)。
98
+ ⇒ 对插件可见的契约面是重建,不是行为变化。
99
+
100
+ ### 仍未验证
101
+
102
+ - 设置页的**人工视觉确认**:桌面端与网页端都还没有"用眼睛看过一次"(打开 设置 → 指令记忆)。
103
+ - 隔离网页宿主的 `pulls` 为 0(没跑过真实 Agent 回合);真实回合的拉取计数只在桌面端有 5052 次观测。
104
+
105
+ ### 已知环境坑(不是缺陷)
106
+
107
+ `npm test` 第一步 `lint.mjs` 在 DSH 的 Windows 沙箱下会误报 `13 FAILED TO PARSE`:它用
108
+ `spawnSync(..., { encoding: 'utf8' })` 捕获子进程输出,而该沙箱不允许管道 stdio(EPERM),
109
+ `res.status` 变成 `null`。等价手工核验(逐个 `node --check`)13/13 全部通过,CI 亦不受影响。
110
+
10
111
  ## [1.4.1] — 2026-10-22
11
112
 
12
113
  运行期缺陷(桌面端 / 网页版审计)与安全审查(对 1.4.0 源码)发现的问题修复,
@@ -122,7 +223,8 @@
122
223
  - 本轮新增:`smoke-test.mjs` 覆盖相对 `$DSH_HOME` 的绝对化与 `planHistoryPrune()` 的
123
224
  份数/字节/保底规则;`contract-test.mjs` 断言五个逐字段上限随快照发布且与代码一致,
124
225
  并验证 `?lite=1` 的连接帧与变更帧都是紧凑 rev 标记、普通流仍收完整快照;
125
- `ui-test.mjs` 断言每个字段标签都指向真实控件、校验错误带 `role="alert"`。
226
+ `ui-test.mjs` 断言每个字段标签都指向真实控件、校验错误带 `role="alert"`,并以
227
+ EventSource 桩确认客户端确实以 `?lite=1` 订阅、收到更新的 rev 时会回拉 `state`。
126
228
 
127
229
  ## [1.4.0] — 2026-10-18
128
230
 
package/README.md CHANGED
@@ -77,8 +77,11 @@ dsh plugin --profile web remove dsh-instruction-memory
77
77
  (多个会作为同一画像的不同侧面综合)。启用后,形象段会以「## 关于用户(用户形象)」为抬头注入在
78
78
  指令块**之前**,抬头明确把它限定为"仅供调整回答侧重":AI 应在**保证回答正确、完整、客观且扣题**的
79
79
  前提下据此调整解释深浅、详略、举例与建议方向,**不得为了迎合而改变事实、省略风险提示或偏离问题本身**,
80
- 无关时直接忽略。形象不是让 AI 扮演的角色。形象段独立于指令字数上限,始终完整注入、不做预算裁剪;
81
- 页面会显示已启用形象的合计字符数,便于你权衡提示词体积。总开关关闭时形象与指令一起停用。
80
+ 无关时直接忽略。形象不是让 AI 扮演的角色。形象段与指令**共用同一个注入字数上限**:
81
+ 形象优先占用,指令只拿剩下的余量(早期版本让形象完全绕过预算,十几个填满的形象能在指令预算还没被
82
+ 考虑之前就塞进约 5 万字符)。形象自身另有 4000 字符的硬上限,超出部分按顺序截断并说明。
83
+ 页面上的用量条**同时计入**形象与指令,所以它显示的比例就是宿主真正用来裁剪的那个数字。
84
+ 总开关关闭时形象与指令一起停用。
82
85
 
83
86
  总开关关闭时,该提示词段落(指令与用户形象)**整个注销**,系统提示词与未安装本插件时逐字节一致;
84
87
  总开关开启但没有任何生效指令、也没有启用形象时,段落同样不出现。
@@ -131,6 +134,10 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。相对的 `$DS
131
134
  > 记忆看起来凭空消失,而真实数据其实好好躺在 `.bak` 里。改为复制后主文件在原子替换成功
132
135
  > 前始终待在原位;同时启动时若发现 `memory.json` 缺失而 `.bak` 可用,会**从备份恢复并写回**
133
136
  > 主路径,而不是从零开始。
137
+ >
138
+ > 覆盖 `.bak` 之前会先确认**主文件确实可用**:只有解析得出合法存储(或主文件为空/缺失)才覆盖。
139
+ > 早期版本只判断"字节数不为 0",于是一份被截断的 `memory.json`(非空但解析不了)会把唯一完好的
140
+ > `.bak` 覆盖掉——恰好在最需要它的时刻销毁了它。现在"非空但不可解析"的主文件**不会**动 `.bak`。
134
141
 
135
142
  ### 多实例与多窗口(并发保护)
136
143
 
@@ -144,6 +151,11 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。相对的 `$DS
144
151
  请求都带上它看到的 `rev`(`baseRev`);若与服务端当前 `rev` 不一致(另一个窗口/进程刚
145
152
  提交过),服务端拒绝本次修改并返回**最新快照**,面板自动刷新并提示重试——防止旧窗口
146
153
  把别的窗口刚删除的条目"复活"回去。不带 `baseRev` 的旧客户端保持原有行为。
154
+ - **内存与磁盘的修订号比对**:写盘前先读一次磁盘上的 `rev`。若磁盘已经被另一个进程推到更高的
155
+ `rev`,说明本进程内存里的那份快照已经过期,此时**拒绝写入并提示"另一个 DSH 进程刚保存过"**,
156
+ 而不是继续写。早期版本只把 `rev` 递增到 `max(自己, 磁盘)+1`,看起来修订号单调,实际上是把
157
+ 过期内存整份盖回磁盘——另一个窗口刚存的东西会**静默消失**,调用方还收到"已保存"。
158
+ `baseRev` 是"同一进程内"的乐观锁,这一条补的是"跨进程"的那一半。
147
159
 
148
160
  ### 外部改动自动刷新(SSE)
149
161
 
@@ -151,13 +163,17 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。相对的 `$DS
151
163
  `Accept: text/event-stream`),存储一旦在面板之外发生变化,已打开的面板**自动更新**,
152
164
  无需手动「重新载入」:
153
165
 
154
- - **触发来源**:其他窗口经同一宿主保存(写盘成功即向所有订阅者推送快照);`memory.json`
166
+ - **触发来源**:其他窗口经同一宿主保存(写盘成功即向所有订阅者推送变更);`memory.json`
155
167
  被外部进程或手工改写(宿主以 `fs.watch` 监听**存储目录**——原子替换在 Windows 上会
156
168
  使文件级监听失效——去抖 250ms 后经与写盘相同的队列重读,内容确有变化才重载并推送;
157
169
  自己的写入盘后磁盘与内存一致,天然被同一比较抑制,不会回声)。
158
- - **回声抑制**:发起修改的窗口也会收到自己那次写入的推送帧,但帧内容与它已吸收的响应
159
- 快照逐字节相同,客户端按内容比对直接丢弃——否则刚出现的「已保存」提示会被空消息的
160
- 推送帧瞬间冲掉。
170
+ - **推送的是变更,不是全量**:设置页以 `?lite=1` 订阅,宿主只推 `{ rev, lite: true }`
171
+ (约 40 字节),面板发现 rev 比自己渲染的更新时才回拉一次 `state`。满配存储的一份快照
172
+ 约 1.24 M 字符(中文约 3.4 MB),逐次全量推送会把这些字节乘以打开的窗口数。不带该参数
173
+ 的旧客户端仍收到完整快照,两代客户端与两代宿主可任意组合。
174
+ - **回声抑制**:发起修改的窗口也会收到自己那次写入的推送,但帧里的 rev 与它已吸收的响应
175
+ 快照相同,客户端按 rev 比对直接丢弃(旧客户端的完整帧则按内容逐字节比对)——否则刚出现
176
+ 的「已保存」提示会被空消息的推送帧瞬间冲掉。
161
177
  - **降级而非报错**:宿主不支持或连接中断时 `EventSource` 按 `retry: 5000` 自动重连,
162
178
  重连失败期间面板照常可用(「重新载入」仍是兜底),不会弹出错误。
163
179
  - 编辑器中未保存的草稿**不受推送影响**;保存时按当前 `rev` 提交,外部若已抢先修改
@@ -529,13 +545,15 @@ DNS 重绑定恰好推翻了它**:`evil.com` 解析到 `127.0.0.1` 之后,
529
545
 
530
546
  - 绑定到**任何非回环地址**(`--host 0.0.0.0`、`::` 或具体 IP,即主动对外发布)时 Host 校验**自动关闭**——那种情况下无法预知合法 Host,强行限制只会打断所有正常客户端;`Origin` 校验仍然生效。
531
547
  - 无 `Host` 的 HTTP/1.1 请求由 Node 的 HTTP 解析器先以 400 拒掉,根本到不了路由。
532
- - 端口不参与 `Origin` 比对(反向代理可能改写端口),主机名才是重绑定攻击的对象。
548
+ - **端口参与 `Origin` 比对**(早期版本忽略端口,于是 `127.0.0.1:1` 这种同主机不同端口的页面也算同源):比对前先把默认端口归一(`http:80` / `https:443` 视同未写端口),其余情况要求 scheme、主机名、端口三者一致。`Host` 末尾的 FQDN 点(`localhost.`)会被忽略,避免同一台机器的两种写法被当成跨源。
533
549
 
534
550
  此外路由还读取 **`Sec-Fetch-Site`**(Fetch Metadata):现代浏览器给每个请求都打上它,
535
551
  即使某些技巧(如 `no-cors` fetch)不带 `Origin`,`evil.com` 发来的请求仍会被标记为
536
- `cross-site`——命中即 **403**。仅拒绝 `cross-site`;`same-site` 必须放行(同站判定不看端口,
537
- GUI 与路由本就同主机不同端口),`none`(用户直接导航)放行;curl、Node 与 2020 年前的
538
- 老浏览器根本不发这个头,也不受影响(它们由 Host 与 content-type 校验兜底)。
552
+ `cross-site`——命中即 **403**。在回环绑定(受保护)时 **`cross-site` 与 `same-site` 都拒绝**:
553
+ "同站"判定不看端口,而回环上的任何其他端口都是别人跑的本地服务,放行 `same-site` 等于让任意
554
+ 本地页面改写你的长期指令。`none`(用户直接导航)放行;curl、Node 与 2020 年前的老浏览器根本
555
+ 不发这个头,也不受影响(它们由 Host 与 content-type 校验兜底)。非回环绑定(主动对外发布)时
556
+ 只拒绝 `cross-site`,因为反向代理可能合法地改写端口。
539
557
 
540
558
  ### 经反向代理对外发布(自行承担风险)
541
559
 
@@ -582,7 +600,7 @@ GUI 与路由本就同主机不同端口),`none`(用户直接导航)放
582
600
 
583
601
  | 等级 | 证明什么 | 覆盖 |
584
602
  | --- | --- | --- |
585
- | ① **真实启动** | 在该宿主版本上真的把 DSH 跑起来:插件装载、注入真的进入**组装后的提示**、插件 HTTP 路由可应答、客户端设置页模块被下发(Windows / Node 22) | `0.1.3-alpha.2` · `0.1.5-alpha.1` · `0.1.5-alpha.2` · `0.1.5-rc.1` · `0.1.5-rc.2` · `0.1.5-rc.3` · **`0.2.0-rc.1`** ⁽*⁾ · **`0.2.0-rc.2`** ⁽**⁾ · `0.2.1-alpha.2` ⁽***⁾ · **`0.2.1-alpha.3`** ⁽***⁾ · **`0.2.1-alpha.4`** ⁽***⁾ |
603
+ | ① **真实启动** | 在该宿主版本上真的把 DSH 跑起来:插件装载、注入真的进入**组装后的提示**、插件 HTTP 路由可应答、客户端设置页模块被下发(Windows / Node 22) | `0.1.3-alpha.2` · `0.1.5-alpha.1` · `0.1.5-alpha.2` · `0.1.5-rc.1` · `0.1.5-rc.2` · `0.1.5-rc.3` · **`0.2.0-rc.1`** ⁽*⁾ · **`0.2.0-rc.2`** ⁽**⁾ · `0.2.1-alpha.2` ⁽***⁾ · **`0.2.1-alpha.3`** ⁽***⁾ · **`0.2.1-alpha.4`** ⁽***⁾ · **`0.2.1-rc.2`** · **`0.2.1-rc.3`** ⁽⁴⁾ |
586
604
  | ② **契约核验** | 抓该版本**真实发布的源码**,逐个核验本插件实际用到的每个符号:`systemPrompt.section()`、`assemble()` 是否求值**函数型** `text`、`FILE_REFERENCE=900` / `TOOL_BASH=1000` 仍在(决定 order 950 是否空档)、重名注册是否抛错、`webServer.register()` / `get host()`、`ctx.inject()` / `ctx.effect()`、`dsh.client.inject` 两包在该版本是否存在且声明了 `dsh.client` | 声明范围接住的**全部 18 个已发布版本**(`0.1.3-alpha.2` → `0.2.1-alpha.4`) |
587
605
  | ③ **真实执行** | 把插件**真的挂载**进该版本的 `@deepseek-ai/cordis`,跑装载 / 路由注册 / 卸载共 9 项断言 | cordis `4.0.2` 与 `4.0.4`(各 9/9) |
588
606
 
@@ -592,6 +610,8 @@ GUI 与路由本就同主机不同端口),`none`(用户直接导航)放
592
610
 
593
611
  ⁽***⁾ **`0.2.1-alpha.2`(2026-10-10)与 `0.2.1-alpha.3` / `0.2.1-alpha.4`(2026-10-16)的①档**:隔离安装该版本宿主树 + 独立 `DSH_HOME` 装入 npm 正式发布物,真实启动 web 宿主后以 **21 项 HTTP 断言**驱动(CRUD、落盘持久化、SSE 连接帧、`section` 注册/注销、4 项请求门禁、IPv6 同源等价与混源拒绝、30 连发稳定性、清理回基线)——三个版本各 **21/21 PASS**。证据:`dsh-memory-verification/out/compat-1016-live-live-alpha2.json`、`dsh-memory-verification/out/compat-133-live-alpha{3,4}.json`。同样**未做设置页人工视觉确认**。
594
612
 
613
+ ⁽⁴⁾ **`0.2.1-rc.2` / `0.2.1-rc.3` 的①档**(2026-10-24,插件 1.4.2):`rc.3` 上隔离 `DSH_HOME` + 真实 `dsh web` 宿主驱动 **21 项 HTTP 断言 21/21 PASS**,桌面端活体 `pulls=5052`、p50 0.72 ms;并且**首次直接抓到了客户端模块的下发证据**——boot HTML 的预加载清单含 `dsh-instruction-memory/client.js`,该合并 bundle 返回 `200 text/javascript`(2,204,270 字节),正文含 Client 半源码与「指令记忆」文案。因此上一条 ⁽*⁾ 里"无法直接抓到 `window.__DSH_BOOT__`"的限制**已对 rc.3 解除**。`rc.2` 未单独起宿主,靠它与 `rc.3` 的逐文件字节比对(仅 `package.json` 与一行构建号不同)承接结论。证据:`dsh-memory-verification/out/compat-1016-live-latest-rc3.json`、`…-rc3b.json`。仍**未做设置页人工视觉确认**。
614
+
595
615
  此外 `host-range-test.mjs` 的固定矩阵覆盖 `0.1.6-alpha.2`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2` 与 `0.2.1-alpha.1`~`0.2.1-alpha.4`(回归守卫把最新验证过的宿主逐名钉住),并会**读取本机实际安装的宿主版本**做实时断言。
596
616
 
597
617
  **关于 1.0.10 新增的 `ctx.inject`。** 这是新版唯一的硬依赖增量:`inject` 去掉了 `webServer`,设置页路由改为非阻塞的 `ctx.inject(['webServer'], …)` 子 fiber(原因见上)。为此逐个核验了 **cordis 全部 8 个已发布版本**(`4.0.1-rc.1` → `4.0.5-alpha.2`)——**每一个都具备 `ctx.inject`**(其中 4.0.2 / 4.0.4 另做了真实挂载执行,4.0.5-alpha.1 与 4.0.5-alpha.2 为源码符号核验),因此声明范围内不存在因它而失效的宿主版本。
package/lib/client.js CHANGED
@@ -1224,9 +1224,18 @@ window.__ModuleLoader__.load({
1224
1224
  // `entryChars` is the budgeted half. Older hosts publish neither field,
1225
1225
  // and there `chars` is already instructions-only, so the fallback is
1226
1226
  // correct on both.
1227
- const used = typeof inj.entryChars === 'number' && inj.entryChars >= 0
1227
+ const entryUsed = typeof inj.entryChars === 'number' && inj.entryChars >= 0
1228
1228
  ? inj.entryChars
1229
1229
  : (typeof inj.chars === 'number' && inj.chars >= 0 ? inj.chars : 0)
1230
+ // The Host caps `entryChars + profileChars` against the same `budget`, so
1231
+ // measuring `entryChars` alone made the meter understate usage by the
1232
+ // whole persona block and read green while the Host was truncating.
1233
+ // Counted only alongside `entryChars`: without it the fallback above is
1234
+ // already the whole section, and adding profiles again would double-count.
1235
+ const profileUsed = typeof inj.entryChars === 'number' && inj.entryChars >= 0
1236
+ && typeof inj.profileChars === 'number' && inj.profileChars >= 0
1237
+ ? inj.profileChars : 0
1238
+ const used = entryUsed + profileUsed
1230
1239
  const countFlagged = (list, wantTruncated) =>
1231
1240
  Array.isArray(list)
1232
1241
  ? list.filter((item) => item !== null && typeof item === 'object'